Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

</details>

Expand Down Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"@vitejs/plugin-rsc": "catalog:",
"drizzle-kit": "catalog:",
"postcss": "catalog:",
"react-server-dom-webpack": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
Expand Down
1 change: 0 additions & 1 deletion benchmarks/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-server-dom-webpack": "catalog:",
"vinext": "workspace:*"
},
"devDependencies": {
Expand Down
1 change: 0 additions & 1 deletion examples/app-router-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"vinext": "workspace:*",
"@vinext/cloudflare": "workspace:*",
"@vitejs/plugin-rsc": "catalog:",
"react-server-dom-webpack": "catalog:",
"@cloudflare/vite-plugin": "catalog:",
"wrangler": "catalog:"
},
Expand Down
1 change: 0 additions & 1 deletion examples/app-router-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 0 additions & 1 deletion examples/benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 0 additions & 1 deletion examples/fumadocs-docs-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
1 change: 0 additions & 1 deletion examples/hackernews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"vite": "catalog:",
"vinext": "workspace:*",
"@vitejs/plugin-rsc": "catalog:",
"react-server-dom-webpack": "catalog:",
"@cloudflare/vite-plugin": "catalog:",
"wrangler": "catalog:"
},
Expand Down
1 change: 0 additions & 1 deletion examples/nextra-docs-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-server-dom-webpack": "catalog:",
"vinext": "workspace:*"
},
"devDependencies": {
Expand Down
1 change: 0 additions & 1 deletion examples/static-export/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@vitejs/plugin-rsc": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-server-dom-webpack": "catalog:",
"vinext": "workspace:*",
"vite": "catalog:",
"wrangler": "catalog:"
Expand Down
1 change: 0 additions & 1 deletion examples/tpr-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"@vitejs/plugin-rsc": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-server-dom-webpack": "catalog:",
"vite": "catalog:",
"vinext": "workspace:*"
},
Expand Down
1 change: 0 additions & 1 deletion examples/workers-cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"vinext": "workspace:*",
"@vinext/cloudflare": "workspace:*",
"@vitejs/plugin-rsc": "catalog:",
"react-server-dom-webpack": "catalog:",
"@cloudflare/vite-plugin": "catalog:",
"wrangler": "catalog:"
},
Expand Down
9 changes: 2 additions & 7 deletions packages/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,15 @@
"am-i-vibing": "catalog:",
"image-size": "catalog:",
"pathslash": "catalog:",
"react-server-dom-webpack": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@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": {
Expand All @@ -215,9 +213,6 @@
},
"@vitejs/plugin-rsc": {
"optional": true
},
"react-server-dom-webpack": {
"optional": true
}
},
"engines": {
Expand Down
10 changes: 7 additions & 3 deletions packages/vinext/src/build/client-build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,25 +213,29 @@ 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`.
* Derived from the package list so the regex and {@link isRscFrameworkModule}
* 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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vinext/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
10 changes: 5 additions & 5 deletions packages/vinext/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { execFileSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import {
detectPackageManager,
detectPackageManagerProductionCommand,
ensureViteConfigCompatibility,
hasAppDir,
hasViteConfig,
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/vinext/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ declare global {
* inlined Flight payload kind.
* Each `<script>` calls `self.__VINEXT_RSC_CHUNKS__.push(chunk)`.
* The browser RSC entry monkey-patches this array's `push` method to feed a
* `ReadableStream` that is consumed by `react-server-dom-webpack`.
* `ReadableStream` that is consumed by the plugin-rsc Flight runtime.
*/
// oxlint-disable-next-line no-var
var __VINEXT_RSC_CHUNKS__: (string | [3, string])[] | undefined;
Expand Down
Loading
Loading