Skip to content

Commit 2de5c27

Browse files
feat: create @launchdarkly/js-contract-test-utils package (#1163)
**Requirements** - [ ] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** - [SDK-2008](https://launchdarkly.atlassian.net/browse/SDK-2008) - Reference branch: `devin/SDK-1866-1772645970` **Describe the solution you've provided** Creates a new **private** package `@launchdarkly/js-contract-test-utils` at `packages/tooling/contract-test-utils/` that consolidates duplicated contract test infrastructure shared across client-side SDKs, and migrates browser, electron, react-native, and React SDK contract tests to consume it. ### Package exports | Import Path | Contents | Resolution | |---|---|---| | `.` | Universal types (`CommandParams`, `ConfigParams`), `makeLogger`, `ClientPool` | Source `.ts` | | `./client` | Re-exports universal + client-side `TestHook` (fetch-based) | Source `.ts` | All types import `LDContext`/`LDEvaluationReason`/`LDLogger` from `@launchdarkly/js-client-sdk-common` for cross-SDK compatibility. ### Migrations included For each of **browser**, **electron**, **react-native**, and **React** contract test entities: - Deleted local `CommandParams.ts`, `ConfigParams.ts`, `makeLogger.ts`, `TestHook.ts` - Updated consuming files (`ClientEntity.ts`, `ClientFactory.ts`, `TestHarnessWebSocket.ts`, `ClientRoot.tsx` as applicable) to import from `@launchdarkly/js-contract-test-utils/client` - Added `"@launchdarkly/js-contract-test-utils": "workspace:^"` dependency ### Review feedback addressed - Converted `CommandType`, `ValueType`, `HookStage` from TypeScript `enum` to `as const` objects with derived union types per repo guidelines - Fixed browser entity dependency versions from `"*"` to `"workspace:^"` - Consolidated duplicate `HookStage` definition (was in both `CommandParams.ts` and `ConfigParams.ts`, now only in `CommandParams.ts`) - Removed `.js` extensions from shared package internal imports for Next.js Turbopack compatibility (React SDK uses `next build` with Turbopack, which resolves from `.ts` source and can't find `.js` targets) - Enabled `unstable_enablePackageExports` in react-native entity's `metro.config.js` so Metro can resolve the `./client` subpath export - Fixed `ClientPool.nextId()` off-by-one: now returns current counter value before incrementing, so the first ID is `"0"` (matching existing consumer patterns) - Simplified `ClientPool.add()` API: now calls `nextId()` internally and returns the assigned ID (`add(client: T): string` instead of requiring the caller to pass an ID) **Human review checklist** - [ ] **README example is stale**: `packages/tooling/contract-test-utils/README.md` shows old `ClientPool` usage (`pool.nextId()` then `pool.add(id, entity)`) — should be updated to use the new `pool.add(entity)` API that returns the ID - [ ] **`as const` type compatibility**: Verify `CommandType`, `ValueType`, and `HookStage` union types derived from `as const` objects are assignable everywhere the old `enum`/`type` union was used (especially in switch statements and type narrowing) - [ ] **Metro `unstable_enablePackageExports`**: Confirm this doesn't cause unexpected resolution changes for other dependencies in the react-native entity **Items for reviewer attention** 1. **No unit tests** — This is test infrastructure itself (test coverage checkbox unchecked). Confirm whether tests are expected at this stage. 2. **Source-only resolution** — All exports resolve to `.ts` source files (no compiled `dist/`). Internal imports use extensionless paths for compatibility with both bundlers (Vite, esbuild, Metro) and Next.js Turbopack. If a future consumer requires compiled ESM output, `.js` extensions would need to be added back (per Node.js ESM spec), but current consumers work correctly with source-only resolution. 3. **Server-side migration not included** — Server-node and Shopify Oxygen contract tests are not migrated in this PR (deferred to follow-up per earlier discussion). The `ClientPool` utility is exported but not yet consumed. 4. **Metro bundler configuration** — React Native's Metro bundler doesn't support `package.json` `exports` field by default. Enabled `unstable_enablePackageExports` in the react-native entity's `metro.config.js` to resolve `@launchdarkly/js-contract-test-utils/client`. This flag is widely supported in Metro 0.81+ but remains marked "unstable" - please verify it doesn't cause unexpected issues with other package resolutions. **Describe alternatives you've considered** - Considered using plain string union types (`type CommandType = 'evaluate' | 'evaluateAll' | ...`) but this would break the `CommandType.EvaluateFlag` access pattern used throughout all consumer files. The `as const` object pattern preserves both the named access and the string union type. - Considered importing from SDK-specific packages (e.g. `@launchdarkly/electron-client-sdk`) but this would create circular dependencies. Using `@launchdarkly/js-client-sdk-common` keeps the shared package independent. - Considered keeping `.js` extensions in imports (per Node.js ESM spec) but Turbopack cannot resolve these when consuming TypeScript source. Extensionless imports work for all current consumers (bundlers + Turbopack) and are acceptable since the package is private and source-only. - Considered adding fallback `main` field to package.json for Metro compatibility, but enabling `unstable_enablePackageExports` is the recommended approach and cleaner long-term. **Additional context** - All 4 migrated SDK contract tests compile and pass lint/prettier checks - Package uses workspace protocol (`workspace:^`) for monorepo dependencies - No bundle size changes (contract test code is not included in SDK bundles) - All 43/43 CI checks pass **Updates since last revision** - Migrated React SDK contract tests (deleted 4 local files, updated imports in `ClientEntity.ts`, `ClientRoot.tsx`, `TestHarnessWebSocket.ts`) - Fixed Turbopack module resolution by removing `.js` extensions from shared package internal imports - Fixed Metro bundler resolution for react-native by enabling `unstable_enablePackageExports` in `metro.config.js` - Fixed `ClientPool.nextId()` off-by-one bug to match existing consumer patterns (first ID is now `"0"` instead of `"1"`) - Simplified `ClientPool.add()` to internally call `nextId()` and return the assigned ID (API changed from `add(id: string, client: T): void` to `add(client: T): string`) - All CI checks now pass (43/43) --- **Link to Devin Session:** https://app.devin.ai/sessions/f541db4796ce485a892086b8ea79bf95 **Requested by:** @joker23 [SDK-2008]: https://launchdarkly.atlassian.net/browse/SDK-2008?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches multiple contract-test runners and introduces package `exports`/Metro resolution changes; low production impact but moderate risk of breaking test builds across platforms due to module-resolution and shared type changes. > > **Overview** > Introduces a new **private** workspace package, `@launchdarkly/js-contract-test-utils`, to centralize shared contract-test infrastructure (common command/config types, `makeLogger`, a fetch-based client-side `TestHook`, and a generic `ClientPool`), using `@launchdarkly/js-client-sdk-common` for cross-SDK type compatibility and exposing a `./client` subpath export. > > Migrates the browser, Electron, React Native, and React contract-test entities to consume these shared exports, deleting their duplicated local type/logger/hook files, updating imports/dependencies accordingly, and enabling Metro `unstable_enablePackageExports` so React Native can resolve the `./client` subpath. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2ab4f54. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent bbdd6c6 commit 2de5c27

37 files changed

Lines changed: 307 additions & 1149 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"packages/store/node-server-sdk-redis",
3838
"packages/store/node-server-sdk-dynamodb",
3939
"packages/telemetry/node-server-sdk-otel",
40+
"packages/tooling/contract-test-utils",
4041
"packages/tooling/jest",
4142
"packages/tooling/jest/example/react-native-example",
4243
"packages/sdk/browser",

packages/sdk/browser/contract-tests/entity/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
1313
},
1414
"dependencies": {
15-
"@launchdarkly/js-client-sdk": "*"
15+
"@launchdarkly/js-client-sdk": "workspace:^",
16+
"@launchdarkly/js-contract-test-utils": "workspace:^"
1617
},
1718
"devDependencies": {
1819
"@trivago/prettier-plugin-sort-imports": "^4.1.1",

packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk';
2-
3-
import { CommandParams, CommandType, ValueType } from './CommandParams';
4-
import { CreateInstanceParams, SDKConfigParams } from './ConfigParams';
5-
import { makeLogger } from './makeLogger';
6-
import TestHook from './TestHook';
2+
import {
3+
CommandParams,
4+
CommandType,
5+
CreateInstanceParams,
6+
makeLogger,
7+
SDKConfigParams,
8+
ClientSideTestHook as TestHook,
9+
ValueType,
10+
} from '@launchdarkly/js-contract-test-utils/client';
711

812
export const badCommandError = new Error('unsupported command');
913
export const malformedCommand = new Error('command was malformed');

packages/sdk/browser/contract-tests/entity/src/CommandParams.ts

Lines changed: 0 additions & 157 deletions
This file was deleted.

packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LDLogger } from '@launchdarkly/js-client-sdk';
2+
import { makeLogger } from '@launchdarkly/js-contract-test-utils/client';
23

34
import { ClientEntity, newSdkClientEntity } from './ClientEntity';
4-
import { makeLogger } from './makeLogger';
55

66
export default class TestHarnessWebSocket {
77
private _ws?: WebSocket;

packages/sdk/browser/contract-tests/entity/src/makeLogger.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/sdk/electron/contract-tests/entity/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"vite": "^5.4.21"
3333
},
3434
"dependencies": {
35+
"@launchdarkly/js-contract-test-utils": "workspace:^",
3536
"body-parser": "^2.2.2",
3637
"electron-squirrel-startup": "^1.0.1",
3738
"express": "^5.2.1"

packages/sdk/electron/contract-tests/entity/src/ClientEntity.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import path from 'node:path';
66

77
// eslint-disable-next-line import/no-extraneous-dependencies
88
import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/electron-client-sdk';
9-
10-
import { CommandParams, CommandType, ValueType } from './CommandParams';
11-
import { CreateInstanceParams, SDKConfigParams } from './ConfigParams';
12-
import { makeLogger } from './makeLogger';
13-
import TestHook from './TestHook';
9+
import {
10+
CommandParams,
11+
CommandType,
12+
CreateInstanceParams,
13+
makeLogger,
14+
SDKConfigParams,
15+
ClientSideTestHook as TestHook,
16+
ValueType,
17+
} from '@launchdarkly/js-contract-test-utils/client';
1418

1519
export const badCommandError = new Error('unsupported command');
1620
export const malformedCommand = new Error('command was malformed');

packages/sdk/electron/contract-tests/entity/src/ClientFactory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { CommandParams, CreateInstanceParams } from '@launchdarkly/js-contract-test-utils/client';
2+
13
import { ClientEntity, createEntity } from './ClientEntity';
2-
import { CommandParams } from './CommandParams';
3-
import { CreateInstanceParams } from './ConfigParams';
44

55
export default class ClientFactory {
66
private _clientCounter = 0;

0 commit comments

Comments
 (0)