diff --git a/Option2Plan.md b/Option2Plan.md
new file mode 100644
index 00000000000..50f50c2f654
--- /dev/null
+++ b/Option2Plan.md
@@ -0,0 +1,58 @@
+# Option 2 (MF-Native RSC Actions) – Execution Plan
+
+Goal: execute remote “use server” modules in-process via Module Federation, without HTTP forwarding, while keeping HTTP as a safe fallback. This must work for RSC, SSR, client hydration, shared packages, and layered share scopes.
+
+## Outcomes
+- Host can `getServerAction(id)` for remote actions loaded via MF.
+- No filesystem coupling; remotes discovered over HTTP via MF manifest data.
+- Layer-aware sharing preserved (rsc vs client).
+- Tests cover MF-native path plus fallback to HTTP.
+- Works when remotes are deployed separately (no shared filesystem) and when the remote HTTP server is offline (MF container already loaded).
+
+## Workstream A – Remote publishes the right metadata
+1) **MF manifest extraData**: ensure remote (e.g., app2) publishes in `manifest.additionalData`:
+ - `rsc.serverActionsManifest`, `rsc.clientManifest`, `rsc.remote.url`, `rsc.remote.serverContainer`, `rsc.remote.actionsEndpoint`.
+ - `rsc.exposeTypes` marking `./server-actions` as `server-action` (and other exposes as `client-component`, `server-component`, etc.).
+ - (Optional) `rsc.layers` map so the host knows which shareScope each expose expects.
+2) **Public paths**: remoteEntry.server.js, mf-stats.json, manifests all served over HTTP (no fs paths).
+3) **Share scopes**: keep `shareScope: 'rsc'` for server layer, `'client'` for browser; include `react`, `react/jsx-runtime`, `react/jsx-dev-runtime`, `react-server-dom-webpack/server`.
+
+## Workstream B – Host runtime plugin wiring
+1) **Runtime plugin** (rscRuntimePlugin):
+ - On remote load, read MF manifest extraData (or mf-stats fallback).
+ - Fetch `serverActionsManifest` URL; cache by URL.
+ - For `server-actions` exposes, call `registerServerReference(fn, id, exportName)` for every manifest entry belonging to that remote.
+ - Use `resolveShare` / `loadShare` hooks to map to the correct shareScope based on issuer layer (rsc vs client), avoiding alias hacks.
+ - Emit structured logs (DEBUG) for: manifest fetch, module load, registration counts, and failures; surface a non-fatal warning when MF load fails so HTTP fallback remains available.
+2) **Host RSC entry**: ensure host imports remote `server-actions` so MF loads the container in the RSC layer (or trigger via lazy load during first request).
+3) **Option ordering** in `/react` handler:
+ - Try `getServerAction(id)` (populated by runtime plugin).
+ - If missing and remote match → HTTP forward (Option 1) as fallback.
+ - Add a small counter/metric for MF hits vs HTTP fallbacks.
+
+## Workstream C – Loader & plugin alignment
+1) **rsc-server-loader**: when processing MF-imported modules, preserve the canonical module URL from the manifest (do not rewrite to host paths) when calling `registerServerReference`.
+2) **react-server-dom-webpack plugin**: ensure manifest generation includes MF-imported actions (buildInfo flags already set; just respect provided module URLs).
+3) **No build-time manifest merge** in the host; everything discovered at runtime via MF data.
+4) **Client manifest for SSR (optional stretch)**: allow runtime plugin to fetch `clientManifest` from remote and merge into SSR worker’s manifest if we later SSR remote client islands.
+
+## Workstream D – Tests
+1) Unit: assert runtime plugin fetches extraData URLs, registers actions, and populates `getServerAction` without HTTP.
+2) Integration (node --test): start host only, stop remote HTTP server, verify remote actions still execute (pure MF). Then start remote server to confirm HTTP fallback still works when MF load fails.
+3) E2E: browser flows unchanged; add a case where host uses MF-native action and HTTP server is down.
+4) Negative tests: malformed extraData URL, missing manifest, incompatible shareScope, and mismatched React version in remote (should log and fall back).
+5) Performance check: compare latency of MF-native vs HTTP forward; assert no extra network calls for manifest after first fetch (cache hit).
+
+## Rollout Steps
+1) Update remote MF config to emit extraData URLs over HTTP.
+2) Update rscRuntimePlugin to prefer extraData, cache by manifest URL, and use loadShare for layer-aware React sharing.
+3) Ensure host server-entry imports remote server-actions (or lazy load before first action).
+4) Remove any residual build-time merges; rely solely on runtime discovery.
+5) Add tests (unit + integration + e2e) for MF-native path and fallback behavior.
+6) Add observability: lightweight counters for MF registrations, MF load failures, HTTP fallbacks; optionally expose in logs or a diagnostics endpoint.
+
+## Open Questions
+- MF runtime “factory is not a function” on Node: diagnose container format vs runtime plugin; may need `@module-federation/node` runtimePlugin ordering or `library.type: 'commonjs-module'` alignment.
+- Should remote also publish RSC client manifest URL for host-side SSR of remote client components? If yes, extend runtime plugin to fetch/merge client manifests for SSR worker.
+- Do we need per-remote auth headers/cookies when fetching manifests over HTTP? If so, allow a configurable fetch hook in the runtime plugin.
+- How to handle version skew of `react-server-dom-webpack` between host and remote? Consider validating major/minor in extraData and warning/falling back to HTTP.
diff --git a/apps/rsc-demo/.gitignore b/apps/rsc-demo/.gitignore
new file mode 100644
index 00000000000..26972fcd86b
--- /dev/null
+++ b/apps/rsc-demo/.gitignore
@@ -0,0 +1,41 @@
+# 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
+notes/*.md
+!notes/RESEARCH.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/.nvmrc b/apps/rsc-demo/.nvmrc
new file mode 100644
index 00000000000..3c5535cf60a
--- /dev/null
+++ b/apps/rsc-demo/.nvmrc
@@ -0,0 +1 @@
+18.19.1
diff --git a/apps/rsc-demo/.prettierignore b/apps/rsc-demo/.prettierignore
new file mode 100644
index 00000000000..fea8f052ae5
--- /dev/null
+++ b/apps/rsc-demo/.prettierignore
@@ -0,0 +1,27 @@
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.DS_Store
+.eslintcache
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+*.html
+*.json
+*.md
diff --git a/apps/rsc-demo/.prettierrc.js b/apps/rsc-demo/.prettierrc.js
new file mode 100644
index 00000000000..205c7ef5f08
--- /dev/null
+++ b/apps/rsc-demo/.prettierrc.js
@@ -0,0 +1,18 @@
+/**
+ * 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';
+
+module.exports = {
+ arrowParens: 'always',
+ bracketSpacing: false,
+ singleQuote: true,
+ jsxBracketSameLine: true,
+ trailingComma: 'es5',
+ printWidth: 80,
+};
diff --git a/apps/rsc-demo/AGENTS.md b/apps/rsc-demo/AGENTS.md
new file mode 100644
index 00000000000..7121b4ae7aa
--- /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 `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/ARCHITECTURE_PROPOSAL.md b/apps/rsc-demo/ARCHITECTURE_PROPOSAL.md
new file mode 100644
index 00000000000..47490fe67fc
--- /dev/null
+++ b/apps/rsc-demo/ARCHITECTURE_PROPOSAL.md
@@ -0,0 +1,622 @@
+# Module Federation Native RSC Architecture
+
+## Executive Summary
+
+This document proposes a unified architecture that leverages Module Federation's runtime, manifest system, and plugin hooks to solve RSC's SSR module resolution challenges without manual component maps or HTTP-based server action forwarding.
+
+---
+
+## Core Design Principles
+
+1. **Single Source of Truth**: Extend `mf-manifest.json` to include RSC metadata
+2. **Runtime Plugin Resolution**: Use MF runtime plugins to resolve client components during SSR
+3. **In-Process Server Actions**: Load remote server actions via MF instead of HTTP forwarding
+4. **Automatic Discovery**: No manual componentMap - derive from manifest at runtime
+5. **Shared React Singleton**: Leverage MF share scope for React version consistency
+
+---
+
+## Architecture Components
+
+### 1. Extended Manifest Schema
+
+Extend `mf-manifest.json` via the `additionalData` hook to include RSC-specific data:
+
+```typescript
+// New manifest structure
+interface RSCManifest extends Manifest {
+ rsc: {
+ // Which layer this manifest represents
+ layer: 'client' | 'rsc' | 'ssr';
+
+ // Client component registry (replaces react-client-manifest.json)
+ clientComponents: {
+ [exposePath: string]: {
+ // Webpack module ID for SSR resolution
+ moduleId: string;
+ // Chunk files for client-side loading
+ chunks: string[];
+ // Export names
+ exports: ('default' | '*' | string)[];
+ // Original file path (for debugging)
+ filePath: string;
+ };
+ };
+
+ // Server actions registry (replaces react-server-actions-manifest.json)
+ serverActions: {
+ [actionId: string]: {
+ // MF module path to import
+ modulePath: string;
+ // Export name
+ exportName: string;
+ // Is async action
+ async: boolean;
+ };
+ };
+
+ // For federated apps - remote RSC metadata
+ remoteRSC?: {
+ [remoteName: string]: {
+ // URL to fetch remote's RSC manifest
+ manifestUrl: string;
+ // Server actions endpoint (fallback)
+ actionsEndpoint: string;
+ // Remote server container for direct MF loading
+ serverEntry: string;
+ };
+ };
+ };
+}
+```
+
+### 2. Build-Time: Manifest Generation
+
+Extend the webpack build to embed RSC data into MF manifest:
+
+```javascript
+// In build.js - client layer config
+new ModuleFederationPlugin({
+ name: 'app1',
+ exposes: {
+ './Button': './src/Button.js',
+ './Counter': './src/DemoCounterButton.js',
+ },
+ manifest: {
+ additionalData: async ({ stats, compiler, compilation }) => {
+ // Read the react-client-manifest generated by ReactServerWebpackPlugin
+ const clientManifest = JSON.parse(
+ compilation.assets['react-client-manifest.json'].source()
+ );
+
+ // Transform to our format
+ const clientComponents = {};
+ for (const [filePath, entry] of Object.entries(clientManifest)) {
+ // Map file path to expose path if it matches
+ const exposePath = findExposePathForFile(stats.exposes, filePath);
+ if (exposePath) {
+ clientComponents[exposePath] = {
+ moduleId: entry.id,
+ chunks: entry.chunks,
+ exports: [entry.name],
+ filePath: filePath.replace('file://', ''),
+ };
+ }
+ }
+
+ // Attach to stats
+ stats.rsc = {
+ layer: 'client',
+ clientComponents,
+ serverActions: {}, // Client layer doesn't have actions
+ };
+
+ return stats;
+ },
+ },
+});
+```
+
+### 3. Runtime Plugin: SSR Component Resolution
+
+Create an MF runtime plugin that replaces the manual componentMap:
+
+```javascript
+// packages/app-shared/plugins/rscSSRResolverPlugin.js
+import { FederationRuntimePlugin } from '@module-federation/runtime';
+
+export function rscSSRResolverPlugin(): FederationRuntimePlugin {
+ // Cache for loaded component modules
+ const componentCache = new Map();
+
+ // Registry built from manifest
+ let componentRegistry = null;
+
+ return {
+ name: 'rsc-ssr-resolver-plugin',
+
+ // Hook into initialization to load manifest
+ async init(args) {
+ const { options, origin } = args;
+
+ // Load our own manifest
+ const manifestUrl = `${options.remotes?.__PUBLIC_PATH__ || ''}/mf-manifest.json`;
+ const manifest = await fetch(manifestUrl).then(r => r.json());
+
+ // Build component registry from manifest
+ if (manifest.rsc?.clientComponents) {
+ componentRegistry = manifest.rsc.clientComponents;
+
+ // Setup global webpack_require resolver for SSR
+ setupSSRResolver(componentRegistry, origin);
+ }
+
+ return args;
+ },
+
+ // Hook into remote manifest loading
+ async loadSnapshot(args) {
+ const { manifestJson, remote } = args;
+
+ // When loading a remote's manifest, also capture its RSC data
+ if (manifestJson.rsc?.clientComponents) {
+ // Merge remote's components into our registry with namespace
+ for (const [path, component] of Object.entries(manifestJson.rsc.clientComponents)) {
+ const namespacedPath = `${remote.name}/${path.replace('./', '')}`;
+ componentRegistry[namespacedPath] = {
+ ...component,
+ // Mark as remote for special loading
+ remote: remote.name,
+ };
+ }
+ }
+
+ return args;
+ },
+
+ // Hook into module loading for server actions
+ async onLoad(args) {
+ const { id, module, origin } = args;
+
+ // Check if this is a server-actions module
+ if (id.includes('server-actions') && args.exposeKey) {
+ // Register all actions from this module
+ const actionManifest = await fetchRemoteActionManifest(origin, args.remote);
+ registerServerActionsFromManifest(actionManifest, module);
+ }
+
+ return args;
+ },
+ };
+}
+
+// SSR Resolver Setup
+function setupSSRResolver(registry, federationHost) {
+ // Replace the manual componentMap approach with manifest-driven resolution
+ globalThis.__webpack_require__ = function(moduleId) {
+ // Check cache first
+ if (componentCache.has(moduleId)) {
+ return componentCache.get(moduleId);
+ }
+
+ // Parse the module ID to find component
+ // Format: "(client)/./src/Component.js" or "(client)/../remote/src/Component.js"
+ const match = moduleId.match(/\(client\)\/(.+)/);
+ const relativePath = match ? match[1] : moduleId;
+
+ // Find in registry by path matching
+ for (const [exposePath, component] of Object.entries(registry)) {
+ if (component.filePath.endsWith(relativePath) ||
+ component.moduleId === moduleId) {
+
+ // Load the actual module
+ let resolvedModule;
+
+ if (component.remote) {
+ // Remote component - use MF loadRemote
+ resolvedModule = federationHost.loadRemote(
+ `${component.remote}/${exposePath.replace('./', '')}`
+ );
+ } else {
+ // Local component - use webpack require
+ resolvedModule = __non_webpack_require__(
+ `./build/client/${getChunkForComponent(component)}`
+ );
+ }
+
+ componentCache.set(moduleId, resolvedModule);
+ return resolvedModule;
+ }
+ }
+
+ // Fallback for truly missing components
+ console.warn(`RSC SSR: Component not found in manifest: ${moduleId}`);
+ return { default: () => null };
+ };
+
+ // Chunk loading for SSR (no-op since we pre-resolve)
+ globalThis.__webpack_chunk_load__ = () => Promise.resolve();
+}
+```
+
+### 4. Server Actions: In-Process MF Loading
+
+Replace HTTP forwarding with direct MF module loading:
+
+```javascript
+// packages/app-shared/plugins/rscServerActionsPlugin.js
+import { FederationRuntimePlugin } from '@module-federation/runtime';
+import { registerServerReference } from 'react-server-dom-webpack/server';
+
+// Global registry for all server actions (local + remote)
+const serverActionRegistry = new Map();
+
+export function rscServerActionsPlugin(): FederationRuntimePlugin {
+ return {
+ name: 'rsc-server-actions-plugin',
+
+ // When a remote's server-actions module is loaded via MF
+ async onLoad(args) {
+ const { id, module, exposeInfo } = args;
+
+ // Detect server-actions expose
+ if (exposeInfo?.name === './server-actions' || id.includes('server-actions')) {
+ const remote = args.remoteInfo;
+
+ // Fetch the remote's server actions manifest
+ const manifestUrl = remote.rsc?.serverActionsManifest ||
+ `${remote.entry.replace('remoteEntry.js', '')}react-server-actions-manifest.json`;
+
+ const actionsManifest = await fetch(manifestUrl).then(r => r.json());
+
+ // Register each action from the manifest
+ for (const [actionId, actionDef] of Object.entries(actionsManifest)) {
+ const actionFn = module[actionDef.name];
+ if (typeof actionFn === 'function') {
+ // Register with React's server reference system
+ registerServerReference(
+ actionFn,
+ actionId,
+ actionDef.name
+ );
+
+ // Also store in our registry for direct access
+ serverActionRegistry.set(actionId, {
+ fn: actionFn,
+ remote: remote.name,
+ async: actionDef.async ?? true,
+ });
+ }
+ }
+
+ console.log(`[RSC] Registered ${Object.keys(actionsManifest).length} actions from ${remote.name}`);
+ }
+
+ return args;
+ },
+ };
+}
+
+// Unified action getter - works for local and remote actions
+export function getServerAction(actionId) {
+ // Check our unified registry first
+ if (serverActionRegistry.has(actionId)) {
+ return serverActionRegistry.get(actionId).fn;
+ }
+
+ // Fallback to global registry (for local actions)
+ if (globalThis.__RSC_SERVER_ACTION_REGISTRY__?.[actionId]) {
+ return globalThis.__RSC_SERVER_ACTION_REGISTRY__[actionId];
+ }
+
+ throw new Error(`Server action not found: ${actionId}`);
+}
+```
+
+### 5. SSR Entry: Manifest-Driven (No Manual Imports)
+
+Replace the manual componentMap with manifest-driven resolution:
+
+```javascript
+// src/framework/ssr-entry.js - NEW VERSION
+import { createFromNodeStream } from 'react-server-dom-webpack/client.node';
+import { renderToPipeableStream } from 'react-dom/server';
+import { init as initFederation } from '@module-federation/runtime';
+import { rscSSRResolverPlugin } from '@rsc-demo/app-shared/plugins/rscSSRResolverPlugin';
+
+// Initialize MF with our RSC plugin
+const federation = initFederation({
+ name: 'app1_ssr',
+ remotes: [
+ {
+ name: 'app2',
+ entry: 'http://localhost:4102/remoteEntry.server.js',
+ },
+ ],
+ plugins: [
+ rscSSRResolverPlugin(),
+ ],
+});
+
+// Wait for federation to initialize (loads manifests, sets up resolvers)
+await federation.ready;
+
+// SSR Manifest is built dynamically from MF manifest
+function buildSSRManifest() {
+ const moduleMap = {};
+
+ // Get component registry from our plugin
+ const registry = federation.getPluginData('rsc-ssr-resolver-plugin', 'componentRegistry');
+
+ for (const [exposePath, component] of Object.entries(registry)) {
+ moduleMap[component.moduleId] = {
+ default: { id: component.moduleId, name: 'default', chunks: [] },
+ '*': { id: component.moduleId, name: '*', chunks: [] },
+ '': { id: component.moduleId, name: '', chunks: [] },
+ };
+ }
+
+ return {
+ moduleLoading: { prefix: '', crossOrigin: null },
+ moduleMap,
+ serverModuleMap: null,
+ };
+}
+
+export async function renderFlightToHTML(flightBuffer, clientManifest) {
+ // Build SSR manifest from MF manifest data (no manual componentMap!)
+ const ssrManifest = buildSSRManifest();
+
+ // Create stream from flight buffer
+ const { Readable } = await import('stream');
+ const flightStream = Readable.from([flightBuffer]);
+
+ // Deserialize the React tree
+ const tree = await createFromNodeStream(flightStream, ssrManifest);
+
+ // Render to HTML
+ return new Promise((resolve, reject) => {
+ let html = '';
+ const { pipe } = renderToPipeableStream(tree, {
+ onShellReady() {
+ pipe({
+ write: (chunk) => { html += chunk.toString(); },
+ end: () => resolve(html),
+ });
+ },
+ onShellError: reject,
+ onError: (err) => console.error('SSR streaming error:', err),
+ });
+ });
+}
+```
+
+### 6. API Server: Unified Action Handling
+
+```javascript
+// server/api.server.js - Updated POST handler
+import { init as initFederation } from '@module-federation/runtime';
+import { rscServerActionsPlugin, getServerAction } from '@rsc-demo/app-shared/plugins/rscServerActionsPlugin';
+
+// Initialize federation with server actions plugin
+const federation = initFederation({
+ name: 'app1_rsc',
+ remotes: [
+ {
+ name: 'app2',
+ entry: 'http://localhost:4102/remoteEntry.server.js',
+ },
+ ],
+ plugins: [
+ rscServerActionsPlugin(),
+ ],
+});
+
+// Preload remote server actions
+async function preloadRemoteActions() {
+ // This triggers the onLoad hook which registers actions
+ await federation.loadRemote('app2/server-actions');
+}
+
+// Unified action handler - no HTTP forwarding needed
+app.post('/react', async (req, res) => {
+ const actionId = req.headers['rsc-action'];
+
+ // Ensure remote actions are loaded
+ await preloadRemoteActions();
+
+ // Get action from unified registry (local or remote)
+ const action = getServerAction(actionId);
+
+ if (!action) {
+ return res.status(404).json({ error: `Action not found: ${actionId}` });
+ }
+
+ // Decode arguments
+ const args = await decodeReply(req.body);
+
+ // Execute action (works for both local and remote!)
+ const result = await action(...args);
+
+ // Re-render and stream
+ const rscBuffer = await renderRSCToBuffer({ ...currentProps });
+ streamFlightResponse(res, rscBuffer);
+});
+```
+
+---
+
+## Benefits of This Architecture
+
+| Benefit | Description |
+|---------|-------------|
+| **No Manual componentMap** | Components auto-discovered from manifest |
+| **Unified Manifest** | Single source of truth for MF + RSC data |
+| **In-Process Actions** | Remote actions loaded via MF, no HTTP hop |
+| **Type Safety** | Manifest can generate TypeScript types |
+| **Dynamic Discovery** | New remotes auto-register their components |
+| **Version Safety** | MF share scope ensures React singleton |
+| **Preloading** | Can prefetch remote chunks based on manifest |
+| **Error Handling** | MF's `errorLoadRemote` hook provides fallbacks |
+
+---
+
+## Implementation Phases
+
+### Phase 1: Manifest Extension (Build-Time)
+1. Extend `additionalData` hook to embed RSC client manifest
+2. Embed server actions manifest in mf-manifest.json
+3. Add remote RSC metadata URLs
+
+### Phase 2: SSR Resolution Plugin (Runtime)
+1. Create `rscSSRResolverPlugin`
+2. Replace manual componentMap with manifest-driven resolution
+3. Handle remote component loading via MF
+
+### Phase 3: Server Actions Plugin (Runtime)
+1. Create `rscServerActionsPlugin`
+2. Auto-register actions on remote module load
+3. Remove HTTP forwarding code
+
+### Phase 4: Cleanup
+1. Remove react-client-manifest.json from separate file (embedded in MF manifest)
+2. Remove componentMap from ssr-entry.js
+3. Remove HTTP proxy code from api.server.js
+
+---
+
+## File Changes Required
+
+```
+apps/rsc-demo/
+├── packages/
+│ ├── app-shared/
+│ │ └── plugins/
+│ │ ├── rscSSRResolverPlugin.js # NEW
+│ │ ├── rscServerActionsPlugin.js # NEW
+│ │ └── rscManifestPlugin.js # NEW (build-time)
+│ ├── app1/
+│ │ ├── scripts/
+│ │ │ ├── build.js # MODIFY (add manifest plugin)
+│ │ │ └── client.build.js # MODIFY (additionalData hook)
+│ │ ├── src/framework/
+│ │ │ └── ssr-entry.js # REWRITE (manifest-driven)
+│ │ └── server/
+│ │ └── api.server.js # MODIFY (unified actions)
+│ └── app2/
+│ └── (same changes as app1)
+```
+
+---
+
+## Runtime Flow Diagram
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ BUILD TIME │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ ReactServerWebpackPlugin ──► react-client-manifest.json │
+│ │ │
+│ ▼ │
+│ ModuleFederationPlugin.additionalData() ──► mf-manifest.json │
+│ │ (includes rsc.clientComponents) │
+│ ▼ │
+│ Same for server-actions ──► mf-manifest.json.rsc.serverActions │
+│ │
+└─────────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│ RUNTIME (SSR) │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ initFederation({ plugins: [rscSSRResolverPlugin()] }) │
+│ │ │
+│ ├──► Load mf-manifest.json │
+│ ├──► Build componentRegistry from rsc.clientComponents │
+│ ├──► Setup globalThis.__webpack_require__ │
+│ │ │
+│ ▼ │
+│ renderFlightToHTML(flightBuffer) │
+│ │ │
+│ ├──► createFromNodeStream() parses $L references │
+│ ├──► __webpack_require__(moduleId) resolves from registry │
+│ └──► renderToPipeableStream() generates HTML │
+│ │
+└─────────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│ RUNTIME (Server Actions) │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ initFederation({ plugins: [rscServerActionsPlugin()] }) │
+│ │ │
+│ ├──► loadRemote('app2/server-actions') │
+│ │ │ │
+│ │ └──► onLoad hook fires │
+│ │ │ │
+│ │ ├──► Fetch react-server-actions-manifest │
+│ │ └──► registerServerReference() for each │
+│ │ │
+│ ▼ │
+│ POST /react (action request) │
+│ │ │
+│ ├──► getServerAction(actionId) │
+│ │ └──► Returns local OR remote action function │
+│ │ │
+│ ├──► Execute action(...args) │
+│ └──► Stream updated Flight response │
+│ │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Key Insights from Research
+
+### Module Federation Runtime Capabilities Used
+
+1. **Plugin Hooks**:
+ - `init` - Load manifests and setup resolvers
+ - `loadSnapshot` - Intercept remote manifest loading
+ - `onLoad` - Intercept module loads for action registration
+ - `fetch` - Custom manifest fetching if needed
+ - `errorLoadRemote` - Fallback for failed loads
+
+2. **Manifest Extension**:
+ - `additionalData` hook allows embedding any custom data
+ - Can include full react-client-manifest.json content
+ - Remote discovery through manifest's remotes array
+
+3. **Share Scope**:
+ - React/react-dom shared as singletons
+ - Separate share scopes for client/rsc/ssr layers
+ - Version negotiation handled by MF runtime
+
+4. **Module Cache**:
+ - `moduleCache.get(remoteName)` prevents duplicate loads
+ - Can pre-warm cache for critical remotes
+
+### Why This Works
+
+The key insight is that MF already solves the core problems RSC SSR faces:
+
+| RSC Problem | MF Solution |
+|-------------|-------------|
+| "Which chunks contain this component?" | `manifest.exposes[path].assets.js.sync` |
+| "What's the webpack module ID?" | Embed in `rsc.clientComponents[path].moduleId` |
+| "How to load remote modules in Node?" | `loadRemote()` with async-node target |
+| "How to share React across apps?" | Share scope with singleton + version matching |
+| "How to discover remote capabilities?" | Manifest loading via `loadSnapshot` hook |
+
+---
+
+## Next Steps
+
+1. **Prototype the manifest extension** - Add `additionalData` hook to current builds
+2. **Create SSR resolver plugin** - Test with single app first
+3. **Create server actions plugin** - Test local-to-remote action execution
+4. **Remove legacy code** - Delete componentMap, HTTP forwarding
+5. **Add tests** - Ensure manifest-driven resolution works reliably
diff --git a/apps/rsc-demo/CODE_OF_CONDUCT.md b/apps/rsc-demo/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000000..f049d4c5317
--- /dev/null
+++ b/apps/rsc-demo/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at . All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/apps/rsc-demo/Dockerfile b/apps/rsc-demo/Dockerfile
new file mode 100644
index 00000000000..c0d7899c53b
--- /dev/null
+++ b/apps/rsc-demo/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:lts-hydrogen
+
+WORKDIR /opt/notes-app
+
+COPY package.json package-lock.json ./
+
+RUN npm install --legacy-peer-deps
+
+COPY . .
+
+ENTRYPOINT [ "npm", "run" ]
+CMD [ "start" ]
diff --git a/apps/rsc-demo/FEDERATION_NATIVE_RSC.md b/apps/rsc-demo/FEDERATION_NATIVE_RSC.md
new file mode 100644
index 00000000000..2472d11311a
--- /dev/null
+++ b/apps/rsc-demo/FEDERATION_NATIVE_RSC.md
@@ -0,0 +1,815 @@
+# Federation-Native RSC Architecture
+
+## Problem Statement
+
+The current RSC implementation has several non-federation patterns:
+
+1. **Manual `componentMap`** - Every client component must be hand-imported in `ssr-entry.js`
+2. **JSON File Fetching** - Runtime plugin fetches `mf-stats.json`, `react-server-actions-manifest.json` separately
+3. **String-based Resolution** - SSR uses path string matching, not MF protocol
+4. **No `loadRemote` Integration** - SSR doesn't use the core federation primitive
+5. **HTTP Server Actions Fallback** - Remote actions still default to HTTP forwarding
+
+## Design Philosophy
+
+**Core Principle**: Use `loadRemote()` as the universal module resolver for SSR.
+
+Instead of:
+```javascript
+// Current: Manual componentMap + string matching
+const componentMap = { './src/Button.js': { default: Button } };
+__webpack_require__(moduleId) → componentMap[path]
+```
+
+Do:
+```javascript
+// Federation-native: loadRemote is the resolver
+__webpack_require__(moduleId) → loadRemote(moduleId, { layer: 'ssr' })
+```
+
+---
+
+## Architecture Overview
+
+### The Key Insight: Layer-Aware Remote Entries
+
+Each federated app builds **three remote entries**:
+
+```
+app1/
+├── remoteEntry.client.js → Exposes client-layer modules
+├── remoteEntry.server.js → Exposes RSC-layer modules
+└── remoteEntry.ssr.js → Exposes SSR-layer modules (NEW)
+```
+
+The SSR layer exposes the **same modules** but compiled for SSR context:
+- Actual component code (not client references)
+- No `'use server'` transforms (error stubs)
+- Node.js compatible (no browser APIs)
+
+### Module Resolution Protocol
+
+```
+RSC Flight Stream contains: $L{"id":"(client)/./src/Button.js", ...}
+ ↓
+ __webpack_require__("(client)/./src/Button.js")
+ ↓
+ rscSSRPlugin intercepts the call
+ ↓
+ Maps moduleId → federation expose path
+ "(client)/./src/Button.js" → "app1/Button"
+ ↓
+ loadRemote("app1/Button", { layer: 'ssr' })
+ ↓
+ Federation runtime loads from remoteEntry.ssr.js
+ ↓
+ Returns actual component for SSR rendering
+```
+
+---
+
+## Implementation
+
+### 1. Build Configuration: SSR Remote Entry
+
+Add a dedicated SSR layer remote entry that exposes client components:
+
+```javascript
+// scripts/ssr.build.js - ENHANCED
+new ModuleFederationPlugin({
+ name: 'app1',
+ filename: 'remoteEntry.ssr.js', // NEW: SSR-specific entry
+ library: { type: 'commonjs-module', name: 'app1_ssr' },
+
+ // Expose ALL client components for SSR resolution
+ exposes: {
+ './DemoCounterButton': './src/DemoCounterButton.js',
+ './EditButton': './src/EditButton.js',
+ './Button': './src/Button.js',
+ // Auto-generated from react-client-manifest.json at build time
+ },
+
+ // SSR-specific share scope
+ shared: {
+ react: { shareScope: 'ssr', singleton: true },
+ 'react-dom': { shareScope: 'ssr', singleton: true },
+ },
+
+ shareScope: ['ssr'],
+
+ // Include the SSR resolver plugin
+ runtimePlugins: [
+ require.resolve('@module-federation/node/runtimePlugin'),
+ require.resolve('./rscSSRResolverPlugin.js'),
+ ],
+})
+```
+
+### 2. Build-Time: Auto-Generate Exposes from Client Manifest
+
+Instead of manually listing exposes, derive them from the client manifest:
+
+```javascript
+// scripts/generateSSRExposes.js
+const clientManifest = require('../build/react-client-manifest.json');
+
+function generateSSRExposes() {
+ const exposes = {};
+
+ for (const [filePath, entry] of Object.entries(clientManifest)) {
+ // Convert file path to expose name
+ // "file:///path/to/app1/src/Button.js" → "./Button"
+ const relativePath = filePath.replace(/^file:\/\/.*\/src\//, './').replace('.js', '');
+ const sourcePath = filePath.replace('file://', '');
+
+ exposes[relativePath] = sourcePath;
+ }
+
+ return exposes;
+}
+
+module.exports = { generateSSRExposes };
+```
+
+### 3. Runtime Plugin: Federation-Native SSR Resolver
+
+The core innovation - replace `__webpack_require__` with `loadRemote`:
+
+```javascript
+// plugins/rscSSRResolverPlugin.js
+const { FederationHost } = require('@module-federation/runtime');
+
+function rscSSRResolverPlugin() {
+ // Module cache - populated by loadRemote, used by __webpack_require__
+ const ssrModuleCache = new Map();
+
+ // Maps RSC module IDs to federation expose paths
+ // Built from manifest during init
+ const moduleIdToExpose = new Map();
+
+ let federationHost = null;
+
+ return {
+ name: 'rsc-ssr-resolver-plugin',
+ version: '2.0.0',
+
+ /**
+ * beforeInit: Capture federation host and build module ID mapping
+ */
+ beforeInit(args) {
+ federationHost = args.origin;
+ return args;
+ },
+
+ /**
+ * init: Setup __webpack_require__ to use loadRemote
+ */
+ init(args) {
+ const { origin } = args;
+
+ // Build moduleId → expose mapping from the manifest
+ // The manifest contains all the RSC client component metadata
+ buildModuleIdMapping(origin);
+
+ // Install federation-native webpack_require
+ installFederationResolver(origin);
+
+ return args;
+ },
+
+ /**
+ * loadSnapshot: When loading remote manifests, capture RSC metadata
+ */
+ async loadSnapshot(args) {
+ const { manifestJson, remote } = args;
+
+ // Extract RSC component mappings from manifest
+ if (manifestJson?.rsc?.clientComponents) {
+ for (const [exposePath, component] of Object.entries(manifestJson.rsc.clientComponents)) {
+ // Map moduleId → remote/expose
+ moduleIdToExpose.set(component.moduleId, {
+ remote: remote.name,
+ expose: exposePath,
+ moduleId: component.moduleId,
+ });
+ }
+ }
+
+ return args;
+ },
+
+ /**
+ * onLoad: Cache loaded modules for fast __webpack_require__ lookup
+ */
+ async onLoad(args) {
+ const { id, exposeModule, remote, expose } = args;
+
+ // Store in cache with multiple keys for flexible lookup
+ if (exposeModule) {
+ const cacheKey = `${remote?.name || 'local'}/${expose}`;
+ ssrModuleCache.set(cacheKey, exposeModule);
+
+ // Also cache by moduleId if we have the mapping
+ for (const [moduleId, mapping] of moduleIdToExpose.entries()) {
+ if (mapping.remote === remote?.name && mapping.expose === expose) {
+ ssrModuleCache.set(moduleId, exposeModule);
+ }
+ }
+ }
+
+ return args;
+ },
+ };
+
+ /**
+ * Build the moduleId → expose mapping from host's manifest
+ */
+ function buildModuleIdMapping(host) {
+ // Get RSC metadata from host's options (injected at build time)
+ const rscConfig = host.options?.rsc || {};
+
+ if (rscConfig.clientComponents) {
+ for (const [exposePath, component] of Object.entries(rscConfig.clientComponents)) {
+ moduleIdToExpose.set(component.moduleId, {
+ remote: host.name,
+ expose: exposePath,
+ moduleId: component.moduleId,
+ });
+ }
+ }
+ }
+
+ /**
+ * Install __webpack_require__ that uses loadRemote
+ */
+ function installFederationResolver(host) {
+ // The magic: __webpack_require__ becomes loadRemote
+ globalThis.__webpack_require__ = function federationRequire(moduleId) {
+ // 1. Check cache first (instant return for already-loaded modules)
+ if (ssrModuleCache.has(moduleId)) {
+ return ssrModuleCache.get(moduleId);
+ }
+
+ // 2. Parse the moduleId to find the mapping
+ const cleanId = moduleId.replace(/^\(client\)\//, '');
+
+ // 3. Look up in our moduleId → expose mapping
+ let mapping = moduleIdToExpose.get(moduleId) || moduleIdToExpose.get(cleanId);
+
+ // 4. If not found, try to derive from path
+ if (!mapping) {
+ mapping = deriveExposeFromPath(cleanId, host);
+ }
+
+ if (!mapping) {
+ console.warn(`[RSC-SSR] No mapping for moduleId: ${moduleId}`);
+ return { default: () => null, __esModule: true };
+ }
+
+ // 5. Use loadRemote to get the module
+ // IMPORTANT: This is synchronous because modules should be pre-loaded
+ // during manifest loading. If not cached, we have a problem.
+ const cacheKey = `${mapping.remote}/${mapping.expose}`;
+
+ if (ssrModuleCache.has(cacheKey)) {
+ const module = ssrModuleCache.get(cacheKey);
+ ssrModuleCache.set(moduleId, module); // Cache under original key too
+ return module;
+ }
+
+ // 6. Fallback: Async load (should rarely happen if preloading works)
+ console.warn(`[RSC-SSR] Module not preloaded, loading sync: ${cacheKey}`);
+
+ // For sync resolution, we need the module to be pre-loaded
+ // This is a safety fallback - in production, modules should be pre-cached
+ throw new Error(
+ `Module ${moduleId} not preloaded. Ensure preloadSSRModules() was called.`
+ );
+ };
+
+ // Chunk loading is a no-op for SSR (modules are pre-loaded)
+ globalThis.__webpack_chunk_load__ = () => Promise.resolve();
+
+ // Module cache for compatibility
+ globalThis.__webpack_require__.c = {};
+ }
+
+ /**
+ * Derive expose path from file path
+ */
+ function deriveExposeFromPath(cleanId, host) {
+ // "./src/Button.js" → "./Button"
+ // "../shared-rsc/src/Widget.js" → "@rsc-demo/shared-rsc/Widget"
+
+ const parts = cleanId.split('/');
+ const filename = parts.pop().replace('.js', '');
+
+ // Check if it's a local expose
+ if (cleanId.startsWith('./src/')) {
+ return {
+ remote: host.name,
+ expose: `./${filename}`,
+ moduleId: cleanId,
+ };
+ }
+
+ // Check for package references
+ if (cleanId.includes('@rsc-demo/')) {
+ const match = cleanId.match(/@rsc-demo\/([^/]+)/);
+ if (match) {
+ return {
+ remote: match[1],
+ expose: `./${filename}`,
+ moduleId: cleanId,
+ };
+ }
+ }
+
+ return null;
+ }
+}
+
+module.exports = rscSSRResolverPlugin;
+```
+
+### 4. Preload SSR Modules During Initialization
+
+Before rendering, preload all client components via federation:
+
+```javascript
+// framework/ssrPreloader.js
+const { init, loadRemote, preloadRemote } = require('@module-federation/runtime');
+
+/**
+ * Preload all SSR modules from the manifest
+ * Call this once at server startup, not per-request
+ */
+async function preloadSSRModules(federationHost) {
+ const manifest = federationHost.options.manifest;
+
+ // Get all client component exposes
+ const exposes = manifest?.rsc?.clientComponents || {};
+
+ const preloadPromises = [];
+
+ for (const [exposePath, component] of Object.entries(exposes)) {
+ // Preload each component
+ preloadPromises.push(
+ loadRemote(`${federationHost.name}/${exposePath.replace('./', '')}`)
+ .catch(err => {
+ console.warn(`[SSR] Failed to preload ${exposePath}:`, err.message);
+ return null;
+ })
+ );
+ }
+
+ // Also preload remote components
+ for (const remote of federationHost.options.remotes || []) {
+ preloadPromises.push(
+ preloadRemote([{ nameOrAlias: remote.name, exposes: ['*'] }])
+ .catch(err => {
+ console.warn(`[SSR] Failed to preload remote ${remote.name}:`, err.message);
+ })
+ );
+ }
+
+ await Promise.all(preloadPromises);
+
+ console.log(`[SSR] Preloaded ${Object.keys(exposes).length} local components`);
+}
+
+module.exports = { preloadSSRModules };
+```
+
+### 5. New SSR Entry: Zero Manual Imports
+
+The new SSR entry has **no manual imports** - everything comes from federation:
+
+```javascript
+// src/framework/ssr-entry.js - REWRITTEN
+import { createFromNodeStream } from 'react-server-dom-webpack/client.node';
+import { renderToPipeableStream } from 'react-dom/server';
+import { Readable } from 'stream';
+
+// Federation runtime is initialized by the plugin
+// __webpack_require__ is replaced by the plugin to use loadRemote
+
+/**
+ * Render an RSC flight stream to HTML
+ * No componentMap needed - federation handles all resolution
+ */
+export async function renderFlightToHTML(flightBuffer, ssrManifest) {
+ // The ssrManifest maps moduleIds to chunk info
+ // But our __webpack_require__ ignores chunks (uses loadRemote instead)
+
+ const flightStream = Readable.from([flightBuffer]);
+
+ // createFromNodeStream will call __webpack_require__ for each $L reference
+ // Our plugin intercepts and uses loadRemote to resolve
+ const tree = await createFromNodeStream(flightStream, ssrManifest);
+
+ return new Promise((resolve, reject) => {
+ let html = '';
+ const { pipe } = renderToPipeableStream(tree, {
+ onShellReady() {
+ pipe({
+ write: chunk => { html += chunk.toString(); },
+ end: () => resolve(html),
+ });
+ },
+ onShellError: reject,
+ onError: err => console.error('SSR streaming error:', err),
+ });
+ });
+}
+
+/**
+ * Build SSR manifest from federation metadata
+ * No file reading - uses data from federation init
+ */
+export function buildSSRManifest(federationHost) {
+ const moduleMap = {};
+
+ // Get RSC metadata from federation host
+ const rscConfig = federationHost.options?.rsc || {};
+ const clientComponents = rscConfig.clientComponents || {};
+
+ for (const [exposePath, component] of Object.entries(clientComponents)) {
+ const moduleId = component.moduleId;
+ moduleMap[moduleId] = {
+ default: { id: moduleId, name: 'default', chunks: [] },
+ '*': { id: moduleId, name: '*', chunks: [] },
+ '': { id: moduleId, name: '', chunks: [] },
+ };
+ }
+
+ return {
+ moduleLoading: { prefix: '', crossOrigin: null },
+ moduleMap,
+ serverModuleMap: null,
+ };
+}
+```
+
+### 6. Server Actions: Federation-Native Registration
+
+Extend the pattern to server actions - use `loadRemote` instead of manifest fetching:
+
+```javascript
+// plugins/rscServerActionsPlugin.js
+const { registerServerReference } = require('react-server-dom-webpack/server');
+
+function rscServerActionsPlugin() {
+ const registeredActions = new Set();
+
+ return {
+ name: 'rsc-server-actions-plugin',
+ version: '2.0.0',
+
+ /**
+ * initContainer: Auto-register server actions from remote
+ */
+ async initContainer(args) {
+ const { remoteInfo, remoteEntryExports } = args;
+
+ // Check if this remote has server-actions expose
+ // This info comes from the manifest loaded during init
+ const rscConfig = args.remoteSnapshot?.rsc || {};
+ const exposeTypes = rscConfig.exposeTypes || {};
+
+ if (exposeTypes['./server-actions'] === 'server-action') {
+ await registerActionsFromRemote(
+ remoteInfo,
+ remoteEntryExports,
+ rscConfig
+ );
+ }
+
+ return args;
+ },
+
+ /**
+ * onLoad: Register actions when server-actions module is loaded
+ */
+ async onLoad(args) {
+ const { expose, exposeModule, remote } = args;
+
+ if (expose === './server-actions' && exposeModule) {
+ await registerActionsFromModule(
+ remote?.name || 'local',
+ exposeModule,
+ args.remoteSnapshot?.rsc
+ );
+ }
+
+ return args;
+ },
+ };
+
+ async function registerActionsFromRemote(remoteInfo, remoteEntry, rscConfig) {
+ const remoteName = remoteInfo.name;
+ const registrationKey = `${remoteName}:server-actions`;
+
+ if (registeredActions.has(registrationKey)) return;
+
+ try {
+ // Use the remote entry's get() to load server-actions
+ // This is pure federation protocol - no HTTP fetching
+ const factory = await remoteEntry.get('./server-actions');
+ if (!factory) return;
+
+ const module = await factory();
+ await registerActionsFromModule(remoteName, module, rscConfig);
+
+ registeredActions.add(registrationKey);
+ } catch (err) {
+ console.warn(`[RSC] Failed to register actions from ${remoteName}:`, err.message);
+ }
+ }
+
+ async function registerActionsFromModule(remoteName, module, rscConfig) {
+ // Get action metadata from RSC config (part of manifest)
+ const serverActions = rscConfig?.serverActions || {};
+
+ for (const [actionId, actionDef] of Object.entries(serverActions)) {
+ const fn = actionDef.exportName === 'default'
+ ? module.default
+ : module[actionDef.exportName];
+
+ if (typeof fn === 'function') {
+ registerServerReference(fn, actionId, actionDef.exportName);
+ }
+ }
+ }
+}
+
+module.exports = rscServerActionsPlugin;
+```
+
+---
+
+## Manifest Structure: Embedding RSC Metadata
+
+All RSC data lives in `mf-manifest.json` via `additionalData`:
+
+```javascript
+// Build config: Embed RSC metadata into MF manifest
+new ModuleFederationPlugin({
+ manifest: {
+ additionalData: async ({ compilation }) => {
+ // Read react-client-manifest.json (build artifact)
+ const clientManifest = JSON.parse(
+ compilation.assets['react-client-manifest.json'].source()
+ );
+
+ // Read react-server-actions-manifest.json (build artifact)
+ const actionsManifest = JSON.parse(
+ compilation.assets['react-server-actions-manifest.json'].source()
+ );
+
+ // Transform to federation-friendly format
+ return {
+ rsc: {
+ layer: 'rsc',
+
+ // Client components: moduleId → expose mapping
+ clientComponents: transformClientManifest(clientManifest),
+
+ // Server actions: actionId → metadata
+ serverActions: transformActionsManifest(actionsManifest),
+
+ // Expose types for quick lookups
+ exposeTypes: {
+ './Button': 'client-component',
+ './server-actions': 'server-action',
+ },
+ },
+ };
+ },
+ },
+});
+
+function transformClientManifest(manifest) {
+ const result = {};
+
+ for (const [filePath, entry] of Object.entries(manifest)) {
+ // Derive expose name from file path
+ const exposeName = deriveExposeName(filePath);
+
+ result[exposeName] = {
+ moduleId: entry.id,
+ chunks: entry.chunks,
+ exports: [entry.name],
+ filePath: filePath.replace('file://', ''),
+ };
+ }
+
+ return result;
+}
+
+function transformActionsManifest(manifest) {
+ const result = {};
+
+ for (const [actionId, entry] of Object.entries(manifest)) {
+ result[actionId] = {
+ moduleId: entry.id,
+ exportName: entry.name,
+ async: true,
+ };
+ }
+
+ return result;
+}
+```
+
+---
+
+## Runtime Flow Diagram
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ SERVER STARTUP │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ 1. init(@module-federation/runtime) │
+│ │ │
+│ ├─► Load mf-manifest.json (contains rsc.clientComponents) │
+│ │ │
+│ ├─► rscSSRResolverPlugin.init() │
+│ │ └─► Build moduleIdToExpose mapping from manifest.rsc │
+│ │ └─► Install federation-native __webpack_require__ │
+│ │ │
+│ └─► preloadSSRModules() │
+│ └─► loadRemote() for each clientComponent │
+│ └─► Populate ssrModuleCache │
+│ │
+│ 2. rscServerActionsPlugin.initContainer() │
+│ └─► remoteEntry.get('./server-actions') │
+│ └─► registerServerReference() for each action │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ REQUEST HANDLING │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ GET / │
+│ │ │
+│ ├─► RSC Server renders to Flight stream │
+│ │ └─► $L{"id":"(client)/./src/Button.js",...} │
+│ │ │
+│ ├─► SSR Worker: renderFlightToHTML(flightBuffer) │
+│ │ │ │
+│ │ ├─► createFromNodeStream() parses Flight │
+│ │ │ │ │
+│ │ │ └─► For each $L reference: │
+│ │ │ │ │
+│ │ │ └─► __webpack_require__("(client)/./src/Button.js") │
+│ │ │ │ │
+│ │ │ └─► rscSSRResolverPlugin intercepts │
+│ │ │ │ │
+│ │ │ ├─► Check ssrModuleCache (preloaded) │
+│ │ │ │ └─► Return cached module ✓ │
+│ │ │ │ │
+│ │ │ └─► If not cached: loadRemote('app1/Button') │
+│ │ │ │
+│ │ └─► renderToPipeableStream(tree) → HTML │
+│ │ │
+│ └─► Return SSR HTML + embedded Flight data │
+│ │
+│ POST /react (Server Action) │
+│ │ │
+│ ├─► Extract actionId from header │
+│ │ │
+│ ├─► getServerAction(actionId) │
+│ │ └─► Already registered by plugin during init │
+│ │ │
+│ ├─► Execute action(...args) │
+│ │ │
+│ └─► Re-render and stream Flight response │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Layer-Aware Module Loading
+
+### Option A: Separate Remote Entries
+
+```javascript
+// Different remote entries per layer
+remotes: {
+ 'app2/client': 'app2@http://localhost:4102/remoteEntry.client.js',
+ 'app2/ssr': 'app2@http://localhost:4102/remoteEntry.ssr.js',
+ 'app2/rsc': 'app2@http://localhost:4102/remoteEntry.server.js',
+}
+
+// Resolution based on context
+loadRemote('app2/Button', { layer: 'ssr' })
+ → loads from remoteEntry.ssr.js
+```
+
+### Option B: Resource Query Decoration
+
+```javascript
+// Single remote with layer query
+loadRemote('app2/Button?layer=ssr')
+
+// Plugin intercepts and routes to correct entry
+beforeRequest(args) {
+ const layer = parseLayerFromQuery(args.id);
+ if (layer === 'ssr') {
+ args.remote.entry = args.remote.entry.replace('.client.js', '.ssr.js');
+ }
+ return args;
+}
+```
+
+### Option C: Share Scope Based Resolution
+
+```javascript
+// SSR components stored in 'ssr' share scope
+shared: {
+ '@app1/Button': { shareScope: 'ssr', get: () => loadRemote('app1/Button') },
+}
+
+// Resolution uses share scope
+resolveShare(args) {
+ if (args.scope === 'ssr' && isClientComponent(args.pkgName)) {
+ return loadRemote(args.pkgName);
+ }
+}
+```
+
+---
+
+## Benefits Over Current Architecture
+
+| Aspect | Current | Federation-Native |
+|--------|---------|-------------------|
+| **Component Discovery** | Manual componentMap | Auto from mf-manifest |
+| **Module Resolution** | String path matching | loadRemote() protocol |
+| **JSON Fetching** | 3+ separate fetches | Single manifest read |
+| **Server Actions** | HTTP forwarding + manifest fetch | Direct loadRemote |
+| **Remote Components** | Manual imports | Transparent federation |
+| **Preloading** | None | Federation preloadRemote |
+| **Caching** | Custom cache | Share scope + moduleCache |
+| **Error Handling** | Try/catch blocks | errorLoadRemote hook |
+| **Type Safety** | None | Manifest-driven types |
+
+---
+
+## Implementation Phases
+
+### Phase 1: Build Infrastructure
+- [ ] Add SSR remote entry generation (`remoteEntry.ssr.js`)
+- [ ] Auto-generate exposes from react-client-manifest.json
+- [ ] Embed RSC metadata in mf-manifest.json additionalData
+
+### Phase 2: SSR Resolution Plugin
+- [ ] Implement rscSSRResolverPlugin
+- [ ] Replace __webpack_require__ with loadRemote
+- [ ] Add module preloading at startup
+
+### Phase 3: Server Actions Plugin
+- [ ] Implement rscServerActionsPlugin
+- [ ] Register actions via federation protocol
+- [ ] Remove HTTP forwarding default
+
+### Phase 4: Cleanup
+- [ ] Remove manual componentMap from ssr-entry.js
+- [ ] Remove JSON fetching from rscRuntimePlugin
+- [ ] Remove HTTP fallback for server actions
+
+---
+
+## Key Files to Modify
+
+```
+apps/rsc-demo/packages/
+├── app-shared/
+│ └── plugins/
+│ ├── rscSSRResolverPlugin.js # NEW - Core SSR resolver
+│ └── rscServerActionsPlugin.js # NEW - Actions via federation
+│
+├── app1/
+│ ├── scripts/
+│ │ ├── build.js # Add SSR MF entry
+│ │ └── ssr.build.js # Add MF plugin with exposes
+│ │
+│ └── src/framework/
+│ └── ssr-entry.js # REWRITE - No componentMap
+│
+└── app2/
+ └── (same structure)
+```
+
+---
+
+## Summary
+
+The federation-native approach transforms RSC SSR from a **parallel system** that happens to coexist with MF into a **native federation consumer**. Every module resolution goes through `loadRemote()`, metadata comes from the manifest, and the runtime plugin system provides all the hooks needed for RSC-specific behavior.
+
+**The core insight**: `__webpack_require__` in SSR context should be `loadRemote()` with a layer parameter. This single change eliminates manual component maps, separate JSON fetching, and creates a unified resolution path for local and remote components.
diff --git a/apps/rsc-demo/LICENSE b/apps/rsc-demo/LICENSE
new file mode 100644
index 00000000000..b96dcb0480a
--- /dev/null
+++ b/apps/rsc-demo/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/rsc-demo/README.md b/apps/rsc-demo/README.md
new file mode 100644
index 00000000000..475c0cadaaa
--- /dev/null
+++ b/apps/rsc-demo/README.md
@@ -0,0 +1,163 @@
+# 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`, run `nvm i` before running `npm install` to install the recommended Node version.)
+
+ ```
+ npm install --legacy-peer-deps
+ npm start
+ ```
+
+(Or `npm run start:prod` for a production build.)
+
+Then open http://localhost:4000.
+
+The app won't work until you set up the database, as described below.
+
+
+ Setup with Docker (optional)
+
You can also start dev build of the app by using docker-compose.
+
⚠️ This is completely optional, and is only for people who prefer Docker to global installs!
+
If you prefer Docker, make sure you have docker and docker-compose installed then run:
+
docker-compose up
+
Running seed script
+
1. Run containers in the detached mode
+
docker-compose up -d
+
2. Run seed script
+
docker-compose exec notes-app npm run seed
+
If you'd rather not use Docker, skip this section and continue 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 currently run via **HTTP forwarding** from app1 → app2.
+- The architecture is designed to also support **in‑process MF‑native actions** (no HTTP hop) as a next step.
+
+For a deep dive into the layering, manifests, and both federation strategies (HTTP proxy + MF‑native design), see `notes/RESEARCH.md`.
+
+## 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/credentials.js b/apps/rsc-demo/credentials.js
new file mode 100644
index 00000000000..edc6d3d66e7
--- /dev/null
+++ b/apps/rsc-demo/credentials.js
@@ -0,0 +1,7 @@
+module.exports = {
+ host: process.env.DB_HOST || 'localhost',
+ database: 'notesapi',
+ user: 'notesadmin',
+ password: 'password',
+ port: '5432',
+};
diff --git a/apps/rsc-demo/docker-compose.yml b/apps/rsc-demo/docker-compose.yml
new file mode 100644
index 00000000000..567f78d5e24
--- /dev/null
+++ b/apps/rsc-demo/docker-compose.yml
@@ -0,0 +1,34 @@
+version: '3.8'
+services:
+ postgres:
+ image: postgres:13
+ environment:
+ POSTGRES_USER: notesadmin
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: notesapi
+ ports:
+ - '5432:5432'
+ volumes:
+ - ./scripts/init_db.sh:/docker-entrypoint-initdb.d/init_db.sh
+ - db:/var/lib/postgresql/data
+
+ notes-app:
+ build:
+ context: .
+ depends_on:
+ - postgres
+ ports:
+ - '4000:4000'
+ environment:
+ DB_HOST: postgres
+ PORT: 4000
+ volumes:
+ - ./notes:/opt/notes-app/notes
+ - ./public:/opt/notes-app/public
+ - ./scripts:/opt/notes-app/scripts
+ - ./server:/opt/notes-app/server
+ - ./src:/opt/notes-app/src
+ - ./credentials.js:/opt/notes-app/credentials.js
+
+volumes:
+ db:
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..248d72e95ba
--- /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..67b8e4c1c70
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/framework/router.js
@@ -0,0 +1,162 @@
+/**
+ * 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/framework/ssr-resolver.js b/apps/rsc-demo/packages/app-shared/framework/ssr-resolver.js
new file mode 100644
index 00000000000..0e995ad2e86
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/framework/ssr-resolver.js
@@ -0,0 +1,94 @@
+'use strict';
+
+/**
+ * Federated SSR component resolver.
+ *
+ * Uses the registry set on globalThis.__RSC_SSR_REGISTRY__ by ssr-entry.js
+ * (loaded from mf-manifest.json at runtime) and merged with remote components
+ * by rscSSRRuntimePlugin's loadSnapshot hook.
+ *
+ * No build-time injection needed. Falls back to a no-op component if registry
+ * is missing to keep SSR resilient.
+ */
+
+let registryCache = null;
+let resolverInstalled = false;
+
+function loadRSCRegistry() {
+ if (registryCache) return registryCache;
+ if (globalThis.__RSC_SSR_REGISTRY__) {
+ registryCache = globalThis.__RSC_SSR_REGISTRY__;
+ return registryCache;
+ }
+ return null;
+}
+
+function normalizeId(moduleId) {
+ if (typeof moduleId !== 'string') return moduleId;
+ const match = moduleId.match(/\(client\)\/(.+)/);
+ const id = match ? match[1] : moduleId;
+ return id.startsWith('./') ? id : `./${id}`;
+}
+
+/**
+ * Install a global __webpack_require__ that can resolve client references
+ * emitted by the React Flight stream during SSR.
+ */
+function installFederatedSSRResolver() {
+ if (resolverInstalled) return;
+
+ // Capture the webpack require created by the SSR bundle.
+ const webpackRequire =
+ typeof __webpack_require__ === 'function' ? __webpack_require__ : null;
+
+ // Registry injected by runtime plugin (may still be null during very early init)
+ const registry = loadRSCRegistry() || {};
+
+ // noop chunk loader (SSR never loads additional chunks)
+ globalThis.__webpack_chunk_load__ = () => Promise.resolve();
+
+ globalThis.__webpack_require__ = function federatedSSRRequire(moduleId) {
+ const normalizedId = normalizeId(moduleId);
+
+ // Fast path: try webpack module id directly
+ if (webpackRequire) {
+ try {
+ return webpackRequire(normalizedId);
+ } catch (_e) {
+ // continue to registry lookup
+ }
+ }
+
+ // Registry lookup (helps when manifest id doesn't match webpack id exactly)
+ const entry =
+ registry[moduleId] ||
+ registry[normalizedId] ||
+ registry[`(client)/${normalizedId.replace(/^\.\//, '')}`];
+
+ if (entry && webpackRequire) {
+ const request = entry.request || normalizedId;
+ try {
+ const mod = webpackRequire(request);
+ return mod;
+ } catch (_e) {
+ // fall through to fallback
+ }
+ }
+
+ // Fallback: render nothing instead of crashing SSR
+ // Silent fallback: render nothing instead of crashing SSR
+ return {
+ __esModule: true,
+ default: function PlaceholderComponent() {
+ return null;
+ },
+ };
+ };
+
+ globalThis.__webpack_require__.__isFederatedSSRResolver = true;
+ resolverInstalled = true;
+}
+
+module.exports = {
+ installFederatedSSRResolver,
+};
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..585990f9da0
--- /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/rscRuntimePlugin.js b/apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js
new file mode 100644
index 00000000000..470d3e438c2
--- /dev/null
+++ b/apps/rsc-demo/packages/app-shared/scripts/rscRuntimePlugin.js
@@ -0,0 +1,528 @@
+'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 mf-stats.json 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');
+
+// 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);
+ }
+}
+
+/**
+ * Fetch and cache a remote's mf-stats.json
+ */
+async function getMFManifest(remoteUrl) {
+ if (remoteMFManifests.has(remoteUrl)) return remoteMFManifests.get(remoteUrl);
+ try {
+ if (remoteUrl.startsWith('http')) {
+ const candidates = [
+ remoteUrl.replace(/\/[^/]+$/, '/mf-stats.json'),
+ remoteUrl.replace(/\/[^/]+$/, '/mf-manifest.server-stats.json'),
+ ];
+
+ for (const statsUrl of candidates) {
+ log('Fetching MF manifest from:', statsUrl);
+ try {
+ const res = await fetch(statsUrl);
+ if (!res.ok) {
+ log('Failed to fetch', statsUrl, res.status);
+ continue;
+ }
+ const json = await res.json();
+ remoteMFManifests.set(remoteUrl, json);
+ return json;
+ } catch (e) {
+ log('Error fetching', statsUrl, e.message);
+ }
+ }
+ remoteMFManifests.set(remoteUrl, null);
+ return null;
+ }
+
+ // File-based remote container; read mf-stats.json from disk (deprecated)
+ const candidates = [
+ remoteUrl.replace(/[^/\\]+$/, 'mf-stats.json'),
+ remoteUrl.replace(/[^/\\]+$/, 'mf-manifest.server-stats.json'),
+ ];
+
+ const statsPath = candidates.find((p) => fs.existsSync(p));
+ if (statsPath) {
+ log(
+ 'WARNING: reading mf-stats.json from disk; prefer HTTP mf-stats for remotes.'
+ );
+ const json = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
+ remoteMFManifests.set(remoteUrl, json);
+ return json;
+ }
+ log('mf-stats.json not found at', statsPath);
+ remoteMFManifests.set(remoteUrl, null);
+ return null;
+ } catch (e) {
+ log('Error fetching mf-stats.json:', e.message);
+ remoteMFManifests.set(remoteUrl, null);
+ return null;
+ }
+}
+
+/**
+ * Fetch and cache a remote's mf-stats.json to get RSC config (+additionalData)
+ */
+async function getRemoteRSCConfig(remoteUrl) {
+ if (remoteRSCConfigs.has(remoteUrl)) {
+ return remoteRSCConfigs.get(remoteUrl);
+ }
+
+ try {
+ const stats = await getMFManifest(remoteUrl);
+ 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;
+ }
+ remoteRSCConfigs.set(remoteUrl, rscConfig);
+ log('Loaded RSC config:', JSON.stringify(rscConfig, null, 2));
+ return rscConfig;
+ } catch (error) {
+ log('Error fetching RSC config:', error.message);
+ remoteRSCConfigs.set(remoteUrl, null);
+ return null;
+ }
+}
+
+/**
+ * Fetch and cache a remote's server actions manifest
+ */
+async function getRemoteServerActionsManifest(remoteUrl) {
+ if (remoteServerActionsManifests.has(remoteUrl)) {
+ return remoteServerActionsManifests.get(remoteUrl);
+ }
+
+ try {
+ const rscConfig = await getRemoteRSCConfig(remoteUrl);
+ let manifestUrl =
+ rscConfig?.serverActionsManifest ||
+ rscConfig?.additionalData?.serverActionsManifest ||
+ (rscConfig?.remote?.actionsEndpoint
+ ? rscConfig.remote.actionsEndpoint.replace(
+ /\/react$/,
+ '/react-server-actions-manifest.json'
+ )
+ : null) ||
+ remoteUrl.replace(/\/[^/]+$/, '/react-server-actions-manifest.json');
+
+ log('Fetching server actions manifest from:', manifestUrl);
+
+ if (manifestUrl.startsWith('http')) {
+ const response = await fetch(manifestUrl);
+ if (!response.ok) {
+ log('Failed to fetch server actions manifest:', response.status);
+ return null;
+ }
+ const manifest = await response.json();
+ 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);
+ remoteServerActionsManifests.set(remoteUrl, null);
+ 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);
+ remoteServerActionsManifests.set(remoteUrl, null);
+ return null;
+ }
+}
+
+/**
+ * Register server actions from a loaded module
+ */
+function registerServerActionsFromModule(
+ remoteName,
+ remoteUrl,
+ exposeModule,
+ manifest
+) {
+ if (!exposeModule || !manifest) {
+ return 0;
+ }
+
+ let registeredCount = 0;
+ const remoteHost = getHostFromUrl(remoteUrl);
+
+ 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;
+ }
+
+ // Basic ownership check: prefer host match, otherwise fall back to name heuristic
+ const entryHost = getHostFromUrl(entry.id);
+ if (remoteHost && entryHost && entryHost !== remoteHost) {
+ continue;
+ }
+ if (!entryHost) {
+ if (
+ !entry.id.includes(`/packages/${remoteName}/`) &&
+ !entry.id.includes(`/${remoteName}/src/`)
+ ) {
+ // Can't confidently match; allow registration for single-remote setups
+ if (remoteHost) {
+ 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) {
+ const remoteName =
+ remoteInfo?.name || remoteInfo?.entryGlobalName || 'remote';
+ const remoteEntry = remoteInfo?.entry;
+ const registrationKey = `${remoteName}:./server-actions`;
+
+ if (registeredRemotes.has(registrationKey)) {
+ return;
+ }
+ if (registeringRemotes.has(registrationKey)) {
+ return registeringRemotes.get(registrationKey);
+ }
+
+ const work = (async () => {
+ try {
+ const manifest = await getRemoteServerActionsManifest(remoteEntry);
+ if (!manifest) {
+ log('No server actions manifest during init for', remoteName);
+ return;
+ }
+
+ if (!remoteEntryExports?.get) {
+ log('remoteEntryExports.get is missing for', remoteName);
+ return;
+ }
+
+ const factory = await remoteEntryExports.get('./server-actions');
+ if (!factory) {
+ log('No ./server-actions expose found for', remoteName);
+ return;
+ }
+ const exposeModule = await factory();
+ const count = registerServerActionsFromModule(
+ remoteName,
+ remoteEntry,
+ exposeModule,
+ manifest
+ );
+ if (count > 0) {
+ registeredRemotes.add(registrationKey);
+ log(`Registered ${count} server actions at init for ${remoteName}`);
+ }
+ } 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',
+
+ /**
+ * beforeInit: Inject RSC-specific configuration
+ */
+ beforeInit(args) {
+ log('beforeInit - origin:', args.origin?.name);
+
+ // Store host's RSC config if available
+ if (args.userOptions?.rsc) {
+ log('Host RSC config:', JSON.stringify(args.userOptions.rsc, null, 2));
+ }
+
+ return args;
+ },
+
+ /**
+ * beforeRegisterRemote: Validate and enhance remote registration
+ */
+ beforeRegisterRemote(args) {
+ log(
+ 'beforeRegisterRemote - remote:',
+ args.remote?.name,
+ 'entry:',
+ args.remote?.entry
+ );
+ return args;
+ },
+
+ /**
+ * 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).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:
+ // 1. The expose name (./server-actions)
+ // 2. RSC config from mf-stats.json (rsc.exposeTypes)
+ const isServerActionsExpose =
+ exposeKey === './server-actions' ||
+ exposeKey.includes('server-actions') ||
+ exposeKey.includes('actions');
+
+ 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
+ const rscConfig = await getRemoteRSCConfig(remoteEntry);
+
+ // Validate this is actually a server-action module using manifest metadata
+ if (rscConfig?.exposeTypes?.[exposeKey]) {
+ const exposeType = rscConfig.exposeTypes[exposeKey];
+ if (exposeType !== 'server-action' && exposeType !== 'server-actions') {
+ log(
+ 'Expose type is',
+ exposeType,
+ '- not registering as server actions'
+ );
+ return args;
+ }
+ }
+
+ // Fetch the server actions manifest
+ const manifest = await getRemoteServerActionsManifest(remoteEntry);
+ 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,
+ remoteEntry,
+ 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-actions
+ * 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
+ );
+
+ return args;
+ },
+
+ /**
+ * resolveShare: Layer-aware share resolution
+ *
+ * Uses the RSC layer metadata to ensure correct share scope selection.
+ * This helps prevent cross-layer React instance issues.
+ */
+ resolveShare(args) {
+ log('resolveShare - pkgName:', args.pkgName, 'scope:', args.scope);
+
+ // The share scope should already be set correctly by the MF config
+ // This hook can be used for additional validation or dynamic resolution
+
+ return args;
+ },
+
+ /**
+ * errorLoadRemote: Handle remote loading errors gracefully
+ */
+ async errorLoadRemote(args) {
+ log('errorLoadRemote - id:', args.id, 'error:', args.error?.message);
+
+ // For server actions, we can fall back to HTTP forwarding (Option 1)
+ // The api.server.js handler already has this fallback logic
+
+ 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..cb626ac0cb9
--- /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',
+ init(args) {
+ // Initialize registry from build-time injected data
+ initializeRegistry();
+ return args;
+ },
+ async loadSnapshot(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..b69662d33b0
--- /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/credentials.js b/apps/rsc-demo/packages/app1/credentials.js
new file mode 100644
index 00000000000..713f9391d6e
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/credentials.js
@@ -0,0 +1,7 @@
+module.exports = {
+ user: 'notesadmin',
+ password: 'password',
+ host: 'localhost',
+ database: 'notesapi',
+ port: 5432,
+};
diff --git a/apps/rsc-demo/packages/app1/package.json b/apps/rsc-demo/packages/app1/package.json
new file mode 100644
index 00000000000..f23568bf15e
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/package.json
@@ -0,0 +1,33 @@
+{
+ "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",
+ "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..0c50d969afc
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/build.js
@@ -0,0 +1,44 @@
+/**
+ * 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.');
+ }
+}
+
+const compiler = webpack([clientConfig, serverConfig, ssrConfig]);
+
+compiler.run((err, stats) => {
+ handleStats(err, stats);
+});
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..95598d2c4be
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/client.build.js
@@ -0,0 +1,227 @@
+'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 the generated mf-stats.json so runtime
+ * plugins (and SSR) can resolve client components without a hand-built
+ * componentMap. We keep the data small: moduleId, chunks, export name,
+ * and original file path.
+ */
+ manifest: {
+ additionalData: async ({stats, compilation}) => {
+ const asset = compilation.getAsset('react-client-manifest.json');
+ if (!asset) {
+ return stats;
+ }
+
+ const source = asset.source.source().toString();
+ const clientManifest = JSON.parse(source);
+ const clientComponents = {};
+
+ for (const [filePath, entry] of Object.entries(clientManifest)) {
+ const moduleId = entry.id;
+ const exportName =
+ entry.name && entry.name !== '*' ? entry.name : 'default';
+ clientComponents[moduleId] = {
+ moduleId,
+ request: moduleId.replace(/^\(client\)\//, './'),
+ chunks: entry.chunks || [],
+ exports: exportName ? [exportName] : [],
+ filePath: filePath.replace(/^file:\/\//, ''),
+ };
+ }
+
+ stats.additionalData = stats.additionalData || {};
+ stats.additionalData.rsc = {
+ layer: 'client',
+ shareScope: 'client',
+ isRSC: false,
+ clientComponents,
+ };
+
+ // Mirror on top-level for convenience (some tooling reads stats.rsc)
+ stats.rsc = stats.additionalData.rsc;
+ return stats;
+ },
+ },
+ }),
+ ],
+ 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..cf8462f59c5
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/seed.js
@@ -0,0 +1,92 @@
+/**
+ * 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} = require('fs/promises');
+const startOfYear = require('date-fns/startOfYear');
+const credentials = require('../credentials');
+
+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))
+ );
+
+ 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, (err) => {
+ if (err) {
+ throw err;
+ }
+ });
+ })
+ );
+}
+
+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..0b8fa7a7f98
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/server.build.js
@@ -0,0 +1,285 @@
+'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/remoteEntry.server.js';
+
+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)
+ //
+ // TODO (Option 2 - Deep MF Integration):
+ // To fully federate server actions via MF (not HTTP forwarding), we would need to:
+ // 1. Modify rsc-server-loader.js to call registerServerReference for remote modules
+ // 2. Modify react-server-dom-webpack-plugin.js to include remote actions in manifest
+ // 3. Ensure remote 'use server' modules register with host's serverActionRegistry
+ // See: packages/react-server-dom-webpack/cjs/rsc-server-loader.js
+ // See: packages/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js
+ new ModuleFederationPlugin({
+ name: 'app1',
+ filename: 'remoteEntry.server.js',
+ runtime: false,
+ // Consume app2's RSC container over HTTP (script remote)
+ 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',
+ shareScope: 'rsc',
+ 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,
+ 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,
+ requiredVersion: false,
+ singleton: true,
+ shareScope: 'rsc',
+ layer: WEBPACK_LAYERS.rsc,
+ issuerLayer: WEBPACK_LAYERS.rsc,
+ },
+ 'react-server-dom-webpack/server.node.unbundled': {
+ import: rsdwServerUnbundledPath,
+ 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..0b5febf88d1
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/scripts/ssr.build.js
@@ -0,0 +1,188 @@
+'use strict';
+
+const path = require('path');
+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 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',
+ additionalData: ({stats, compilation}) => {
+ const asset = compilation.getAsset('react-ssr-manifest.json');
+ if (!asset) return stats;
+ const ssrManifest = JSON.parse(asset.source.source().toString());
+ const moduleMap = ssrManifest?.moduleMap || {};
+ const clientComponents = {};
+ for (const [moduleId, exportsMap] of Object.entries(moduleMap)) {
+ const anyExport = exportsMap['*'] || Object.values(exportsMap)[0];
+ const specifier = anyExport?.specifier || moduleId;
+ clientComponents[moduleId] = {
+ moduleId,
+ request: specifier,
+ chunks: [],
+ exports: Object.keys(exportsMap),
+ filePath: specifier.replace(/^file:\/\//, ''),
+ };
+ }
+ stats.additionalData = stats.additionalData || {};
+ stats.additionalData.rsc = {
+ layer: 'ssr',
+ shareScope: 'client',
+ clientComponents,
+ };
+ stats.rsc = stats.additionalData.rsc;
+ return stats;
+ },
+ },
+ 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,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.ssr,
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ },
+ 'react-dom': {
+ singleton: true,
+ requiredVersion: false,
+ shareScope: 'client',
+ layer: WEBPACK_LAYERS.ssr,
+ issuerLayer: WEBPACK_LAYERS.ssr,
+ },
+ },
+ shareScope: ['client'],
+ shareStrategy: 'version-first',
+ }),
+ ],
+ 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..a125acc4f18
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/server/api.server.js
@@ -0,0 +1,733 @@
+/**
+ * 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} = 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';
+
+// Host app runs on 4101 by default (tests assume this)
+const PORT = process.env.PORT || 4101;
+
+// Remote app configuration for federated server actions (Option 1 - HTTP forwarding)
+// 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}
+ *
+ * TODO (Option 2 - Deep MF Integration):
+ * Instead of HTTP forwarding, remote actions could be executed via MF:
+ * 1. Import remote action modules via MF in server-entry.js
+ * 2. Remote 'use server' functions register with host's serverActionRegistry
+ * 3. getServerAction(actionId) returns federated functions directly
+ * This requires changes to:
+ * - rsc-server-loader.js to handle remote module registration
+ * - react-server-dom-webpack-plugin.js to include remote actions in manifest
+ * - server.node.js to support federated action lookups
+ * Remote manifests (react-server-actions-manifest.json) would then be merged
+ * into the host's manifest instead of being consulted via HTTP.
+ */
+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,
+ 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
+ );
+
+ // 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();
+
+ // 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];
+
+ // Ensure any MF-native remote actions are registered into the host
+ // registry before we attempt lookup. This enables Option 2 for app2.
+ await ensureRemoteActionsRegistered(server);
+
+ // 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)
+ let actionFn = server.getServerAction(actionId);
+ let actionName = actionId.split('#')[1] || 'default';
+
+ // If MF-native registration did not provide a function, fall back to
+ // Option 1 (HTTP forwarding) for known remote actions.
+ if (!actionFn) {
+ const remoteApp = getRemoteAppForAction(actionId);
+ 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.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');
+
+ // 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');
+
+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 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 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 unlink(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) {
+ // Parse and validate sleep time to prevent DoS
+ const MAX_SLEEP_MS = 10000;
+ const requested = parseInt(req.params.ms, 10);
+ // Use fixed durations to avoid user-controlled timer (CodeQL security)
+ let sleepMs;
+ if (!Number.isFinite(requested) || requested <= 0) {
+ sleepMs = 0;
+ } else if (requested >= MAX_SLEEP_MS) {
+ sleepMs = MAX_SLEEP_MS;
+ } else {
+ sleepMs = requested;
+ }
+ setTimeout(() => {
+ res.json({ok: true});
+ }, 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..17b35dbe2e0
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/server/ssr-worker.js
@@ -0,0 +1,36 @@
+/**
+ * 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 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..4cc55e6b695
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/App.js
@@ -0,0 +1,56 @@
+/**
+ * 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';
+
+export default function App({selectedId, isEditing, searchText}) {
+ return (
+
+ Calls app2's incrementCount action via HTTP forwarding through app1
+
+
+
+
+
+
+ Count: {count}
+
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
+
+ Action flows: Client → app1 server → HTTP forward → app2 server →
+ execute
+
+
+ );
+}
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..916c68f38d4
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/FederatedDemo.server.js
@@ -0,0 +1,96 @@
+/**
+ * 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 (app2-remote.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
+ *
+ * TODO (Option 2 - Deep MF Integration for Server Actions):
+ * To invoke app2's server actions via MF (not HTTP forwarding):
+ * 1. The remote 'use server' module would need to register with app1's serverActionRegistry
+ * 2. The action ID would need to be in app1's react-server-actions-manifest.json
+ * 3. Changes required in:
+ * - packages/react-server-dom-webpack/cjs/rsc-server-loader.js
+ * - packages/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js
+ * - packages/react-server-dom-webpack/server.node.js
+ * Currently, cross-app actions use HTTP forwarding (Option 1) instead.
+ */
+
+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 via HTTP forwarding (Option 1)
+ * - 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 packages/react-server-dom-webpack/:
+ * - plugin to merge remote manifests
+ * - loader to handle remote client references
+ */
+export default function FederatedDemo() {
+ 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: Via HTTP forwarding (see FederatedActionDemo)
+
+
+
+ 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..990cf52c0f1
--- /dev/null
+++ b/apps/rsc-demo/packages/app1/src/Note.js
@@ -0,0 +1,66 @@
+/**
+ * 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 noteResponse = await fetch(`http://localhost:4000/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..41b97cbf43c
--- /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..b5845b1ea48
--- /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..585ef81dcf6
--- /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..fea34f07f56
--- /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..990cf52c0f1
--- /dev/null
+++ b/apps/rsc-demo/packages/app2/src/Note.js
@@ -0,0 +1,66 @@
+/**
+ * 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 noteResponse = await fetch(`http://localhost:4000/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..41b97cbf43c
--- /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..b5845b1ea48
--- /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..585ef81dcf6
--- /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..fea34f07f56
--- /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 (
+