Skip to content

fix(sdk): the browser build imports no Node built-in, so webpack 5 builds it without fallbacks - #1039

Merged
alexmercerpo merged 4 commits into
fix/g01-web-worker-conditionsfrom
fix/h01-browser-node-builtins
Nov 3, 2026
Merged

alexmercerpo merged 4 commits into
fix/g01-web-worker-conditionsfrom
fix/h01-browser-node-builtins

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1038

What and why

Bug fix. Fixes #1034.

A webpack 5 build of create() + one tool stopped with UnhandledSchemeError: Reading from "node:cluster" is not handled by plugins unless the app stubbed Node built-ins. This happened for a page (target: 'web') and for a Web Worker (target: 'webworker'). The SDK's per-module browser build kept six node: imports from the shared local server and Unix socket code. Vite swaps them for empty modules and esbuild can mark them external, but webpack 5 refuses the node: scheme. Two chains reached them:

  • node:cluster, node:net and node:path were imported statically: index.mjs → provider/create-container → duty/duty.lifecycle → server/security/unix-socket-guard. The persistent question store took the same path. Both only need findReplaceableDirectory from the socket guard.
  • node:http and node:stream (shared/unix-socket-fetch), node:os and node:path (shared/shared-files), and node:net (shared/shared-serve) came through FrontMcpInstance.runShared()'s import('../shared/index.js'). That import is dynamic, but webpack follows it.

What changes

  • findReplaceableDirectory moves to server/security/replaceable-directory.ts, which uses dirname and stat from @frontmcp/utils. Duties, the question store, the shared server's files and the socket guard import it from there, so the socket guard and its node: imports are left to the Express host, which is already behind #express-host.
  • runShared() loads the shared server through a new #shared-server subpath import, following the pattern of #express-host and #sse-transport. Its browser and worker variant is a stub. The browser builds no longer contain the shared server, and runShared() in a page rejects with BrowserEntryPointUnavailableError instead of ReferenceError: process is not defined.
  • runUnixSocket() rejects in a browser with the same error before it builds a server. Before, it built the whole instance, logged Failed instantiating, and rejected with ProviderConstructionError: ... ExpressHostAdapter is not available in browser environments.
  • BrowserEntryPointUnavailableError (BROWSER_ENTRY_POINT_UNAVAILABLE, exported from @frontmcp/sdk, with entryPoint) follows the wording of feat(sdk): jobs.store: false, and no Redis or SQLite store in a browser build #1028's BrowserStoreUnavailableError, "A browser cannot …". For example: A browser cannot run runShared(): the shared local server needs stdio, a child process and a Unix socket or loopback port. Call it from a Node entry, and use create() in a page.

Node builds are unchanged. dist/index.js and dist/esm/index.mjs still bundle the shared server, and the code that moved is the same. @frontmcp/utils, @frontmcp/auth and @frontmcp/protocol already reached no Node built-in from their browser builds, so they don't change.

Node-only features elsewhere (the ESM loader's cache, offload workers, job admission, the stdio console, auth's JWKS and CIMD DNS checks) load their built-ins with a require() on first use. In the ES module browser build that becomes esbuild's __require shim, which no bundler follows, and it throws only if that feature runs. dist/browser/index.cjs, the CommonJS build for a require() without the module condition (jest-environment-jsdom, which runs on Node), keeps those require()s. It no longer has any of the six module-level ones. Web bundlers resolve index.mjs.

Bundle size

nx run-many -t build -p utils auth protocol sdk plugin-webmcp --skip-nx-cache, then nx test demo-e2e-bundle-size:

Base (G01) This PR Budget before → after
create() + one tool, esbuild, minified 1,294,971 1,295,143 (+172) 1,295,507 → 1,295,679
create() + WebMcpPlugin + one tool 1,302,155 1,302,327 (+172) 1,302,379 → 1,302,551
Vite initial chunk, create() + one tool 761,771 761,135 (−636) 761,903 → 761,267

Each budget is the new size plus the same headroom as before (536 / 224 / 132).

  • The esbuild bundles grow by the runUnixSocket() check in front-mcp.mjs, which every create() loads. The check loads the error class only when it refuses, which keeps the class (about 300 bytes) out of every page. The shared server was already out of them: the measurement keeps on-demand chunks out.
  • Vite's initial chunk shrinks: unix-socket-guard and the empty modules Vite gave its three built-ins leave it, and replaceable-directory is smaller.

On top of #1038 (with #1037's plugin check, which adds 60 bytes below this PR): create() + one tool 1,295,031 → 1,295,203 (+172), with WebMcpPlugin 1,302,215 → 1,302,387 (+172), Vite initial chunk 761,836 → 761,200 (−636). The budgets above (1,295,679 / 1,302,551 / 761,267) hold: the first two are #1038's budgets plus this PR's +172, the Vite budget is lowered.

User-facing change

  • What changed: webpack 5 builds of a page or a Web Worker need no Node fallbacks for FrontMCP. The browser builds leave out the shared local server. runShared() and runUnixSocket() reject in a browser with the new BrowserEntryPointUnavailableError (BROWSER_ENTRY_POINT_UNAVAILABLE), exported from @frontmcp/sdk.

  • Before → after:

    • webpack 5, target: 'web' or 'webworker': up to 1.10 the build stopped on node:cluster, node:net, node:path, node:http, node:stream and node:os unless the app set resolve.fallback or rewrote the node: scheme. It now builds with no fallback.
    • runShared() in a page: ReferenceError: process is not defined → BrowserEntryPointUnavailableError.
    • runUnixSocket() in a page: ProviderConstructionError (ExpressHostAdapter is not available in browser environments) after building the server → BrowserEntryPointUnavailableError, before building anything.
  • frontmcp.dev pages affected (if known): the browser deployment page (webpack, browser limitations, troubleshooting), the shared local server page (errors), and the SDK errors reference.

  • Skills catalog entries updated:

    • frontmcp-deployment/references/build-for-browser.md:
      • a limitations row for runShared() and runUnixSocket();
      • a webpack 5 note (no Node fallbacks, what the browser builds leave out, the error, and what happened up to 1.10);
      • troubleshooting rows for webpack's UnhandledSchemeError and BrowserEntryPointUnavailableError.
    • frontmcp-deployment/references/shared-local-server.md: a Limits entry, which says a browser has no shared local server and names the error.

    The catalog had no "stub Node built-ins" advice for webpack to remove.

Tests

  • apps/e2e/demo-e2e-bundle-size/e2e/browser-web-worker.e2e.spec.ts (extended). The webpack helper now builds with no Node fallbacks: no resolve.fallback and no node: scheme rewrite. The esbuild Web Worker bundle no longer stubs Node built-ins. A webpack target: 'web' case joins target: 'webworker' and esbuild conditions: ['worker']. Each bundle is checked for Node variants and run without process, Buffer, global or setImmediate, and must post the tool's result.
    • On the base, without the fallbacks, all three failed: both webpack builds on the six node: URIs, and the esbuild bundle once its built-in stub was gone.
    • With the fix, all three pass.
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-node-only.e2e.spec.ts (new).
    • A guard bundles the browser entries of @frontmcp/sdk, @frontmcp/utils, @frontmcp/auth, @frontmcp/protocol and @frontmcp/protocol/client. It follows only each package's own modules (relative and # imports) and fails on any Node built-in they import, statically or with import(), whether named node:x or by its bare name. It names the importing file. On the base, the SDK failed with 9 imports from unix-socket-guard, unix-socket-fetch, shared-files, shared-serve and shared-server.options; the other four passed. Now all five pass.
      • The guard follows what a bundler resolves from dist/browser (and protocol's browser variants, since protocol has no dist/browser), rather than grepping text, so esbuild's __require and the worker source string in offload-worker.source don't count.
    • A webpack target: 'web' page that calls runShared() and runUnixSocket() gets BROWSER_ENTRY_POINT_UNAVAILABLE from both. On the base, the webpack build failed.
  • libs/sdk/src/front-mcp/__tests__/node-only-entry-points.browser.spec.ts (new). With the #shared-server browser stub and a browser runtime, runShared() and runUnixSocket() reject with BrowserEntryPointUnavailableError (an InternalMcpError), with the code, entryPoint and message. Red on the base (no #shared-server).
  • Other files updated:
    • scripts/worker-export-conditions.test.mjs: #shared-server is a stub on Cloudflare, and a browser Web Worker, Vercel Edge and a page resolve its browser variant.
    • fixtures/sdk-root-exports.json pins the new export.
    • apps/e2e/demo-e2e-dts-consumer/fixture/shared-server-must-compile.ts: a consumer narrows to BrowserEntryPointUnavailableError and reads its code and entryPoint, and BrowserRefusedEntryPoint rejects an entry point a browser doesn't refuse.
  • Results:
    • nx test demo-e2e-bundle-size: 11 suites, 51 tests passed, with the sizes above.
    • nx test demo-e2e-shared-server: 2 suites, 13 tests passed (Node behaviour).
    • nx test demo-e2e-cloudflare: 2 suites, 16 tests passed. One suite is skipped as on the base.
    • nx test demo-e2e-dts-consumer: 35 tests passed.
    • nx test sdk: 666 suites, 9,304 tests passed.
    • nx test skills: 249 passed.
    • node --test scripts/*.test.mjs: 87 passed.
  • Lint and format:
    • nx run-many -t lint -p sdk skills: 0 errors. The root ESLint config's # allow-list now includes shared-server.
    • tsc --noEmit -p apps/e2e/demo-e2e-bundle-size/tsconfig.e2e.json: no errors. The changed SDK files compile against the SDK's tsconfig with --noEmit.
    • Prettier clean, fix-unused-imports clean.
  • By hand: checked the six module-level require()s are gone from dist/browser/index.cjs, and that dist/index.js and dist/esm/index.mjs still bundle the shared server while dist/browser holds only its stub.
  • The guard catches a regression: with a node:path and an fs import added to replaceable-directory, and an import('node:net') added to the browser stub (reached only through import()), the guard named each import and both webpack builds stopped. Reverted.

Summary by CodeRabbit

  • New Features
    • Browser and Web Worker builds can use SDK entry points without pulling in Node.js-only code.
    • Calls to shared-server and Unix-socket entry points in browsers now fail with a clear BROWSER_ENTRY_POINT_UNAVAILABLE error identifying the unavailable entry point.
  • Documentation
    • Updated browser deployment and shared-server guidance to describe these limitations and the resulting error.

The SDK's browser build imported node:cluster, node:net, node:path,
node:http, node:stream and node:os from the shared local server and Unix
socket code, so a webpack 5 build of create() + one tool, for a page or a
Web Worker, stopped with UnhandledSchemeError unless the app stubbed Node
built-ins. Two chains reached them: duty.lifecycle and the persistent
question store import findReplaceableDirectory from unix-socket-guard, and
runShared() imports the shared server with import(), which webpack follows.

findReplaceableDirectory moves to its own module, which uses
@frontmcp/utils' dirname, so the socket guard is left to the Express host.
runShared() loads the shared server through a #shared-server subpath
import whose browser and worker variant is a stub: the browser builds no
longer contain the shared server, and runShared() in a page rejects with
BrowserEntryPointUnavailableError (BROWSER_ENTRY_POINT_UNAVAILABLE)
instead of "process is not defined". runUnixSocket() refuses in a browser
with the same error before it builds a server. Node builds are unchanged.

Fixes #1034
… consumer

The new error and BrowserRefusedEntryPoint had no consumer coverage, unlike
BrowserStoreUnavailableError. The shared server fixture now narrows to the
error, reads its code and entryPoint, and rejects an entry point a browser
does not refuse.
…cket() refuses

runUnixSocket()'s browser check imported the error class statically, so every
create() in a page carried it. The class now loads on the refusal path, as
runShared()'s browser stub already does, and create() + one tool shrinks by
about 300 bytes. Budgets are the new sizes plus the same headroom.
…main) into fix/h01-browser-node-builtins

build-for-browser.md keeps #1038's corrected Web Worker sentence and this branch's webpack 5 sentence. The browser budgets are re-fitted from a fresh measurement in the next commit.
@frontegg-david
frontegg-david marked this pull request as ready for review November 3, 2026 10:51
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The SDK now routes browser builds away from shared-server code and rejects browser calls to shared-server and Unix-socket entry points with a browser-specific error. Browser bundle tests cover page and worker targets, and documentation describes the browser limitations.

Changes: Browser-safe SDK entry points

Layer / File(s) Summary
Extract replaceable-directory search
libs/sdk/src/server/security/replaceable-directory.ts, libs/sdk/src/server/security/unix-socket-guard.ts, libs/sdk/src/duty/duty.lifecycle.ts, libs/sdk/src/elicitation/questions/question-store.factory.ts, libs/sdk/src/shared/shared-files.ts
The directory-search type and function move from the Unix-socket guard to a separate module. Their consumers import them from the new module.
Route browser entry points to refusals
libs/sdk/src/errors/*, libs/sdk/src/shared/browser-shared-server.ts, libs/sdk/src/front-mcp/front-mcp.ts, libs/sdk/package.json, libs/sdk/src/front-mcp/__tests__/*, apps/e2e/demo-e2e-dts-consumer/*, jest.imports-mapper.js, tsconfig.base.json, eslint.config.mjs, scripts/worker-export-conditions.test.mjs
Browser and worker package conditions select a runShared stub. runUnixSocket() rejects in a browser runtime. Both rejections use BrowserEntryPointUnavailableError, whose type and browser refusal entry points are exported and covered by tests and declaration fixtures.
Verify browser bundles and document limits
apps/e2e/demo-e2e-bundle-size/e2e/*, apps/e2e/demo-e2e-bundle-size/e2e/helpers/browser-bundle.ts, apps/e2e/demo-e2e-bundle-size/e2e/fixtures/*, apps/e2e/demo-e2e-bundle-size/fixture/*, libs/skills/catalog/frontmcp-deployment/references/*
Browser bundle checks cover package entries and browser refusals. Webpack checks both page and worker targets without stubbing Node built-ins. The deployment guidance and bundle-size limits are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant FrontMcpInstance
  participant PackageImports
  participant BrowserSharedServer
  Caller->>FrontMcpInstance: call runShared()
  FrontMcpInstance->>PackageImports: import #shared-server
  PackageImports->>BrowserSharedServer: resolve browser condition
  FrontMcpInstance->>BrowserSharedServer: call runShared()
  BrowserSharedServer-->>Caller: reject with BrowserEntryPointUnavailableError
Loading

Possibly related PRs

Suggested reviewers: alexmercerpo

Merge Risk: Minimal · up to 7cfc1

No actionable merge-blocking risk is established by the supplied evidence.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Out of Scope Changes check Warning The supplied change summary reports an ESLint allow-list change for #crypto-* and #async-*. Those aliases do not support issue [#1034], which concerns browser Node-built-in imports and Node-only e… Remove the unrelated #crypto-* and #async-* ESLint allow-list change from this PR, or link it to a directly applicable coding requirement.
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: webpack 5 browser builds no longer require Node fallbacks because the browser build avoids Node built-ins.
Description check Passed The description is complete and follows the required structure. It explains the reason for the change, documents user-facing behavior and affected pages, lists updated skills entries, and provides det…
Linked Issues check Passed Issue [#1034] requires webpack 5 page and Web Worker builds of create() plus one tool to use no Node fallbacks and no Node built-ins in browser entries. The PR adds the #shared-server browser and …
Full details: Description check

Explanation

The description is complete and follows the required structure. It explains the reason for the change, documents user-facing behavior and affected pages, lists updated skills entries, and provides detailed test and validation results.

Full details: Linked Issues check

Explanation

Issue [#1034] requires webpack 5 page and Web Worker builds of create() plus one tool to use no Node fallbacks and no Node built-ins in browser entries. The PR adds the #shared-server browser and worker stubs, moves findReplaceableDirectory away from the Unix-socket guard, and adds browser refusal errors for runShared() and runUnixSocket(). The reported webpack, esbuild, Node-import guard, runtime, package, and type tests cover these requirements. Node-only behavior remains in the Node variant.

Full details: Out of Scope Changes check

Explanation

The supplied change summary reports an ESLint allow-list change for #crypto-* and #async-*. Those aliases do not support issue [#1034], which concerns browser Node-built-in imports and Node-only entry points. The other reported changes have a direct connection to the issue through implementation, tests, documentation, or build configuration.

Resolution

Remove the unrelated #crypto-* and #async-* ESLint allow-list change from this PR, or link it to a directly applicable coding requirement.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 4 functions across 20 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 7fac0d6 and 7cfc1c1.

Files selected for processing: 25
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-node-only.e2e.spec.ts
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-web-worker.e2e.spec.ts
  • apps/e2e/demo-e2e-bundle-size/e2e/fixtures/sdk-root-exports.json
  • apps/e2e/demo-e2e-bundle-size/e2e/helpers/browser-bundle.ts
  • apps/e2e/demo-e2e-bundle-size/fixture/node-only-entry-points.mjs
  • apps/e2e/demo-e2e-dts-consumer/e2e/dts-consumer.e2e.spec.ts
  • apps/e2e/demo-e2e-dts-consumer/fixture/shared-server-must-compile.ts
  • eslint.config.mjs
  • jest.imports-mapper.js
  • libs/sdk/package.json
  • libs/sdk/src/duty/duty.lifecycle.ts
  • libs/sdk/src/elicitation/questions/question-store.factory.ts
  • libs/sdk/src/errors/browser-entry-point.errors.ts
  • libs/sdk/src/errors/index.ts
  • libs/sdk/src/front-mcp/__tests__/node-only-entry-points.browser.spec.ts
  • libs/sdk/src/front-mcp/front-mcp.ts
  • libs/sdk/src/server/security/replaceable-directory.ts
  • libs/sdk/src/server/security/unix-socket-guard.ts
  • libs/sdk/src/shared/browser-shared-server.ts
  • libs/sdk/src/shared/shared-files.ts
  • libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md
  • libs/skills/catalog/frontmcp-deployment/references/shared-local-server.md
  • scripts/worker-export-conditions.test.mjs
  • tsconfig.base.json

Included review availability: This review used your included allowance. Your plan provides up to 5 included reviews per hour; 0 remain after this review.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-03T11:06:27.913Z
Commit: 366a6c4d

@alexmercerpo
alexmercerpo merged commit 10f4cc7 into main Nov 3, 2026
47 checks passed
@alexmercerpo
alexmercerpo deleted the fix/h01-browser-node-builtins branch November 3, 2026 12:14
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.

webpack 5 page builds of the SDK fail on node: imports from the shared-server code

2 participants