fix(core): add SystemJS fallback for ESM server bundle execution - #8290
fix(core): add SystemJS fallback for ESM server bundle execution#8290SyMind wants to merge 11 commits into
Conversation
Deploying rsbuild with
|
| Latest commit: |
4de1ab5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://39df365c.rsbuild-v2.pages.dev |
| Branch Preview URL: | https://systemjs-runner.rsbuild-v2.pages.dev |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac9db0accd
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2159a4aec0
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a94c3b5eb
ℹ️ 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".
| setter( | ||
| this.#processImport( | ||
| namespace, | ||
| specifier, | ||
| registration.importMetadata[index], | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Preserve live bindings imported from external modules
When an external ESM mutates an exported let, its namespace getter updates, but this setter is invoked only once and SWC-generated setters copy the current property value into the bundle's local binding. For example, a bundle that imports { ready, mark }, calls external mark() to set ready = true, and then reads ready will still see false; external dependencies need a mechanism that preserves live imported bindings rather than a one-time setter call.
Useful? React with 👍 / 👎.
| for (const dependency of moduleNode.dependencies) { | ||
| if (!nextAncestors.has(dependency.id)) { | ||
| await this.#evaluateModule(dependency, nextAncestors); | ||
| } |
There was a problem hiding this comment.
Start independent async dependencies before awaiting them
When sibling bundle chunks both use top-level await, awaiting each recursive evaluation inside this loop prevents the second dependency from starting until the first has completely settled. Native ESM starts independent async siblings before waiting for their completion, so this fallback serializes their latency and changes observable execution ordering; schedule dependency evaluations first and await their completion collectively while retaining cycle handling.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds a new execution path for ESM server bundles when the runtime does not support vm.SourceTextModule, by evaluating bundles via a SystemJS (System.register) transform + runtime. This enhances compatibility across supported Node.js versions while keeping the existing ESM VM-based runner when available.
Changes:
- Make the runner factory async and select between
CommonJsRunner,EsmRunner, and the newSystemJsRunnerbased on bundle format andvm.SourceTextModuleavailability. - Introduce
SystemJsRunner/SystemJsEvaluatorplus an SWC-basedtransformToSystemJshelper for lazy ESM→SystemJS transformation with inline source maps. - Add unit tests covering import.meta behavior, source-map stack mapping, and missing static external export diagnostics.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/tests/systemJsRunner.test.ts | Adds tests validating SystemJS runner behavior (import.meta, source maps, missing exports). |
| packages/core/src/server/runner/type.ts | Updates runner factory interface to return Promise<Runner>. |
| packages/core/src/server/runner/systemJsTransform.ts | Adds SWC-based ESM→SystemJS transform with inline source map handling. |
| packages/core/src/server/runner/systemJs.ts | Implements SystemJS evaluator/runtime, module lifecycle, and external resolution. |
| packages/core/src/server/runner/index.ts | Updates runner selection logic to include CommonJS and SystemJS fallback, and awaits factory creation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Summary
This PR adds a SystemJS-based fallback for evaluating ESM server bundles when Node.js does not expose
vm.SourceTextModule.The server runner now selects the execution strategy based on the bundle format and runtime capabilities:
CommonJsRunnervm.SourceTextModuleavailableEsmRunnervm.SourceTextModuleunavailableSystemJsRunnerThis allows Rsbuild to execute ESM server output without requiring
--experimental-vm-modules, while preserving the existing ESM runner where it is available.Design
SystemJS transformation
Bundle files are transformed lazily from ESM to anonymous
System.registermodules using Rspack's bundled SWC transform.The transform:
No transformed bundle snapshot or persistent transform cache is introduced. Source is read through the existing runner options when a bundle module is registered.
Module registration and lifecycle
SystemJsEvaluatorimplements the runtime required by SWC-generatedSystem.registeroutput.Each module is represented by a
SystemJsModuleNodethat tracks:The evaluator captures one anonymous
System.registercall per bundle module and executes modules in three phases:ESM semantics
The evaluator implements the ESM behavior required by generated server bundles:
Symbol.toStringTag = 'Module';import.meta.urlis derived from the absolute bundle module path;SyntaxError.Module resolution
Bundle module IDs are normalized as absolute paths and must remain inside the configured output directory.
Dependencies are divided into two categories:
isBundleOutput, loaded throughreadFileSync, transformed, and evaluated bySystemJsEvaluator.import().Resolving externals from the bundle importer is important for applications whose dependencies are installed next to the application rather than next to
@rsbuild/core.Error handling and source maps
Runner errors use the
[rsbuild:runner]prefix and preserve the original error throughcause.The SWC transform emits an inline source map. Runtime frames produced by the SystemJS transform are mapped back to their corresponding bundle locations before the error is propagated.
Missing static external exports use an ESM-compatible error message:
Why not
module.registerHooks()A native ESM loader hook could serve bundle output directly from memfs, but it cannot provide safe cache invalidation across rebuilds:
module.registerHooks()was added in Node.js 22.15.0, while@rsbuild/coresupports Node.js^20.19.0 || >=22.12.0.require.cache. Reusing a stable URL such as/dist/index.jsafter a rebuild returns the previous module without calling theloadhook again./${timestamp}/dist/index.js, loads the latest source but creates a new cached module graph on every rebuild. Node.js exposes no public API for evicting these ESM cache entries, so old module namespaces and their retained state remain alive and memory usage can grow throughout a long-running watch session.SystemJsEvaluatorinstead owns a runner-scoped module cache. Replacing the runner releases the whole module graph for garbage collection, while still allowing bundle source to be read from memfs.References: Node.js
module.registerHooks(), ES modules are cached as URLs, and ESM uses a separate cache.Related Links