Skip to content

Commit 281d5fd

Browse files
committed
feat(tanstack): add browser federation coverage across hosts
- Make Rsbuild SSR federation opt-in and eager React host shares - Add browser tests for hydration, remote interaction, and offline fallbacks - Document support boundaries and update release workflows
1 parent c73cf94 commit 281d5fd

19 files changed

Lines changed: 438 additions & 154 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ jobs:
3434
- run: pnpm format:check
3535
- run: pnpm typecheck
3636
- run: pnpm build
37+
- run: pnpm exec playwright install --with-deps chromium
3738
- run: pnpm test
3839
- run: pnpm pack:tanstack

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
- run: pnpm format:check
6767
- run: pnpm typecheck
6868
- run: pnpm build
69+
- run: pnpm exec playwright install --with-deps chromium
6970
- run: pnpm test
7071
- run: pnpm pack:tanstack
7172
- name: Compute dist-tag

‎README.md‎

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,17 @@ export default defineConfig({
4848
```
4949

5050
Defaults are `remoteEntry.js`, `manifest: true`, and
51-
`hostInitInjectLocation: "entry"`. React and `react-dom` are shared singleton
52-
defaults; any explicit entries in `shared` take precedence.
51+
`hostInitInjectLocation: "entry"`. React and `react-dom` are shared
52+
singletons; when the config declares `remotes`, they are also eager so the
53+
host's own React stays in the share scope. Explicit entries in `shared` take
54+
precedence.
5355

5456
Do not set a global Module Federation `target`. TanStack Start builds client
5557
and SSR environments from one Vite configuration, so the wrapper leaves target
5658
selection to `@module-federation/vite` for each environment.
5759

5860
The package supports Node 22.18+, 24.11+, and 26+, plus Vite 7/8 and TanStack
59-
Start 1.x. Use Vite 8 when developing SSR hosts that load remotes; Vite 7
61+
Start 1.167.43+. Use Vite 8 when developing SSR hosts that load remotes; Vite 7
6062
remains supported for production builds.
6163

6264
The package root remains a Vite compatibility export. New Vite projects can
@@ -75,35 +77,47 @@ export default defineConfig({
7577
...tanstackStartModuleFederation({
7678
federation: {
7779
name: "host",
80+
shareStrategy: "loaded-first",
7881
remotes: { remote: "remote@https://example.test/mf-manifest.json" },
7982
},
8083
}),
8184
],
8285
});
8386
```
8487

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.
88+
The Rsbuild adapter emits script-compatible browser output, so its manifest can
89+
be consumed by Vite and Rsbuild hosts. It adds eager, singleton `react` and
90+
`react-dom` entries without changing the caller's config object. Hosts should
91+
use `shareStrategy: "loaded-first"` so one offline remote cannot fail startup.
9092

91-
Server federation mode emits `dist/server/index.cjs`; browser-only mode keeps
92-
TanStack Start's standard `dist/server/index.js` entry.
93+
SSR federation for Rsbuild is experimental and opt-in: `server: true` adds an
94+
async-node CommonJS server container and changes the server entry to
95+
`dist/server/index.cjs`.
9396

94-
## Example
97+
## Support status
9598

96-
The workspace includes Vite and Rsbuild pairs. `apps/vite-remote` exposes a
97-
stateful React card to `apps/vite-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.
99+
| Host | Remote | Browser federation | Federated SSR |
100+
| ------- | ------- | ------------------ | ----------------------------- |
101+
| Vite | Vite | Supported | Supported |
102+
| Vite | Rsbuild | Supported | Not supported |
103+
| Rsbuild | Vite | Supported | Not supported |
104+
| Rsbuild | Rsbuild | Supported | Experimental (`server: true`) |
105+
106+
[`TODO.md`](TODO.md) tracks the remaining SSR work.
107+
108+
## Examples
109+
110+
The workspace has four TanStack Start apps; see [`apps/README.md`](apps/README.md).
101111

102112
```bash
103113
pnpm install
104114
pnpm start
105115
```
106116

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.
117+
Open the Vite host at http://127.0.0.1:3000 or the Rsbuild host at
118+
http://127.0.0.1:3003. Each host renders a card from both remotes.
119+
120+
`pnpm test` builds everything, starts all four apps, and drives both hosts in
121+
headless Chromium. It checks hydration, remote interactivity, console errors,
122+
and the fallback when a remote is offline. Run
123+
`pnpm exec playwright install chromium` once before the first local test run.

‎TODO.md‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
This file tracks the work required before `@module-federation/tanstack` can
44
claim SSR support across both Vite and Rsbuild/Rspack.
55

6-
Assessment date: 2026-09-18.
6+
Assessment date: 2026-09-18. Updated 2026-10-06: the Rsbuild server adapter is now
7+
opt-in (`server: true`), and browser tests cover hydration, shared React
8+
identity, and remote outages for every host/remote pair.
79

810
## Current support boundary
911

@@ -12,11 +14,11 @@ Assessment date: 2026-09-18.
1214
| Vite | Vite | Supported | The Vite host response contains remote markup before hydration. |
1315
| Vite | Rsbuild | Not supported | The Rsbuild remote loads in the browser after hydration. |
1416
| Rsbuild | Vite | Not supported | The Vite remote loads through the browser runtime after hydration. |
15-
| Rsbuild | Rsbuild | Not yet supported | The adapter can build a server container, but no end-to-end SSR fixture exists. |
17+
| Rsbuild | Rsbuild | Experimental | `server: true` builds a server container, but no end-to-end SSR fixture exists. |
1618

1719
The Rsbuild examples are TanStack Start SSR applications, so their local route
18-
shells render on the server. Their Module Federation configuration passes
19-
`server: false`; federated components are client-only. Do not describe that as
20+
shells render on the server. Their Module Federation configuration uses the
21+
default browser-only mode; federated components are client-only. Do not describe that as
2022
federated SSR.
2123

2224
The supported claim today is:
@@ -70,8 +72,8 @@ Reference implementation paths:
7072
- Core only installs its startup dependency handling when
7173
`experiments.asyncStartup` is enabled. The TanStack adapter does not default
7274
this option today.
73-
- Both committed Rsbuild examples pass `server: false`, so the server adapter is
74-
covered by compiler-contract tests, not by a running host/remote pair.
75+
- Both committed Rsbuild examples use the default browser-only mode, so the
76+
server adapter is covered by compiler-contract tests, not by a running host/remote pair.
7577

7678
Reference implementation paths:
7779

@@ -231,7 +233,7 @@ This aligns the host with the existing Rsbuild server output.
231233

232234
| Host | Remote | Development | Production | Initial remote HTML | Hydration and interaction |
233235
| --------- | --------- | ------------ | ---------- | ------------------- | ------------------------- |
234-
| Vite 8 | Vite 8 | Existing | Existing | Existing | Add browser assertion |
236+
| Vite 8 | Vite 8 | Existing | Add | Existing | Existing (development) |
235237
| Vite 7 | Vite 7 | Not required | Add | Add | Add |
236238
| Rsbuild 2 | Rsbuild 2 | Add | Add | Add | Add |
237239
| Vite 8 | Rsbuild 2 | Add | Add | Add | Add |
@@ -258,7 +260,7 @@ Each matrix row must verify:
258260

259261
- [ ] Update the root README only after a matrix row satisfies every acceptance
260262
criterion.
261-
- [ ] Document `server: false` as browser-only federation, not SSR federation.
263+
- [x] Document browser-only federation as the Rsbuild default and SSR as opt-in.
262264
- [ ] Document `dist/server/index.cjs` when the CommonJS Rsbuild server adapter
263265
is enabled.
264266
- [ ] Document the chosen cross-bundler server-entry format.
@@ -283,3 +285,15 @@ SSR support for a host/remote combination is complete only when:
283285

284286
Until then, keep the package description precise: Vite-to-Vite SSR is supported;
285287
Rsbuild and cross-bundler SSR are in progress.
288+
289+
## Example and development gaps
290+
291+
- [ ] Run the browser tests against production builds, not only development
292+
servers.
293+
- [ ] A Vite remote with `exposes` does not hydrate when opened directly in
294+
development: `@module-federation/vite` 1.22 forces host-driven init for
295+
exposing containers (`forceClientInjected`). Report upstream, or add a
296+
standalone init path.
297+
- [ ] Decide whether the Rsbuild adapter should default hosts to
298+
`shareStrategy: "loaded-first"`. With `version-first`, one offline remote
299+
fails host startup with `RUNTIME-003`; the example sets it explicitly.

‎apps/README.md‎

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
# TanStack Start examples
22

3-
Both sides of this example are TanStack Start applications.
3+
Four TanStack Start applications: a host and a remote for each bundler. Each
4+
host consumes both remotes, so every host/remote combination runs.
45

5-
- `vite-remote` exposes `./StatusCard` from its Module Federation container and also
6-
renders the component on its own `/` route.
7-
- `vite-host` loads `tanstack_vite_remote/StatusCard` from the remote running on
8-
port 3001. Its own application runs on port 3000.
6+
| App | Port | Role |
7+
| ---------------- | ---- | ---------------------------------------------------------------------- |
8+
| `vite-host` | 3000 | Server-renders the Vite remote; loads the Rsbuild remote on the client |
9+
| `vite-remote` | 3001 | Exposes `./StatusCard`, rendered into the Vite host's initial HTML |
10+
| `rsbuild-remote` | 3002 | Exposes `./StatusCard` through a browser manifest |
11+
| `rsbuild-host` | 3003 | Loads both remotes with `loadRemote()` after hydration |
912

1013
From the repository root:
1114

1215
```bash
1316
pnpm start
1417
```
1518

16-
The federation wrapper must precede `tanstackStart()` in both Vite configs.
17-
React and React DOM use the wrapper's singleton defaults.
18-
19-
Each package's `start` script runs Vite's development server. In Vite's CLI,
20-
the bare `vite` command means “start”; the literal `vite start` command would
21-
treat `start` as a project directory.
22-
23-
`rsbuild-remote` exposes the same kind of card through Rspack with a browser
24-
manifest. Both hosts consume both remotes by manifest URL after hydration.
25-
These examples exercise same-bundler and cross-bundler client interoperability;
26-
they do not claim that cross-bundler SSR remote rendering is supported.
27-
28-
`pnpm start` runs all four applications on ports 3000 through 3003.
19+
Then open http://127.0.0.1:3000 or http://127.0.0.1:3003. Every remote card has
20+
a counter button; clicking it proves the remote shares the host's React
21+
instance.
22+
23+
## What each host shows
24+
25+
- **Vite host.** The route loader imports the Vite remote, so its card is in
26+
the server response ("Rendered on the server") and hydrates in place. The
27+
Rsbuild card mounts after hydration. Both remotes sit behind
28+
`RemoteBoundary`, an error boundary with a fallback card, so a failed remote
29+
never blanks the page.
30+
- **Rsbuild host.** `RemoteCardSlot` calls `loadRemote()` in an effect and
31+
shows "Remote unavailable" when a remote fails. The host uses
32+
`shareStrategy: "loaded-first"`, so it does not fetch every remote's
33+
manifest at startup.
34+
35+
Stop the Rsbuild remote while both hosts are open and reload them: each host
36+
keeps rendering and shows its fallback card.
37+
38+
## Notes
39+
40+
- The federation wrapper must come before `tanstackStart()` in the Vite
41+
configs.
42+
- Each `start` script runs the bundler's development server. In Vite's CLI the
43+
bare `vite` command starts the dev server; `vite start` would treat `start`
44+
as a project directory.
45+
- In development, opening the Vite remote directly (port 3001) server-renders
46+
the page but does not hydrate: `@module-federation/vite` waits for a host to
47+
initialize a container that has `exposes`. It hydrates when loaded through a
48+
host, and its production build hydrates standalone.
49+
- Cross-bundler remotes render on the client only. Cross-bundler SSR is not
50+
supported yet.

‎apps/rsbuild-host/rsbuild.config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ export default defineConfig({
1313
federation: {
1414
dts: false,
1515
name: "tanstack_rsbuild_host",
16+
// Resolve remotes on demand so one offline remote cannot block host startup.
17+
shareStrategy: "loaded-first",
1618
remotes: {
1719
tanstack_vite_remote: "tanstack_vite_remote@http://127.0.0.1:3001/mf-manifest.json",
1820
tanstack_rsbuild_remote: "tanstack_rsbuild_remote@http://127.0.0.1:3002/mf-manifest.json",
1921
},
2022
},
21-
server: false,
2223
}),
2324
],
2425
});

‎apps/rsbuild-remote/rsbuild.config.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export default defineConfig({
1818
exposes: { "./StatusCard": "./src/components/StatusCard.tsx" },
1919
name: "tanstack_rsbuild_remote",
2020
},
21-
server: false,
2221
}),
2322
],
2423
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Component, Suspense, type ReactNode } from "react";
2+
3+
type Props = { children: ReactNode; fallback: string; name: string };
4+
5+
/** Keeps a failed remote from taking down the host route. */
6+
export class RemoteBoundary extends Component<Props, { failed: boolean }> {
7+
state = { failed: false };
8+
9+
static getDerivedStateFromError() {
10+
return { failed: true };
11+
}
12+
13+
componentDidCatch(error: unknown) {
14+
console.warn(`[${this.props.name}] remote failed to render`, error);
15+
}
16+
17+
render() {
18+
if (this.state.failed) {
19+
return <div className="remote-loading">{this.props.name} is unavailable</div>;
20+
}
21+
22+
return (
23+
<Suspense fallback={<div className="remote-loading">{this.props.fallback}</div>}>
24+
{this.props.children}
25+
</Suspense>
26+
);
27+
}
28+
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createFileRoute } from "@tanstack/react-router";
2-
import { lazy, Suspense, useEffect, useState } from "react";
2+
import { lazy, useEffect, useState } from "react";
3+
import { RemoteBoundary } from "../components/RemoteBoundary";
34

45
const RemoteStatusCard = lazy(() => import("tanstack_vite_remote/StatusCard"));
56
const RsbuildStatusCard = lazy(() => import("tanstack_rsbuild_remote/StatusCard"));
@@ -16,8 +17,8 @@ function Home() {
1617
<p className="eyebrow">TanStack Start × Module Federation</p>
1718
<h1>Two full-stack apps. One React tree.</h1>
1819
<p className="lede">
19-
This page belongs to the Vite host on port 3000. The interactive card below is owned and
20-
built by a second TanStack Start application on port 3001.
20+
This page belongs to the Vite host on port 3000. The Vite remote on port 3001 renders its
21+
card on the server; the Rsbuild remote on port 3002 loads in the browser after hydration.
2122
</p>
2223
</header>
2324

@@ -29,9 +30,9 @@ function Home() {
2930
<p>The route, document shell, and server response come from the host app.</p>
3031
</article>
3132

32-
<Suspense fallback={<div className="remote-loading">Loading the remote app…</div>}>
33+
<RemoteBoundary fallback="Loading the remote app…" name="Vite remote">
3334
<RemoteStatusCard />
34-
</Suspense>
35+
</RemoteBoundary>
3536

3637
<RsbuildRemoteSlot />
3738
</section>
@@ -49,8 +50,8 @@ function RsbuildRemoteSlot() {
4950
if (!mounted) return <div className="remote-loading">Rsbuild remote loads after hydration</div>;
5051

5152
return (
52-
<Suspense fallback={<div className="remote-loading">Loading the Rsbuild remote…</div>}>
53+
<RemoteBoundary fallback="Loading the Rsbuild remote…" name="Rsbuild remote">
5354
<RsbuildStatusCard />
54-
</Suspense>
55+
</RemoteBoundary>
5556
);
5657
}

‎docs/RELEASING.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ summary: Release and trusted npm publishing for @module-federation/tanstack.
44
read_when:
55
- Preparing a release
66
- Updating release automation
7-
updated_at: 2026-09-16
7+
updated_at: 2026-10-06
88
---
99

1010
# Releasing
@@ -41,8 +41,12 @@ The publish workflow reruns the gate, checks that the source is reachable from
4141
a different dist-tag. Configure npm trusted publishing for repository
4242
`module-federation/tanstack`, workflow `release.yml`, environment `Publish`.
4343

44-
The release gate builds the Vite and Rsbuild example pairs. It checks the Vite
45-
Vite host server bundle and both directions of client manifest interoperability. The
46-
Rsbuild example remote is browser-only; package tests cover the adapter's
47-
async-node CommonJS SSR configuration. Browser-level coverage for hydrated
48-
cross-bundler rendering remains follow-up work.
44+
The release gate builds the Vite and Rsbuild example pairs and checks their
45+
manifests and server bundles. It then starts all four apps and drives both hosts
46+
in headless Chromium: Vite-to-Vite SSR markup, hydration, remote interactivity,
47+
one shared React instance, no console errors, and the fallback when a remote is
48+
offline. Package tests cover the opt-in Rsbuild async-node CommonJS SSR
49+
configuration.
50+
51+
The test suite needs Chromium. Locally, run `pnpm exec playwright install
52+
chromium` once; the workflows install it before `pnpm test`.

0 commit comments

Comments
 (0)