Skip to content

Commit 895417c

Browse files
committed
feat: add Rsbuild adapter and interop examples
1 parent cd00f8d commit 895417c

43 files changed

Lines changed: 2218 additions & 886 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
node_modules/
22
dist/
33
.turbo/
4+
.mf/
5+
@mf-types/
46
*.log
57
pnpm-debug.log*
68
coverage/

‎README.md‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
# @module-federation/tanstack
22

3-
Module Federation support for TanStack Start, built on the Vite integration.
3+
Module Federation support for TanStack Start on Vite and Rsbuild. Rsbuild uses
4+
Rspack under the hood.
45

56
## Install
67

78
```bash
89
pnpm add @module-federation/tanstack @tanstack/react-start
910
```
1011

12+
Install the adapter for the bundler you use. They are optional peers so an
13+
Rsbuild-only application does not install or resolve the Vite adapter, and a
14+
Vite-only application does not install or resolve the Rsbuild adapter.
15+
16+
```bash
17+
pnpm add @module-federation/vite vite # Vite
18+
pnpm add @module-federation/rsbuild-plugin @rsbuild/core # Rsbuild
19+
```
20+
1121
## Configure
1222

1323
The federation wrapper must come before `tanstackStart()` so Module Federation
1424
can normalize TanStack's client and SSR entrypoints:
1525

1626
```ts
17-
import { tanstackStartModuleFederation } from "@module-federation/tanstack";
27+
import { tanstackStartModuleFederation } from "@module-federation/tanstack/vite";
1828
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
1929
import react from "@vitejs/plugin-react";
20-
import { nitro } from "nitro/vite";
2130
import { defineConfig } from "vite";
2231

2332
export default defineConfig({
@@ -34,7 +43,6 @@ export default defineConfig({
3443
}),
3544
tanstackStart(),
3645
react(),
37-
nitro(),
3846
],
3947
});
4048
```
@@ -51,15 +59,51 @@ The package supports Node 22.18+, 24.11+, and 26+, plus Vite 7/8 and TanStack
5159
Start 1.x. Use Vite 8 when developing SSR hosts that load remotes; Vite 7
5260
remains supported for production builds.
5361

62+
The package root remains a Vite compatibility export. New Vite projects can
63+
import `/vite` explicitly. Rsbuild projects must import `/rsbuild`:
64+
65+
```ts
66+
import { defineConfig } from "@rsbuild/core";
67+
import { pluginReact } from "@rsbuild/plugin-react";
68+
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
69+
import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild";
70+
71+
export default defineConfig({
72+
plugins: [
73+
pluginReact(),
74+
tanstackStart(),
75+
...tanstackStartModuleFederation({
76+
federation: {
77+
name: "host",
78+
remotes: { remote: "remote@https://example.test/mf-manifest.json" },
79+
},
80+
}),
81+
],
82+
});
83+
```
84+
85+
The Rsbuild adapter registers browser and SSR federation plugins by default. It
86+
emits script-compatible browser output and async-node CommonJS SSR output so
87+
its manifest can be consumed by Vite and Rsbuild hosts. The wrapper adds eager,
88+
singleton `react` and `react-dom` entries without changing the caller's config
89+
object. Set `server: false` for browser-only remotes.
90+
91+
Server federation mode emits `dist/server/index.cjs`; browser-only mode keeps
92+
TanStack Start's standard `dist/server/index.js` entry.
93+
5494
## Example
5595

56-
The workspace includes two TanStack Start apps. `apps/remote` exposes a
57-
stateful React card, and `apps/host` consumes it through this package.
96+
The workspace includes Vite and Rsbuild pairs. `apps/remote` exposes a
97+
stateful React card to `apps/host`. `apps/rsbuild-remote` exposes a second card
98+
to the Vite host, while `apps/rsbuild-host` consumes the Vite remote. Both
99+
cross-bundler links use manifest URLs. They render after hydration, so this is
100+
browser interop coverage, not an unverified SSR interoperability claim.
58101

59102
```bash
60103
pnpm install
61104
pnpm start
62105
```
63106

64-
Open the host at http://localhost:3000. The remote also runs as its own
65-
TanStack Start application at http://localhost:3001.
107+
This starts all four applications so both same-bundler and cross-bundler paths
108+
are available. Open the Vite host at http://localhost:3000 or the Rsbuild host
109+
at http://localhost:3003.

‎apps/README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# TanStack Start example
1+
# TanStack Start examples
22

33
Both sides of this example are TanStack Start applications.
44

@@ -18,3 +18,10 @@ React and React DOM use the wrapper's singleton defaults.
1818
Each package's `start` script runs Vite's development server. In Vite's CLI,
1919
the bare `vite` command means “start”; the literal `vite start` command would
2020
treat `start` as a project directory.
21+
22+
`rsbuild-remote` exposes the same kind of card through Rspack with a browser
23+
manifest. Both hosts consume both remotes by manifest URL after hydration.
24+
These examples exercise same-bundler and cross-bundler client interoperability;
25+
they do not claim that cross-bundler SSR remote rendering is supported.
26+
27+
`pnpm start` runs all four applications on ports 3000 through 3003.

‎apps/host/src/remote.d.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ declare module "tanstack_remote/StatusCard" {
44
const StatusCard: ComponentType;
55
export default StatusCard;
66
}
7+
8+
declare module "tanstack_rsbuild_remote/StatusCard" {
9+
import type { ComponentType } from "react";
10+
11+
const StatusCard: ComponentType;
12+
export default StatusCard;
13+
}

‎apps/host/src/routes/index.tsx‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createFileRoute } from "@tanstack/react-router";
2-
import { lazy, Suspense } from "react";
2+
import { lazy, Suspense, useEffect, useState } from "react";
33

44
const RemoteStatusCard = lazy(() => import("tanstack_remote/StatusCard"));
5+
const RsbuildStatusCard = lazy(() => import("tanstack_rsbuild_remote/StatusCard"));
56

67
export const Route = createFileRoute("/")({
78
loader: () => import("tanstack_remote/StatusCard").then(() => null),
@@ -31,7 +32,25 @@ function Home() {
3132
<Suspense fallback={<div className="remote-loading">Loading the remote app…</div>}>
3233
<RemoteStatusCard />
3334
</Suspense>
35+
36+
<RsbuildRemoteSlot />
3437
</section>
3538
</main>
3639
);
3740
}
41+
42+
function RsbuildRemoteSlot() {
43+
const [mounted, setMounted] = useState(false);
44+
45+
useEffect(() => {
46+
setMounted(true);
47+
}, []);
48+
49+
if (!mounted) return <div className="remote-loading">Rsbuild remote loads after hydration</div>;
50+
51+
return (
52+
<Suspense fallback={<div className="remote-loading">Loading the Rsbuild remote…</div>}>
53+
<RsbuildStatusCard />
54+
</Suspense>
55+
);
56+
}

‎apps/host/vite.config.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export default defineConfig({
1414
name: "tanstack_remote",
1515
entry: "http://127.0.0.1:3001/remoteEntry.js",
1616
},
17+
tanstack_rsbuild_remote: {
18+
type: "global",
19+
name: "tanstack_rsbuild_remote",
20+
entry: "http://127.0.0.1:3002/mf-manifest.json",
21+
},
1722
},
1823
}),
1924
tanstackStart(),

‎apps/rsbuild-host/package.json‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "tanstack-start-rsbuild-host",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"build": "rsbuild build",
8+
"dev": "rsbuild dev --port 3003 --strict-port",
9+
"start": "rsbuild dev --host 127.0.0.1 --port 3003 --strict-port",
10+
"typecheck": "tsc --noEmit"
11+
},
12+
"dependencies": {
13+
"@module-federation/runtime": "2.9.0",
14+
"@tanstack/react-router": "1.170.38",
15+
"@tanstack/react-start": "1.168.56",
16+
"react": "19.3.0",
17+
"react-dom": "19.3.0"
18+
},
19+
"devDependencies": {
20+
"@module-federation/rsbuild-plugin": "2.9.0",
21+
"@module-federation/tanstack": "workspace:*",
22+
"@rsbuild/core": "2.2.8",
23+
"@rsbuild/plugin-react": "2.1.0",
24+
"@types/node": "24.10.1",
25+
"@types/react": "19.3.0",
26+
"@types/react-dom": "19.3.0",
27+
"typescript": "6.0.3"
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineConfig } from "@rsbuild/core";
2+
import { pluginReact } from "@rsbuild/plugin-react";
3+
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
4+
import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild";
5+
6+
export default defineConfig({
7+
dev: { lazyCompilation: false },
8+
server: { host: "127.0.0.1", port: 3003 },
9+
plugins: [
10+
pluginReact(),
11+
tanstackStart(),
12+
...tanstackStartModuleFederation({
13+
federation: {
14+
dts: false,
15+
name: "tanstack_rsbuild_host",
16+
remotes: {
17+
tanstack_remote: "tanstack_remote@http://127.0.0.1:3001/mf-manifest.json",
18+
tanstack_rsbuild_remote: "tanstack_rsbuild_remote@http://127.0.0.1:3002/mf-manifest.json",
19+
},
20+
},
21+
server: false,
22+
}),
23+
],
24+
});

‎apps/rsbuild-host/src/assets.d.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.css?url" {
2+
const href: string;
3+
export default href;
4+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { loadRemote } from "@module-federation/runtime";
2+
import { useEffect, useState, type ComponentType } from "react";
3+
4+
type RemoteModule = {
5+
default: ComponentType;
6+
};
7+
8+
export function RemoteCardSlot({ fallback, remote }: { fallback: string; remote: string }) {
9+
const [RemoteCard, setRemoteCard] = useState<ComponentType | null>(null);
10+
const [failed, setFailed] = useState(false);
11+
12+
useEffect(() => {
13+
let active = true;
14+
setRemoteCard(null);
15+
setFailed(false);
16+
17+
loadRemote<RemoteModule>(remote)
18+
.then((remoteModule) => {
19+
if (!active) return;
20+
if (remoteModule?.default) {
21+
setRemoteCard(() => remoteModule.default);
22+
} else {
23+
setFailed(true);
24+
}
25+
})
26+
.catch(() => {
27+
if (active) setFailed(true);
28+
});
29+
30+
return () => {
31+
active = false;
32+
};
33+
}, [remote]);
34+
35+
if (failed) return <div className="remote-loading">Remote unavailable</div>;
36+
if (!RemoteCard) return <div className="remote-loading">{fallback}</div>;
37+
return <RemoteCard />;
38+
}

0 commit comments

Comments
 (0)