Repository navigation
fix(sdk): the browser build imports no Node built-in, so webpack 5 builds it without fallbacks - #1039
Conversation
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.
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-03T11:06:27.913Z |
Stacked on #1038
What and why
Bug fix. Fixes #1034.
A webpack 5 build of
create()+ one tool stopped withUnhandledSchemeError: Reading from "node:cluster" is not handled by pluginsunless 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 sixnode: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 thenode:scheme. Two chains reached them:node:cluster,node:netandnode:pathwere 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 needfindReplaceableDirectoryfrom the socket guard.node:httpandnode:stream(shared/unix-socket-fetch),node:osandnode:path(shared/shared-files), andnode:net(shared/shared-serve) came throughFrontMcpInstance.runShared()'simport('../shared/index.js'). That import is dynamic, but webpack follows it.What changes
findReplaceableDirectorymoves toserver/security/replaceable-directory.ts, which usesdirnameandstatfrom@frontmcp/utils. Duties, the question store, the shared server's files and the socket guard import it from there, so the socket guard and itsnode:imports are left to the Express host, which is already behind#express-host.runShared()loads the shared server through a new#shared-serversubpath import, following the pattern of#express-hostand#sse-transport. Itsbrowserandworkervariant is a stub. The browser builds no longer contain the shared server, andrunShared()in a page rejects withBrowserEntryPointUnavailableErrorinstead ofReferenceError: process is not defined.runUnixSocket()rejects in a browser with the same error before it builds a server. Before, it built the whole instance, loggedFailed instantiating, and rejected withProviderConstructionError: ... ExpressHostAdapter is not available in browser environments.BrowserEntryPointUnavailableError(BROWSER_ENTRY_POINT_UNAVAILABLE, exported from@frontmcp/sdk, withentryPoint) follows the wording of feat(sdk): jobs.store: false, and no Redis or SQLite store in a browser build #1028'sBrowserStoreUnavailableError, "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.jsanddist/esm/index.mjsstill bundle the shared server, and the code that moved is the same.@frontmcp/utils,@frontmcp/authand@frontmcp/protocolalready 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__requireshim, which no bundler follows, and it throws only if that feature runs.dist/browser/index.cjs, the CommonJS build for arequire()without themodulecondition (jest-environment-jsdom, which runs on Node), keeps thoserequire()s. It no longer has any of the six module-level ones. Web bundlers resolveindex.mjs.Bundle size
nx run-many -t build -p utils auth protocol sdk plugin-webmcp --skip-nx-cache, thennx test demo-e2e-bundle-size:create()+ one tool, esbuild, minifiedcreate()+ WebMcpPlugin + one toolcreate()+ one toolEach budget is the new size plus the same headroom as before (536 / 224 / 132).
runUnixSocket()check infront-mcp.mjs, which everycreate()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.unix-socket-guardand the empty modules Vite gave its three built-ins leave it, andreplaceable-directoryis 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()andrunUnixSocket()reject in a browser with the newBrowserEntryPointUnavailableError(BROWSER_ENTRY_POINT_UNAVAILABLE), exported from@frontmcp/sdk.Before → after:
target: 'web'or'webworker': up to 1.10 the build stopped onnode:cluster,node:net,node:path,node:http,node:streamandnode:osunless the app setresolve.fallbackor rewrote thenode: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:runShared()andrunUnixSocket();UnhandledSchemeErrorandBrowserEntryPointUnavailableError.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: noresolve.fallbackand nonode:scheme rewrite. The esbuild Web Worker bundle no longer stubs Node built-ins. A webpacktarget: 'web'case joinstarget: 'webworker'and esbuildconditions: ['worker']. Each bundle is checked for Node variants and run withoutprocess,Buffer,globalorsetImmediate, and must post the tool's result.node:URIs, and the esbuild bundle once its built-in stub was gone.apps/e2e/demo-e2e-bundle-size/e2e/browser-node-only.e2e.spec.ts(new).@frontmcp/sdk,@frontmcp/utils,@frontmcp/auth,@frontmcp/protocoland@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 withimport(), whether namednode:xor by its bare name. It names the importing file. On the base, the SDK failed with 9 imports fromunix-socket-guard,unix-socket-fetch,shared-files,shared-serveandshared-server.options; the other four passed. Now all five pass.dist/browser(and protocol's browser variants, since protocol has nodist/browser), rather than grepping text, so esbuild's__requireand the worker source string inoffload-worker.sourcedon't count.target: 'web'page that callsrunShared()andrunUnixSocket()getsBROWSER_ENTRY_POINT_UNAVAILABLEfrom 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-serverbrowser stub and a browser runtime,runShared()andrunUnixSocket()reject withBrowserEntryPointUnavailableError(anInternalMcpError), with the code,entryPointand message. Red on the base (no#shared-server).scripts/worker-export-conditions.test.mjs:#shared-serveris a stub on Cloudflare, and a browser Web Worker, Vercel Edge and a page resolve its browser variant.fixtures/sdk-root-exports.jsonpins the new export.apps/e2e/demo-e2e-dts-consumer/fixture/shared-server-must-compile.ts: a consumer narrows toBrowserEntryPointUnavailableErrorand reads itscodeandentryPoint, andBrowserRefusedEntryPointrejects an entry point a browser doesn't refuse.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.nx run-many -t lint -p sdk skills: 0 errors. The root ESLint config's#allow-list now includesshared-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.fix-unused-importsclean.require()s are gone fromdist/browser/index.cjs, and thatdist/index.jsanddist/esm/index.mjsstill bundle the shared server whiledist/browserholds only its stub.node:pathand anfsimport added toreplaceable-directory, and animport('node:net')added to the browser stub (reached only throughimport()), the guard named each import and both webpack builds stopped. Reverted.Summary by CodeRabbit
BROWSER_ENTRY_POINT_UNAVAILABLEerror identifying the unavailable entry point.