From 66c5bb417bdf82ed663213f45a0f4263693b4613 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Tue, 11 Apr 2023 15:52:22 +0100 Subject: [PATCH 01/36] create new package --- packages/sdk/vercel/README.md | 1 + packages/sdk/vercel/jest.config.json | 9 +++++ packages/sdk/vercel/package.json | 48 ++++++++++++++++++++++++ packages/sdk/vercel/tsconfig.eslint.json | 5 +++ packages/sdk/vercel/tsconfig.json | 23 ++++++++++++ packages/sdk/vercel/tsconfig.ref.json | 7 ++++ 6 files changed, 93 insertions(+) create mode 100644 packages/sdk/vercel/README.md create mode 100644 packages/sdk/vercel/jest.config.json create mode 100644 packages/sdk/vercel/package.json create mode 100644 packages/sdk/vercel/tsconfig.eslint.json create mode 100644 packages/sdk/vercel/tsconfig.json create mode 100644 packages/sdk/vercel/tsconfig.ref.json diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md new file mode 100644 index 0000000000..5ebdafafb1 --- /dev/null +++ b/packages/sdk/vercel/README.md @@ -0,0 +1 @@ +# LaunchDarkly Vercel Edge SDK \ No newline at end of file diff --git a/packages/sdk/vercel/jest.config.json b/packages/sdk/vercel/jest.config.json new file mode 100644 index 0000000000..9ce715d06b --- /dev/null +++ b/packages/sdk/vercel/jest.config.json @@ -0,0 +1,9 @@ +{ + "transform": { "^.+\\.ts?$": "ts-jest" }, + "testMatch": ["**/*.test.ts?(x)"], + "testPathIgnorePatterns": ["node_modules", "example", "dist"], + "modulePathIgnorePatterns": ["dist"], + "testEnvironment": "node", + "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], + "collectCoverageFrom": ["src/**/*.ts"] + } \ No newline at end of file diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json new file mode 100644 index 0000000000..02f30b08ac --- /dev/null +++ b/packages/sdk/vercel/package.json @@ -0,0 +1,48 @@ +{ + "name": "@launchdarkly/cloudflare-server-sdk", + "version": "0.0.1", + "description": "LaunchDarkly Server-Side SDK for Vercel Edge", + "packageManager": "yarn@3.4.1", + "keywords": [ + "launchdarkly", + "vercel", + "edge" + ], + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "type": "module", + "files": [ + "/dist" + ], + "scripts": { + "build": "yarn tsc", + "tsw": "yarn tsc --watch", + "start": "yarn tsw", + "lint": "eslint . --ext .ts", + "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", + "test": "jest --ci --runInBand", + "check": "yarn prettier && yarn lint && yarn tsc && yarn test" + }, + "dependencies": { + "@launchdarkly/js-server-sdk-common": "0.2.0", + "crypto-js": "^4.1.1" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "eslint": "^8.37.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "launchdarkly-js-test-helpers": "^2.2.0", + "prettier": "^2.8.7", + "ts-jest": "^29.1.0", + "typedoc": "0.23.26", + "typescript": "^5.0.3" + } + } \ No newline at end of file diff --git a/packages/sdk/vercel/tsconfig.eslint.json b/packages/sdk/vercel/tsconfig.eslint.json new file mode 100644 index 0000000000..b8c7797a06 --- /dev/null +++ b/packages/sdk/vercel/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["/**/*.ts"], + "exclude": ["node_modules"] + } \ No newline at end of file diff --git a/packages/sdk/vercel/tsconfig.json b/packages/sdk/vercel/tsconfig.json new file mode 100644 index 0000000000..572c80dd93 --- /dev/null +++ b/packages/sdk/vercel/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + // Uses "." so it can load package.json. + "rootDir": ".", + "outDir": "dist", + "target": "es2021", + "lib": ["es2021"], + "jsx": "react", + "module": "es2022", + "moduleResolution": "node", + "types": ["@cloudflare/workers-types", "jest", "node"], + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + // "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"] + } \ No newline at end of file diff --git a/packages/sdk/vercel/tsconfig.ref.json b/packages/sdk/vercel/tsconfig.ref.json new file mode 100644 index 0000000000..db8f58a5c3 --- /dev/null +++ b/packages/sdk/vercel/tsconfig.ref.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json", "src/**/testData.json"], + "compilerOptions": { + "composite": true + } +} \ No newline at end of file From 120a6c65148d1bf1a69e677987839c0537775282 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Wed, 12 Apr 2023 14:52:15 +0100 Subject: [PATCH 02/36] inspiration from cloudflare work --- packages/sdk/vercel/jest.config.json | 16 ++-- packages/sdk/vercel/package.json | 95 ++++++++++--------- .../src/createLdClient/LDClientVercel.ts | 23 +++++ .../src/createLdClient/createCallbacks.ts | 20 ++++ .../src/createLdClient/createFeatureStore.ts | 76 +++++++++++++++ .../src/createLdClient/createOptions.ts | 70 ++++++++++++++ packages/sdk/vercel/src/index.ts | 54 +++++++++++ .../src/platform/crypto/cryptoJSHasher.ts | 48 ++++++++++ .../src/platform/crypto/cryptoJSHmac.ts | 44 +++++++++ .../sdk/vercel/src/platform/crypto/index.ts | 23 +++++ .../sdk/vercel/src/platform/crypto/types.ts | 3 + .../sdk/vercel/src/platform/eventSource.ts | 44 +++++++++ packages/sdk/vercel/src/platform/index.ts | 13 +++ packages/sdk/vercel/src/platform/info.ts | 19 ++++ packages/sdk/vercel/src/platform/requests.ts | 19 ++++ packages/sdk/vercel/src/utils/noop.ts | 1 + packages/sdk/vercel/tsconfig.eslint.json | 8 +- packages/sdk/vercel/tsconfig.json | 44 ++++----- packages/sdk/vercel/tsconfig.ref.json | 12 +-- 19 files changed, 545 insertions(+), 87 deletions(-) create mode 100644 packages/sdk/vercel/src/createLdClient/LDClientVercel.ts create mode 100644 packages/sdk/vercel/src/createLdClient/createCallbacks.ts create mode 100644 packages/sdk/vercel/src/createLdClient/createFeatureStore.ts create mode 100644 packages/sdk/vercel/src/createLdClient/createOptions.ts create mode 100644 packages/sdk/vercel/src/index.ts create mode 100644 packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts create mode 100644 packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts create mode 100644 packages/sdk/vercel/src/platform/crypto/index.ts create mode 100644 packages/sdk/vercel/src/platform/crypto/types.ts create mode 100644 packages/sdk/vercel/src/platform/eventSource.ts create mode 100644 packages/sdk/vercel/src/platform/index.ts create mode 100644 packages/sdk/vercel/src/platform/info.ts create mode 100644 packages/sdk/vercel/src/platform/requests.ts create mode 100644 packages/sdk/vercel/src/utils/noop.ts diff --git a/packages/sdk/vercel/jest.config.json b/packages/sdk/vercel/jest.config.json index 9ce715d06b..6174807746 100644 --- a/packages/sdk/vercel/jest.config.json +++ b/packages/sdk/vercel/jest.config.json @@ -1,9 +1,9 @@ { - "transform": { "^.+\\.ts?$": "ts-jest" }, - "testMatch": ["**/*.test.ts?(x)"], - "testPathIgnorePatterns": ["node_modules", "example", "dist"], - "modulePathIgnorePatterns": ["dist"], - "testEnvironment": "node", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "collectCoverageFrom": ["src/**/*.ts"] - } \ No newline at end of file + "transform": { "^.+\\.ts?$": "ts-jest" }, + "testMatch": ["**/*.test.ts?(x)"], + "testPathIgnorePatterns": ["node_modules", "example", "dist"], + "modulePathIgnorePatterns": ["dist"], + "testEnvironment": "node", + "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], + "collectCoverageFrom": ["src/**/*.ts"] +} diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 02f30b08ac..2ce04fb481 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -1,48 +1,49 @@ { - "name": "@launchdarkly/cloudflare-server-sdk", - "version": "0.0.1", - "description": "LaunchDarkly Server-Side SDK for Vercel Edge", - "packageManager": "yarn@3.4.1", - "keywords": [ - "launchdarkly", - "vercel", - "edge" - ], - "main": "./dist/src/index.js", - "types": "./dist/src/index.d.ts", - "type": "module", - "files": [ - "/dist" - ], - "scripts": { - "build": "yarn tsc", - "tsw": "yarn tsc --watch", - "start": "yarn tsw", - "lint": "eslint . --ext .ts", - "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", - "test": "jest --ci --runInBand", - "check": "yarn prettier && yarn lint && yarn tsc && yarn test" - }, - "dependencies": { - "@launchdarkly/js-server-sdk-common": "0.2.0", - "crypto-js": "^4.1.1" - }, - "devDependencies": { - "@types/crypto-js": "^4.1.1", - "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "eslint": "^8.37.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^4.2.1", - "jest": "^29.5.0", - "launchdarkly-js-test-helpers": "^2.2.0", - "prettier": "^2.8.7", - "ts-jest": "^29.1.0", - "typedoc": "0.23.26", - "typescript": "^5.0.3" - } - } \ No newline at end of file + "name": "@launchdarkly/cloudflare-server-sdk", + "version": "0.0.1", + "description": "LaunchDarkly Server-Side SDK for Vercel Edge", + "packageManager": "yarn@3.4.1", + "keywords": [ + "launchdarkly", + "vercel", + "edge" + ], + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "type": "module", + "files": [ + "/dist" + ], + "scripts": { + "build": "yarn tsc", + "tsw": "yarn tsc --watch", + "start": "yarn tsw", + "lint": "eslint . --ext .ts", + "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", + "test": "jest --ci --runInBand", + "check": "yarn prettier && yarn lint && yarn tsc && yarn test" + }, + "dependencies": { + "@launchdarkly/js-server-sdk-common": "0.2.0", + "crypto-js": "^4.1.1" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "@vercel/edge-config": "^0.1.7", + "eslint": "^8.37.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "launchdarkly-js-test-helpers": "^2.2.0", + "prettier": "^2.8.7", + "ts-jest": "^29.1.0", + "typedoc": "0.23.26", + "typescript": "^5.0.3" + } +} diff --git a/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts b/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts new file mode 100644 index 0000000000..b85be05bc2 --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts @@ -0,0 +1,23 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; +import { EventEmitter } from 'node:events'; +import { LDClientImpl, LDOptions } from '@launchdarkly/js-server-sdk-common'; +import VercelPlatform from '../platform'; +import createOptions from './createOptions'; +import createCallbacks from './createCallbacks'; + +export default class LDClientVercel extends LDClientImpl { + emitter: EventEmitter; + + // sdkKey is only used to query the Edge Config, not to initialize with LD servers + constructor(kvNamespace: EdgeConfigClient, sdkKey: string, options: LDOptions = {}) { + const emitter = new EventEmitter(); + + super( + 'n/a', + new VercelPlatform(), + createOptions(kvNamespace, sdkKey, options), + createCallbacks(emitter) + ); + this.emitter = emitter; + } +} diff --git a/packages/sdk/vercel/src/createLdClient/createCallbacks.ts b/packages/sdk/vercel/src/createLdClient/createCallbacks.ts new file mode 100644 index 0000000000..06e3eb0548 --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createCallbacks.ts @@ -0,0 +1,20 @@ +import { EventEmitter } from 'node:events'; +import noop from '../utils/noop'; + +const createCallbacks = (emitter: EventEmitter) => ({ + onError: (err: Error) => { + if (emitter.listenerCount('error')) { + emitter.emit('error', err); + } + }, + onFailed: (err: Error) => { + emitter.emit('failed', err); + }, + onReady: () => { + emitter.emit('ready'); + }, + onUpdate: noop, + hasEventListeners: () => false, +}); + +export default createCallbacks; diff --git a/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts b/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts new file mode 100644 index 0000000000..2d572cda01 --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts @@ -0,0 +1,76 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; +import type { + DataKind, + LDLogger, + LDFeatureStore, + LDFeatureStoreDataStorage, + LDFeatureStoreItem, + LDFeatureStoreKindData, +} from '@launchdarkly/js-server-sdk-common'; +import noop from '../utils/noop'; + +const createFeatureStore = (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) => { + const key = `LD-Env-${sdkKey}`; + const store: LDFeatureStore = { + get( + kind: DataKind, + flagKey: string, + callback: (res: LDFeatureStoreItem | null) => void = noop + ): void { + logger.debug(`Requesting ${flagKey} from ${key}`); + edgeConfig + .get(key) + .then((i) => { + if (i === null) { + logger.error('Feature data not found in Edge Config.'); + } + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + const item = i as LDFeatureStoreItem; + callback(item[kindKey][flagKey]); + }) + .catch((err) => { + logger.error(err); + callback(null); + }); + }, + all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + logger.debug(`Requesting all ${kindKey} data from Edge Config.`); + edgeConfig + .get(key) + .then((i) => { + if (i === null) { + logger.error('Feature data not found in Edge Config.'); + } + const item = i as LDFeatureStoreItem; + callback(item[kindKey]); + }) + .catch((err) => { + logger.error(err); + callback({}); + }); + }, + initialized(callback: (isInitialized: boolean) => void = noop): void { + edgeConfig.get(key).then((item) => { + const result = item !== null; + logger.debug(`Is ${key} initialized? ${result}`); + callback(result); + }); + }, + init(allData: LDFeatureStoreDataStorage, callback: () => void): void { + callback(); + }, + getDescription(): string { + return 'Vercel Edge Config'; + }, + + // unused + close: noop, + delete: noop, + upsert: noop, + }; + + return store; +}; + +export default createFeatureStore; diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.ts b/packages/sdk/vercel/src/createLdClient/createOptions.ts new file mode 100644 index 0000000000..d13d2d07bf --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createOptions.ts @@ -0,0 +1,70 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; +import { BasicLogger, LDLogger, LDOptions, SafeLogger } from '@launchdarkly/js-server-sdk-common'; +// import { version } from '../../package.json'; +import createFeatureStore from './createFeatureStore'; + +type SupportedLDOptions = Pick; +const allowedOptions = ['logger', 'featureStore']; + +const defaults = { + stream: false, + sendEvents: false, + offline: false, + useLdd: true, + allAttributesPrivate: false, + privateAttributes: [], + contextKeysCapacity: 1000, + contextKeysFlushInterval: 300, + diagnosticOptOut: true, + diagnosticRecordingInterval: 900, + wrapperName: 'cloudflare', + wrapperVersion: 'version', +}; + +export const finalizeLogger = ({ logger }: SupportedLDOptions) => { + const fallbackLogger = new BasicLogger({ + level: 'info', + // eslint-disable-next-line no-console + destination: console.error, + }); + + return logger ? new SafeLogger(logger, fallbackLogger) : fallbackLogger; +}; + +export const finalizeFeatureStore = ( + edgeConfig: EdgeConfigClient, + sdkKey: string, + { featureStore }: SupportedLDOptions, + logger: LDLogger +) => featureStore ?? createFeatureStore(edgeConfig, sdkKey, logger); + +const createOptions = ( + edgeConfig: EdgeConfigClient, + sdkKey: string, + options: SupportedLDOptions = { + logger: undefined, + featureStore: undefined, + } +) => { + if (!sdkKey) { + throw new Error('You must configure the client with a client key'); + } + + if (!edgeConfig || typeof edgeConfig !== 'object' || !edgeConfig.get) { + throw new Error('You must configure the client with an Edge Config SDK instance'); + } + + Object.entries(options).forEach(([key]) => { + if (!allowedOptions.includes(key)) { + throw new Error(`Configuration option: ${key} not supported`); + } + }); + + const logger = finalizeLogger(options); + const featureStore = finalizeFeatureStore(edgeConfig, sdkKey, options, logger); + const finalOptions = { ...defaults, ...options, logger, featureStore }; + logger.debug(`Using LD options: ${JSON.stringify(finalOptions)}`); + return finalOptions; +}; + +export default createOptions; diff --git a/packages/sdk/vercel/src/index.ts b/packages/sdk/vercel/src/index.ts new file mode 100644 index 0000000000..51e97608c4 --- /dev/null +++ b/packages/sdk/vercel/src/index.ts @@ -0,0 +1,54 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; +import { + LDClient, + LDFlagsState, + LDFlagsStateOptions, + LDOptions, + LDContext, + LDEvaluationDetail, + LDFlagValue, +} from '@launchdarkly/js-server-sdk-common'; +import createLDClient from './createLDClient'; + +type LDClientSubset = Pick< + LDClient, + 'variation' | 'variationDetail' | 'allFlagsState' | 'waitForInitialization' +>; + +const init = ( + edgeSdk: EdgeConfigClient, + sdkKey: string, + options: LDOptions = {} +): LDClientSubset => { + const client = createLDClient(edgeSdk, sdkKey, options); + return { + variation( + key: string, + context: LDContext, + defaultValue: LDFlagValue, + callback?: (err: any, res: LDFlagValue) => void + ): Promise { + return client.variation(key, context, defaultValue, callback); + }, + variationDetail( + key: string, + context: LDContext, + defaultValue: LDFlagValue, + callback?: (err: any, res: LDEvaluationDetail) => void + ): Promise { + return client.variationDetail(key, context, defaultValue, callback); + }, + allFlagsState( + context: LDContext, + o?: LDFlagsStateOptions, + callback?: (err: Error | null, res: LDFlagsState | null) => void + ): Promise { + return client.allFlagsState(context, o, callback); + }, + waitForInitialization(): Promise { + return client.waitForInitialization(); + }, + }; +}; + +export default init; diff --git a/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts b/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts new file mode 100644 index 0000000000..760e1238f8 --- /dev/null +++ b/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts @@ -0,0 +1,48 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import CryptoJS from 'crypto-js'; +import { Hasher as LDHasher } from '@launchdarkly/js-server-sdk-common'; +import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types'; + +export default class CryptoJSHasher implements LDHasher { + private cryptoJSHasher; + + constructor(algorithm: SupportedHashAlgorithm) { + let algo; + + switch (algorithm) { + case 'sha1': + algo = CryptoJS.algo.SHA1; + break; + case 'sha256': + algo = CryptoJS.algo.SHA256; + break; + default: + throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); + } + + this.cryptoJSHasher = algo.create(); + } + + digest(encoding: SupportedOutputEncoding): string { + const result = this.cryptoJSHasher.finalize(); + + let enc; + switch (encoding) { + case 'base64': + enc = CryptoJS.enc.Base64; + break; + case 'hex': + enc = CryptoJS.enc.Hex; + break; + default: + throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + } + + return result.toString(enc); + } + + update(data: string): this { + this.cryptoJSHasher.update(data); + return this; + } +} diff --git a/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts b/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts new file mode 100644 index 0000000000..e8fb257016 --- /dev/null +++ b/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts @@ -0,0 +1,44 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import CryptoJS from 'crypto-js'; +import { Hmac as LDHmac } from '@launchdarkly/js-server-sdk-common'; +import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types'; + +export default class CryptoJSHmac implements LDHmac { + private CryptoJSHmac; + + constructor(algorithm: SupportedHashAlgorithm, key: string) { + let algo; + + switch (algorithm) { + case 'sha1': + algo = CryptoJS.algo.SHA1; + break; + case 'sha256': + algo = CryptoJS.algo.SHA256; + break; + default: + throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); + } + + this.CryptoJSHmac = CryptoJS.algo.HMAC.create(algo, key); + } + + digest(encoding: SupportedOutputEncoding): string { + const result = this.CryptoJSHmac.finalize(); + + if (encoding === 'base64') { + return result.toString(CryptoJS.enc.Base64); + } + + if (encoding === 'hex') { + return result.toString(CryptoJS.enc.Hex); + } + + throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + } + + update(data: string): this { + this.CryptoJSHmac.update(data); + return this; + } +} diff --git a/packages/sdk/vercel/src/platform/crypto/index.ts b/packages/sdk/vercel/src/platform/crypto/index.ts new file mode 100644 index 0000000000..caf815b670 --- /dev/null +++ b/packages/sdk/vercel/src/platform/crypto/index.ts @@ -0,0 +1,23 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import type { Crypto, Hasher, Hmac } from '@launchdarkly/js-server-sdk-common'; +import CryptoJSHasher from './cryptoJSHasher'; +import CryptoJSHmac from './cryptoJSHmac'; +import { SupportedHashAlgorithm } from './types'; + +/** + * Uses crypto-js as substitute to node:crypto because we do so + * for cloudflare and this way we can DRY up down the line + */ +export default class CloudflareCrypto implements Crypto { + createHash(algorithm: SupportedHashAlgorithm): Hasher { + return new CryptoJSHasher(algorithm); + } + + createHmac(algorithm: SupportedHashAlgorithm, key: string): Hmac { + return new CryptoJSHmac(algorithm, key); + } + + randomUUID(): string { + return crypto.randomUUID(); + } +} diff --git a/packages/sdk/vercel/src/platform/crypto/types.ts b/packages/sdk/vercel/src/platform/crypto/types.ts new file mode 100644 index 0000000000..819fe72584 --- /dev/null +++ b/packages/sdk/vercel/src/platform/crypto/types.ts @@ -0,0 +1,3 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +export type SupportedHashAlgorithm = 'sha1' | 'sha256'; +export type SupportedOutputEncoding = 'base64' | 'hex'; diff --git a/packages/sdk/vercel/src/platform/eventSource.ts b/packages/sdk/vercel/src/platform/eventSource.ts new file mode 100644 index 0000000000..b8967d4922 --- /dev/null +++ b/packages/sdk/vercel/src/platform/eventSource.ts @@ -0,0 +1,44 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import CryptoJS from 'crypto-js'; +import { Hmac as LDHmac } from '@launchdarkly/js-server-sdk-common'; +import { SupportedHashAlgorithm, SupportedOutputEncoding } from './crypto/types'; + +export default class CryptoJSHmac implements LDHmac { + private CryptoJSHmac; + + constructor(algorithm: SupportedHashAlgorithm, key: string) { + let algo; + + switch (algorithm) { + case 'sha1': + algo = CryptoJS.algo.SHA1; + break; + case 'sha256': + algo = CryptoJS.algo.SHA256; + break; + default: + throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); + } + + this.CryptoJSHmac = CryptoJS.algo.HMAC.create(algo, key); + } + + digest(encoding: SupportedOutputEncoding): string { + const result = this.CryptoJSHmac.finalize(); + + if (encoding === 'base64') { + return result.toString(CryptoJS.enc.Base64); + } + + if (encoding === 'hex') { + return result.toString(CryptoJS.enc.Hex); + } + + throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + } + + update(data: string): this { + this.CryptoJSHmac.update(data); + return this; + } +} diff --git a/packages/sdk/vercel/src/platform/index.ts b/packages/sdk/vercel/src/platform/index.ts new file mode 100644 index 0000000000..5837283c25 --- /dev/null +++ b/packages/sdk/vercel/src/platform/index.ts @@ -0,0 +1,13 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import type { Crypto, Info, Platform, Requests } from '@launchdarkly/js-server-sdk-common'; +import CloudflareCrypto from './crypto'; +import CloudflareInfo from './info'; +import CloudflareRequests from './requests'; + +export default class CloudflarePlatform implements Platform { + info: Info = new CloudflareInfo(); + + crypto: Crypto = new CloudflareCrypto(); + + requests: Requests = new CloudflareRequests(); +} diff --git a/packages/sdk/vercel/src/platform/info.ts b/packages/sdk/vercel/src/platform/info.ts new file mode 100644 index 0000000000..c57006ec90 --- /dev/null +++ b/packages/sdk/vercel/src/platform/info.ts @@ -0,0 +1,19 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common'; + +// import packageJson from '../../package.json'; + +export default class CloudflareInfo implements Info { + platformData(): PlatformData { + return { + name: 'Cloudflare worker', + }; + } + + sdkData(): SdkData { + return { + name: 'packageJson.name', + version: 'packageJson.version', + }; + } +} diff --git a/packages/sdk/vercel/src/platform/requests.ts b/packages/sdk/vercel/src/platform/requests.ts new file mode 100644 index 0000000000..9112c15841 --- /dev/null +++ b/packages/sdk/vercel/src/platform/requests.ts @@ -0,0 +1,19 @@ +// TODO: DRY out vercel/cloudflare/shared stuff +import type { + EventSource, + EventSourceInitDict, + Options, + Response, + Requests, +} from '@launchdarkly/js-server-sdk-common'; +import MockEventSource from './eventSource'; + +export default class CloudflareRequests implements Requests { + fetch(url: string, options: Options = {}): Promise { + return fetch(url, options); + } + + createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { + return new MockEventSource(url, eventSourceInitDict); + } +} diff --git a/packages/sdk/vercel/src/utils/noop.ts b/packages/sdk/vercel/src/utils/noop.ts new file mode 100644 index 0000000000..2d1ec23827 --- /dev/null +++ b/packages/sdk/vercel/src/utils/noop.ts @@ -0,0 +1 @@ +export default () => {}; diff --git a/packages/sdk/vercel/tsconfig.eslint.json b/packages/sdk/vercel/tsconfig.eslint.json index b8c7797a06..56c9b38305 100644 --- a/packages/sdk/vercel/tsconfig.eslint.json +++ b/packages/sdk/vercel/tsconfig.eslint.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", - "include": ["/**/*.ts"], - "exclude": ["node_modules"] - } \ No newline at end of file + "extends": "./tsconfig.json", + "include": ["/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/sdk/vercel/tsconfig.json b/packages/sdk/vercel/tsconfig.json index 572c80dd93..e86166ee22 100644 --- a/packages/sdk/vercel/tsconfig.json +++ b/packages/sdk/vercel/tsconfig.json @@ -1,23 +1,23 @@ { - "compilerOptions": { - // Uses "." so it can load package.json. - "rootDir": ".", - "outDir": "dist", - "target": "es2021", - "lib": ["es2021"], - "jsx": "react", - "module": "es2022", - "moduleResolution": "node", - "types": ["@cloudflare/workers-types", "jest", "node"], - "resolveJsonModule": true, - "allowJs": true, - "checkJs": false, - // "noEmit": true, - "isolatedModules": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - }, - "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"] - } \ No newline at end of file + "compilerOptions": { + // Uses "." so it can load package.json. + "rootDir": ".", + "outDir": "dist", + "target": "es2021", + "lib": ["es2021"], + "jsx": "react", + "module": "es2022", + "moduleResolution": "node", + "types": ["jest", "node"], + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + // "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"] +} diff --git a/packages/sdk/vercel/tsconfig.ref.json b/packages/sdk/vercel/tsconfig.ref.json index db8f58a5c3..832c1d8dd7 100644 --- a/packages/sdk/vercel/tsconfig.ref.json +++ b/packages/sdk/vercel/tsconfig.ref.json @@ -1,7 +1,7 @@ { - "extends": "./tsconfig.json", - "include": ["src/**/*", "package.json", "src/**/testData.json"], - "compilerOptions": { - "composite": true - } -} \ No newline at end of file + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json", "src/**/testData.json"], + "compilerOptions": { + "composite": true + } +} From 914fbc833575269e3591ec8752485cd2fbe65f76 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Wed, 12 Apr 2023 15:54:10 +0100 Subject: [PATCH 03/36] working version using local link in dev project --- package.json | 3 +- packages/sdk/vercel/package.json | 8 ++- .../src/createLdClient/createOptions.ts | 2 +- .../sdk/vercel/src/createLdClient/index.ts | 8 +++ .../sdk/vercel/src/platform/crypto/index.ts | 8 ++- .../sdk/vercel/src/platform/eventSource.ts | 57 ++++++++----------- packages/sdk/vercel/src/platform/index.ts | 14 ++--- packages/sdk/vercel/src/platform/info.ts | 4 +- packages/sdk/vercel/src/platform/requests.ts | 5 +- packages/shared/sdk-server/src/api/index.ts | 2 + packages/shared/sdk-server/src/index.ts | 1 + .../sdk-server/src/store/AsyncStoreFacade.ts | 1 - tsconfig.json | 3 + 13 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 packages/sdk/vercel/src/createLdClient/index.ts diff --git a/package.json b/package.json index 56aec9e604..1c883a2de6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "workspaces": [ "packages/shared/common", "packages/shared/sdk-server", - "packages/sdk/server-node" + "packages/sdk/server-node", + "packages/sdk/vercel" ], "private": true, "scripts": { diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 2ce04fb481..02897a3c1d 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -1,5 +1,5 @@ { - "name": "@launchdarkly/cloudflare-server-sdk", + "name": "@launchdarkly/vercel-server-sdk", "version": "0.0.1", "description": "LaunchDarkly Server-Side SDK for Vercel Edge", "packageManager": "yarn@3.4.1", @@ -24,12 +24,14 @@ "check": "yarn prettier && yarn lint && yarn tsc && yarn test" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "0.2.0", - "crypto-js": "^4.1.1" + "@launchdarkly/js-server-sdk-common": "^0.2.0", + "crypto-js": "^4.1.1", + "uuid": "^9.0.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.5.0", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "@vercel/edge-config": "^0.1.7", diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.ts b/packages/sdk/vercel/src/createLdClient/createOptions.ts index d13d2d07bf..ff61471c51 100644 --- a/packages/sdk/vercel/src/createLdClient/createOptions.ts +++ b/packages/sdk/vercel/src/createLdClient/createOptions.ts @@ -17,7 +17,7 @@ const defaults = { contextKeysFlushInterval: 300, diagnosticOptOut: true, diagnosticRecordingInterval: 900, - wrapperName: 'cloudflare', + wrapperName: 'vercel', wrapperVersion: 'version', }; diff --git a/packages/sdk/vercel/src/createLdClient/index.ts b/packages/sdk/vercel/src/createLdClient/index.ts new file mode 100644 index 0000000000..c34623aefb --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/index.ts @@ -0,0 +1,8 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; +import { LDOptions } from '@launchdarkly/js-server-sdk-common'; +import LDClientVercel from './LDClientVercel'; + +const createLdClient = (edgeConfig: EdgeConfigClient, sdkKey: string, options: LDOptions = {}) => + new LDClientVercel(edgeConfig, sdkKey, options); + +export default createLdClient; diff --git a/packages/sdk/vercel/src/platform/crypto/index.ts b/packages/sdk/vercel/src/platform/crypto/index.ts index caf815b670..77522a1048 100644 --- a/packages/sdk/vercel/src/platform/crypto/index.ts +++ b/packages/sdk/vercel/src/platform/crypto/index.ts @@ -1,5 +1,9 @@ +/* eslint-disable class-methods-use-this */ // TODO: DRY out vercel/cloudflare/shared stuff import type { Crypto, Hasher, Hmac } from '@launchdarkly/js-server-sdk-common'; +// TODO: Find another way to get a UUID +// eslint-disable-next-line import/no-extraneous-dependencies +import { v4 as uuidv4 } from 'uuid'; import CryptoJSHasher from './cryptoJSHasher'; import CryptoJSHmac from './cryptoJSHmac'; import { SupportedHashAlgorithm } from './types'; @@ -8,7 +12,7 @@ import { SupportedHashAlgorithm } from './types'; * Uses crypto-js as substitute to node:crypto because we do so * for cloudflare and this way we can DRY up down the line */ -export default class CloudflareCrypto implements Crypto { +export default class VercelCrypto implements Crypto { createHash(algorithm: SupportedHashAlgorithm): Hasher { return new CryptoJSHasher(algorithm); } @@ -18,6 +22,6 @@ export default class CloudflareCrypto implements Crypto { } randomUUID(): string { - return crypto.randomUUID(); + return uuidv4(); } } diff --git a/packages/sdk/vercel/src/platform/eventSource.ts b/packages/sdk/vercel/src/platform/eventSource.ts index b8967d4922..2dd97c05e8 100644 --- a/packages/sdk/vercel/src/platform/eventSource.ts +++ b/packages/sdk/vercel/src/platform/eventSource.ts @@ -1,44 +1,33 @@ // TODO: DRY out vercel/cloudflare/shared stuff -import CryptoJS from 'crypto-js'; -import { Hmac as LDHmac } from '@launchdarkly/js-server-sdk-common'; -import { SupportedHashAlgorithm, SupportedOutputEncoding } from './crypto/types'; - -export default class CryptoJSHmac implements LDHmac { - private CryptoJSHmac; - - constructor(algorithm: SupportedHashAlgorithm, key: string) { - let algo; - - switch (algorithm) { - case 'sha1': - algo = CryptoJS.algo.SHA1; - break; - case 'sha256': - algo = CryptoJS.algo.SHA256; - break; - default: - throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); - } - - this.CryptoJSHmac = CryptoJS.algo.HMAC.create(algo, key); +import type { EventSource, EventSourceInitDict } from '@launchdarkly/js-sdk-common'; + +export default class MockEventSource implements EventSource { + handlers: Record void> = {}; + + closed = false; + + url: string; + + options: EventSourceInitDict; + + constructor(url: string, options: EventSourceInitDict) { + this.url = url; + this.options = options; } - digest(encoding: SupportedOutputEncoding): string { - const result = this.CryptoJSHmac.finalize(); + onclose: (() => void) | undefined; + + onerror: (() => void) | undefined; - if (encoding === 'base64') { - return result.toString(CryptoJS.enc.Base64); - } + onopen: (() => void) | undefined; - if (encoding === 'hex') { - return result.toString(CryptoJS.enc.Hex); - } + onretrying: ((e: { delayMillis: number }) => void) | undefined; - throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + addEventListener(type: string, listener: (event?: { data?: any }) => void): void { + this.handlers[type] = listener; } - update(data: string): this { - this.CryptoJSHmac.update(data); - return this; + close(): void { + this.closed = true; } } diff --git a/packages/sdk/vercel/src/platform/index.ts b/packages/sdk/vercel/src/platform/index.ts index 5837283c25..ba689baf3c 100644 --- a/packages/sdk/vercel/src/platform/index.ts +++ b/packages/sdk/vercel/src/platform/index.ts @@ -1,13 +1,13 @@ // TODO: DRY out vercel/cloudflare/shared stuff import type { Crypto, Info, Platform, Requests } from '@launchdarkly/js-server-sdk-common'; -import CloudflareCrypto from './crypto'; -import CloudflareInfo from './info'; -import CloudflareRequests from './requests'; +import VercelCrypto from './crypto'; +import VercelInfo from './info'; +import VercelRequests from './requests'; -export default class CloudflarePlatform implements Platform { - info: Info = new CloudflareInfo(); +export default class VercelPlatform implements Platform { + info: Info = new VercelInfo(); - crypto: Crypto = new CloudflareCrypto(); + crypto: Crypto = new VercelCrypto(); - requests: Requests = new CloudflareRequests(); + requests: Requests = new VercelRequests(); } diff --git a/packages/sdk/vercel/src/platform/info.ts b/packages/sdk/vercel/src/platform/info.ts index c57006ec90..ed1fda04e2 100644 --- a/packages/sdk/vercel/src/platform/info.ts +++ b/packages/sdk/vercel/src/platform/info.ts @@ -3,10 +3,10 @@ import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-co // import packageJson from '../../package.json'; -export default class CloudflareInfo implements Info { +export default class VercelInfo implements Info { platformData(): PlatformData { return { - name: 'Cloudflare worker', + name: 'Vercel worker', }; } diff --git a/packages/sdk/vercel/src/platform/requests.ts b/packages/sdk/vercel/src/platform/requests.ts index 9112c15841..74ab40401d 100644 --- a/packages/sdk/vercel/src/platform/requests.ts +++ b/packages/sdk/vercel/src/platform/requests.ts @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ // TODO: DRY out vercel/cloudflare/shared stuff import type { EventSource, @@ -8,8 +9,10 @@ import type { } from '@launchdarkly/js-server-sdk-common'; import MockEventSource from './eventSource'; -export default class CloudflareRequests implements Requests { +export default class VercelRequests implements Requests { fetch(url: string, options: Options = {}): Promise { + // Think this should be available to us in Edge Workers/middleware + // @ts-ignore return fetch(url, options); } diff --git a/packages/shared/sdk-server/src/api/index.ts b/packages/shared/sdk-server/src/api/index.ts index 978cea6c6e..ffd30fc3c6 100644 --- a/packages/shared/sdk-server/src/api/index.ts +++ b/packages/shared/sdk-server/src/api/index.ts @@ -1,6 +1,8 @@ export * from './data'; export * from './options'; export * from './LDClient'; +export * from './interfaces/DataKind'; +export * from './subsystems/LDFeatureStore'; export * from './subsystems/LDStreamProcessor'; // These are items that should be less frequently used, and therefore they diff --git a/packages/shared/sdk-server/src/index.ts b/packages/shared/sdk-server/src/index.ts index 1ff11992e8..ae210cbf63 100644 --- a/packages/shared/sdk-server/src/index.ts +++ b/packages/shared/sdk-server/src/index.ts @@ -4,6 +4,7 @@ import BigSegmentStoreStatusProviderImpl from './BigSegmentStatusProviderImpl'; export * as integrations from './integrations'; export * as platform from '@launchdarkly/js-sdk-common'; export * from './api'; +export * from './store'; export * from '@launchdarkly/js-sdk-common'; export { LDClientImpl, BigSegmentStoreStatusProviderImpl }; diff --git a/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts b/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts index ddf483ac43..358a402b7b 100644 --- a/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts +++ b/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts @@ -13,7 +13,6 @@ import promisify from '../async/promisify'; * * This allows for using a store using async/await instead of callbacks. * - * @internal */ export default class AsyncStoreFacade { private store: LDFeatureStore; diff --git a/tsconfig.json b/tsconfig.json index f5ab4970dd..62e79aea16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,9 @@ }, { "path": "./packages/sdk/server-node/tsconfig.ref.json" + }, + { + "path": "./packages/sdk/vercel/tsconfig.ref.json" } ] } From ebc6d26dbac282197055dbfa32f2d931e7069b8b Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Wed, 12 Apr 2023 16:14:33 +0100 Subject: [PATCH 04/36] add missed file --- packages/shared/sdk-server/src/store/index.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/shared/sdk-server/src/store/index.ts diff --git a/packages/shared/sdk-server/src/store/index.ts b/packages/shared/sdk-server/src/store/index.ts new file mode 100644 index 0000000000..ff3b0c3049 --- /dev/null +++ b/packages/shared/sdk-server/src/store/index.ts @@ -0,0 +1,4 @@ +import AsyncStoreFacade from './AsyncStoreFacade'; + +// eslint-disable-next-line import/prefer-default-export +export { AsyncStoreFacade }; From 1b176c0548bf632a7c069dc285452d34c33059f3 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 14:59:30 +0100 Subject: [PATCH 05/36] add tests --- .../createLdClient/createCallbacks.test.ts | 59 ++++++++++ .../createLdClient/createFeatureStore.test.ts | 105 ++++++++++++++++++ .../src/createLdClient/createOptions.test.ts | 10 ++ packages/sdk/vercel/src/utils/mockEdge.ts | 10 ++ packages/sdk/vercel/src/utils/testData.json | 50 +++++++++ 5 files changed, 234 insertions(+) create mode 100644 packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts create mode 100644 packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts create mode 100644 packages/sdk/vercel/src/createLdClient/createOptions.test.ts create mode 100644 packages/sdk/vercel/src/utils/mockEdge.ts create mode 100644 packages/sdk/vercel/src/utils/testData.json diff --git a/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts b/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts new file mode 100644 index 0000000000..aaf5d94088 --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts @@ -0,0 +1,59 @@ +import { EventEmitter } from 'node:events'; +import createCallbacks from './createCallbacks'; +import noop from '../utils/noop'; + +describe('createCallbacks', () => { + let emitter: EventEmitter; + const err = new Error('test error'); + + beforeEach(() => { + emitter = new EventEmitter(); + emitter.emit = jest.fn(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('onError', () => { + emitter.on('error', noop); + + const { onError } = createCallbacks(emitter); + onError(err); + + expect(emitter.emit).toHaveBeenNthCalledWith(1, 'error', err); + }); + + test('onError should not be called', () => { + const { onError } = createCallbacks(emitter); + onError(err); + + expect(emitter.emit).not.toHaveBeenCalled(); + }); + + test('onFailed', () => { + const { onFailed } = createCallbacks(emitter); + onFailed(err); + + expect(emitter.emit).toHaveBeenNthCalledWith(1, 'failed', err); + }); + + test('onReady', () => { + const { onReady } = createCallbacks(emitter); + onReady(); + + expect(emitter.emit).toHaveBeenNthCalledWith(1, 'ready'); + }); + + test('onUpdate should be noop', () => { + const { onUpdate } = createCallbacks(emitter); + + expect(onUpdate.toString()).toEqual(noop.toString()); + }); + + test('hasEventListeners', () => { + const { hasEventListeners } = createCallbacks(emitter); + + expect(hasEventListeners()).toBeFalsy(); + }); +}); diff --git a/packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts b/packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts new file mode 100644 index 0000000000..52e896ba95 --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts @@ -0,0 +1,105 @@ +import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common'; +import createFeatureStore from './createFeatureStore'; + +import mockEdge from '../utils/mockEdge'; +import * as testData from '../utils/testData.json'; + +describe('createFeatureStore', () => { + const sdkKey = 'sdkKey'; + const configKey = `LD-Env-${sdkKey}`; + const mockLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + const mockGet = mockEdge.get as jest.Mock; + let featureStore: LDFeatureStore; + let asyncFeatureStore: AsyncStoreFacade; + + beforeEach(() => { + mockGet.mockImplementation(() => Promise.resolve(testData)); + featureStore = createFeatureStore(mockEdge, sdkKey, mockLogger); + asyncFeatureStore = new AsyncStoreFacade(featureStore); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('get', () => { + test('get flag', async () => { + const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); + + expect(mockGet).toHaveBeenCalledWith(configKey); + expect(flag).toEqual(testData.flags.testFlag1); + }); + + test('invalid flag key', async () => { + const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'invalid'); + + expect(flag).toBeUndefined(); + }); + + test('invalid edge config key', async () => { + mockGet.mockImplementation(() => Promise.resolve(null)); + const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); + + expect(flag).toBeNull(); + }); + }); + + describe('all', () => { + test('all flags', async () => { + const flag = await asyncFeatureStore.all({ namespace: 'features' }); + + expect(mockGet).toHaveBeenCalledWith(configKey); + expect(flag).toEqual(testData.flags); + }); + + test('invalid DataKind', async () => { + const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' }); + + expect(flag).toBeUndefined(); + }); + + test('invalid edge config key', async () => { + mockGet.mockImplementation(() => Promise.resolve(null)); + const flag = await asyncFeatureStore.all({ namespace: 'flags11' }); + + expect(flag).toEqual({}); + }); + }); + + describe('initialized', () => { + test('is initialized', async () => { + const isInitialized = await asyncFeatureStore.initialized(); + + expect(mockGet).toHaveBeenCalledWith(configKey); + expect(isInitialized).toBeTruthy(); + }); + + test('not initialized', async () => { + mockGet.mockImplementation(() => Promise.resolve(null)); + const isInitialized = await asyncFeatureStore.initialized(); + + expect(mockGet).toHaveBeenCalledWith(configKey); + expect(isInitialized).toBeFalsy(); + }); + }); + + describe('init & getDescription', () => { + test('init', (done) => { + const cb = jest.fn(() => { + done(); + }); + featureStore.init(testData, cb); + }); + + test('getDescription', async () => { + const description = featureStore.getDescription?.(); + + expect(description).toEqual('Vercel Edge Config'); + }); + }); +}); diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.test.ts b/packages/sdk/vercel/src/createLdClient/createOptions.test.ts new file mode 100644 index 0000000000..5c82718a2a --- /dev/null +++ b/packages/sdk/vercel/src/createLdClient/createOptions.test.ts @@ -0,0 +1,10 @@ +import createOptions from './createOptions'; +import mockEdge from '../utils/mockEdge'; + +describe('createOptions', () => { + it('throws without SDK key', () => { + expect(() => { + createOptions(mockEdge, ''); + }).toThrowError(/You must configure the client with a client key/); + }); +}); diff --git a/packages/sdk/vercel/src/utils/mockEdge.ts b/packages/sdk/vercel/src/utils/mockEdge.ts new file mode 100644 index 0000000000..1d6d0b3e96 --- /dev/null +++ b/packages/sdk/vercel/src/utils/mockEdge.ts @@ -0,0 +1,10 @@ +import { EdgeConfigClient } from '@vercel/edge-config'; + +const mockEdge: EdgeConfigClient = { + get: jest.fn(), + getAll: jest.fn(), + digest: jest.fn(), + has: jest.fn(), +}; + +export default mockEdge; diff --git a/packages/sdk/vercel/src/utils/testData.json b/packages/sdk/vercel/src/utils/testData.json new file mode 100644 index 0000000000..16838d9b25 --- /dev/null +++ b/packages/sdk/vercel/src/utils/testData.json @@ -0,0 +1,50 @@ +{ + "flags": { + "testFlag1": { + "key": "testFlag1", + "on": false, + "prerequisites": [], + "targets": [], + "rules": [], + "fallthrough": { + "variation": 0 + }, + "offVariation": 1, + "variations": [true, false], + "clientSideAvailability": { + "usingMobileKey": true, + "usingEnvironmentId": true + }, + "clientSide": true, + "salt": "aef830243d6640d0a973be89988e008d", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": null, + "version": 2, + "deleted": false + }, + "testFlag2": { + "key": "testFlag2", + "on": false, + "prerequisites": [], + "targets": [], + "rules": [], + "fallthrough": { + "variation": 0 + }, + "offVariation": 1, + "variations": [true, false], + "clientSideAvailability": { + "usingMobileKey": true, + "usingEnvironmentId": true + }, + "clientSide": true, + "salt": "aef830243d6640d0a973be89988e008d", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": null, + "version": 2, + "deleted": false + } + } + } \ No newline at end of file From 6f0d25e9079b83b12acacb77e20b553693226b66 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 15:03:02 +0100 Subject: [PATCH 06/36] use package.json --- packages/sdk/vercel/src/createLdClient/createOptions.ts | 5 +++-- packages/sdk/vercel/src/platform/info.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.ts b/packages/sdk/vercel/src/createLdClient/createOptions.ts index ff61471c51..42fec1b3e7 100644 --- a/packages/sdk/vercel/src/createLdClient/createOptions.ts +++ b/packages/sdk/vercel/src/createLdClient/createOptions.ts @@ -1,6 +1,6 @@ import { EdgeConfigClient } from '@vercel/edge-config'; import { BasicLogger, LDLogger, LDOptions, SafeLogger } from '@launchdarkly/js-server-sdk-common'; -// import { version } from '../../package.json'; +import { version } from '../../package.json'; import createFeatureStore from './createFeatureStore'; type SupportedLDOptions = Pick; @@ -8,6 +8,7 @@ const allowedOptions = ['logger', 'featureStore']; const defaults = { stream: false, + // TODO: Investigate if we can actually send events sendEvents: false, offline: false, useLdd: true, @@ -18,7 +19,7 @@ const defaults = { diagnosticOptOut: true, diagnosticRecordingInterval: 900, wrapperName: 'vercel', - wrapperVersion: 'version', + wrapperVersion: version, }; export const finalizeLogger = ({ logger }: SupportedLDOptions) => { diff --git a/packages/sdk/vercel/src/platform/info.ts b/packages/sdk/vercel/src/platform/info.ts index ed1fda04e2..3cbd80f199 100644 --- a/packages/sdk/vercel/src/platform/info.ts +++ b/packages/sdk/vercel/src/platform/info.ts @@ -1,7 +1,8 @@ +/* eslint-disable class-methods-use-this */ // TODO: DRY out vercel/cloudflare/shared stuff import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common'; -// import packageJson from '../../package.json'; +import packageJson from '../../package.json'; export default class VercelInfo implements Info { platformData(): PlatformData { @@ -12,8 +13,8 @@ export default class VercelInfo implements Info { sdkData(): SdkData { return { - name: 'packageJson.name', - version: 'packageJson.version', + name: packageJson.name, + version: packageJson.version, }; } } From 30e3f1b9ce69e0be99595d8b9ccda410c3b9211e Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 15:04:17 +0100 Subject: [PATCH 07/36] fix package.json reading --- packages/sdk/vercel/src/createLdClient/createOptions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.ts b/packages/sdk/vercel/src/createLdClient/createOptions.ts index 42fec1b3e7..27d28f5660 100644 --- a/packages/sdk/vercel/src/createLdClient/createOptions.ts +++ b/packages/sdk/vercel/src/createLdClient/createOptions.ts @@ -1,6 +1,6 @@ import { EdgeConfigClient } from '@vercel/edge-config'; import { BasicLogger, LDLogger, LDOptions, SafeLogger } from '@launchdarkly/js-server-sdk-common'; -import { version } from '../../package.json'; +import packageJson from '../../package.json'; import createFeatureStore from './createFeatureStore'; type SupportedLDOptions = Pick; @@ -19,7 +19,7 @@ const defaults = { diagnosticOptOut: true, diagnosticRecordingInterval: 900, wrapperName: 'vercel', - wrapperVersion: version, + wrapperVersion: packageJson.version, }; export const finalizeLogger = ({ logger }: SupportedLDOptions) => { From e6cda5ba7d6c7f80e5a516436a3e8c28682cdb95 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 15:16:01 +0100 Subject: [PATCH 08/36] small stuff --- packages/sdk/vercel/package.json | 4 ++-- packages/sdk/vercel/src/utils/testData.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 02897a3c1d..770ec4b7f3 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/vercel-server-sdk", - "version": "0.0.1", + "version": "0.1.0", "description": "LaunchDarkly Server-Side SDK for Vercel Edge", "packageManager": "yarn@3.4.1", "keywords": [ @@ -15,7 +15,7 @@ "/dist" ], "scripts": { - "build": "yarn tsc", + "build": "yarn tsc --declaration", "tsw": "yarn tsc --watch", "start": "yarn tsw", "lint": "eslint . --ext .ts", diff --git a/packages/sdk/vercel/src/utils/testData.json b/packages/sdk/vercel/src/utils/testData.json index 16838d9b25..e0bbda786d 100644 --- a/packages/sdk/vercel/src/utils/testData.json +++ b/packages/sdk/vercel/src/utils/testData.json @@ -47,4 +47,4 @@ "deleted": false } } - } \ No newline at end of file + } From 7ff9fc649cd6597c78f0ce7f0cc323fc9199479c Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 15:21:20 +0100 Subject: [PATCH 09/36] update readme --- packages/sdk/vercel/README.md | 56 ++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md index 5ebdafafb1..c30e91481e 100644 --- a/packages/sdk/vercel/README.md +++ b/packages/sdk/vercel/README.md @@ -1 +1,55 @@ -# LaunchDarkly Vercel Edge SDK \ No newline at end of file +# LaunchDarkly Vercel Edge SDK + +This library supports using Vercel [Edge Config](https://vercel.com/docs/concepts/edge-network/edge-config) to replace the default in-memory feature store of the [LaunchDarkly Node.js SDK](https://github.com/launchdarkly/vercel-server-sdk). + +For more information, see the [SDK features guide](https://docs.launchdarkly.com/sdk/features/storing-data). + +## Installation + +```shell +npm i @launchdarkly/vercel-server-sdk +``` + +or yarn: + +```shell +yarn add -D @launchdarkly/vercel-server-sdk +``` + +## Quickstart + +Initialize the ldClient with the [Vercel Edge SDK](https://vercel.com/docs/concepts/edge-network/edge-config/edge-config-sdk) and your LaunchDarkly client side sdk key: + +```typescript +import init from '@launchdarkly/vercel-server-sdk' +import { createClient } from '@vercel/edge-config' + +const edgeClient = createClient(process.env.EDGE_CONFIG) +const ldClient = init(edgeClient, 'YOUR CLIENT-SIDE SDK KEY'); +``` + + +To learn more, head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/vercel). + +## Developing this SDK + +```shell +# at js-core repo root +yarn && yarn build && cd packages/sdk/vercel +# run tests +yarn test +``` + +## About LaunchDarkly + +- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. +- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +- Explore LaunchDarkly + - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information + - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides + - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation + - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates \ No newline at end of file From f4b80528f2a4aad7037af1cb1fb32e2537e04824 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Thu, 13 Apr 2023 17:53:30 +0100 Subject: [PATCH 10/36] some pr feedback --- packages/sdk/vercel/src/platform/crypto/index.ts | 8 ++++---- packages/sdk/vercel/src/platform/requests.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/sdk/vercel/src/platform/crypto/index.ts b/packages/sdk/vercel/src/platform/crypto/index.ts index 77522a1048..e22fafbab0 100644 --- a/packages/sdk/vercel/src/platform/crypto/index.ts +++ b/packages/sdk/vercel/src/platform/crypto/index.ts @@ -1,9 +1,6 @@ /* eslint-disable class-methods-use-this */ // TODO: DRY out vercel/cloudflare/shared stuff import type { Crypto, Hasher, Hmac } from '@launchdarkly/js-server-sdk-common'; -// TODO: Find another way to get a UUID -// eslint-disable-next-line import/no-extraneous-dependencies -import { v4 as uuidv4 } from 'uuid'; import CryptoJSHasher from './cryptoJSHasher'; import CryptoJSHmac from './cryptoJSHmac'; import { SupportedHashAlgorithm } from './types'; @@ -22,6 +19,9 @@ export default class VercelCrypto implements Crypto { } randomUUID(): string { - return uuidv4(); + // This is available in Vercel Edge + // TODO: Use tsconfig instead of ts-ignore + // @ts-ignore + return crypto.randomUUID(); } } diff --git a/packages/sdk/vercel/src/platform/requests.ts b/packages/sdk/vercel/src/platform/requests.ts index 74ab40401d..056012334c 100644 --- a/packages/sdk/vercel/src/platform/requests.ts +++ b/packages/sdk/vercel/src/platform/requests.ts @@ -12,6 +12,7 @@ import MockEventSource from './eventSource'; export default class VercelRequests implements Requests { fetch(url: string, options: Options = {}): Promise { // Think this should be available to us in Edge Workers/middleware + // TODO: Use tsconfig instead of ts-ignore // @ts-ignore return fetch(url, options); } From 4153c36e83ce4f009b17d1b55782d580fd899c99 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 11:17:24 +0100 Subject: [PATCH 11/36] pull in main and build changes --- packages/sdk/vercel/package.json | 11 ++++++++--- .../vercel/src/createLdClient/createFeatureStore.ts | 2 +- packages/sdk/vercel/src/index.ts | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 770ec4b7f3..c013803c3d 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -8,14 +8,18 @@ "vercel", "edge" ], - "main": "./dist/src/index.js", - "types": "./dist/src/index.d.ts", "type": "module", + "exports": { + "require": "./dist/cjs/src/index.js", + "import": "./dist/esm/src/index.js" + }, + "main": "./dist/cjs/src/index.js", + "types": "./dist/cjs/src/index.d.ts", "files": [ "/dist" ], "scripts": { - "build": "yarn tsc --declaration", + "build": "../../../scripts/build-package.sh", "tsw": "yarn tsc --watch", "start": "yarn tsw", "lint": "eslint . --ext .ts", @@ -25,6 +29,7 @@ }, "dependencies": { "@launchdarkly/js-server-sdk-common": "^0.2.0", + "@launchdarkly/js-server-sdk-common-edge": "0.0.2", "crypto-js": "^4.1.1", "uuid": "^9.0.0" }, diff --git a/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts b/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts index 2d572cda01..e3e3c74189 100644 --- a/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts +++ b/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts @@ -6,7 +6,7 @@ import type { LDFeatureStoreDataStorage, LDFeatureStoreItem, LDFeatureStoreKindData, -} from '@launchdarkly/js-server-sdk-common'; +} from '@launchdarkly/js-server-sdk-common-edge'; import noop from '../utils/noop'; const createFeatureStore = (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) => { diff --git a/packages/sdk/vercel/src/index.ts b/packages/sdk/vercel/src/index.ts index 51e97608c4..adc2a680ee 100644 --- a/packages/sdk/vercel/src/index.ts +++ b/packages/sdk/vercel/src/index.ts @@ -8,7 +8,7 @@ import { LDEvaluationDetail, LDFlagValue, } from '@launchdarkly/js-server-sdk-common'; -import createLDClient from './createLDClient'; +import createLdClient from './createLdClient'; type LDClientSubset = Pick< LDClient, @@ -20,7 +20,7 @@ const init = ( sdkKey: string, options: LDOptions = {} ): LDClientSubset => { - const client = createLDClient(edgeSdk, sdkKey, options); + const client = createLdClient(edgeSdk, sdkKey, options); return { variation( key: string, From 7ee300949545c644d257962ddfd0c338f78cd647 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 11:31:12 +0100 Subject: [PATCH 12/36] move to using shared edge-common package --- .../createFeatureStore.test.ts | 6 +- .../createFeatureStore.ts | 4 +- .../src/createLdClient/LDClientVercel.ts | 23 ----- .../createLdClient/createCallbacks.test.ts | 59 ------------ .../src/createLdClient/createCallbacks.ts | 20 ---- .../src/createLdClient/createOptions.test.ts | 10 -- .../src/createLdClient/createOptions.ts | 71 -------------- .../sdk/vercel/src/createLdClient/index.ts | 8 -- packages/sdk/vercel/src/createPlatformInfo.ts | 22 +++++ packages/sdk/vercel/src/index.ts | 95 +++++++++---------- .../src/platform/crypto/cryptoJSHasher.ts | 48 ---------- .../src/platform/crypto/cryptoJSHmac.ts | 44 --------- .../sdk/vercel/src/platform/crypto/index.ts | 27 ------ .../sdk/vercel/src/platform/crypto/types.ts | 3 - .../sdk/vercel/src/platform/eventSource.ts | 33 ------- packages/sdk/vercel/src/platform/index.ts | 13 --- packages/sdk/vercel/src/platform/info.ts | 20 ---- packages/sdk/vercel/src/platform/requests.ts | 23 ----- packages/sdk/vercel/src/utils/noop.ts | 1 - 19 files changed, 74 insertions(+), 456 deletions(-) rename packages/sdk/vercel/src/{createLdClient => }/createFeatureStore.test.ts (96%) rename packages/sdk/vercel/src/{createLdClient => }/createFeatureStore.ts (97%) delete mode 100644 packages/sdk/vercel/src/createLdClient/LDClientVercel.ts delete mode 100644 packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts delete mode 100644 packages/sdk/vercel/src/createLdClient/createCallbacks.ts delete mode 100644 packages/sdk/vercel/src/createLdClient/createOptions.test.ts delete mode 100644 packages/sdk/vercel/src/createLdClient/createOptions.ts delete mode 100644 packages/sdk/vercel/src/createLdClient/index.ts create mode 100644 packages/sdk/vercel/src/createPlatformInfo.ts delete mode 100644 packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts delete mode 100644 packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts delete mode 100644 packages/sdk/vercel/src/platform/crypto/index.ts delete mode 100644 packages/sdk/vercel/src/platform/crypto/types.ts delete mode 100644 packages/sdk/vercel/src/platform/eventSource.ts delete mode 100644 packages/sdk/vercel/src/platform/index.ts delete mode 100644 packages/sdk/vercel/src/platform/info.ts delete mode 100644 packages/sdk/vercel/src/platform/requests.ts delete mode 100644 packages/sdk/vercel/src/utils/noop.ts diff --git a/packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts b/packages/sdk/vercel/src/createFeatureStore.test.ts similarity index 96% rename from packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts rename to packages/sdk/vercel/src/createFeatureStore.test.ts index 52e896ba95..6141139fe7 100644 --- a/packages/sdk/vercel/src/createLdClient/createFeatureStore.test.ts +++ b/packages/sdk/vercel/src/createFeatureStore.test.ts @@ -1,8 +1,8 @@ -import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common'; +import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common-edge'; import createFeatureStore from './createFeatureStore'; -import mockEdge from '../utils/mockEdge'; -import * as testData from '../utils/testData.json'; +import mockEdge from './utils/mockEdge'; +import * as testData from './utils/testData.json'; describe('createFeatureStore', () => { const sdkKey = 'sdkKey'; diff --git a/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts b/packages/sdk/vercel/src/createFeatureStore.ts similarity index 97% rename from packages/sdk/vercel/src/createLdClient/createFeatureStore.ts rename to packages/sdk/vercel/src/createFeatureStore.ts index e3e3c74189..477818b133 100644 --- a/packages/sdk/vercel/src/createLdClient/createFeatureStore.ts +++ b/packages/sdk/vercel/src/createFeatureStore.ts @@ -1,13 +1,13 @@ import { EdgeConfigClient } from '@vercel/edge-config'; -import type { +import { DataKind, LDLogger, LDFeatureStore, LDFeatureStoreDataStorage, LDFeatureStoreItem, LDFeatureStoreKindData, + noop } from '@launchdarkly/js-server-sdk-common-edge'; -import noop from '../utils/noop'; const createFeatureStore = (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) => { const key = `LD-Env-${sdkKey}`; diff --git a/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts b/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts deleted file mode 100644 index b85be05bc2..0000000000 --- a/packages/sdk/vercel/src/createLdClient/LDClientVercel.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { EdgeConfigClient } from '@vercel/edge-config'; -import { EventEmitter } from 'node:events'; -import { LDClientImpl, LDOptions } from '@launchdarkly/js-server-sdk-common'; -import VercelPlatform from '../platform'; -import createOptions from './createOptions'; -import createCallbacks from './createCallbacks'; - -export default class LDClientVercel extends LDClientImpl { - emitter: EventEmitter; - - // sdkKey is only used to query the Edge Config, not to initialize with LD servers - constructor(kvNamespace: EdgeConfigClient, sdkKey: string, options: LDOptions = {}) { - const emitter = new EventEmitter(); - - super( - 'n/a', - new VercelPlatform(), - createOptions(kvNamespace, sdkKey, options), - createCallbacks(emitter) - ); - this.emitter = emitter; - } -} diff --git a/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts b/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts deleted file mode 100644 index aaf5d94088..0000000000 --- a/packages/sdk/vercel/src/createLdClient/createCallbacks.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { EventEmitter } from 'node:events'; -import createCallbacks from './createCallbacks'; -import noop from '../utils/noop'; - -describe('createCallbacks', () => { - let emitter: EventEmitter; - const err = new Error('test error'); - - beforeEach(() => { - emitter = new EventEmitter(); - emitter.emit = jest.fn(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - test('onError', () => { - emitter.on('error', noop); - - const { onError } = createCallbacks(emitter); - onError(err); - - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'error', err); - }); - - test('onError should not be called', () => { - const { onError } = createCallbacks(emitter); - onError(err); - - expect(emitter.emit).not.toHaveBeenCalled(); - }); - - test('onFailed', () => { - const { onFailed } = createCallbacks(emitter); - onFailed(err); - - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'failed', err); - }); - - test('onReady', () => { - const { onReady } = createCallbacks(emitter); - onReady(); - - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'ready'); - }); - - test('onUpdate should be noop', () => { - const { onUpdate } = createCallbacks(emitter); - - expect(onUpdate.toString()).toEqual(noop.toString()); - }); - - test('hasEventListeners', () => { - const { hasEventListeners } = createCallbacks(emitter); - - expect(hasEventListeners()).toBeFalsy(); - }); -}); diff --git a/packages/sdk/vercel/src/createLdClient/createCallbacks.ts b/packages/sdk/vercel/src/createLdClient/createCallbacks.ts deleted file mode 100644 index 06e3eb0548..0000000000 --- a/packages/sdk/vercel/src/createLdClient/createCallbacks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EventEmitter } from 'node:events'; -import noop from '../utils/noop'; - -const createCallbacks = (emitter: EventEmitter) => ({ - onError: (err: Error) => { - if (emitter.listenerCount('error')) { - emitter.emit('error', err); - } - }, - onFailed: (err: Error) => { - emitter.emit('failed', err); - }, - onReady: () => { - emitter.emit('ready'); - }, - onUpdate: noop, - hasEventListeners: () => false, -}); - -export default createCallbacks; diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.test.ts b/packages/sdk/vercel/src/createLdClient/createOptions.test.ts deleted file mode 100644 index 5c82718a2a..0000000000 --- a/packages/sdk/vercel/src/createLdClient/createOptions.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import createOptions from './createOptions'; -import mockEdge from '../utils/mockEdge'; - -describe('createOptions', () => { - it('throws without SDK key', () => { - expect(() => { - createOptions(mockEdge, ''); - }).toThrowError(/You must configure the client with a client key/); - }); -}); diff --git a/packages/sdk/vercel/src/createLdClient/createOptions.ts b/packages/sdk/vercel/src/createLdClient/createOptions.ts deleted file mode 100644 index 27d28f5660..0000000000 --- a/packages/sdk/vercel/src/createLdClient/createOptions.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { EdgeConfigClient } from '@vercel/edge-config'; -import { BasicLogger, LDLogger, LDOptions, SafeLogger } from '@launchdarkly/js-server-sdk-common'; -import packageJson from '../../package.json'; -import createFeatureStore from './createFeatureStore'; - -type SupportedLDOptions = Pick; -const allowedOptions = ['logger', 'featureStore']; - -const defaults = { - stream: false, - // TODO: Investigate if we can actually send events - sendEvents: false, - offline: false, - useLdd: true, - allAttributesPrivate: false, - privateAttributes: [], - contextKeysCapacity: 1000, - contextKeysFlushInterval: 300, - diagnosticOptOut: true, - diagnosticRecordingInterval: 900, - wrapperName: 'vercel', - wrapperVersion: packageJson.version, -}; - -export const finalizeLogger = ({ logger }: SupportedLDOptions) => { - const fallbackLogger = new BasicLogger({ - level: 'info', - // eslint-disable-next-line no-console - destination: console.error, - }); - - return logger ? new SafeLogger(logger, fallbackLogger) : fallbackLogger; -}; - -export const finalizeFeatureStore = ( - edgeConfig: EdgeConfigClient, - sdkKey: string, - { featureStore }: SupportedLDOptions, - logger: LDLogger -) => featureStore ?? createFeatureStore(edgeConfig, sdkKey, logger); - -const createOptions = ( - edgeConfig: EdgeConfigClient, - sdkKey: string, - options: SupportedLDOptions = { - logger: undefined, - featureStore: undefined, - } -) => { - if (!sdkKey) { - throw new Error('You must configure the client with a client key'); - } - - if (!edgeConfig || typeof edgeConfig !== 'object' || !edgeConfig.get) { - throw new Error('You must configure the client with an Edge Config SDK instance'); - } - - Object.entries(options).forEach(([key]) => { - if (!allowedOptions.includes(key)) { - throw new Error(`Configuration option: ${key} not supported`); - } - }); - - const logger = finalizeLogger(options); - const featureStore = finalizeFeatureStore(edgeConfig, sdkKey, options, logger); - const finalOptions = { ...defaults, ...options, logger, featureStore }; - logger.debug(`Using LD options: ${JSON.stringify(finalOptions)}`); - return finalOptions; -}; - -export default createOptions; diff --git a/packages/sdk/vercel/src/createLdClient/index.ts b/packages/sdk/vercel/src/createLdClient/index.ts deleted file mode 100644 index c34623aefb..0000000000 --- a/packages/sdk/vercel/src/createLdClient/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EdgeConfigClient } from '@vercel/edge-config'; -import { LDOptions } from '@launchdarkly/js-server-sdk-common'; -import LDClientVercel from './LDClientVercel'; - -const createLdClient = (edgeConfig: EdgeConfigClient, sdkKey: string, options: LDOptions = {}) => - new LDClientVercel(edgeConfig, sdkKey, options); - -export default createLdClient; diff --git a/packages/sdk/vercel/src/createPlatformInfo.ts b/packages/sdk/vercel/src/createPlatformInfo.ts new file mode 100644 index 0000000000..4bcb2880ef --- /dev/null +++ b/packages/sdk/vercel/src/createPlatformInfo.ts @@ -0,0 +1,22 @@ +import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common-edge'; + +import { name, version } from '../package.json'; + +class VercelPlatformInfo implements Info { + platformData(): PlatformData { + return { + name: 'Vercel Edge', + }; + } + + sdkData(): SdkData { + return { + name, + version, + }; + } +} + +const createPlatformInfo = () => new VercelPlatformInfo(); + +export default createPlatformInfo; diff --git a/packages/sdk/vercel/src/index.ts b/packages/sdk/vercel/src/index.ts index adc2a680ee..b65b133a85 100644 --- a/packages/sdk/vercel/src/index.ts +++ b/packages/sdk/vercel/src/index.ts @@ -1,54 +1,53 @@ -import { EdgeConfigClient } from '@vercel/edge-config'; +/** + * This is the API reference for the Vercel LaunchDarkly SDK. + * + * In typical usage, you will call {@link init} once at startup time to obtain an instance of + * {@link LDClient}, which provides access to all of the SDK's functionality. + * + * For more information, see the SDK reference guide. + * + * @packageDocumentation + */ +import type { EdgeConfigClient } from '@vercel/edge-config'; import { + BasicLogger, + init as initEdge, LDClient, - LDFlagsState, - LDFlagsStateOptions, LDOptions, - LDContext, - LDEvaluationDetail, - LDFlagValue, -} from '@launchdarkly/js-server-sdk-common'; -import createLdClient from './createLdClient'; +} from '@launchdarkly/js-server-sdk-common-edge'; +import createFeatureStore from './createFeatureStore'; +import createPlatformInfo from './createPlatformInfo'; -type LDClientSubset = Pick< - LDClient, - 'variation' | 'variationDetail' | 'allFlagsState' | 'waitForInitialization' ->; +export * from '@launchdarkly/js-server-sdk-common-edge'; -const init = ( - edgeSdk: EdgeConfigClient, - sdkKey: string, - options: LDOptions = {} -): LDClientSubset => { - const client = createLdClient(edgeSdk, sdkKey, options); - return { - variation( - key: string, - context: LDContext, - defaultValue: LDFlagValue, - callback?: (err: any, res: LDFlagValue) => void - ): Promise { - return client.variation(key, context, defaultValue, callback); - }, - variationDetail( - key: string, - context: LDContext, - defaultValue: LDFlagValue, - callback?: (err: any, res: LDEvaluationDetail) => void - ): Promise { - return client.variationDetail(key, context, defaultValue, callback); - }, - allFlagsState( - context: LDContext, - o?: LDFlagsStateOptions, - callback?: (err: Error | null, res: LDFlagsState | null) => void - ): Promise { - return client.allFlagsState(context, o, callback); - }, - waitForInitialization(): Promise { - return client.waitForInitialization(); - }, - }; -}; +export type { LDClient }; -export default init; +/** + * Creates an instance of the Vercel LaunchDarkly client. + * + * Applications should instantiate a single instance for the lifetime of the worker. + * The client will begin attempting to connect to the configured Vercel Edge Config as + * soon as it is created. To determine when it is ready to use, call {@link LDClient.waitForInitialization}. + * + * **Important:** Do **not** try to instantiate `LDClient` with its constructor + * (`new LDClient()/new LDClientImpl()/new LDClient()`); the SDK does not currently support + * this. + * + * @param edgeConfig + * The Vercel KV configured for LaunchDarkly. + * @param sdkKey + * The client side SDK key. This is only used to query the edgeConfig above, + * not to connect with LaunchDarkly servers. + * @param options + * Optional configuration settings. The only supported option is logger. + * @return + * The new {@link LDClient} instance. + */ +export const init = (sdkKey: string, edgeConfig: EdgeConfigClient, options: LDOptions = {}) => { + const logger = options.logger ?? BasicLogger.get(); + return initEdge(sdkKey, createPlatformInfo(), { + featureStore: createFeatureStore(edgeConfig, sdkKey, logger), + logger, + ...options, + }); +}; diff --git a/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts b/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts deleted file mode 100644 index 760e1238f8..0000000000 --- a/packages/sdk/vercel/src/platform/crypto/cryptoJSHasher.ts +++ /dev/null @@ -1,48 +0,0 @@ -// TODO: DRY out vercel/cloudflare/shared stuff -import CryptoJS from 'crypto-js'; -import { Hasher as LDHasher } from '@launchdarkly/js-server-sdk-common'; -import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types'; - -export default class CryptoJSHasher implements LDHasher { - private cryptoJSHasher; - - constructor(algorithm: SupportedHashAlgorithm) { - let algo; - - switch (algorithm) { - case 'sha1': - algo = CryptoJS.algo.SHA1; - break; - case 'sha256': - algo = CryptoJS.algo.SHA256; - break; - default: - throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); - } - - this.cryptoJSHasher = algo.create(); - } - - digest(encoding: SupportedOutputEncoding): string { - const result = this.cryptoJSHasher.finalize(); - - let enc; - switch (encoding) { - case 'base64': - enc = CryptoJS.enc.Base64; - break; - case 'hex': - enc = CryptoJS.enc.Hex; - break; - default: - throw new Error('unsupported output encoding. Only base64 and hex are supported.'); - } - - return result.toString(enc); - } - - update(data: string): this { - this.cryptoJSHasher.update(data); - return this; - } -} diff --git a/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts b/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts deleted file mode 100644 index e8fb257016..0000000000 --- a/packages/sdk/vercel/src/platform/crypto/cryptoJSHmac.ts +++ /dev/null @@ -1,44 +0,0 @@ -// TODO: DRY out vercel/cloudflare/shared stuff -import CryptoJS from 'crypto-js'; -import { Hmac as LDHmac } from '@launchdarkly/js-server-sdk-common'; -import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types'; - -export default class CryptoJSHmac implements LDHmac { - private CryptoJSHmac; - - constructor(algorithm: SupportedHashAlgorithm, key: string) { - let algo; - - switch (algorithm) { - case 'sha1': - algo = CryptoJS.algo.SHA1; - break; - case 'sha256': - algo = CryptoJS.algo.SHA256; - break; - default: - throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.'); - } - - this.CryptoJSHmac = CryptoJS.algo.HMAC.create(algo, key); - } - - digest(encoding: SupportedOutputEncoding): string { - const result = this.CryptoJSHmac.finalize(); - - if (encoding === 'base64') { - return result.toString(CryptoJS.enc.Base64); - } - - if (encoding === 'hex') { - return result.toString(CryptoJS.enc.Hex); - } - - throw new Error('unsupported output encoding. Only base64 and hex are supported.'); - } - - update(data: string): this { - this.CryptoJSHmac.update(data); - return this; - } -} diff --git a/packages/sdk/vercel/src/platform/crypto/index.ts b/packages/sdk/vercel/src/platform/crypto/index.ts deleted file mode 100644 index e22fafbab0..0000000000 --- a/packages/sdk/vercel/src/platform/crypto/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable class-methods-use-this */ -// TODO: DRY out vercel/cloudflare/shared stuff -import type { Crypto, Hasher, Hmac } from '@launchdarkly/js-server-sdk-common'; -import CryptoJSHasher from './cryptoJSHasher'; -import CryptoJSHmac from './cryptoJSHmac'; -import { SupportedHashAlgorithm } from './types'; - -/** - * Uses crypto-js as substitute to node:crypto because we do so - * for cloudflare and this way we can DRY up down the line - */ -export default class VercelCrypto implements Crypto { - createHash(algorithm: SupportedHashAlgorithm): Hasher { - return new CryptoJSHasher(algorithm); - } - - createHmac(algorithm: SupportedHashAlgorithm, key: string): Hmac { - return new CryptoJSHmac(algorithm, key); - } - - randomUUID(): string { - // This is available in Vercel Edge - // TODO: Use tsconfig instead of ts-ignore - // @ts-ignore - return crypto.randomUUID(); - } -} diff --git a/packages/sdk/vercel/src/platform/crypto/types.ts b/packages/sdk/vercel/src/platform/crypto/types.ts deleted file mode 100644 index 819fe72584..0000000000 --- a/packages/sdk/vercel/src/platform/crypto/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: DRY out vercel/cloudflare/shared stuff -export type SupportedHashAlgorithm = 'sha1' | 'sha256'; -export type SupportedOutputEncoding = 'base64' | 'hex'; diff --git a/packages/sdk/vercel/src/platform/eventSource.ts b/packages/sdk/vercel/src/platform/eventSource.ts deleted file mode 100644 index 2dd97c05e8..0000000000 --- a/packages/sdk/vercel/src/platform/eventSource.ts +++ /dev/null @@ -1,33 +0,0 @@ -// TODO: DRY out vercel/cloudflare/shared stuff -import type { EventSource, EventSourceInitDict } from '@launchdarkly/js-sdk-common'; - -export default class MockEventSource implements EventSource { - handlers: Record void> = {}; - - closed = false; - - url: string; - - options: EventSourceInitDict; - - constructor(url: string, options: EventSourceInitDict) { - this.url = url; - this.options = options; - } - - onclose: (() => void) | undefined; - - onerror: (() => void) | undefined; - - onopen: (() => void) | undefined; - - onretrying: ((e: { delayMillis: number }) => void) | undefined; - - addEventListener(type: string, listener: (event?: { data?: any }) => void): void { - this.handlers[type] = listener; - } - - close(): void { - this.closed = true; - } -} diff --git a/packages/sdk/vercel/src/platform/index.ts b/packages/sdk/vercel/src/platform/index.ts deleted file mode 100644 index ba689baf3c..0000000000 --- a/packages/sdk/vercel/src/platform/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: DRY out vercel/cloudflare/shared stuff -import type { Crypto, Info, Platform, Requests } from '@launchdarkly/js-server-sdk-common'; -import VercelCrypto from './crypto'; -import VercelInfo from './info'; -import VercelRequests from './requests'; - -export default class VercelPlatform implements Platform { - info: Info = new VercelInfo(); - - crypto: Crypto = new VercelCrypto(); - - requests: Requests = new VercelRequests(); -} diff --git a/packages/sdk/vercel/src/platform/info.ts b/packages/sdk/vercel/src/platform/info.ts deleted file mode 100644 index 3cbd80f199..0000000000 --- a/packages/sdk/vercel/src/platform/info.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable class-methods-use-this */ -// TODO: DRY out vercel/cloudflare/shared stuff -import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common'; - -import packageJson from '../../package.json'; - -export default class VercelInfo implements Info { - platformData(): PlatformData { - return { - name: 'Vercel worker', - }; - } - - sdkData(): SdkData { - return { - name: packageJson.name, - version: packageJson.version, - }; - } -} diff --git a/packages/sdk/vercel/src/platform/requests.ts b/packages/sdk/vercel/src/platform/requests.ts deleted file mode 100644 index 056012334c..0000000000 --- a/packages/sdk/vercel/src/platform/requests.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable class-methods-use-this */ -// TODO: DRY out vercel/cloudflare/shared stuff -import type { - EventSource, - EventSourceInitDict, - Options, - Response, - Requests, -} from '@launchdarkly/js-server-sdk-common'; -import MockEventSource from './eventSource'; - -export default class VercelRequests implements Requests { - fetch(url: string, options: Options = {}): Promise { - // Think this should be available to us in Edge Workers/middleware - // TODO: Use tsconfig instead of ts-ignore - // @ts-ignore - return fetch(url, options); - } - - createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { - return new MockEventSource(url, eventSourceInitDict); - } -} diff --git a/packages/sdk/vercel/src/utils/noop.ts b/packages/sdk/vercel/src/utils/noop.ts deleted file mode 100644 index 2d1ec23827..0000000000 --- a/packages/sdk/vercel/src/utils/noop.ts +++ /dev/null @@ -1 +0,0 @@ -export default () => {}; From 6f65333bfaea68c24600108deee1070853bc32cc Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 11:55:53 +0100 Subject: [PATCH 13/36] change featurestore to class implementation --- packages/sdk/vercel/src/createFeatureStore.ts | 131 +++++++++--------- packages/sdk/vercel/src/index.ts | 4 +- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/packages/sdk/vercel/src/createFeatureStore.ts b/packages/sdk/vercel/src/createFeatureStore.ts index 477818b133..443c382948 100644 --- a/packages/sdk/vercel/src/createFeatureStore.ts +++ b/packages/sdk/vercel/src/createFeatureStore.ts @@ -1,76 +1,77 @@ import { EdgeConfigClient } from '@vercel/edge-config'; -import { +import type { DataKind, LDLogger, LDFeatureStore, LDFeatureStoreDataStorage, LDFeatureStoreItem, - LDFeatureStoreKindData, - noop + LDFeatureStoreKindData } from '@launchdarkly/js-server-sdk-common-edge'; +import { noop } from '@launchdarkly/js-server-sdk-common-edge'; -const createFeatureStore = (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) => { - const key = `LD-Env-${sdkKey}`; - const store: LDFeatureStore = { - get( - kind: DataKind, - flagKey: string, - callback: (res: LDFeatureStoreItem | null) => void = noop - ): void { - logger.debug(`Requesting ${flagKey} from ${key}`); - edgeConfig - .get(key) - .then((i) => { - if (i === null) { - logger.error('Feature data not found in Edge Config.'); - } - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - const item = i as LDFeatureStoreItem; - callback(item[kindKey][flagKey]); - }) - .catch((err) => { - logger.error(err); - callback(null); - }); - }, - all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - logger.debug(`Requesting all ${kindKey} data from Edge Config.`); - edgeConfig - .get(key) - .then((i) => { - if (i === null) { - logger.error('Feature data not found in Edge Config.'); - } - const item = i as LDFeatureStoreItem; - callback(item[kindKey]); - }) - .catch((err) => { - logger.error(err); - callback({}); - }); - }, - initialized(callback: (isInitialized: boolean) => void = noop): void { - edgeConfig.get(key).then((item) => { - const result = item !== null; - logger.debug(`Is ${key} initialized? ${result}`); - callback(result); - }); - }, - init(allData: LDFeatureStoreDataStorage, callback: () => void): void { - callback(); - }, - getDescription(): string { - return 'Vercel Edge Config'; - }, +class VercelFeatureStore implements LDFeatureStore { + private edgeConfig: EdgeConfigClient + private configKey: string + private logger: LDLogger - // unused - close: noop, - delete: noop, - upsert: noop, - }; + constructor (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) { + this.edgeConfig = edgeConfig + this.configKey = `LD-Env-${sdkKey}`; + this.logger = logger + } + + get(kind: DataKind, flagKey: string, callback: (res: LDFeatureStoreItem | null) => void): void { + this.logger.debug(`Requesting ${flagKey} from ${this.configKey}`); + this.edgeConfig + .get(this.configKey) + .then((i) => { + if (i === null) { + this.logger.error('Feature data not found in Edge Config.'); + } + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + const item = i as LDFeatureStoreItem; + callback(item[kindKey][flagKey]); + }) + .catch((err) => { + this.logger.error(err); + callback(null); + }); + } + all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + this.logger.debug(`Requesting all ${kindKey} data from Edge Config.`); + this.edgeConfig + .get(this.configKey) + .then((i) => { + if (i === null) { + this.logger.error('Feature data not found in Edge Config.'); + } + const item = i as LDFeatureStoreItem; + callback(item[kindKey]); + }) + .catch((err) => { + this.logger.error(err); + callback({}); + }); + } + initialized(callback: (isInitialized: boolean) => void = noop): void { + this.edgeConfig.get(this.configKey).then((item) => { + const result = item !== null; + this.logger.debug(`Is ${this.configKey} initialized? ${result}`); + callback(result); + }); + } + init(allData: LDFeatureStoreDataStorage, callback: () => void): void { + callback(); + } + getDescription(): string { + return 'Vercel Edge Config'; + } - return store; -}; + // unused + close = noop + delete = noop + upsert = noop +} -export default createFeatureStore; +export default VercelFeatureStore; diff --git a/packages/sdk/vercel/src/index.ts b/packages/sdk/vercel/src/index.ts index b65b133a85..b1265063b5 100644 --- a/packages/sdk/vercel/src/index.ts +++ b/packages/sdk/vercel/src/index.ts @@ -34,7 +34,7 @@ export type { LDClient }; * this. * * @param edgeConfig - * The Vercel KV configured for LaunchDarkly. + * The Vercel Edge Config client configured for LaunchDarkly. * @param sdkKey * The client side SDK key. This is only used to query the edgeConfig above, * not to connect with LaunchDarkly servers. @@ -46,7 +46,7 @@ export type { LDClient }; export const init = (sdkKey: string, edgeConfig: EdgeConfigClient, options: LDOptions = {}) => { const logger = options.logger ?? BasicLogger.get(); return initEdge(sdkKey, createPlatformInfo(), { - featureStore: createFeatureStore(edgeConfig, sdkKey, logger), + featureStore: new createFeatureStore(edgeConfig, sdkKey, logger), logger, ...options, }); From 9a80e548018c5e64578d8a95c41a7aa2a601019a Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 11:57:07 +0100 Subject: [PATCH 14/36] rename file for clarity --- packages/sdk/vercel/src/index.ts | 4 ++-- ...reateFeatureStore.test.ts => vercelFeatureStore.test.ts} | 6 +++--- .../src/{createFeatureStore.ts => vercelFeatureStore.ts} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/sdk/vercel/src/{createFeatureStore.test.ts => vercelFeatureStore.test.ts} (94%) rename packages/sdk/vercel/src/{createFeatureStore.ts => vercelFeatureStore.ts} (100%) diff --git a/packages/sdk/vercel/src/index.ts b/packages/sdk/vercel/src/index.ts index b1265063b5..54056a6be7 100644 --- a/packages/sdk/vercel/src/index.ts +++ b/packages/sdk/vercel/src/index.ts @@ -15,7 +15,7 @@ import { LDClient, LDOptions, } from '@launchdarkly/js-server-sdk-common-edge'; -import createFeatureStore from './createFeatureStore'; +import VercelFeatureStore from './vercelFeatureStore'; import createPlatformInfo from './createPlatformInfo'; export * from '@launchdarkly/js-server-sdk-common-edge'; @@ -46,7 +46,7 @@ export type { LDClient }; export const init = (sdkKey: string, edgeConfig: EdgeConfigClient, options: LDOptions = {}) => { const logger = options.logger ?? BasicLogger.get(); return initEdge(sdkKey, createPlatformInfo(), { - featureStore: new createFeatureStore(edgeConfig, sdkKey, logger), + featureStore: new VercelFeatureStore(edgeConfig, sdkKey, logger), logger, ...options, }); diff --git a/packages/sdk/vercel/src/createFeatureStore.test.ts b/packages/sdk/vercel/src/vercelFeatureStore.test.ts similarity index 94% rename from packages/sdk/vercel/src/createFeatureStore.test.ts rename to packages/sdk/vercel/src/vercelFeatureStore.test.ts index 6141139fe7..76a5a516de 100644 --- a/packages/sdk/vercel/src/createFeatureStore.test.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.test.ts @@ -1,10 +1,10 @@ import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common-edge'; -import createFeatureStore from './createFeatureStore'; +import VercelFeatureStore from './vercelFeatureStore'; import mockEdge from './utils/mockEdge'; import * as testData from './utils/testData.json'; -describe('createFeatureStore', () => { +describe('VercelFeatureStore', () => { const sdkKey = 'sdkKey'; const configKey = `LD-Env-${sdkKey}`; const mockLogger = { @@ -19,7 +19,7 @@ describe('createFeatureStore', () => { beforeEach(() => { mockGet.mockImplementation(() => Promise.resolve(testData)); - featureStore = createFeatureStore(mockEdge, sdkKey, mockLogger); + featureStore = new VercelFeatureStore(mockEdge, sdkKey, mockLogger); asyncFeatureStore = new AsyncStoreFacade(featureStore); }); diff --git a/packages/sdk/vercel/src/createFeatureStore.ts b/packages/sdk/vercel/src/vercelFeatureStore.ts similarity index 100% rename from packages/sdk/vercel/src/createFeatureStore.ts rename to packages/sdk/vercel/src/vercelFeatureStore.ts From 91e3ebda4b8334fc57230426afc662e563436b54 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 11:58:23 +0100 Subject: [PATCH 15/36] add changelog --- packages/sdk/vercel/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/sdk/vercel/CHANGELOG.md diff --git a/packages/sdk/vercel/CHANGELOG.md b/packages/sdk/vercel/CHANGELOG.md new file mode 100644 index 0000000000..aa2119f903 --- /dev/null +++ b/packages/sdk/vercel/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to the LaunchDarkly SDK for Vercel Edge Config will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). \ No newline at end of file From 9503f1cbecbfd82bf6721b092ac222820fc35230 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:23:14 +0100 Subject: [PATCH 16/36] get tsc to build types --- scripts/build-package.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-package.sh b/scripts/build-package.sh index cebf3a3719..ef9e2374ef 100755 --- a/scripts/build-package.sh +++ b/scripts/build-package.sh @@ -23,8 +23,8 @@ ESM_PACKAGE_JSON=$( jq -n \ --arg type "module" \ '{ name: $name, version: $version, type: $type }' ) -tsc --module commonjs --outDir dist/cjs/ +tsc --module commonjs --declaration --outDir dist/cjs/ echo "$CJS_PACKAGE_JSON" > dist/cjs/package.json -tsc --module es2022 --outDir dist/esm/ +tsc --module es2022 --declaration --outDir dist/esm/ echo "$ESM_PACKAGE_JSON" > dist/esm/package.json From 42338bb0eb578431910f37ad9216d915f066c1eb Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:23:26 +0100 Subject: [PATCH 17/36] add types to export as well --- packages/sdk/vercel/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index c013803c3d..72340b778e 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -11,7 +11,8 @@ "type": "module", "exports": { "require": "./dist/cjs/src/index.js", - "import": "./dist/esm/src/index.js" + "import": "./dist/esm/src/index.js", + "types": "./dist/esm/src/index.d.ts" }, "main": "./dist/cjs/src/index.js", "types": "./dist/cjs/src/index.d.ts", From 0dbaa18229558bb7dc58b5291ba395052546082e Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:29:04 +0100 Subject: [PATCH 18/36] update platforminfo --- packages/sdk/vercel/src/createPlatformInfo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/vercel/src/createPlatformInfo.ts b/packages/sdk/vercel/src/createPlatformInfo.ts index 4bcb2880ef..71d3f51a3a 100644 --- a/packages/sdk/vercel/src/createPlatformInfo.ts +++ b/packages/sdk/vercel/src/createPlatformInfo.ts @@ -1,6 +1,6 @@ import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common-edge'; -import { name, version } from '../package.json'; +import packageJson from '../package.json'; class VercelPlatformInfo implements Info { platformData(): PlatformData { @@ -11,8 +11,8 @@ class VercelPlatformInfo implements Info { sdkData(): SdkData { return { - name, - version, + name: packageJson.name, + version: packageJson.version, }; } } From 4d85d4ab2e4610ec208e74df6113e87ee4f57c02 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:29:14 +0100 Subject: [PATCH 19/36] attempt switch to async --- packages/sdk/vercel/src/vercelFeatureStore.ts | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/packages/sdk/vercel/src/vercelFeatureStore.ts b/packages/sdk/vercel/src/vercelFeatureStore.ts index 443c382948..9bc133ee05 100644 --- a/packages/sdk/vercel/src/vercelFeatureStore.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.ts @@ -20,46 +20,41 @@ class VercelFeatureStore implements LDFeatureStore { this.logger = logger } - get(kind: DataKind, flagKey: string, callback: (res: LDFeatureStoreItem | null) => void): void { + async get(kind: DataKind, flagKey: string, callback: (res: LDFeatureStoreItem | null) => void): Promise { this.logger.debug(`Requesting ${flagKey} from ${this.configKey}`); - this.edgeConfig - .get(this.configKey) - .then((i) => { - if (i === null) { - this.logger.error('Feature data not found in Edge Config.'); - } - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - const item = i as LDFeatureStoreItem; - callback(item[kindKey][flagKey]); - }) - .catch((err) => { - this.logger.error(err); - callback(null); - }); + try { + const config = await this.edgeConfig.get(this.configKey) + if (config === null) { + this.logger.error('Feature data not found in Edge Config.'); + } + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + const item = config as LDFeatureStoreItem; + callback(item[kindKey][flagKey]); + } catch (err) { + this.logger.error(err); + callback(null); + } } - all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { + async all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): Promise { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; this.logger.debug(`Requesting all ${kindKey} data from Edge Config.`); - this.edgeConfig - .get(this.configKey) - .then((i) => { - if (i === null) { - this.logger.error('Feature data not found in Edge Config.'); - } - const item = i as LDFeatureStoreItem; - callback(item[kindKey]); - }) - .catch((err) => { - this.logger.error(err); - callback({}); - }); + try { + const config = await this.edgeConfig.get(this.configKey) + if (config === null) { + this.logger.error('Feature data not found in Edge Config.'); + } + const item = config as LDFeatureStoreItem; + callback(item[kindKey]); + } catch (err) { + this.logger.error(err); + callback({}); + } } - initialized(callback: (isInitialized: boolean) => void = noop): void { - this.edgeConfig.get(this.configKey).then((item) => { - const result = item !== null; - this.logger.debug(`Is ${this.configKey} initialized? ${result}`); - callback(result); - }); + async initialized(callback: (isInitialized: boolean) => void = noop): Promise { + const config = await this.edgeConfig.get(this.configKey) + const result = config !== null; + this.logger.debug(`Is ${this.configKey} initialized? ${result}`); + callback(result); } init(allData: LDFeatureStoreDataStorage, callback: () => void): void { callback(); From 8767f7ce33de449a9c11f7d276400ac8527186b8 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:36:09 +0100 Subject: [PATCH 20/36] github actions changes --- .github/workflows/manual-publish-docs.yml | 1 + .github/workflows/manual-publish.yml | 1 + .github/workflows/release-please.yml | 21 ++++++++++++++++++++ .github/workflows/vercel.yml | 24 +++++++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 .github/workflows/vercel.yml diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml index 49eefc5774..4d13ee2d92 100644 --- a/.github/workflows/manual-publish-docs.yml +++ b/.github/workflows/manual-publish-docs.yml @@ -12,6 +12,7 @@ on: - packages/shared/sdk-server-edge - packages/sdk/cloudflare - packages/sdk/server-node + - packages/sdk/vercel name: Publish Documentation jobs: build-publish: diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index bc1dd418a6..3c6134a5c2 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -13,6 +13,7 @@ on: - packages/shared/sdk-server-edge - packages/sdk/cloudflare - packages/sdk/server-node + - packages/sdk/vercel prerelease: description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.' type: boolean diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b1e40dffb2..000735c7de 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -13,6 +13,7 @@ jobs: package-sdk-server-edge-released: ${{ steps.release.outputs['packages/shared/sdk-server-edge--release_created'] }} package-cloudflare-released: ${{ steps.release.outputs['packages/sdk/cloudflare--release_created'] }} package-server-node-released: ${{ steps.release.outputs['packages/sdk/server-node--release_created'] }} + package-vercel-released: ${{ steps.release.outputs['packages/sdk/vercel--release_created'] }} steps: - uses: google-github-actions/release-please-action@v3 id: release @@ -120,3 +121,23 @@ jobs: with: workspace_path: packages/sdk/server-node aws_assume_role: ${{ vars.AWS_ROLE_ARN }} + + release-vercel: + runs-on: ubuntu-latest + needs: ['release-please'] + permissions: + id-token: write + contents: write + if: ${{ needs.release-please.outputs.package-vercel-released }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + registry-url: 'https://registry.npmjs.org' + - id: release-common + name: Full release of packages/sdk/vercel + uses: ./actions/full-release + with: + workspace_path: packages/sdk/vercel + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml new file mode 100644 index 0000000000..bb16a60e2d --- /dev/null +++ b/.github/workflows/vercel.yml @@ -0,0 +1,24 @@ +name: sdk/vercel + +on: + push: + branches: [main] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main] + paths-ignore: + - '**.md' + +jobs: + build-test-vercel: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - id: shared + name: Shared CI Steps + uses: ./actions/ci + with: + workspace_name: '@launchdarkly/vercel-server-sdk' + workspace_path: packages/sdk/vercel From 458a2d90e4bda000369fd9f22f2c4568041297b4 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:47:06 +0100 Subject: [PATCH 21/36] changes for typedoc --- packages/sdk/vercel/package.json | 3 ++- packages/sdk/vercel/tsconfig.json | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 72340b778e..bf836ec6e0 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -20,13 +20,14 @@ "/dist" ], "scripts": { + "doc": "../../../scripts/build-doc.sh .", "build": "../../../scripts/build-package.sh", "tsw": "yarn tsc --watch", "start": "yarn tsw", "lint": "eslint . --ext .ts", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", "test": "jest --ci --runInBand", - "check": "yarn prettier && yarn lint && yarn tsc && yarn test" + "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { "@launchdarkly/js-server-sdk-common": "^0.2.0", diff --git a/packages/sdk/vercel/tsconfig.json b/packages/sdk/vercel/tsconfig.json index e86166ee22..d52294d88d 100644 --- a/packages/sdk/vercel/tsconfig.json +++ b/packages/sdk/vercel/tsconfig.json @@ -3,20 +3,19 @@ // Uses "." so it can load package.json. "rootDir": ".", "outDir": "dist", - "target": "es2021", - "lib": ["es2021"], - "jsx": "react", - "module": "es2022", + "target": "es6", + "lib": ["es6"], + "module": "commonjs", + "strict": true, + "noImplicitOverride": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, // enables importers to jump to source + "resolveJsonModule": true, + "stripInternal": true, "moduleResolution": "node", "types": ["jest", "node"], - "resolveJsonModule": true, - "allowJs": true, - "checkJs": false, - // "noEmit": true, - "isolatedModules": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, "skipLibCheck": true }, "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"] From 54998a1c06c6a25ac53c570dd5627023a4af4636 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:50:00 +0100 Subject: [PATCH 22/36] prettier --- packages/sdk/vercel/src/utils/testData.json | 92 +++++++++---------- packages/sdk/vercel/src/vercelFeatureStore.ts | 34 ++++--- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/packages/sdk/vercel/src/utils/testData.json b/packages/sdk/vercel/src/utils/testData.json index e0bbda786d..54bda1946b 100644 --- a/packages/sdk/vercel/src/utils/testData.json +++ b/packages/sdk/vercel/src/utils/testData.json @@ -1,50 +1,50 @@ { - "flags": { - "testFlag1": { - "key": "testFlag1", - "on": false, - "prerequisites": [], - "targets": [], - "rules": [], - "fallthrough": { - "variation": 0 - }, - "offVariation": 1, - "variations": [true, false], - "clientSideAvailability": { - "usingMobileKey": true, - "usingEnvironmentId": true - }, - "clientSide": true, - "salt": "aef830243d6640d0a973be89988e008d", - "trackEvents": false, - "trackEventsFallthrough": false, - "debugEventsUntilDate": null, - "version": 2, - "deleted": false + "flags": { + "testFlag1": { + "key": "testFlag1", + "on": false, + "prerequisites": [], + "targets": [], + "rules": [], + "fallthrough": { + "variation": 0 }, - "testFlag2": { - "key": "testFlag2", - "on": false, - "prerequisites": [], - "targets": [], - "rules": [], - "fallthrough": { - "variation": 0 - }, - "offVariation": 1, - "variations": [true, false], - "clientSideAvailability": { - "usingMobileKey": true, - "usingEnvironmentId": true - }, - "clientSide": true, - "salt": "aef830243d6640d0a973be89988e008d", - "trackEvents": false, - "trackEventsFallthrough": false, - "debugEventsUntilDate": null, - "version": 2, - "deleted": false - } + "offVariation": 1, + "variations": [true, false], + "clientSideAvailability": { + "usingMobileKey": true, + "usingEnvironmentId": true + }, + "clientSide": true, + "salt": "aef830243d6640d0a973be89988e008d", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": null, + "version": 2, + "deleted": false + }, + "testFlag2": { + "key": "testFlag2", + "on": false, + "prerequisites": [], + "targets": [], + "rules": [], + "fallthrough": { + "variation": 0 + }, + "offVariation": 1, + "variations": [true, false], + "clientSideAvailability": { + "usingMobileKey": true, + "usingEnvironmentId": true + }, + "clientSide": true, + "salt": "aef830243d6640d0a973be89988e008d", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": null, + "version": 2, + "deleted": false } } +} diff --git a/packages/sdk/vercel/src/vercelFeatureStore.ts b/packages/sdk/vercel/src/vercelFeatureStore.ts index 9bc133ee05..9d845642a6 100644 --- a/packages/sdk/vercel/src/vercelFeatureStore.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.ts @@ -5,25 +5,29 @@ import type { LDFeatureStore, LDFeatureStoreDataStorage, LDFeatureStoreItem, - LDFeatureStoreKindData + LDFeatureStoreKindData, } from '@launchdarkly/js-server-sdk-common-edge'; import { noop } from '@launchdarkly/js-server-sdk-common-edge'; class VercelFeatureStore implements LDFeatureStore { - private edgeConfig: EdgeConfigClient - private configKey: string - private logger: LDLogger + private edgeConfig: EdgeConfigClient; + private configKey: string; + private logger: LDLogger; - constructor (edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) { - this.edgeConfig = edgeConfig + constructor(edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) { + this.edgeConfig = edgeConfig; this.configKey = `LD-Env-${sdkKey}`; - this.logger = logger + this.logger = logger; } - - async get(kind: DataKind, flagKey: string, callback: (res: LDFeatureStoreItem | null) => void): Promise { + + async get( + kind: DataKind, + flagKey: string, + callback: (res: LDFeatureStoreItem | null) => void + ): Promise { this.logger.debug(`Requesting ${flagKey} from ${this.configKey}`); try { - const config = await this.edgeConfig.get(this.configKey) + const config = await this.edgeConfig.get(this.configKey); if (config === null) { this.logger.error('Feature data not found in Edge Config.'); } @@ -39,7 +43,7 @@ class VercelFeatureStore implements LDFeatureStore { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; this.logger.debug(`Requesting all ${kindKey} data from Edge Config.`); try { - const config = await this.edgeConfig.get(this.configKey) + const config = await this.edgeConfig.get(this.configKey); if (config === null) { this.logger.error('Feature data not found in Edge Config.'); } @@ -51,7 +55,7 @@ class VercelFeatureStore implements LDFeatureStore { } } async initialized(callback: (isInitialized: boolean) => void = noop): Promise { - const config = await this.edgeConfig.get(this.configKey) + const config = await this.edgeConfig.get(this.configKey); const result = config !== null; this.logger.debug(`Is ${this.configKey} initialized? ${result}`); callback(result); @@ -64,9 +68,9 @@ class VercelFeatureStore implements LDFeatureStore { } // unused - close = noop - delete = noop - upsert = noop + close = noop; + delete = noop; + upsert = noop; } export default VercelFeatureStore; From 4b7759ea32b05028507e5b461ee028bc772209a9 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:53:31 +0100 Subject: [PATCH 23/36] update readme --- packages/sdk/vercel/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md index c30e91481e..f1c95d5a8c 100644 --- a/packages/sdk/vercel/README.md +++ b/packages/sdk/vercel/README.md @@ -4,7 +4,7 @@ This library supports using Vercel [Edge Config](https://vercel.com/docs/concept For more information, see the [SDK features guide](https://docs.launchdarkly.com/sdk/features/storing-data). -## Installation +## Install ```shell npm i @launchdarkly/vercel-server-sdk @@ -25,7 +25,15 @@ import init from '@launchdarkly/vercel-server-sdk' import { createClient } from '@vercel/edge-config' const edgeClient = createClient(process.env.EDGE_CONFIG) -const ldClient = init(edgeClient, 'YOUR CLIENT-SIDE SDK KEY'); +const ldClient = init('YOUR CLIENT-SIDE SDK KEY', edgeClient); + +await ldClient.waitForInitialization() +const ldContext = { + kind: 'org', + key: 'my-org-key', + someAttribute: 'my-attribute-value' +} +const flagValue = await ldClient.variation('my-flag', ldContext, true); ``` From 6f346c7ac56ff89c4da6f583e5fc676ae048183c Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:54:29 +0100 Subject: [PATCH 24/36] add required newlines --- packages/sdk/vercel/src/vercelFeatureStore.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sdk/vercel/src/vercelFeatureStore.ts b/packages/sdk/vercel/src/vercelFeatureStore.ts index 9d845642a6..3c34075e86 100644 --- a/packages/sdk/vercel/src/vercelFeatureStore.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.ts @@ -39,6 +39,7 @@ class VercelFeatureStore implements LDFeatureStore { callback(null); } } + async all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): Promise { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; this.logger.debug(`Requesting all ${kindKey} data from Edge Config.`); @@ -54,22 +55,27 @@ class VercelFeatureStore implements LDFeatureStore { callback({}); } } + async initialized(callback: (isInitialized: boolean) => void = noop): Promise { const config = await this.edgeConfig.get(this.configKey); const result = config !== null; this.logger.debug(`Is ${this.configKey} initialized? ${result}`); callback(result); } + init(allData: LDFeatureStoreDataStorage, callback: () => void): void { callback(); } + getDescription(): string { return 'Vercel Edge Config'; } // unused close = noop; + delete = noop; + upsert = noop; } From f83e2d65430b2467059953e0933d79b261e95af0 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 12:57:09 +0100 Subject: [PATCH 25/36] prettier again --- packages/sdk/vercel/src/vercelFeatureStore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/vercel/src/vercelFeatureStore.ts b/packages/sdk/vercel/src/vercelFeatureStore.ts index 3c34075e86..851b8ba31a 100644 --- a/packages/sdk/vercel/src/vercelFeatureStore.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.ts @@ -11,7 +11,9 @@ import { noop } from '@launchdarkly/js-server-sdk-common-edge'; class VercelFeatureStore implements LDFeatureStore { private edgeConfig: EdgeConfigClient; + private configKey: string; + private logger: LDLogger; constructor(edgeConfig: EdgeConfigClient, sdkKey: string, logger: LDLogger) { @@ -75,7 +77,7 @@ class VercelFeatureStore implements LDFeatureStore { close = noop; delete = noop; - + upsert = noop; } From 24893ed46b13d8149cdbf048de18644b96ef6791 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Fri, 21 Apr 2023 13:00:15 +0100 Subject: [PATCH 26/36] remove todo from readme --- packages/sdk/vercel/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md index f1c95d5a8c..017e5b0cab 100644 --- a/packages/sdk/vercel/README.md +++ b/packages/sdk/vercel/README.md @@ -36,7 +36,6 @@ const ldContext = { const flagValue = await ldClient.variation('my-flag', ldContext, true); ``` - To learn more, head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/vercel). ## Developing this SDK From 4a8d7959297f6d33b61796cb98a1bd1bc638f62e Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 10:10:56 +0100 Subject: [PATCH 27/36] pr feedback --- packages/sdk/vercel/README.md | 16 ++++++++-------- packages/sdk/vercel/package.json | 11 +++-------- scripts/build-package.sh | 4 ++-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md index 017e5b0cab..7137b4d2b5 100644 --- a/packages/sdk/vercel/README.md +++ b/packages/sdk/vercel/README.md @@ -21,18 +21,18 @@ yarn add -D @launchdarkly/vercel-server-sdk Initialize the ldClient with the [Vercel Edge SDK](https://vercel.com/docs/concepts/edge-network/edge-config/edge-config-sdk) and your LaunchDarkly client side sdk key: ```typescript -import init from '@launchdarkly/vercel-server-sdk' -import { createClient } from '@vercel/edge-config' +import init from '@launchdarkly/vercel-server-sdk'; +import { createClient } from '@vercel/edge-config'; -const edgeClient = createClient(process.env.EDGE_CONFIG) +const edgeClient = createClient(process.env.EDGE_CONFIG); const ldClient = init('YOUR CLIENT-SIDE SDK KEY', edgeClient); -await ldClient.waitForInitialization() -const ldContext = { +await ldClient.waitForInitialization(); +const ldContext = { kind: 'org', key: 'my-org-key', - someAttribute: 'my-attribute-value' -} + someAttribute: 'my-attribute-value', +}; const flagValue = await ldClient.variation('my-flag', ldContext, true); ``` @@ -59,4 +59,4 @@ yarn test - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation - - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates \ No newline at end of file + - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index bf836ec6e0..aac91d641f 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -11,11 +11,9 @@ "type": "module", "exports": { "require": "./dist/cjs/src/index.js", - "import": "./dist/esm/src/index.js", - "types": "./dist/esm/src/index.d.ts" + "import": "./dist/esm/src/index.js" }, "main": "./dist/cjs/src/index.js", - "types": "./dist/cjs/src/index.d.ts", "files": [ "/dist" ], @@ -23,22 +21,19 @@ "doc": "../../../scripts/build-doc.sh .", "build": "../../../scripts/build-package.sh", "tsw": "yarn tsc --watch", - "start": "yarn tsw", + "start": "rimraf dist && yarn tsw", "lint": "eslint . --ext .ts", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", "test": "jest --ci --runInBand", "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "^0.2.0", "@launchdarkly/js-server-sdk-common-edge": "0.0.2", - "crypto-js": "^4.1.1", - "uuid": "^9.0.0" + "crypto-js": "^4.1.1" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.5.0", - "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "@vercel/edge-config": "^0.1.7", diff --git a/scripts/build-package.sh b/scripts/build-package.sh index ef9e2374ef..93e38fe009 100755 --- a/scripts/build-package.sh +++ b/scripts/build-package.sh @@ -23,8 +23,8 @@ ESM_PACKAGE_JSON=$( jq -n \ --arg type "module" \ '{ name: $name, version: $version, type: $type }' ) -tsc --module commonjs --declaration --outDir dist/cjs/ +tsc --module commonjs --outDir dist/cjs/ echo "$CJS_PACKAGE_JSON" > dist/cjs/package.json -tsc --module es2022 --declaration --outDir dist/esm/ +tsc --module es2022 --outDir dist/esm/ echo "$ESM_PACKAGE_JSON" > dist/esm/package.json From 9f414db4d53404aaff69f371ec6a7c00a28fa0cf Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 10:16:15 +0100 Subject: [PATCH 28/36] add initial release config --- release-please-config.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release-please-config.json b/release-please-config.json index 013f9dc94e..b6bde76d31 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -14,6 +14,11 @@ }, "packages/sdk/cloudflare": { "bump-minor-pre-major": true + }, + "packages/type/my-package": { + "bump-minor-pre-major": true, + "release-as": "0.1.0", + "bootstrap-sha": "4a8d7959297f6d33b61796cb98a1bd1bc638f62e" } }, "plugins": ["node-workspace"] From bfd66c45f01e5cfdfb45e568ce5714fcf44c7e3c Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 10:25:37 +0100 Subject: [PATCH 29/36] add tests for createPlatformInfo --- packages/sdk/vercel/package.json | 2 +- .../sdk/vercel/src/createPlatformInfo.test.ts | 22 +++++++++++++++++++ packages/sdk/vercel/src/createPlatformInfo.ts | 6 ++--- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/sdk/vercel/src/createPlatformInfo.test.ts diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index aac91d641f..de1ac7525e 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -24,7 +24,7 @@ "start": "rimraf dist && yarn tsw", "lint": "eslint . --ext .ts", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'", - "test": "jest --ci --runInBand", + "test": "jest --ci --runInBand --coverage", "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { diff --git a/packages/sdk/vercel/src/createPlatformInfo.test.ts b/packages/sdk/vercel/src/createPlatformInfo.test.ts new file mode 100644 index 0000000000..8c2434cc49 --- /dev/null +++ b/packages/sdk/vercel/src/createPlatformInfo.test.ts @@ -0,0 +1,22 @@ +import { name, version } from '../package.json'; + +import createPlatformInfo from './createPlatformInfo'; + +describe('Vercel Platform Info', () => { + it('platformData shows correct information', () => { + const platformData = createPlatformInfo(); + + expect(platformData.platformData()).toEqual({ + name: 'Vercel Edge', + }); + }); + + it('sdkData shows correct information', () => { + const platformData = createPlatformInfo(); + + expect(platformData.sdkData()).toEqual({ + name: name, + version: version, + }); + }); +}); diff --git a/packages/sdk/vercel/src/createPlatformInfo.ts b/packages/sdk/vercel/src/createPlatformInfo.ts index 71d3f51a3a..b2fe070cb0 100644 --- a/packages/sdk/vercel/src/createPlatformInfo.ts +++ b/packages/sdk/vercel/src/createPlatformInfo.ts @@ -1,6 +1,6 @@ import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common-edge'; -import packageJson from '../package.json'; +import { name, version } from '../package.json'; class VercelPlatformInfo implements Info { platformData(): PlatformData { @@ -11,8 +11,8 @@ class VercelPlatformInfo implements Info { sdkData(): SdkData { return { - name: packageJson.name, - version: packageJson.version, + name: name, + version: version, }; } } From 8bfd57c67efc02dba8241b7e579b021418a72f62 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 10:44:48 +0100 Subject: [PATCH 30/36] working tests for index.ts, need to fix types --- packages/sdk/vercel/src/index.test.ts | 50 +++++++++++++++++++++ packages/sdk/vercel/src/utils/testData.json | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/sdk/vercel/src/index.test.ts diff --git a/packages/sdk/vercel/src/index.test.ts b/packages/sdk/vercel/src/index.test.ts new file mode 100644 index 0000000000..a4d9e0e5c8 --- /dev/null +++ b/packages/sdk/vercel/src/index.test.ts @@ -0,0 +1,50 @@ +import type { EdgeConfigClient } from '@vercel/edge-config'; +import { LDClient } from '@launchdarkly/js-server-sdk-common-edge'; +import { init } from './index'; +import * as testData from './utils/testData.json'; +import mockEdge from './utils/mockEdge'; + + +const sdkKey = 'test-sdk-key'; +const flagKey = 'testFlag1'; +const context = { kind: 'user', key: 'test-user-key-1' }; + +describe('init', () => { + let ldClient: LDClient; + + beforeAll(async () => { + // @ts-ignore + mockEdge.get = jest.fn(((key: string) => testData)) + ldClient = init(sdkKey, mockEdge); + await ldClient.waitForInitialization(); + }); + + afterAll(() => { + ldClient.close(); + }); + + test('variation', async () => { + const flagDetail = await ldClient.variation(flagKey, context, false); + expect(flagDetail).toBeTruthy(); + }); + + test('variationDetail', async () => { + const flagDetail = await ldClient.variationDetail(flagKey, context, false); + expect(flagDetail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }); + }); + + test('allFlags', async () => { + const allFlags = await ldClient.allFlagsState(context); + + expect(allFlags).toBeDefined(); + expect(allFlags.toJSON()).toEqual({ + $flagsState: { + testFlag1: { debugEventsUntilDate: null, variation: 0, version: 2 }, + testFlag2: { debugEventsUntilDate: null, variation: 1, version: 2 }, + }, + $valid: true, + testFlag1: true, + testFlag2: false, + }); + }); +}); diff --git a/packages/sdk/vercel/src/utils/testData.json b/packages/sdk/vercel/src/utils/testData.json index 54bda1946b..e5ebbdb071 100644 --- a/packages/sdk/vercel/src/utils/testData.json +++ b/packages/sdk/vercel/src/utils/testData.json @@ -2,7 +2,7 @@ "flags": { "testFlag1": { "key": "testFlag1", - "on": false, + "on": true, "prerequisites": [], "targets": [], "rules": [], From d32cb694e0590d183c89ace0246a652e08a25639 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 12:18:21 +0100 Subject: [PATCH 31/36] type issues --- packages/sdk/vercel/src/index.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sdk/vercel/src/index.test.ts b/packages/sdk/vercel/src/index.test.ts index a4d9e0e5c8..7c59c86c6e 100644 --- a/packages/sdk/vercel/src/index.test.ts +++ b/packages/sdk/vercel/src/index.test.ts @@ -1,10 +1,9 @@ -import type { EdgeConfigClient } from '@vercel/edge-config'; +import type { EdgeConfigClient, EdgeConfigValue } from '@vercel/edge-config'; import { LDClient } from '@launchdarkly/js-server-sdk-common-edge'; import { init } from './index'; import * as testData from './utils/testData.json'; import mockEdge from './utils/mockEdge'; - const sdkKey = 'test-sdk-key'; const flagKey = 'testFlag1'; const context = { kind: 'user', key: 'test-user-key-1' }; @@ -14,7 +13,7 @@ describe('init', () => { beforeAll(async () => { // @ts-ignore - mockEdge.get = jest.fn(((key: string) => testData)) + mockEdge.get = jest.fn(async () => testData as EdgeConfigValue); ldClient = init(sdkKey, mockEdge); await ldClient.waitForInitialization(); }); From b475f379c3aa8eefc247b5ff44e32eef2b6dd11a Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 12:19:04 +0100 Subject: [PATCH 32/36] type issues --- packages/sdk/vercel/src/index.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/vercel/src/index.test.ts b/packages/sdk/vercel/src/index.test.ts index 7c59c86c6e..5e6f4642eb 100644 --- a/packages/sdk/vercel/src/index.test.ts +++ b/packages/sdk/vercel/src/index.test.ts @@ -1,4 +1,4 @@ -import type { EdgeConfigClient, EdgeConfigValue } from '@vercel/edge-config'; +import type { EdgeConfigValue } from '@vercel/edge-config'; import { LDClient } from '@launchdarkly/js-server-sdk-common-edge'; import { init } from './index'; import * as testData from './utils/testData.json'; @@ -12,6 +12,8 @@ describe('init', () => { let ldClient: LDClient; beforeAll(async () => { + // I can't figure out a way around the generic types used in @vercel/edge-config causing us type issues here. + // The tests work as expected // @ts-ignore mockEdge.get = jest.fn(async () => testData as EdgeConfigValue); ldClient = init(sdkKey, mockEdge); From f4e3e975856518fcc68f540e36f4d482c6b16f5d Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 12:19:21 +0100 Subject: [PATCH 33/36] prettier the comments --- packages/sdk/vercel/src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/vercel/src/index.test.ts b/packages/sdk/vercel/src/index.test.ts index 5e6f4642eb..76706116fc 100644 --- a/packages/sdk/vercel/src/index.test.ts +++ b/packages/sdk/vercel/src/index.test.ts @@ -12,7 +12,7 @@ describe('init', () => { let ldClient: LDClient; beforeAll(async () => { - // I can't figure out a way around the generic types used in @vercel/edge-config causing us type issues here. + // I can't figure out a way around the generic types used in @vercel/edge-config causing us type issues here. // The tests work as expected // @ts-ignore mockEdge.get = jest.fn(async () => testData as EdgeConfigValue); From d34fbc51306a416c89ad33db5677d841f767a2c7 Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 12:22:34 +0100 Subject: [PATCH 34/36] increase test coverage --- packages/sdk/vercel/src/vercelFeatureStore.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sdk/vercel/src/vercelFeatureStore.test.ts b/packages/sdk/vercel/src/vercelFeatureStore.test.ts index 76a5a516de..956f41c70e 100644 --- a/packages/sdk/vercel/src/vercelFeatureStore.test.ts +++ b/packages/sdk/vercel/src/vercelFeatureStore.test.ts @@ -41,6 +41,12 @@ describe('VercelFeatureStore', () => { expect(flag).toBeUndefined(); }); + test('invalid namespace key', async () => { + const flag = await asyncFeatureStore.get({ namespace: 'invalid' }, 'testFlag1'); + + expect(flag).toBe(null); + }); + test('invalid edge config key', async () => { mockGet.mockImplementation(() => Promise.resolve(null)); const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); From bc70619e59f88b6336cf84f79b1c28c55e54d44b Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Mon, 24 Apr 2023 12:34:56 +0100 Subject: [PATCH 35/36] linter --- packages/sdk/vercel/src/createPlatformInfo.test.ts | 4 ++-- packages/sdk/vercel/src/createPlatformInfo.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sdk/vercel/src/createPlatformInfo.test.ts b/packages/sdk/vercel/src/createPlatformInfo.test.ts index 8c2434cc49..fad14f69b6 100644 --- a/packages/sdk/vercel/src/createPlatformInfo.test.ts +++ b/packages/sdk/vercel/src/createPlatformInfo.test.ts @@ -15,8 +15,8 @@ describe('Vercel Platform Info', () => { const platformData = createPlatformInfo(); expect(platformData.sdkData()).toEqual({ - name: name, - version: version, + name, + version, }); }); }); diff --git a/packages/sdk/vercel/src/createPlatformInfo.ts b/packages/sdk/vercel/src/createPlatformInfo.ts index b2fe070cb0..4bcb2880ef 100644 --- a/packages/sdk/vercel/src/createPlatformInfo.ts +++ b/packages/sdk/vercel/src/createPlatformInfo.ts @@ -11,8 +11,8 @@ class VercelPlatformInfo implements Info { sdkData(): SdkData { return { - name: name, - version: version, + name, + version, }; } } From 41f5d0ca5741414e837d5d5dfb6435c1acd5de4e Mon Sep 17 00:00:00 2001 From: Fabian Feldberg Date: Tue, 25 Apr 2023 12:06:35 +0100 Subject: [PATCH 36/36] update release please bootstrap sha --- release-please-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-please-config.json b/release-please-config.json index b6bde76d31..5cdb3b3cbc 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -18,7 +18,7 @@ "packages/type/my-package": { "bump-minor-pre-major": true, "release-as": "0.1.0", - "bootstrap-sha": "4a8d7959297f6d33b61796cb98a1bd1bc638f62e" + "bootstrap-sha": "bc70619e59f88b6336cf84f79b1c28c55e54d44b" } }, "plugins": ["node-workspace"]