Skip to content

fix(node,sdk): compile remote code without eval and outside V8's compilation cache - #5030

Open
ScriptedAlchemy wants to merge 10 commits into
mainfrom
fix/node-chunk-loader-no-eval
Open

fix(node,sdk): compile remote code without eval and outside V8's compilation cache#5030
ScriptedAlchemy wants to merge 10 commits into
mainfrom
fix/node-chunk-loader-no-eval

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Member

Why

Two related memory costs in long-running Node SSR hosts that re-execute remotes (see discussion #4566):

  1. Remote chunks fetched over HTTP by @module-federation/node's runtime plugin were executed with direct eval of a (function(exports, require, __dirname, __filename) {…}) wrapper (fetchAndRun in packages/node/src/runtimePlugin.ts; present since the plugin's first commit in feat(node): runtime plugin for node envs #2361, and the only HTTP path since the http-vm implementation was removed in fix(node): allow fetch override on runtime plugin #2603). Functions created by direct eval capture the enclosing scope, so every module factory in the chunk kept the fetched data string alive on top of the flattened wrapper V8 retains as script source. A live 3 MB chunk cost 6 MB of strings.
  2. V8 keeps every distinct script it compiles in an isolate-wide compilation cache (source plus compiled code). It never ages out on its own and is evicted only when the heap nears V8's own limit, which defaults from physical memory, not the container's cgroup. A host that force-registers a genuinely new remote build on each refresh grew ~6.4 MB per refresh with a 3 MB chunk, identically for eval, vm.Script, vm.runInThisContext, new Function and vm.compileFunction, and heap snapshots never show it because writing a snapshot clears the cache.

What

Boundary: runtime-core and runtime know nothing about Node compilation; the sdk's Node code owns the compile primitives and the V8 policy; @module-federation/node decides that fetched chunks are remote code and compiles them through its own dependency. Nothing is exported through __webpack_require__.federation.runtime.

  • packages/sdk/src/node.ts: buildCommonJsWrapper(parameters, source) is the single wrapper shape; compileCommonJsModule({ source, filename, parameters, importModuleDynamically }) compiles via vm.Script when tryGetVm() obtains the module, otherwise new Function, and a compile error from the chosen backend propagates (never retried on the other one); withRemoteCompilationPolicy(compile) flips --no-compilation-cache around one synchronous compile and restores it in finally, with a depth counter on a globalThis symbol shared by every copy of the package, no-op when the process already runs with the flag (execArgv / NODE_OPTIONS), FEDERATION_REMOTE_COMPILATION_CACHE=disable (default) | default, and no-op when v8.setFlagsFromString is missing or throws; compileRemoteCommonJsModule(options) composes them. loadScriptNode uses the composition for remote entries. Builtin lookups (vm, v8) are memoised on a process-wide global and prefer process.getBuiltinModule (see the eval-cache note under Measured).
  • packages/node/src/runtimePlugin.ts: compileChunk(source, filename) calls compileRemoteCommonJsModule imported from @module-federation/sdk, with CHUNK_WRAPPER_PARAMS and the plugin's importModuleDynamically fallback. fetchAndRun and loadFromFs use it; the stringified httpEvalStrategy keeps its self-contained new Function with a conformance test on the parameter list. No lookup on federation.runtime, no legacy fallback.
  • Tests: sdk node-compile.spec.ts covers the wrapper, both backends, error propagation, and the policy (initial state, execArgv, nesting, thrown error, missing setFlagsFromString, env default), with the builtin memo and getBuiltinModule controlled per test; node tests mock the sdk entry point and assert chunks compile through it with the chunk URL as filename.
  • packages/node/__benchmarks__/remote-compilation-memory.mjs: reproducible heap benchmark (with policy ~flat, without ~170 MB across 40 unique 2 MB scripts on Node 22).
  • Changesets: @module-federation/node patch, @module-federation/sdk patch.

Measured

Host on @module-federation/runtime HEAD, enhanced-built async-node remote with a 3 MB exposed chunk, heap after two full GCs:

scenario before after
chunk source strings per live container 2 (6.0 MB) 1 (3.0 MB)
live-container heap 19.7 MB 16.7 MB
30 forced refreshes, identical build 24.2 MB 17.7 MB
40 forced refreshes, new build each time 269.8 MB 18.0 MB
same, with FEDERATION_REMOTE_COMPILATION_CACHE=default 272.9 MB

Cost of the toggle: one uncached compile per remote execution, ~37 ms for 2 MB against ~4 ms cached; in the full reload loop the median reload went from 67 ms to 68 ms. The toggle is process-wide for the duration of the compile, so a worker thread compiling in that window misses the cache once; no correctness impact. Its effect is verifiable at runtime (a repeat compile of the same source takes ~2,500 µs with the cache off versus ~30 µs on).

Alternatives evaluated and not adopted (kept here for the record)

The compilation cache has no per-script option (vm.Script only knows cachedData/produceCachedData, which are the unrelated serialized code cache) and no eviction API; per-context isolation does not help because the cache is per isolate. Ways to clear it after the fact, all measured on the same loop with 150 MB of live objects underneath (20 new-build refreshes):

approach heap at refresh 20 event-loop pause per reload notes
flag toggle around the compile (this PR) 179 MB, flat 0 ms median reload 68 ms vs 67 ms baseline
inspector HeapProfiler.collectGarbage after each reload 178 MB, flat 170 to 207 ms (~540 ms on a 500 MB heap) no flags or debugger needed, works in worker threads; denied under Node's --permission mode (ERR_ACCESS_DENIED, permission Inspector); the collection is asynchronous, so the session must be disconnected on a macrotask after the callback, and disconnecting inside the callback deadlocks
gc({ type: 'major', execution: 'sync', flavor: 'last-resort' }) with --expose-gc flat same as the inspector --expose-gc is accepted in NODE_OPTIONS, so it is available on env-only hosts; still a stop-the-world per reload
v8.writeHeapSnapshot after each reload 180 MB, flat 11.3 to 19.6 s (28 s at 300 MB of objects) clearing the cache is an undocumented side effect of the snapshot generator
--no-compilation-cache at startup flat 0 ms rejected in NODE_OPTIONS, so Lambda-style hosts cannot set it; disables the cache for all code
cache left on, --max-old-space-size=200 plateau at 115 MB GC pauses under pressure V8 evicts the cache only near its own limit; default limit derives from machine RAM (4,144 MB on a 129 GB box), so a 512 MB container is killed first

An earlier revision of this branch shipped the inspector collection (with the exposed-gc fallback) as a FEDERATION_COMPILATION_CACHE=gc strategy; it was removed in favour of the flag toggle alone, which has no pause and works in every environment tested, including under --permission. Charts for these runs: https://gist.github.com/ScriptedAlchemy/775fa1d0f2ebee25c68558246b4fdb68

🤖 Generated with Claude Code

Remote chunks fetched over HTTP by the node runtime plugin were executed via
direct `eval` of a wrapper string. Functions created by direct eval capture the
enclosing scope, so every module factory in the chunk kept the fetched `data`
string alive in its closure context in addition to the flattened wrapper that
V8 retains as script source. A live 3 MB chunk therefore cost 6 MB, and in
long-running SSR hosts that re-execute remotes the duplicate copy came along
with every generation.

Introduce a shared `compileChunk` helper: on Node it compiles through
`vm.Script` with the chunk URL as the filename (same code path the filesystem
loader already used), and on runtimes without `vm` it uses `new Function`,
which does not capture scope. The stringified `httpEvalStrategy` in the
filesystem strategies gets the same treatment.

Measured on a 3 MB chunk with a heap snapshot after GC: one chunk source
string retained per live container instead of two, live-container heap
19.7 MB -> 16.7 MB, and 30 forced re-registrations stay flat at 17.7 MB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e14380a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 48 packages
Name Type
@module-federation/node Patch
@module-federation/sdk Patch
@module-federation/modern-js-v3 Patch
@module-federation/modern-js Patch
@module-federation/nextjs-mf Patch
@module-federation/rsbuild-plugin Patch
@module-federation/rstest Patch
node-dynamic-remote-new-version Patch
node-dynamic-remote Patch
@module-federation/devtools Patch
@module-federation/cli Patch
@module-federation/dts-plugin Patch
@module-federation/enhanced Patch
@module-federation/esbuild Patch
@module-federation/managers Patch
@module-federation/manifest Patch
@module-federation/metro Patch
@module-federation/observability-plugin Patch
@module-federation/retry-plugin Patch
@module-federation/rspack Patch
@module-federation/rspress-plugin Patch
@module-federation/runtime-core Patch
@module-federation/runtime Patch
@module-federation/storybook-addon Patch
@module-federation/utilities Patch
@module-federation/webpack-bundler-runtime Patch
@module-federation/bridge-react-webpack-plugin Patch
@module-federation/bridge-react Patch
@module-federation/bridge-vue3 Patch
shared-tree-shaking-with-server-host Patch
shared-tree-shaking-with-server-provider Patch
@module-federation/playground Patch
remote5 Patch
remote6 Patch
shared-tree-shaking-no-server-host Patch
shared-tree-shaking-no-server-provider Patch
@module-federation/metro-plugin-rnc-cli Patch
@module-federation/metro-plugin-rnef Patch
@module-federation/metro-plugin-rock Patch
website-new Patch
@module-federation/runtime-tools Patch
@module-federation/inject-external-runtime-core-plugin Patch
create-module-federation Patch
@module-federation/error-codes Patch
@module-federation/third-party-dts-extractor Patch
@module-federation/treeshake-frontend Patch
@module-federation/treeshake-server Patch
@module-federation/bridge-shared Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T21:49:09.750452Z 06082e3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@module-federation/devtools

pnpm add https://pkg.pr.new/@module-federation/devtools@e14380a

@module-federation/cli

pnpm add https://pkg.pr.new/@module-federation/cli@e14380a

create-module-federation

pnpm add https://pkg.pr.new/create-module-federation@e14380a

@module-federation/dts-plugin

pnpm add https://pkg.pr.new/@module-federation/dts-plugin@e14380a

@module-federation/enhanced

pnpm add https://pkg.pr.new/@module-federation/enhanced@e14380a

@module-federation/error-codes

pnpm add https://pkg.pr.new/@module-federation/error-codes@e14380a

@module-federation/esbuild

pnpm add https://pkg.pr.new/@module-federation/esbuild@e14380a

@module-federation/managers

pnpm add https://pkg.pr.new/@module-federation/managers@e14380a

@module-federation/manifest

pnpm add https://pkg.pr.new/@module-federation/manifest@e14380a

@module-federation/metro

pnpm add https://pkg.pr.new/@module-federation/metro@e14380a

@module-federation/metro-plugin-rnc-cli

pnpm add https://pkg.pr.new/@module-federation/metro-plugin-rnc-cli@e14380a

@module-federation/metro-plugin-rnef

pnpm add https://pkg.pr.new/@module-federation/metro-plugin-rnef@e14380a

@module-federation/metro-plugin-rock

pnpm add https://pkg.pr.new/@module-federation/metro-plugin-rock@e14380a

@module-federation/modern-js

pnpm add https://pkg.pr.new/@module-federation/modern-js@e14380a

@module-federation/modern-js-v3

pnpm add https://pkg.pr.new/@module-federation/modern-js-v3@e14380a

@module-federation/native-federation-tests

pnpm add https://pkg.pr.new/@module-federation/native-federation-tests@e14380a

@module-federation/native-federation-typescript

pnpm add https://pkg.pr.new/@module-federation/native-federation-typescript@e14380a

@module-federation/nextjs-mf

pnpm add https://pkg.pr.new/@module-federation/nextjs-mf@e14380a

@module-federation/node

pnpm add https://pkg.pr.new/@module-federation/node@e14380a

@module-federation/observability-plugin

pnpm add https://pkg.pr.new/@module-federation/observability-plugin@e14380a

@module-federation/playground

pnpm add https://pkg.pr.new/@module-federation/playground@e14380a

@module-federation/retry-plugin

pnpm add https://pkg.pr.new/@module-federation/retry-plugin@e14380a

@module-federation/rsbuild-plugin

pnpm add https://pkg.pr.new/@module-federation/rsbuild-plugin@e14380a

@module-federation/rspack

pnpm add https://pkg.pr.new/@module-federation/rspack@e14380a

@module-federation/rspress-plugin

pnpm add https://pkg.pr.new/@module-federation/rspress-plugin@e14380a

@module-federation/rstest

pnpm add https://pkg.pr.new/@module-federation/rstest@e14380a

@module-federation/runtime

pnpm add https://pkg.pr.new/@module-federation/runtime@e14380a

@module-federation/runtime-core

pnpm add https://pkg.pr.new/@module-federation/runtime-core@e14380a

@module-federation/runtime-tools

pnpm add https://pkg.pr.new/@module-federation/runtime-tools@e14380a

@module-federation/sdk

pnpm add https://pkg.pr.new/@module-federation/sdk@e14380a

@module-federation/storybook-addon

pnpm add https://pkg.pr.new/@module-federation/storybook-addon@e14380a

@module-federation/third-party-dts-extractor

pnpm add https://pkg.pr.new/@module-federation/third-party-dts-extractor@e14380a

@module-federation/treeshake-frontend

pnpm add https://pkg.pr.new/@module-federation/treeshake-frontend@e14380a

@module-federation/treeshake-server

pnpm add https://pkg.pr.new/@module-federation/treeshake-server@e14380a

@module-federation/typescript

pnpm add https://pkg.pr.new/@module-federation/typescript@e14380a

@module-federation/utilities

pnpm add https://pkg.pr.new/@module-federation/utilities@e14380a

@module-federation/webpack-bundler-runtime

pnpm add https://pkg.pr.new/@module-federation/webpack-bundler-runtime@e14380a

@module-federation/bridge-react

pnpm add https://pkg.pr.new/@module-federation/bridge-react@e14380a

@module-federation/bridge-react-webpack-plugin

pnpm add https://pkg.pr.new/@module-federation/bridge-react-webpack-plugin@e14380a

@module-federation/bridge-shared

pnpm add https://pkg.pr.new/@module-federation/bridge-shared@e14380a

@module-federation/bridge-vue3

pnpm add https://pkg.pr.new/@module-federation/bridge-vue3@e14380a

@module-federation/inject-external-runtime-core-plugin

pnpm add https://pkg.pr.new/@module-federation/inject-external-runtime-core-plugin@e14380a

commit: e14380a

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Report

11 package(s) changed, 32 unchanged.

Package dist + ESM entry

Package Total dist (raw) Delta ESM gzip Delta
@module-federation/bridge-react 313.4 kB +4.7 kB (+1.5%) 1.5 kB -4 B (-0.3%)
@module-federation/bridge-vue3 218.1 kB +26.4 kB (+13.8%) 29.6 kB +2.2 kB (+8.2%)
@module-federation/node 199.5 kB +1.1 kB (+0.6%) 217 B no change
@module-federation/playground 28.94 MB +25.3 kB (+0.1%) 46.4 kB +737 B (+1.6%)
@module-federation/runtime 20.4 kB +13 B (+0.1%) 724 B no change
@module-federation/sdk 140.2 kB +11.2 kB (+8.7%) 829 B +44 B (+5.6%)

Bundle targets

Package Web bundle (gzip) Delta Node bundle (gzip) Delta
@module-federation/bridge-react 18.5 kB +5.1 kB (+37.6%) 19.1 kB +4.8 kB (+33.9%)
@module-federation/bridge-vue3 24.3 kB +4.3 kB (+21.4%) 24.9 kB +4.8 kB (+23.7%)
@module-federation/cli 2.3 kB -1 B (-0.0%) 2.4 kB -33 B (-1.3%)
@module-federation/core 1.0 kB -2 B (-0.2%) 1.0 kB -33 B (-3.0%)
@module-federation/devtools 30.3 kB no change 30.3 kB -24 B (-0.1%)
@module-federation/enhanced 2.7 kB -3 B (-0.1%) 2.8 kB -43 B (-1.5%)
@module-federation/metro-plugin-rnc-cli 416 B +2 B (+0.5%) 435 B -25 B (-5.4%)
@module-federation/node 8.3 kB -830 B (-8.9%) 8.3 kB -883 B (-9.4%)
@module-federation/playground 42.2 kB +737 B (+1.7%) 42.2 kB +737 B (+1.7%)
@module-federation/sdk 6.6 kB +2.2 kB (+48.6%) 7.0 kB +1.2 kB (+20.4%)

Consumer scenarios

Scenario Web output (gzip) Delta Node output (gzip) Delta Gap (node-web) Delta
Enhanced remoteEntry 24.0 kB +1.8 kB (+7.9%) 24.6 kB +829 B (+3.4%) +629 B -973 B

Total dist (raw): 36.23 MB (+68.8 kB (+0.2%))
Total ESM gzip: 114.4 kB (+3.0 kB (+2.7%))
Total web bundle (gzip): 264.5 kB (+11.4 kB (+4.5%))
Total node bundle (gzip): 266.0 kB (+10.5 kB (+4.1%))
Tracked ./bundler entry gzip: 563 B (no change)
Tracked ./bundler web bundle (gzip): 4.9 kB (no change)
Tracked ./bundler node bundle (gzip): 4.9 kB (no change)

Bundle sizes are generated with rslib (Rspack). Package-root metrics preserve the historical report. Tracked subpath exports such as ./bundler are measured separately so ENV_TARGET-driven tree-shaking is visible. Bare imports are externalized to keep package-level sizes consistent, and assets are emitted as resources.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06082e3cb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/node/src/runtimePlugin.ts Outdated
Comment on lines +204 to +207
filename,
importModuleDynamically:
//@ts-ignore
vm.constants?.USE_MAIN_CONTEXT_DEFAULT_LOADER ?? importNodeModule,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve a local referrer for native imports

On Node 24, when an HTTP-fetched chunk contains a native dynamic import()—for example, from an ESM external or a webpack-ignored import—USE_MAIN_CONTEXT_DEFAULT_LOADER resolves it using this HTTP URL as the referrer. Node rejects bare packages with ERR_INVALID_URL and relative specifiers with ERR_UNSUPPORTED_ESM_URL_SCHEME, whereas the previous direct eval resolved them from the local host module. Use a file-based identifier for module resolution, or supply an import callback that resolves Node imports from a local base while preserving the URL separately for diagnostics.

AGENTS.md reference: AGENTS.md:L289-L289

Useful? React with 👍 / 👎.

Every distinct script V8 compiles lands in an isolate-wide compilation cache
holding its source text and compiled code. The cache never ages out on its own
(40 full GCs and 400 MB of unrelated churn left it intact) and is evicted only
when the heap approaches V8's own limit, which defaults from physical memory
rather than the container's cgroup. A host that force-registers a genuinely new
remote build on each refresh therefore grew about 6.4 MB per refresh with a
3 MB chunk, regardless of whether the chunk was compiled with eval, vm.Script,
new Function or vm.compileFunction, and heap snapshots never showed it because
writing a snapshot clears the cache.

Wrap the two places that compile remote code, loadScriptNode in the sdk and
compileChunk in the node runtime plugin, in withoutCompilationCache(), which
flips --no-compilation-cache around the synchronous compile call and restores
the flag afterwards. FEDERATION_KEEP_COMPILATION_CACHE=true opts out.

Measured (3 MB chunk, unique build per forced refresh, 40 refreshes): heap
16.8 MB -> 18.0 MB with the change, 16.8 MB -> 272.9 MB with the opt-out.
Identical-build refreshes are unchanged at 18.0 MB. An uncached 2 MB compile
costs about 37 ms against 4 ms cached, paid once per remote execution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ScriptedAlchemy ScriptedAlchemy changed the title fix(node): compile fetched chunks with vm.Script instead of eval fix(node,sdk): compile remote code without eval and outside V8's compilation cache Sep 4, 2026
…flag is unavailable

--no-compilation-cache is not accepted in NODE_OPTIONS, so hosts that only
control environment variables (Lambda-style runtimes) cannot use the startup
flag, and runtimes without v8.setFlagsFromString cannot use the scoped toggle.
Add FEDERATION_COMPILATION_CACHE=flag|gc|off: flag (default) keeps the scoped
toggle; gc compiles normally and then runs one full garbage collection through
the inspector once the compile burst settles, which clears the whole cache
(71 ms on a small heap, ~540 ms on a 500 MB heap, against 28 s for a heap
snapshot); off leaves the cache alone. gc is also the automatic fallback when
setFlagsFromString is missing. The GC timer is unref'd and the inspector
session is disconnected after post() returns, since disconnecting from inside
the synchronous callback deadlocks the session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ScriptedAlchemy

Copy link
Copy Markdown
Member Author

Measurements behind this PR, from the harness described in #4566 (3 MB exposed chunk, heap after two full GCs).

Chunk loader. Direct eval made every chunk function capture the fetched source in its closure context, on top of the flattened wrapper V8 retains as script source. Compiling through vm.Script drops the second copy:

eval fix

Compilation cache. When each forced refresh loads a new build, V8 keeps every distinct compiled script; the growth is identical across compile APIs and invisible to heap snapshots (writing one clears the cache):

compile methods

new build per refresh

With the bypass, the same loop is flat; FEDERATION_KEEP_COMPILATION_CACHE=true restores the old behaviour, and under --max-old-space-size=200 V8 evicts the cache on its own near its limit:

with fix

ScriptedAlchemy and others added 4 commits September 4, 2026 23:01
…g the inspector session

HeapProfiler.collectGarbage completes asynchronously; disconnecting right after
post() cancelled it, which left the gc strategy ineffective. Disconnect on the
next macrotask after the completion callback instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Node's permission model (--permission) denies inspector.Session.connect with
ERR_ACCESS_DENIED. When that happens the gc strategy now uses an exposed gc()
with V8's last-resort flavor, which also clears the compilation cache and can be
enabled through NODE_OPTIONS=--expose-gc on env-only hosts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop the inspector-based and exposed-gc fallbacks; the compilation cache is
handled solely by flipping --no-compilation-cache around the synchronous
compile call, with FEDERATION_KEEP_COMPILATION_CACHE=true as the opt-out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Separate compilation from policy with a single implementation in the sdk:
`buildCommonJsWrapper` (wrapper shape), `compileCommonJsModule` (vm.Script
when vm is obtainable, otherwise new Function; errors never retried on the
other backend) and `withRemoteCompilationPolicy` (process-level manager for
V8's compilation cache flag: shared globalThis depth counter, toggled only on
0->1 and 1->0, skipped when the process already runs with
--no-compilation-cache or FEDERATION_REMOTE_COMPILATION_CACHE=default).
The helpers are re-exported through runtime-core and runtime so the node
plugin reaches them via __webpack_require__.federation.runtime, with a
new Function fallback for older runtimes. FEDERATION_KEEP_COMPILATION_CACHE
is removed. Adds sdk behaviour tests, node fallback and httpEvalStrategy
conformance tests, and a memory benchmark harness.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread packages/sdk/src/node.ts
parameters: string[],
source: string,
): string {
return `(function(${parameters.join(', ')}) {${source}\n})`;
ScriptedAlchemy and others added 3 commits September 6, 2026 20:13
A direct eval is cached by V8 under the calling script. When the sdk runs
inside a remote entry, each new build's copy evaluating eval('require') left an
eval-cache entry that pinned that entry's source for the life of the process
(measured ~0.2 MB per forced refresh with new builds). Resolve builtins once
per process on a shared global, preferring process.getBuiltinModule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, not federation.runtime

Remove the compile helpers from the runtime and runtime-core exports; they
are Node execution infrastructure, not runtime APIs. The sdk gains
compileRemoteCommonJsModule (policy + compile), which loadScriptNode and the
node runtime plugin use directly. The plugin no longer looks the helpers up on
__webpack_require__.federation.runtime and no longer carries a new Function
fallback for older runtimes: it compiles through its own dependency. That also
removes the silent fallback path where a remote bundled against an older
runtime namespace compiled chunks with no cache policy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants