UNPKG

vite-plugin-react-server

Version:
145 lines (132 loc) 6.08 kB
import type { Plugin } from "vite"; import { join } from "node:path"; import { lstatSync, readlinkSync, symlinkSync, unlinkSync } from "node:fs"; import { transportPkgDir, transportRoot } from "./transportDir.js"; import { getNodeEnv } from "../config/getNodeEnv.js"; /** * Vite plugin that aliases `react-server-dom-esm/*` imports to the vendored * copy shipped with this plugin. This eliminates the need for consumers to * install `react-server-dom-esm` separately or use patch-package. * * Browser client entries use true ESM files for Rollup tree-shaking. * Server/static entries are CJS and must be loadable via native Node import() * (not eval'd as ESM by Vite's module runner, which lacks require()). * * In dev mode, we ensure the vendored package is reachable from node_modules * so Vite's module runner can externalize and natively import() CJS entries. */ export function vitePluginVendorAlias(): Plugin { return { name: "vite-plugin-react-server:vendor-alias", enforce: "pre", config(config, _env) { const pkg = transportPkgDir; // Pick the dev/prod variant of the browser client from the SAME unified // `mode` that `resolveUserConfig` uses for the React build define — never // from `env.mode` alone. // // `env.mode` (configEnv.mode) is pre-populated with the command default // ("production" for `vite build`, "development" for serve), so it cannot // distinguish explicit user intent. Under `NODE_ENV=development vite // build` it would be "production" and this alias would pull the // PRODUCTION rsdom browser client even though React itself and the dev // `.rsc` are development — yielding the runtime error "Failed to read a // RSC payload created by a development version of React on the server // while using a production version on the client." // // Mirror resolveUserConfig's rule: an explicit `config.mode` (set only // when the user passes `--mode <m>` or authors `mode` in their config) // wins; otherwise mirror NODE_ENV (normalized by getNodeEnv). This keeps // the rsdom browser client's dev/prod choice locked to the same mode that // selects the dev-vs-prod React build. const explicitMode = typeof config.mode === "string" && config.mode !== "" ? config.mode : undefined; const mode = explicitMode ?? getNodeEnv(); const isProd = mode === "production"; return { resolve: { alias: [ // Browser client → ESM for Rollup tree-shaking { find: "react-server-dom-esm/client.browser", replacement: join(pkg, "esm", isProd ? "react-server-dom-esm-client.browser.production.js" : "react-server-dom-esm-client.browser.development.js") }, ], }, }; }, configResolved(config) { // Allow serving vendored files when the plugin is linked or in a monorepo. // Must be done in configResolved to append to the resolved allow list // (setting in config hook can override Vite's defaults). if (config.command === "serve" && config.server?.fs?.allow) { if (!config.server.fs.allow.includes(transportRoot)) { config.server.fs.allow.push(transportRoot); } } // Ensure vendored package is reachable via Node resolution in ALL Vite // contexts (dev server, vitest, SSR workers, custom scripts). // Vite's module runner resolves bare imports via Node — not plugin hooks — // so the package must be in node_modules for CJS entries to work. ensureVendoredPackageLinked(config.root); }, resolveId(source) { if (!source.startsWith("react-server-dom-esm")) return; if (source === "react-server-dom-esm/client.browser") return; // Server/static entries: mark external so the runner/bundler uses native // import() rather than eval(). The resolved path points into the vendored // copy (reachable via symlink in dev, directly in build). if (isServerEntry(source)) { return { id: resolveVendored(source), external: true }; } return resolveVendored(source); }, }; } /** * Ensure `node_modules/react-server-dom-esm` links to the vendored copy. * Only creates a symlink if no real install exists. Safe to call multiple times. */ function ensureVendoredPackageLinked(root?: string): void { const pkg = transportPkgDir; const target = join(root ?? process.cwd(), "node_modules", "react-server-dom-esm"); try { const stat = (() => { try { return lstatSync(target); } catch { return null; } })(); if (stat?.isSymbolicLink()) { // Update symlink if it points elsewhere if (readlinkSync(target) !== pkg) { unlinkSync(target); symlinkSync(pkg, target, "junction"); } } else if (!stat) { // No existing file — create symlink symlinkSync(pkg, target, "junction"); } // If a real directory/file exists (user installed it), leave it alone } catch { // Non-fatal: symlink creation can fail on some systems } } function isServerEntry(source: string): boolean { return source.includes("/server") || source.includes("/static"); } const subpathMap: Record<string, string> = { "react-server-dom-esm": "index.js", "react-server-dom-esm/client": "client.js", "react-server-dom-esm/client.browser": "client.browser.js", "react-server-dom-esm/client.node": "client.node.js", "react-server-dom-esm/server": "server.node.js", "react-server-dom-esm/server.node": "server.node.js", "react-server-dom-esm/static": "static.node.js", "react-server-dom-esm/static.node": "static.node.js", }; function resolveVendored(source: string): string { const file = subpathMap[source]; if (file) return join(transportPkgDir, file); const subpath = source.replace("react-server-dom-esm", ""); return join(transportPkgDir, subpath || "index.js"); }