diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index b034150f169..970ccc101a6 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -138,6 +138,11 @@ jobs:
uses: ./.github/workflows/e2e-router.yml
secrets: inherit
+ e2e-rsc:
+ needs: checkout-install
+ uses: ./.github/workflows/e2e-rsc.yml
+ secrets: inherit
+
e2e-metro:
permissions:
contents: read
diff --git a/.github/workflows/e2e-rsc.yml b/.github/workflows/e2e-rsc.yml
new file mode 100644
index 00000000000..63078a36863
--- /dev/null
+++ b/.github/workflows/e2e-rsc.yml
@@ -0,0 +1,84 @@
+# .github/workflows/e2e-rsc.yml
+name: E2E RSC Demo
+
+on:
+ workflow_call:
+
+jobs:
+ e2e-rsc:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Cache Tool Downloads
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache
+ key: ${{ runner.os }}-toolcache-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-toolcache-
+
+ - name: Set Playwright cache status
+ run: |
+ if [ -d "$HOME/.cache/ms-playwright" ] || [ -d "$HOME/.cache/Cypress" ]; then
+ echo "PLAYWRIGHT_CACHE_HIT=true" >> "$GITHUB_ENV"
+ else
+ echo "PLAYWRIGHT_CACHE_HIT=false" >> "$GITHUB_ENV"
+ fi
+
+ - name: Install Pnpm
+ run: |
+ corepack prepare pnpm@8.11.0 --activate
+ corepack enable
+
+ - name: Setup Node.js 18
+ uses: actions/setup-node@v5
+ with:
+ node-version: '18'
+ cache: 'pnpm'
+
+ - name: Set Nx SHA
+ uses: nrwl/nx-set-shas@v3
+
+ - name: Set SKIP_DEVTOOLS_POSTINSTALL environment variable
+ run: echo "SKIP_DEVTOOLS_POSTINSTALL=true" >> "$GITHUB_ENV"
+
+ - name: Install Dependencies
+ run: pnpm install
+
+ - name: Install Playwright Browsers
+ run: pnpm -C apps/rsc-demo/packages/e2e exec playwright install --with-deps chromium
+
+ - name: Run Build for All Packages
+ run: npx nx run-many --targets=build --projects=tag:type:pkg
+
+ - name: Build RSC Demo
+ run: npx nx run rsc-demo:build
+
+ - name: Verify SSR Registry Source (MF Manifest)
+ run: |
+ echo "Checking mf-manifest.ssr.json additionalData.rsc.clientComponents..."
+ node -e "const mf=require('./apps/rsc-demo/packages/app1/build/mf-manifest.ssr.json'); const cc=mf?.additionalData?.rsc?.clientComponents||mf?.rsc?.clientComponents||{}; if(!Object.keys(cc).length){console.error('ERROR: app1 mf-manifest.ssr.json missing rsc.clientComponents'); process.exit(1)}"
+ node -e "const mf=require('./apps/rsc-demo/packages/app2/build/mf-manifest.ssr.json'); const cc=mf?.additionalData?.rsc?.clientComponents||mf?.rsc?.clientComponents||{}; if(!Object.keys(cc).length){console.error('ERROR: app2 mf-manifest.ssr.json missing rsc.clientComponents'); process.exit(1)}"
+ echo "SSR registry verified via mf-manifest.ssr.json in both apps"
+
+ - name: Verify Manifest Files
+ run: |
+ echo "Checking manifest files..."
+ test -f apps/rsc-demo/packages/app1/build/react-client-manifest.json || (echo "ERROR: react-client-manifest.json missing in app1" && exit 1)
+ test -f apps/rsc-demo/packages/app1/build/react-ssr-manifest.json || (echo "ERROR: react-ssr-manifest.json missing in app1" && exit 1)
+ test -f apps/rsc-demo/packages/app1/build/react-server-actions-manifest.json || (echo "ERROR: react-server-actions-manifest.json missing in app1" && exit 1)
+ test -f apps/rsc-demo/packages/app2/build/react-client-manifest.json || (echo "ERROR: react-client-manifest.json missing in app2" && exit 1)
+ test -f apps/rsc-demo/packages/app2/build/react-ssr-manifest.json || (echo "ERROR: react-ssr-manifest.json missing in app2" && exit 1)
+ test -f apps/rsc-demo/packages/app2/build/react-server-actions-manifest.json || (echo "ERROR: react-server-actions-manifest.json missing in app2" && exit 1)
+ echo "All manifest files present"
+
+ - name: Run RSC Tests (Node)
+ run: npx nx run rsc-demo:test:rsc
+
+ - name: Run RSC E2E Tests (Playwright)
+ run: npx nx run rsc-demo:test:e2e
diff --git a/README.md b/README.md
index 4d43d56612d..fc97ba26eba 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,12 @@ You can consider the module federation capabilities provided by this repository
To get started with Module Federation, see the [Quick Start](https://module-federation.io/guide/start/quick-start.html).
+## 🧪 React Server Components (RSC) Demo
+
+This repo includes an experimental RSC + Module Federation reference implementation:
+- Demo app: `apps/rsc-demo/`
+- Architecture notes (with Mermaid diagrams): `RSC_MF_ARCHITECTURE.md`
+
## 🧑💻 Community
Come and chat with us on [Discussions](https://github.com/module-federation/universe/discussions) or [Discord](https://discord.gg/n69NnT3ACV)! The Module federation team and users are active there, and we're always looking for contributions.
diff --git a/RSC_MF_ARCHITECTURE.md b/RSC_MF_ARCHITECTURE.md
new file mode 100644
index 00000000000..986da562c74
--- /dev/null
+++ b/RSC_MF_ARCHITECTURE.md
@@ -0,0 +1,516 @@
+# RSC + Module Federation (rsc-demo) — Implementation Guide
+
+This is the **single, consolidated** doc for the `apps/rsc-demo/` reference implementation: how the build works, how runtime resolution works, and what we changed in `react-server-dom-webpack` (RSDW) to make the whole system “just work”.
+
+## Table of Contents
+
+- [Goals](#goals)
+- [Glossary](#glossary)
+- [Repo Layout](#repo-layout)
+- [Architecture At A Glance](#architecture-at-a-glance)
+- [Build System](#build-system)
+ - [Three Layers](#three-layers)
+ - [Share Scopes](#share-scopes)
+ - [Webpack Config Entry Points](#webpack-config-entry-points)
+- [Manifests And Metadata](#manifests-and-metadata)
+ - [React Manifests](#react-manifests)
+ - [MF Manifests](#mf-manifests)
+ - [`additionalData.rsc` Fields](#additionaldatarsc-fields)
+ - [`manifest.rsc` (build input) vs `additionalData.rsc` (manifest output)](#manifestrsc-build-input-vs-additionaldatarsc-manifest-output)
+- [Vendored `react-server-dom-webpack` Patch Set](#vendored-react-server-dom-webpack-patch-set)
+ - [Baseline + Diff Artifact](#baseline--diff-artifact)
+ - [What We Changed (Minimal Functional Patch)](#what-we-changed-minimal-functional-patch)
+ - [RSC Loaders (Client / RSC / SSR)](#rsc-loaders-client--rsc--ssr)
+ - [Server Action Registry (Global)](#server-action-registry-global)
+ - [ReactFlightPlugin Patches](#reactflightplugin-patches)
+ - [Node Register Patches](#node-register-patches)
+- [Runtime Behavior](#runtime-behavior)
+ - [Client-Side Federation](#client-side-federation)
+ - [Server-Side Federation (RSC)](#server-side-federation-rsc)
+ - [SSR Rendering (HTML From Flight)](#ssr-rendering-html-from-flight)
+ - [SSR Export Retention (Tree-Shaking Fix)](#ssr-export-retention-tree-shaking-fix)
+ - [Federated Server Actions](#federated-server-actions)
+- [Testing + CI](#testing--ci)
+- [Invariants / Guardrails](#invariants--guardrails)
+- [Known Limitations + Follow-Ups](#known-limitations--follow-ups)
+- [Appendix](#appendix)
+
+## Goals
+
+- **Monolithic UX** for federated RSC apps: no placeholder components and no silent “render null” fallbacks.
+- **MF-native server actions are the default**: remote actions execute **in-process** via Module Federation; HTTP proxying exists only as fallback.
+- **No “strict mode” env toggles** required for correctness. (Debug logging exists, but behavior does not change.)
+- **Layer-correct React resolution**:
+ - RSC layer resolves `react-server` exports.
+ - SSR + client resolve normal React exports.
+- **SSR must not crash** from webpack tree-shaking exports that React reads dynamically.
+
+## Glossary
+
+- **Host**: the app that renders the page and consumes remotes (demo: `app1`).
+- **Remote**: the app that provides federated modules (demo: `app2`).
+- **Client layer**: browser build.
+- **RSC layer**: server build that resolves `react-server` exports (`resolve.conditionNames` includes `react-server`).
+- **SSR layer**: server build that renders HTML from an RSC Flight stream (`target: async-node`), and must **not** run with `react-server` conditions at runtime.
+- **Share scopes**:
+ - `rsc` → RSC server bundles (ensures React resolves to `react-server` builds).
+ - `client` → browser and SSR bundles (ensures React resolves to normal client builds).
+- **React manifests**:
+ - `react-client-manifest.json` (Flight client references)
+ - `react-server-actions-manifest.json` (server action IDs → exports)
+ - `react-ssr-manifest.json` (SSR module map)
+- **MF manifests**:
+ - `mf-manifest.json` / `mf-manifest.server.json` / `mf-manifest.ssr.json` (+ `*-stats.json`)
+ - `additionalData.rsc` embeds RSC metadata into MF manifests.
+
+## Repo Layout
+
+- Demo app root: `apps/rsc-demo/`
+ - Host: `apps/rsc-demo/packages/app1/`
+ - Remote: `apps/rsc-demo/packages/app2/`
+ - Shared runtime/build helpers: `apps/rsc-demo/packages/app-shared/`
+ - Tests (Node + Playwright): `apps/rsc-demo/packages/e2e/`
+- Vendored React Server DOM bindings: `packages/react-server-dom-webpack/`
+- MF manifest metadata: `packages/manifest/src/rscManifestMetadata.ts`
+
+## Architecture At A Glance
+
+### Build outputs per app (three webpack layers)
+
+```mermaid
+flowchart LR
+ subgraph App2[Remote: app2]
+ A2C[client build] --> A2COut[remoteEntry.client.js mf-manifest.json react-client-manifest.json]
+ A2R[rsc build] --> A2ROut[remoteEntry.server.js mf-manifest.server.json react-server-actions-manifest.json]
+ A2S[ssr build] --> A2SOut[ssr.js mf-manifest.ssr.json react-ssr-manifest.json]
+ end
+
+ subgraph App1[Host: app1]
+ A1C[client build] --> A1COut[app shell + hydration mf-manifest.json react-client-manifest.json]
+ A1R[rsc build] --> A1ROut[server.rsc.js mf-manifest.server.json]
+ A1S[ssr build] --> A1SOut[ssr.js mf-manifest.ssr.json react-ssr-manifest.json]
+ end
+```
+
+### RSC render with a remote Server Component
+
+```mermaid
+sequenceDiagram
+ participant B as Browser
+ participant H as app1 (host server)
+ participant R as app1 RSC bundle (server.rsc.js)
+ participant MF as MF runtime (@module-federation/node)
+ participant M2 as app2 mf-manifest.server.json
+ participant C2 as app2 remoteEntry.server.js
+
+ B->>H: GET /
+ H->>R: renderRSCToBuffer() / renderApp()
+ R->>MF: import('app2/RemoteServerWidget')
+ MF->>M2: fetch mf-manifest.server.json
+ MF->>C2: fetch + evaluate remoteEntry.server.js (+ chunks)
+ C2-->>MF: factory(module)
+ MF-->>R: module exports
+ R-->>H: RSC Flight stream
+ H-->>B: HTML (SSR) + embedded Flight
+```
+
+### MF-native server actions (default) with HTTP fallback
+
+```mermaid
+sequenceDiagram
+ participant B as Browser
+ participant H as app1 /react (host)
+ participant R as app1 RSC bundle
+ participant MF as MF runtime (@module-federation/node + rscRuntimePlugin)
+ participant M2 as app2 mf-manifest.server.json
+ participant SA as app2 react-server-actions-manifest.json
+ participant C2 as app2 remoteEntry.server.js
+ participant A2 as app2 server-actions module
+
+ B->>H: POST /react (header: rsc-action: )
+ H->>MF: ensureRemoteActionsRegistered()
+ MF->>M2: fetch mf-manifest.server.json
+ MF->>SA: fetch react-server-actions-manifest.json
+ MF->>C2: load remoteEntry.server.js
+ MF->>A2: remoteEntry.get('./server-actions')()
+ MF-->>R: registerServerReference(fn, id, export)
+ H->>R: getServerAction(actionId)
+ R-->>H: action function (from serverActionRegistry)
+ H->>H: execute action in-process
+ H-->>B: Flight response (no proxy hop)
+
+ note over H: If MF-native lookup/registration fails, app1 forwards to app2 over HTTP as fallback.
+```
+
+### SSR rendering path (HTML from Flight)
+
+```mermaid
+sequenceDiagram
+ participant B as Browser
+ participant H as app1 GET /
+ participant R as app1 RSC bundle
+ participant W as app1 ssr-worker.js (node child process)
+ participant S as app1 SSR bundle (ssr.js)
+
+ B->>H: GET /
+ H->>R: renderRSCToBuffer()
+ H->>W: spawn ssr-worker (NODE_OPTIONS cleared)
+ W->>S: load ssr.js (renderFlightToHTML)
+ S-->>W: HTML string
+ W-->>H: HTML string
+ H-->>B: shell HTML with SSR + embedded Flight for hydration
+```
+
+## Build System
+
+This is an Nx + pnpm monorepo. The RSC demo’s build logic is intentionally explicit: you can read “the system” by reading the build scripts.
+
+### Three Layers
+
+Each app builds three outputs:
+
+- **client**: browser JS + `remoteEntry.client.js`
+- **rsc**: server bundle that resolves `react-server` exports + `remoteEntry.server.js`
+- **ssr**: server bundle for `react-dom/server` HTML rendering + `react-ssr-manifest.json`
+
+The layers exist because “RSC server execution”, “SSR HTML rendering”, and “browser hydration” have incompatible requirements:
+
+- RSC layer must resolve `react-server` exports.
+- SSR needs `react-dom/server`, which must not resolve to `react-server`.
+- Browser needs normal client builds.
+
+### Share Scopes
+
+We enforce two share scopes:
+
+- `rsc`: used by RSC-layer federation.
+- `client`: used by browser and SSR federation.
+
+Every MF config in the demo sets `experiments: { asyncStartup: true }` and avoids `eager: true`.
+
+### Webpack Config Entry Points
+
+- app1 (host):
+ - client: `apps/rsc-demo/packages/app1/scripts/client.build.js`
+ - rsc: `apps/rsc-demo/packages/app1/scripts/server.build.js`
+ - ssr: `apps/rsc-demo/packages/app1/scripts/ssr.build.js`
+- app2 (remote):
+ - all layers: `apps/rsc-demo/packages/app2/scripts/build.js`
+
+## Manifests And Metadata
+
+### React Manifests
+
+Generated by `react-server-dom-webpack/plugin` (vendored):
+
+- `react-client-manifest.json` (client refs for Flight)
+- `react-server-actions-manifest.json` (server action IDs)
+- `react-ssr-manifest.json` (SSR module map)
+
+### MF Manifests
+
+Generated by MF enhanced plugin:
+
+- `mf-manifest.json` (client layer)
+- `mf-manifest.server.json` (rsc layer)
+- `mf-manifest.ssr.json` (ssr layer)
+
+### `additionalData.rsc` Fields
+
+The MF manifest plugin attaches RSC metadata in:
+- `packages/manifest/src/rscManifestMetadata.ts`
+
+Core fields used by the demo:
+
+- `additionalData.rsc.layer`: `client | rsc | ssr`
+- `additionalData.rsc.shareScope`: `client | rsc`
+- `additionalData.rsc.isRSC`: boolean
+- `additionalData.rsc.conditionNames`: for debugging / reproducibility
+- `additionalData.rsc.clientComponents`: registry used by SSR to map Flight client references → SSR module IDs
+
+In the demo, app2 also publishes:
+- `additionalData.rsc.exposeTypes` (a map marking `./server-actions` as `server-action`), which the runtime plugin uses to decide what to register as actions.
+
+### `manifest.rsc` (build input) vs `additionalData.rsc` (manifest output)
+
+In this repo, we treat **MF manifests as the transport** for RSC metadata.
+
+- **Build-time input**: each `ModuleFederationPlugin` instance can pass `manifest.rsc` config.
+ - Example: `apps/rsc-demo/packages/app2/scripts/build.js` sets `manifest.rsc.remote`, `manifest.rsc.exposeTypes`, and URLs for manifests.
+- **Build-time output**: the manifest plugin computes/normalizes and then writes the final object into:
+ - `mf-manifest*.json` → `additionalData.rsc` (and also `rsc` for convenience)
+
+The normalizer lives here:
+- `packages/manifest/src/rscManifestMetadata.ts`
+
+Practical schema (subset used by the demo):
+
+```ts
+type RscLayer = 'client' | 'rsc' | 'ssr';
+type RscShareScope = 'client' | 'rsc';
+
+interface ManifestRscOptions {
+ layer?: RscLayer;
+ shareScope?: RscShareScope;
+ isRSC?: boolean;
+ conditionNames?: string[];
+
+ // Optional: published URLs so other containers can discover manifests/endpoints
+ serverActionsManifest?: string; // e.g. http://remote/react-server-actions-manifest.json
+ clientManifest?: string; // e.g. http://remote/react-client-manifest.json
+
+ // Optional: declared remote metadata (used by runtime plugin + fallback routing)
+ remote?: {
+ name: string;
+ url: string;
+ actionsEndpoint?: string; // e.g. http://remote/react (HTTP fallback)
+ serverContainer?: string; // e.g. http://remote/remoteEntry.server.js
+ };
+
+ // Optional: classify exposes so runtime can treat some as server actions
+ exposeTypes?: Record;
+
+ // Optional override: client component registry for SSR moduleMap resolution.
+ // If omitted, `rscManifestMetadata.ts` derives it from React manifests:
+ // - client layer: react-client-manifest.json
+ // - ssr layer: react-ssr-manifest.json (preferred) or react-client-manifest.json (fallback)
+ clientComponents?: Record;
+}
+```
+
+Where `additionalData.rsc.clientComponents` comes from (when not overridden):
+
+- client build: derived from `react-client-manifest.json`
+- ssr build: derived from `react-ssr-manifest.json` (preferred), otherwise from `react-client-manifest.json`
+
+Where this metadata is consumed:
+
+- **SSR worker**: preloads `globalThis.__RSC_SSR_REGISTRY__` from `mf-manifest.ssr.json` (preferred) or `react-ssr-manifest.json`:
+ - `apps/rsc-demo/packages/app1/server/ssr-worker.js`
+- **MF-native server actions**: runtime plugin uses:
+ - `exposeTypes` to detect `server-action` exposes
+ - `serverActionsManifest` (or a computed sibling URL) to fetch action IDs
+ - `remote.actionsEndpoint` for HTTP fallback URL construction
+ - `apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js`
+
+## Vendored `react-server-dom-webpack` Patch Set
+
+We vendor `react-server-dom-webpack@19.2.0` into `packages/react-server-dom-webpack/` so we can:
+
+- expose stable, consumable loader entrypoints (`rsc-*-loader`)
+- emit manifests early enough for MF compilation hooks
+- provide a server action registry that survives MF share-scope / module duplication edge cases
+
+### Baseline + Diff Artifact
+
+- Baseline: npm `react-server-dom-webpack@19.2.0`
+- Minimal functional diff artifact: `arch-doc/rsdw-diffs/rsdw-vendored-vs-npm-19.2.0.functional.diff`
+
+### What We Changed (Minimal Functional Patch)
+
+Changed/added files (functional):
+
+- `packages/react-server-dom-webpack/package.json`
+ - `"private": true`
+ - exports new entrypoints:
+ - `react-server-dom-webpack/rsc-client-loader`
+ - `react-server-dom-webpack/rsc-server-loader`
+ - `react-server-dom-webpack/rsc-ssr-loader`
+- `packages/react-server-dom-webpack/server.node.js`
+ - wraps `registerServerReference()` to populate a global registry on `globalThis`
+ - exports `getServerAction()`, `getDynamicServerActionsManifest()`, `clearServerActionRegistry()`
+- `packages/react-server-dom-webpack/server.node.unbundled.js`
+ - similar registry behavior for unbundled node usage
+- `packages/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js`
+ - emits manifests at `PROCESS_ASSETS_STAGE_SUMMARIZE`
+ - emits `react-server-actions-manifest.json` and merges action entries from loaders
+- `packages/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js`
+ - supports inline `'use server'` functions by injecting registration calls
+- Added loaders:
+ - `packages/react-server-dom-webpack/cjs/rsc-client-loader.js`
+ - `packages/react-server-dom-webpack/cjs/rsc-server-loader.js`
+ - `packages/react-server-dom-webpack/cjs/rsc-ssr-loader.js`
+
+### RSC Loaders (Client / RSC / SSR)
+
+Loader entrypoints used by the demo:
+
+- **client layer**: `react-server-dom-webpack/rsc-client-loader`
+ - turns file-level `'use server'` exports into `createServerReference()` stubs
+ - records entries into `serverReferencesMap` (read by the plugin)
+- **rsc layer**: `react-server-dom-webpack/rsc-server-loader`
+ - turns `'use client'` modules into `createClientModuleProxy(file://...)`
+ - registers file-level `'use server'` exports via `registerServerReference`
+ - registers named inline `'use server'` functions and records them into `inlineServerActionsMap`
+- **ssr layer**: `react-server-dom-webpack/rsc-ssr-loader`
+ - replaces `'use server'` exports with throw-stubs (SSR must not execute actions)
+
+### Server Action Registry (Global)
+
+Why a global registry exists:
+
+- In MF scenarios it’s possible to end up with multiple module instances of RSDW across different containers/chunks.
+- Without a shared registry, actions can be registered in one instance and looked up in another, yielding “missing action” failures.
+
+Where:
+- `packages/react-server-dom-webpack/server.node.js`
+
+Exports used by the demo host:
+- `getServerAction(actionId)`
+- `getDynamicServerActionsManifest()`
+
+### ReactFlightPlugin Patches
+
+Where:
+- `packages/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js`
+
+What changed:
+
+- emit `react-client-manifest.json` and `react-server-actions-manifest.json` earlier (`PROCESS_ASSETS_STAGE_SUMMARIZE`) so MF’s compilation hooks can read them
+- merge server actions from:
+ - AST-discovered `'use server'` file exports
+ - `serverReferencesMap` (client loader)
+ - `inlineServerActionsMap` (server loader)
+
+### Node Register Patches
+
+Where:
+- `packages/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js`
+
+What changed:
+- adds “inline action” detection (functions whose body begins with `'use server'`) and injects `registerServerReference(...)` calls so those actions are discoverable.
+
+## Runtime Behavior
+
+### Client-Side Federation
+
+Client-side federation is demonstrated by:
+- `apps/rsc-demo/packages/app1/src/RemoteButton.js`
+
+Behavior:
+- loads `app2/Button` via MF on the client after mount
+- **throws** on load failure (no “unavailable” placeholder UI)
+
+### Server-Side Federation (RSC)
+
+Server-side federation is demonstrated by:
+- `apps/rsc-demo/packages/app1/src/FederatedDemo.server.js`
+
+Behavior:
+- RSC server imports `app2/RemoteServerWidget` and renders it as part of the server component tree.
+
+### SSR Rendering (HTML From Flight)
+
+SSR is implemented via:
+
+- SSR worker (separate process without `react-server`):
+ - `apps/rsc-demo/packages/app1/server/ssr-worker.js`
+- SSR bundle entry:
+ - `apps/rsc-demo/packages/app1/src/framework/ssr-entry.js`
+
+Key points:
+
+- The server renders RSC to a Flight buffer.
+- The worker loads `build/ssr.js` and calls `renderFlightToHTML(flightBuffer, clientManifest)`.
+- SSR resolves client references using a preloaded registry (`globalThis.__RSC_SSR_REGISTRY__`) built from `react-ssr-manifest.json` or `mf-manifest.ssr.json`.
+
+### SSR Export Retention (Tree-Shaking Fix)
+
+The real SSR failure mode is webpack tree-shaking:
+
+- React SSR reads exports dynamically from the SSR bundle.
+- Webpack can’t see those accesses statically → it can prune exports → SSR renders an `undefined` export.
+
+Fix (build-time, not runtime placeholders):
+
+- `apps/rsc-demo/packages/app-shared/scripts/AutoIncludeClientComponentsPlugin.js`
+ - reads `react-client-manifest.json`
+ - `compilation.addInclude(...)` for every referenced client module
+ - calls `moduleGraph.getExportsInfo(mod).setUsedInUnknownWay(runtime)` so webpack keeps exports
+
+SSR bundle config also sets:
+- `optimization.mangleExports = false`
+- `optimization.concatenateModules = false`
+
+### Federated Server Actions
+
+Server actions have two execution paths:
+
+1) **MF-native (default)**: remote action executes in-process via MF.
+2) **HTTP forwarding (fallback)**: host proxies the Flight request to the remote.
+
+#### MF-native path (default)
+
+Pieces:
+
+- Host action handler calls `ensureRemoteActionsRegistered()`:
+ - `apps/rsc-demo/packages/app1/server/api.server.js`
+- Host RSC bundle triggers remote module load:
+ - `apps/rsc-demo/packages/app1/src/server-entry.js` (`require('app2/server-actions')`)
+- Runtime plugin registers actions on remote load:
+ - `apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js`
+
+How registration works:
+
+- Runtime plugin loads remote `mf-manifest.server(.json|stats.json)` and reads `additionalData.rsc.exposeTypes`.
+- For exposes marked `server-action`, it fetches `react-server-actions-manifest.json`.
+- It loads the expose module and calls `registerServerReference(fn, id, exportName)` for each manifest entry.
+- Patched RSDW stores these in `globalThis.__RSC_SERVER_ACTION_REGISTRY__`, so `getServerAction(actionId)` works from the host.
+
+#### HTTP forwarding fallback
+
+Where:
+- `apps/rsc-demo/packages/app1/server/api.server.js` (`forwardActionToRemote`)
+
+Behavior:
+- if `getServerAction(actionId)` is missing after MF-native registration attempts, the host proxies the Flight request to `app2`’s `/react`.
+
+## Testing + CI
+
+Local:
+
+- build packages: `pnpm -w build:pkg`
+- RSC tests: `npx nx run rsc-demo:test:rsc --skip-nx-cache`
+- Playwright E2E: `npx nx run rsc-demo:test:e2e --skip-nx-cache`
+
+CI:
+
+- Adds an RSC E2E workflow: `.github/workflows/e2e-rsc.yml`
+- The main workflow includes the RSC E2E job: `.github/workflows/build-and-test.yml`
+
+What we assert in tests:
+
+- remote client component loads via MF and is interactive
+- remote server component renders in the host server component tree
+- MF-native server actions execute with **no proxy hop** (asserted via response headers)
+- SSR is deterministic and doesn’t require placeholder components
+
+## Invariants / Guardrails
+
+- MF configs must set `experiments: { asyncStartup: true }`.
+- Do **not** use `eager: true` for shared modules; async init is expected.
+- Keep share scopes separated by layer: `rsc` vs `client`.
+- SSR worker must not run with `react-server` condition at runtime (`NODE_OPTIONS` stripped).
+
+## Known Limitations + Follow-Ups
+
+- Full server-side federation of `'use client'` components (rendering remote client islands via SSR) needs a more general registry/manifest merge strategy. The demo shows the shape and keeps the hard problems explicit.
+- HTTP forwarding exists as fallback for robustness; long-term production usage should aim to make MF-native the only path.
+
+## Appendix
+
+### RSDW diff reproduction
+
+The minimal functional diff is checked in:
+- `arch-doc/rsdw-diffs/rsdw-vendored-vs-npm-19.2.0.functional.diff`
+
+To reproduce a full file-level diff locally:
+
+```bash
+tmpdir="$(mktemp -d)"
+cd "$tmpdir"
+npm pack react-server-dom-webpack@19.2.0
+tar -xzf react-server-dom-webpack-19.2.0.tgz
+cd - >/dev/null
+diff -ruN "$tmpdir/package" "packages/react-server-dom-webpack" || true
+```
diff --git a/apps/rsc-demo/.gitignore b/apps/rsc-demo/.gitignore
new file mode 100644
index 00000000000..faa39902381
--- /dev/null
+++ b/apps/rsc-demo/.gitignore
@@ -0,0 +1,40 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+**/node_modules/
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+**/dist
+/packages/app1/dist
+/packages/app2/dist
+**/build
+/packages/app1/build
+/packages/app2/build
+
+# notes (runtime)
+packages/*/notes/*.md
+test-results/
+
+# misc
+.DS_Store
+.eslintcache
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# vscode
+.vscode
diff --git a/apps/rsc-demo/AGENTS.md b/apps/rsc-demo/AGENTS.md
new file mode 100644
index 00000000000..16e732f180c
--- /dev/null
+++ b/apps/rsc-demo/AGENTS.md
@@ -0,0 +1,43 @@
+# Repository Guidelines
+
+## Project Structure & Modules
+- Monorepo managed by `pnpm`. Primary apps live in `packages/app1` and `packages/app2`; shared RSC tooling is in repo root `packages/react-server-dom-webpack`.
+- App source: `packages/*/src`. Servers: `packages/*/server`. Webpack configs and build scripts: `packages/*/scripts`.
+- Tests: unit/integration in `packages/e2e/rsc`, Playwright E2E in `packages/e2e/e2e`. Build output lands in `packages/*/build` (gitignored).
+
+## Build, Test, Dev Commands
+- `pnpm install` — install workspace deps.
+- `pnpm start` — run app1 dev server with webpack watch (bundler + server).
+- `pnpm --filter app2 start` — same for app2.
+- `pnpm run build` — production builds for app1 and app2 (client + server layers).
+- `pnpm test` — top-level test entry; runs RSC tests and MF tests after building.
+- `pnpm run test:rsc` — RSC unit/integration tests (Node `--test`).
+- `pnpm run test:e2e:rsc` — Playwright smoke for the RSC notes apps.
+- `pnpm run test:e2e` — all Playwright suites (requires prior build).
+
+## Coding Style & Naming
+- JavaScript/React with ES modules; prefer functional components.
+- Indent with 2 spaces; keep files ASCII-only unless existing file uses Unicode.
+- Client components carry the `'use client'` directive; server actions/components avoid it. Name server action files `*.server.js` when possible.
+- Webpack chunk/module ids are kept readable (`chunkIds: 'named', moduleIds: 'named'`).
+
+## Testing Guidelines
+- Frameworks: Node’s built-in `node --test`, Playwright for E2E.
+- Place unit/integration specs under `packages/e2e/rsc`. Name with `.test.js`.
+- E2E specs live in `packages/e2e/e2e`; keep them idempotent and avoid relying on pre-existing data.
+- Run `pnpm run build` before E2E to ensure assets exist.
+
+## Commit & PR Expectations
+- Use concise, descriptive commit messages (e.g., `fix: inline action manifest ids`).
+- For PRs, include: summary of changes, testing performed (`pnpm test:rsc`, `pnpm test:e2e:rsc`), and any follow-up risks or TODOs.
+
+## Module Federation Configuration
+- ALL Module Federation plugins MUST include `experiments: { asyncStartup: true }` in their configuration (both client and server).
+- ALL shared modules MUST use `eager: false` - no exceptions. The federation runtime handles async loading.
+- Server-side code using asyncStartup bundles must `await` the module loads since module init is async.
+- Use separate share scopes for different layers: `'client'` for browser bundles, `'rsc'` for RSC server bundles.
+- Shared modules must also specify `layer` and `issuerLayer` matching the webpack layer they belong to (e.g., `client`, `rsc`, `ssr`).
+
+## Security & Config Tips
+- Do not check `packages/*/build` or credentials into git; `.gitignore` already covers build artifacts.
+- If enabling Postgres locally, gate with `USE_POSTGRES` and ensure fallback to the mock DB for offline runs.
diff --git a/apps/rsc-demo/README.md b/apps/rsc-demo/README.md
new file mode 100644
index 00000000000..279c2d1a9b8
--- /dev/null
+++ b/apps/rsc-demo/README.md
@@ -0,0 +1,149 @@
+# React Server Components Demo
+
+* [What is this?](#what-is-this)
+* [When will I be able to use this?](#when-will-i-be-able-to-use-this)
+* [Should I use this demo for benchmarks?](#should-i-use-this-demo-for-benchmarks)
+* [Setup](#setup)
+* [DB Setup](#db-setup)
+ + [Step 1. Create the Database](#step-1-create-the-database)
+ + [Step 2. Connect to the Database](#step-2-connect-to-the-database)
+ + [Step 3. Run the seed script](#step-3-run-the-seed-script)
+* [Module Federation & RSC](#module-federation--rsc)
+* [Notes about this app](#notes-about-this-app)
+ + [Interesting things to try](#interesting-things-to-try)
+* [Built by (A-Z)](#built-by-a-z)
+* [Code of Conduct](#code-of-conduct)
+* [License](#license)
+
+## What is this?
+
+This is a demo app built with Server Components, an experimental React feature. **We strongly recommend [watching our talk introducing Server Components](https://reactjs.org/server-components) before exploring this demo.** The talk includes a walkthrough of the demo code and highlights key points of how Server Components work and what features they provide.
+
+**Update (March 2023):** This demo has been updated to match the [latest conventions](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components).
+
+## When will I be able to use this?
+
+Server Components are an experimental feature and **are not ready for adoption**. For now, we recommend experimenting with Server Components via this demo app. **Use this in your projects at your own risk.**
+
+## Should I use this demo for benchmarks?
+
+If you use this demo to compare React Server Components to the framework of your choice, keep this in mind:
+
+* **This demo doesn’t have server rendering.** Server Components are a separate (but complementary) technology from Server Rendering (SSR). Server Components let you run some of your components purely on the server. SSR, on the other hand, lets you generate HTML before any JavaScript loads. This demo *only* shows Server Components, and not SSR. Because it doesn't have SSR, the initial page load in this demo has a client-server network waterfall, and **will be much slower than any SSR framework**. However, Server Components are meant to be integrated together with SSR, and they *will* be in a future release.
+* **This demo doesn’t have an efficient bundling strategy.** When you use Server Components, a bundler plugin will automatically split the client JS bundle. However, the way it's currently being split is not necessarily optimal. We are investigating more efficient ways to split the bundles, but they are out of scope of this demo.
+* **This demo doesn’t have partial refetching.** Currently, when you click on different “notes”, the entire app shell is refetched from the server. However, that’s not ideal: for example, it’s unnecessary to refetch the sidebar content if all that changed is the inner content of the right pane. Partial refetching is an [open area of research](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#open-areas-of-research) and we don’t yet know how exactly it will work.
+
+This demo is provided “as is” to show the parts that are ready for experimentation. It is not intended to reflect the performance characteristics of a real app driven by a future stable release of Server Components.
+
+## Setup
+
+You will need to have [Node 18 LTS](https://nodejs.org/en) in order to run this demo. (If you use `nvm`, use the repo root `.nvmrc` or run `nvm install 18`.)
+
+ ```
+ pnpm install
+ pnpm start
+ ```
+
+(Or `pnpm start:prod` for a production build.)
+
+Then open http://localhost:4101.
+
+By default the demo runs with an in-memory store (no Postgres required). If you'd like Postgres-backed persistence, follow the DB setup below.
+
+## DB Setup
+
+This demo uses Postgres. First, follow its [installation link](https://wiki.postgresql.org/wiki/Detailed_installation_guides) for your platform.
+
+Alternatively, you can check out this [fork](https://github.com/pomber/server-components-demo/) which will let you run the demo app without needing a database. However, you won't be able to execute SQL queries (but fetch should still work). There is also [another fork](https://github.com/prisma/server-components-demo) that uses Prisma with SQLite, so it doesn't require additional setup.
+
+The below example will set up the database for this app, assuming that you have a UNIX-like platform:
+
+### Step 1. Create the Database
+
+```
+psql postgres
+
+CREATE DATABASE notesapi;
+CREATE ROLE notesadmin WITH LOGIN PASSWORD 'password';
+ALTER ROLE notesadmin WITH SUPERUSER;
+ALTER DATABASE notesapi OWNER TO notesadmin;
+\q
+```
+
+### Step 2. Connect to the Database
+
+```
+psql -d postgres -U notesadmin;
+
+\c notesapi
+
+DROP TABLE IF EXISTS notes;
+CREATE TABLE notes (
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ title TEXT,
+ body TEXT
+);
+
+\q
+```
+
+### Step 3. Run the seed script
+
+Finally, run `npm run seed` to populate some data.
+
+And you're done!
+
+## Module Federation & RSC
+
+This fork additionally experiments with **React Server Components + Module Federation** across two apps (`packages/app1`, `packages/app2`):
+
+- Client‑side federation is handled with `@module-federation/enhanced` in the **client** layer.
+- RSC/server federation is handled with a Node MF container in the **rsc** layer.
+- Federated server actions support **in‑process MF‑native actions** (no HTTP hop) with **HTTP forwarding** as a fallback.
+- The demo vendors `react-server-dom-webpack` (repo root: `packages/react-server-dom-webpack`) so loaders/plugin behavior can be inspected and iterated on in this repo.
+
+For the single consolidated implementation guide (including the vendored `react-server-dom-webpack` patch set), see `RSC_MF_ARCHITECTURE.md` at the repo root.
+
+## Notes about this app
+
+The demo is a note-taking app called **React Notes**. It consists of a few major parts:
+
+- It uses a Webpack plugin (not defined in this repo) that allows us to only include client components in build artifacts
+- An Express server that:
+ - Serves API endpoints used in the app
+ - Renders Server Components into a special format that we can read on the client
+- A React app containing Server and Client components used to build React Notes
+
+This demo is built on top of our Webpack plugin, but this is not how we envision using Server Components when they are stable. They are intended to be used in a framework that supports server rendering — for example, in Next.js. This is an early demo -- the real integration will be developed in the coming months. Learn more in the [announcement post](https://reactjs.org/server-components).
+
+### Interesting things to try
+
+- Expand note(s) by hovering over the note in the sidebar, and clicking the expand/collapse toggle. Next, create or delete a note. What happens to the expanded notes?
+- Change a note's title while editing, and notice how editing an existing item animates in the sidebar. What happens if you edit a note in the middle of the list?
+- Search for any title. With the search text still in the search input, create a new note with a title matching the search text. What happens?
+- Search while on Slow 3G, observe the inline loading indicator.
+- Switch between two notes back and forth. Observe we don't send new responses next time we switch them again.
+- Uncomment the `await fetch('http://localhost:4000/sleep/....')` call in `Note.js` or `NoteList.js` to introduce an artificial delay and trigger Suspense.
+ - If you only uncomment it in `Note.js`, you'll see the fallback every time you open a note.
+ - If you only uncomment it in `NoteList.js`, you'll see the list fallback on first page load.
+ - If you uncomment it in both, it won't be very interesting because we have nothing new to show until they both respond.
+- Add a new Server Component and place it above the search bar in `App.js`. Import `db` from `db.js` and use `await db.query()` from it to get the number of notes. Oberserve what happens when you add or delete a note.
+
+You can watch a [recorded walkthrough of all these demo points here](https://youtu.be/La4agIEgoNg?t=600) with timestamps. (**Note:** this recording is slightly outdated because the repository has been updated to match the [latest conventions](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components).)
+
+## Built by (A-Z)
+
+- [Andrew Clark](https://twitter.com/acdlite)
+- [Dan Abramov](https://twitter.com/dan_abramov)
+- [Joe Savona](https://twitter.com/en_JS)
+- [Lauren Tan](https://twitter.com/sugarpirate_)
+- [Sebastian Markbåge](https://twitter.com/sebmarkbage)
+- [Tate Strickland](http://www.tatestrickland.com/) (Design)
+
+## [Code of Conduct](https://engineering.fb.com/codeofconduct/)
+Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read the [full text](https://engineering.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated.
+
+## License
+This demo is MIT licensed.
diff --git a/apps/rsc-demo/package.json b/apps/rsc-demo/package.json
new file mode 100644
index 00000000000..41381da5d81
--- /dev/null
+++ b/apps/rsc-demo/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "react-notes",
+ "version": "0.1.0",
+ "private": true,
+ "packageManager": "pnpm@8.11.0",
+ "engines": {
+ "node": "^18"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "7.21.3",
+ "@babel/plugin-transform-modules-commonjs": "^7.21.2",
+ "@babel/preset-react": "^7.18.6",
+ "@babel/register": "^7.21.0",
+ "acorn-jsx": "^5.3.2",
+ "acorn-loose": "^8.3.0",
+ "babel-loader": "8.3.0",
+ "busboy": "^1.6.0",
+ "compression": "^1.7.4",
+ "concurrently": "^7.6.0",
+ "date-fns": "^2.29.3",
+ "excerpts": "^0.0.3",
+ "express": "^4.18.2",
+ "html-webpack-plugin": "5.5.0",
+ "marked": "^4.2.12",
+ "nodemon": "^2.0.21",
+ "pg": "^8.10.0",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-error-boundary": "^3.1.4",
+ "react-server-dom-webpack": "workspace:*",
+ "resolve": "1.22.1",
+ "rimraf": "^4.4.0",
+ "sanitize-html": "^2.10.0",
+ "server-only": "^0.0.1",
+ "webpack": "5.76.2"
+ },
+ "devDependencies": {
+ "@module-federation/enhanced": "workspace:*",
+ "@module-federation/runtime": "workspace:*",
+ "@playwright/test": "^1.48.2",
+ "cross-env": "^7.0.3",
+ "jsdom": "^24.1.1",
+ "prettier": "1.19.1",
+ "supertest": "^7.1.4"
+ },
+ "scripts": {
+ "start": "pnpm --filter app1 start",
+ "start:app1": "pnpm --filter app1 start",
+ "start:app2": "pnpm --filter app2 start",
+ "start:prod": "pnpm --filter app1 start:prod",
+ "build": "pnpm --filter app2 build && pnpm --filter app1 build",
+ "build:app1": "pnpm --filter app1 build",
+ "build:app2": "pnpm --filter app2 build",
+ "build:mf": "pnpm --filter app2 build && pnpm --filter app1 build",
+ "test": "pnpm run build && pnpm --filter e2e test:rsc && pnpm --filter e2e test:e2e:rsc",
+ "test:all": "pnpm run build && pnpm --filter e2e test:rsc && pnpm --filter e2e test:e2e:rsc",
+ "test:e2e": "pnpm run build && pnpm --filter e2e test:e2e:rsc",
+ "test:e2e:rsc": "pnpm run build && pnpm --filter e2e test:e2e:rsc",
+ "test:e2e:mf": "echo \"mf e2e skipped\"",
+ "test:rsc": "pnpm --filter e2e test:rsc",
+ "test:mf": "echo \"mf tests skipped\"",
+ "prettier": "prettier --write **/*.js"
+ },
+ "babel": {
+ "presets": [
+ [
+ "@babel/preset-react",
+ {
+ "runtime": "automatic"
+ }
+ ]
+ ]
+ },
+ "nodemonConfig": {
+ "ignore": [
+ "build/*"
+ ]
+ }
+}
diff --git a/apps/rsc-demo/packages/app-shared/framework/bootstrap.js b/apps/rsc-demo/packages/app-shared/framework/bootstrap.js
new file mode 100644
index 00000000000..5c014fc52a9
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/framework/bootstrap.js
@@ -0,0 +1,54 @@
+/**
+ * Shared client bootstrap for the RSC notes apps.
+ *
+ * This is imported by both app1 and app2 via their local
+ * src/framework/bootstrap.js wrappers so that the boot logic
+ * stays in one place.
+ */
+
+import { createRoot, hydrateRoot } from 'react-dom/client';
+import { ErrorBoundary } from 'react-error-boundary';
+import { Router, callServer, initFromSSR } from './router';
+
+// Set up global callServer for server action references
+// This is used by the server-action-client-loader transformation
+globalThis.__RSC_CALL_SERVER__ = callServer;
+
+const rootElement = document.getElementById('root');
+
+// Check if we have SSR data embedded in the page
+const rscDataElement = document.getElementById('__RSC_DATA__');
+
+if (rscDataElement && rootElement && rootElement.children.length > 0) {
+ // Hydration path: SSR'd HTML exists, hydrate from embedded RSC data
+ try {
+ const rscData = JSON.parse(rscDataElement.textContent);
+ initFromSSR(rscData);
+ hydrateRoot(rootElement, );
+ } catch (error) {
+ console.error('Hydration failed, falling back to client render:', error);
+ const root = createRoot(rootElement);
+ root.render();
+ }
+} else if (rootElement) {
+ // Client-only path: no SSR, render from scratch
+ const root = createRoot(rootElement);
+ root.render();
+}
+
+function Root() {
+ return (
+
+
+
+ );
+}
+
+function Error({ error }) {
+ return (
+
+
Application Error
+
{error.stack}
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app-shared/framework/router.js b/apps/rsc-demo/packages/app-shared/framework/router.js
new file mode 100644
index 00000000000..c5265f727ea
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/framework/router.js
@@ -0,0 +1,168 @@
+/**
+ * Shared router implementation for the RSC notes apps.
+ *
+ * This is imported by both app1 and app2 via their local
+ * src/framework/router.js wrappers so that navigation,
+ * callServer, and SSR integration stay in sync.
+ */
+
+'use client';
+
+import {
+ createContext,
+ startTransition,
+ useContext,
+ useState,
+ use,
+} from 'react';
+import {
+ createFromFetch,
+ createFromReadableStream,
+ encodeReply,
+} from 'react-server-dom-webpack/client';
+
+// RSC Action header (must match server)
+const RSC_ACTION_HEADER = 'rsc-action';
+
+export async function callServer(actionId, args) {
+ const body = await encodeReply(args);
+
+ const response = await fetch('/react', {
+ method: 'POST',
+ headers: {
+ Accept: 'text/x-component',
+ [RSC_ACTION_HEADER]: actionId,
+ },
+ body,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Server action failed: ${await response.text()}`);
+ }
+
+ const resultHeader = response.headers.get('X-Action-Result');
+ const actionResult = resultHeader ? JSON.parse(resultHeader) : undefined;
+
+ return actionResult;
+}
+
+const RouterContext = createContext();
+const initialCache = new Map();
+
+export function initFromSSR(rscData) {
+ const initialLocation = {
+ selectedId: null,
+ isEditing: false,
+ searchText: '',
+ };
+ const locationKey = JSON.stringify(initialLocation);
+
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(rscData));
+ controller.close();
+ },
+ });
+
+ const content = createFromReadableStream(stream);
+ initialCache.set(locationKey, content);
+}
+
+export function Router() {
+ const [cache, setCache] = useState(initialCache);
+ const [location, setLocation] = useState({
+ selectedId: null,
+ isEditing: false,
+ searchText: '',
+ });
+
+ const locationKey = JSON.stringify(location);
+ let content = cache.get(locationKey);
+ if (!content) {
+ content = createFromFetch(
+ fetch('/react?location=' + encodeURIComponent(locationKey)),
+ );
+ cache.set(locationKey, content);
+ }
+
+ function refresh(response) {
+ startTransition(() => {
+ const nextCache = new Map();
+ if (response != null) {
+ const locationKey = response.headers.get('X-Location');
+ const nextLocation = JSON.parse(locationKey);
+ const nextContent = createFromReadableStream(response.body);
+ nextCache.set(locationKey, nextContent);
+ navigate(nextLocation);
+ }
+ setCache(nextCache);
+ });
+ }
+
+ function navigate(nextLocation) {
+ startTransition(() => {
+ setLocation((loc) => ({
+ ...loc,
+ ...nextLocation,
+ }));
+ });
+ }
+
+ return (
+
+ {use(content)}
+
+ );
+}
+
+export function useRouter() {
+ const context = useContext(RouterContext);
+ if (!context) {
+ return {
+ location: { selectedId: null, isEditing: false, searchText: '' },
+ navigate: () => {},
+ refresh: () => {},
+ };
+ }
+ return context;
+}
+
+export function useMutation({ endpoint, method }) {
+ const { refresh } = useRouter();
+ const [isSaving, setIsSaving] = useState(false);
+ const [didError, setDidError] = useState(false);
+ const [error, setError] = useState(null);
+ if (didError) {
+ throw error;
+ }
+
+ async function performMutation(payload, requestedLocation) {
+ setIsSaving(true);
+ try {
+ const response = await fetch(
+ `${endpoint}?location=${encodeURIComponent(
+ JSON.stringify(requestedLocation),
+ )}`,
+ {
+ method,
+ body: JSON.stringify(payload),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+ refresh(response);
+ } catch (e) {
+ setDidError(true);
+ setError(e);
+ } finally {
+ setIsSaving(false);
+ }
+ }
+
+ return [isSaving, performMutation];
+}
diff --git a/apps/rsc-demo/packages/app-shared/mf/runtimeLogPlugin.js b/apps/rsc-demo/packages/app-shared/mf/runtimeLogPlugin.js
new file mode 100644
index 00000000000..8b4fc3fc2ce
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/mf/runtimeLogPlugin.js
@@ -0,0 +1,100 @@
+'use strict';
+
+/**
+ * Lightweight runtime plugin for Module Federation.
+ *
+ * This is used for experimentation and introspection. When MF_DEBUG is truthy,
+ * it logs key lifecycle events for both browser and Node runtimes:
+ * - beforeInit / beforeRequest / afterResolve / onLoad
+ * - beforeLoadShare / loadShare
+ *
+ * The plugin is safe to include in production builds; logging is gated by
+ * MF_DEBUG so it stays silent by default.
+ */
+
+const isDebugEnabled = () => {
+ try {
+ // Guard for browser environments where process may not exist
+ return (
+ typeof process !== 'undefined' &&
+ process &&
+ process.env &&
+ process.env.MF_DEBUG
+ );
+ } catch (_e) {
+ return false;
+ }
+};
+
+const log = (...args) => {
+ if (!isDebugEnabled()) {
+ return;
+ }
+ // eslint-disable-next-line no-console
+ console.log('[MF runtime]', ...args);
+};
+
+/**
+ * Runtime plugin factory.
+ *
+ * NOTE: This must be a function returning the plugin object, as expected by
+ * @module-federation/enhanced runtime.
+ */
+function runtimeLogPlugin() {
+ return {
+ name: 'mf-runtime-log-plugin',
+
+ beforeInit(args) {
+ log('beforeInit', args && { name: args.name, version: args.version });
+ return args;
+ },
+
+ beforeRequest(args) {
+ log('beforeRequest', {
+ id: args.id,
+ from: args.from,
+ to: args.to,
+ method: args.method,
+ });
+ return args;
+ },
+
+ afterResolve(args) {
+ log('afterResolve', {
+ id: args.id,
+ resolved: args.resolved,
+ origin: args.origin,
+ });
+ return args;
+ },
+
+ onLoad(args) {
+ log('onLoad', {
+ id: args.id,
+ module: args.moduleId,
+ url: args.url,
+ });
+ return args;
+ },
+
+ async beforeLoadShare(args) {
+ log('beforeLoadShare', {
+ pkg: args.shareKey,
+ scope: args.shareScope,
+ requiredVersion: args.requiredVersion,
+ });
+ return args;
+ },
+
+ async loadShare(args) {
+ log('loadShare', {
+ pkg: args.shareKey,
+ scope: args.shareScope,
+ resolved: !!args.resolved,
+ });
+ return args;
+ },
+ };
+}
+
+module.exports = runtimeLogPlugin;
diff --git a/apps/rsc-demo/packages/app-shared/scripts/AutoIncludeClientComponentsPlugin.js b/apps/rsc-demo/packages/app-shared/scripts/AutoIncludeClientComponentsPlugin.js
new file mode 100644
index 00000000000..e6724707e7d
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/scripts/AutoIncludeClientComponentsPlugin.js
@@ -0,0 +1,102 @@
+'use strict';
+
+/**
+ * AutoIncludeClientComponentsPlugin
+ *
+ * Ensures SSR bundles include the client component modules referenced by
+ * `react-client-manifest.json`, and marks their exports as "used" so webpack
+ * doesn't tree-shake them away. React reads these exports dynamically at
+ * runtime (via the SSR resolver), so static analysis can't see the usage.
+ */
+class AutoIncludeClientComponentsPlugin {
+ constructor(options = {}) {
+ this.entryName = options.entryName || 'ssr';
+ this.manifestFilename =
+ options.manifestFilename || 'react-client-manifest.json';
+ }
+
+ apply(compiler) {
+ compiler.hooks.finishMake.tapAsync(
+ 'AutoIncludeClientComponentsPlugin',
+ async (compilation, callback) => {
+ try {
+ const { getEntryRuntime } = require('webpack/lib/util/runtime');
+ const fs = require('fs');
+ const path = require('path');
+
+ const manifestPath = path.join(
+ compiler.options.output.path,
+ this.manifestFilename,
+ );
+ if (!fs.existsSync(manifestPath)) return callback();
+
+ const clientManifest = JSON.parse(
+ fs.readFileSync(manifestPath, 'utf8'),
+ );
+ const entries = Object.values(clientManifest || {});
+ if (!entries.length) return callback();
+
+ const runtime = getEntryRuntime(compilation, this.entryName);
+
+ let SingleEntryDependency;
+ try {
+ // webpack >= 5.98
+ SingleEntryDependency = require('webpack/lib/dependencies/SingleEntryDependency');
+ } catch (_e) {
+ // webpack <= 5.97
+ SingleEntryDependency = require('webpack/lib/dependencies/EntryDependency');
+ }
+
+ const unique = new Set(
+ entries
+ .map((e) => e && e.id)
+ .filter(Boolean)
+ .map((moduleId) => {
+ const withoutPrefix = String(moduleId).replace(
+ /^\(client\)\//,
+ '',
+ );
+ return withoutPrefix.startsWith('.')
+ ? withoutPrefix
+ : `./${withoutPrefix}`;
+ }),
+ );
+
+ const includes = [...unique].map(
+ (req) =>
+ new Promise((resolve, reject) => {
+ const dep = new SingleEntryDependency(req);
+ dep.loc = { name: 'rsc-client-include' };
+ compilation.addInclude(
+ compiler.context,
+ dep,
+ { name: this.entryName },
+ (err, mod) => {
+ if (err) return reject(err);
+ if (mod) {
+ try {
+ compilation.moduleGraph
+ .getExportsInfo(mod)
+ .setUsedInUnknownWay(runtime);
+ } catch (_e) {
+ // best effort: don't fail the build if webpack internals change
+ }
+ }
+ resolve();
+ },
+ );
+ }),
+ );
+
+ await Promise.all(includes);
+ callback();
+ } catch (err) {
+ callback(err);
+ }
+ },
+ );
+ }
+}
+
+module.exports = AutoIncludeClientComponentsPlugin;
+module.exports.default = AutoIncludeClientComponentsPlugin;
diff --git a/apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js b/apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js
new file mode 100644
index 00000000000..5cb535bad95
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js
@@ -0,0 +1,521 @@
+'use strict';
+
+/**
+ * RSC Runtime Plugin for Module Federation
+ *
+ * This plugin provides RSC (React Server Components) integration with Module Federation:
+ *
+ * 1. **Server Actions (Option 2)**: When a remote's server-actions module is loaded via MF,
+ * this plugin automatically registers those actions with React's serverActionRegistry.
+ * This enables in-process execution of federated server actions without HTTP forwarding.
+ *
+ * 2. **Manifest-Driven Configuration**: Reads RSC metadata from federation manifest/stats to:
+ * - Discover remote's actionsEndpoint for HTTP fallback
+ * - Know which exposes are server-actions vs client-components
+ * - Get the server actions manifest URL
+ *
+ * 3. **Layer-Aware Share Resolution**: Uses rsc.layer and rsc.shareScope metadata
+ * to ensure proper share scope selection (rsc vs client vs ssr).
+ *
+ * Usage:
+ * runtimePlugins: [
+ * require.resolve('@module-federation/node/runtimePlugin'),
+ * require.resolve('./rscRuntimePlugin.js'),
+ * ]
+ */
+
+const LOG_PREFIX = '[RSC-MF]';
+const DEBUG = process.env.RSC_MF_DEBUG === '1';
+const fs = require('fs');
+const path = require('path');
+
+const FETCH_TIMEOUT_MS = 5000;
+
+// Cache for remote RSC configs loaded from mf-stats.json
+const remoteRSCConfigs = new Map();
+
+// Cache for remote MF manifests (mf-stats.json)
+const remoteMFManifests = new Map();
+
+// Cache for remote server actions manifests
+const remoteServerActionsManifests = new Map();
+
+// Track which remotes have had their actions registered
+const registeredRemotes = new Set();
+// Track in-flight registrations to avoid double work
+const registeringRemotes = new Map();
+
+function getHostFromUrl(value) {
+ try {
+ const url = new URL(value);
+ return url.host;
+ } catch (_e) {
+ return null;
+ }
+}
+
+/**
+ * Log helper - only logs if DEBUG is enabled
+ */
+function log(...args) {
+ if (DEBUG) {
+ console.log(LOG_PREFIX, ...args);
+ }
+}
+
+function isResponseLike(value) {
+ return (
+ value &&
+ typeof value === 'object' &&
+ typeof value.json === 'function' &&
+ typeof value.status === 'number'
+ );
+}
+
+async function fetchJson(url, origin) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+ try {
+ let res;
+
+ if (origin?.loaderHook?.lifecycle?.fetch?.emit) {
+ try {
+ res = await origin.loaderHook.lifecycle.fetch.emit(url, {
+ signal: controller.signal,
+ });
+ } catch (_e) {
+ // ignore and fall back to global fetch
+ }
+ }
+
+ if (!isResponseLike(res)) {
+ res = await fetch(url, { signal: controller.signal });
+ }
+
+ if (!isResponseLike(res) || !res.ok) {
+ return null;
+ }
+
+ return await res.json();
+ } catch (e) {
+ log('Error fetching JSON', url, e?.message || String(e));
+ return null;
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
+
+function getSiblingRemoteUrl(remoteEntryUrl, filename) {
+ try {
+ const url = new URL(filename, remoteEntryUrl);
+ return url.href;
+ } catch (_e) {
+ return remoteEntryUrl.replace(/\/[^/]+$/, `/${filename}`);
+ }
+}
+
+function getSiblingRemotePath(remoteEntryPath, filename) {
+ return path.join(path.dirname(remoteEntryPath), filename);
+}
+
+/**
+ * Fetch and cache a remote's mf-stats.json
+ */
+async function getMFManifest(remoteUrl, origin) {
+ if (remoteMFManifests.has(remoteUrl)) return remoteMFManifests.get(remoteUrl);
+ try {
+ if (remoteUrl.startsWith('http')) {
+ const candidates = [
+ getSiblingRemoteUrl(remoteUrl, 'mf-manifest.server-stats.json'),
+ getSiblingRemoteUrl(remoteUrl, 'mf-manifest.server.json'),
+ getSiblingRemoteUrl(remoteUrl, 'mf-stats.json'),
+ getSiblingRemoteUrl(remoteUrl, 'mf-manifest.json'),
+ ];
+
+ for (const statsUrl of candidates) {
+ log('Fetching MF manifest from:', statsUrl);
+ const json = await fetchJson(statsUrl, origin);
+ if (!json) continue;
+
+ // Prefer an RSC-layer manifest when available (server runtime plugin).
+ const rsc = json?.rsc || json?.additionalData?.rsc || null;
+ const isRscLayer = rsc?.isRSC === true || rsc?.layer === 'rsc';
+ if (isRscLayer || statsUrl.includes('mf-manifest.server')) {
+ remoteMFManifests.set(remoteUrl, json);
+ return json;
+ }
+ }
+ return null;
+ }
+
+ // File-based remote container; read mf-stats.json from disk (deprecated)
+ const candidates = [
+ getSiblingRemotePath(remoteUrl, 'mf-manifest.server-stats.json'),
+ getSiblingRemotePath(remoteUrl, 'mf-manifest.server.json'),
+ getSiblingRemotePath(remoteUrl, 'mf-stats.json'),
+ getSiblingRemotePath(remoteUrl, 'mf-manifest.json'),
+ ];
+
+ const statsPath = candidates.find((p) => fs.existsSync(p));
+ if (statsPath) {
+ log(
+ 'WARNING: reading federation stats/manifest from disk; prefer HTTP for remotes.',
+ );
+ const json = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
+ remoteMFManifests.set(remoteUrl, json);
+ return json;
+ }
+ log('Federation stats/manifest not found for', remoteUrl);
+ return null;
+ } catch (e) {
+ log('Error fetching federation stats/manifest:', e.message);
+ return null;
+ }
+}
+
+/**
+ * Fetch and cache a remote's mf-stats.json to get RSC config (+additionalData)
+ */
+async function getRemoteRSCConfig(remoteUrl, origin) {
+ if (remoteRSCConfigs.has(remoteUrl)) {
+ return remoteRSCConfigs.get(remoteUrl);
+ }
+
+ try {
+ const stats = await getMFManifest(remoteUrl, origin);
+ const additionalRsc = stats?.additionalData?.rsc || null;
+ let rscConfig = stats?.rsc || additionalRsc || null;
+ if (rscConfig && additionalRsc) {
+ rscConfig = { ...additionalRsc, ...rscConfig };
+ }
+ if (stats?.additionalData && rscConfig) {
+ rscConfig.additionalData = stats.additionalData;
+ }
+ // Avoid permanently caching null/empty config; allow retry (dev startup races).
+ if (rscConfig) {
+ remoteRSCConfigs.set(remoteUrl, rscConfig);
+ }
+ log('Loaded RSC config:', JSON.stringify(rscConfig, null, 2));
+ return rscConfig;
+ } catch (error) {
+ log('Error fetching RSC config:', error.message);
+ return null;
+ }
+}
+
+/**
+ * Fetch and cache a remote's server actions manifest
+ */
+async function getRemoteServerActionsManifest(remoteUrl, origin) {
+ if (remoteServerActionsManifests.has(remoteUrl)) {
+ return remoteServerActionsManifests.get(remoteUrl);
+ }
+
+ try {
+ const rscConfig = await getRemoteRSCConfig(remoteUrl, origin);
+ let manifestUrl =
+ rscConfig?.serverActionsManifest ||
+ rscConfig?.additionalData?.serverActionsManifest ||
+ (rscConfig?.remote?.actionsEndpoint
+ ? rscConfig.remote.actionsEndpoint.replace(
+ /\/react$/,
+ '/react-server-actions-manifest.json',
+ )
+ : null) ||
+ getSiblingRemoteUrl(remoteUrl, 'react-server-actions-manifest.json');
+
+ log('Fetching server actions manifest from:', manifestUrl);
+
+ if (manifestUrl.startsWith('http')) {
+ const manifest = await fetchJson(manifestUrl, origin);
+ if (!manifest) return null;
+ remoteServerActionsManifests.set(remoteUrl, manifest);
+ log(
+ 'Loaded server actions manifest with',
+ Object.keys(manifest).length,
+ 'actions',
+ );
+ return manifest;
+ }
+
+ if (!fs.existsSync(manifestUrl)) {
+ log('Server actions manifest not found at', manifestUrl);
+ return null;
+ }
+ log(
+ 'WARNING: loading server actions manifest from disk; prefer HTTP manifest.',
+ );
+ const manifest = JSON.parse(fs.readFileSync(manifestUrl, 'utf8'));
+ remoteServerActionsManifests.set(remoteUrl, manifest);
+ log(
+ 'Loaded server actions manifest with',
+ Object.keys(manifest).length,
+ 'actions (fs)',
+ );
+ return manifest;
+ } catch (error) {
+ log('Error fetching server actions manifest:', error.message);
+ return null;
+ }
+}
+
+/**
+ * Register server actions from a loaded module
+ */
+function registerServerActionsFromModule(remoteName, exposeModule, manifest) {
+ if (!exposeModule || !manifest) {
+ return 0;
+ }
+
+ let registeredCount = 0;
+
+ try {
+ // Get registerServerReference from react-server-dom-webpack/server
+ // This is available because we're in the RSC layer
+ const {
+ registerServerReference,
+ } = require('react-server-dom-webpack/server');
+
+ for (const [actionId, entry] of Object.entries(manifest)) {
+ if (!entry || !entry.id || !entry.name) {
+ continue;
+ }
+
+ const exportName = entry.name;
+ const fn =
+ exportName === 'default'
+ ? exposeModule.default
+ : exposeModule[exportName];
+
+ if (typeof fn === 'function') {
+ registerServerReference(fn, entry.id, exportName);
+ registeredCount++;
+ log(`Registered action: ${actionId} -> ${exportName}`);
+ }
+ }
+ } catch (error) {
+ log('Error registering server actions:', error.message);
+ }
+
+ return registeredCount;
+}
+
+async function registerRemoteActionsAtInit(
+ remoteInfo,
+ remoteEntryExports,
+ origin,
+) {
+ const remoteName =
+ remoteInfo?.name || remoteInfo?.entryGlobalName || 'remote';
+ const remoteEntry = remoteInfo?.entry;
+ const registrationKey = `${remoteName}:server-actions:init`;
+
+ if (registeredRemotes.has(registrationKey)) {
+ return;
+ }
+ if (registeringRemotes.has(registrationKey)) {
+ return registeringRemotes.get(registrationKey);
+ }
+
+ const work = (async () => {
+ try {
+ if (!remoteEntry) return;
+
+ const rscConfig = await getRemoteRSCConfig(remoteEntry, origin);
+ const exposeTypes =
+ rscConfig?.exposeTypes && typeof rscConfig.exposeTypes === 'object'
+ ? rscConfig.exposeTypes
+ : null;
+ const exposesToRegister = new Set();
+ if (exposeTypes) {
+ for (const [key, type] of Object.entries(exposeTypes)) {
+ if (type === 'server-action' && typeof key === 'string') {
+ exposesToRegister.add(key);
+ }
+ }
+ }
+ if (exposesToRegister.size === 0) {
+ log('No server-action exposes declared for', remoteName);
+ return;
+ }
+
+ const manifest = await getRemoteServerActionsManifest(
+ remoteEntry,
+ origin,
+ );
+ if (!manifest) {
+ log('No server actions manifest during init for', remoteName);
+ return;
+ }
+
+ if (!remoteEntryExports?.get) {
+ log('remoteEntryExports.get is missing for', remoteName);
+ return;
+ }
+
+ for (const exposeKey of exposesToRegister) {
+ const factory = await remoteEntryExports.get(exposeKey);
+ if (!factory) continue;
+ const exposeModule = await factory();
+ const count = registerServerActionsFromModule(
+ remoteName,
+ exposeModule,
+ manifest,
+ );
+ if (count > 0) {
+ registeredRemotes.add(registrationKey);
+ registeredRemotes.add(`${remoteName}:${exposeKey}`);
+ log(
+ `Registered ${count} server actions at init for ${remoteName}:${exposeKey}`,
+ );
+ }
+ }
+ } catch (error) {
+ log('Error registering actions at init for', remoteName, error.message);
+ } finally {
+ registeringRemotes.delete(registrationKey);
+ }
+ })();
+
+ registeringRemotes.set(registrationKey, work);
+ return work;
+}
+
+function rscRuntimePlugin() {
+ return {
+ name: 'rsc-runtime-plugin',
+ version: '1.0.0',
+
+ /**
+ * afterResolve: After a remote module is resolved, we can access remote info
+ */
+ async afterResolve(args) {
+ log(
+ 'afterResolve - id:',
+ args.id,
+ 'expose:',
+ args.expose,
+ 'remote:',
+ args.remote?.name,
+ );
+
+ // Pre-fetch RSC config for this remote if we haven't already
+ if (args.remote?.entry && !remoteRSCConfigs.has(args.remote.entry)) {
+ // Don't await - let it happen in background
+ getRemoteRSCConfig(args.remote.entry, args.origin).catch(() => {});
+ }
+
+ return args;
+ },
+
+ /**
+ * onLoad: When a remote module is loaded, register server actions if applicable
+ *
+ * This is the key hook for Option 2 (MF-native server actions):
+ * - Detect if the loaded module is a server-actions expose
+ * - Fetch the remote's server actions manifest
+ * - Register each action with React's serverActionRegistry
+ */
+ async onLoad(args) {
+ log('onLoad - expose:', args.expose, 'remote:', args.remote?.name);
+
+ // Only process server-actions exposes from remotes
+ if (!args.remote || !args.expose) {
+ return args;
+ }
+
+ const remoteName = args.remote.name;
+ const remoteEntry = args.remote.entry;
+ const exposeKey = args.expose;
+
+ // Check if this is a server-actions module
+ // We can detect this by:
+ // - RSC config from mf-manifest additionalData.rsc.exposeTypes
+ const rscConfig = await getRemoteRSCConfig(remoteEntry, args.origin);
+ const exposeTypes =
+ rscConfig?.exposeTypes && typeof rscConfig.exposeTypes === 'object'
+ ? rscConfig.exposeTypes
+ : null;
+ const isServerActionsExpose =
+ !!exposeTypes && exposeTypes[exposeKey] === 'server-action';
+
+ if (!isServerActionsExpose) {
+ log('Not a server-actions expose, skipping registration');
+ return args;
+ }
+
+ // Skip if already registered
+ const registrationKey = `${remoteName}:${exposeKey}`;
+ if (registeredRemotes.has(registrationKey)) {
+ log('Actions already registered for', registrationKey);
+ return args;
+ }
+
+ log('Detected server-actions expose, attempting registration...');
+
+ // Get the RSC config to validate and get manifest URL
+ // Fetch the server actions manifest
+ const manifest = await getRemoteServerActionsManifest(
+ remoteEntry,
+ args.origin,
+ );
+ if (!manifest) {
+ log('No server actions manifest available for', remoteName);
+ return args;
+ }
+
+ // Get the loaded module
+ const exposeModule = args.exposeModule;
+ if (!exposeModule) {
+ log('No exposeModule available');
+ return args;
+ }
+
+ // Register the server actions
+ const count = registerServerActionsFromModule(
+ remoteName,
+ exposeModule,
+ manifest,
+ );
+
+ if (count > 0) {
+ registeredRemotes.add(registrationKey);
+ log(
+ `Registered ${count} server actions from ${remoteName}:${exposeKey}`,
+ );
+ }
+
+ return args;
+ },
+
+ /**
+ * initContainer: After remote container init, eagerly register server-action exposes
+ * so server actions are available before first request.
+ */
+ async initContainer(args) {
+ log(
+ 'initContainer - remote:',
+ args.remoteInfo?.name,
+ 'entry:',
+ args.remoteInfo?.entry,
+ );
+
+ await registerRemoteActionsAtInit(
+ args.remoteInfo,
+ args.remoteEntryExports,
+ args.origin,
+ );
+
+ return args;
+ },
+ };
+}
+
+// Export for use as runtime plugin
+module.exports = rscRuntimePlugin;
+module.exports.default = rscRuntimePlugin;
+
+// Export utilities for external use (e.g., from api.server.js)
+module.exports.getRemoteRSCConfig = getRemoteRSCConfig;
+module.exports.getRemoteServerActionsManifest = getRemoteServerActionsManifest;
+module.exports.registeredRemotes = registeredRemotes;
diff --git a/apps/rsc-demo/packages/app-shared/scripts/rscSSRRuntimePlugin.js b/apps/rsc-demo/packages/app-shared/scripts/rscSSRRuntimePlugin.js
new file mode 100644
index 00000000000..862f597fc3a
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/scripts/rscSSRRuntimePlugin.js
@@ -0,0 +1,62 @@
+'use strict';
+
+/**
+ * SSR Runtime Plugin
+ *
+ * The host's registry is loaded at runtime by ssr-entry.js from mf-manifest.json.
+ * This plugin merges remote component registries when loadSnapshot is called.
+ */
+
+function rscSSRRuntimePlugin() {
+ let registryInitialized = false;
+
+ function initializeRegistry() {
+ if (registryInitialized) return;
+ registryInitialized = true;
+
+ // Ensure registry exists (ssr-entry.js should have set it already)
+ globalThis.__RSC_SSR_REGISTRY__ = globalThis.__RSC_SSR_REGISTRY__ || {};
+
+ // Also check for preloaded manifest (legacy/manual preload support)
+ if (globalThis.__RSC_SSR_MANIFEST__) {
+ const registry =
+ globalThis.__RSC_SSR_MANIFEST__?.additionalData?.rsc
+ ?.clientComponents ||
+ globalThis.__RSC_SSR_MANIFEST__?.rsc?.clientComponents ||
+ null;
+ if (registry) {
+ Object.assign(globalThis.__RSC_SSR_REGISTRY__, registry);
+ }
+ }
+ }
+
+ function mergeRegistryFrom(manifestJson) {
+ if (!manifestJson) return;
+ const registry =
+ manifestJson?.additionalData?.rsc?.clientComponents ||
+ manifestJson?.rsc?.clientComponents ||
+ null;
+ if (registry) {
+ // Merge remote components into existing registry
+ globalThis.__RSC_SSR_REGISTRY__ = globalThis.__RSC_SSR_REGISTRY__ || {};
+ Object.assign(globalThis.__RSC_SSR_REGISTRY__, registry);
+ }
+ }
+
+ return {
+ name: 'rsc-ssr-runtime-plugin',
+ beforeInit(args) {
+ // Ensure the host registry exists; ssr-worker or server startup preloads it.
+ initializeRegistry();
+ return args;
+ },
+ async loadRemoteSnapshot(args) {
+ // Merge remote components from loaded manifests
+ mergeRegistryFrom(args.manifestJson);
+ return args;
+ },
+ };
+}
+
+module.exports = rscSSRRuntimePlugin;
+module.exports.default = rscSSRRuntimePlugin;
diff --git a/apps/rsc-demo/packages/app-shared/scripts/webpackShared.js b/apps/rsc-demo/packages/app-shared/scripts/webpackShared.js
new file mode 100644
index 00000000000..38a212a224f
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/scripts/webpackShared.js
@@ -0,0 +1,26 @@
+'use strict';
+
+/**
+ * Shared webpack layer and loader configuration for app1/app2.
+ * Keep this minimal to avoid over-abstracting, but centralize
+ * the pieces that must stay identical between host and remote.
+ */
+
+const WEBPACK_LAYERS = {
+ rsc: 'rsc',
+ ssr: 'ssr',
+ client: 'client',
+ shared: 'shared',
+};
+
+const babelLoader = {
+ loader: 'babel-loader',
+ options: {
+ presets: [['@babel/preset-react', { runtime: 'automatic' }]],
+ },
+};
+
+module.exports = {
+ WEBPACK_LAYERS,
+ babelLoader,
+};
diff --git a/apps/rsc-demo/packages/app1/package.json b/apps/rsc-demo/packages/app1/package.json
new file mode 100644
index 00000000000..24642fb800e
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "app1",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "start": "npm run build:dev && npm run server",
+ "start:prod": "cross-env NODE_ENV=production node server/api.server.js",
+ "server": "cross-env NODE_ENV=development node server/api.server.js",
+ "build:dev": "cross-env NODE_ENV=development node scripts/build.js",
+ "build": "cross-env NODE_ENV=production node scripts/build.js",
+ "test": "echo \"(app1 tests run at root)\""
+ },
+ "dependencies": {
+ "@rsc-demo/shared-rsc": "workspace:*",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-server-dom-webpack": "workspace:*",
+ "express": "^4.18.2",
+ "compression": "^1.7.4"
+ },
+ "devDependencies": {
+ "@babel/core": "7.21.3",
+ "@babel/plugin-transform-modules-commonjs": "^7.21.2",
+ "@babel/preset-react": "^7.18.6",
+ "@babel/register": "^7.21.0",
+ "@module-federation/enhanced": "workspace:*",
+ "@module-federation/node": "workspace:*",
+ "babel-loader": "8.3.0",
+ "concurrently": "^7.6.0",
+ "cross-env": "^7.0.3",
+ "html-webpack-plugin": "5.5.0",
+ "rimraf": "^4.4.0",
+ "webpack": "5.76.2"
+ }
+}
diff --git a/apps/rsc-demo/packages/app1/project.json b/apps/rsc-demo/packages/app1/project.json
new file mode 100644
index 00000000000..7d1ce46216b
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/project.json
@@ -0,0 +1,27 @@
+{
+ "name": "rsc-app1",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "apps/rsc-demo/packages/app1/src",
+ "projectType": "application",
+ "tags": ["rsc", "demo"],
+ "targets": {
+ "build": {
+ "executor": "nx:run-commands",
+ "outputs": ["{projectRoot}/build"],
+ "options": {
+ "cwd": "apps/rsc-demo/packages/app1",
+ "command": "pnpm run build"
+ }
+ },
+ "serve": {
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "apps/rsc-demo/packages/app1",
+ "command": "pnpm run start",
+ "env": {
+ "PORT": "4101"
+ }
+ }
+ }
+ }
+}
diff --git a/apps/rsc-demo/packages/app1/public/checkmark.svg b/apps/rsc-demo/packages/app1/public/checkmark.svg
new file mode 100644
index 00000000000..fde2dfbca21
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/checkmark.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/rsc-demo/packages/app1/public/chevron-down.svg b/apps/rsc-demo/packages/app1/public/chevron-down.svg
new file mode 100644
index 00000000000..6222f780b7f
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/chevron-down.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/rsc-demo/packages/app1/public/chevron-up.svg b/apps/rsc-demo/packages/app1/public/chevron-up.svg
new file mode 100644
index 00000000000..fc8c1930933
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/chevron-up.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/rsc-demo/packages/app1/public/cross.svg b/apps/rsc-demo/packages/app1/public/cross.svg
new file mode 100644
index 00000000000..3a108586386
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/cross.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/rsc-demo/packages/app1/public/favicon.ico b/apps/rsc-demo/packages/app1/public/favicon.ico
new file mode 100644
index 00000000000..d80eeb8413f
Binary files /dev/null and b/apps/rsc-demo/packages/app1/public/favicon.ico differ
diff --git a/apps/rsc-demo/packages/app1/public/index.html b/apps/rsc-demo/packages/app1/public/index.html
new file mode 100644
index 00000000000..cb8b14bbe8d
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ React Notes
+
+
+
+
+
+
diff --git a/apps/rsc-demo/packages/app1/public/logo.svg b/apps/rsc-demo/packages/app1/public/logo.svg
new file mode 100644
index 00000000000..ea77a618d94
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/apps/rsc-demo/packages/app1/public/style.css b/apps/rsc-demo/packages/app1/public/style.css
new file mode 100644
index 00000000000..7742845ebf1
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/public/style.css
@@ -0,0 +1,700 @@
+/* -------------------------------- CSSRESET --------------------------------*/
+/* CSS Reset adapted from https://dev.to/hankchizljaw/a-modern-css-reset-6p3 */
+/* Box sizing rules */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* Remove default padding */
+ul[class],
+ol[class] {
+ padding: 0;
+}
+
+/* Remove default margin */
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+ul[class],
+ol[class],
+li,
+figure,
+figcaption,
+blockquote,
+dl,
+dd {
+ margin: 0;
+}
+
+/* Set core body defaults */
+body {
+ min-height: 100vh;
+ scroll-behavior: smooth;
+ text-rendering: optimizeSpeed;
+ line-height: 1.5;
+}
+
+/* Remove list styles on ul, ol elements with a class attribute */
+ul[class],
+ol[class] {
+ list-style: none;
+}
+
+/* A elements that don't have a class get default styles */
+a:not([class]) {
+ text-decoration-skip-ink: auto;
+}
+
+/* Make images easier to work with */
+img {
+ max-width: 100%;
+ display: block;
+}
+
+/* Natural flow and rhythm in articles by default */
+article > * + * {
+ margin-block-start: 1em;
+}
+
+/* Inherit fonts for inputs and buttons */
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+/* Remove all animations and transitions for people that prefer not to see them */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
+/* -------------------------------- /CSSRESET --------------------------------*/
+
+:root {
+ /* Colors */
+ --main-border-color: #ddd;
+ --primary-border: #037dba;
+ --gray-20: #404346;
+ --gray-60: #8a8d91;
+ --gray-70: #bcc0c4;
+ --gray-80: #c9ccd1;
+ --gray-90: #e4e6eb;
+ --gray-95: #f0f2f5;
+ --gray-100: #f5f7fa;
+ --primary-blue: #037dba;
+ --secondary-blue: #0396df;
+ --tertiary-blue: #c6efff;
+ --flash-blue: #4cf7ff;
+ --outline-blue: rgba(4, 164, 244, 0.6);
+ --navy-blue: #035e8c;
+ --red-25: #bd0d2a;
+ --secondary-text: #65676b;
+ --white: #fff;
+ --yellow: #fffae1;
+
+ --outline-box-shadow: 0 0 0 2px var(--outline-blue);
+ --outline-box-shadow-contrast: 0 0 0 2px var(--navy-blue);
+
+ /* Fonts */
+ --sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto,
+ Ubuntu, Helvetica, sans-serif;
+ --monospace: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
+ monospace;
+}
+
+html {
+ font-size: 100%;
+}
+
+body {
+ font-family: var(--sans-serif);
+ background: var(--gray-100);
+ font-weight: 400;
+ line-height: 1.75;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5 {
+ margin: 0;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+h1 {
+ font-size: 3.052rem;
+}
+h2 {
+ font-size: 2.441rem;
+}
+h3 {
+ font-size: 1.953rem;
+}
+h4 {
+ font-size: 1.563rem;
+}
+h5 {
+ font-size: 1.25rem;
+}
+small,
+.text_small {
+ font-size: 0.8rem;
+}
+pre,
+code {
+ font-family: var(--monospace);
+ border-radius: 6px;
+}
+pre {
+ background: var(--gray-95);
+ padding: 12px;
+ line-height: 1.5;
+}
+code {
+ background: var(--yellow);
+ padding: 0 3px;
+ font-size: 0.94rem;
+ word-break: break-word;
+}
+pre code {
+ background: none;
+}
+a {
+ color: var(--primary-blue);
+}
+
+.text-with-markdown h1,
+.text-with-markdown h2,
+.text-with-markdown h3,
+.text-with-markdown h4,
+.text-with-markdown h5 {
+ margin-block: 2rem 0.7rem;
+ margin-inline: 0;
+}
+
+.text-with-markdown blockquote {
+ font-style: italic;
+ color: var(--gray-20);
+ border-left: 3px solid var(--gray-80);
+ padding-left: 10px;
+}
+
+hr {
+ border: 0;
+ height: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+/* ---------------------------------------------------------------------------*/
+.main {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+ overflow: hidden;
+}
+
+.col {
+ height: 100%;
+}
+.col:last-child {
+ flex-grow: 1;
+}
+
+.logo {
+ height: 20px;
+ width: 22px;
+ margin-inline-end: 10px;
+}
+
+.edit-button {
+ border-radius: 100px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ padding: 6px 20px 8px;
+ cursor: pointer;
+ font-weight: 700;
+ outline-style: none;
+}
+.edit-button--solid {
+ background: var(--primary-blue);
+ color: var(--white);
+ border: none;
+ margin-inline-start: 6px;
+ transition: all 0.2s ease-in-out;
+}
+.edit-button--solid:hover {
+ background: var(--secondary-blue);
+}
+.edit-button--solid:focus {
+ box-shadow: var(--outline-box-shadow-contrast);
+}
+.edit-button--outline {
+ background: var(--white);
+ color: var(--primary-blue);
+ border: 1px solid var(--primary-blue);
+ margin-inline-start: 12px;
+ transition: all 0.1s ease-in-out;
+}
+.edit-button--outline:disabled {
+ opacity: 0.5;
+}
+.edit-button--outline:hover:not([disabled]) {
+ background: var(--primary-blue);
+ color: var(--white);
+}
+.edit-button--outline:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+
+ul.notes-list {
+ padding: 16px 0;
+}
+.notes-list > li {
+ padding: 0 16px;
+}
+.notes-empty {
+ padding: 16px;
+}
+
+.sidebar {
+ background: var(--white);
+ box-shadow:
+ 0px 8px 24px rgba(0, 0, 0, 0.1),
+ 0px 2px 2px rgba(0, 0, 0, 0.1);
+ overflow-y: scroll;
+ z-index: 1000;
+ flex-shrink: 0;
+ max-width: 350px;
+ min-width: 250px;
+ width: 30%;
+}
+.sidebar-header {
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+ padding: 36px 16px 16px;
+ display: flex;
+ align-items: center;
+}
+.sidebar-menu {
+ padding: 0 16px 16px;
+ display: flex;
+ justify-content: space-between;
+}
+.sidebar-menu > .search {
+ position: relative;
+ flex-grow: 1;
+}
+.sidebar-note-list-item {
+ position: relative;
+ margin-bottom: 12px;
+ padding: 16px;
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ max-height: 100px;
+ transition: max-height 250ms ease-out;
+ transform: scale(1);
+}
+.sidebar-note-list-item.note-expanded {
+ max-height: 300px;
+ transition: max-height 0.5s ease;
+}
+.sidebar-note-list-item.flash {
+ animation-name: flash;
+ animation-duration: 0.6s;
+}
+
+.sidebar-note-open {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ z-index: 0;
+ border: none;
+ border-radius: 6px;
+ text-align: start;
+ background: var(--gray-95);
+ cursor: pointer;
+ outline-style: none;
+ color: transparent;
+ font-size: 0px;
+}
+.sidebar-note-open:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+.sidebar-note-open:hover {
+ background: var(--gray-90);
+}
+.sidebar-note-header {
+ z-index: 1;
+ max-width: 85%;
+ pointer-events: none;
+}
+.sidebar-note-header > strong {
+ display: block;
+ font-size: 1.25rem;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.sidebar-note-toggle-expand {
+ z-index: 2;
+ border-radius: 50%;
+ height: 24px;
+ border: 1px solid var(--gray-60);
+ cursor: pointer;
+ flex-shrink: 0;
+ visibility: hidden;
+ opacity: 0;
+ cursor: default;
+ transition:
+ visibility 0s linear 20ms,
+ opacity 300ms;
+ outline-style: none;
+}
+.sidebar-note-toggle-expand:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+.sidebar-note-open:hover + .sidebar-note-toggle-expand,
+.sidebar-note-open:focus + .sidebar-note-toggle-expand,
+.sidebar-note-toggle-expand:hover,
+.sidebar-note-toggle-expand:focus {
+ visibility: visible;
+ opacity: 1;
+ transition:
+ visibility 0s linear 0s,
+ opacity 300ms;
+}
+.sidebar-note-toggle-expand img {
+ width: 10px;
+ height: 10px;
+}
+
+.sidebar-note-excerpt {
+ pointer-events: none;
+ z-index: 2;
+ flex: 1 1 250px;
+ color: var(--secondary-text);
+ position: relative;
+ animation: slideIn 100ms;
+}
+
+.search input {
+ padding: 0 16px;
+ border-radius: 100px;
+ border: 1px solid var(--gray-90);
+ width: 100%;
+ height: 100%;
+ outline-style: none;
+}
+.search input:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+.search .spinner {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+}
+
+.note-viewer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.note {
+ background: var(--white);
+ box-shadow:
+ 0px 0px 5px rgba(0, 0, 0, 0.1),
+ 0px 0px 1px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ height: 95%;
+ width: 95%;
+ min-width: 400px;
+ padding: 8%;
+ overflow-y: auto;
+}
+.note--empty-state {
+ margin-inline: 20px 20px;
+}
+.note-text--empty-state {
+ font-size: 1.5rem;
+}
+.note-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap-reverse;
+ margin-inline-start: -12px;
+}
+.note-menu {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-grow: 1;
+}
+.note-title {
+ line-height: 1.3;
+ flex-grow: 1;
+ overflow-wrap: break-word;
+ margin-inline-start: 12px;
+}
+.note-updated-at {
+ color: var(--secondary-text);
+ white-space: nowrap;
+ margin-inline-start: 12px;
+}
+.note-preview {
+ margin-block-start: 50px;
+}
+
+.note-editor {
+ background: var(--white);
+ display: flex;
+ height: 100%;
+ width: 100%;
+ padding: 58px;
+ overflow-y: auto;
+}
+.note-editor .label {
+ margin-bottom: 20px;
+}
+.note-editor-form {
+ display: flex;
+ flex-direction: column;
+ width: 400px;
+ flex-shrink: 0;
+ position: sticky;
+ top: 0;
+}
+.note-editor-form input,
+.note-editor-form textarea {
+ background: none;
+ border: 1px solid var(--gray-70);
+ border-radius: 2px;
+ font-family: var(--monospace);
+ font-size: 0.8rem;
+ padding: 12px;
+ outline-style: none;
+}
+.note-editor-form input:focus,
+.note-editor-form textarea:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+.note-editor-form input {
+ height: 44px;
+ margin-bottom: 16px;
+}
+.note-editor-form textarea {
+ height: 100%;
+ max-width: 400px;
+}
+.note-editor-menu {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ margin-bottom: 12px;
+}
+.note-editor-preview {
+ margin-inline-start: 40px;
+ width: 100%;
+}
+.note-editor-done,
+.note-editor-delete {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 100px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ padding: 6px 20px 8px;
+ cursor: pointer;
+ font-weight: 700;
+ margin-inline-start: 12px;
+ outline-style: none;
+ transition: all 0.2s ease-in-out;
+}
+.note-editor-done:disabled,
+.note-editor-delete:disabled {
+ opacity: 0.5;
+}
+.note-editor-done {
+ border: none;
+ background: var(--primary-blue);
+ color: var(--white);
+}
+.note-editor-done:focus {
+ box-shadow: var(--outline-box-shadow-contrast);
+}
+.note-editor-done:hover:not([disabled]) {
+ background: var(--secondary-blue);
+}
+.note-editor-delete {
+ border: 1px solid var(--red-25);
+ background: var(--white);
+ color: var(--red-25);
+}
+.note-editor-delete:focus {
+ box-shadow: var(--outline-box-shadow);
+}
+.note-editor-delete:hover:not([disabled]) {
+ background: var(--red-25);
+ color: var(--white);
+}
+/* Hack to color our svg */
+.note-editor-delete:hover:not([disabled]) img {
+ filter: grayscale(1) invert(1) brightness(2);
+}
+.note-editor-done > img {
+ width: 14px;
+}
+.note-editor-delete > img {
+ width: 10px;
+}
+.note-editor-done > img,
+.note-editor-delete > img {
+ margin-inline-end: 12px;
+}
+.note-editor-done[disabled],
+.note-editor-delete[disabled] {
+ opacity: 0.5;
+}
+
+.label {
+ display: inline-block;
+ border-radius: 100px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ font-weight: 700;
+ padding: 4px 14px;
+}
+.label--preview {
+ background: rgba(38, 183, 255, 0.15);
+ color: var(--primary-blue);
+}
+
+.text-with-markdown p {
+ margin-bottom: 16px;
+}
+.text-with-markdown img {
+ width: 100%;
+}
+
+/* https://codepen.io/mandelid/pen/vwKoe */
+.spinner {
+ display: inline-block;
+ transition: opacity linear 0.1s 0.2s;
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(80, 80, 80, 0.5);
+ border-radius: 50%;
+ border-top-color: #fff;
+ animation: spin 1s ease-in-out infinite;
+ opacity: 0;
+}
+.spinner--active {
+ opacity: 1;
+}
+
+.skeleton::after {
+ content: 'Loading...';
+}
+.skeleton {
+ height: 100%;
+ background-color: #eee;
+ background-image: linear-gradient(90deg, #eee, #f5f5f5, #eee);
+ background-size: 200px 100%;
+ background-repeat: no-repeat;
+ border-radius: 4px;
+ display: block;
+ line-height: 1;
+ width: 100%;
+ animation: shimmer 1.2s ease-in-out infinite;
+ color: transparent;
+}
+.skeleton:first-of-type {
+ margin: 0;
+}
+.skeleton--button {
+ border-radius: 100px;
+ padding: 6px 20px 8px;
+ width: auto;
+}
+.v-stack + .v-stack {
+ margin-block-start: 0.8em;
+}
+
+.offscreen {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ width: 1px;
+ position: absolute;
+}
+
+/* ---------------------------------------------------------------------------*/
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200px 0;
+ }
+ 100% {
+ background-position: calc(200px + 100%) 0;
+ }
+}
+
+@keyframes slideIn {
+ 0% {
+ top: -10px;
+ opacity: 0;
+ }
+ 100% {
+ top: 0;
+ opacity: 1;
+ }
+}
+
+@keyframes flash {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.05);
+ opacity: 0.9;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
diff --git a/apps/rsc-demo/packages/app1/scripts/build.js b/apps/rsc-demo/packages/app1/scripts/build.js
new file mode 100644
index 00000000000..2260c15282e
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/build.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+const path = require('path');
+const rimraf = require('rimraf');
+const webpack = require('webpack');
+
+// Clean build directory before starting
+rimraf.sync(path.resolve(__dirname, '../build'));
+
+const clientConfig = require('./client.build');
+const serverConfig = require('./server.build');
+const ssrConfig = require('./ssr.build');
+
+function handleStats(err, stats) {
+ if (err) {
+ console.error(err.stack || err);
+ if (err.details) {
+ console.error(err.details);
+ }
+ process.exit(1);
+ }
+ const info = stats.toJson();
+ if (stats.hasErrors()) {
+ console.log('Finished running webpack with errors.');
+ info.errors.forEach((e) => console.error(e));
+ process.exit(1);
+ } else {
+ console.log('Finished running webpack.');
+ }
+}
+
+function runWebpack(config) {
+ return new Promise((resolve) => {
+ const compiler = webpack(config);
+ compiler.run((err, stats) => {
+ handleStats(err, stats);
+ compiler.close(() => resolve(stats));
+ });
+ });
+}
+
+(async () => {
+ await runWebpack([clientConfig, serverConfig]);
+ await runWebpack(ssrConfig);
+})();
diff --git a/apps/rsc-demo/packages/app1/scripts/client.build.js b/apps/rsc-demo/packages/app1/scripts/client.build.js
new file mode 100644
index 00000000000..2df92ab0190
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/client.build.js
@@ -0,0 +1,197 @@
+'use strict';
+
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');
+const {
+ ModuleFederationPlugin,
+} = require('@module-federation/enhanced/webpack');
+const {
+ WEBPACK_LAYERS,
+ babelLoader,
+} = require('../../app-shared/scripts/webpackShared');
+
+const context = path.resolve(__dirname, '..');
+const isProduction = process.env.NODE_ENV === 'production';
+
+/**
+ * Client bundle configuration
+ *
+ * Uses webpack layers for proper code separation:
+ * - 'use server' modules → createServerReference() calls (tree-shaken)
+ * - 'use client' modules → actual component code (bundled)
+ * - Server components → excluded from client bundle
+ */
+const clientConfig = {
+ context,
+ mode: isProduction ? 'production' : 'development',
+ devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
+ entry: {
+ main: {
+ import: path.resolve(__dirname, '../src/framework/bootstrap.js'),
+ layer: WEBPACK_LAYERS.client, // Entry point is in client layer
+ },
+ },
+ output: {
+ path: path.resolve(__dirname, '../build'),
+ filename: '[name].js',
+ publicPath: 'auto',
+ },
+ optimization: {
+ minimize: false,
+ chunkIds: 'named',
+ moduleIds: 'named',
+ },
+ // Enable webpack layers (stable feature)
+ experiments: {
+ layers: true,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ // Exclude node_modules EXCEPT our workspace packages
+ exclude: (modulePath) => {
+ // Include shared-components (workspace package)
+ if (
+ modulePath.includes('shared-components') ||
+ modulePath.includes('shared-rsc')
+ )
+ return false;
+ // Exclude other node_modules
+ return /node_modules/.test(modulePath);
+ },
+ // Use oneOf for layer-based loader selection
+ oneOf: [
+ // RSC layer: Server Components
+ // Transforms 'use client' → client reference proxies
+ // Transforms 'use server' → registerServerReference
+ {
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ layer: WEBPACK_LAYERS.rsc,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-server-loader',
+ ),
+ },
+ ],
+ },
+ // SSR layer: Server-Side Rendering
+ // Transforms 'use server' → error stubs (can't call actions during SSR)
+ // Passes through 'use client' (renders actual components)
+ {
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ layer: WEBPACK_LAYERS.ssr,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-ssr-loader',
+ ),
+ },
+ ],
+ },
+ // Client/Browser layer (default)
+ // Transforms 'use server' → createServerReference() stubs
+ // Passes through 'use client' (actual component code)
+ {
+ layer: WEBPACK_LAYERS.client,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-client-loader',
+ ),
+ },
+ ],
+ },
+ ],
+ },
+ // CSS handling (if needed)
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ inject: true,
+ template: path.resolve(__dirname, '../public/index.html'),
+ }),
+ // Generate client manifest for 'use client' components
+ new ReactServerWebpackPlugin({ isServer: false }),
+ // Enable Module Federation for the client bundle (app1 as a host).
+ // This runs in the client layer, so we use a dedicated 'client' shareScope
+ // and mark shares as client-layer React/DOM.
+ new ModuleFederationPlugin({
+ name: 'app1',
+ filename: 'remoteEntry.client.js',
+ runtime: false,
+ // Consume app2's federated modules (Button, DemoCounterButton)
+ remotes: {
+ app2: 'app2@http://localhost:4102/remoteEntry.client.js',
+ },
+ experiments: {
+ asyncStartup: true,
+ },
+ shared: {
+ react: {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.client,
+ issuerLayer: WEBPACK_LAYERS.client,
+ },
+ 'react-dom': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.client,
+ issuerLayer: WEBPACK_LAYERS.client,
+ },
+ '@rsc-demo/shared-rsc': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.client,
+ issuerLayer: WEBPACK_LAYERS.client,
+ },
+ 'shared-components': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.client,
+ issuerLayer: WEBPACK_LAYERS.client,
+ },
+ },
+ // Initialize default + client scopes; this share lives in 'client'.
+ shareScope: ['default', 'client'],
+ shareStrategy: 'version-first',
+ /**
+ * Attach RSC-aware metadata to mf-stats/mf-manifest so SSR can resolve
+ * client references without app-level copy/paste logic.
+ */
+ manifest: {
+ rsc: {
+ layer: 'client',
+ shareScope: 'client',
+ isRSC: false,
+ },
+ },
+ }),
+ ],
+ resolve: {
+ // Condition names for proper module resolution per layer
+ // Client bundle uses browser conditions
+ conditionNames: ['browser', 'import', 'require', 'default'],
+ },
+};
+
+module.exports = clientConfig;
diff --git a/apps/rsc-demo/packages/app1/scripts/init_db.sh b/apps/rsc-demo/packages/app1/scripts/init_db.sh
new file mode 100755
index 00000000000..b6e1a2f69cc
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/init_db.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -e
+
+psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
+ DROP TABLE IF EXISTS notes;
+ CREATE TABLE notes (
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ title TEXT,
+ body TEXT
+ );
+EOSQL
diff --git a/apps/rsc-demo/packages/app1/scripts/seed.js b/apps/rsc-demo/packages/app1/scripts/seed.js
new file mode 100644
index 00000000000..cfa9deb09a9
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/seed.js
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+const { readdir, unlink, writeFile, mkdir } = require('fs/promises');
+const startOfYear = require('date-fns/startOfYear');
+
+const credentials = {
+ host: process.env.DB_HOST || 'localhost',
+ database: process.env.DB_NAME || 'notesapi',
+ user: process.env.DB_USER || 'notesadmin',
+ password: process.env.DB_PASSWORD || 'password',
+ port: Number(process.env.DB_PORT || 5432),
+};
+
+const NOTES_PATH = './notes';
+const pool = new Pool(credentials);
+
+const now = new Date();
+const startOfThisYear = startOfYear(now);
+// Thanks, https://stackoverflow.com/a/9035732
+function randomDateBetween(start, end) {
+ return new Date(
+ start.getTime() + Math.random() * (end.getTime() - start.getTime()),
+ );
+}
+
+const dropTableStatement = 'DROP TABLE IF EXISTS notes;';
+const createTableStatement = `CREATE TABLE notes (
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ title TEXT,
+ body TEXT
+);`;
+const insertNoteStatement = `INSERT INTO notes(title, body, created_at, updated_at)
+ VALUES ($1, $2, $3, $3)
+ RETURNING *`;
+const seedData = [
+ [
+ 'Meeting Notes',
+ 'This is an example note. It contains **Markdown**!',
+ randomDateBetween(startOfThisYear, now),
+ ],
+ [
+ 'Make a thing',
+ `It's very easy to make some words **bold** and other words *italic* with
+Markdown. You can even [link to React's website!](https://www.reactjs.org).`,
+ randomDateBetween(startOfThisYear, now),
+ ],
+ [
+ 'A note with a very long title because sometimes you need more words',
+ `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing)
+notes in this app! These note live on the server in the \`notes\` folder.
+
+`,
+ randomDateBetween(startOfThisYear, now),
+ ],
+ ['I wrote this note today', 'It was an excellent note.', now],
+];
+
+async function seed() {
+ await pool.query(dropTableStatement);
+ await pool.query(createTableStatement);
+ const res = await Promise.all(
+ seedData.map((row) => pool.query(insertNoteStatement, row)),
+ );
+
+ await mkdir(path.resolve(NOTES_PATH), { recursive: true });
+ const oldNotes = await readdir(path.resolve(NOTES_PATH));
+ await Promise.all(
+ oldNotes
+ .filter((filename) => filename.endsWith('.md'))
+ .map((filename) => unlink(path.resolve(NOTES_PATH, filename))),
+ );
+
+ await Promise.all(
+ res.map(({ rows }) => {
+ const id = rows[0].id;
+ const content = rows[0].body;
+ const data = new Uint8Array(Buffer.from(content));
+ return writeFile(path.resolve(NOTES_PATH, `${id}.md`), data);
+ }),
+ );
+}
+
+seed();
diff --git a/apps/rsc-demo/packages/app1/scripts/server.build.js b/apps/rsc-demo/packages/app1/scripts/server.build.js
new file mode 100644
index 00000000000..531aaa3fe64
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/server.build.js
@@ -0,0 +1,290 @@
+'use strict';
+
+const path = require('path');
+const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');
+const {
+ ModuleFederationPlugin,
+} = require('@module-federation/enhanced/webpack');
+const {
+ WEBPACK_LAYERS,
+ babelLoader,
+} = require('../../app-shared/scripts/webpackShared');
+
+// React 19 exports don't expose these subpaths via "exports", so resolve by file path
+const reactPkgRoot = path.dirname(require.resolve('react/package.json'));
+const reactServerEntry = path.join(reactPkgRoot, 'react.react-server.js');
+const reactJSXServerEntry = path.join(
+ reactPkgRoot,
+ 'jsx-runtime.react-server.js',
+);
+const reactJSXDevServerEntry = path.join(
+ reactPkgRoot,
+ 'jsx-dev-runtime.react-server.js',
+);
+const rsdwServerPath = path.resolve(
+ require.resolve('react-server-dom-webpack/package.json'),
+ '..',
+ 'server.node.js',
+);
+const rsdwServerUnbundledPath = require.resolve(
+ 'react-server-dom-webpack/server.node.unbundled',
+);
+
+// Allow overriding remote location; default to HTTP for local dev server.
+const app2RemoteUrl =
+ process.env.APP2_REMOTE_URL ||
+ 'http://localhost:4102/mf-manifest.server.json';
+
+const context = path.resolve(__dirname, '..');
+const isProduction = process.env.NODE_ENV === 'production';
+
+/**
+ * Server bundle configuration (for RSC rendering)
+ *
+ * This builds the RSC server entry with resolve.conditionNames: ['react-server', ...]
+ * which means React packages resolve to their server versions at BUILD time.
+ * No --conditions=react-server flag needed at runtime!
+ */
+const serverConfig = {
+ context,
+ mode: isProduction ? 'production' : 'development',
+ devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
+ target: 'async-node',
+ entry: {
+ server: {
+ // Bundle server-entry.js which exports ReactApp and rendering utilities
+ import: path.resolve(__dirname, '../src/server-entry.js'),
+ layer: WEBPACK_LAYERS.rsc, // Entry point is in RSC layer
+ },
+ },
+ output: {
+ path: path.resolve(__dirname, '../build'),
+ filename: '[name].rsc.js',
+ libraryTarget: 'commonjs2',
+ // Allow Node federation runtime to fetch chunks over HTTP (needed for remote entry)
+ publicPath: 'auto',
+ },
+ optimization: {
+ minimize: false,
+ chunkIds: 'named',
+ moduleIds: 'named',
+ },
+ experiments: {
+ layers: true,
+ },
+ module: {
+ rules: [
+ // Allow imports without .js extension in ESM modules (only for workspace packages)
+ {
+ test: /\.m?js$/,
+ include: (modulePath) => {
+ return (
+ modulePath.includes('shared-components') ||
+ modulePath.includes('shared-rsc')
+ );
+ },
+ resolve: { fullySpecified: false },
+ },
+ {
+ test: /\.js$/,
+ // Exclude node_modules EXCEPT our workspace packages
+ exclude: (modulePath) => {
+ if (
+ modulePath.includes('shared-components') ||
+ modulePath.includes('shared-rsc')
+ )
+ return false;
+ return /node_modules/.test(modulePath);
+ },
+ oneOf: [
+ // RSC layer for server bundle
+ {
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ layer: WEBPACK_LAYERS.rsc,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-server-loader',
+ ),
+ },
+ ],
+ },
+ // Default to RSC layer for server bundle
+ {
+ layer: WEBPACK_LAYERS.rsc,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-server-loader',
+ ),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ plugins: [
+ // Generate server actions manifest for local 'use server' modules.
+ // Remote actions are registered at runtime via rscRuntimePlugin using the
+ // remote's published manifest URL (mf-stats additionalData).
+ new ReactServerWebpackPlugin({
+ isServer: true,
+ }),
+ // Enable Module Federation for the RSC server bundle (app1 as a Node host).
+ // This is the RSC layer, so we use a dedicated 'rsc' shareScope and
+ // mark React/RSDW as rsc-layer shares.
+ //
+ // SERVER-SIDE FEDERATION: app1 consumes app2's RSC container for:
+ // - Server components (rendered in app1's RSC stream)
+ // - Client component references (serialized as $L client refs)
+ //
+ // MF-native server actions (default):
+ // - app2 exposes './server-actions' and publishes its server actions manifest URL
+ // via mf-stats additionalData.rsc.
+ // - rscRuntimePlugin loads that manifest and registers server references into the
+ // shared serverActionRegistry when the remote action module is loaded via MF.
+ // - app1's Express action handler triggers that registration on-demand before
+ // resolving the action ID (fallback: HTTP forwarding).
+ new ModuleFederationPlugin({
+ name: 'app1',
+ filename: 'remoteEntry.server.js',
+ runtime: false,
+ // Consume app2's RSC container via manifest.json over HTTP
+ remotes: {
+ app2: `app2@${app2RemoteUrl}`,
+ },
+ remoteType: 'script',
+ experiments: {
+ asyncStartup: true,
+ },
+ // Use a server-specific manifest name so we don't clobber the client mf-stats.json
+ // (which now carries the clientComponents registry for SSR).
+ manifest: {
+ fileName: 'mf-manifest.server',
+ },
+ runtimePlugins: [
+ require.resolve('@module-federation/node/runtimePlugin'),
+ require.resolve('../../app-shared/scripts/rscRuntimePlugin.js'),
+ ],
+ shared: {
+ react: {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ import: reactServerEntry,
+ shareKey: 'react',
+ allowNodeModulesSuffixMatch: true,
+ },
+ 'react-dom': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ allowNodeModulesSuffixMatch: true,
+ },
+ 'react/jsx-runtime': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ import: reactJSXServerEntry,
+ shareKey: 'react/jsx-runtime',
+ allowNodeModulesSuffixMatch: true,
+ },
+ 'react/jsx-dev-runtime': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ import: reactJSXDevServerEntry,
+ shareKey: 'react/jsx-dev-runtime',
+ allowNodeModulesSuffixMatch: true,
+ },
+ 'react-server-dom-webpack': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ 'react-server-dom-webpack/server': {
+ // Match require('react-server-dom-webpack/server') if any code uses it
+ import: rsdwServerPath,
+ eager: false,
+ requiredVersion: false,
+ singleton: true,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ 'react-server-dom-webpack/server.node': {
+ // The rsc-server-loader emits require('react-server-dom-webpack/server.node')
+ // This resolves it to the correct server writer (no --conditions flag needed)
+ import: rsdwServerPath,
+ eager: false,
+ requiredVersion: false,
+ singleton: true,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ 'react-server-dom-webpack/server.node.unbundled': {
+ import: rsdwServerUnbundledPath,
+ eager: false,
+ requiredVersion: false,
+ singleton: true,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ '@rsc-demo/shared-rsc': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ 'shared-components': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ },
+ // Initialize only the RSC share scope for server bundle to force react-server shares.
+ shareScope: ['rsc'],
+ shareStrategy: 'version-first',
+ }),
+ ],
+ resolve: {
+ // Server uses react-server condition for proper RSC module resolution
+ conditionNames: ['react-server', 'node', 'import', 'require', 'default'],
+ alias: {
+ // CRITICAL: Force all imports of react-server-dom-webpack/server.node to use our
+ // patched wrapper that exposes getServerAction and the shared serverActionRegistry.
+ // Without this alias, the MF share scope may provide the unpatched npm package version,
+ // causing server actions to register to a different registry than the one used by
+ // getServerAction() at runtime.
+ 'react-server-dom-webpack/server.node': rsdwServerPath,
+ 'react-server-dom-webpack/server': rsdwServerPath,
+ },
+ },
+};
+
+module.exports = serverConfig;
diff --git a/apps/rsc-demo/packages/app1/scripts/ssr.build.js b/apps/rsc-demo/packages/app1/scripts/ssr.build.js
new file mode 100644
index 00000000000..ef7b8639e0c
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/ssr.build.js
@@ -0,0 +1,174 @@
+'use strict';
+
+const path = require('path');
+const webpack = require('webpack');
+const {
+ ModuleFederationPlugin,
+} = require('@module-federation/enhanced/webpack');
+const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');
+const {
+ WEBPACK_LAYERS,
+ babelLoader,
+} = require('../../app-shared/scripts/webpackShared');
+const AutoIncludeClientComponentsPlugin = require('../../app-shared/scripts/AutoIncludeClientComponentsPlugin');
+
+const context = path.resolve(__dirname, '..');
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+/**
+ * SSR bundle configuration (for server-side rendering of client components)
+ * This builds client components for Node.js execution during SSR
+ */
+const ssrConfig = {
+ context,
+ mode: isProduction ? 'production' : 'development',
+ devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
+ target: 'async-node',
+ node: {
+ // Use real __dirname so ssr-entry.js can find mf-manifest.json at runtime
+ __dirname: false,
+ },
+ entry: {
+ ssr: {
+ import: path.resolve(__dirname, '../src/framework/ssr-entry.js'),
+ layer: WEBPACK_LAYERS.ssr, // Entry point is in SSR layer
+ },
+ },
+ output: {
+ path: path.resolve(__dirname, '../build'),
+ filename: '[name].js',
+ libraryTarget: 'commonjs2',
+ publicPath: 'auto',
+ },
+ optimization: {
+ minimize: false,
+ chunkIds: 'named',
+ moduleIds: 'named',
+ // Preserve 'default' export names so React SSR can resolve client components
+ mangleExports: false,
+ // Disable module concatenation so client components have individual module IDs
+ // This is required for SSR to resolve client component references from the flight stream
+ concatenateModules: false,
+ },
+ experiments: {
+ layers: true,
+ },
+ module: {
+ rules: [
+ // Allow imports without .js extension in ESM modules (only for workspace packages)
+ {
+ test: /\.m?js$/,
+ include: (modulePath) => {
+ return (
+ modulePath.includes('shared-components') ||
+ modulePath.includes('shared-rsc')
+ );
+ },
+ resolve: { fullySpecified: false },
+ },
+ {
+ test: /\.js$/,
+ // Exclude node_modules EXCEPT our workspace packages
+ exclude: (modulePath) => {
+ if (
+ modulePath.includes('shared-components') ||
+ modulePath.includes('shared-rsc')
+ )
+ return false;
+ return /node_modules/.test(modulePath);
+ },
+ oneOf: [
+ // SSR layer: transforms 'use server' to stubs, keeps client components
+ {
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ layer: WEBPACK_LAYERS.ssr,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-ssr-loader',
+ ),
+ },
+ ],
+ },
+ // Default to SSR layer for SSR bundle
+ {
+ layer: WEBPACK_LAYERS.ssr,
+ use: [
+ babelLoader,
+ {
+ loader: require.resolve(
+ 'react-server-dom-webpack/rsc-ssr-loader',
+ ),
+ },
+ ],
+ },
+ ],
+ },
+ // CSS handling (if needed)
+ {
+ test: /\.css$/,
+ use: ['null-loader'], // Ignore CSS in SSR bundle
+ },
+ ],
+ },
+ plugins: [
+ // Generate SSR manifest for client component resolution during SSR
+ new ReactServerWebpackPlugin({
+ isServer: true,
+ ssrManifestFilename: 'react-ssr-manifest.json',
+ // Use a different filename to avoid overwriting the RSC manifest
+ // SSR doesn't need server actions (they're stubs that throw errors)
+ serverActionsManifestFilename: 'react-ssr-server-actions.json',
+ }),
+ // Lightweight federation runtime to run SSR runtime plugins (no exposes needed)
+ new ModuleFederationPlugin({
+ name: 'app1-ssr',
+ filename: 'remoteEntry.ssr.js',
+ runtime: false,
+ manifest: {
+ fileName: 'mf-manifest.ssr',
+ rsc: {
+ layer: 'ssr',
+ shareScope: 'client',
+ },
+ },
+ remotes: {
+ app2: 'app2@http://localhost:4102/remoteEntry.client.js',
+ },
+ experiments: { asyncStartup: true },
+ runtimePlugins: [
+ require.resolve('@module-federation/node/runtimePlugin'),
+ require.resolve('../../app-shared/scripts/rscSSRRuntimePlugin.js'),
+ ],
+ shared: {
+ react: {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.ssr,
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ },
+ 'react-dom': {
+ singleton: true,
+ eager: false,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.ssr,
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ },
+ },
+ shareScope: ['client'],
+ shareStrategy: 'version-first',
+ }),
+ new AutoIncludeClientComponentsPlugin(),
+ ],
+ resolve: {
+ // SSR uses node conditions
+ conditionNames: ['node', 'import', 'require', 'default'],
+ },
+};
+
+module.exports = ssrConfig;
diff --git a/apps/rsc-demo/packages/app1/server/api.server.js b/apps/rsc-demo/packages/app1/server/api.server.js
new file mode 100644
index 00000000000..9122059a170
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/server/api.server.js
@@ -0,0 +1,763 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+/**
+ * Express Server for RSC Application
+ *
+ * This server uses BUNDLED RSC code from webpack.
+ * The webpack build uses resolve.conditionNames: ['react-server', ...]
+ * to resolve React packages at BUILD time.
+ *
+ * NO --conditions=react-server flag needed at runtime!
+ */
+
+const express = require('express');
+const compress = require('compression');
+const Busboy = require('busboy');
+const { readFileSync, existsSync } = require('fs');
+const { unlink, writeFile, mkdir } = require('fs').promises;
+const { spawn } = require('child_process');
+const { PassThrough } = require('stream');
+const path = require('path');
+const React = require('react');
+
+// RSC Action header (similar to Next.js's 'Next-Action')
+const RSC_ACTION_HEADER = 'rsc-action';
+// Debug headers for E2E assertions about action execution path.
+const RSC_FEDERATION_ACTION_MODE_HEADER = 'x-federation-action-mode';
+const RSC_FEDERATION_ACTION_REMOTE_HEADER = 'x-federation-action-remote';
+
+// Host app runs on 4101 by default (tests assume this)
+const PORT = process.env.PORT || 4101;
+// Used by server components to resolve same-origin API fetches.
+if (!process.env.RSC_API_ORIGIN) {
+ process.env.RSC_API_ORIGIN = `http://localhost:${PORT}`;
+}
+
+// Remote app configuration for federated server actions (Option 1 - HTTP forwarding fallback)
+// Action IDs prefixed with 'remote:app2:' or containing 'app2/' are forwarded to app2
+const REMOTE_APP_CONFIG = {
+ app2: {
+ url: process.env.APP2_URL || 'http://localhost:4102',
+ // Patterns to match action IDs that belong to app2
+ patterns: [
+ /^remote:app2:/, // Explicit prefix
+ /app2\/src\//, // File path contains app2
+ /packages\/app2\//, // Full package path
+ ],
+ },
+};
+
+/**
+ * Check if an action ID belongs to a remote app and compute the ID that the
+ * remote server should see.
+ *
+ * For example, an ID like `remote:app2:file:///...#increment` should be
+ * forwarded as `file:///...#increment` so it matches the remote manifest keys.
+ *
+ * @param {string} actionId - The (possibly prefixed) server action ID
+ * @returns {{ app: string, config: object, forwardedId: string } | null}
+ *
+ * This routing is used for:
+ * - HTTP forwarding fallback (Option 1) when an action is not registered in-process.
+ * - Debug headers in the host response when a remote is identified.
+ *
+ * MF-native registration happens by loading the remote action module via Module
+ * Federation (see app1/src/server-entry.js). The rscRuntimePlugin then fetches the
+ * remote's server actions manifest URL from mf-stats additionalData.rsc and calls
+ * registerServerReference(...) to populate the shared serverActionRegistry.
+ */
+function getRemoteAppForAction(actionId) {
+ for (const [app, config] of Object.entries(REMOTE_APP_CONFIG)) {
+ for (const pattern of config.patterns) {
+ if (pattern.test(actionId)) {
+ // Strip explicit remote prefix if present so the remote sees the
+ // original manifest ID (e.g. file:///...#name).
+ let forwardedId = actionId;
+ const prefix = `remote:${app}:`;
+ if (forwardedId.startsWith(prefix)) {
+ forwardedId = forwardedId.slice(prefix.length);
+ }
+ return { app, config, forwardedId };
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Forward a server action request to a remote app (Option 1)
+ * Proxies the full request/response to preserve RSC Flight protocol
+ */
+async function forwardActionToRemote(
+ req,
+ res,
+ forwardedActionId,
+ remoteName,
+ remoteConfig,
+) {
+ const targetUrl = `${remoteConfig.url}/react${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`;
+
+ // Log federation forwarding (use %s to avoid format string injection)
+ console.log(
+ '[Federation] Forwarding action %s to %s',
+ forwardedActionId,
+ targetUrl,
+ );
+
+ res.set(RSC_FEDERATION_ACTION_MODE_HEADER, 'proxy');
+ if (remoteName) {
+ res.set(RSC_FEDERATION_ACTION_REMOTE_HEADER, remoteName);
+ }
+
+ // Collect request body
+ const bodyChunks = [];
+ req.on('data', (chunk) => bodyChunks.push(chunk));
+
+ await new Promise((resolve, reject) => {
+ req.on('end', resolve);
+ req.on('error', reject);
+ });
+
+ const bodyBuffer = Buffer.concat(bodyChunks);
+
+ // Start from original headers so we preserve cookies/auth/etc.
+ const headers = { ...req.headers };
+
+ // Never forward host/header values directly; let fetch set Host.
+ delete headers.host;
+ delete headers.connection;
+ delete headers['content-length'];
+
+ // Force the action header to the ID the remote expects.
+ headers[RSC_ACTION_HEADER] = forwardedActionId;
+
+ // Ensure content-type is present if we have a body.
+ if (
+ bodyBuffer.length &&
+ !headers['content-type'] &&
+ !headers['Content-Type']
+ ) {
+ headers['content-type'] = 'application/octet-stream';
+ }
+
+ // Forward to remote app
+ const response = await fetch(targetUrl, {
+ method: 'POST',
+ headers,
+ body: bodyBuffer,
+ });
+
+ // Copy response headers (with null check for headers object)
+ if (response.headers && typeof response.headers.entries === 'function') {
+ for (const [key, value] of response.headers.entries()) {
+ // Skip some headers that shouldn't be forwarded
+ if (
+ !['content-encoding', 'transfer-encoding', 'connection'].includes(
+ key.toLowerCase(),
+ )
+ ) {
+ res.set(key, value);
+ }
+ }
+ }
+
+ res.status(response.status);
+
+ // Get full response body and write it (more reliable than streaming with getReader)
+ // This works better with test frameworks like supertest
+ const body = await response.text();
+ if (body) {
+ res.write(body);
+ }
+ res.end();
+}
+
+// Database will be loaded from bundled RSC server
+// This is lazy-loaded to allow the bundle to be loaded first
+let pool = null;
+const app = express();
+
+app.use(compress());
+const buildDir = path.resolve(__dirname, '../build');
+app.use(express.static(buildDir, { index: false }));
+app.use('/build', express.static(buildDir));
+app.use(express.static(path.resolve(__dirname, '../public'), { index: false }));
+
+// Lazy-load the bundled RSC server code
+// This is built by webpack with react-server condition resolved at build time
+// With asyncStartup: true, the require returns a promise that resolves to the module
+let rscServerPromise = null;
+let rscServerResolved = null;
+let remoteActionsInitPromise = null;
+
+async function getRSCServer() {
+ if (rscServerResolved) {
+ return rscServerResolved;
+ }
+ if (!rscServerPromise) {
+ const bundlePath = path.resolve(__dirname, '../build/server.rsc.js');
+ if (!existsSync(bundlePath)) {
+ throw new Error(
+ 'RSC server bundle not found. Run `pnpm build` first.\n' +
+ 'The server bundle is built with webpack and includes React with react-server exports.',
+ );
+ }
+ const mod = require(bundlePath);
+ // With asyncStartup, the module might be a promise or have async init
+ rscServerPromise = Promise.resolve(mod).then((resolved) => {
+ rscServerResolved = resolved;
+ return resolved;
+ });
+ }
+ return rscServerPromise;
+}
+
+async function ensureRemoteActionsRegistered(server) {
+ // Option 2: In-process MF-native federated actions.
+ // If the RSC server exposes registerRemoteApp2Actions, call it once to
+ // register remote actions into the shared serverActionRegistry. We guard
+ // with a promise so multiple /react requests don't re-register.
+ if (!server || typeof server.registerRemoteApp2Actions !== 'function') {
+ return;
+ }
+ if (!remoteActionsInitPromise) {
+ remoteActionsInitPromise = Promise.resolve().then(async () => {
+ try {
+ await server.registerRemoteApp2Actions();
+ } catch (error) {
+ console.error(
+ '[Federation] Failed to register remote actions via Module Federation:',
+ error,
+ );
+ // Allow a future attempt if registration fails.
+ remoteActionsInitPromise = null;
+ }
+ });
+ }
+ return remoteActionsInitPromise;
+}
+
+async function getPool() {
+ if (!pool) {
+ const server = await getRSCServer();
+ pool = server.pool;
+ }
+ return pool;
+}
+
+if (!process.env.RSC_TEST_MODE) {
+ app
+ .listen(PORT, () => {
+ console.log(`React Notes listening at ${PORT}...`);
+ console.log('Using bundled RSC server (no --conditions flag needed)');
+ })
+ .on('error', function (error) {
+ if (error.syscall !== 'listen') {
+ throw error;
+ }
+ const isPipe = (portOrPipe) => Number.isNaN(portOrPipe);
+ const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;
+ switch (error.code) {
+ case 'EACCES':
+ console.error(bind + ' requires elevated privileges');
+ process.exit(1);
+ break;
+ case 'EADDRINUSE':
+ console.error(bind + ' is already in use');
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+ });
+}
+
+function handleErrors(fn) {
+ return async function (req, res, next) {
+ try {
+ return await fn(req, res);
+ } catch (x) {
+ next(x);
+ }
+ };
+}
+
+async function readRequestBody(req) {
+ if (req.body && typeof req.body === 'string') {
+ return req.body;
+ }
+ if (req.body && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
+ return JSON.stringify(req.body);
+ }
+ return new Promise((resolve, reject) => {
+ const chunks = [];
+ req.on('data', (c) => chunks.push(c));
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
+ req.on('error', reject);
+ });
+}
+
+/**
+ * Render RSC to a buffer (flight stream)
+ * Uses the bundled RSC server code (webpack-built with react-server condition)
+ */
+async function renderRSCToBuffer(props) {
+ const manifest = readFileSync(
+ path.resolve(__dirname, '../build/react-client-manifest.json'),
+ 'utf8',
+ );
+ const moduleMap = JSON.parse(manifest);
+
+ // Use bundled RSC server (await for asyncStartup)
+ const server = await getRSCServer();
+
+ return new Promise((resolve, reject) => {
+ const chunks = [];
+ const passThrough = new PassThrough();
+ passThrough.on('data', (chunk) => chunks.push(chunk));
+ passThrough.on('end', () => resolve(Buffer.concat(chunks)));
+ passThrough.on('error', reject);
+
+ const { pipe } = server.renderApp(props, moduleMap);
+ pipe(passThrough);
+ });
+}
+
+/**
+ * Render RSC flight stream to HTML using SSR worker
+ * The SSR worker uses the bundled SSR code (webpack-built without react-server condition)
+ */
+function renderSSR(rscBuffer) {
+ return new Promise((resolve, reject) => {
+ const workerPath = path.resolve(__dirname, './ssr-worker.js');
+ const ssrWorker = spawn('node', [workerPath], {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ // SSR worker must NOT run with react-server condition; strip NODE_OPTIONS.
+ env: { ...process.env, NODE_OPTIONS: '' },
+ });
+
+ const chunks = [];
+ ssrWorker.stdout.on('data', (chunk) => chunks.push(chunk));
+ ssrWorker.stdout.on('end', () =>
+ resolve(Buffer.concat(chunks).toString('utf8')),
+ );
+
+ ssrWorker.stderr.on('data', (data) => {
+ console.error('SSR Worker stderr:', data.toString());
+ });
+
+ ssrWorker.on('error', reject);
+ ssrWorker.on('close', (code) => {
+ if (code !== 0 && chunks.length === 0) {
+ reject(new Error(`SSR worker exited with code ${code}`));
+ }
+ });
+
+ // Send RSC flight data to worker
+ ssrWorker.stdin.write(rscBuffer);
+ ssrWorker.stdin.end();
+ });
+}
+
+app.get(
+ '/',
+ handleErrors(async function (_req, res) {
+ await waitForWebpack();
+
+ const props = {
+ selectedId: null,
+ isEditing: false,
+ searchText: '',
+ };
+
+ // Check if SSR bundle exists
+ const ssrBundlePath = path.resolve(__dirname, '../build/ssr.js');
+ if (!existsSync(ssrBundlePath)) {
+ // Fallback to shell if SSR bundle not built
+ const html = readFileSync(
+ path.resolve(__dirname, '../build/index.html'),
+ 'utf8',
+ );
+ res.send(html);
+ return;
+ }
+
+ try {
+ // Step 1: Render RSC to flight stream (using bundled RSC server)
+ const rscBuffer = await renderRSCToBuffer(props);
+
+ // Step 2: Render flight stream to HTML using SSR worker (using bundled SSR code)
+ const ssrHtml = await renderSSR(rscBuffer);
+
+ // Step 3: Inject SSR HTML into the shell template
+ const shellHtml = readFileSync(
+ path.resolve(__dirname, '../build/index.html'),
+ 'utf8',
+ );
+
+ // Embed the RSC flight data for hydration
+ const rscDataScript = ``;
+
+ // Replace the empty root div with SSR content + RSC data
+ const finalHtml = shellHtml.replace(
+ '',
+ `
${ssrHtml}
${rscDataScript}`,
+ );
+
+ res.send(finalHtml);
+ } catch (error) {
+ console.error('SSR Error, falling back to shell:', error);
+ // Fallback to shell rendering on error
+ const html = readFileSync(
+ path.resolve(__dirname, '../build/index.html'),
+ 'utf8',
+ );
+ res.send(html);
+ }
+ }),
+);
+
+async function renderReactTree(res, props) {
+ await waitForWebpack();
+ const manifest = readFileSync(
+ path.resolve(__dirname, '../build/react-client-manifest.json'),
+ 'utf8',
+ );
+ const moduleMap = JSON.parse(manifest);
+
+ // Use bundled RSC server (await for asyncStartup)
+ const server = await getRSCServer();
+ const { pipe } = server.renderApp(props, moduleMap);
+ pipe(res);
+}
+
+function sendResponse(req, res, redirectToId) {
+ const location = JSON.parse(req.query.location);
+ if (redirectToId) {
+ location.selectedId = redirectToId;
+ }
+ res.set('X-Location', JSON.stringify(location));
+ renderReactTree(res, {
+ selectedId: location.selectedId,
+ isEditing: location.isEditing,
+ searchText: location.searchText,
+ });
+}
+
+app.get('/react', function (req, res) {
+ sendResponse(req, res, null);
+});
+
+// Server Actions endpoint - spec-compliant implementation
+// Uses RSC-Action header to identify action (like Next.js's Next-Action)
+//
+// FEDERATED ACTIONS:
+// - Option 2 (preferred): In-process MF-native actions. Remote 'use server'
+// modules from app2 are imported via Module Federation in server-entry.js
+// and registered into the shared serverActionRegistry. getServerAction(id)
+// returns a callable function that runs in this process.
+// - Option 1 (fallback): HTTP forwarding. If an action ID matches a remote
+// app pattern but is not registered via MF, the request is forwarded to
+// that app's /react endpoint and the response is proxied back.
+app.post(
+ '/react',
+ handleErrors(async function (req, res) {
+ const actionId = req.get(RSC_ACTION_HEADER);
+
+ if (!actionId) {
+ res.status(400).send('Missing RSC-Action header');
+ return;
+ }
+
+ await waitForWebpack();
+
+ // Get the bundled RSC server (await for asyncStartup)
+ const server = await getRSCServer();
+
+ // Option 2 (default): if the action isn't already registered locally,
+ // attempt MF-native remote registration and retry lookup. This avoids
+ // relying on action-id pattern heuristics for correctness.
+ let actionFn = server.getServerAction(actionId);
+ if (typeof actionFn !== 'function') {
+ await ensureRemoteActionsRegistered(server);
+ actionFn = server.getServerAction(actionId);
+ }
+
+ // Load server actions manifest from build
+ const manifestPath = path.resolve(
+ __dirname,
+ '../build/react-server-actions-manifest.json',
+ );
+ let serverActionsManifest = {};
+ if (existsSync(manifestPath)) {
+ serverActionsManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
+ }
+
+ // Merge dynamic inline actions registered at runtime
+ const dynamicManifest = server.getDynamicServerActionsManifest() || {};
+ serverActionsManifest = Object.assign(
+ {},
+ serverActionsManifest,
+ dynamicManifest,
+ );
+
+ const actionEntry = serverActionsManifest[actionId];
+
+ // Load and execute the action
+ // First check the global registry (for inline server actions registered at runtime)
+ // Then fall back to module exports (for file-level 'use server' from manifest)
+ const remoteApp = getRemoteAppForAction(actionId);
+
+ // If MF-native registration did not provide a function, fall back to
+ // Option 1 (HTTP forwarding) for known remote actions.
+ if (!actionFn) {
+ if (remoteApp) {
+ // Use %s to avoid format string injection
+ console.log(
+ '[Federation] Action %s belongs to %s, no MF-registered handler found, forwarding via HTTP...',
+ actionId,
+ remoteApp.app,
+ );
+ await forwardActionToRemote(
+ req,
+ res,
+ remoteApp.forwardedId,
+ remoteApp.app,
+ remoteApp.config,
+ );
+ return;
+ }
+ }
+
+ if (!actionFn && actionEntry) {
+ // For bundled server actions, they should be in the registry
+ // File-level actions are also bundled into server.rsc.js
+ // Use %s to avoid format string injection
+ console.warn(
+ 'Action %s not in registry, manifest entry:',
+ actionId,
+ actionEntry,
+ );
+ }
+
+ if (typeof actionFn !== 'function') {
+ res
+ .status(404)
+ .send(
+ `Server action "${actionId}" not found. ` +
+ `Ensure the action module is imported in server-entry.js.`,
+ );
+ return;
+ }
+
+ // Decode the action arguments using React's Flight Reply protocol
+ const contentType = req.headers['content-type'] || '';
+ let args;
+ if (contentType.startsWith('multipart/form-data')) {
+ const busboy = new Busboy({ headers: req.headers });
+ const pending = server.decodeReplyFromBusboy(
+ busboy,
+ serverActionsManifest,
+ );
+ req.pipe(busboy);
+ args = await pending;
+ } else {
+ const body = await readRequestBody(req);
+ args = await server.decodeReply(body, serverActionsManifest);
+ }
+
+ // Execute the server action
+ const result = await actionFn(...(Array.isArray(args) ? args : [args]));
+
+ // Return the result as RSC Flight stream
+ res.set('Content-Type', 'text/x-component');
+ if (remoteApp) {
+ res.set(RSC_FEDERATION_ACTION_MODE_HEADER, 'mf');
+ res.set(RSC_FEDERATION_ACTION_REMOTE_HEADER, remoteApp.app);
+ }
+
+ // For now, re-render the app tree with the action result
+ const location = req.query.location
+ ? JSON.parse(req.query.location)
+ : {
+ selectedId: null,
+ isEditing: false,
+ searchText: '',
+ };
+
+ // Include action result in response header for client consumption
+ if (result !== undefined) {
+ res.set('X-Action-Result', JSON.stringify(result));
+ }
+
+ renderReactTree(res, {
+ selectedId: location.selectedId,
+ isEditing: location.isEditing,
+ searchText: location.searchText,
+ });
+ }),
+);
+
+const NOTES_PATH = path.resolve(__dirname, '../notes');
+
+async function ensureNotesDir() {
+ await mkdir(NOTES_PATH, { recursive: true });
+}
+
+async function safeUnlink(filePath) {
+ try {
+ await unlink(filePath);
+ } catch (error) {
+ if (error && error.code === 'ENOENT') return;
+ throw error;
+ }
+}
+
+app.post(
+ '/notes',
+ express.json(),
+ handleErrors(async function (req, res) {
+ const now = new Date();
+ const pool = await getPool();
+ const result = await pool.query(
+ 'insert into notes (title, body, created_at, updated_at) values ($1, $2, $3, $3) returning id',
+ [req.body.title, req.body.body, now],
+ );
+ const insertedId = result.rows[0].id;
+ await ensureNotesDir();
+ await writeFile(
+ path.resolve(NOTES_PATH, `${insertedId}.md`),
+ req.body.body,
+ 'utf8',
+ );
+ sendResponse(req, res, insertedId);
+ }),
+);
+
+app.put(
+ '/notes/:id',
+ express.json(),
+ handleErrors(async function (req, res) {
+ const now = new Date();
+ const updatedId = Number(req.params.id);
+ // Validate ID is a positive integer to prevent path traversal
+ if (!Number.isInteger(updatedId) || updatedId <= 0) {
+ res.status(400).send('Invalid note ID');
+ return;
+ }
+ const pool = await getPool();
+ await pool.query(
+ 'update notes set title = $1, body = $2, updated_at = $3 where id = $4',
+ [req.body.title, req.body.body, now, updatedId],
+ );
+ await ensureNotesDir();
+ await writeFile(
+ path.resolve(NOTES_PATH, `${updatedId}.md`),
+ req.body.body,
+ 'utf8',
+ );
+ sendResponse(req, res, null);
+ }),
+);
+
+app.delete(
+ '/notes/:id',
+ handleErrors(async function (req, res) {
+ const noteId = Number(req.params.id);
+ // Validate ID is a positive integer to prevent path traversal
+ if (!Number.isInteger(noteId) || noteId <= 0) {
+ res.status(400).send('Invalid note ID');
+ return;
+ }
+ const pool = await getPool();
+ await pool.query('delete from notes where id = $1', [noteId]);
+ await safeUnlink(path.resolve(NOTES_PATH, `${noteId}.md`));
+ sendResponse(req, res, null);
+ }),
+);
+
+app.get(
+ '/notes',
+ handleErrors(async function (_req, res) {
+ const pool = await getPool();
+ const { rows } = await pool.query('select * from notes order by id desc');
+ res.json(rows);
+ }),
+);
+
+app.get(
+ '/notes/:id',
+ handleErrors(async function (req, res) {
+ const noteId = Number(req.params.id);
+ // Validate ID is a positive integer
+ if (!Number.isInteger(noteId) || noteId <= 0) {
+ res.status(400).send('Invalid note ID');
+ return;
+ }
+ const pool = await getPool();
+ const { rows } = await pool.query('select * from notes where id = $1', [
+ noteId,
+ ]);
+ res.json(rows[0]);
+ }),
+);
+
+app.get('/sleep/:ms', function (req, res) {
+ // Use allowlist of fixed durations to prevent resource exhaustion (CodeQL security)
+ // This avoids user-controlled timer values entirely
+ const ALLOWED_SLEEP_MS = [0, 100, 500, 1000, 2000, 5000, 10000];
+ const requested = parseInt(req.params.ms, 10);
+ // Find the closest allowed value that doesn't exceed the request
+ const sleepMs = ALLOWED_SLEEP_MS.reduce((closest, allowed) => {
+ if (allowed <= requested && allowed > closest) return allowed;
+ return closest;
+ }, 0);
+ setTimeout(() => {
+ res.json({ ok: true, actualSleep: sleepMs });
+ }, sleepMs);
+});
+
+app.use(express.static('build', { index: false }));
+app.use(express.static('public', { index: false }));
+
+async function waitForWebpack() {
+ const requiredFiles = [
+ path.resolve(__dirname, '../build/index.html'),
+ path.resolve(__dirname, '../build/server.rsc.js'),
+ path.resolve(__dirname, '../build/react-client-manifest.json'),
+ ];
+
+ // In test mode we don't want to loop forever; just assert once.
+ const isTest = !!process.env.RSC_TEST_MODE;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const missing = requiredFiles.filter((file) => !existsSync(file));
+ if (missing.length === 0) {
+ return;
+ }
+
+ const msg =
+ 'Could not find webpack build output: ' +
+ missing.map((f) => path.basename(f)).join(', ') +
+ '. Will retry in a second...';
+ console.log(msg);
+
+ if (isTest) {
+ // In tests, fail fast instead of looping forever.
+ throw new Error(msg);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+}
+
+module.exports = app;
diff --git a/apps/rsc-demo/packages/app1/server/package.json b/apps/rsc-demo/packages/app1/server/package.json
new file mode 100644
index 00000000000..cd4d70b9771
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/server/package.json
@@ -0,0 +1,4 @@
+{
+ "type": "commonjs",
+ "main": "./api.server.js"
+}
diff --git a/apps/rsc-demo/packages/app1/server/ssr-worker.js b/apps/rsc-demo/packages/app1/server/ssr-worker.js
new file mode 100644
index 00000000000..8d8dbb3045b
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/server/ssr-worker.js
@@ -0,0 +1,109 @@
+/**
+ * SSR Worker (app1)
+ *
+ * This worker renders RSC flight streams to HTML using react-dom/server.
+ * It must run WITHOUT --conditions=react-server to access react-dom/server.
+ */
+
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+
+function buildRegistryFromSSRManifest(manifestPath) {
+ try {
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ const moduleMap = manifest?.moduleMap || {};
+ const registry = {};
+ for (const [moduleId, exportsMap] of Object.entries(moduleMap)) {
+ const ssrRequest = moduleId.replace(/^\(client\)/, '(ssr)');
+ registry[moduleId] = {
+ moduleId,
+ request: ssrRequest,
+ ssrRequest,
+ chunks: [],
+ exports: Object.keys(exportsMap),
+ filePath:
+ (exportsMap['*'] || Object.values(exportsMap)[0])?.specifier?.replace(
+ /^file:\/\//,
+ '',
+ ) || undefined,
+ };
+ }
+ return Object.keys(registry).length ? registry : null;
+ } catch (_e) {
+ return null;
+ }
+}
+
+function buildRegistryFromMFManifest(manifestPath) {
+ try {
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ const reg =
+ manifest?.additionalData?.rsc?.clientComponents ||
+ manifest?.rsc?.clientComponents ||
+ null;
+ if (!reg) return null;
+ // Normalize: ensure request is set (ssrRequest preferred)
+ const out = {};
+ for (const [id, entry] of Object.entries(reg)) {
+ out[id] = {
+ ...entry,
+ request:
+ entry.ssrRequest ||
+ entry.request ||
+ id.replace(/^\(client\)/, '(ssr)'),
+ };
+ }
+ return out;
+ } catch (_e) {
+ return null;
+ }
+}
+
+// Preload RSC registry for SSR resolver (prefer SSR manifest for correct ids)
+(() => {
+ const baseDir = path.resolve(__dirname, '../build');
+ const mfSSR = path.join(baseDir, 'mf-manifest.ssr.json');
+ const ssrManifest = path.join(baseDir, 'react-ssr-manifest.json');
+ const mfClient = path.join(baseDir, 'mf-manifest.json');
+
+ let registry = null;
+ if (fs.existsSync(mfSSR)) registry = buildRegistryFromMFManifest(mfSSR);
+ if (!registry && fs.existsSync(ssrManifest))
+ registry = buildRegistryFromSSRManifest(ssrManifest);
+ if (!registry && fs.existsSync(mfClient))
+ registry = buildRegistryFromMFManifest(mfClient);
+
+ if (registry) {
+ globalThis.__RSC_SSR_REGISTRY__ = registry;
+ }
+})();
+
+const ssrBundlePromise = Promise.resolve(require('../build/ssr.js'));
+const clientManifest = require('../build/react-client-manifest.json');
+
+async function renderSSR() {
+ const chunks = [];
+
+ process.stdin.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ process.stdin.on('end', async () => {
+ try {
+ const flightData = Buffer.concat(chunks);
+ const ssrBundle = await ssrBundlePromise;
+ const html = await ssrBundle.renderFlightToHTML(
+ flightData,
+ clientManifest,
+ );
+ process.stdout.write(html);
+ } catch (error) {
+ console.error('SSR Worker Error:', error);
+ process.exit(1);
+ }
+ });
+}
+
+renderSSR();
diff --git a/apps/rsc-demo/packages/app1/src/App.js b/apps/rsc-demo/packages/app1/src/App.js
new file mode 100644
index 00000000000..88a6ce9c22d
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/App.js
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { Suspense } from 'react';
+
+import Note from './Note';
+import NoteList from './NoteList';
+import EditButton from './EditButton';
+import SearchField from './SearchField';
+import NoteSkeleton from './NoteSkeleton';
+import NoteListSkeleton from './NoteListSkeleton';
+import DemoCounter from './DemoCounter.server';
+import InlineActionDemo from './InlineActionDemo.server';
+import SharedDemo from './SharedDemo.server';
+import FederatedDemo from './FederatedDemo.server';
+import RemoteButton from './RemoteButton';
+import FederatedActionDemo from './FederatedActionDemo';
+
+export default function App({ selectedId, isEditing, searchText }) {
+ return (
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/FederatedDemo.server.js b/apps/rsc-demo/packages/app1/src/FederatedDemo.server.js
new file mode 100644
index 00000000000..31c44890a98
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/FederatedDemo.server.js
@@ -0,0 +1,101 @@
+/**
+ * FederatedDemo.server.js - Server Component that imports federated modules from app2
+ *
+ * This demonstrates SERVER-SIDE Module Federation:
+ * - app1's RSC server imports components from app2's MF container (remoteEntry.server.js)
+ * - The imported components render server-side in app1's RSC stream
+ * - React/RSDW are shared via 'rsc' shareScope (singleton)
+ *
+ * For 'use client' components from app2:
+ * - They serialize to client references ($L) in the RSC payload
+ * - The actual component code is loaded by app1's client via client-side federation
+ *
+ * For server components from app2:
+ * - They execute in app1's RSC server and render their output inline
+ *
+ * For server actions from app2:
+ * - Default: MF-native (in-process). app2's action module is loaded via MF and
+ * actions are registered into the shared serverActionRegistry.
+ * - Fallback: HTTP forwarding when MF-native action lookup/registration fails.
+ */
+
+import React from 'react';
+
+/**
+ * FederatedDemo.server.js - Server Component demonstrating server-side federation concepts
+ *
+ * IMPORTANT: Server-side federation of 'use client' components requires additional work:
+ * - The RSC server needs to serialize 'use client' components as client references ($L)
+ * - The client manifest (react-client-manifest.json) must include the remote component
+ * - Currently, app1's manifest only knows about app1's components, not app2's
+ *
+ * For full server-side federation of 'use client' components, we would need to:
+ * 1. Merge app2's client manifest into app1's at build time, OR
+ * 2. Have app1's RSC server dynamically load and merge app2's client manifest
+ *
+ * For now, this component demonstrates the CONCEPT of server-side federation
+ * without actually importing 'use client' components from app2.
+ *
+ * What DOES work for server-side federation:
+ * - Pure server components from app2 (no 'use client' directive)
+ * - Server actions: MF-native (fallback: HTTP)
+ * - The FederatedActionDemo client component handles client-side federation
+ *
+ * TODO (Option 2 - Deep MF Integration):
+ * To fully support server-side federation of 'use client' components:
+ * 1. Modify webpack build to merge remote client manifests
+ * 2. Ensure action IDs from remotes are included in host manifest
+ * 3. Changes needed in repo root packages/react-server-dom-webpack/:
+ * - plugin to merge remote manifests
+ * - loader to handle remote client references
+ */
+export default async function FederatedDemo() {
+ const { default: RemoteServerWidget } = await import(
+ 'app2/RemoteServerWidget'
+ );
+
+ return (
+
+
+ Server-Side Federation Demo
+
+
+ This server component demonstrates the architecture for server-side MF.
+
+
+ Current Status:
+
+
Server components: Ready (pure RSC from remotes)
+
Client components: Via client-side MF (see RemoteButton)
+
Server actions: MF-native (fallback: HTTP)
+
+
+
+ Full 'use client' federation requires manifest merging (TODO)
+
This demonstrates server actions used from a Server Component.
+
Current message count: {snapshot.count}
+
+ {snapshot.messages.map((msg, i) => (
+
{msg}
+ ))}
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/Note.js b/apps/rsc-demo/packages/app1/src/Note.js
new file mode 100644
index 00000000000..e3de7ad8848
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/Note.js
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { format } from 'date-fns';
+
+// Uncomment if you want to read from a file instead.
+// import {readFile} from 'fs/promises';
+// import {resolve} from 'path';
+
+import NotePreview from './NotePreview';
+import EditButton from './EditButton';
+import NoteEditor from './NoteEditor';
+
+export default async function Note({ selectedId, isEditing }) {
+ if (selectedId === null) {
+ if (isEditing) {
+ return (
+
+ );
+ } else {
+ return (
+
+
+ Click a note on the left to view something! 🥺
+
+
+ );
+ }
+ }
+
+ const apiOrigin =
+ process.env.RSC_API_ORIGIN ||
+ `http://localhost:${process.env.PORT || 4101}`;
+ const noteResponse = await fetch(`${apiOrigin}/notes/${selectedId}`);
+ const note = await noteResponse.json();
+
+ let { id, title, body, updated_at } = note;
+ const updatedAt = new Date(updated_at);
+
+ // We could also read from a file instead.
+ // body = await readFile(resolve(`./notes/${note.id}.md`), 'utf8');
+
+ // Now let's see how the Suspense boundary above lets us not block on this.
+ // await fetch('http://localhost:4000/sleep/3000');
+
+ if (isEditing) {
+ return ;
+ } else {
+ return (
+
+
+
{title}
+
+
+ Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")}
+
+ Edit
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/NoteList.js b/apps/rsc-demo/packages/app1/src/NoteList.js
new file mode 100644
index 00000000000..160aa8f8318
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/NoteList.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { db } from './db';
+import SidebarNote from './SidebarNote';
+
+export default async function NoteList({ searchText }) {
+ // const notes = await (await fetch('http://localhost:4000/notes')).json();
+
+ // WARNING: This is for demo purposes only.
+ // We don't encourage this in real apps. There are far safer ways to access
+ // data in a real application!
+ const notes = (
+ await db.query(
+ `select * from notes where title ilike $1 order by id desc`,
+ ['%' + searchText + '%'],
+ )
+ ).rows;
+
+ // Now let's see how the Suspense boundary above lets us not block on this.
+ // await fetch('http://localhost:4000/sleep/3000');
+
+ return notes.length > 0 ? (
+
+ {notes.map((note) => (
+
+
+
+ ))}
+
+ ) : (
+
+ {searchText
+ ? `Couldn't find any notes titled "${searchText}".`
+ : 'No notes created yet!'}{' '}
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/NoteListSkeleton.js b/apps/rsc-demo/packages/app1/src/NoteListSkeleton.js
new file mode 100644
index 00000000000..5d638e549a1
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/NoteListSkeleton.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export default function NoteListSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/NotePreview.js b/apps/rsc-demo/packages/app1/src/NotePreview.js
new file mode 100644
index 00000000000..a7133508e1e
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/NotePreview.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import TextWithMarkdown from './TextWithMarkdown';
+
+export default function NotePreview({ body }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app1/src/NoteSkeleton.js b/apps/rsc-demo/packages/app1/src/NoteSkeleton.js
new file mode 100644
index 00000000000..77a4129e0a7
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/NoteSkeleton.js
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export default function NoteSkeleton({ isEditing }) {
+ return isEditing ? : ;
+}
+
+function NoteEditorSkeleton() {
+ return (
+
This demonstrates server actions used from a Server Component.
+
Current message count: {snapshot.count}
+
+ {snapshot.messages.map((msg, i) => (
+
{msg}
+ ))}
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app2/src/Note.js b/apps/rsc-demo/packages/app2/src/Note.js
new file mode 100644
index 00000000000..a44d8c68ee3
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/Note.js
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { format } from 'date-fns';
+
+// Uncomment if you want to read from a file instead.
+// import {readFile} from 'fs/promises';
+// import {resolve} from 'path';
+
+import NotePreview from './NotePreview';
+import EditButton from './EditButton';
+import NoteEditor from './NoteEditor';
+
+export default async function Note({ selectedId, isEditing }) {
+ if (selectedId === null) {
+ if (isEditing) {
+ return (
+
+ );
+ } else {
+ return (
+
+
+ Click a note on the left to view something! 🥺
+
+
+ );
+ }
+ }
+
+ const apiOrigin =
+ process.env.RSC_API_ORIGIN ||
+ `http://localhost:${process.env.PORT || 4102}`;
+ const noteResponse = await fetch(`${apiOrigin}/notes/${selectedId}`);
+ const note = await noteResponse.json();
+
+ let { id, title, body, updated_at } = note;
+ const updatedAt = new Date(updated_at);
+
+ // We could also read from a file instead.
+ // body = await readFile(resolve(`./notes/${note.id}.md`), 'utf8');
+
+ // Now let's see how the Suspense boundary above lets us not block on this.
+ // await fetch('http://localhost:4000/sleep/3000');
+
+ if (isEditing) {
+ return ;
+ } else {
+ return (
+
+
+
{title}
+
+
+ Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")}
+
+ Edit
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app2/src/NoteList.js b/apps/rsc-demo/packages/app2/src/NoteList.js
new file mode 100644
index 00000000000..160aa8f8318
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/NoteList.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { db } from './db';
+import SidebarNote from './SidebarNote';
+
+export default async function NoteList({ searchText }) {
+ // const notes = await (await fetch('http://localhost:4000/notes')).json();
+
+ // WARNING: This is for demo purposes only.
+ // We don't encourage this in real apps. There are far safer ways to access
+ // data in a real application!
+ const notes = (
+ await db.query(
+ `select * from notes where title ilike $1 order by id desc`,
+ ['%' + searchText + '%'],
+ )
+ ).rows;
+
+ // Now let's see how the Suspense boundary above lets us not block on this.
+ // await fetch('http://localhost:4000/sleep/3000');
+
+ return notes.length > 0 ? (
+
+ {notes.map((note) => (
+
+
+
+ ))}
+
+ ) : (
+
+ {searchText
+ ? `Couldn't find any notes titled "${searchText}".`
+ : 'No notes created yet!'}{' '}
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app2/src/NoteListSkeleton.js b/apps/rsc-demo/packages/app2/src/NoteListSkeleton.js
new file mode 100644
index 00000000000..5d638e549a1
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/NoteListSkeleton.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export default function NoteListSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app2/src/NotePreview.js b/apps/rsc-demo/packages/app2/src/NotePreview.js
new file mode 100644
index 00000000000..a7133508e1e
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/NotePreview.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import TextWithMarkdown from './TextWithMarkdown';
+
+export default function NotePreview({ body }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/rsc-demo/packages/app2/src/NoteSkeleton.js b/apps/rsc-demo/packages/app2/src/NoteSkeleton.js
new file mode 100644
index 00000000000..77a4129e0a7
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/NoteSkeleton.js
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export default function NoteSkeleton({ isEditing }) {
+ return isEditing ? : ;
+}
+
+function NoteEditorSkeleton() {
+ return (
+