Skip to content

feat: add server-side TestHook and migrate server-node/shopify-oxygen to shared utils [SDK-2009]#1168

Draft
joker23 wants to merge 20 commits intomainfrom
devin/1773165219-sdk-2009-server-testhook-migrations
Draft

feat: add server-side TestHook and migrate server-node/shopify-oxygen to shared utils [SDK-2009]#1168
joker23 wants to merge 20 commits intomainfrom
devin/1773165219-sdk-2009-server-testhook-migrations

Conversation

@joker23
Copy link
Contributor

@joker23 joker23 commented Mar 10, 2026

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

  • SDK-2009 — Add server-side TestHook and ClientPool to contract-test-utils
  • Subtask 2 of SDK-1866

Describe the solution you've provided

Moves the server-node TestHook (which uses got for HTTP callbacks) into the shared @launchdarkly/js-contract-test-utils package and migrates both server-node and Shopify Oxygen contract tests to use shared infrastructure. Also deduplicates shared types (command params, config params) so both client and server contract tests consume them from the shared package.

Changes to packages/tooling/contract-test-utils/:

  • src/server-side/TestHook.ts — moved from server-node contract tests (exact rename, no content changes)
  • src/server.ts — barrel re-exporting from ./index.js plus ServerSideTestHook (mirrors client.ts pattern)
  • src/client.ts / src/index.ts — barrel exports using .js extensions (required for compiled ESM output)
  • src/types/compat.ts — minimal LDContext, LDEvaluationReason, and LDLogger type definitions compatible with both client and server SDKs, allowing shared code to avoid importing from either SDK directly
  • src/types/CommandParams.ts — imports from compat.ts; includes both client and server command fields (e.g. migrationVariation, registerFlagChangeListener)
  • src/types/ConfigParams.ts — same pattern; includes both clientSide and server-specific fields (bigSegments, dataSystem)
  • src/logging/makeLogger.ts — now imports LDLogger from compat.ts instead of @launchdarkly/js-client-sdk-common
  • package.json:
    • All three subpath exports (., ./client, ./server) point to compiled dist/ output
    • Three build scripts: build (universal only), build:client (client + universal), build:server (server + universal)
    • got and @launchdarkly/node-server-sdk added as optional peer dependencies

Three-tier TypeScript build configuration:

  • tsconfig.json (base) — excludes both TestHook files and barrel files (client.ts, server.ts). Compiles only universal code with zero SDK dependencies. Used by build script.
  • tsconfig.client.json — extends base, re-includes client files, excludes server files. Requires @launchdarkly/js-client-sdk-common. Used by build:client script.
  • tsconfig.server.json — extends base, re-includes server files, excludes client files. Requires @launchdarkly/node-server-sdk + got. Used by build:server script.

Migrations:

  • server-node:
    • Deleted local TestHook.ts; imports ServerSideTestHook from shared package
    • Replaced inline clientCounter + Record<string, SdkClientEntity> in index.ts with shared ClientPool<SdkClientEntity>
    • Imported CommandParams, CreateInstanceParams, SDKConfigParams from shared package (~130 lines of duplicated local type definitions removed)
    • Added type casts where shared types use unknown or string but SDK expects specific types (e.g., pe.defaultValue as boolean, migrationVariation.context as LDContext, migrationVariation.defaultStage as LDMigrationStage)
  • Shopify Oxygen:
    • Replaced inline Record<string, LDClient> + counter with shared ClientPool<LDClient>
    • Updated JSDoc comment to be more general (not Shopify-specific)
    • Added noExternal to tsup.config.ts to bundle shared package inline
    • Client IDs changed from "client-1" to "1" (auto-generated by ClientPool.add())

CI Workflow Updates:

  • .github/workflows/browser.yml — added build:client step before contract test adapter build
  • .github/workflows/electron.yaml — added build:client step before contract tests entity build
  • .github/workflows/react-native-contract-tests.yml — added build:client step before contract test adapter build
  • .github/workflows/react.yaml — added build:client step before topological build (must run before Next.js/Turbopack resolves imports)
  • .github/workflows/server-node.yml — added build:server step before contract test build
  • .github/workflows/shopify-oxygen.yml — added build step before contract test build

Build command matrix by CI environment:

  • Universal only (build): shopify-oxygen — has no SDK dependencies
  • Client + universal (build:client): browser, electron, react-native, react — have @launchdarkly/js-client-sdk-common but not got/node-server-sdk
  • Server + universal (build:server): server-node — has @launchdarkly/node-server-sdk + got but may lack client SDK

Updates since last revision:

Implemented three-tier TypeScript build configuration to handle different SDK dependency profiles across CI environments:

  1. Split tsconfig.json into three configs: Base config now excludes both client and server TestHook files, making it universal-only (zero SDK dependencies). New tsconfig.client.json and tsconfig.server.json extend base and selectively re-include their respective files.

  2. Added build:client script: Browser/Electron/React-Native/React CI workflows now explicitly run build:client to compile dist/client.js, which was missing when they ran the default build script (universal only).

  3. Shopify Oxygen uses universal build: Changed from build:server to build since Shopify Oxygen only imports from the base package (no subpath), not /server.

  4. Fixed React workflow build ordering: Moved build:client to run before the topological yarn workspaces foreach ... run build because the React contract tests' Next.js/Turbopack build happens during that step and needs dist/client.js to already exist when resolving @launchdarkly/js-contract-test-utils/client imports.

This fixes CI failures where different environments tried to compile code that imported SDKs not installed in their focused dependency trees (e.g., React CI trying to compile server-side TestHook which imports got).

⚠️ Items for reviewer attention:

  1. Compat types pattern: Types use minimal compatible definitions (LDContext = Record<string, any>, LDLogger = { error(...), warn(...), ... }) to avoid importing from SDK packages directly. This allows one unified set of type files to work for both client and server, but requires consumers to add explicit casts when passing values to SDK methods.

  2. Type safety tradeoff: The casts in sdkClientEntity.ts could mask real type errors if the contract test harness sends incorrect types. However, this follows the pattern from PR feat: create shared contract test utilities package (SDK-1866) #1151 which was already merged and validated. Notable casts:

    • pe.defaultValue as boolean | number | string (3 locations)
    • migrationVariation.context as LDContext (1 location)
    • migrationOperation.context as LDContext (2 locations)
    • params.identifyEvent!.context as LDContext (1 location)
    • migrationVariation.defaultStage as LDMigrationStage (3 locations)
  3. Three-tsconfig approach: Each CI environment must use its appropriate build script. Using the wrong script causes "Cannot find module" errors. Future files must be added to the appropriate tsconfig exclusion lists.

  4. CI environment requirements:

    • Browser/Electron/React-Native/React: use build:client (client deps available, server deps missing)
    • Server-node: use build:server (server deps available, client deps may be missing)
    • Shopify-oxygen: use build (universal only, minimal deps)
  5. ClientPool API change: ClientPool.add(client) now auto-generates and returns the client ID instead of requiring an explicit ID. Shopify Oxygen client IDs changed from "client-1" to "1". CI contract tests pass, confirming the test harness handles this correctly.

  6. got path mapping: tsconfig.json uses a hardcoded relative path ("../../../node_modules/got/dist/source") which is fragile and depends on Yarn's hoisting layout. This is a workaround because @launchdarkly/js-client-sdk-common doesn't work with node16 module resolution while got v14 requires it.

  7. React workflow build ordering is critical: The build:client step must run before the topological yarn workspaces foreach ... run build because Next.js/Turbopack resolves the @launchdarkly/js-contract-test-utils/client import during the contract tests build. If dist/client.js doesn't exist yet, Turbopack fails with "Module not found".

Describe alternatives you've considered

  • Three-tier type architecture (universal/client/server split into separate directories) — initially implemented but reverted in favor of the simpler compat.ts pattern from PR feat: create shared contract test utilities package (SDK-1866) #1151, which uses one unified set of type files with minimal compatible type definitions
  • Asymmetric exports (client from source, server from dist) — attempted but couldn't make it work reliably across all CI environments; unified dist/ approach with explicit build steps is more maintainable
  • Two-tsconfig approach (just base + server) — didn't work because client CI environments tried to compile universal files that imported from @launchdarkly/js-client-sdk-common, causing peer dependency warnings and potential issues. Three tiers (universal/client/server) cleanly separates concerns.

Additional context

Link to Devin Session: https://app.devin.ai/sessions/f0a2bd3c755e49c3b639bd3ee770effe
Requested by: @joker23

No test coverage added in this PR — the existing contract test suites serve as integration tests for the shared utilities.

CI Status: ✅ All 41/41 checks passing

… to shared utils [SDK-2009]

- Add ServerSideTestHook using got for HTTP callbacks in contract-test-utils
- Create src/server.ts barrel re-exporting index + ServerSideTestHook
- Add tsconfig.server.json excluding client-side sources
- Add ./server subpath export pointing to compiled dist/server.js
- Add got and @launchdarkly/node-server-sdk as runtime dependencies
- Migrate server-node contract tests to import from shared package
- Migrate Shopify Oxygen to use shared ClientPool<LDClient>
- Update Shopify Oxygen tsup.config.ts with noExternal for shared package

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25566 bytes
Compressed size limit: 26000
Uncompressed size: 125383 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 24534 bytes
Compressed size limit: 25000
Uncompressed size: 84907 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 172466 bytes
Compressed size limit: 200000
Uncompressed size: 802026 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 22062 bytes
Compressed size limit: 24000
Uncompressed size: 114438 bytes

devin-ai-integration bot and others added 12 commits March 10, 2026 18:08
- Change ./server export to point to source .ts files (like ./client)
  so contract-test-utils doesn't need a separate build step in CI
- Restore .js extensions in relative imports for node16 moduleResolution
- server.ts only exports ServerSideTestHook and ClientPool (not index)
  to avoid pulling in client-sdk-common deps in server context

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…to CI

- Revert ./server subpath export to point to compiled dist/server.js
  (source exports don't work at runtime with node16 module resolution)
- Add 'Build shared contract test utils (server)' step to server-node
  CI workflow so dist/ files exist before contract tests build
- server.ts only re-exports ServerSideTestHook and ClientPool to avoid
  pulling in client-sdk-common deps in server context

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
- Only include src/server.ts and src/server-side/* to avoid compiling
  files that depend on @launchdarkly/js-client-sdk-common which isn't
  available in the server-node CI environment

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
These are only needed by the ./server subpath. Making them optional
peer dependencies prevents them from being hoisted into unrelated
workspaces (e.g. React SDK) where @types/cacheable-request conflicts
with TypeScript compilation.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…elated CI

The default 'build' script was running 'tsc' which compiled server-side
files (TestHook.ts) that depend on @launchdarkly/node-server-sdk and got.
When other CI jobs (e.g. React SDK) run 'yarn workspaces foreach ... build',
this compilation fails because those optional peer deps aren't installed.

Since ./ and ./client exports point to source .ts files (no compilation
needed), the build script is now a no-op. Server-side dist files are
produced by 'build:server' which is explicitly called in server-node CI.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…ution

index.ts is consumed as source .ts (not compiled), so .js extensions
break Turbopack module resolution in the React SDK contract tests.
Only server.ts (compiled to dist/) needs .js extensions.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…d ClientPool

- Replace inline clientCounter + Record with shared ClientPool<SdkClientEntity>
- Replace local SdkConfigOptions with shared SDKConfigParams (extended for server)
- Type newSdkClientEntity options with ServerCreateInstanceParams
- Re-export shared config types through ./server barrel for node16 consumers
- Import TestHook, SDKConfigParams, CreateInstanceParams from shared ./server

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…sdk-common dep in CI

The build:server compilation in CI doesn't have @launchdarkly/js-client-sdk-common
available (focused install for server-node only). Reverting type re-exports from
server.ts and keeping server-node config types local. ClientPool deduplication in
index.ts is preserved.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…K dependency isolation

- src/types/ now contains only SDK-independent types (no LDContext/LDEvaluationReason)
- src/client-side/types/ has client-specific types importing from js-client-sdk-common
- src/server-side/types/ has server-specific types importing from js-server-sdk-common
- server.ts barrel re-exports shared + server types (bigSegments, dataSystem, migrations)
- client.ts barrel re-exports shared + client types (SDKConfigClientSideParams)
- index.ts exports only universal types with no SDK dependency
- server-node contract tests now import CommandParams, CreateInstanceParams, SDKConfigParams from shared package
- Added @launchdarkly/js-server-sdk-common as optional peer dependency

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…1151

- Add src/types/compat.ts with minimal LDContext and LDEvaluationReason types
  compatible with both client and server SDKs (no SDK imports needed)
- Merge all types back into unified CommandParams.ts and ConfigParams.ts
  (includes both client-specific and server-specific fields)
- Delete src/client-side/types/ and src/server-side/types/ directories
  (no longer needed with compat types approach)
- Simplify barrel exports: client.ts re-exports from index, server.ts
  re-exports from types/ directly
- Remove @launchdarkly/js-server-sdk-common peer dependency
- Add necessary casts in server-node consumer for SDK-specific types

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
devin-ai-integration bot and others added 7 commits March 11, 2026 15:52
…st/)

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Client-side consumers (browser, electron, react) use bundlers that
resolve .ts source directly - no build step needed. Server-side
consumers (server-node) compile with tsc and run with node - need
compiled .js output. Mixed export strategy is a technical constraint,
not an inconsistency.

server.ts now explicitly lists all exports with .js extensions instead
of re-exporting from ./index.js to avoid pulling in extensionless
imports in the compiled output.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…CI workflows

- build script now runs tsc (compiles client + types + utilities, excludes server files)
- build:server compiles everything including server files
- All three subpath exports (., ./client, ./server) consistently point to dist/
- server.ts now re-exports from ./index.js like client.ts (consistent pattern)
- Added explicit contract-test-utils build steps to browser, electron, shopify-oxygen, and react-native CI workflows

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
- makeLogger.ts now imports LDLogger from compat.ts instead of @launchdarkly/js-client-sdk-common
- tsconfig.server.json excludes src/client.ts and src/client-side/** (mirrors tsconfig.json's server exclusion)
- shopify-oxygen CI uses build:server instead of build (server CI lacks client SDK deps)

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…onments

- build (tsconfig.json): universal only - excludes both TestHook files (no SDK deps needed)
- build:client (tsconfig.client.json): client + universal - excludes server files
- build:server (tsconfig.server.json): server + universal - excludes client files
- shopify-oxygen CI uses build (universal), browser/electron/react-native use build:client, server-node uses build:server

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
The React contract tests import from @launchdarkly/js-contract-test-utils/client
which requires dist/client.js. The topological build only runs the default 'build'
script (universal only), so an explicit build:client step is needed.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
The React contract tests' Next.js/Turbopack build happens during the
topological 'yarn workspaces foreach ... run build' step. The build:client
step must run before that so dist/client.js exists when Turbopack resolves
the @launchdarkly/js-contract-test-utils/client import.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant