Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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