-
-
Notifications
You must be signed in to change notification settings - Fork 30
feat(core): add Module Federation compatibility mode #1407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ScriptedAlchemy
wants to merge
17
commits into
main
Choose a base branch
from
feat/federation-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
21e4f49
feat(core): add federation compatibility mode for node tests
f1473ad
test(e2e): cover federation compatibility mode
48b1091
feat(examples): add module federation example apps
64d7d65
docs: add federation config documentation
40086e4
chore(examples): bump @module-federation/rstest canary to 1b77b1a
510f431
fix(core): harden federation runtime shims after review
af97a66
fix(core): normalize absolute paths in federation dynamic import fall…
bedbf1a
chore(examples): bump @module-federation/rstest canary to e87b169
773bf1f
fix(core): reinstall federation dynamic import hook per test file
93e69c2
chore(examples): bump @module-federation/rstest canary to 5a6de69
faaa146
docs: clarify federation support scope
309d3b9
chore(examples): remove federation gitignore
7d3e861
chore(examples): use Rsbuild for federation remotes
a8417c4
fix(core): refresh global setup federation hook
511e893
chore: sync module federation canary
dde8b2a
fix(core): preserve federation dynamic import origins
4e9cb4f
fix(core): resolve federation fallback packages from origin
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Loaded through the `__rstest_dynamic_import__` global fallback by absolute | ||
| // filesystem path — the way vm-evaluated Module Federation chunks resolve | ||
| // externalized dynamic imports. Must stay un-bundled (plain .mjs on disk). | ||
| export const answer = 42; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { expect, it } from '@rstest/core'; | ||
|
|
||
| it('should expose the federation flag and dynamic import fallback', async () => { | ||
| expect((globalThis as any).__rstest_federation__).toBe(true); | ||
|
|
||
| const dynamicImport = (globalThis as any).__rstest_dynamic_import__; | ||
| expect(typeof dynamicImport).toBe('function'); | ||
|
|
||
| // The fallback must load modules via native dynamic import, the way | ||
| // vm-evaluated Module Federation runtime chunks rely on it. | ||
| const pathModule = await dynamicImport('node:path'); | ||
| expect(typeof pathModule.join).toBe('function'); | ||
| }); | ||
|
|
||
| it('should load absolute file paths through the dynamic import fallback', async () => { | ||
| const dynamicImport = (globalThis as any).__rstest_dynamic_import__; | ||
|
|
||
| // Federated async-node chunks call the fallback with raw absolute paths | ||
| // (`C:\...` on Windows), which must be normalized to `file://` URLs before | ||
| // they reach native `import()`. | ||
| const { join } = await import('node:path'); | ||
| const mod = await dynamicImport(join(__dirname, 'absolute-target.mjs')); | ||
| expect(mod.answer).toBe(42); | ||
| }); | ||
|
|
||
| it('should set the federation flag during global setup', () => { | ||
| expect(process.env.RSTEST_E2E_FEDERATION_IN_SETUP).toBe('true'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "e2e-federation-basic", | ||
| "private": true, | ||
| "type": "module" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { defineConfig } from '@rstest/core'; | ||
|
|
||
| export default defineConfig({ | ||
| federation: true, | ||
| globalSetup: ['./setup.ts'], | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export const setup = (): void => { | ||
| // Record the worker-wide flag so the test can assert it was already set | ||
| // while global setup code ran. | ||
| process.env.RSTEST_E2E_FEDERATION_IN_SETUP = String( | ||
| (globalThis as any).__rstest_federation__, | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { expect, it } from '@rstest/core'; | ||
|
|
||
| it('should enable federation mode from the CLI flag', () => { | ||
| expect((globalThis as any).__rstest_federation__).toBe(true); | ||
| expect(typeof (globalThis as any).__rstest_dynamic_import__).toBe('function'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "e2e-federation-cli", | ||
| "private": true, | ||
| "type": "module" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { expect, it } from '@rstest/core'; | ||
|
|
||
| it('should not install federation shims by default', () => { | ||
| expect((globalThis as any).__rstest_federation__).toBe(false); | ||
| expect((globalThis as any).__rstest_dynamic_import__).toBeUndefined(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "e2e-federation-disabled", | ||
| "private": true, | ||
| "type": "module" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { describe, it } from '@rstest/core'; | ||
| import { runRstestCli } from '../scripts'; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
|
|
||
| describe('federation', () => { | ||
| it('should install federation runtime shims when enabled in config', async () => { | ||
| const { expectExecSuccess } = await runRstestCli({ | ||
| command: 'rstest', | ||
| args: ['run'], | ||
| options: { | ||
| nodeOptions: { | ||
| cwd: join(__dirname, 'fixtures/basic'), | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| await expectExecSuccess(); | ||
| }); | ||
|
|
||
| it('should not install federation runtime shims by default', async () => { | ||
| const { expectExecSuccess } = await runRstestCli({ | ||
| command: 'rstest', | ||
| args: ['run'], | ||
| options: { | ||
| nodeOptions: { | ||
| cwd: join(__dirname, 'fixtures/disabled'), | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| await expectExecSuccess(); | ||
| }); | ||
|
|
||
| it('should enable federation mode via the --federation CLI flag', async () => { | ||
| const { expectExecSuccess } = await runRstestCli({ | ||
| command: 'rstest', | ||
| args: ['run', '--federation'], | ||
| options: { | ||
| nodeOptions: { | ||
| cwd: join(__dirname, 'fixtures/cli'), | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| await expectExecSuccess(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Module Federation example | ||
|
|
||
| [中文](./README_zh-cn.md) | ||
|
|
||
| A Module Federation case with React, built with Rspack and tested with Rstest's `federation` compatibility mode. | ||
|
|
||
| ## Project directory | ||
|
|
||
| ### component-app | ||
|
|
||
| Exposes UI components (`Button`, `Dialog`, `Logo`, `ToolTip`) via Module Federation. It is a pure `remote`, with both a browser build (`dist/`) and a Node-targeted build (`dist-node/`) for server-side consumption. | ||
|
|
||
| ### main-app | ||
|
|
||
| The top-level app, which consumes `component-app` over HTTP and `node-local-remote` via a local CommonJS path. It is a pure `host`. | ||
|
|
||
| ### node-local-remote | ||
|
|
||
| A minimal Node-targeted remote consumed directly from its built `remoteEntry.js` on disk, without an HTTP server. | ||
|
|
||
| ## How to use | ||
|
|
||
| - `pnpm install` | ||
| - `pnpm run start` | ||
|
|
||
| After running these commands, open your browser at `http://localhost:3002` and open the DevTools network tab to see resource loading details. | ||
|
|
||
| ## Testing | ||
|
|
||
| - `pnpm run test` runs the Rstest suites of `main-app` (jsdom host consuming both remotes) and `component-app` (Node SSR against the local remote). | ||
| - Both projects enable `federation: true` in `rstest.config.ts` and configure Module Federation through the `@module-federation/rstest` plugin. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Module Federation 示例 | ||
|
ScriptedAlchemy marked this conversation as resolved.
Outdated
|
||
|
|
||
| [English](./README.md) | ||
|
|
||
| 一个基于 React 的 Module Federation 项目案例,使用 Rspack 构建,并通过 Rstest 的 `federation` 兼容模式进行测试。 | ||
|
|
||
| ## 目录结构 | ||
|
|
||
| ### component-app | ||
|
|
||
| 通过 Module Federation 暴露 UI 组件(`Button`、`Dialog`、`Logo`、`ToolTip`)。它是一个纯粹的 `remote`,同时提供浏览器构建(`dist/`)和面向 Node 的构建(`dist-node/`)用于服务端消费。 | ||
|
|
||
| ### main-app | ||
|
|
||
| 上层 App,通过 HTTP 消费 `component-app`,并通过本地 CommonJS 路径消费 `node-local-remote`。它是一个纯粹的 `host`。 | ||
|
|
||
| ### node-local-remote | ||
|
|
||
| 一个最小化的 Node 端 remote,直接从磁盘上构建出的 `remoteEntry.js` 消费,无需启动 HTTP 服务。 | ||
|
|
||
| ## 如何使用 | ||
|
|
||
| - `pnpm install` | ||
| - `pnpm run start` | ||
|
|
||
| 执行完上述命令后,打开浏览器访问 `http://localhost:3002`,并打开 DevTools 的 Network 面板查看资源加载详情。 | ||
|
|
||
| ## 测试 | ||
|
|
||
| - `pnpm run test` 会运行 `main-app`(jsdom 环境的 host,消费两个 remote)和 `component-app`(基于本地 remote 的 Node SSR)的 Rstest 测试。 | ||
| - 两个项目都在 `rstest.config.ts` 中开启了 `federation: true`,并通过 `@module-federation/rstest` 插件配置 Module Federation。 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import React from 'react'; | ||
| import Button from './src/Button'; | ||
| import Dialog from './src/Dialog'; | ||
| import Logo from './src/Logo'; | ||
| export default class App extends React.Component { | ||
| constructor(props) { | ||
| super(props); | ||
| this.state = { | ||
| dialogVisible: false, | ||
| }; | ||
| this.handleClick = this.handleClick.bind(this); | ||
| this.handleSwitchVisible = this.handleSwitchVisible.bind(this); | ||
| } | ||
| handleClick(ev) { | ||
| console.log(ev); | ||
| this.setState({ | ||
| dialogVisible: true, | ||
| }); | ||
| } | ||
| handleSwitchVisible(visible) { | ||
| this.setState({ | ||
| dialogVisible: visible, | ||
| }); | ||
| } | ||
| render() { | ||
| return ( | ||
| <div> | ||
| <Logo /> | ||
| <br /> | ||
| <Button /> | ||
| <br /> | ||
|
|
||
| <button type="button" onClick={this.handleClick}> | ||
| click to open dialog | ||
| </button> | ||
| <Dialog | ||
| switchVisible={this.handleSwitchVisible} | ||
| visible={this.state.dialogVisible} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import React from 'react'; | ||
| import { createRoot } from 'react-dom/client'; | ||
| import App from './App'; | ||
|
|
||
| createRoot(document.getElementById('app')).render(<App />); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| import('./bootstrap.js'); |
|
ScriptedAlchemy marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "@examples/federation-component-app", | ||
| "private": true, | ||
| "scripts": { | ||
| "build": "rspack build --config rspack.browser.config.js", | ||
| "build:node": "rspack build --config rspack.node.config.js", | ||
| "serve": "serve dist -p 3001", | ||
| "serve:node": "serve dist-node -p 3001", | ||
| "start": "concurrently \"npm run build\" \"npm run serve\"", | ||
| "pretest": "pnpm -C ../node-local-remote build:node", | ||
| "test": "rstest --reporter=verbose" | ||
| }, | ||
| "dependencies": { | ||
| "@module-federation/node": "2.7.32", | ||
| "react": "^19.2.7", | ||
| "react-dom": "^19.2.7" | ||
| }, | ||
| "devDependencies": { | ||
| "@module-federation/enhanced": "https://pkg.pr.new/module-federation/core/@module-federation/enhanced@3fe0a1a3b6b5eab3d2d8eba2e4fd554fc1bda214", | ||
| "@module-federation/rstest": "https://pkg.pr.new/module-federation/core/@module-federation/rstest@5a6de69", | ||
| "@rsbuild/plugin-react": "^2.0.1", | ||
| "@rspack/cli": "~2.0.8", | ||
| "@rspack/core": "~2.0.8", | ||
| "@rstest/core": "workspace:*", | ||
| "concurrently": "8.2.2", | ||
| "serve": "14.2.3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Document</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| </body> | ||
| </html> |
62 changes: 62 additions & 0 deletions
62
examples/federation/component-app/rspack.browser.config.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| const { HtmlRspackPlugin } = require('@rspack/core'); | ||
| const { | ||
| ModuleFederationPlugin, | ||
| } = require('@module-federation/enhanced/rspack'); | ||
|
|
||
| module.exports = { | ||
| entry: './index.js', | ||
| mode: 'development', | ||
| devtool: 'hidden-source-map', | ||
| output: { | ||
| publicPath: 'http://localhost:3001/', | ||
| clean: true, | ||
| }, | ||
| resolve: { | ||
| extensions: ['.jsx', '.js', '.json', '.wasm'], | ||
| }, | ||
| experiments: { | ||
| css: true, | ||
| }, | ||
| module: { | ||
| rules: [ | ||
| { test: /\.(jpg|png|gif|jpeg)$/, type: 'asset/resource' }, | ||
| { | ||
| test: /\.css$/, | ||
| type: 'css/auto', | ||
| }, | ||
| { | ||
| test: /\.(js|jsx)$/, | ||
| use: { | ||
| loader: 'builtin:swc-loader', | ||
| options: { | ||
| jsc: { | ||
| parser: { syntax: 'ecmascript', jsx: true }, | ||
| transform: { react: { runtime: 'automatic' } }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| plugins: [ | ||
| new ModuleFederationPlugin({ | ||
| name: 'component_app', | ||
| experiments: { | ||
| asyncStartup: true, | ||
| }, | ||
| filename: 'remoteEntry.js', | ||
| library: { type: 'var', name: 'component_app' }, | ||
| exposes: { | ||
| './Button': './src/Button.jsx', | ||
| './Dialog': './src/Dialog.jsx', | ||
| './Logo': './src/Logo.jsx', | ||
| './ToolTip': './src/ToolTip.jsx', | ||
| }, | ||
| shared: { | ||
| react: { singleton: true, requiredVersion: '^19.2.0' }, | ||
| 'react-dom': { singleton: true, requiredVersion: '^19.2.0' }, | ||
| }, | ||
| }), | ||
| new HtmlRspackPlugin({ template: './public/index.html' }), | ||
| ], | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.