Skip to content

feat(core): add Module Federation compatibility mode#1407

Open
ScriptedAlchemy wants to merge 41 commits into
mainfrom
feat/federation-v2
Open

feat(core): add Module Federation compatibility mode#1407
ScriptedAlchemy wants to merge 41 commits into
mainfrom
feat/federation-v2

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Contributor

Summary

Rstest's Node test runtime overrides import() and webpack chunk handlers for mocking, which breaks tests that load Module Federation remotes. This PR adds an opt-in federation compatibility mode so federated apps can be tested in Node:

  • New federation: boolean config option and --federation CLI flag.
  • Runtime shims in mockRuntimeCode.js, gated on globalThis.__rstest_federation__: a placeholder chunk-handler bootstrap, an __rstest_dynamic_import__ fallback for non-mocked specifiers, and a __webpack_require__.f Proxy guard so federation's chunk loaders coexist with the mock runtime.
  • Federation example apps (examples/federation), 8 unit tests, an e2e suite with 3 fixtures, and bilingual docs.

This mode pairs with the @module-federation/rstest Rsbuild plugin (module-federation/core#4320); the examples pin its latest pkg.pr.new canary (1b77b1a). Browser mode is unaffected. Supersedes #842.

Validation: lint, typecheck, build, unit tests, e2e, and federation examples all pass locally (webkit e2e skipped: missing system deps).

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

ScriptedAlchemy added 5 commits June 12, 2026 00:55
Comment thread packages/core/src/core/plugins/mockRuntimeCode.js Fixed
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying rstest with  Cloudflare Pages  Cloudflare Pages

Latest commit: 11b1872
Status: ✅  Deploy successful!
Preview URL: https://bea0ade3.rstest.pages.dev
Branch Preview URL: https://feat-federation-v2.rstest.pages.dev

View logs

@github-actions

Copy link
Copy Markdown

pkg.pr.new preview 40086e4 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@40086e4
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@40086e4
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@40086e4
  • pnpm add https://pkg.pr.new/@rstest/browser@40086e4
  • pnpm add https://pkg.pr.new/@rstest/browser-react@40086e4
  • pnpm add https://pkg.pr.new/@rstest/core@40086e4
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@40086e4
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@40086e4

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Rsdoctor Bundle Diff Analysis

Found 13 projects in monorepo, 4 projects with changes.

📊 Quick Summary
Project Total Size Change
adapter-rsbuild 5.4 KB 0
adapter-rslib 25.6 KB 0
adapter-rspack 9.3 KB 0
browser 2.0 MB +95.0 B (0.0%)
browser-react 6.4 KB 0
browser-ui 809.6 KB +59.0 B (0.0%)
coverage-istanbul 18.8 KB 0
coverage-v8 19.4 KB 0
core/browser 1.1 MB +316.0 B (0.0%)
core/loaders 869.0 B 0
core/main 1.9 MB +3.3 KB (0.2%)
vscode/extension 27.0 MB 0
vscode/worker 14.5 KB 0
📋 Detailed Reports (Click to expand)

📁 browser

Path: packages/browser/.rsdoctor/rsdoctor-data.json

📌 Baseline Commit: bfc0f2255f | PR: #1410

Metric Current Baseline Change
📊 Total Size 2.0 MB 2.0 MB +95.0 B (0.0%)
📄 JavaScript 2.0 MB 2.0 MB +91.0 B (0.0%)
🎨 CSS 11.8 KB 11.8 KB 0
🌐 HTML 765.0 B 763.0 B +2.0 B (0.3%)
📁 Other Assets 40.5 KB 40.5 KB +2.0 B (0.0%)

📦 Download Diff Report: browser Bundle Diff

📁 browser-ui

Path: packages/browser-ui/.rsdoctor/rsdoctor-data.json

📌 Baseline Commit: bfc0f2255f | PR: #1410

Metric Current Baseline Change
📊 Total Size 809.6 KB 809.5 KB +59.0 B (0.0%)
📄 JavaScript 781.0 KB 781.0 KB +57.0 B (0.0%)
🎨 CSS 11.8 KB 11.8 KB 0
🌐 HTML 765.0 B 763.0 B +2.0 B (0.3%)
📁 Other Assets 16.0 KB 16.0 KB 0

📦 Download Diff Report: browser-ui Bundle Diff

📁 core/browser

Path: packages/core/.rsdoctor/browser/rsdoctor-data.json

📌 Baseline Commit: bfc0f2255f | PR: #1410

Metric Current Baseline Change
📊 Total Size 1.1 MB 1.1 MB +316.0 B (0.0%)
📄 JavaScript 970.0 KB 970.0 KB 0
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 114.9 KB 114.6 KB +316.0 B (0.3%)

📦 Download Diff Report: core/browser Bundle Diff

📁 core/main

Path: packages/core/.rsdoctor/main/rsdoctor-data.json

📌 Baseline Commit: bfc0f2255f | PR: #1410

Metric Current Baseline Change
📊 Total Size 1.9 MB 1.9 MB +3.3 KB (0.2%)
📄 JavaScript 1.6 MB 1.6 MB +3.0 KB (0.2%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 292.3 KB 291.9 KB +379.0 B (0.1%)

📦 Download Diff Report: core/main Bundle Diff

Generated by Rsdoctor GitHub Action

@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: 40086e4ff7

ℹ️ 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/core/src/runtime/worker/runInPool.ts Outdated
Comment thread packages/core/src/core/plugins/mockRuntimeCode.js
- drop the stale `__rstest_dynamic_import__` global when a reused worker
  (`isolate: false`) runs a non-federation file after a federation one
- wrap a pre-existing `__webpack_require__.f` with the same chunk-handler
  guard proxy so the Module Federation node runtime cannot overwrite
  `readFileVm` / `require` in that runtime-module ordering
- remove the dead local `__rstest_dynamic_import__` binding flagged by code
  scanning: the runtime module is IIFE-isolated, so only the global is
  observable

@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: 510f431fba

ℹ️ 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/core/src/core/plugins/mockRuntimeCode.js Outdated
ScriptedAlchemy pushed a commit to module-federation/core that referenced this pull request Jun 12, 2026
State that federation: true requires the unreleased @rstest/core federation
build (web-infra-dev/rstest#1407, pkg.pr.new canary), declare the
rstest:federation plugin name a stable public contract, and reword the
provenance blurb: the package is rstest federation mode's companion plugin,
not an extraction from @rstest/core.
ScriptedAlchemy pushed a commit to module-federation/core that referenced this pull request Jun 12, 2026
Install the pkg.pr.new canaries from web-infra-dev/rstest#1407 (commit
40086e4) for @rstest/core, browser, browser-react, and the rsbuild/rslib
adapters so the demos' federation: true flag exercises real rstest
federation support instead of being a silent no-op on 0.8.x. Bump
@rsbuild/core and @rsbuild/plugin-react to the 2.x line the canary uses
and document the required pairing in the demo configs.
ScriptedAlchemy added 2 commits June 12, 2026 03:24
…back

Raw absolute specifiers (`C:\...`) reach native `import()` when federated
async-node chunks call the global fallback; convert them through
`pathToFileURL` like `resolveDynamicImport.ts` does so Windows drive
letters are not parsed as URL schemes.

@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: bedbf1a056

ℹ️ 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/core/src/core/plugins/mockRuntimeCode.js Outdated
The hook closure binds `import()` to the vm dynamic-import context of the
bundle that installed it, so a worker reused with `isolate: false` must not
carry the previous file's hook into the next federation-enabled file. Drop
it unconditionally in `preparePool`; `mockRuntimeCode.js` reinstalls a
fresh one bound to the current bundle.

@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: 773bf1f044

ℹ️ 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/core/src/core/plugins/mockRuntimeCode.js
@github-actions

Copy link
Copy Markdown

pkg.pr.new preview 773bf1f · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/browser@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/browser-react@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/core@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@773bf1f
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@773bf1f

Comment thread website/docs/en/config/test/federation.mdx
Comment thread examples/federation/.gitignore Outdated
Comment thread examples/federation/component-app/package.json

@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: 309d3b9b59

ℹ️ 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/core/src/runtime/worker/globalSetupWorker.ts Outdated
Comment thread .github/workflows/test.yml Outdated
Comment thread examples/federation/README_zh-cn.md Outdated
Comment thread examples/federation/component-app/test/Button.ssr.test.ts
Comment thread packages/core/src/runtime/worker/runInPool.ts Outdated
@ScriptedAlchemy

Copy link
Copy Markdown
Contributor Author

RFC: Module Federation Compatibility Mode

This PR adds an opt-in Module Federation compatibility mode for Rstest's
Node-based runner.

Related work:

Summary

Federated tests have two runtimes sharing responsibility for module loading:

  • Rstest owns the test worker runtime: mocks, module cache resets, setup files,
    global setup, dynamic import interception, source maps, and worker isolation.
  • Module Federation owns remote containers, shared scope initialization, remote
    entry loading, runtime plugins, and async chunk loading.

Without a compatibility layer, Module Federation's Node runtime can evaluate
remote entries and chunks through fs, vm, eval, HTTP fetch, or native
import() paths that bypass the Rstest bundle runtime. That breaks the
assumption that test code, mocks, and dynamically loaded chunks all execute
inside the same controlled worker runtime.

The contract in this PR is intentionally small:

  • Rstest exposes federation: true and --federation to prepare the test
    runtime.
  • @module-federation/rstest supplies the Module Federation build/runtime
    defaults for Rstest builds.
  • App builds continue to use the normal app-side Module Federation plugins, such
    as @module-federation/rsbuild-plugin.

Why this needs a first-class mode

Rstest rewrites and bundles test modules so APIs such as rs.mock,
rs.unmock, rs.resetModules, dynamic import interception, and global setup can
work predictably.

In ordinary test bundles, dynamic imports are routed through Rstest's injected
dynamic import hook. That hook preserves the import origin and keeps resolution
aligned with Rstest's mock and module-cache state.

Module Federation changes the graph shape:

  • A host can load an HTTP remote entry.
  • A host can load a local Node remote entry.
  • A remote entry can load more chunks through Rspack's async-node runtime.
  • @module-federation/node runtime plugins can patch chunk handlers.
  • Chunks evaluated through vm or eval do not receive Rstest's usual
    function-argument-injected dynamic import hook.

So a single test can cross from "Rstest-controlled code" into
"Federation-controlled code" and back. Compatibility mode makes that boundary
explicit and stable.

Runtime model

flowchart LR
  A["Rstest config: federation: true"] --> B["Worker/global setup sets globalThis.__rstest_federation__"]
  B --> C["mockRuntimeCode installs federation shims"]
  C --> D["Rstest mock/runtime ownership remains active"]
  E["@module-federation/rstest"] --> F["ModuleFederationPlugin plus Rstest-safe defaults"]
  F --> G["Remote entries and chunks load in Node-compatible form"]
  D --> H["Federated test executes inside the Rstest worker"]
  G --> H
Loading

federation: true does not configure remotes, exposes, or shared dependencies.
It only enables Rstest's runtime compatibility behavior. The Module Federation
graph stays in the bundler plugin.

Runtime requirements

1. Opt-in behavior

federation defaults to false. Users opt in through config:

import { defineConfig } from '@rstest/core';

export default defineConfig({
  federation: true,
});

or through the CLI for one-off runs:

rstest run --federation

This keeps existing Node, jsdom, happy-dom, and browser-mode behavior unchanged.
Browser Mode is not enabled by this flag. The flag applies to Rstest's
Node-based runner, including DOM-simulated environments that still execute in
Node workers.

2. The runtime flag must be set before user code

The worker sets globalThis.__rstest_federation__ before evaluating:

  • global setup bundles
  • setup files
  • test bundles
  • federation runtime chunks reached from those bundles

The runtime module gates its shims on that flag, so setting it late would make
behavior depend on load order.

3. Dynamic import fallback must preserve import origins

Federated async-node chunks can call __rstest_dynamic_import__ from code that
was evaluated by vm or eval. Those chunks do not receive the normal injected
hook, so compatibility mode provides a globalThis.__rstest_dynamic_import__
fallback.

The fallback must:

  • load Node builtins directly
  • normalize absolute filesystem paths through pathToFileURL
  • resolve relative specifiers against the injected origin file
  • resolve bare specifiers through createRequire(origin)
  • fall back to native import() when no origin is available

This keeps federation chunk loading aligned with the same origin-aware dynamic
import model Rstest uses for non-federated code.

4. Worker reuse must not leak stale hooks

With isolate: false, multiple test files can reuse the same worker. The
worker now clears the global dynamic import fallback before each file so the
current bundle reinstalls a hook bound to the current file's dynamic-import
context.

That prevents a federation file from leaving a stale fallback behind for later
files, and prevents later federation entries from reusing an origin from an
earlier bundle.

5. Federation chunk handlers must not bypass Rstest

Module Federation can install placeholder chunk handlers such as consumes and
remotes before its runtime fully initializes. In test bundles, those
placeholders can throw too early.

Compatibility mode no-ops only the known throwing placeholders and allows the
real federation runtime to replace them later.

It also protects Rstest-owned chunk-loading handlers. If the Module Federation
Node runtime tries to overwrite handlers such as
__webpack_require__.f.readFileVm or __webpack_require__.f.require, remote
chunks can be evaluated outside the Rstest runtime. The guard preserves the
first initialized handler and warns on later overwrite attempts.

6. Dynamic external imports must still respect mocks

Dynamic import(request) of an external module can produce a different module
id from the id patched by a hoisted rs.mock(request). The runtime keeps a
request-to-mocked-id alias so dynamic external imports resolve to the mocked
factory when a mock exists, and to the original module when it does not.

This matters for federated code because remote containers and runtime plugins
often cross module boundaries through dynamic imports.

Build-side split

The companion @module-federation/rstest package is the build-side half for
Rstest test builds. It provides a Rsbuild plugin that:

  • injects ModuleFederationPlugin
  • expects to run under Rstest caller names (rstest or rstest-browser)
  • defaults Rstest builds to Node-compatible federation output unless browser
    target is requested
  • disables test-unfriendly defaults such as DTS and manifest generation
  • enables async startup
  • for Node target, applies async-node output defaults, the node runtime plugin,
    CommonJS-compatible container output, and federation externals bypasses

This keeps Rstest from becoming a Module Federation configuration surface. Rstest
only supplies the test-runtime switch. Module Federation remains responsible for
remotes, exposes, shared scope, runtime plugins, and target-specific bundler
defaults.

For standalone application or remote prebuilds, examples should use the normal
app-side federation plugin, such as @module-federation/rsbuild-plugin. The
@module-federation/rstest plugin is for the Rstest host/test build, not for
replacing every app build configuration.

Example shape

import { federation } from '@module-federation/rstest';
import { defineConfig } from '@rstest/core';

export default defineConfig({
  federation: true,
  plugins: [
    federation({
      name: 'main_app',
      remotes: {
        'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
      },
    }),
  ],
});

Non-goals

  • This does not auto-discover or configure remotes.
  • This does not enable Rstest Browser Mode.
  • This does not replace @module-federation/rsbuild-plugin for app builds.
  • This does not make every possible remote-loading shape supported. It defines
    the Rstest and Module Federation runtime contract needed by the supported
    Node-oriented flows.

Validation in this PR

The PR includes coverage for the compatibility contract:

  • config and CLI opt-in
  • disabled-by-default behavior
  • global setup federation flag propagation
  • global dynamic import fallback installation
  • absolute path normalization for native import()
  • origin-aware relative and bare package resolution
  • isolate: false worker reuse
  • mockRuntimeCode.js federation shims
  • examples for HTTP remotes, local Node remotes, SSR-style tests, jsdom host
    tests, and CSR app builds
  • English and Chinese docs for the federation option

Reviewer checklist

  • Is the public API small enough: one boolean config option plus one CLI flag?
  • Is the split between Rstest runtime compatibility and Module Federation build
    configuration clear?
  • Are the runtime shims sufficiently gated behind federation: true?
  • Do the examples show the intended user path with @module-federation/rstest
    for test builds and app-side federation plugins for app builds?
  • Are Browser Mode and unsupported remote-loading shapes scoped clearly enough?

@9aoy

9aoy commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

commonjs e2e test failed ⬇️

❌ FAIL federation/index.test.ts > federation > should install federation runtime shims when enabled in config (retry x2)
Error: Test failed with exit code 1. Logs:

Rstest v0.10.6
✗ reuse.test.ts (1)
✗ should reinstall the federation dynamic import fallback for reused workers (3ms)
Cannot find module '/Users/runner/work/rstest/rstest/e2e/federation/fixtures/basic/dist/.rstest-temp/reuse-target.mjs' imported from /Users/runner/work/rstest/rstest/packages/core/dist/0~resolveDynamicImport.js

Repro command:

cd e2e && RSTEST_OUTPUT_MODULE=false pnpm exec rstest 'federation/index.test.ts' --testNamePattern 'federation > should install federation runtime shims when enabled in config'

https://github.com/web-infra-dev/rstest/actions/runs/28143939302/job/83347403961

@ScriptedAlchemy

Copy link
Copy Markdown
Contributor Author

@9aoy fixed, thanks for the repro.

Root cause: in the CommonJS / RSTEST_OUTPUT_MODULE=false path, a federation chunk could call the global __rstest_dynamic_import__ fallback without the injected third-argument origin. The fallback then resolved relative imports from the runtime/test bundle context, which is why ./reuse-target.mjs was looked up under dist/.rstest-temp / packages/core/dist/0~resolveDynamicImport.js instead of the current test file origin.

The fix adds a worker-global current-origin hook (__rstest_dynamic_import_origin__) and refreshes it before global setup, setup files, and test entries. The fallback now uses origin || globalThis.__rstest_dynamic_import_origin__ for relative/bare specifier resolution, so CJS federation chunks resolve from the active entry.

Verified with the exact repro:

cd e2e && RSTEST_OUTPUT_MODULE=false pnpm exec rstest 'federation/index.test.ts' --testNamePattern 'federation > should install federation runtime shims when enabled in config'

Also rechecked the latest PR head (edc4ee1): macOS/Windows unit and e2e checks are green.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

pkg.pr.new preview 647b5d0 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/browser@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/browser-react@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/core@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@647b5d0
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@647b5d0

@9aoy

9aoy commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@ScriptedAlchemy hey, the main branch now supports modifying Rstest config from an Rsbuild plugin. It hasn’t been released to npm yet, but if you’re interested, you can merge the main branch, give it a try, and help us test this feature 👀 .

https://rstest.rs/config/build/plugins#modify-rstest-config

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.

3 participants