diff --git a/package.json b/package.json index 28fb7bf636..b049e674fe 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "packages/sdk/shopify-oxygen", "packages/sdk/shopify-oxygen/contract-tests", "packages/sdk/shopify-oxygen/example", - "packages/sdk/browser/example" + "packages/sdk/browser/example", + "packages/tooling/contract-test-utils" ], "private": true, "scripts": { diff --git a/packages/sdk/browser/contract-tests/adapter/package.json b/packages/sdk/browser/contract-tests/adapter/package.json index 13515342f0..235d339233 100644 --- a/packages/sdk/browser/contract-tests/adapter/package.json +++ b/packages/sdk/browser/contract-tests/adapter/package.json @@ -2,39 +2,13 @@ "name": "browser-contract-test-adapter", "version": "1.0.0", "description": "Adapts REST interface to a websocket for use in browsers.", - "main": "dist/index.js", "scripts": { - "build": "tsc", - "start": "yarn build && node dist/index.js", - "lint": "eslint ./src", - "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore" + "build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter", + "start": "yarn build && sdk-testharness-server adapter" }, "author": "", "license": "UNLICENSED", "dependencies": { - "body-parser": "^1.20.3", - "cors": "^2.8.5", - "express": "^4.21.0", - "ws": "^8.18.0" - }, - "devDependencies": { - "@eslint/js": "^9.10.0", - "@trivago/prettier-plugin-sort-imports": "^4.1.1", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/ws": "^8.5.12", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "eslint": "^8.45.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.6.3", - "eslint-plugin-prettier": "^5.0.0", - "globals": "^15.9.0", - "prettier": "^3.0.0", - "typescript": "^5.6.2", - "typescript-eslint": "^8.5.0" + "@launchdarkly/js-contract-test-utils": "workspace:*" } } diff --git a/packages/sdk/browser/contract-tests/adapter/tsconfig.json b/packages/sdk/browser/contract-tests/adapter/tsconfig.json deleted file mode 100644 index f6ad77fb4c..0000000000 --- a/packages/sdk/browser/contract-tests/adapter/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "moduleResolution": "node", - "outDir": "dist", - "sourceMap": true, - "skipLibCheck": true - }, - "lib": ["ES6"], - "exclude": ["**/*.test.ts", "dist", "node_modules"] -} diff --git a/packages/sdk/browser/contract-tests/entity/package.json b/packages/sdk/browser/contract-tests/entity/package.json index 6994e63bb1..d2123889ff 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": "*", + "@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/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..82bb577a9e 100644 --- a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts @@ -1,96 +1,22 @@ -import { LDLogger } from '@launchdarkly/js-client-sdk'; - -import { ClientEntity, newSdkClientEntity } from './ClientEntity'; -import { makeLogger } from './makeLogger'; - -export default class TestHarnessWebSocket { - private _ws?: WebSocket; - private readonly _entities: Record = {}; - private _clientCounter = 0; - private _logger: LDLogger = makeLogger('TestHarnessWebSocket'); - - constructor(private readonly _url: string) {} - - connect() { - this._logger.info(`Connecting to web socket.`); - this._ws = new WebSocket(this._url, ['v1']); - this._ws.onopen = () => { - this._logger.info('Connected to websocket.'); - }; - this._ws.onclose = () => { - this._logger.info('Websocket closed. Attempting to reconnect in 1 second.'); - setTimeout(() => { - this.connect(); - }, 1000); - }; - this._ws.onerror = (err) => { - this._logger.info(`error:`, err); - }; - - this._ws.onmessage = async (msg) => { - this._logger.info('Test harness message', msg); - const data = JSON.parse(msg.data); - const resData: any = { reqId: data.reqId }; - switch (data.command) { - case 'getCapabilities': - resData.capabilities = [ - 'client-side', - 'service-endpoints', - 'tags', - 'user-type', - 'inline-context-all', - 'anonymous-redaction', - 'strongly-typed', - 'client-prereq-events', - 'client-per-context-summaries', - 'track-hooks', - ]; - - break; - case 'createClient': - { - resData.resourceUrl = `/clients/${this._clientCounter}`; - resData.status = 201; - const entity = await newSdkClientEntity(data.body); - this._entities[this._clientCounter] = entity; - this._clientCounter += 1; - } - break; - case 'runCommand': - if (Object.prototype.hasOwnProperty.call(this._entities, data.id)) { - const entity = this._entities[data.id]; - const body = await entity.doCommand(data.body); - resData.body = body; - resData.status = body ? 200 : 204; - } else { - resData.status = 404; - this._logger.warn(`Client did not exist: ${data.id}`); - } - - break; - case 'deleteClient': - if (Object.prototype.hasOwnProperty.call(this._entities, data.id)) { - const entity = this._entities[data.id]; - entity.close(); - delete this._entities[data.id]; - } else { - resData.status = 404; - this._logger.warn(`Could not delete client because it did not exist: ${data.id}`); - } - break; - default: - break; - } - - this.send(resData); - }; - } - - disconnect() { - this._ws?.close(); - } - - send(data: unknown) { - this._ws?.send(JSON.stringify(data)); +import { TestHarnessWebSocket as SharedTestHarnessWebSocket } from '@launchdarkly/js-contract-test-utils/client'; + +import { newSdkClientEntity } from './ClientEntity'; + +const CAPABILITIES = [ + 'client-side', + 'service-endpoints', + 'tags', + 'user-type', + 'inline-context-all', + 'anonymous-redaction', + 'strongly-typed', + 'client-prereq-events', + 'client-per-context-summaries', + 'track-hooks', +]; + +export default class TestHarnessWebSocket extends SharedTestHarnessWebSocket { + constructor(url: string) { + super(url, CAPABILITIES, newSdkClientEntity); } } 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..23188fc6cd 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, + ValueType, +} from '@launchdarkly/js-contract-test-utils'; +import { ClientSideTestHook as TestHook } 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..f0965b00d7 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'; + 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/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/adapter/package.json b/packages/sdk/react-native/contract-tests/adapter/package.json index 88c409a3c7..b105967cb7 100644 --- a/packages/sdk/react-native/contract-tests/adapter/package.json +++ b/packages/sdk/react-native/contract-tests/adapter/package.json @@ -2,23 +2,13 @@ "name": "react-native-contract-test-adapter", "version": "1.0.0", "description": "Adapts REST interface to a websocket for use in React Native.", - "main": "dist/index.js", "scripts": { - "build": "tsc", - "start": "yarn build && node dist/index.js" + "build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter", + "start": "yarn build && sdk-testharness-server adapter" }, "author": "", "license": "UNLICENSED", "dependencies": { - "body-parser": "^1.20.3", - "cors": "^2.8.5", - "express": "^4.21.0", - "ws": "^8.18.0" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/ws": "^8.5.12", - "typescript": "^5.6.2" + "@launchdarkly/js-contract-test-utils": "workspace:*" } } diff --git a/packages/sdk/react-native/contract-tests/adapter/src/index.ts b/packages/sdk/react-native/contract-tests/adapter/src/index.ts deleted file mode 100644 index 5dfffa3bd8..0000000000 --- a/packages/sdk/react-native/contract-tests/adapter/src/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable no-console */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import bodyParser from 'body-parser'; -import cors from 'cors'; -import { randomUUID } from 'crypto'; -import express from 'express'; -import http from 'node:http'; -import util from 'node:util'; -import { WebSocketServer } from 'ws'; - -let server: http.Server | undefined; - -async function main() { - const wss = new WebSocketServer({ port: 8001 }); - const waiters: Record void> = {}; - - console.log('Running contract test harness adapter.'); - wss.on('connection', async (ws) => { - ws.on('error', console.error); - - ws.on('message', (stringData: string) => { - const data = JSON.parse(stringData); - if (Object.prototype.hasOwnProperty.call(waiters, data.reqId)) { - waiters[data.reqId](data); - delete waiters[data.reqId]; - } else { - console.error('Did not find outstanding request', data.reqId); - } - }); - - const send = (data: { [key: string]: unknown; reqId: string }): Promise => { - let resolver: (data: unknown) => void; - const waiter = new Promise((resolve) => { - resolver = resolve; - }); - // @ts-expect-error The body of the above assignment runs sequentially. - waiters[data.reqId] = resolver; - ws.send(JSON.stringify(data)); - return waiter; - }; - - if (server) { - await util.promisify(server.close).call(server); - server = undefined; - } - - const app = express(); - - const port = 8000; - - app.use( - cors({ - origin: '*', - allowedHeaders: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - }), - ); - app.use(bodyParser.json()); - - app.get('/', async (_req, res) => { - const commandResult = await send({ command: 'getCapabilities', reqId: randomUUID() }); - res.header('Content-Type', 'application/json'); - res.json(commandResult); - }); - - app.delete('/', () => { - process.exit(); - }); - - app.post('/', async (req, res) => { - const commandResult = await send({ - command: 'createClient', - body: req.body, - reqId: randomUUID(), - }); - if (commandResult.resourceUrl) { - res.set('Location', commandResult.resourceUrl); - } - if (commandResult.status) { - res.status(commandResult.status); - } - res.send(); - }); - - app.post('/clients/:id', async (req, res) => { - const commandResult = await send({ - command: 'runCommand', - id: req.params.id, - body: req.body, - reqId: randomUUID(), - }); - if (commandResult.status) { - res.status(commandResult.status); - } - if (commandResult.body) { - res.write(JSON.stringify(commandResult.body)); - } - res.send(); - }); - - app.delete('/clients/:id', async (req, res) => { - await send({ command: 'deleteClient', id: req.params.id, reqId: randomUUID() }); - res.send(); - }); - - server = app.listen(port, () => { - console.log('Listening on port %d', port); - }); - }); -} -main(); diff --git a/packages/sdk/react-native/contract-tests/adapter/tsconfig.json b/packages/sdk/react-native/contract-tests/adapter/tsconfig.json deleted file mode 100644 index 693bf1f0c2..0000000000 --- a/packages/sdk/react-native/contract-tests/adapter/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "lib": ["ES6"], - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "moduleResolution": "node", - "outDir": "dist", - "sourceMap": true, - "skipLibCheck": true - }, - "exclude": ["**/*.test.ts", "dist", "node_modules"] -} diff --git a/packages/sdk/react-native/contract-tests/entity/App.tsx b/packages/sdk/react-native/contract-tests/entity/App.tsx index 0293facee3..12928670ba 100644 --- a/packages/sdk/react-native/contract-tests/entity/App.tsx +++ b/packages/sdk/react-native/contract-tests/entity/App.tsx @@ -1,7 +1,23 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import TestHarnessWebSocket from './src/TestHarnessWebSocket'; +import { TestHarnessWebSocket } from '@launchdarkly/js-contract-test-utils/client'; + +import { newSdkClientEntity } from './src/ClientEntity'; + +const capabilities = [ + 'client-side', + 'mobile', + 'service-endpoints', + 'tags', + 'user-type', + 'inline-context-all', + 'anonymous-redaction', + 'strongly-typed', + 'client-prereq-events', + 'client-per-context-summaries', + 'track-hooks', +]; const styles = StyleSheet.create({ container: { @@ -24,7 +40,12 @@ export default function App() { const [connected, setConnected] = useState(false); useEffect(() => { - const ws = new TestHarnessWebSocket('ws://localhost:8001', setConnected); + const ws = new TestHarnessWebSocket( + 'ws://localhost:8001', + capabilities, + newSdkClientEntity, + setConnected, + ); ws.connect(); return () => ws.disconnect(); }, []); 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..02667bf4f6 100644 --- a/packages/sdk/react-native/contract-tests/entity/metro.config.js +++ b/packages/sdk/react-native/contract-tests/entity/metro.config.js @@ -5,6 +5,7 @@ */ const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); +const fs = require('fs'); // Find the project and workspace directories const projectRoot = __dirname; @@ -22,5 +23,24 @@ 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 for subpath imports +config.resolver.unstable_enablePackageExports = true; +// 5. Handle TypeScript .js extension convention: when a .js import is not found, +// try resolving the .ts equivalent (needed for workspace packages using ESM imports) +const originalResolveRequest = config.resolver.resolveRequest; +config.resolver.resolveRequest = (context, moduleName, platform) => { + if (moduleName.startsWith('.') && moduleName.endsWith('.js')) { + const tsName = moduleName.replace(/\.js$/, '.ts'); + const fromDir = path.dirname(context.originModulePath); + const tsPath = path.resolve(fromDir, tsName); + if (fs.existsSync(tsPath)) { + return context.resolveRequest(context, tsName, platform); + } + } + if (originalResolveRequest) { + return originalResolveRequest(context, moduleName, platform); + } + return context.resolveRequest(context, moduleName, platform); +}; 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..c78d035358 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, + ValueType, +} from '@launchdarkly/js-contract-test-utils'; +import { ClientSideTestHook as TestHook } 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/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/server-node/contract-tests/package.json b/packages/sdk/server-node/contract-tests/package.json index 2b00e78f3f..a239d4c665 100644 --- a/packages/sdk/server-node/contract-tests/package.json +++ b/packages/sdk/server-node/contract-tests/package.json @@ -4,7 +4,7 @@ "main": "dist/src/index.js", "scripts": { "start": "node --inspect dist/src/index.js", - "build": "tsc", + "build": "yarn workspace @launchdarkly/js-contract-test-utils build:server && tsc", "dev": "tsc --watch" }, "type": "module", @@ -12,6 +12,7 @@ "license": "Apache-2.0", "private": true, "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:^", "@launchdarkly/node-server-sdk": "workspace:^", "body-parser": "^1.19.0", "express": "^4.17.1", diff --git a/packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts b/packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts index 2c061f05f3..406f7b1a86 100644 --- a/packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts +++ b/packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts @@ -1,5 +1,6 @@ import got from 'got'; +import { ServerSideTestHook as TestHook } from '@launchdarkly/js-contract-test-utils/server'; import ld, { createMigration, DataSourceOptions, @@ -20,7 +21,6 @@ import ld, { import BigSegmentTestStore from './BigSegmentTestStore.js'; import { Log, sdkLogger } from './log.js'; -import TestHook from './TestHook.js'; const badCommandError = new Error('unsupported command'); export { badCommandError }; diff --git a/packages/sdk/shopify-oxygen/contract-tests/package.json b/packages/sdk/shopify-oxygen/contract-tests/package.json index 30d72f702e..f2124f2020 100644 --- a/packages/sdk/shopify-oxygen/contract-tests/package.json +++ b/packages/sdk/shopify-oxygen/contract-tests/package.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "private": true, "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:^", "@launchdarkly/js-server-sdk-common": "workspace:^", "@launchdarkly/shopify-oxygen-sdk": "workspace:^", "express": "^5.0.1" diff --git a/packages/sdk/shopify-oxygen/contract-tests/src/utils/clientPool.ts b/packages/sdk/shopify-oxygen/contract-tests/src/utils/clientPool.ts index f509631aee..bf61f76b5f 100644 --- a/packages/sdk/shopify-oxygen/contract-tests/src/utils/clientPool.ts +++ b/packages/sdk/shopify-oxygen/contract-tests/src/utils/clientPool.ts @@ -1,42 +1,23 @@ import { Response } from 'express'; +import { ClientPool as GenericClientPool } from '@launchdarkly/js-contract-test-utils'; import { LDClient } from '@launchdarkly/js-server-sdk-common'; import { init } from '@launchdarkly/shopify-oxygen-sdk'; /* eslint-disable no-console */ -// NOTE: Currently, this is a very simple client pool that only really handles the -// very limited Oxygen specific use cases... we should be expand this to be more -// general purpose in the future and maybe even come up with some shared ts interface -// to facilitate future contract testing. - -// TODO: currently this class will handle the response sending as well, which may technically -// sit outside the scope of what it SHOULD be doing. We should refactor this to be more -// general purpose and allow the caller to handle the response sending. - /** - * ClientPool is a singleton that manages a pool of LDClient instances. Currently there is - * no separation between a managed client and this pool. Which means all of the client specs - * will be implemented in this class. + * ClientPool manages a pool of LDClient instances for Shopify Oxygen contract tests. + * It uses the shared generic ClientPool for client storage and ID generation, and + * handles the Oxygen-specific client creation, command execution, and response sending. * * @see https://github.com/launchdarkly/sdk-test-harness/blob/v2/docs/service_spec.md */ export default class ClientPool { - private _clients: Record = {}; - private _clientCounter = 0; - - constructor() { - this._clients = {}; - this._clientCounter = 0; - } - - private _makeId(): string { - this._clientCounter += 1; - return `client-${this._clientCounter}`; - } + private _pool = new GenericClientPool(); public async runCommand(id: string, body: any, res: Response): Promise { - const client = this._clients[id]; + const client = this._pool.get(id); // TODO: handle the 'itCanFailCase' if (client) { try { @@ -69,10 +50,10 @@ export default class ClientPool { } public async deleteClient(id: string, res: Response): Promise { - const client = this._clients[id]; + const client = this._pool.get(id); if (client) { client.close(); - delete this._clients[id]; + this._pool.remove(id); res.status(204); res.send(); } else { @@ -83,7 +64,7 @@ export default class ClientPool { public async createClient(options: any, res: Response): Promise { try { - const id = this._makeId(); + const id = this._pool.nextId(); const { configuration: { credential = 'unknown-sdk-key', polling }, } = options; @@ -101,7 +82,7 @@ export default class ClientPool { }); await client.waitForInitialization({ timeout: 10 }); - this._clients[id] = client; + this._pool.add(id, client); res.status(201); res.set('Location', `/clients/${id}`); if (!client.initialized()) { diff --git a/packages/sdk/shopify-oxygen/contract-tests/tsup.config.ts b/packages/sdk/shopify-oxygen/contract-tests/tsup.config.ts index 0758f21c40..300be1bd30 100644 --- a/packages/sdk/shopify-oxygen/contract-tests/tsup.config.ts +++ b/packages/sdk/shopify-oxygen/contract-tests/tsup.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ entry: { index: 'src/index.ts', }, + noExternal: ['@launchdarkly/js-contract-test-utils'], minify: true, format: ['esm', 'cjs'], splitting: false, diff --git a/packages/tooling/contract-test-utils/README.md b/packages/tooling/contract-test-utils/README.md new file mode 100644 index 0000000000..c3236266cc --- /dev/null +++ b/packages/tooling/contract-test-utils/README.md @@ -0,0 +1,228 @@ +# @launchdarkly/js-contract-test-utils + +Shared utilities for LaunchDarkly JavaScript SDK contract tests. This package consolidates duplicated contract test code across the browser, React Native, Electron, server-node, and Shopify Oxygen SDKs. + +This is a **private** package (not published to npm) used only within this monorepo. + +## CLI + +The package provides a CLI executable for running contract test infrastructure: + +```bash +# Full name +sdk-testharness-server + +# Alias +sts +``` + +### Commands + +#### `adapter` + +Starts the REST-to-WebSocket adapter server. This bridges the HTTP REST-based test harness protocol to a WebSocket connection for browser-like environments (browser, React Native, etc.). + +The adapter runs two servers: +1. A **WebSocket server** (default port `8001`) that the entity (browser/RN app) connects to +2. An **HTTP REST server** (default port `8000`) that the test harness sends commands to + +```bash +sdk-testharness-server adapter +``` + +### Configuration + +The CLI loads configuration from a `contract-test.config` file in the current working directory. Supported formats (searched in this order): + +- `contract-test.config.json` +- `contract-test.config.js` +- `contract-test.config.mjs` +- `contract-test.config.cjs` +- `contract-test.config.ts` (requires Node.js >= 22 or a loader like `tsx`) +- `contract-test.config.mts` + +If no config file is found, defaults are used. + +**Example `contract-test.config.json`:** + +```json +{ + "adapter": { + "wsPort": 8001, + "httpPort": 8000 + } +} +``` + +**Example typed config (`contract-test.config.ts`):** + +```ts +import type { ContractTestConfig } from '@launchdarkly/js-contract-test-utils/adapter'; + +const config: ContractTestConfig = { + adapter: { + wsPort: 8001, + httpPort: 8000, + }, +}; + +export default config; +``` + +## 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`, `TestHarnessWebSocket` | Source `.ts` (for bundlers) | +| `@launchdarkly/js-contract-test-utils/server` | Server-side `TestHook` | Compiled `dist/` (for Node.js) | +| `@launchdarkly/js-contract-test-utils/adapter` | `startAdapter()`, `ContractTestConfig` type | Compiled `dist/` (for Node.js) | + +### 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, + TestHarnessWebSocket, +} from '@launchdarkly/js-contract-test-utils/client'; +``` + +- **`TestHarnessWebSocket`** -- Manages the WebSocket dispatch loop between the adapter and the entity. Constructed with a URL, capabilities list, and a factory function for creating client entities. +- **`ClientSideTestHook`** -- Hook implementation using `fetch()` to report hook execution data back to the test harness. + +### Server-side (`"./server"`) + +For server-node and Shopify Oxygen contract tests. Includes all universal exports plus: + +```ts +import { + ServerSideTestHook, + ClientPool, +} from '@launchdarkly/js-contract-test-utils/server'; +``` + +- **`ServerSideTestHook`** -- Hook implementation using `got` for HTTP requests. +- **`ClientPool`** -- Generic pool for managing client entity lifecycles (`add`, `get`, `remove`, `nextId`). + +### Adapter (`"./adapter"`) + +For the REST-to-WebSocket bridge used by browser and React Native adapters: + +```ts +import { startAdapter } from '@launchdarkly/js-contract-test-utils/adapter'; +import type { ContractTestConfig, AdapterOptions } from '@launchdarkly/js-contract-test-utils/adapter'; +``` + +## Build Scripts + +```bash +# Full build (all outputs) +yarn build + +# Server-side only (for server-node contract tests) +yarn build:server + +# Adapter + CLI only (for browser/RN adapter packages) +yarn build:adapter +``` + +## Usage in Adapter Packages + +Browser and React Native adapter packages delegate entirely to the CLI: + +```json +{ + "scripts": { + "build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter", + "start": "yarn build && sdk-testharness-server adapter" + }, + "dependencies": { + "@launchdarkly/js-contract-test-utils": "workspace:*" + } +} +``` + +No local source code or TypeScript configuration is needed -- the adapter package is just a thin wrapper that invokes the shared CLI. + +## Usage in Entity Packages + +### Browser / React Native / Electron (client-side) + +```ts +import { + TestHarnessWebSocket, + ClientSideTestHook, + CommandParams, + makeLogger, +} from '@launchdarkly/js-contract-test-utils/client'; + +const ws = new TestHarnessWebSocket( + 'ws://localhost:8001', + ['client-side', 'mobile', 'service-endpoints', /* ... */], + async (config) => createClientEntity(config), +); +ws.connect(); +``` + +### Server-node / Shopify Oxygen (server-side) + +```ts +import { + ServerSideTestHook, + ClientPool, + makeLogger, +} from '@launchdarkly/js-contract-test-utils/server'; + +const pool = new ClientPool(); +const id = pool.nextId(); +pool.add(id, entity); +``` + +## Architecture + +``` +contract-test-utils/ + src/ + bin/ + sdk-testharness-server.ts # CLI entry point + loadConfig.ts # Config file loader + adapter/ + startAdapter.ts # REST-to-WebSocket bridge + client-side/ + TestHook.ts # fetch()-based hook reporting + TestHarnessWebSocket.ts # WebSocket dispatch loop + server-side/ + TestHook.ts # got-based hook reporting + ClientPool.ts # Generic entity pool + logging/ + makeLogger.ts # Logger factory + types/ + CommandParams.ts # Command/response type definitions + ConfigParams.ts # SDK configuration type definitions + ContractTestConfig.ts # CLI config file type + compat.ts # Minimal cross-SDK type aliases + index.ts # Universal exports + client.ts # Client-side exports + server.ts # Server-side exports + adapter.ts # Adapter 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..ed867efa0a --- /dev/null +++ b/packages/tooling/contract-test-utils/package.json @@ -0,0 +1,69 @@ +{ + "name": "@launchdarkly/js-contract-test-utils", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "sdk-testharness-server": "./dist/bin/sdk-testharness-server.js", + "sts": "./dist/bin/sdk-testharness-server.js" + }, + "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" + }, + "./server": { + "types": "./dist/server.d.ts", + "import": "./dist/server.js", + "default": "./dist/server.js" + }, + "./adapter": { + "types": "./dist/adapter.d.ts", + "import": "./dist/adapter.js", + "default": "./dist/adapter.js" + } + }, + "typesVersions": { + "*": { + "client": [ + "./src/client.ts" + ], + "server": [ + "./dist/server.d.ts" + ], + "adapter": [ + "./dist/adapter.d.ts" + ] + } + }, + "scripts": { + "build": "tsc", + "build:server": "tsc -p tsconfig.server.json", + "build:adapter": "tsc -p tsconfig.adapter.json && chmod +x dist/bin/sdk-testharness-server.js", + "clean": "rimraf dist" + }, + "dependencies": { + "@launchdarkly/js-client-sdk-common": "workspace:^", + "@launchdarkly/node-server-sdk": "workspace:^", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "got": "14.4.7", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^18.11.9", + "@types/ws": "^8.5.12", + "typescript": "^4.9.0" + } +} diff --git a/packages/tooling/contract-test-utils/src/adapter.ts b/packages/tooling/contract-test-utils/src/adapter.ts new file mode 100644 index 0000000000..5aabfa2442 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/adapter.ts @@ -0,0 +1,4 @@ +// Adapter exports (Node.js REST-to-WebSocket bridge for browser-like contract tests) +export { startAdapter } from './adapter/startAdapter.js'; +export type { AdapterOptions } from './adapter/startAdapter.js'; +export type { ContractTestConfig } from './types/ContractTestConfig.js'; diff --git a/packages/sdk/browser/contract-tests/adapter/src/index.ts b/packages/tooling/contract-test-utils/src/adapter/startAdapter.ts similarity index 72% rename from packages/sdk/browser/contract-tests/adapter/src/index.ts rename to packages/tooling/contract-test-utils/src/adapter/startAdapter.ts index 5dfffa3bd8..d3673ed8d1 100644 --- a/packages/sdk/browser/contract-tests/adapter/src/index.ts +++ b/packages/tooling/contract-test-utils/src/adapter/startAdapter.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ - /* eslint-disable @typescript-eslint/no-explicit-any */ import bodyParser from 'body-parser'; import cors from 'cors'; @@ -9,10 +8,32 @@ import http from 'node:http'; import util from 'node:util'; import { WebSocketServer } from 'ws'; -let server: http.Server | undefined; +export interface AdapterOptions { + /** Port for the WebSocket server that connects to the entity. Defaults to 8001. */ + wsPort?: number; + /** Port for the HTTP REST server that the test harness connects to. Defaults to 8000. */ + httpPort?: number; +} -async function main() { - const wss = new WebSocketServer({ port: 8001 }); +/** + * Starts the contract test adapter that bridges the REST-based test harness + * protocol to a WebSocket connection for browser-like environments (browser, + * React Native, etc.). + * + * The adapter runs two servers: + * 1. A WebSocket server that the entity (browser/RN app) connects to + * 2. An Express HTTP server that the test harness sends REST commands to + * + * Commands from the test harness are forwarded over the WebSocket to the entity, + * and responses are relayed back. + */ +export function startAdapter(options?: AdapterOptions): void { + const wsPort = options?.wsPort ?? 8001; + const httpPort = options?.httpPort ?? 8000; + + let server: http.Server | undefined; + + const wss = new WebSocketServer({ port: wsPort }); const waiters: Record void> = {}; console.log('Running contract test harness adapter.'); @@ -47,8 +68,6 @@ async function main() { const app = express(); - const port = 8000; - app.use( cors({ origin: '*', @@ -104,9 +123,8 @@ async function main() { res.send(); }); - server = app.listen(port, () => { - console.log('Listening on port %d', port); + server = app.listen(httpPort, () => { + console.log('Listening on port %d', httpPort); }); }); } -main(); diff --git a/packages/tooling/contract-test-utils/src/bin/loadConfig.ts b/packages/tooling/contract-test-utils/src/bin/loadConfig.ts new file mode 100644 index 0000000000..f0c29e6351 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/bin/loadConfig.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ +import fs from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import type { ContractTestConfig } from '../types/ContractTestConfig.js'; + +const CONFIG_BASE = 'contract-test.config'; +const EXTENSIONS = ['.json', '.js', '.mjs', '.cjs', '.ts', '.mts']; + +/** + * Loads a contract test configuration file from the current working directory. + * Searches for `contract-test.config.{json,js,mjs,cjs,ts,mts}` in order. + * + * - `.json` files are parsed with JSON.parse + * - `.js`, `.mjs`, `.cjs`, `.ts`, `.mts` files are loaded via dynamic import + * (TypeScript files require Node.js >= 22 or a loader like tsx) + * + * If no config file is found, returns an empty config (defaults will be used). + */ +export async function loadConfig(cwd: string = process.cwd()): Promise { + for (const ext of EXTENSIONS) { + const filePath = path.join(cwd, `${CONFIG_BASE}${ext}`); + if (!fs.existsSync(filePath)) { + continue; + } + + console.log(`Loading config from ${CONFIG_BASE}${ext}`); + + if (ext === '.json') { + const content = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(content) as ContractTestConfig; + } + + // For JS/TS files, use dynamic import + const module = await import(pathToFileURL(filePath).href); + return (module.default ?? module) as ContractTestConfig; + } + + // No config file found — use defaults + return {}; +} diff --git a/packages/tooling/contract-test-utils/src/bin/sdk-testharness-server.ts b/packages/tooling/contract-test-utils/src/bin/sdk-testharness-server.ts new file mode 100644 index 0000000000..b5570b6888 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/bin/sdk-testharness-server.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +import { startAdapter } from '../adapter/startAdapter.js'; +import { loadConfig } from './loadConfig.js'; + +const COMMANDS = ['adapter'] as const; +type Command = (typeof COMMANDS)[number]; + +async function main() { + const args = process.argv.slice(2); + const command = args[0] as Command | undefined; + + if (!command || !COMMANDS.includes(command)) { + console.error('Usage: sdk-testharness-server '); + console.error(`Commands: ${COMMANDS.join(', ')}`); + process.exit(1); + } + + const config = await loadConfig(); + + switch (command) { + case 'adapter': + startAdapter(config.adapter); + break; + default: + console.error(`Unknown command: ${command}`); + process.exit(1); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts b/packages/tooling/contract-test-utils/src/client-side/TestHarnessWebSocket.ts similarity index 81% rename from packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts rename to packages/tooling/contract-test-utils/src/client-side/TestHarnessWebSocket.ts index 4bc88606a4..26b82412e1 100644 --- a/packages/sdk/react-native/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/tooling/contract-test-utils/src/client-side/TestHarnessWebSocket.ts @@ -1,27 +1,39 @@ -import { LDLogger } from '@launchdarkly/react-native-client-sdk'; +import { LDLogger } from '../types/compat.js'; -import { ClientEntity, newSdkClientEntity } from './ClientEntity'; -import { makeLogger } from './makeLogger'; +import { makeLogger } from '../logging/makeLogger.js'; + +export interface ClientEntity { + close: () => void; + doCommand: (params: any) => Promise; +} + +export type CreateClientEntityFn = (options: any) => Promise; export default class TestHarnessWebSocket { private _ws?: WebSocket; private readonly _entities: Record = {}; private _clientCounter = 0; private _logger: LDLogger = makeLogger('TestHarnessWebSocket'); + private _capabilities: string[]; + private _createClientEntity: CreateClientEntityFn; private _intentionalClose = false; private _onConnectionChange?: (connected: boolean) => void; constructor( private readonly _url: string, + capabilities: string[], + createClientEntity: CreateClientEntityFn, onConnectionChange?: (connected: boolean) => void, ) { + this._capabilities = capabilities; + this._createClientEntity = createClientEntity; this._onConnectionChange = onConnectionChange; } connect() { this._intentionalClose = false; this._logger.info(`Connecting to web socket.`); - this._ws = new WebSocket(this._url, 'v1'); + this._ws = new WebSocket(this._url, ['v1']); this._ws.onopen = () => { this._logger.info('Connected to websocket.'); this._onConnectionChange?.(true); @@ -45,26 +57,14 @@ export default class TestHarnessWebSocket { const resData: any = { reqId: data.reqId }; switch (data.command) { case 'getCapabilities': - resData.capabilities = [ - 'client-side', - 'mobile', - 'service-endpoints', - 'tags', - 'user-type', - 'inline-context-all', - 'anonymous-redaction', - 'strongly-typed', - 'client-prereq-events', - 'client-per-context-summaries', - 'track-hooks', - ]; + resData.capabilities = this._capabilities; break; case 'createClient': try { resData.resourceUrl = `/clients/${this._clientCounter}`; resData.status = 201; - const entity = await newSdkClientEntity(data.body); + const entity = await this._createClientEntity(data.body); this._entities[this._clientCounter] = entity; this._clientCounter += 1; } catch (e: any) { 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..183362f372 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/client.ts @@ -0,0 +1,11 @@ +// Re-export universal exports +export * from './index.js'; + +// Client-side exports +export { default as ClientSideTestHook } from './client-side/TestHook.js'; +export type { + HookData as ClientSideHookData, + HookErrors as ClientSideHookErrors, +} from './client-side/TestHook.js'; +export { default as TestHarnessWebSocket } from './client-side/TestHarnessWebSocket.js'; +export type { ClientEntity, CreateClientEntityFn } from './client-side/TestHarnessWebSocket.js'; 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..7a27da4a11 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/index.ts @@ -0,0 +1,21 @@ +// Universal exports (no SDK dependency) +export * from './types/CommandParams.js'; +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, + type HookStage as ConfigHookStage, +} from './types/ConfigParams.js'; +export { makeLogger } from './logging/makeLogger.js'; +export { ClientPool } from './server-side/ClientPool.js'; diff --git a/packages/sdk/browser/contract-tests/entity/src/makeLogger.ts b/packages/tooling/contract-test-utils/src/logging/makeLogger.ts similarity index 93% rename from packages/sdk/browser/contract-tests/entity/src/makeLogger.ts rename to packages/tooling/contract-test-utils/src/logging/makeLogger.ts index a8cf9f165d..65954ad83f 100644 --- a/packages/sdk/browser/contract-tests/entity/src/makeLogger.ts +++ b/packages/tooling/contract-test-utils/src/logging/makeLogger.ts @@ -1,4 +1,4 @@ -import { LDLogger } from '@launchdarkly/js-client-sdk'; +import { LDLogger } from '../types/compat.js'; 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..864bb90ce8 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/server-side/ClientPool.ts @@ -0,0 +1,60 @@ +/** + * 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 { + this._clientCounter += 1; + return this._clientCounter.toString(); + } + + /** + * Add a client entity to the pool with the given ID. + * @param id - The unique identifier for the client. + * @param client - The client entity to store. + */ + add(id: string, client: T): void { + this._clients[id] = client; + } + + /** + * 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/server-node/contract-tests/src/TestHook.ts b/packages/tooling/contract-test-utils/src/server-side/TestHook.ts similarity index 100% rename from packages/sdk/server-node/contract-tests/src/TestHook.ts rename to packages/tooling/contract-test-utils/src/server-side/TestHook.ts diff --git a/packages/tooling/contract-test-utils/src/server.ts b/packages/tooling/contract-test-utils/src/server.ts new file mode 100644 index 0000000000..3b2a7c9893 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/server.ts @@ -0,0 +1,9 @@ +// Re-export universal exports (includes ClientPool) +export * from './index.js'; + +// Server-side exports +export { default as ServerSideTestHook } from './server-side/TestHook.js'; +export type { + HookData as ServerSideHookData, + HookErrors as ServerSideHookErrors, +} from './server-side/TestHook.js'; diff --git a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts b/packages/tooling/contract-test-utils/src/types/CommandParams.ts similarity index 97% rename from packages/sdk/browser/contract-tests/entity/src/CommandParams.ts rename to packages/tooling/contract-test-utils/src/types/CommandParams.ts index 11251d31ed..04dba0194b 100644 --- a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts +++ b/packages/tooling/contract-test-utils/src/types/CommandParams.ts @@ -1,4 +1,4 @@ -import { LDContext, LDEvaluationReason } from '@launchdarkly/js-client-sdk'; +import { LDContext, LDEvaluationReason } from './compat.js'; export enum CommandType { EvaluateFlag = 'evaluate', diff --git a/packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts b/packages/tooling/contract-test-utils/src/types/ConfigParams.ts similarity index 94% rename from packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts rename to packages/tooling/contract-test-utils/src/types/ConfigParams.ts index 73a7917a98..3225565057 100644 --- a/packages/sdk/electron/contract-tests/entity/src/ConfigParams.ts +++ b/packages/tooling/contract-test-utils/src/types/ConfigParams.ts @@ -1,5 +1,4 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { LDContext } from '@launchdarkly/electron-client-sdk'; +import { LDContext } from './compat.js'; export interface CreateInstanceParams { configuration: SDKConfigParams; diff --git a/packages/tooling/contract-test-utils/src/types/ContractTestConfig.ts b/packages/tooling/contract-test-utils/src/types/ContractTestConfig.ts new file mode 100644 index 0000000000..39291ff06c --- /dev/null +++ b/packages/tooling/contract-test-utils/src/types/ContractTestConfig.ts @@ -0,0 +1,10 @@ +import type { AdapterOptions } from '../adapter/startAdapter.js'; + +/** + * Configuration for the contract test tooling. + * Can be specified in a `contract-test.config.{json,js,mjs,ts,mts}` file. + */ +export interface ContractTestConfig { + /** Configuration for the adapter command. */ + adapter?: AdapterOptions; +} diff --git a/packages/tooling/contract-test-utils/src/types/compat.ts b/packages/tooling/contract-test-utils/src/types/compat.ts new file mode 100644 index 0000000000..687e8b0f20 --- /dev/null +++ b/packages/tooling/contract-test-utils/src/types/compat.ts @@ -0,0 +1,32 @@ +/** + * Minimal type definitions for contract test utilities. + * + * These are compatible with the corresponding types from both + * @launchdarkly/js-client-sdk and @launchdarkly/node-server-sdk, + * allowing the shared package to work without depending on either SDK directly. + */ + +/** + * A minimal LDContext type compatible with both client and server SDKs. + * Contract test harness passes context objects through without deep inspection. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type LDContext = Record; + +/** + * A minimal LDLogger interface compatible with both client and server SDKs. + */ +export interface LDLogger { + debug(...args: any[]): void; + info(...args: any[]): void; + warn(...args: any[]): void; + error(...args: any[]): void; +} + +/** + * A minimal LDEvaluationReason type compatible with both client and server SDKs. + */ +export interface LDEvaluationReason { + kind: string; + [key: string]: unknown; +} diff --git a/packages/tooling/contract-test-utils/tsconfig.adapter.json b/packages/tooling/contract-test-utils/tsconfig.adapter.json new file mode 100644 index 0000000000..67f1ca3325 --- /dev/null +++ b/packages/tooling/contract-test-utils/tsconfig.adapter.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/adapter/**/*", "src/adapter.ts", "src/bin/**/*", "src/types/ContractTestConfig.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/tooling/contract-test-utils/tsconfig.json b/packages/tooling/contract-test-utils/tsconfig.json new file mode 100644 index 0000000000..3c47238f16 --- /dev/null +++ b/packages/tooling/contract-test-utils/tsconfig.json @@ -0,0 +1,25 @@ +{ + "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, + "baseUrl": ".", + "paths": { + "got": ["../../../node_modules/got/dist/source"] + } + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/tooling/contract-test-utils/tsconfig.server.json b/packages/tooling/contract-test-utils/tsconfig.server.json new file mode 100644 index 0000000000..8b221e028d --- /dev/null +++ b/packages/tooling/contract-test-utils/tsconfig.server.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "exclude": ["src/client-side/**", "src/client.ts", "dist", "node_modules"] +}