Skip to content

Commit d74cb3b

Browse files
cursoragentScriptedAlchemy
authored andcommitted
feat(examples): add dual rsc federation apps and coupled e2e
1 parent 66ff4d9 commit d74cb3b

33 files changed

Lines changed: 1154 additions & 45 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
playwright-report
3+
test-results
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Rsbuild RSC + Module Federation manifest example (remote variant)
2+
3+
This package mirrors the base `rsbuild-rsc-federation` example and runs on a separate port for dual-app testing.
4+
5+
## Run
6+
7+
From repository root:
8+
9+
```bash
10+
pnpm --filter examples-rsbuild-rsc-federation-remote... install
11+
pnpm --filter examples-rsbuild-rsc-federation-remote run build
12+
pnpm --filter examples-rsbuild-rsc-federation-remote run verify:manifest
13+
```
14+
15+
## E2E smoke test
16+
17+
For the combined dual-app Playwright run (both apps booted before test execution), use:
18+
19+
```bash
20+
pnpm --filter examples-rsbuild-rsc-federation run test:e2e
21+
```
22+
23+
This package also supports standalone e2e:
24+
25+
```bash
26+
pnpm --filter examples-rsbuild-rsc-federation-remote... install
27+
pnpm --filter examples-rsbuild-rsc-federation-remote run test:e2e
28+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('renders demo app and supports interaction', async ({ page }) => {
4+
await page.goto('/');
5+
6+
await expect(page.getByTestId('app-ready')).toBeVisible();
7+
await expect(page.getByTestId('status-text')).toHaveText(
8+
'client entry ready',
9+
);
10+
await expect(page.getByTestId('component-rendered')).toHaveText(
11+
'InteractiveClientDemo',
12+
);
13+
await expect(page.getByTestId('counter-value')).toHaveText('0');
14+
15+
await page.getByTestId('increment-button').click();
16+
await page.getByTestId('increment-button').click();
17+
18+
await expect(page.getByTestId('counter-value')).toHaveText('2');
19+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "examples-rsbuild-rsc-federation-remote",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "rsbuild build",
7+
"dev:e2e": "rsbuild dev --port 3331",
8+
"verify:manifest": "node ./scripts/verify-manifest.mjs",
9+
"build:verify": "pnpm run build && pnpm run verify:manifest",
10+
"test:e2e": "playwright test --config ./playwright.config.ts"
11+
},
12+
"dependencies": {
13+
"react": "^19.2.4",
14+
"react-dom": "^19.2.4",
15+
"react-server-dom-rspack": "0.0.1-beta.0",
16+
"rsbuild-rsc-federation-shared": "workspace:*"
17+
},
18+
"devDependencies": {
19+
"@module-federation/node": "^2.7.32",
20+
"@module-federation/runtime-tools": "0.24.1",
21+
"@playwright/test": "1.58.2",
22+
"@rsbuild/core": "2.0.0-beta.7",
23+
"@rsbuild/plugin-react": "^1.4.5",
24+
"@rspack/core": "workspace:*",
25+
"@types/node": "^20.19.33",
26+
"@types/react": "^19.2.14",
27+
"@types/react-dom": "^19.2.3",
28+
"rsbuild-plugin-rsc": "0.0.1-beta.0",
29+
"typescript": "^5.9.3"
30+
}
31+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import fs from 'node:fs';
2+
import { defineConfig, devices } from '@playwright/test';
3+
4+
const chromePath = '/usr/bin/google-chrome';
5+
const hasSystemChrome = fs.existsSync(chromePath);
6+
7+
export default defineConfig({
8+
testDir: './e2e',
9+
timeout: 60_000,
10+
expect: {
11+
timeout: 10_000,
12+
},
13+
fullyParallel: false,
14+
reporter: 'line',
15+
use: {
16+
baseURL: 'http://localhost:3331',
17+
trace: 'on-first-retry',
18+
},
19+
webServer: {
20+
command: 'pnpm run dev:e2e',
21+
cwd: import.meta.dirname,
22+
url: 'http://localhost:3331',
23+
reuseExistingServer: false,
24+
timeout: 120_000,
25+
},
26+
projects: [
27+
{
28+
name: 'chromium',
29+
use: {
30+
...devices['Desktop Chrome'],
31+
launchOptions: hasSystemChrome
32+
? {
33+
executablePath: chromePath,
34+
args: ['--no-sandbox'],
35+
}
36+
: {
37+
args: ['--no-sandbox'],
38+
},
39+
},
40+
},
41+
],
42+
});
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import path from 'node:path';
2+
import { defineConfig } from '@rsbuild/core';
3+
import { pluginReact } from '@rsbuild/plugin-react';
4+
import { rspack } from '@rspack/core';
5+
import { Layers, pluginRSC } from 'rsbuild-plugin-rsc';
6+
7+
const containerName = 'rsbuild_container';
8+
type EnvironmentName = 'server' | 'client';
9+
10+
const unifiedExposes = {
11+
'./button': {
12+
import: './src/exposed-client.tsx',
13+
environments: ['server', 'client'],
14+
layers: {
15+
server: Layers.rsc,
16+
},
17+
},
18+
'./composed': {
19+
import: './src/exposed-composed.tsx',
20+
environments: ['server', 'client'],
21+
layers: {
22+
server: Layers.rsc,
23+
},
24+
},
25+
'./consumer': {
26+
import: './src/rsc-consumer.ts',
27+
environments: ['server'],
28+
layers: {
29+
server: Layers.rsc,
30+
},
31+
},
32+
'./server-mixed': {
33+
import: './src/server-mixed-consumer.ts',
34+
environments: ['server'],
35+
layers: {
36+
server: Layers.rsc,
37+
},
38+
},
39+
} as const;
40+
41+
function resolveExposes(environment: EnvironmentName) {
42+
return Object.fromEntries(
43+
Object.entries(unifiedExposes)
44+
.filter(([, definition]) => definition.environments.includes(environment))
45+
.map(([exposeKey, definition]) => [
46+
exposeKey,
47+
{
48+
import: definition.import,
49+
...(definition.layers[environment]
50+
? { layer: definition.layers[environment] }
51+
: {}),
52+
},
53+
]),
54+
);
55+
}
56+
57+
const rscLayerShared = {
58+
singleton: true,
59+
requiredVersion: false,
60+
shareScope: 'rsc',
61+
layer: Layers.rsc,
62+
issuerLayer: Layers.rsc,
63+
} as const;
64+
65+
const ssrLayerShared = {
66+
singleton: true,
67+
requiredVersion: false,
68+
shareScope: 'ssr',
69+
layer: Layers.ssr,
70+
issuerLayer: Layers.ssr,
71+
} as const;
72+
73+
const clientLayerShared = {
74+
singleton: true,
75+
requiredVersion: false,
76+
shareScope: 'client',
77+
} as const;
78+
79+
export default defineConfig({
80+
plugins: [
81+
pluginReact(),
82+
pluginRSC({
83+
layers: {
84+
ssr: path.join(import.meta.dirname, './src/framework/entry.ssr.tsx'),
85+
},
86+
}),
87+
],
88+
environments: {
89+
server: {
90+
source: {
91+
entry: {
92+
index: {
93+
import: './src/framework/entry.rsc.tsx',
94+
layer: Layers.rsc,
95+
},
96+
},
97+
},
98+
},
99+
client: {
100+
source: {
101+
entry: {
102+
index: './src/framework/entry.client.tsx',
103+
},
104+
},
105+
},
106+
},
107+
tools: {
108+
rspack: (config, { target }) => {
109+
const isServerBuild = target === 'node';
110+
const environment: EnvironmentName = isServerBuild ? 'server' : 'client';
111+
const remotes = {
112+
remote: 'remote',
113+
};
114+
115+
if (isServerBuild) {
116+
config.target = 'async-node';
117+
}
118+
119+
config.plugins ||= [];
120+
config.plugins.push(
121+
new rspack.container.ModuleFederationPlugin({
122+
name: containerName,
123+
filename: isServerBuild ? 'remoteEntry.js' : 'remoteEntry.client.js',
124+
library: isServerBuild
125+
? { type: 'commonjs-module' }
126+
: { type: 'var', name: containerName },
127+
manifest: isServerBuild ? true : { fileName: 'mf-manifest.client' },
128+
exposes: resolveExposes(environment),
129+
remotes,
130+
runtimePlugins: isServerBuild
131+
? ['@module-federation/node/runtimePlugin']
132+
: [],
133+
experiments: {
134+
asyncStartup: true,
135+
},
136+
shared: isServerBuild
137+
? {
138+
react: rscLayerShared,
139+
'react/jsx-runtime': rscLayerShared,
140+
'react-dom': ssrLayerShared,
141+
'react-dom/server': ssrLayerShared,
142+
'react-server-dom-rspack/server.node': rscLayerShared,
143+
'rsbuild-rsc-federation-shared': {
144+
request: 'rsbuild-rsc-federation-shared',
145+
import: 'rsbuild-rsc-federation-shared',
146+
shareKey: 'rsc-shared-key',
147+
shareScope: 'rsc',
148+
requiredVersion: '^1.0.0',
149+
singleton: true,
150+
layer: Layers.rsc,
151+
issuerLayer: Layers.rsc,
152+
},
153+
'rsbuild-rsc-federation-shared/server-actions': {
154+
request: 'rsbuild-rsc-federation-shared/server-actions',
155+
import: 'rsbuild-rsc-federation-shared/server-actions',
156+
shareKey: 'rsc-shared-actions-key',
157+
shareScope: 'rsc',
158+
requiredVersion: '^1.0.0',
159+
singleton: true,
160+
layer: Layers.rsc,
161+
issuerLayer: Layers.rsc,
162+
},
163+
}
164+
: {
165+
react: clientLayerShared,
166+
'react/jsx-runtime': clientLayerShared,
167+
'react-dom': clientLayerShared,
168+
},
169+
}),
170+
);
171+
172+
return config;
173+
},
174+
},
175+
});

0 commit comments

Comments
 (0)