Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
21e4f49
feat(core): add federation compatibility mode for node tests
Jun 11, 2026
f1473ad
test(e2e): cover federation compatibility mode
Jun 11, 2026
48b1091
feat(examples): add module federation example apps
Jun 11, 2026
64d7d65
docs: add federation config documentation
Jun 11, 2026
40086e4
chore(examples): bump @module-federation/rstest canary to 1b77b1a
Jun 12, 2026
510f431
fix(core): harden federation runtime shims after review
Jun 12, 2026
af97a66
fix(core): normalize absolute paths in federation dynamic import fall…
Jun 12, 2026
bedbf1a
chore(examples): bump @module-federation/rstest canary to e87b169
Jun 12, 2026
773bf1f
fix(core): reinstall federation dynamic import hook per test file
Jun 12, 2026
93e69c2
chore(examples): bump @module-federation/rstest canary to 5a6de69
Jun 12, 2026
faaa146
docs: clarify federation support scope
Jun 12, 2026
309d3b9
chore(examples): remove federation gitignore
Jun 12, 2026
7d3e861
chore(examples): use Rsbuild for federation remotes
Jun 12, 2026
a8417c4
fix(core): refresh global setup federation hook
Jun 12, 2026
511e893
chore: sync module federation canary
Jun 12, 2026
dde8b2a
fix(core): preserve federation dynamic import origins
Jun 12, 2026
4e9cb4f
fix(core): resolve federation fallback packages from origin
Jun 12, 2026
94ce23d
Merge remote-tracking branch 'origin/main' into feat/federation-v2
Jun 23, 2026
c6e4bbf
chore(examples): align federation example rsbuild versions
Jun 23, 2026
b917dca
test(e2e): cover federation with disabled isolation
Jun 24, 2026
1803e97
Merge remote-tracking branch 'origin/main' into feat/federation-v2
Jun 26, 2026
665d226
fix(core): resolve federation fallback imports from worker origin
Jun 26, 2026
1033ae4
chore(examples): align federation rsbuild dependency
Jun 26, 2026
9273d2b
chore: dedupe lockfile after rsbuild upgrade
Jun 26, 2026
edc4ee1
docs: fix federation added version
Jun 26, 2026
304cf54
Merge origin/main into feat/federation-v2
Jun 29, 2026
214e258
Merge remote PR updates into feat/federation-v2
Jun 29, 2026
f430d09
fix(core): satisfy federation runtime type checks
Jun 29, 2026
29019da
chore(examples): align federation rsbuild core versions
Jun 29, 2026
cbd5fc8
chore: dedupe federation lockfile
Jun 29, 2026
3bed4e8
refactor(core): reuse federation asset lookup
Jun 29, 2026
41ebe18
Merge remote-tracking branch 'origin/main' into feat/federation-v2
Jun 30, 2026
56f1a19
refactor(core): clarify federation runtime hooks
Jun 30, 2026
647b5d0
Merge remote-tracking branch 'origin/main' into feat/federation-v2
Jul 2, 2026
ee68ea7
chore(examples): adopt module-federation core canary a48a1fa
Jul 2, 2026
e3c7808
chore(examples): dedupe federation lockfile for core canary a48a1fa
Jul 2, 2026
ec96ba3
chore: dedupe lockfile
Jul 2, 2026
a64980b
Merge remote-tracking branch 'origin/main' into feat/federation-v2
Jul 2, 2026
bef12d1
Merge remote-tracking branch 'origin/main' into feat/federation-v2
ScriptedAlchemy Jul 5, 2026
1d7a1e1
Merge remote-tracking branch 'origin/main' into feat/federation-v2
ScriptedAlchemy Jul 5, 2026
11b1872
Merge remote-tracking branch 'origin/feat/federation-v2' into feat/fe…
ScriptedAlchemy Jul 5, 2026
409e8e9
Merge remote-tracking branch 'origin/main' into feat/federation-v2
ScriptedAlchemy Jul 21, 2026
455e5a5
fix(core): preserve federation output mode after plugin config
ScriptedAlchemy Jul 21, 2026
af970e3
test(federation): use latest integration canary
ScriptedAlchemy Jul 22, 2026
78f4f4d
fix(core): address federation runtime review
ScriptedAlchemy Jul 22, 2026
d94383c
Merge remote-tracking branch 'origin/main' into feat/federation-v2
ScriptedAlchemy Jul 22, 2026
1347227
fix(browser): warn for ignored federation config
ScriptedAlchemy Jul 22, 2026
f8d97c8
chore: dedupe lockfile
ScriptedAlchemy Jul 22, 2026
35b9ec2
Merge remote-tracking branch 'origin/main' into feat/federation-v2
ScriptedAlchemy Jul 23, 2026
c30107a
chore: dedupe lockfile after main merge
ScriptedAlchemy Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ jobs:
- name: Run Examples Test
run: pnpm run test:examples

- name: Run Federation Example Test
if: runner.os == 'macOS'
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
run: pnpm run test:federation

# ======== e2e ========
e2e:
needs: [prepare, lint, ut]
Expand Down
17 changes: 17 additions & 0 deletions e2e/federation/fixtures/basic/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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 set the federation flag during global setup', () => {
expect(process.env.RSTEST_E2E_FEDERATION_IN_SETUP).toBe('true');
});
5 changes: 5 additions & 0 deletions e2e/federation/fixtures/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "e2e-federation-basic",
"private": true,
"type": "module"
}
6 changes: 6 additions & 0 deletions e2e/federation/fixtures/basic/rstest.config.ts
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'],
});
7 changes: 7 additions & 0 deletions e2e/federation/fixtures/basic/setup.ts
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__,
);
};
6 changes: 6 additions & 0 deletions e2e/federation/fixtures/cli/index.test.ts
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');
});
5 changes: 5 additions & 0 deletions e2e/federation/fixtures/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "e2e-federation-cli",
"private": true,
"type": "module"
}
6 changes: 6 additions & 0 deletions e2e/federation/fixtures/disabled/index.test.ts
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();
});
5 changes: 5 additions & 0 deletions e2e/federation/fixtures/disabled/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "e2e-federation-disabled",
"private": true,
"type": "module"
}
51 changes: 51 additions & 0 deletions e2e/federation/index.test.ts
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();
});
});
5 changes: 5 additions & 0 deletions examples/federation/.gitignore
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
dist-*/
.rstest-*
.DS_Store
31 changes: 31 additions & 0 deletions examples/federation/README.md
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.
31 changes: 31 additions & 0 deletions examples/federation/README_zh-cn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Module Federation 示例
Comment thread
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。
43 changes: 43 additions & 0 deletions examples/federation/component-app/App.jsx
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>
);
}
}
5 changes: 5 additions & 0 deletions examples/federation/component-app/bootstrap.js
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 />);
1 change: 1 addition & 0 deletions examples/federation/component-app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import('./bootstrap.js');
28 changes: 28 additions & 0 deletions examples/federation/component-app/package.json
Comment thread
ScriptedAlchemy marked this conversation as resolved.
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@1b77b1a",
"@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"
}
}
12 changes: 12 additions & 0 deletions examples/federation/component-app/public/index.html
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 examples/federation/component-app/rspack.browser.config.js
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' }),
],
};
Loading
Loading