From c1f2d65df8a660d6c1ffd803891ea3170a1bad6b Mon Sep 17 00:00:00 2001 From: James Date: Fri, 28 Aug 2026 16:02:32 +0100 Subject: [PATCH 1/7] feat(rsc): remove hard RSDW requirement --- .github/workflows/ci.yml | 8 + README.md | 9 +- apps/web/package.json | 1 - benchmarks/vinext/package.json | 1 - examples/app-router-cloudflare/package.json | 1 - examples/app-router-playground/package.json | 1 - examples/benchmarks/package.json | 1 - examples/fumadocs-docs-template/package.json | 1 - examples/hackernews/package.json | 1 - examples/nextra-docs-template/package.json | 1 - examples/static-export/package.json | 1 - examples/tpr-demo/package.json | 1 - examples/workers-cache/package.json | 1 - packages/vinext/package.json | 9 +- .../vinext/src/build/client-build-config.ts | 10 +- packages/vinext/src/check.ts | 2 +- packages/vinext/src/cli.ts | 10 +- packages/vinext/src/global.d.ts | 2 +- packages/vinext/src/index.ts | 264 +++- packages/vinext/src/init.ts | 74 +- .../src/server/app-rsc-redirect-flight.ts | 2 +- packages/vinext/src/utils/project.ts | 11 +- packages/vinext/src/utils/react-version.ts | 226 +++- playwright.config.ts | 40 +- pnpm-lock.yaml | 1162 ++++++++--------- pnpm-workspace.yaml | 6 +- scripts/e2e-deploy.sh | 12 +- tests/app-router-dev-server.test.ts | 11 +- tests/app-router-rsc-plugin.test.ts | 27 +- tests/build-optimization.test.ts | 1 + tests/check.test.ts | 2 +- tests/create-vinext-app.test.ts | 7 +- tests/css-url-assets.test.ts | 2 +- tests/deploy.test.ts | 11 +- tests/e2e/app-router/navigation-flows.spec.ts | 45 + .../app-rsc-isolated-standalone/basic.spec.ts | 27 + .../setup-and-start.mjs | 155 +++ tests/e2e/app-rsdw-override/override.spec.ts | 38 + .../app-with-src/dev-overlay-recovery.spec.ts | 32 +- .../runtime-error-sourcemap.spec.ts | 2 +- tests/fixtures/app-basic/package.json | 1 - tests/fixtures/app-bfcache/package.json | 1 - tests/fixtures/app-cjs-violation/package.json | 1 - .../app-with-src/src/app/action-button.tsx | 26 + .../fixtures/app-with-src/src/app/actions.ts | 5 + tests/fixtures/app-with-src/src/app/page.tsx | 2 + tests/fixtures/app-with-src/vite.config.ts | 30 +- tests/fixtures/cf-app-basic/package.json | 1 - tests/fixtures/cf-sentry-app/package.json | 1 - .../ecosystem/better-auth/package.json | 3 +- .../fixtures/ecosystem/next-intl/package.json | 3 +- .../ecosystem/next-themes/package.json | 3 +- .../next-view-transitions/package.json | 3 +- tests/fixtures/ecosystem/nuqs/package.json | 3 +- tests/fixtures/ecosystem/shadcn/package.json | 3 +- tests/fixtures/esm-externals/package.json | 1 - .../font-google-multiple/package.json | 1 - .../global-not-found-basic/package.json | 1 - .../global-not-found-css-order/package.json | 1 - tests/fixtures/ppr-impact-demo/package.json | 1 - .../root-layout-redirect/package.json | 1 - .../static-export-basepath/package.json | 1 - tests/fixtures/static-export/package.json | 1 - .../use-params-app-pages/package.json | 1 - tests/init.test.ts | 437 ++++++- tests/react-rsc-compatibility.test.ts | 199 +++ 66 files changed, 2096 insertions(+), 853 deletions(-) create mode 100644 tests/e2e/app-rsc-isolated-standalone/basic.spec.ts create mode 100644 tests/e2e/app-rsc-isolated-standalone/setup-and-start.mjs create mode 100644 tests/e2e/app-rsdw-override/override.spec.ts create mode 100644 tests/fixtures/app-with-src/src/app/action-button.tsx create mode 100644 tests/fixtures/app-with-src/src/app/actions.ts create mode 100644 tests/react-rsc-compatibility.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b356d3d15b..b099a3f854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -553,6 +553,14 @@ jobs: label: app-with-src shardIndex: 1 shardTotal: 1 + - project: app-rsdw-override-prod + label: app-rsdw-override-prod + shardIndex: 1 + shardTotal: 1 + - project: app-rsc-isolated-standalone + label: app-rsc-isolated-standalone + shardIndex: 1 + shardTotal: 1 - project: standalone-output label: standalone-output shardIndex: 1 diff --git a/README.md b/README.md index 28740f63f0..ab08d01a1c 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,13 @@ npm install -D vite @vitejs/plugin-react If you're using the App Router, also install: ```bash -npm install react-server-dom-webpack npm install -D @vitejs/plugin-rsc ``` +`@vitejs/plugin-rsc` includes the React Flight runtime used by default. Projects +that intentionally use a different React release channel can install the +matching `react-server-dom-webpack` version as an explicit runtime override. + Replace `next` with `vinext` in your scripts: ```json @@ -768,7 +771,7 @@ Analysis of the build output shows two main factors: 1. **Tree-shaking**: Vite/Rolldown produces a smaller React+ReactDOM bundle than Next.js/Turbopack. Rolldown's more aggressive dead-code elimination accounts for roughly half the overall difference. 2. **Framework overhead**: Next.js ships more client-side infrastructure (router, Turbopack runtime loader, prefetching, error handling) than vinext's lighter client runtime. -Both frameworks ship the same app code and the same RSC client runtime (`react-server-dom-webpack`). The difference is in how much of React's internals survive tree-shaking and how much framework plumbing each tool adds. +Both frameworks ship the same app code and the React Flight client runtime. Vinext receives that runtime from `@vitejs/plugin-rsc`; Next.js carries its own compiled copy. The difference is in how much of React's internals survive tree-shaking and how much framework plumbing each tool adds. @@ -892,7 +895,7 @@ Or add it to your `package.json` as a file dependency: } ``` -vinext has peer dependencies on `react ^19.2.6`, `react-dom ^19.2.6`, `react-server-dom-webpack ^19.2.6`, and `vite ^8.0.0`. Then replace `next` with `vinext` in your scripts and run as normal. +vinext has peer dependencies on `react ^19.2.6`, `react-dom ^19.2.6`, and `vite ^8.0.0`. App Router projects also use the optional `@vitejs/plugin-rsc` peer, which provides the default Flight runtime. Its vendored runtime requires React 19.2.8 or newer; `vinext init` upgrades older stable React versions unless the project declares a matching `react-server-dom-webpack` override. Then replace `next` with `vinext` in your scripts and run as normal. ## Contributing diff --git a/apps/web/package.json b/apps/web/package.json index 521fba6a1e..c3eed6162d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,6 @@ "@vitejs/plugin-rsc": "catalog:", "drizzle-kit": "catalog:", "postcss": "catalog:", - "react-server-dom-webpack": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", diff --git a/benchmarks/vinext/package.json b/benchmarks/vinext/package.json index d041fbc637..7a115e1e87 100644 --- a/benchmarks/vinext/package.json +++ b/benchmarks/vinext/package.json @@ -11,7 +11,6 @@ "@vitejs/plugin-react": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-server-dom-webpack": "catalog:", "vinext": "workspace:*" }, "devDependencies": { diff --git a/examples/app-router-cloudflare/package.json b/examples/app-router-cloudflare/package.json index 79257b252c..d79dd355dd 100644 --- a/examples/app-router-cloudflare/package.json +++ b/examples/app-router-cloudflare/package.json @@ -15,7 +15,6 @@ "vinext": "workspace:*", "@vinext/cloudflare": "workspace:*", "@vitejs/plugin-rsc": "catalog:", - "react-server-dom-webpack": "catalog:", "@cloudflare/vite-plugin": "catalog:", "wrangler": "catalog:" }, diff --git a/examples/app-router-playground/package.json b/examples/app-router-playground/package.json index cf78f99c64..938a909946 100644 --- a/examples/app-router-playground/package.json +++ b/examples/app-router-playground/package.json @@ -20,7 +20,6 @@ "ms": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-server-dom-webpack": "catalog:", "recma-codehike": "catalog:", "remark-codehike": "catalog:", "use-count-up": "catalog:", diff --git a/examples/benchmarks/package.json b/examples/benchmarks/package.json index 0bd89ad778..f322a76ecb 100644 --- a/examples/benchmarks/package.json +++ b/examples/benchmarks/package.json @@ -13,7 +13,6 @@ "vite": "catalog:", "vinext": "workspace:*", "@vitejs/plugin-rsc": "catalog:", - "react-server-dom-webpack": "catalog:", "@cloudflare/vite-plugin": "catalog:", "@cloudflare/kumo": "catalog:", "@phosphor-icons/react": "catalog:", diff --git a/examples/fumadocs-docs-template/package.json b/examples/fumadocs-docs-template/package.json index a9c469af77..8127d791df 100644 --- a/examples/fumadocs-docs-template/package.json +++ b/examples/fumadocs-docs-template/package.json @@ -31,7 +31,6 @@ "@vitejs/plugin-react": "catalog:", "@vitejs/plugin-rsc": "catalog:", "postcss": "catalog:", - "react-server-dom-webpack": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", "vinext": "workspace:*", diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 8a7ddb2f3d..cacd1e8846 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -17,7 +17,6 @@ "vite": "catalog:", "vinext": "workspace:*", "@vitejs/plugin-rsc": "catalog:", - "react-server-dom-webpack": "catalog:", "@cloudflare/vite-plugin": "catalog:", "wrangler": "catalog:" }, diff --git a/examples/nextra-docs-template/package.json b/examples/nextra-docs-template/package.json index 29da87c08c..aab4b6d2c6 100644 --- a/examples/nextra-docs-template/package.json +++ b/examples/nextra-docs-template/package.json @@ -11,7 +11,6 @@ "@vitejs/plugin-react": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-server-dom-webpack": "catalog:", "vinext": "workspace:*" }, "devDependencies": { diff --git a/examples/static-export/package.json b/examples/static-export/package.json index 5eff90566f..48c8372018 100644 --- a/examples/static-export/package.json +++ b/examples/static-export/package.json @@ -12,7 +12,6 @@ "@vitejs/plugin-rsc": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-server-dom-webpack": "catalog:", "vinext": "workspace:*", "vite": "catalog:", "wrangler": "catalog:" diff --git a/examples/tpr-demo/package.json b/examples/tpr-demo/package.json index 3bd316720d..2f99e5f342 100644 --- a/examples/tpr-demo/package.json +++ b/examples/tpr-demo/package.json @@ -14,7 +14,6 @@ "@vitejs/plugin-rsc": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-server-dom-webpack": "catalog:", "vite": "catalog:", "vinext": "workspace:*" }, diff --git a/examples/workers-cache/package.json b/examples/workers-cache/package.json index 43f70ab057..13e00272e8 100644 --- a/examples/workers-cache/package.json +++ b/examples/workers-cache/package.json @@ -15,7 +15,6 @@ "vinext": "workspace:*", "@vinext/cloudflare": "workspace:*", "@vitejs/plugin-rsc": "catalog:", - "react-server-dom-webpack": "catalog:", "@cloudflare/vite-plugin": "catalog:", "wrangler": "catalog:" }, diff --git a/packages/vinext/package.json b/packages/vinext/package.json index 3077902573..10911575f1 100644 --- a/packages/vinext/package.json +++ b/packages/vinext/package.json @@ -196,7 +196,6 @@ "am-i-vibing": "catalog:", "image-size": "catalog:", "pathslash": "catalog:", - "react-server-dom-webpack": "catalog:", "vite": "catalog:", "vite-plus": "catalog:" }, @@ -204,9 +203,8 @@ "@mdx-js/rollup": "^3.0.0", "@vitejs/plugin-react": "^5.1.4 || ^6.0.0", "@vitejs/plugin-rsc": "^0.5.34", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-server-dom-webpack": "^19.2.6", + "react": "^19.2.6 || >=19.3.0-0 <19.4.0 || >=0.0.0-0 <0.0.1", + "react-dom": "^19.2.6 || >=19.3.0-0 <19.4.0 || >=0.0.0-0 <0.0.1", "vite": "^8.0.0" }, "peerDependenciesMeta": { @@ -215,9 +213,6 @@ }, "@vitejs/plugin-rsc": { "optional": true - }, - "react-server-dom-webpack": { - "optional": true } }, "engines": { diff --git a/packages/vinext/src/build/client-build-config.ts b/packages/vinext/src/build/client-build-config.ts index 4d90db2552..43affb1485 100644 --- a/packages/vinext/src/build/client-build-config.ts +++ b/packages/vinext/src/build/client-build-config.ts @@ -213,13 +213,14 @@ export function createClientCodeSplittingConfig( * Splitting React into its own (CSS-free) chunk means global-not-found imports * the framework chunk instead of the layout-bearing entry chunk, so it no * longer inherits the root layout's CSS. The match list mirrors the client - * build's `framework` chunk, plus `react-server-dom-webpack` for the RSC flight - * runtime that the server environment bundles. + * build's `framework` chunk, plus both plugin-rsc's vendored Flight runtime and + * its explicit `react-server-dom-webpack` override path. * * Uses `[\\/]` rather than `/` for the path separator so it matches on Windows * too, per the rolldown `codeSplitting` docs. */ const FRAMEWORK_PACKAGES = ["react", "react-dom", "scheduler", "react-server-dom-webpack"] as const; +const VENDORED_RSC_RUNTIME_PATH = "@vitejs/plugin-rsc/dist/vendor/react-server-dom/"; /** * Regex matching any {@link FRAMEWORK_PACKAGES} package inside `node_modules`. @@ -227,11 +228,14 @@ const FRAMEWORK_PACKAGES = ["react", "react-dom", "scheduler", "react-server-dom * predicate can't drift. */ export const RSC_FRAMEWORK_CHUNK_TEST = new RegExp( - `[\\\\/]node_modules[\\\\/](${FRAMEWORK_PACKAGES.join("|")})[\\\\/]`, + `[\\\\/]node_modules[\\\\/](?:${FRAMEWORK_PACKAGES.join("|")})[\\\\/]|` + + `[\\\\/]node_modules[\\\\/]@vitejs[\\\\/]plugin-rsc[\\\\/]dist[\\\\/]vendor[\\\\/]react-server-dom[\\\\/]`, ); export function isRscFrameworkModule(id: string): boolean { if (!id.includes("node_modules")) return false; + const normalizedId = toSlash(id); + if (normalizedId.includes(`/node_modules/${VENDORED_RSC_RUNTIME_PATH}`)) return true; const pkg = getPackageName(id); return pkg !== null && (FRAMEWORK_PACKAGES as readonly string[]).includes(pkg); } diff --git a/packages/vinext/src/check.ts b/packages/vinext/src/check.ts index c7a3f48ae1..fab32d87e3 100644 --- a/packages/vinext/src/check.ts +++ b/packages/vinext/src/check.ts @@ -1318,7 +1318,7 @@ export function formatReport(result: CheckResult, opts?: { calledFromInit?: bool lines.push(" Or manually:"); lines.push(` 1. Add \x1b[36m"type": "module"\x1b[0m to package.json`); lines.push( - ` 2. Install: \x1b[36m${detectPackageManager(process.cwd())} vinext vite @vitejs/plugin-react${hasAppRouter ? " @vitejs/plugin-rsc react-server-dom-webpack" : ""}\x1b[0m`, + ` 2. Install: \x1b[36m${detectPackageManager(process.cwd())} vinext vite @vitejs/plugin-react${hasAppRouter ? " @vitejs/plugin-rsc" : ""}\x1b[0m`, ); lines.push(` 3. Create vite.config.ts (see docs)`); lines.push(` 4. Run: \x1b[36mnpx vite dev\x1b[0m`); diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 831f826266..f77c4e2076 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -24,6 +24,7 @@ import { execFileSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { detectPackageManager, + detectPackageManagerProductionCommand, ensureViteConfigCompatibility, hasAppDir, hasViteConfig, @@ -577,15 +578,14 @@ async function buildApp() { ? vite.createLogger("info", { allowClearScreen: false }) : createBuildLogger(vite); - // For App Router: upgrade React if needed for react-server-dom-webpack compatibility. - // Without this, builds with older React versions can produce a Worker that crashes at - // runtime with "Cannot read properties of undefined (reading 'moduleMap')". + // Keep App Router's React pair compatible with its Flight runtime. Stable + // React uses plugin-rsc's vendor; prereleases need a matching RSDW override. if (isApp) { const reactUpgrade = getReactUpgradeDeps(process.cwd()); if (reactUpgrade.length > 0) { - const installCmd = detectPackageManager(process.cwd()).replace(/ -D$/, ""); + const installCmd = detectPackageManagerProductionCommand(process.cwd()); const [pm, ...pmArgs] = installCmd.split(" "); - console.log(" Upgrading React for RSC compatibility..."); + console.log(" Installing dependencies for RSC compatibility..."); execFileSync(pm, [...pmArgs, ...reactUpgrade], { cwd: process.cwd(), stdio: "inherit", diff --git a/packages/vinext/src/global.d.ts b/packages/vinext/src/global.d.ts index bcd1b8164a..d5fc751e25 100644 --- a/packages/vinext/src/global.d.ts +++ b/packages/vinext/src/global.d.ts @@ -224,7 +224,7 @@ declare global { * inlined Flight payload kind. * Each `