Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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