Skip to content

feat(bridge): add application-level SSR for React and Vue#4869

Open
tonoizer wants to merge 7 commits into
module-federation:mainfrom
tonoizer:cursor/bridge-app-ssr-e63b
Open

feat(bridge): add application-level SSR for React and Vue#4869
tonoizer wants to merge 7 commits into
module-federation:mainfrom
tonoizer:cursor/bridge-app-ssr-e63b

Conversation

@tonoizer

@tonoizer tonoizer commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a deliberately narrow Bridge SSR V1 for React and Vue and hardens its hydration and lifecycle boundaries. A federated provider renders one buffered HTML island on the server; the host emits that HTML and its optional dehydrated state exactly once in an instance-scoped document slot, while serialized host context carries only a versioned module/instance reference.

Before React hydrateRoot or Vue application mount, the host installs a document hydration registry. The client resolves a reference to an immutable slot snapshot, validates its identity and direct-child structure, and consumes it once. Missing slots intentionally follow the existing CSR path; malformed, duplicated, or identity-conflicting slots fail with contextual hydration errors. After a successful consume, later SPA visits render through CSR rather than rehydrating stale server markup.

The React adapter now keeps Hook structure and callback/ref handling stable, serializes asynchronous renders, rejects late mounts, forwards asynchronous failures to the host boundary, and destroys nested providers exactly once with deferred React-root cleanup. The Vue adapter retains one app/router across ordinary prop changes using shallow-reactive props, remounts only for mount-critical router changes, waits for router readiness during hydration, and prevents cancelled mounts from being published.

The public SSR result remains limited to protocol version, module name, instance ID, HTML, and optional JSON state. A separate identity-only reference and result-to-reference boundary prevent HTML from entering host JSON. React Router aliasing remains the V1 integration model, enforced by server/browser build and bundle-exclusion checks.

The Rsbuild demo covers React host to Vue remote and Vue host to React remote in development and production. A separate non-blocking Vite smoke path covers React host to Vue remote. Tests cover the registry contract, HTML-safe serialization, reference conversion, React/Vue lifecycle behavior, no-JS output, hydration, deep links with query/hash, interactivity, multiple instances, and SPA revisit behavior.

Bridge SSR V1 intentionally does not provide a shared timeout or CSR fallback for remote load/render failures. A slow or broken remote can therefore fail the host response. Assets, status/header policy, streaming, React data routers, Nuxt, and Modern.js adapters remain out of scope.

Validation

  • pnpm install --frozen-lockfile
  • pnpm exec prettier --check .
  • pnpm run build:packages (43/43 tasks)
  • pnpm exec turbo run test --filter=@module-federation/bridge-shared --filter=@module-federation/bridge-react --filter=@module-federation/bridge-vue3 (10/10 tasks; 88 focused tests)
  • pnpm exec turbo run lint --filter=@module-federation/bridge-shared --filter=@module-federation/bridge-react --filter=@module-federation/bridge-vue3 (successful no-op because these packages expose no Turbo lint tasks)
  • pnpm run ci:local --only=e2e-router (26/26 tests)
  • pnpm run ci:local --only=e2e-bridge-ssr (required development/production paths and browser-bundle gate passed; non-blocking Vite smoke passed)
  • Changeset status validation passed for the three Bridge packages as minor releases.

Local validation used the repository-pinned pnpm 10.28.0. corepack enable could not replace the system pnpm shim because /usr/local/bin is not writable; no dependency or validation command relied on that replacement.

Related Issue

Closes #4868

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the documentation.

@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8813e97

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 52 packages
Name Type
@module-federation/bridge-shared Major
@module-federation/bridge-react Major
@module-federation/bridge-vue3 Major
bridge-ssr-host-vue Patch
bridge-ssr-host Patch
bridge-ssr-demo-shared Patch
@module-federation/modern-js-v3 Major
@module-federation/modern-js Major
@module-federation/playground Patch
remote5 Patch
remote6 Patch
bridge-ssr-remote-react Patch
bridge-ssr-remote-vue Patch
shared-tree-shaking-with-server-host Patch
shared-tree-shaking-with-server-provider Patch
@module-federation/runtime Major
@module-federation/enhanced Major
@module-federation/rspack Major
@module-federation/webpack-bundler-runtime Major
@module-federation/sdk Major
@module-federation/runtime-tools Major
@module-federation/managers Major
@module-federation/manifest Major
@module-federation/dts-plugin Major
@module-federation/third-party-dts-extractor Major
@module-federation/devtools Major
@module-federation/bridge-react-webpack-plugin Major
@module-federation/retry-plugin Major
@module-federation/rsbuild-plugin Major
@module-federation/error-codes Major
@module-federation/inject-external-runtime-core-plugin Major
@module-federation/runtime-core Major
create-module-federation Major
@module-federation/cli Major
@module-federation/rspress-plugin Major
@module-federation/treeshake-server Major
@module-federation/treeshake-frontend Major
@module-federation/metro Major
@module-federation/metro-plugin-rnef Major
@module-federation/metro-plugin-rock Major
@module-federation/metro-plugin-rnc-cli Major
@module-federation/esbuild Patch
@module-federation/nextjs-mf Patch
@module-federation/node Patch
@module-federation/observability-plugin Patch
website-new Patch
@module-federation/storybook-addon Major
shared-tree-shaking-no-server-host Patch
shared-tree-shaking-no-server-provider Patch
@module-federation/utilities Patch
node-dynamic-remote-new-version Patch
node-dynamic-remote Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploy Preview for module-federation-docs ready!

Name Link
🔨 Latest commit ede9998
🔍 Latest deploy log https://app.netlify.com/projects/module-federation-docs/deploys/6a4cf733a3214f0008040a50
😎 Deploy Preview https://deploy-preview-4869--module-federation-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@tonoizer
tonoizer marked this pull request as ready for review July 3, 2026 06:02
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@2heal1 2heal1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few inline comments on the issues we discussed.

Comment thread packages/bridge/bridge-react/src/remote/RemoteAppWrapper.tsx Outdated
Comment thread apps/bridge-ssr-demo/host/src/lib/normalizeRequestUrl.ts Outdated
Comment thread packages/bridge/bridge-react/package.json Outdated
@tonoizer
tonoizer requested a review from 2heal1 July 7, 2026 07:05
@tonoizer

tonoizer commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ed191ac — a small hardening pass from a full self-review of the branch:

  • Failed remote load after SSR now surfaces the host fallback: previously, if loader() rejected during hydration, the page silently kept the frozen server markup with no interactivity and no error. The load error is now rethrown into the wrapping ErrorBoundary (regression test added).
  • renderToString props are merged once and reused for both the rendered element and the serialized state, so the shipped HTML and hydration props can never diverge.
  • Demo cleanups: the request-URL normalizer is now a single shared module (it existed as two hand-synced copies — the previous review fix had to patch both), and the dev server no longer caches a rejected SSR bundle promise across requests.

Two API-level points worth a maintainer opinion (no code change here):

  1. renderRemoteAppToString / renderToString reserve prop names (url, export, instance, …) via a meta-key denylist, so app props with those names are silently stripped. Documented, but a future major could take an explicit props: {...} field instead of spreading — happy to go either way.
  2. On direct visits the demo ships remote HTML twice (in the DOM and again in the host-context JSON needed for host-app hydration symmetry). A follow-up could read the markup back from the SSR'd container instead; noting it as a known trade-off for now.

All 13 Playwright e2e specs and unit suites pass locally on this head.

@tonoizer

tonoizer commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a hardening commit (37d9162) addressing three of the open discussion points from the description:

  • SSR failure now degrades to CSR instead of failing the host response. renderRemoteAppToString accepts fallback: 'csr' | 'throw' (default 'csr') and an optional timeout. A remote that fails to load, lacks renderToString (older remote build — this doubles as the version-skew story), throws, or times out resolves to { html: '', state: undefined, error }; the host serves the page and that remote client-renders after hydration. Demo exercises it via /react-remote?mfSsrError=react.
  • Remote HTML now ships once. New readBridgeSSRHtmlFromDocument(moduleName) (bridge-shared, re-exported from bridge-react/ssr and bridge-vue3) reads the server markup back from the rendered container before hydration, so the demo's host context script carries state only instead of a second serialized copy of the remote HTML.
  • Multi-instance state lifecycle. The per-moduleName SSR state cache is now reference-counted; unmounting one instance of a remote no longer clears the hydration state of a still-mounted sibling.

New unit tests in bridge-shared/bridge-react cover the fallback modes, timeout, DOM recovery, and ref-counting; a new ssr-payload Playwright project asserts the host context JSON contains no remote markup and that a forced SSR failure still serves an interactive page. Full suite is green locally: 16 e2e specs + bridge-shared/bridge-react/vue3-bridge unit suites.

@tonoizer tonoizer changed the title feat(bridge): application-level SSR for React and Vue remotes feat(bridge): add instance-scoped application SSR foundation Jul 9, 2026
@tonoizer

tonoizer commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed f39a7d9 — architecture reset for the SSR foundation.

Review identified that module-scoped hydration state is only correct for one remote mount. Rather than extend the reference-counted cache, this update replaces it with instance-scoped Bridge slots, explicit request/props input, and remote-owned dehydration. Each rendered remote now hydrates from its own container and state payload.

The branch no longer rewrites React Router imports or fabricates data-router loader state. V1 supports explicit classic-router SSR through string adapters, while the shared result contract is stream-capable for a future non-breaking streaming adapter. First-class React Router data loaders remain out of scope for this PR.

This also removes the unrelated Metro/schema drift and restores the valid dts.consumeTypes.family: 0 schema behavior. The updated demo is the acceptance gate: React and Vue SSR, no-JS output, hydration, navigation, multiple instances of one remote, and production-mode Playwright all pass locally on pnpm 10.28.0 / Node 24. The required Node 20 rerun remains pending because Node 20 is not installed in this environment.

@tonoizer tonoizer changed the title feat(bridge): add instance-scoped application SSR foundation feat(bridge): add application-level SSR for React and Vue Jul 10, 2026
@tonoizer
tonoizer force-pushed the cursor/bridge-app-ssr-e63b branch from d927242 to c203d59 Compare July 18, 2026 13:10
Key the hydration registry by moduleName+instanceId, hydrate empty islands
from markers, destroy React roots when mount DOM identity changes, require a
registry for client references, and soft-cancel aborted Vue mounts so failed
renders cannot leak remotes.
Claim registry snapshots before provider.render and fail them on cancel so
SPA revisits cannot hydrate stale request-time state. KeepAlive reactivation
falls back to CSR, and demo hosts stop re-seeding SSR refs after navigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Application-level SSR for bridge remotes (React & Vue): renderToString contract + hydration

3 participants