diff --git a/package.json b/package.json index 28fb7bf636..0d7db2c1ea 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "packages/store/node-server-sdk-redis", "packages/store/node-server-sdk-dynamodb", "packages/telemetry/node-server-sdk-otel", + "packages/tooling/contract-test-utils", "packages/tooling/jest", "packages/tooling/jest/example/react-native-example", "packages/sdk/browser", diff --git a/packages/sdk/browser/contract-tests/entity/package.json b/packages/sdk/browser/contract-tests/entity/package.json index 6994e63bb1..1289f59963 100644 --- a/packages/sdk/browser/contract-tests/entity/package.json +++ b/packages/sdk/browser/contract-tests/entity/package.json @@ -12,7 +12,8 @@ "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore" }, "dependencies": { - "@launchdarkly/js-client-sdk": "*" + "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/js-contract-test-utils": "workspace:^" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", diff --git a/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts b/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts index 0d50a90bb8..da8165a9af 100644 --- a/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts +++ b/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts @@ -1,9 +1,13 @@ import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk'; - -import { CommandParams, CommandType, ValueType } from './CommandParams'; -import { CreateInstanceParams, SDKConfigParams } from './ConfigParams'; -import { makeLogger } from './makeLogger'; -import TestHook from './TestHook'; +import { + CommandParams, + CommandType, + CreateInstanceParams, + makeLogger, + SDKConfigParams, + ClientSideTestHook as TestHook, + ValueType, +} from '@launchdarkly/js-contract-test-utils/client'; export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); diff --git a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts b/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts deleted file mode 100644 index 11251d31ed..0000000000 --- a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { LDContext, LDEvaluationReason } from '@launchdarkly/js-client-sdk'; - -export enum CommandType { - EvaluateFlag = 'evaluate', - EvaluateAllFlags = 'evaluateAll', - IdentifyEvent = 'identifyEvent', - CustomEvent = 'customEvent', - AliasEvent = 'aliasEvent', - FlushEvents = 'flushEvents', - ContextBuild = 'contextBuild', - ContextConvert = 'contextConvert', - ContextComparison = 'contextComparison', - SecureModeHash = 'secureModeHash', -} - -export enum ValueType { - Bool = 'bool', - Int = 'int', - Double = 'double', - String = 'string', - Any = 'any', -} - -export interface CommandParams { - command: CommandType; - evaluate?: EvaluateFlagParams; - evaluateAll?: EvaluateAllFlagsParams; - customEvent?: CustomEventParams; - identifyEvent?: IdentifyEventParams; - contextBuild?: ContextBuildParams; - contextConvert?: ContextConvertParams; - contextComparison?: ContextComparisonPairParams; - secureModeHash?: SecureModeHashParams; -} - -export interface EvaluateFlagParams { - flagKey: string; - context?: LDContext; - user?: any; - valueType: ValueType; - defaultValue: unknown; - detail: boolean; -} - -export interface EvaluateFlagResponse { - value: unknown; - variationIndex?: number; - reason?: LDEvaluationReason; -} - -export interface EvaluateAllFlagsParams { - context?: LDContext; - user?: any; - withReasons: boolean; - clientSideOnly: boolean; - detailsOnlyForTrackedFlags: boolean; -} - -export interface EvaluateAllFlagsResponse { - state: Record; -} - -export interface CustomEventParams { - eventKey: string; - context?: LDContext; - user?: any; - data?: unknown; - omitNullData: boolean; - metricValue?: number; -} - -export interface IdentifyEventParams { - context?: LDContext; - user?: any; -} - -export interface ContextBuildParams { - single?: ContextBuildSingleParams; - multi?: ContextBuildSingleParams[]; -} - -export interface ContextBuildSingleParams { - kind?: string; - key: string; - name?: string; - anonymous?: boolean; - private?: string[]; - custom?: Record; -} - -export interface ContextBuildResponse { - output: string; - error: string; -} - -export interface ContextConvertParams { - input: string; -} - -export interface ContextComparisonPairParams { - context1: ContextComparisonParams; - context2: ContextComparisonParams; -} - -export interface ContextComparisonParams { - single?: ContextComparisonSingleParams; - multi?: ContextComparisonSingleParams[]; -} - -export interface ContextComparisonSingleParams { - kind: string; - key: string; - attributes?: AttributeDefinition[]; - privateAttributes?: PrivateAttribute[]; -} - -export interface AttributeDefinition { - name: string; - value?: unknown; -} - -export interface PrivateAttribute { - value: string; - literal: boolean; -} - -export interface ContextComparisonResponse { - equals: boolean; -} - -export interface SecureModeHashParams { - context?: LDContext; - user?: any; -} - -export interface SecureModeHashResponse { - result: string; -} - -export enum HookStage { - BeforeEvaluation = 'beforeEvaluation', - AfterEvaluation = 'afterEvaluation', -} - -export interface EvaluationSeriesContext { - flagKey: string; - context: LDContext; - defaultValue: unknown; - method: string; -} - -export interface HookExecutionPayload { - evaluationSeriesContext?: EvaluationSeriesContext; - evaluationSeriesData?: Record; - evaluationDetail?: EvaluateFlagResponse; - stage?: HookStage; -} diff --git a/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts b/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts deleted file mode 100644 index 520170e82c..0000000000 --- a/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { LDContext } from '@launchdarkly/js-client-sdk'; - -export interface CreateInstanceParams { - configuration: SDKConfigParams; - tag: string; -} - -export interface SDKConfigParams { - credential: string; - startWaitTimeMs?: number; // UnixMillisecondTime - initCanFail?: boolean; - serviceEndpoints?: SDKConfigServiceEndpointsParams; - tls?: SDKConfigTLSParams; - streaming?: SDKConfigStreamingParams; - polling?: SDKConfigPollingParams; - events?: SDKConfigEventParams; - tags?: SDKConfigTagsParams; - clientSide?: SDKConfigClientSideParams; - hooks?: SDKConfigHooksParams; - wrapper?: SDKConfigWrapper; -} - -export interface SDKConfigTLSParams { - skipVerifyPeer?: boolean; - customCAFile?: string; -} - -export interface SDKConfigServiceEndpointsParams { - streaming?: string; - polling?: string; - events?: string; -} - -export interface SDKConfigStreamingParams { - baseUri?: string; - initialRetryDelayMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigPollingParams { - baseUri?: string; - pollIntervalMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigEventParams { - baseUri?: string; - capacity?: number; - enableDiagnostics: boolean; - allAttributesPrivate?: boolean; - globalPrivateAttributes?: string[]; - flushIntervalMs?: number; // UnixMillisecondTime - omitAnonymousContexts?: boolean; - enableGzip?: boolean; -} - -export interface SDKConfigTagsParams { - applicationId?: string; - applicationVersion?: string; -} - -export interface SDKConfigClientSideParams { - initialContext?: LDContext; - initialUser?: any; - evaluationReasons?: boolean; - useReport?: boolean; - includeEnvironmentAttributes?: boolean; -} - -export interface SDKConfigEvaluationHookData { - [key: string]: unknown; -} - -export interface SDKConfigHookInstance { - name: string; - callbackUri: string; - data?: Record; - errors?: Record; -} - -export interface SDKConfigHooksParams { - hooks: SDKConfigHookInstance[]; -} - -export interface SDKConfigWrapper { - name: string; - version: string; -} - -export type HookStage = 'beforeEvaluation' | 'afterEvaluation'; diff --git a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts index 86ba4cf6ac..6f82d0c5ea 100644 --- a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts @@ -1,7 +1,7 @@ import { LDLogger } from '@launchdarkly/js-client-sdk'; +import { makeLogger } from '@launchdarkly/js-contract-test-utils/client'; import { ClientEntity, newSdkClientEntity } from './ClientEntity'; -import { makeLogger } from './makeLogger'; export default class TestHarnessWebSocket { private _ws?: WebSocket; diff --git a/packages/sdk/browser/contract-tests/entity/src/makeLogger.ts b/packages/sdk/browser/contract-tests/entity/src/makeLogger.ts deleted file mode 100644 index a8cf9f165d..0000000000 --- a/packages/sdk/browser/contract-tests/entity/src/makeLogger.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { LDLogger } from '@launchdarkly/js-client-sdk'; - -export function makeLogger(tag: string): LDLogger { - return { - debug(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.debug(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - info(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.info(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - warn(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.warn(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - error(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.error(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - }; -} diff --git a/packages/sdk/electron/contract-tests/entity/package.json b/packages/sdk/electron/contract-tests/entity/package.json index ee86de0614..422b9f1820 100644 --- a/packages/sdk/electron/contract-tests/entity/package.json +++ b/packages/sdk/electron/contract-tests/entity/package.json @@ -32,6 +32,7 @@ "vite": "^5.4.21" }, "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:^", "body-parser": "^2.2.2", "electron-squirrel-startup": "^1.0.1", "express": "^5.2.1" diff --git a/packages/sdk/electron/contract-tests/entity/src/ClientEntity.ts b/packages/sdk/electron/contract-tests/entity/src/ClientEntity.ts index 23e22af963..fc8e1c4a32 100644 --- a/packages/sdk/electron/contract-tests/entity/src/ClientEntity.ts +++ b/packages/sdk/electron/contract-tests/entity/src/ClientEntity.ts @@ -6,11 +6,15 @@ import path from 'node:path'; // eslint-disable-next-line import/no-extraneous-dependencies import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/electron-client-sdk'; - -import { CommandParams, CommandType, ValueType } from './CommandParams'; -import { CreateInstanceParams, SDKConfigParams } from './ConfigParams'; -import { makeLogger } from './makeLogger'; -import TestHook from './TestHook'; +import { + CommandParams, + CommandType, + CreateInstanceParams, + makeLogger, + SDKConfigParams, + ClientSideTestHook as TestHook, + ValueType, +} from '@launchdarkly/js-contract-test-utils/client'; export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); diff --git a/packages/sdk/electron/contract-tests/entity/src/ClientFactory.ts b/packages/sdk/electron/contract-tests/entity/src/ClientFactory.ts index ed5158d9e7..11a0e1048e 100644 --- a/packages/sdk/electron/contract-tests/entity/src/ClientFactory.ts +++ b/packages/sdk/electron/contract-tests/entity/src/ClientFactory.ts @@ -1,6 +1,6 @@ +import { CommandParams, CreateInstanceParams } from '@launchdarkly/js-contract-test-utils/client'; + import { ClientEntity, createEntity } from './ClientEntity'; -import { CommandParams } from './CommandParams'; -import { CreateInstanceParams } from './ConfigParams'; export default class ClientFactory { private _clientCounter = 0; diff --git a/packages/sdk/electron/contract-tests/entity/src/CommandParams.ts b/packages/sdk/electron/contract-tests/entity/src/CommandParams.ts deleted file mode 100644 index c5ed0f70e9..0000000000 --- a/packages/sdk/electron/contract-tests/entity/src/CommandParams.ts +++ /dev/null @@ -1,158 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { LDContext, LDEvaluationReason } from '@launchdarkly/electron-client-sdk'; - -export enum CommandType { - EvaluateFlag = 'evaluate', - EvaluateAllFlags = 'evaluateAll', - IdentifyEvent = 'identifyEvent', - CustomEvent = 'customEvent', - AliasEvent = 'aliasEvent', - FlushEvents = 'flushEvents', - ContextBuild = 'contextBuild', - ContextConvert = 'contextConvert', - ContextComparison = 'contextComparison', - SecureModeHash = 'secureModeHash', -} - -export enum ValueType { - Bool = 'bool', - Int = 'int', - Double = 'double', - String = 'string', - Any = 'any', -} - -export interface CommandParams { - command: CommandType; - evaluate?: EvaluateFlagParams; - evaluateAll?: EvaluateAllFlagsParams; - customEvent?: CustomEventParams; - identifyEvent?: IdentifyEventParams; - contextBuild?: ContextBuildParams; - contextConvert?: ContextConvertParams; - contextComparison?: ContextComparisonPairParams; - secureModeHash?: SecureModeHashParams; -} - -export interface EvaluateFlagParams { - flagKey: string; - context?: LDContext; - user?: any; - valueType: ValueType; - defaultValue: unknown; - detail: boolean; -} - -export interface EvaluateFlagResponse { - value: unknown; - variationIndex?: number; - reason?: LDEvaluationReason; -} - -export interface EvaluateAllFlagsParams { - context?: LDContext; - user?: any; - withReasons: boolean; - clientSideOnly: boolean; - detailsOnlyForTrackedFlags: boolean; -} - -export interface EvaluateAllFlagsResponse { - state: Record; -} - -export interface CustomEventParams { - eventKey: string; - context?: LDContext; - user?: any; - data?: unknown; - omitNullData: boolean; - metricValue?: number; -} - -export interface IdentifyEventParams { - context?: LDContext; - user?: any; -} - -export interface ContextBuildParams { - single?: ContextBuildSingleParams; - multi?: ContextBuildSingleParams[]; -} - -export interface ContextBuildSingleParams { - kind?: string; - key: string; - name?: string; - anonymous?: boolean; - private?: string[]; - custom?: Record; -} - -export interface ContextBuildResponse { - output: string; - error: string; -} - -export interface ContextConvertParams { - input: string; -} - -export interface ContextComparisonPairParams { - context1: ContextComparisonParams; - context2: ContextComparisonParams; -} - -export interface ContextComparisonParams { - single?: ContextComparisonSingleParams; - multi?: ContextComparisonSingleParams[]; -} - -export interface ContextComparisonSingleParams { - kind: string; - key: string; - attributes?: AttributeDefinition[]; - privateAttributes?: PrivateAttribute[]; -} - -export interface AttributeDefinition { - name: string; - value?: unknown; -} - -export interface PrivateAttribute { - value: string; - literal: boolean; -} - -export interface ContextComparisonResponse { - equals: boolean; -} - -export interface SecureModeHashParams { - context?: LDContext; - user?: any; -} - -export interface SecureModeHashResponse { - result: string; -} - -export enum HookStage { - BeforeEvaluation = 'beforeEvaluation', - AfterEvaluation = 'afterEvaluation', -} - -export interface EvaluationSeriesContext { - flagKey: string; - context: LDContext; - defaultValue: unknown; - method: string; -} - -export interface HookExecutionPayload { - evaluationSeriesContext?: EvaluationSeriesContext; - evaluationSeriesData?: Record; - evaluationDetail?: EvaluateFlagResponse; - stage?: HookStage; -} diff --git a/packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts b/packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts deleted file mode 100644 index 73a7917a98..0000000000 --- a/packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts +++ /dev/null @@ -1,96 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { LDContext } from '@launchdarkly/electron-client-sdk'; - -export interface CreateInstanceParams { - configuration: SDKConfigParams; - tag: string; -} - -export interface SDKConfigParams { - credential: string; - startWaitTimeMs?: number; // UnixMillisecondTime - initCanFail?: boolean; - serviceEndpoints?: SDKConfigServiceEndpointsParams; - tls?: SDKConfigTLSParams; - streaming?: SDKConfigStreamingParams; - polling?: SDKConfigPollingParams; - events?: SDKConfigEventParams; - tags?: SDKConfigTagsParams; - clientSide?: SDKConfigClientSideParams; - hooks?: SDKConfigHooksParams; - wrapper?: SDKConfigWrapper; - proxy?: SDKConfigProxyParams; -} - -export interface SDKConfigTLSParams { - skipVerifyPeer?: boolean; - customCAFile?: string; -} - -export interface SDKConfigServiceEndpointsParams { - streaming?: string; - polling?: string; - events?: string; -} - -export interface SDKConfigStreamingParams { - baseUri?: string; - initialRetryDelayMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigPollingParams { - baseUri?: string; - pollIntervalMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigEventParams { - baseUri?: string; - capacity?: number; - enableDiagnostics: boolean; - allAttributesPrivate?: boolean; - globalPrivateAttributes?: string[]; - flushIntervalMs?: number; // UnixMillisecondTime - omitAnonymousContexts?: boolean; - enableGzip?: boolean; -} - -export interface SDKConfigTagsParams { - applicationId?: string; - applicationVersion?: string; -} - -export interface SDKConfigClientSideParams { - initialContext?: LDContext; - initialUser?: any; - evaluationReasons?: boolean; - useReport?: boolean; - includeEnvironmentAttributes?: boolean; -} - -export interface SDKConfigEvaluationHookData { - [key: string]: unknown; -} - -export interface SDKConfigHookInstance { - name: string; - callbackUri: string; - data?: Record; - errors?: Record; -} - -export interface SDKConfigHooksParams { - hooks: SDKConfigHookInstance[]; -} - -export interface SDKConfigProxyParams { - httpProxy?: string; -} - -export interface SDKConfigWrapper { - name: string; - version: string; -} - -export type HookStage = 'beforeEvaluation' | 'afterEvaluation'; diff --git a/packages/sdk/electron/contract-tests/entity/src/TestHook.ts b/packages/sdk/electron/contract-tests/entity/src/TestHook.ts deleted file mode 100644 index 33af83119b..0000000000 --- a/packages/sdk/electron/contract-tests/entity/src/TestHook.ts +++ /dev/null @@ -1,95 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { - EvaluationSeriesContext, - EvaluationSeriesData, - Hook, - HookMetadata, - LDEvaluationDetail, - TrackSeriesContext, -} from '@launchdarkly/electron-client-sdk'; - -export interface HookData { - beforeEvaluation?: Record; - afterEvaluation?: Record; -} - -export interface HookErrors { - beforeEvaluation?: string; - afterEvaluation?: string; - afterTrack?: string; -} - -export default class TestHook implements Hook { - private _name: string; - private _endpoint: string; - private _data?: HookData; - private _errors?: HookErrors; - - constructor(name: string, endpoint: string, data?: HookData, errors?: HookErrors) { - this._name = name; - this._endpoint = endpoint; - this._data = data; - this._errors = errors; - } - - private async _safePost(body: unknown): Promise { - try { - await fetch(this._endpoint, { - method: 'POST', - body: JSON.stringify(body), - }); - } catch { - // The test could move on before the post, so we are ignoring - // failed posts. - } - } - - getMetadata(): HookMetadata { - return { - name: this._name, - }; - } - - beforeEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - ): EvaluationSeriesData { - if (this._errors?.beforeEvaluation) { - throw new Error(this._errors.beforeEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'beforeEvaluation', - }); - return { ...data, ...(this._data?.beforeEvaluation || {}) }; - } - - afterEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - detail: LDEvaluationDetail, - ): EvaluationSeriesData { - if (this._errors?.afterEvaluation) { - throw new Error(this._errors.afterEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'afterEvaluation', - evaluationDetail: detail, - }); - - return { ...data, ...(this._data?.afterEvaluation || {}) }; - } - - afterTrack(hookContext: TrackSeriesContext): void { - if (this._errors?.afterTrack) { - throw new Error(this._errors.afterTrack); - } - this._safePost({ - trackSeriesContext: hookContext, - stage: 'afterTrack', - }); - } -} diff --git a/packages/sdk/electron/contract-tests/entity/src/makeLogger.ts b/packages/sdk/electron/contract-tests/entity/src/makeLogger.ts deleted file mode 100644 index 58292e8e75..0000000000 --- a/packages/sdk/electron/contract-tests/entity/src/makeLogger.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { LDLogger } from '@launchdarkly/electron-client-sdk'; - -export function makeLogger(tag: string): LDLogger { - return { - debug(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.debug(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - info(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.info(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - warn(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.warn(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - error(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.error(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - }; -} diff --git a/packages/sdk/react-native/contract-tests/entity/metro.config.js b/packages/sdk/react-native/contract-tests/entity/metro.config.js index 6cb167be47..2d9a10086b 100644 --- a/packages/sdk/react-native/contract-tests/entity/metro.config.js +++ b/packages/sdk/react-native/contract-tests/entity/metro.config.js @@ -22,5 +22,8 @@ config.resolver.nodeModulesPaths = [ ]; // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` config.resolver.disableHierarchicalLookup = true; +// 4. Enable package.json "exports" field resolution (needed for subpath imports +// like @launchdarkly/js-contract-test-utils/client) +config.resolver.unstable_enablePackageExports = true; module.exports = config; diff --git a/packages/sdk/react-native/contract-tests/entity/package.json b/packages/sdk/react-native/contract-tests/entity/package.json index 38401d25da..5d527273f8 100644 --- a/packages/sdk/react-native/contract-tests/entity/package.json +++ b/packages/sdk/react-native/contract-tests/entity/package.json @@ -11,6 +11,7 @@ "ios": "expo run:ios" }, "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:^", "@launchdarkly/react-native-client-sdk": "workspace:^", "@react-native-async-storage/async-storage": "^2.0.0", "expo": "52.0.14", diff --git a/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts b/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts index 7f2b9ba6ad..19d87944d9 100644 --- a/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts +++ b/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts @@ -1,14 +1,18 @@ +import { + CommandParams, + CommandType, + CreateInstanceParams, + makeLogger, + SDKConfigParams, + ClientSideTestHook as TestHook, + ValueType, +} from '@launchdarkly/js-contract-test-utils/client'; import { AutoEnvAttributes, LDOptions, ReactNativeLDClient, } from '@launchdarkly/react-native-client-sdk'; -import { CommandParams, CommandType, ValueType } from './CommandParams'; -import { CreateInstanceParams, SDKConfigParams } from './ConfigParams'; -import { makeLogger } from './makeLogger'; -import TestHook from './TestHook'; - export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); diff --git a/packages/sdk/react-native/contract-tests/entity/src/CommandParams.ts b/packages/sdk/react-native/contract-tests/entity/src/CommandParams.ts deleted file mode 100644 index 55174970f2..0000000000 --- a/packages/sdk/react-native/contract-tests/entity/src/CommandParams.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { LDContext, LDEvaluationReason } from '@launchdarkly/react-native-client-sdk'; - -export enum CommandType { - EvaluateFlag = 'evaluate', - EvaluateAllFlags = 'evaluateAll', - IdentifyEvent = 'identifyEvent', - CustomEvent = 'customEvent', - AliasEvent = 'aliasEvent', - FlushEvents = 'flushEvents', - ContextBuild = 'contextBuild', - ContextConvert = 'contextConvert', - ContextComparison = 'contextComparison', - SecureModeHash = 'secureModeHash', -} - -export enum ValueType { - Bool = 'bool', - Int = 'int', - Double = 'double', - String = 'string', - Any = 'any', -} - -export interface CommandParams { - command: CommandType; - evaluate?: EvaluateFlagParams; - evaluateAll?: EvaluateAllFlagsParams; - customEvent?: CustomEventParams; - identifyEvent?: IdentifyEventParams; - contextBuild?: ContextBuildParams; - contextConvert?: ContextConvertParams; - contextComparison?: ContextComparisonPairParams; - secureModeHash?: SecureModeHashParams; -} - -export interface EvaluateFlagParams { - flagKey: string; - context?: LDContext; - user?: any; - valueType: ValueType; - defaultValue: unknown; - detail: boolean; -} - -export interface EvaluateFlagResponse { - value: unknown; - variationIndex?: number; - reason?: LDEvaluationReason; -} - -export interface EvaluateAllFlagsParams { - context?: LDContext; - user?: any; - withReasons: boolean; - clientSideOnly: boolean; - detailsOnlyForTrackedFlags: boolean; -} - -export interface EvaluateAllFlagsResponse { - state: Record; -} - -export interface CustomEventParams { - eventKey: string; - context?: LDContext; - user?: any; - data?: unknown; - omitNullData: boolean; - metricValue?: number; -} - -export interface IdentifyEventParams { - context?: LDContext; - user?: any; -} - -export interface ContextBuildParams { - single?: ContextBuildSingleParams; - multi?: ContextBuildSingleParams[]; -} - -export interface ContextBuildSingleParams { - kind?: string; - key: string; - name?: string; - anonymous?: boolean; - private?: string[]; - custom?: Record; -} - -export interface ContextBuildResponse { - output: string; - error: string; -} - -export interface ContextConvertParams { - input: string; -} - -export interface ContextComparisonPairParams { - context1: ContextComparisonParams; - context2: ContextComparisonParams; -} - -export interface ContextComparisonParams { - single?: ContextComparisonSingleParams; - multi?: ContextComparisonSingleParams[]; -} - -export interface ContextComparisonSingleParams { - kind: string; - key: string; - attributes?: AttributeDefinition[]; - privateAttributes?: PrivateAttribute[]; -} - -export interface AttributeDefinition { - name: string; - value?: unknown; -} - -export interface PrivateAttribute { - value: string; - literal: boolean; -} - -export interface ContextComparisonResponse { - equals: boolean; -} - -export interface SecureModeHashParams { - context?: LDContext; - user?: any; -} - -export interface SecureModeHashResponse { - result: string; -} - -export enum HookStage { - BeforeEvaluation = 'beforeEvaluation', - AfterEvaluation = 'afterEvaluation', -} - -export interface EvaluationSeriesContext { - flagKey: string; - context: LDContext; - defaultValue: unknown; - method: string; -} - -export interface HookExecutionPayload { - evaluationSeriesContext?: EvaluationSeriesContext; - evaluationSeriesData?: Record; - evaluationDetail?: EvaluateFlagResponse; - stage?: HookStage; -} diff --git a/packages/sdk/react-native/contract-tests/entity/src/ConfigParams.ts b/packages/sdk/react-native/contract-tests/entity/src/ConfigParams.ts deleted file mode 100644 index 7b15ab053a..0000000000 --- a/packages/sdk/react-native/contract-tests/entity/src/ConfigParams.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { LDContext } from '@launchdarkly/react-native-client-sdk'; - -export interface CreateInstanceParams { - configuration: SDKConfigParams; - tag: string; -} - -export interface SDKConfigParams { - credential: string; - startWaitTimeMs?: number; // UnixMillisecondTime - initCanFail?: boolean; - serviceEndpoints?: SDKConfigServiceEndpointsParams; - tls?: SDKConfigTLSParams; - streaming?: SDKConfigStreamingParams; - polling?: SDKConfigPollingParams; - events?: SDKConfigEventParams; - tags?: SDKConfigTagsParams; - clientSide?: SDKConfigClientSideParams; - hooks?: SDKConfigHooksParams; - wrapper?: SDKConfigWrapper; -} - -export interface SDKConfigTLSParams { - skipVerifyPeer?: boolean; - customCAFile?: string; -} - -export interface SDKConfigServiceEndpointsParams { - streaming?: string; - polling?: string; - events?: string; -} - -export interface SDKConfigStreamingParams { - baseUri?: string; - initialRetryDelayMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigPollingParams { - baseUri?: string; - pollIntervalMs?: number; // UnixMillisecondTime - filter?: string; -} - -export interface SDKConfigEventParams { - baseUri?: string; - capacity?: number; - enableDiagnostics: boolean; - allAttributesPrivate?: boolean; - globalPrivateAttributes?: string[]; - flushIntervalMs?: number; // UnixMillisecondTime - omitAnonymousContexts?: boolean; - enableGzip?: boolean; -} - -export interface SDKConfigTagsParams { - applicationId?: string; - applicationVersion?: string; -} - -export interface SDKConfigClientSideParams { - initialContext?: LDContext; - initialUser?: any; - evaluationReasons?: boolean; - useReport?: boolean; - includeEnvironmentAttributes?: boolean; -} - -export interface SDKConfigEvaluationHookData { - [key: string]: unknown; -} - -export interface SDKConfigHookInstance { - name: string; - callbackUri: string; - data?: Record; - errors?: Record; -} - -export interface SDKConfigHooksParams { - hooks: SDKConfigHookInstance[]; -} - -export interface SDKConfigWrapper { - name: string; - version: string; -} - -export type HookStage = 'beforeEvaluation' | 'afterEvaluation'; diff --git a/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts b/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts index 4bc88606a4..24cd7cff2b 100644 --- a/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts @@ -1,7 +1,7 @@ +import { makeLogger } from '@launchdarkly/js-contract-test-utils/client'; import { LDLogger } from '@launchdarkly/react-native-client-sdk'; import { ClientEntity, newSdkClientEntity } from './ClientEntity'; -import { makeLogger } from './makeLogger'; export default class TestHarnessWebSocket { private _ws?: WebSocket; diff --git a/packages/sdk/react-native/contract-tests/entity/src/TestHook.ts b/packages/sdk/react-native/contract-tests/entity/src/TestHook.ts deleted file mode 100644 index 80db5add4e..0000000000 --- a/packages/sdk/react-native/contract-tests/entity/src/TestHook.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - EvaluationSeriesContext, - EvaluationSeriesData, - Hook, - HookMetadata, - LDEvaluationDetail, - TrackSeriesContext, -} from '@launchdarkly/react-native-client-sdk'; - -export interface HookData { - beforeEvaluation?: Record; - afterEvaluation?: Record; -} - -export interface HookErrors { - beforeEvaluation?: string; - afterEvaluation?: string; - afterTrack?: string; -} - -export default class TestHook implements Hook { - private _name: string; - private _endpoint: string; - private _data?: HookData; - private _errors?: HookErrors; - - constructor(name: string, endpoint: string, data?: HookData, errors?: HookErrors) { - this._name = name; - this._endpoint = endpoint; - this._data = data; - this._errors = errors; - } - - private async _safePost(body: unknown): Promise { - try { - await fetch(this._endpoint, { - method: 'POST', - body: JSON.stringify(body), - }); - } catch { - // The test could move on before the post, so we are ignoring - // failed posts. - } - } - - getMetadata(): HookMetadata { - return { - name: this._name, - }; - } - - beforeEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - ): EvaluationSeriesData { - if (this._errors?.beforeEvaluation) { - throw new Error(this._errors.beforeEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'beforeEvaluation', - }); - return { ...data, ...(this._data?.beforeEvaluation || {}) }; - } - - afterEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - detail: LDEvaluationDetail, - ): EvaluationSeriesData { - if (this._errors?.afterEvaluation) { - throw new Error(this._errors.afterEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'afterEvaluation', - evaluationDetail: detail, - }); - - return { ...data, ...(this._data?.afterEvaluation || {}) }; - } - - afterTrack(hookContext: TrackSeriesContext): void { - if (this._errors?.afterTrack) { - throw new Error(this._errors.afterTrack); - } - this._safePost({ - trackSeriesContext: hookContext, - stage: 'afterTrack', - }); - } -} diff --git a/packages/sdk/react-native/contract-tests/entity/src/makeLogger.ts b/packages/sdk/react-native/contract-tests/entity/src/makeLogger.ts deleted file mode 100644 index 076710fec2..0000000000 --- a/packages/sdk/react-native/contract-tests/entity/src/makeLogger.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { LDLogger } from '@launchdarkly/react-native-client-sdk'; - -export function makeLogger(tag: string): LDLogger { - return { - debug(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.debug(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - info(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.info(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - warn(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.warn(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - error(message: any, ...args: any[]) { - // eslint-disable-next-line no-console - console.error(`${new Date().toISOString()} [${tag}]: ${message}`, ...args); - }, - }; -} diff --git a/packages/sdk/react/contract-tests/app/ClientEntity.ts b/packages/sdk/react/contract-tests/app/ClientEntity.ts index 6dc6607f6f..8b989e6b2a 100644 --- a/packages/sdk/react/contract-tests/app/ClientEntity.ts +++ b/packages/sdk/react/contract-tests/app/ClientEntity.ts @@ -2,13 +2,16 @@ import { useEffect } from 'react'; +import { + CommandParams, + CommandType, + makeLogger, + SDKConfigParams, + ClientSideTestHook as TestHook, + ValueType, +} from '@launchdarkly/js-contract-test-utils/client'; import { LDOptions, LDReactClient, useLDClient } from '@launchdarkly/react-sdk'; -import { CommandParams, CommandType, ValueType } from './CommandParams'; -import { SDKConfigParams } from './ConfigParams'; -import { makeLogger } from './makeLogger'; -import TestHook from './TestHook'; - export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); diff --git a/packages/sdk/react/contract-tests/app/ClientRoot.tsx b/packages/sdk/react/contract-tests/app/ClientRoot.tsx index 9246cc1abf..4dd03c6946 100644 --- a/packages/sdk/react/contract-tests/app/ClientRoot.tsx +++ b/packages/sdk/react/contract-tests/app/ClientRoot.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { CreateInstanceParams } from '@launchdarkly/js-contract-test-utils/client'; import { createClient, createLDReactProviderWithClient, @@ -9,7 +10,6 @@ import { } from '@launchdarkly/react-sdk'; import { ClientInstance, CommandHandler, makeSdkConfig } from './ClientEntity'; -import { CreateInstanceParams } from './ConfigParams'; import TestHarnessWebSocket from './TestHarnessWebSocket'; interface ClientRecord { diff --git a/packages/sdk/react/contract-tests/app/TestHarnessWebSocket.ts b/packages/sdk/react/contract-tests/app/TestHarnessWebSocket.ts index cf3422313a..8d0ae51f8f 100644 --- a/packages/sdk/react/contract-tests/app/TestHarnessWebSocket.ts +++ b/packages/sdk/react/contract-tests/app/TestHarnessWebSocket.ts @@ -1,8 +1,7 @@ +import { CreateInstanceParams, makeLogger } from '@launchdarkly/js-contract-test-utils/client'; import { LDLogger } from '@launchdarkly/react-sdk'; import { CommandHandler } from './ClientEntity'; -import { CreateInstanceParams } from './ConfigParams'; -import { makeLogger } from './makeLogger'; export default class TestHarnessWebSocket { private _ws?: WebSocket; diff --git a/packages/sdk/react/contract-tests/app/TestHook.ts b/packages/sdk/react/contract-tests/app/TestHook.ts deleted file mode 100644 index 9fd580067e..0000000000 --- a/packages/sdk/react/contract-tests/app/TestHook.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - EvaluationSeriesContext, - EvaluationSeriesData, - Hook, - HookMetadata, - LDEvaluationDetail, - TrackSeriesContext, -} from '@launchdarkly/react-sdk'; - -export interface HookData { - beforeEvaluation?: Record; - afterEvaluation?: Record; -} - -export interface HookErrors { - beforeEvaluation?: string; - afterEvaluation?: string; - afterTrack?: string; -} - -export default class TestHook implements Hook { - private _name: string; - private _endpoint: string; - private _data?: HookData; - private _errors?: HookErrors; - - constructor(name: string, endpoint: string, data?: HookData, errors?: HookErrors) { - this._name = name; - this._endpoint = endpoint; - this._data = data; - this._errors = errors; - } - - private async _safePost(body: unknown): Promise { - try { - await fetch(this._endpoint, { - method: 'POST', - body: JSON.stringify(body), - }); - } catch { - // The test could move on before the post, so we are ignoring - // failed posts. - } - } - - getMetadata(): HookMetadata { - return { - name: this._name, - }; - } - - beforeEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - ): EvaluationSeriesData { - if (this._errors?.beforeEvaluation) { - throw new Error(this._errors.beforeEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'beforeEvaluation', - }); - return { ...data, ...(this._data?.beforeEvaluation || {}) }; - } - - afterEvaluation( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - detail: LDEvaluationDetail, - ): EvaluationSeriesData { - if (this._errors?.afterEvaluation) { - throw new Error(this._errors.afterEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'afterEvaluation', - evaluationDetail: detail, - }); - - return { ...data, ...(this._data?.afterEvaluation || {}) }; - } - - afterTrack(hookContext: TrackSeriesContext): void { - if (this._errors?.afterTrack) { - throw new Error(this._errors.afterTrack); - } - this._safePost({ - trackSeriesContext: hookContext, - stage: 'afterTrack', - }); - } -} diff --git a/packages/sdk/react/contract-tests/package.json b/packages/sdk/react/contract-tests/package.json index c23a5d7877..71633c755f 100644 --- a/packages/sdk/react/contract-tests/package.json +++ b/packages/sdk/react/contract-tests/package.json @@ -13,6 +13,7 @@ "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore" }, "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:^", "@launchdarkly/react-sdk": "workspace:^", "next": "16.1.5", "react": "19.2.3", diff --git a/packages/tooling/contract-test-utils/README.md b/packages/tooling/contract-test-utils/README.md new file mode 100644 index 0000000000..b7e592028a --- /dev/null +++ b/packages/tooling/contract-test-utils/README.md @@ -0,0 +1,93 @@ +# @launchdarkly/js-contract-test-utils + +Shared utilities for LaunchDarkly JavaScript SDK contract tests. This package provides universal types, logging utilities, and a client-side test hook that are used across multiple SDK contract test implementations. + +This is a **private** package (not published to npm) used only within this monorepo. + +## Subpath Exports + +The package uses subpath exports to organize code by platform: + +| Import Path | Contents | Resolution | +|---|---|---| +| `@launchdarkly/js-contract-test-utils` | Universal types, logging, `ClientPool` | Source `.ts` (for bundlers) | +| `@launchdarkly/js-contract-test-utils/client` | Client-side `TestHook` | Source `.ts` (for bundlers) | + +### Universal (`"."`) + +Types and utilities with no SDK dependency: + +```ts +import { + CommandType, + ValueType, + CommandParams, + CreateInstanceParams, + SDKConfigParams, + makeLogger, + ClientPool, +} from '@launchdarkly/js-contract-test-utils'; +``` + +### Client-side (`"./client"`) + +For browser, React Native, and Electron contract tests. Includes all universal exports plus: + +```ts +import { + ClientSideTestHook, +} from '@launchdarkly/js-contract-test-utils/client'; +``` + +- **`ClientSideTestHook`** -- Hook implementation using `fetch()` to report hook execution data back to the test harness. + +## Build + +```bash +yarn build +``` + +## Usage in Entity Packages + +### Browser / React Native / Electron (client-side) + +```ts +import { + ClientSideTestHook, + CommandParams, + makeLogger, +} from '@launchdarkly/js-contract-test-utils/client'; +``` + +### Universal types and utilities + +```ts +import { + ClientPool, + makeLogger, + CommandType, +} from '@launchdarkly/js-contract-test-utils'; + +const pool = new ClientPool(); +const id = pool.nextId(); +pool.add(id, entity); +``` + +## Architecture + +``` +contract-test-utils/ + src/ + client-side/ + TestHook.ts # fetch()-based hook reporting + server-side/ + ClientPool.ts # Generic entity pool + logging/ + makeLogger.ts # Logger factory + types/ + CommandParams.ts # Command/response type definitions + ConfigParams.ts # SDK configuration type definitions + compat.ts # Minimal cross-SDK type aliases + index.ts # Universal exports + client.ts # Client-side exports +``` diff --git a/packages/tooling/contract-test-utils/package.json b/packages/tooling/contract-test-utils/package.json new file mode 100644 index 0000000000..de1dd22804 --- /dev/null +++ b/packages/tooling/contract-test-utils/package.json @@ -0,0 +1,38 @@ +{ + "name": "@launchdarkly/js-contract-test-utils", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "default": "./src/index.ts" + }, + "./client": { + "types": "./src/client.ts", + "import": "./src/client.ts", + "default": "./src/client.ts" + } + }, + "typesVersions": { + "*": { + "client": [ + "./src/client.ts" + ] + } + }, + "scripts": { + "build": "tsc", + "clean": "rimraf dist" + }, + "dependencies": { + "@launchdarkly/js-client-sdk-common": "workspace:^" + }, + "devDependencies": { + "@types/node": "^18.11.9", + "typescript": "^4.9.0" + } +} diff --git a/packages/sdk/browser/contract-tests/entity/src/TestHook.ts b/packages/tooling/contract-test-utils/src/client-side/TestHook.ts similarity index 98% rename from packages/sdk/browser/contract-tests/entity/src/TestHook.ts rename to packages/tooling/contract-test-utils/src/client-side/TestHook.ts index 046e058096..9a107f420f 100644 --- a/packages/sdk/browser/contract-tests/entity/src/TestHook.ts +++ b/packages/tooling/contract-test-utils/src/client-side/TestHook.ts @@ -5,7 +5,7 @@ import { HookMetadata, LDEvaluationDetail, TrackSeriesContext, -} from '@launchdarkly/js-client-sdk'; +} from '@launchdarkly/js-client-sdk-common'; export interface HookData { beforeEvaluation?: Record; diff --git a/packages/tooling/contract-test-utils/src/client.ts b/packages/tooling/contract-test-utils/src/client.ts new file mode 100644 index 0000000000..d0a167b519 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/client.ts @@ -0,0 +1,9 @@ +// Re-export universal exports +export * from './index'; + +// Client-side exports +export { default as ClientSideTestHook } from './client-side/TestHook'; +export type { + HookData as ClientSideHookData, + HookErrors as ClientSideHookErrors, +} from './client-side/TestHook'; diff --git a/packages/tooling/contract-test-utils/src/index.ts b/packages/tooling/contract-test-utils/src/index.ts new file mode 100644 index 0000000000..82c54b0dcb --- /dev/null +++ b/packages/tooling/contract-test-utils/src/index.ts @@ -0,0 +1,20 @@ +// Universal exports (no SDK dependency) +export * from './types/CommandParams'; +export { + type CreateInstanceParams, + type SDKConfigParams, + type SDKConfigTLSParams, + type SDKConfigServiceEndpointsParams, + type SDKConfigStreamingParams, + type SDKConfigPollingParams, + type SDKConfigEventParams, + type SDKConfigTagsParams, + type SDKConfigClientSideParams, + type SDKConfigEvaluationHookData, + type SDKConfigHookInstance, + type SDKConfigHooksParams, + type SDKConfigProxyParams, + type SDKConfigWrapper, +} from './types/ConfigParams'; +export { makeLogger } from './logging/makeLogger'; +export { ClientPool } from './server-side/ClientPool'; diff --git a/packages/sdk/react/contract-tests/app/makeLogger.ts b/packages/tooling/contract-test-utils/src/logging/makeLogger.ts similarity index 92% rename from packages/sdk/react/contract-tests/app/makeLogger.ts rename to packages/tooling/contract-test-utils/src/logging/makeLogger.ts index e032866c19..ad01caf65d 100644 --- a/packages/sdk/react/contract-tests/app/makeLogger.ts +++ b/packages/tooling/contract-test-utils/src/logging/makeLogger.ts @@ -1,4 +1,4 @@ -import { LDLogger } from '@launchdarkly/react-sdk'; +import { LDLogger } from '@launchdarkly/js-client-sdk-common'; export function makeLogger(tag: string): LDLogger { return { diff --git a/packages/tooling/contract-test-utils/src/server-side/ClientPool.ts b/packages/tooling/contract-test-utils/src/server-side/ClientPool.ts new file mode 100644 index 0000000000..72e093ddab --- /dev/null +++ b/packages/tooling/contract-test-utils/src/server-side/ClientPool.ts @@ -0,0 +1,63 @@ +/** + * ClientPool is a generic pool that manages a collection of client entities. + * It provides methods to add, retrieve, remove, and generate IDs for clients. + * + * This eliminates duplication of the Record + counter pattern + * used across multiple contract test implementations. + */ +export class ClientPool { + private _clients: Record = {}; + private _clientCounter = 0; + + /** + * Generate a new unique client ID and increment the counter. + * @returns A new unique string ID for the client. + */ + nextId(): string { + const id = this._clientCounter.toString(); + this._clientCounter += 1; + return id; + } + + /** + * Add a client entity to the pool, assigning it the next available ID. + * @param client - The client entity to store. + * @returns The unique string ID assigned to the client. + */ + add(client: T): string { + const id = this.nextId(); + this._clients[id] = client; + return id; + } + + /** + * Retrieve a client entity by its ID. + * @param id - The unique identifier for the client. + * @returns The client entity, or undefined if not found. + */ + get(id: string): T | undefined { + return this._clients[id]; + } + + /** + * Check if a client entity exists in the pool. + * @param id - The unique identifier for the client. + * @returns True if the client exists. + */ + has(id: string): boolean { + return Object.prototype.hasOwnProperty.call(this._clients, id); + } + + /** + * Remove a client entity from the pool by its ID. + * @param id - The unique identifier for the client. + * @returns True if the client was removed, false if it did not exist. + */ + remove(id: string): boolean { + if (this.has(id)) { + delete this._clients[id]; + return true; + } + return false; + } +} diff --git a/packages/sdk/react/contract-tests/app/CommandParams.ts b/packages/tooling/contract-test-utils/src/types/CommandParams.ts similarity index 84% rename from packages/sdk/react/contract-tests/app/CommandParams.ts rename to packages/tooling/contract-test-utils/src/types/CommandParams.ts index 512a03bd36..5d6dcd27d1 100644 --- a/packages/sdk/react/contract-tests/app/CommandParams.ts +++ b/packages/tooling/contract-test-utils/src/types/CommandParams.ts @@ -1,18 +1,5 @@ -import { LDContext, LDEvaluationReason } from '@launchdarkly/react-sdk'; - -export type CommandType = - | 'evaluate' - | 'evaluateAll' - | 'identifyEvent' - | 'customEvent' - | 'aliasEvent' - | 'flushEvents' - | 'contextBuild' - | 'contextConvert' - | 'contextComparison' - | 'secureModeHash'; - -// eslint-disable-next-line @typescript-eslint/no-redeclare +import { LDContext, LDEvaluationReason } from '@launchdarkly/js-client-sdk-common'; + export const CommandType = { EvaluateFlag: 'evaluate', EvaluateAllFlags: 'evaluateAll', @@ -25,10 +12,8 @@ export const CommandType = { ContextComparison: 'contextComparison', SecureModeHash: 'secureModeHash', } as const; +export type CommandType = (typeof CommandType)[keyof typeof CommandType]; -export type ValueType = 'bool' | 'int' | 'double' | 'string' | 'any'; - -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ValueType = { Bool: 'bool', Int: 'int', @@ -36,6 +21,7 @@ export const ValueType = { String: 'string', Any: 'any', } as const; +export type ValueType = (typeof ValueType)[keyof typeof ValueType]; export interface CommandParams { command: CommandType; @@ -153,13 +139,11 @@ export interface SecureModeHashResponse { result: string; } -export type HookStage = 'beforeEvaluation' | 'afterEvaluation'; - -// eslint-disable-next-line @typescript-eslint/no-redeclare export const HookStage = { BeforeEvaluation: 'beforeEvaluation', AfterEvaluation: 'afterEvaluation', } as const; +export type HookStage = (typeof HookStage)[keyof typeof HookStage]; export interface EvaluationSeriesContext { flagKey: string; diff --git a/packages/sdk/react/contract-tests/app/ConfigParams.ts b/packages/tooling/contract-test-utils/src/types/ConfigParams.ts similarity index 92% rename from packages/sdk/react/contract-tests/app/ConfigParams.ts rename to packages/tooling/contract-test-utils/src/types/ConfigParams.ts index 368e38a2bf..6727ff5e0b 100644 --- a/packages/sdk/react/contract-tests/app/ConfigParams.ts +++ b/packages/tooling/contract-test-utils/src/types/ConfigParams.ts @@ -1,4 +1,4 @@ -import { LDContext } from '@launchdarkly/react-sdk'; +import { LDContext } from '@launchdarkly/js-client-sdk-common'; import { HookStage } from './CommandParams'; @@ -20,6 +20,7 @@ export interface SDKConfigParams { clientSide?: SDKConfigClientSideParams; hooks?: SDKConfigHooksParams; wrapper?: SDKConfigWrapper; + proxy?: SDKConfigProxyParams; } export interface SDKConfigTLSParams { @@ -84,6 +85,10 @@ export interface SDKConfigHooksParams { hooks: SDKConfigHookInstance[]; } +export interface SDKConfigProxyParams { + httpProxy?: string; +} + export interface SDKConfigWrapper { name: string; version: string; diff --git a/packages/tooling/contract-test-utils/tsconfig.json b/packages/tooling/contract-test-utils/tsconfig.json new file mode 100644 index 0000000000..166160dbfa --- /dev/null +++ b/packages/tooling/contract-test-utils/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "noImplicitOverride": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "stripInternal": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +}