Skip to content

fix(build): keep browser-reachable modules off node-only imports so next build --turbopack succeeds again - #13704

Closed
seanford wants to merge 4 commits into
diegosouzapw:release/v3.8.51from
seanford:fix/build-combo-control-center-client-bundle
Closed

seanford wants to merge 4 commits into
diegosouzapw:release/v3.8.51from
seanford:fix/build-combo-control-center-client-bundle

Conversation

@seanford

Copy link
Copy Markdown
Contributor

Summary

next build --turbopack — and therefore every Docker image build — has been failing on release/v3.8.51 since the 2026-09-11 owner batch. Docker Hub confirms it: the last next / next-web images were published 2026-09-11 18:16 UTC, and nothing has been published from any of the ~150 commits since. Two commits in that batch each introduced a server → client import edge, and Turbopack fails on the first one it meets, which is why the second was hidden until the first was fixed:

  1. fix(routing): stop round-robin combo opencode targets from collapsing onto opencode-zen (#11912) #13283 made src/lib/combos/controlCenter.ts — imported by the "use client" ComboControlCenterClient page — import resolveProviderAlias() from open-sse/services/model.ts. That module lazily imports the DB layer (@/lib/db/readCache, @/lib/db/models, …); Turbopack follows those await import() edges into the browser bundle, drags the executors (down to the Playwright-backed zai-web browser automation) along, and the build dies with 168 × Module not found: Can't resolve 'child_process' / 'async_hooks' from playwright-core and detect-libc.
  2. fix(oauth): align codebuddy-cn OAuth User-Agent with chat/usage (#12702) #13264 made the browser-reachable provider registry entry open-sse/config/providers/registry/codebuddy-cn/index.ts import CODEBUDDY_CN_USER_AGENT from src/lib/oauth/constants/oauth.ts, which imports open-sse/utils/cursorAgentCliVersion.ts (node:fs / node:os / node:path). With (1) fixed, the build fails one step later on every dashboard page: TurbopackInternalError: Failed to write app endpoint /(dashboard)/dashboard/page — the chunking context (unknown) does not support external modules (request: node:fs).

Changes

  • open-sse/services/providerAlias.ts (new, pure): ALIAS_TO_PROVIDER_ID + resolveProviderAlias(), depending only on the static provider registry. model.ts imports and re-exports both, so its 8 existing server-side importers are untouched; controlCenter.ts imports the pure module.
  • open-sse/config/providers/registry/codebuddy-cn/userAgent.ts (new, dependency-free): CODEBUDDY_CN_USER_AGENT. The registry entry imports it directly; src/lib/oauth/constants/oauth.ts imports and re-exports it, so OAuth, chat headers and the usage/quota client keep sharing the single string that [BUG] deepseek harness + codebuddy cn #12702 requires.
  • Two changelog.d/fixes/ fragments (placeholder 0000 — I'll rename to this PR's number once assigned).

No behaviour change at runtime; this is purely which module the constants live in.

Validation

  • Change type: build / bundling
  • Verified with an import-graph walk over src/ + open-sse/ (client/edge entries → files importing playwright or a Node built-in): at the last commit that published an image (af49d4972) no "use client"/edge entry reached either; at the branch tip exactly these two chains did; after this PR the reachable set is identical to af49d4972 again.
  • Docker build of Dockerfile target runner-web (the same build docker-publish.yml runs) on a clean host: fails on the tip with the errors above, succeeds with this PR (Image … Built).
  • npm run typecheck:core clean; eslint --suppressions-location config/quality/eslint-suppressions.json and prettier --check clean on all touched files.
  • node --import tsx/esm --test on the 19 unit files that exercise resolveProviderAlias, combos/controlCenter, services/model, codebuddy-cn and constants/oauth: 191 tests, all passing.
  • Reconciled with release/v3.8.51 @ c0f92ec98.

Reviewer notes

  • This is a prerequisite for anything else landing as an image from release/v3.8.51, including my three follow-up PRs on /v1/rerank provider nodes.
  • The two fixes are separate commits in case you prefer to cherry-pick.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KHaWGXax8LT4TxyGopEVxX

…ervice

diegosouzapw#13283 made src/lib/combos/controlCenter.ts — imported by the "use client"
ComboControlCenterClient page — import resolveProviderAlias() from
open-sse/services/model.ts. That module lazily imports the DB layer
(@/lib/db/readCache, @/lib/db/models, …); Turbopack follows those edges into
the client bundle, drags the executors (down to the Playwright-backed zai-web
browser automation) along, and `next build --turbopack` fails with 168
"Module not found: Can't resolve 'child_process' / 'async_hooks'" errors.
Every Docker image build since the 2026-09-11 batch fails the same way.

Move ALIAS_TO_PROVIDER_ID and resolveProviderAlias() into a pure
open-sse/services/providerAlias.ts that depends only on the static provider
registry, re-export both from model.ts so the existing server-side importers
are untouched, and point controlCenter.ts at the pure module.

Verified with an import-graph walk over src/ + open-sse/: at the last commit
that published an image no "use client"/edge entry reached a playwright
importer, at the branch tip exactly one did (this chain), and none do after
this change.
…nts module

diegosouzapw#13264 made open-sse/config/providers/registry/codebuddy-cn/index.ts import
CODEBUDDY_CN_USER_AGENT from src/lib/oauth/constants/oauth.ts. The provider
registry is browser-reachable (dashboard model pickers →
src/shared/constants/models.ts → open-sse/config/providerModels.ts →
providerRegistry → every registry entry), and oauth.ts imports
open-sse/utils/cursorAgentCliVersion.ts, which imports node:fs / node:os /
node:path. With the Playwright edge from diegosouzapw#13283 fixed, `next build --turbopack`
then fails one step later on every dashboard page:

  TurbopackInternalError: Failed to write app endpoint /(dashboard)/dashboard/page
  Caused by: the chunking context (unknown) does not support external modules
  (request: node:fs)

Define the constant in a dependency-free
open-sse/config/providers/registry/codebuddy-cn/userAgent.ts, import it there
from the registry entry, and have oauth.ts import + re-export it so the OAuth
config, chat headers and usage/quota client keep sharing the one string that
diegosouzapw#12702 requires.

Verified with an import-graph walk (client/edge entries → files importing a
Node built-in): the set of reachable chains is now identical to the last
release-branch commit that published a Docker image.
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks for this — confirmed the fix: your providerAlias.ts/userAgent.ts split builds
cleanly, has no merge conflicts against the current tip, and the 15 existing test files that
cover resolveProviderAlias, the combo control center and the codebuddy-cn provider all pass
(122/122). The CI reds on this PR are pre-existing base drift on release/v3.8.51
(⚠️ base-red inherited: #12732 — 5 missing .env.example vars, the known
db/providers/deletion.ts import cycle, 4 unrelated ESLint errors; the API Route Typecheck
TS2741 in modelTestRunner.ts was a defect of the base itself, already fixed on the tip by
#13730) — none of them are caused by your change.

There are a few other open PRs attacking the same two import chains (#13436, #13604, #13635,
#13656); yours is the cleanest self-contained fix for the controlCenter.ts chain and the
Docker build has been broken since 2026-09-11, so we're taking this one forward now — the
others will rebase on top. One non-blocking follow-up you may want to pick up later: extending
tests/unit/client-bundle-no-server-only-10692.test.ts so a third occurrence of this pattern is
caught in PR CI rather than in the next real Docker build.

…nerically

The guard test from diegosouzapw#10692 only knew a hand-written list of server-only
modules and ignored dynamic `import()`, so neither of the two regressions
fixed in this PR (diegosouzapw#13264: registry entry → oauth constants → node:fs;
diegosouzapw#13283: combo control center → model.ts → lazy DB import → child_process)
could fail PR CI — they only surfaced in the next Docker build.

- Treat any first-party module that imports a non-polyfillable Node
  builtin (fs, net, tls, child_process, async_hooks, worker_threads, …)
  as server-only, in addition to the explicit list. Polyfilled builtins
  (path, crypto, buffer, …) are deliberately not listed.
- Follow dynamic `import("…")` edges: the bundler still has to build the
  lazy chunk, so a builtin behind one fails the build exactly like a
  static import.
- Report each offending edge once with an example chain and a count
  instead of one chain per client entry point.

On release/v3.8.51 without the two fixes the test now fails with exactly
the two chains above; with them it passes.
@seanford

Copy link
Copy Markdown
Contributor Author

Thanks — picked up the follow-up in 3e1fb9d24 (test(build): make the client-bundle guard catch Node-builtin leaks generically).

The guard from #10692 could not have caught either chain: it only knew a hand-written SERVER_ONLY list (nothing on it sat on the oauth.ts → cursorAgentCliVersion.ts → node:fs path) and it deliberately did not follow dynamic import() (which is exactly how model.ts reaches the DB layer). Changes:

  • Any first-party module that imports a non-polyfillable Node builtin (fs, fs/promises, net, tls, child_process, async_hooks, worker_threads, cluster, dgram, dns, http2, …, with or without the node: prefix) is now treated as server-only, on top of the explicit list. path/crypto/buffer/util/stream/os are intentionally not listed since Turbopack shims those.
  • Dynamic import("…") with a literal specifier is followed: the bundler still has to build the lazy chunk, so a builtin behind it fails the build like a static import.
  • Failure output is grouped by offending edge (importer → server-only module) with one example chain and a count, instead of one chain per client entry point (there were 315 of them for the node:fs chain).

Verified on release/v3.8.51 @ c0f92ec98 without the two fixes — the test fails with exactly these two chains and nothing else:

src/app/(dashboard)/dashboard/HomePageClient.tsx
  → src/shared/components/index.tsx
  → src/shared/components/ModelSelectModal.tsx
  → src/shared/constants/models.ts
  → open-sse/config/providerModels.ts
  → open-sse/config/providerRegistry.ts
  → open-sse/config/providers/index.ts
  → open-sse/config/providers/registry/codebuddy-cn/index.ts
  → src/lib/oauth/constants/oauth.ts
  → open-sse/utils/cursorAgentCliVersion.ts  (imports node:fs)
  (and 314 more client entry points reach the same chain)

src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx
  → src/lib/combos/controlCenter.ts
  → open-sse/services/model.ts
  → src/lib/db/models.ts
  → src/lib/db/core.ts  (imports fs)

With the two fixes on this branch it passes (1/1), and eslint/prettier are clean on the file.

@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @seanford for tracking down both server-to-client import chains that broke next build --turbopack. The 168 child_process/async_hooks errors and the node:fs failure one step later were exactly right.

The same fix is already on release/v3.8.51 through #13436 (merged 2026-09-15). It adds a DB-free open-sse/services/providerAlias.ts, which src/lib/combos/controlCenter.ts now imports resolveProviderAlias from. It also moves CODEBUDDY_CN_USER_AGENT so that open-sse/config/providers/registry/codebuddy-cn/index.ts reads it from providerHeaderProfiles.ts and no longer pulls in src/lib/oauth/constants/oauth.ts. On top of that, #13436 widens the client-bundle guard so it finds this class of import. Nothing here is left to land, so I'm closing this as covered. Thank you for the careful diagnosis.

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.

2 participants