diff --git a/docs/migration-vite-rsc-to-vitejs-plugin-rsc.md b/docs/migration-vite-rsc-to-vitejs-plugin-rsc.md new file mode 100644 index 000000000..9f24c1c98 --- /dev/null +++ b/docs/migration-vite-rsc-to-vitejs-plugin-rsc.md @@ -0,0 +1,149 @@ +# Migration Plan: @hiogawa/vite-rsc → @vitejs/plugin-rsc + +## Status: In Progress + +This document tracks the migration of `@hiogawa/react-server` from `@hiogawa/vite-rsc` to `@vitejs/plugin-rsc`. + +## Completed Changes + +### 1. Dependency Update +- Replaced `@hiogawa/vite-rsc@^0.4.9` with `@vitejs/plugin-rsc@^0.5.9` + +### 2. Import Path Updates + +| Old Path | New Path | +|----------|----------| +| `@hiogawa/vite-rsc/react/rsc` | `@vitejs/plugin-rsc/rsc` | +| `@hiogawa/vite-rsc/react/ssr` | `@vitejs/plugin-rsc/ssr` | +| `@hiogawa/vite-rsc/react/browser` | `@vitejs/plugin-rsc/browser` | +| `@hiogawa/vite-rsc/core/plugin` | `@vitejs/plugin-rsc` (default export) | + +### 3. Removed Custom Transform Plugins + +The following plugins were removed since `@vitejs/plugin-rsc` handles transforms: +- `vitePluginServerUseClient` - "use client" transform in RSC env +- `vitePluginServerUseServer` - "use server" transform in RSC env +- `vitePluginClientUseClient` - client reference handling +- `vitePluginClientUseServer` - "use server" transform in client env + +### 4. Removed Manual Initialization + +`@vitejs/plugin-rsc` auto-initializes `setRequireModule` when imported: +- Removed `initializeReactServer()` from `entry/server.tsx` +- Removed `initializeReactClientSsr()` from `entry/ssr.tsx` +- Removed `initializeReactClientBrowser()` from `entry/browser.tsx` +- Simplified `features/client-component/*.tsx` and `features/server-action/*.tsx` + +### 5. Simplified PluginStateManager + +Removed unused properties: +- `clientReferenceMap` - was populated by removed transform plugins +- `serverReferenceMap` - was populated by removed transform plugins +- `nodeModules.useClient` - was used for node_modules client references +- `serverIds` - was used for HMR tracking +- `shouldReloadRsc()` - depended on removed maps +- `normalizeReferenceId()` - was used by transform plugins +- `prepareDestinationManifest` - was used for client reference preloading + +### 6. Dropped Client Reference Preloading + +Native RSC doesn't support client reference preloading. Removed: +- `prepareDestinationManifest` generation in `router/plugin.ts` +- Client reference CSS collection in `assets/plugin.ts` + +TODO: https://github.com/wakujs/waku/issues/1656 + +### 7. Simplified HMR Logic + +Removed custom RSC HMR event sending since `@vitejs/plugin-rsc` handles `rsc:update` events. + +--- + +### 8. Removed Framework Plugins Now Handled by @vitejs/plugin-rsc + +The following framework plugins were removed since `@vitejs/plugin-rsc` handles their functionality: + +| Plugin | Reason | +|--------|--------| +| `rscParentPlugin` | RSC environment setup handled by plugin (replaced with `frameworkConfigPlugin`) | +| `buildOrchestrationPlugin` | Build orchestration handled by plugin | +| `inject-async-local-storage` | Async local storage injection handled by plugin | +| `validateImportPlugin` | Import validation handled by plugin | +| `serverDepsConfigPlugin()` | Server deps config handled by plugin | +| `serverAssetsPluginServer` | Server assets handling handled by plugin | +| `virtual:react-server-build` | Replaced with `import.meta.viteRsc.loadModule("rsc", "index")` | + +### 9. Deleted Plugin Files + +- `packages/react-server/src/features/client-component/plugin.ts` +- `packages/react-server/src/features/server-action/plugin.tsx` + +### 10. Updated Build Phase Detection + +Replaced `manager.buildType` checks with `this.environment.name` checks: +- `router/plugin.ts` - uses `this.environment.name === "rsc"`, `"client"`, `"ssr"` +- `prerender/plugin.ts` - uses `this.environment.name === "ssr"` +- `assets/plugin.ts` - uses `this.environment?.mode === "dev"` + +--- + +## Remaining Work + +### E2E Testing Required + +E2E tests need to verify: +- Dev server works +- Production build works +- HMR works (both server and client components) +- Server actions work +- SSR streaming works +- File-based routing works + +--- + +## Architecture Comparison + +### Before (with @hiogawa/vite-rsc) + +``` +@hiogawa/react-server +├── Plugin layer +│ ├── rscCore() from vite-rsc - core RSC environment +│ ├── vitePluginFindSourceMapURL() - error overlay +│ ├── vitePluginServerUseClient - "use client" transform +│ ├── vitePluginServerUseServer - "use server" transform +│ ├── vitePluginClientUseClient - client reference virtual module +│ ├── vitePluginClientUseServer - "use server" in client +│ ├── buildOrchestrationPlugin - 4-phase build +│ └── Framework plugins (router, prerender, assets, etc.) +├── Entry points +│ ├── Manual setRequireModule initialization +│ └── Framework-specific routing/rendering +└── Framework features + └── File-based routing, metadata, error handling, etc. +``` + +### After (with @vitejs/plugin-rsc) + +``` +@hiogawa/react-server +├── Plugin layer +│ ├── rsc() from @vitejs/plugin-rsc - handles ALL RSC bundling +│ ├── frameworkConfigPlugin - framework-specific config (outDir, entries) +│ └── Framework plugins (router, prerender, assets, etc.) +├── Entry points +│ ├── Auto-initialized by plugin imports +│ ├── import.meta.viteRsc.loadModule for cross-environment imports +│ └── Framework-specific routing/rendering +└── Framework features + └── File-based routing, metadata, error handling, etc. +``` + +--- + +## Reference + +- `@vitejs/plugin-rsc` source: `/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc` +- Starter example: `/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/starter` +- SSG/Prerender example: `/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/ssg` +- Plugin API: `getPluginApi(config)` returns `{ manager: RscPluginManager }` diff --git a/packages/react-server/package.json b/packages/react-server/package.json index 8de8f4097..593cf3909 100644 --- a/packages/react-server/package.json +++ b/packages/react-server/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@hiogawa/transforms": "workspace:*", - "@hiogawa/vite-rsc": "^0.4.9", + "@vitejs/plugin-rsc": "^0.5.9", "es-module-lexer": "^1.6.0", "fast-glob": "^3.3.3", "vitefu": "^1.0.5" diff --git a/packages/react-server/src/entry/browser.tsx b/packages/react-server/src/entry/browser.tsx index f9a6c2801..07fb599e9 100644 --- a/packages/react-server/src/entry/browser.tsx +++ b/packages/react-server/src/entry/browser.tsx @@ -1,11 +1,10 @@ import * as virtualClientRoutes from "virtual:client-routes"; import { createDebug, memoize, tinyassert } from "@hiogawa/utils"; -import * as ReactClient from "@hiogawa/vite-rsc/react/browser"; import type { RouterHistory } from "@tanstack/history"; +import * as ReactClient from "@vitejs/plugin-rsc/browser"; import React from "react"; import ReactDOMClient from "react-dom/client"; import { rscStream } from "rsc-html-stream/client"; -import { initializeReactClientBrowser } from "../features/client-component/browser"; import { ErrorBoundary } from "../features/error/error-boundary"; import { DefaultGlobalErrorPage } from "../features/error/global-error"; import { @@ -34,8 +33,6 @@ import { createError } from "../server"; const debug = createDebug("react-server:browser"); async function start() { - initializeReactClientBrowser(); - const history = createEncodedBrowserHistory(); const router = new Router(history); diff --git a/packages/react-server/src/entry/server.tsx b/packages/react-server/src/entry/server.tsx index 92ca8fa85..8726625ce 100644 --- a/packages/react-server/src/entry/server.tsx +++ b/packages/react-server/src/entry/server.tsx @@ -1,6 +1,6 @@ import * as serverRoutes from "virtual:server-routes"; import { createDebug, objectPick, objectPickBy } from "@hiogawa/utils"; -import * as ReactServer from "@hiogawa/vite-rsc/react/rsc"; +import * as ReactServer from "@vitejs/plugin-rsc/rsc"; import type { RenderToReadableStreamOptions } from "react-dom/server"; import { DefaultNotFoundPage } from "../features/error/not-found"; import { @@ -23,10 +23,7 @@ import { createActionRedirectResponse, createFlightRedirectResponse, } from "../features/server-action/redirect"; -import { - type ActionResult, - initializeReactServer, -} from "../features/server-action/server"; +import type { ActionResult } from "../features/server-action/server"; import { unwrapStreamRequest } from "../features/server-component/utils"; const debug = createDebug("react-server:rsc"); @@ -57,8 +54,6 @@ export type ReactServerHandlerResult = | ReactServerHandlerStreamResult; export const handler: ReactServerHandler = async (ctx) => { - initializeReactServer(); - const handled = handleTrailingSlash(new URL(ctx.request.url)); if (handled) return handled; diff --git a/packages/react-server/src/entry/ssr.tsx b/packages/react-server/src/entry/ssr.tsx index 6c3fa914c..867d069aa 100644 --- a/packages/react-server/src/entry/ssr.tsx +++ b/packages/react-server/src/entry/ssr.tsx @@ -1,12 +1,11 @@ import { createDebug, tinyassert } from "@hiogawa/utils"; -import * as ReactClient from "@hiogawa/vite-rsc/react/ssr"; import { createMemoryHistory } from "@tanstack/history"; +import * as ReactClient from "@vitejs/plugin-rsc/ssr"; import ReactDOMServer from "react-dom/server.edge"; import { injectRSCPayload } from "rsc-html-stream/server"; import type { DevEnvironment, EnvironmentModuleNode } from "vite"; import type { SsrAssetsType } from "../features/assets/plugin"; import { DEV_SSR_CSS, SERVER_CSS_PROXY } from "../features/assets/shared"; -import { initializeReactClientSsr } from "../features/client-component/ssr"; import { DEFAULT_ERROR_CONTEXT, getErrorContext, @@ -75,7 +74,10 @@ export async function importReactServer(): Promise { if (import.meta.env.DEV) { return $__global.dev.reactServerRunner.import(ENTRY_SERVER_WRAPPER); } else { - return import("virtual:react-server-build" as string); + return import.meta.viteRsc.loadModule( + "rsc", + "index", + ); } } @@ -84,8 +86,6 @@ export async function renderHtml( result: ReactServerHandlerStreamResult, opitons?: { prerender?: boolean }, ) { - initializeReactClientSsr(); - // // ssr root // diff --git a/packages/react-server/src/features/assets/plugin.ts b/packages/react-server/src/features/assets/plugin.ts index b3a6bcb36..82b039ea0 100644 --- a/packages/react-server/src/features/assets/plugin.ts +++ b/packages/react-server/src/features/assets/plugin.ts @@ -23,9 +23,9 @@ export function vitePluginServerAssets({ entryServer: string; }): Plugin[] { return [ - createVirtualPlugin("ssr-assets", async () => { + createVirtualPlugin("ssr-assets", async function () { // dev - if (!manager.buildType) { + if (this.environment?.mode === "dev") { // extract injected by plugins let { head } = await getIndexHtmlTransform($__global.dev.server); @@ -54,44 +54,28 @@ export function vitePluginServerAssets({ return `export default ${JSON.stringify(result)}`; } - // build - if (manager.buildType === "ssr") { - // TODO: (refactor) use RouteManifest? - const manifest: Manifest = JSON.parse( - await fs.promises.readFile( - path.join(manager.outDir, "client", ".vite", "manifest.json"), - "utf-8", - ), - ); - const entry = manifest[ENTRY_BROWSER_WRAPPER]; - tinyassert(entry); - const css = [ - ...(entry.css ?? []), - ...manager.serverAssets.filter((file) => file.endsWith(".css")), - ]; - const head = [ - ...css.map((href) => ``), - ].join("\n"); - const result: SsrAssetsType = { - bootstrapModules: [`/${entry.file}`], - head, - }; - return `export default ${JSON.stringify(result)}`; - } - - tinyassert(false); + // build - use @vitejs/plugin-rsc assets manifest + const manifest: Manifest = JSON.parse( + await fs.promises.readFile( + path.join(manager.outDir, "client", ".vite", "manifest.json"), + "utf-8", + ), + ); + const entry = manifest[ENTRY_BROWSER_WRAPPER]; + tinyassert(entry); + const css = entry.css ?? []; + const head = [ + ...css.map((href) => ``), + ].join("\n"); + const result: SsrAssetsType = { + bootstrapModules: [`/${entry.file}`], + head, + }; + return `export default ${JSON.stringify(result)}`; }), - createVirtualPlugin(DEV_SSR_CSS.split(":")[1]!, async () => { - tinyassert(!manager.buildType); - // normalize it again to workaround inconsistent hmr path issue - // https://github.com/hi-ogawa/reproductions/tree/main/vite-v6-hmr-path - const root = manager.config.root; - const clientReferences = [...manager.clientReferenceMap.keys()].map( - (file) => { - return file.startsWith(root) ? file.slice(root.length) : file; - }, - ); + createVirtualPlugin(DEV_SSR_CSS.split(":")[1]!, async function () { + tinyassert(this.environment?.mode === "dev"); const serverStyleUrls = await collectStyleUrls( $__global.dev.server.environments["rsc"]!, { @@ -101,12 +85,7 @@ export function vitePluginServerAssets({ const clientStyleUrls = await collectStyleUrls( $__global.dev.server.environments.client, { - entries: [ - entryBrowser, - "virtual:client-routes", - // TODO: dev should also use RouteManifest to manage client css - ...clientReferences, - ], + entries: [entryBrowser, "virtual:client-routes"], }, ); const styles = await Promise.all([ @@ -124,10 +103,9 @@ export function vitePluginServerAssets({ return styles.join("\n\n"); }), - createVirtualPlugin(SERVER_CSS_PROXY.split(":")[1]!, async () => { + createVirtualPlugin(SERVER_CSS_PROXY.split(":")[1]!, async function () { // virtual module to proxy css imports from react server to client - // TODO: invalidate + full reload when add/remove css file? - if (!manager.buildType) { + if (this.environment?.mode === "dev") { const urls = await collectStyleUrls( $__global.dev.server.environments["rsc"]!, { @@ -138,56 +116,9 @@ export function vitePluginServerAssets({ // ensure hmr boundary since css module doesn't have `import.meta.hot.accept` return code + `if (import.meta.hot) { import.meta.hot.accept() }`; } - if (manager.buildType === "browser") { - return "export {}"; - } - tinyassert(false); + // build + return "export {}"; }), - - { - name: vitePluginServerAssets.name + ":copy-build", - async writeBundle() { - if (manager.buildType === "browser") { - for (const file of manager.serverAssets) { - await fs.promises.cp( - path.join(manager.outDir, "rsc", file), - path.join(manager.outDir, "client", file), - ); - } - } - }, - }, - ]; -} - -export function serverAssetsPluginServer({ - manager, -}: { manager: PluginStateManager }): Plugin[] { - // 0. track server assets during server build (this plugin) - // 1. copy all server assets to browser build (copy-build plugin) - // 2. out of those, inject links automatically (ssr-assets virtual plugin) - // - .css => stylesheet - // - .woff => font preload - - // TODO - // - css ordering? - // - css code split by route? - - return [ - { - name: serverAssetsPluginServer.name + ":build", - apply: "build", - generateBundle(_options, bundle) { - if (manager.buildType !== "server") { - return; - } - for (const [_k, v] of Object.entries(bundle)) { - if (v.type === "asset") { - manager.serverAssets.push(v.fileName); - } - } - }, - }, ]; } diff --git a/packages/react-server/src/features/client-component/browser.tsx b/packages/react-server/src/features/client-component/browser.tsx index 715a8dd0e..1f745e1ef 100644 --- a/packages/react-server/src/features/client-component/browser.tsx +++ b/packages/react-server/src/features/client-component/browser.tsx @@ -1,20 +1,2 @@ -import { tinyassert } from "@hiogawa/utils"; -import * as ReactClient from "@hiogawa/vite-rsc/react/browser"; - -// @ts-ignore -import clientReferences from "virtual:client-references"; - -async function importWrapper(id: string) { - if (import.meta.env.DEV) { - // @ts-ignore see patch-browser-raw-import plugin - return __vite_rsc_raw_import__(id); - } else { - const dynImport = clientReferences[id]; - tinyassert(dynImport, `client reference not found '${id}'`); - return dynImport(); - } -} - -export function initializeReactClientBrowser() { - ReactClient.setRequireModule({ load: importWrapper }); -} +// @vitejs/plugin-rsc/browser auto-initializes client references +// No initialization needed - just re-export for backwards compatibility diff --git a/packages/react-server/src/features/client-component/plugin.ts b/packages/react-server/src/features/client-component/plugin.ts deleted file mode 100644 index 57ca757b3..000000000 --- a/packages/react-server/src/features/client-component/plugin.ts +++ /dev/null @@ -1,302 +0,0 @@ -import fs from "node:fs"; -import { - getExportNames, - transformDirectiveProxyExport, -} from "@hiogawa/transforms"; -import { createDebug, memoize, tinyassert } from "@hiogawa/utils"; -import { normalizeViteImportAnalysisUrl } from "@hiogawa/vite-rsc/vite-utils"; -import { type Plugin, parseAstAsync } from "vite"; -import type { PluginStateManager } from "../../plugin"; -import { - type CustomModuleMeta, - USE_CLIENT, - USE_CLIENT_RE, - applyPluginToClient, - applyPluginToServer, - createVirtualPlugin, -} from "../../plugin/utils"; - -const debug = createDebug("react-server:plugin:use-client"); - -/* -transform "use client" directive on react server code - -[input] -"use client" -export function Counter() {} - -[output] (react-server) -import { registerClientReference as $$register } from "...runtime..." -export const Counter = $$register("", "Counter"); -*/ -export function vitePluginServerUseClient({ - manager, - runtimePath, -}: { - manager: PluginStateManager; - runtimePath: string; -}): Plugin[] { - // TODO: - // eventually we should try entirely virtual module approach for client reference (not only node_modules) - // so that we can delegate precise resolution (e.g. `?v=` deps optimization hash, `?t=` hmr timestamp) - // to actual client (browser, ssr) environment instead of faking out things on RSC module graph - - // intercept Vite's node resolve to virtualize "use client" in node_modules - const useClientExternalPlugin: Plugin = { - name: "server-virtual-use-client-node-modules", - enforce: "pre", // "pre" to steal Vite's node resolve - apply: "serve", - applyToEnvironment: applyPluginToServer, - resolveId: memoize(async function (this, source, importer) { - if ( - source[0] !== "." && - source[0] !== "/" && - !source.startsWith("virtual") && - !source.startsWith("\0virtual") - ) { - const resolved = await this.resolve(source, importer, { - skipSelf: true, - }); - debug("[rsc.use-client-node-modules.resolveId]", { - source, - resolved, - }); - if (resolved && resolved.id.includes("/node_modules/")) { - const [id] = resolved.id.split("?v="); - tinyassert(id); - const code = await fs.promises.readFile(id!, "utf-8"); - if (code.match(USE_CLIENT_RE)) { - manager.nodeModules.useClient.set(source, { - id, - exportNames: new Set(), - }); - return `\0${VIRTUAL_PREFIX}${source}`; - } - } - return; - } - return; - } satisfies Plugin["resolveId"]), - async load(id, _options) { - if (id.startsWith(`\0${VIRTUAL_PREFIX}`)) { - const source = id.slice(`\0${VIRTUAL_PREFIX}`.length); - const meta = manager.nodeModules.useClient.get(source); - tinyassert(meta); - // node_modules is already transpiled so we can parse it right away - const code = await fs.promises.readFile(meta.id, "utf-8"); - const ast = await parseAstAsync(code); - meta.exportNames = new Set( - getExportNames(ast, { ignoreExportAllDeclaration: true }).exportNames, - ); - // we need to transform to client reference directly - // otherwise `soruce` will be resolved infinitely by recursion - id = wrapId(id); - const result = transformDirectiveProxyExport(ast, { - directive: USE_CLIENT, - runtime: (name) => - `$$ReactServer.registerClientReference({}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, - ignoreExportAllDeclaration: true, - }); - const output = result?.output; - tinyassert(output); - output.prepend(`import * as $$ReactServer from "${runtimePath}";\n`); - const outputCode = output.toString(); - debug("[rsc.use-client-node-modules.load]", { - source, - meta, - id, - outputCode, - }); - return outputCode; - } - return; - }, - }; - - function normalizeId(id: string) { - if (!manager.buildType) { - // normalize client reference during dev - // to align with Vite's import analysis - tinyassert(manager.server); - return normalizeViteImportAnalysisUrl( - manager.server.environments.client, - id, - ); - } else { - // obfuscate reference - return manager.normalizeReferenceId(id); - } - } - - const useClientPlugin: Plugin = { - name: vitePluginServerUseClient.name, - applyToEnvironment: applyPluginToServer, - async transform(code, id, _options) { - // when using external library's server component includes client reference, - // it will end up here with deps optimization hash `?v=` resolved by server module graph. - // this is not entirely free from double module issue, - // but it allows handling simple server-client-mixed package such as react-tweet. - // cf. https://github.com/hi-ogawa/vite-plugins/issues/379 - if (!manager.buildType && id.includes("?v=")) { - id = id.split("?v=")[0]!; - } - manager.serverIds.add(id); - manager.clientReferenceMap.delete(id); - if (!code.includes(USE_CLIENT)) { - return; - } - const clientId = normalizeId(id); - const ast = await parseAstAsync(code); - const result = transformDirectiveProxyExport(ast, { - directive: USE_CLIENT, - runtime: (name) => - `$$ReactServer.registerClientReference({}, ${JSON.stringify(clientId)}, ${JSON.stringify(name)})`, - ignoreExportAllDeclaration: true, - }); - const output = result?.output; - if (!output) { - return; - } - output.prepend(`import * as $$ReactServer from "${runtimePath}";\n`); - manager.clientReferenceMap.set(id, clientId); - if (manager.buildType === "scan") { - // to discover server references imported only by client - // we keep code as is and continue crawling - return; - } - return { - code: output.toString(), - map: output.generateMap(), - meta: { - $$rsc: { - type: "client", - }, - } satisfies CustomModuleMeta, - }; - }, - }; - - let esModuleLexer: typeof import("es-module-lexer"); - const scanStripPlugin: Plugin = { - name: vitePluginServerUseClient + ":strip-strip", - apply: "build", - enforce: "post", - applyToEnvironment: applyPluginToServer, - async buildStart() { - if (manager.buildType !== "scan") return; - - esModuleLexer = await import("es-module-lexer"); - await esModuleLexer.init; - }, - transform(code, _id, _options) { - if (manager.buildType !== "scan") return; - - // During server scan, we strip every modules to only keep imports/exports - // import "x" - // import "y" - // export const f = undefined; - // export const g = undefined; - - // emptify all exports while keeping import statements as side effects - const [imports, exports] = esModuleLexer.parse(code); - const output = [ - imports.map((e) => e.n && `import ${JSON.stringify(e.n)};\n`), - exports.map((e) => - e.n === "default" - ? `export default undefined;\n` - : `export const ${e.n} = undefined;\n`, - ), - ] - .flat() - .filter(Boolean) - .join(""); - return { code: output, map: null }; - }, - }; - - return [ - useClientExternalPlugin, - useClientPlugin, - scanStripPlugin, - patchBrowserRawImport(), - ]; -} - -function wrapId(id: string) { - return id.startsWith(`/@id`) ? id : `/@id/${id.replace("\0", "__x00__")}`; -} - -const VIRTUAL_PREFIX = "virtual:use-client-node-module/"; - -export function vitePluginClientUseClient({ - manager, -}: { - manager: PluginStateManager; -}): Plugin[] { - const devExternalPlugin: Plugin = { - name: vitePluginClientUseClient.name + ":dev-external", - apply: "serve", - applyToEnvironment: applyPluginToClient, - resolveId(source, _importer, _options) { - if (source.startsWith(VIRTUAL_PREFIX)) { - return "\0" + source; - } - return; - }, - load(id, _options) { - if (id.startsWith(`\0${VIRTUAL_PREFIX}`)) { - const source = id.slice(`\0${VIRTUAL_PREFIX}`.length); - const meta = manager.nodeModules.useClient.get(source); - debug("[parent.use-client-node-modules]", { source, meta }); - tinyassert(meta); - return `export {${[...meta.exportNames].join(", ")}} from "${source}"`; - } - return; - }, - }; - - return [ - devExternalPlugin, - - /** - * emit client-references as dynamic import map - * TODO: re-export only used exports via virtual modules? - * - * export default { - * "some-file1": () => import("some-file1"), - * } - */ - createVirtualPlugin("client-references", function () { - if (this.environment.mode === "dev") { - return `export default {};`; - } - tinyassert( - manager.buildType === "browser" || manager.buildType === "ssr", - ); - let result = `export default {\n`; - for (let [id, clientId] of manager.clientReferenceMap) { - // virtual module needs to be mapped back to the original form - const to = id.startsWith("\0") ? id.slice(1) : id; - result += `"${clientId}": () => import("${to}"),\n`; - } - result += "};\n"; - return { code: result, map: null }; - }), - ]; -} - -export function patchBrowserRawImport(): Plugin { - return { - name: "patch-browser-raw-import", - transform: { - order: "post", - handler(code) { - if (code.includes("__vite_rsc_raw_import__")) { - // inject dynamic import last to avoid Vite adding `?import` query to client references - return code.replace("__vite_rsc_raw_import__", "import"); - } - return; - }, - }, - }; -} diff --git a/packages/react-server/src/features/client-component/server.tsx b/packages/react-server/src/features/client-component/server.tsx index d855d4371..69db93fab 100644 --- a/packages/react-server/src/features/client-component/server.tsx +++ b/packages/react-server/src/features/client-component/server.tsx @@ -1 +1 @@ -export { registerClientReference } from "@hiogawa/vite-rsc/react/rsc"; +export { registerClientReference } from "@vitejs/plugin-rsc/rsc"; diff --git a/packages/react-server/src/features/client-component/ssr.tsx b/packages/react-server/src/features/client-component/ssr.tsx index 8af79ed21..508b3e9ad 100644 --- a/packages/react-server/src/features/client-component/ssr.tsx +++ b/packages/react-server/src/features/client-component/ssr.tsx @@ -1,49 +1,2 @@ -import { createDebug, tinyassert } from "@hiogawa/utils"; -import * as ReactClient from "@hiogawa/vite-rsc/react/ssr"; -import * as ReactDOM from "react-dom"; - -// @ts-ignore -import clientReferences from "virtual:client-references"; - -// @ts-ignore -import prepareDestinationManifest from "virtual:prepare-destination-manifest"; - -const debug = createDebug("react-server:ssr-import"); - -async function ssrImport(id: string) { - debug("[__webpack_require__]", { id }); - if (import.meta.env.DEV) { - return import(/* @vite-ignore */ id); - } else { - const dynImport = clientReferences[id]; - tinyassert(dynImport, `client reference not found '${id}'`); - const mod = await dynImport(); - return wrapResourceProxy(mod, prepareDestinationManifest[id]); - } -} - -export function initializeReactClientSsr() { - ReactClient.setRequireModule({ - load: ssrImport, - }); -} - -function wrapResourceProxy(mod: any, deps?: string[]) { - return new Proxy(mod, { - get(target, p, receiver) { - if (p in mod) { - if (deps) { - for (const href of deps) { - ReactDOM.preloadModule(href, { - as: "script", - // vite doesn't allow configuring crossorigin at the moment, so we can hard code it as well. - // https://github.com/vitejs/vite/issues/6648 - crossOrigin: "", - }); - } - } - } - return Reflect.get(target, p, receiver); - }, - }); -} +// @vitejs/plugin-rsc/ssr auto-initializes client references with preload support +// No initialization needed - just re-export for backwards compatibility diff --git a/packages/react-server/src/features/prerender/plugin.ts b/packages/react-server/src/features/prerender/plugin.ts index d80103939..383dd1e43 100644 --- a/packages/react-server/src/features/prerender/plugin.ts +++ b/packages/react-server/src/features/prerender/plugin.ts @@ -35,11 +35,14 @@ export function prerenderPlugin({ { name: prerenderPlugin.name + ":build", enforce: "post", - apply: () => manager.buildType === "ssr", + apply: "build", writeBundle: { sequential: true, handler() { - return processPrerender(prerender, manager.outDir); + // Run during ssr build + if (this.environment.name === "ssr") { + return processPrerender(prerender, manager.outDir); + } }, }, }, diff --git a/packages/react-server/src/features/router/plugin.ts b/packages/react-server/src/features/router/plugin.ts index e386e5124..b7c525dba 100644 --- a/packages/react-server/src/features/router/plugin.ts +++ b/packages/react-server/src/features/router/plugin.ts @@ -9,11 +9,7 @@ import { import FastGlob from "fast-glob"; import type { Plugin, Rollup } from "vite"; import type { PluginStateManager } from "../../plugin"; -import { - type CustomModuleMeta, - createVirtualPlugin, - hashString, -} from "../../plugin/utils"; +import { createVirtualPlugin, hashString } from "../../plugin/utils"; import { type AssetDeps, type RouteAssetDeps, @@ -30,7 +26,8 @@ export function routeManifestPluginServer({ name: "server-route-manifest", apply: "build", async buildEnd(error) { - if (!error && manager.buildType === "server") { + // Run during rsc build + if (!error && this.environment.name === "rsc") { const routeFiles = await FastGlob( path.posix.join( routeDir, @@ -40,14 +37,14 @@ export function routeManifestPluginServer({ for (const routeFile of routeFiles) { const absFile = path.join(manager.config.root, routeFile); const deps = collectModuleDeps(absFile, this); - let ids: string[] = []; + const ids: string[] = []; for (const id of deps) { const info = this.getModuleInfo(id); tinyassert(info); - const meta = info.meta as CustomModuleMeta; - if (meta.$$rsc?.type === "client") { - ids.push(id); - } + // Note: client reference tracking via CustomModuleMeta is no longer available + // since @vitejs/plugin-rsc handles transforms. Route-based asset optimization + // is disabled for now. + // TODO: Use getPluginApi() to access client reference info if needed } const routeKey = routeFile.slice(routeDir.length); manager.routeToClientReferences[routeKey] = ids; @@ -66,7 +63,8 @@ export function routeManifestPluginClient({ name: routeManifestPluginClient.name + ":bundle", apply: "build", generateBundle(_options, bundle) { - if (manager.buildType === "browser") { + // Run during client build + if (this.environment.name === "client") { const facadeModuleDeps: Record = {}; for (const [k, v] of Object.entries(bundle)) { if (v.type === "chunk" && v.facadeModuleId) { @@ -85,17 +83,13 @@ export function routeManifestPluginClient({ routeTree: createFsRouteTree(routeToAssetDeps).tree, }; - const prepareDestinationManifest: Record = {}; - for (const [id, clientId] of manager.clientReferenceMap) { - prepareDestinationManifest[clientId] = - facadeModuleDeps[id]?.js ?? []; - } - manager.prepareDestinationManifest = prepareDestinationManifest; + // TODO: client reference preloading not supported in native RSC + // https://github.com/wakujs/waku/issues/1656 } }, }, - createVirtualPlugin("route-manifest", async () => { - tinyassert(manager.buildType === "ssr"); + createVirtualPlugin("route-manifest", async function () { + tinyassert(this.environment.name === "ssr"); tinyassert(manager.routeManifest); // create asset for browser @@ -115,12 +109,10 @@ export function routeManifestPluginClient({ 2, )}`; }), + // TODO: client reference preloading not supported in native RSC + // https://github.com/wakujs/waku/issues/1656 createVirtualPlugin("prepare-destination-manifest", async function () { - if (this.environment.mode === "dev") { - return `export default {}`; - } - tinyassert(manager.prepareDestinationManifest); - return `export default ${JSON.stringify(manager.prepareDestinationManifest)}`; + return `export default {}`; }), ]; } diff --git a/packages/react-server/src/features/server-action/browser.tsx b/packages/react-server/src/features/server-action/browser.tsx index a808dd3a8..5f7e2ccc4 100644 --- a/packages/react-server/src/features/server-action/browser.tsx +++ b/packages/react-server/src/features/server-action/browser.tsx @@ -1 +1 @@ -export * from "@hiogawa/vite-rsc/react/browser"; +export * from "@vitejs/plugin-rsc/browser"; diff --git a/packages/react-server/src/features/server-action/plugin.tsx b/packages/react-server/src/features/server-action/plugin.tsx deleted file mode 100644 index 81c2713d9..000000000 --- a/packages/react-server/src/features/server-action/plugin.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - transformDirectiveProxyExport, - transformServerActionServer, -} from "@hiogawa/transforms"; -import { createDebug, tinyassert } from "@hiogawa/utils"; -import { type Plugin, parseAstAsync } from "vite"; -import type { PluginStateManager } from "../../plugin"; -import { - USE_SERVER, - applyPluginToClient, - applyPluginToServer, - createVirtualPlugin, -} from "../../plugin/utils"; - -const debug = createDebug("react-server:plugin:server-action"); - -/* -transform "use server" directive on client (browser / ssr) - -[input] -"use server" -export function hello() {} - -[output] -export const hello = $$proxy("", "hello"); -*/ -export function vitePluginClientUseServer({ - manager, - runtimePath, - ssrRuntimePath, -}: { - manager: PluginStateManager; - runtimePath: string; - ssrRuntimePath: string; -}): Plugin { - return { - name: vitePluginClientUseServer.name, - applyToEnvironment: applyPluginToClient, - async transform(code, id, options) { - if (!code.includes(USE_SERVER)) { - manager.serverReferenceMap.delete(id); - return; - } - const serverId = manager.normalizeReferenceId(id); - const ast = await parseAstAsync(code); - const result = transformDirectiveProxyExport(ast, { - directive: USE_SERVER, - code, - runtime: (name) => - `$$ReactClient.createServerReference(` + - `${JSON.stringify(serverId + "#" + name)}, ` + - `$$ReactClient.callServer, ` + - `undefined, ` + - `$$ReactClient.findSourceMapURL, ` + - `${JSON.stringify(name)})`, - ignoreExportAllDeclaration: true, - rejectNonAsyncFunction: true, - }); - const output = result?.output; - if (!output) { - manager.serverReferenceMap.delete(id); - return; - } - // during client build, all server references are expected to be discovered beforehand. - if (manager.buildType && !manager.serverReferenceMap.has(id)) { - throw new Error( - `client imported undiscovered server reference '${id}'`, - ); - } - manager.serverReferenceMap.set(id, serverId); - const importPath = options?.ssr ? ssrRuntimePath : runtimePath; - output.prepend(`import * as $$ReactClient from "${importPath}";\n`); - debug(`[${vitePluginClientUseServer.name}:transform]`, { - id, - outCode: output.toString(), - }); - return { code: output.toString(), map: output.generateMap() }; - }, - }; -} - -/* -transform "use server" directive on react-server - -[input] -"use server" -export function hello() { ... } - -[output] -export function hello() { ... } -hello = $$register(hello, "", "hello"); -*/ -export function vitePluginServerUseServer({ - manager, - runtimePath, -}: { - manager: PluginStateManager; - runtimePath: string; -}): Plugin[] { - const transformPlugin: Plugin = { - name: vitePluginServerUseServer.name, - applyToEnvironment: applyPluginToServer, - async transform(code, id, _options) { - manager.serverReferenceMap.delete(id); - if (!code.includes(USE_SERVER)) { - return; - } - const serverId = manager.normalizeReferenceId(id); - const ast = await parseAstAsync(code); - const { output } = transformServerActionServer(code, ast, { - runtime: (value, name) => - `$$ReactServer.registerServerReference(${value}, ${JSON.stringify(serverId)}, ${JSON.stringify(name)})`, - rejectNonAsyncFunction: true, - // TODO: encryption - encode: (value) => value, - decode: (value) => value, - }); - if (output.hasChanged()) { - manager.serverReferenceMap.set(id, serverId); - output.prepend(`import * as $$ReactServer from "${runtimePath}";\n`); - debug(`[${vitePluginServerUseServer.name}:transform]`, { - id, - outCode: output.toString(), - }); - return { code: output.toString(), map: output.generateMap() }; - } - return; - }, - }; - - // expose server references for RSC build via virtual module - const virtualPlugin = createVirtualPlugin("server-references", async () => { - if (!manager.buildType || manager.buildType === "scan") { - return `export default {}`; - } - tinyassert(manager.buildType === "server"); - let result = `export default {\n`; - for (const [id, serverId] of manager.serverReferenceMap) { - result += `"${serverId}": () => import("${id}"),\n`; - } - result += "};\n"; - debug("[virtual:server-references]", result); - return result; - }); - - return [transformPlugin, virtualPlugin]; -} diff --git a/packages/react-server/src/features/server-action/server.tsx b/packages/react-server/src/features/server-action/server.tsx index 2c16c1bed..8edfbfaa1 100644 --- a/packages/react-server/src/features/server-action/server.tsx +++ b/packages/react-server/src/features/server-action/server.tsx @@ -1,32 +1,12 @@ -import { tinyassert } from "@hiogawa/utils"; -import * as ReactServer from "@hiogawa/vite-rsc/react/rsc"; import type { ReactFormState } from "react-dom/client"; -import { $__global } from "../../global"; import type { ReactServerErrorContext } from "../../server"; -import { findMapInverse } from "../../utils/misc"; -export { registerServerReference } from "@hiogawa/vite-rsc/react/rsc"; +export { registerServerReference } from "@vitejs/plugin-rsc/rsc"; export type ActionResult = { error?: ReactServerErrorContext; data?: ReactFormState | null; }; -export function initializeReactServer() { - ReactServer.setRequireModule({ - load: importServerReference, - }); -} - -async function importServerReference(id: string): Promise { - if (import.meta.env.DEV) { - const file = findMapInverse($__global.dev.manager.serverReferenceMap, id); - tinyassert(file, `server reference not found '${id}'`); - return await import(/* @vite-ignore */ file); - } else { - const mod = await import("virtual:server-references" as string); - const dynImport = mod.default[id]; - tinyassert(dynImport, `server reference not found '${id}'`); - return dynImport(); - } -} +// @vitejs/plugin-rsc/rsc auto-initializes server references +// No initialization needed diff --git a/packages/react-server/src/features/server-action/ssr.tsx b/packages/react-server/src/features/server-action/ssr.tsx index 287f83b82..f011aaf7a 100644 --- a/packages/react-server/src/features/server-action/ssr.tsx +++ b/packages/react-server/src/features/server-action/ssr.tsx @@ -1 +1 @@ -export * from "@hiogawa/vite-rsc/react/ssr"; +export * from "@vitejs/plugin-rsc/ssr"; diff --git a/packages/react-server/src/plugin/index.ts b/packages/react-server/src/plugin/index.ts index 5cf6c32f0..b0fe597f1 100644 --- a/packages/react-server/src/plugin/index.ts +++ b/packages/react-server/src/plugin/index.ts @@ -1,31 +1,16 @@ import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { createDebug, tinyassert, uniq } from "@hiogawa/utils"; +import { tinyassert } from "@hiogawa/utils"; import { type ConfigEnv, type Plugin, type ResolvedConfig, type ViteDevServer, - build, - createBuilder, createServerModuleRunner, - defaultServerConditions, isCSSRequest, } from "vite"; -import { crawlFrameworkPkgs } from "vitefu"; -import { - serverAssetsPluginServer, - vitePluginServerAssets, -} from "../features/assets/plugin"; +import { vitePluginServerAssets } from "../features/assets/plugin"; import { SERVER_CSS_PROXY } from "../features/assets/shared"; -import { - vitePluginClientUseClient, - vitePluginServerUseClient, -} from "../features/client-component/plugin"; -import { - OUTPUT_SERVER_JS_EXT, - createServerPackageJson, -} from "../features/next/plugin"; +import { OUTPUT_SERVER_JS_EXT } from "../features/next/plugin"; import { type PrerenderFn, type PrerenderManifest, @@ -36,36 +21,14 @@ import { routeManifestPluginClient, routeManifestPluginServer, } from "../features/router/plugin"; -import { - vitePluginClientUseServer, - vitePluginServerUseServer, -} from "../features/server-action/plugin"; import { $__global } from "../global"; import { ENTRY_BROWSER_WRAPPER, ENTRY_SERVER_WRAPPER, createVirtualPlugin, - hashString, - wrapClientPlugin, - wrapServerPlugin, } from "./utils"; export { wrapClientPlugin, wrapServerPlugin } from "./utils"; -import rscCore from "@hiogawa/vite-rsc/core/plugin"; -import { vitePluginFindSourceMapURL } from "@hiogawa/vite-rsc/plugin"; - -const debug = createDebug("react-server:plugin"); - -// resolve import paths for `createClientReference`, `createServerReference`, etc... -// since `import "@hiogawa/react-server"` is not always visible for exernal library. -const RUNTIME_BROWSER_PATH = fileURLToPath( - new URL("../runtime/browser.js", import.meta.url), -); -const RUNTIME_SSR_PATH = fileURLToPath( - new URL("../runtime/ssr.js", import.meta.url), -); -const RUNTIME_SERVER_PATH = fileURLToPath( - new URL("../runtime/server.js", import.meta.url), -); +import rsc from "@vitejs/plugin-rsc"; export type { PrerenderManifest }; @@ -79,39 +42,8 @@ class PluginStateManager { outDir!: string; - buildType?: "scan" | "server" | "browser" | "ssr"; - routeToClientReferences: Record = {}; routeManifest?: RouteManifest; - serverAssets: string[] = []; - prepareDestinationManifest?: Record = {}; - - // expose "use client" node modules to client via virtual modules - // to avoid dual package due to deps optimization hash during dev - nodeModules = { - useClient: new Map }>(), - }; - - // all files in parent server - parentIds = new Set(); - // all files in rsc server - serverIds = new Set(); - // "use client" files - clientReferenceMap = new Map(); - - // "use server" files - serverReferenceMap = new Map(); - - shouldReloadRsc(id: string) { - const ok = this.serverIds.has(id) && !this.clientReferenceMap.has(id); - debug("[RscManager.shouldReloadRsc]", { ok, id }); - return ok; - } - - normalizeReferenceId(id: string) { - id = path.relative(this.config.root, id); - return this.buildType ? hashString(id) : id; - } } // persist singleton during build @@ -128,7 +60,6 @@ export type ReactServerPluginOptions = { entryServer?: string; routeDir?: string; outDir?: string; - noAsyncLocalStorage?: boolean; }; export function vitePluginReactServer( @@ -141,14 +72,13 @@ export function vitePluginReactServer( const routeDir = options?.routeDir ?? "src/routes"; const outDir = options?.outDir ?? "dist"; - const rscParentPlugin: Plugin = { + // Framework config plugin - sets up build outputs and environment entries + const frameworkConfigPlugin: Plugin = { name: vitePluginReactServer.name, config(_config, env) { manager.configEnv = env; return { optimizeDeps: { - // this can potentially include unnecessary server only deps for client, - // but there should be no issues except making deps optimization slightly slower. entries: [ path.posix.join( routeDir, @@ -156,14 +86,7 @@ export function vitePluginReactServer( ), ], exclude: ["@hiogawa/react-server"], - include: [ - "react", - "react/jsx-runtime", - "react/jsx-dev-runtime", - "react-dom", - "react-dom/client", - "@hiogawa/react-server > @hiogawa/vite-rsc/react/browser", - ], + include: ["@hiogawa/react-server > @vitejs/plugin-rsc/browser"], }, build: { manifest: true, @@ -183,14 +106,9 @@ export function vitePluginReactServer( }, environments: { rsc: { - resolve: { - conditions: ["react-server", ...defaultServerConditions], - }, build: { outDir: path.join(outDir, "rsc"), sourcemap: true, - ssr: true, - emitAssets: true, manifest: true, rollupOptions: { input: { @@ -201,6 +119,10 @@ export function vitePluginReactServer( }, }, }, + // @vitejs/plugin-rsc options + rsc: { + serverHandler: false, // framework handles server request routing + }, }; }, configResolved(config) { @@ -230,47 +152,20 @@ export function vitePluginReactServer( delete ($__global as any).dev; } }, - transform(_code, id, _options) { - if (!id.includes("/node_modules/")) { - manager.parentIds.add(id); - } - }, async hotUpdate(ctx) { - const isClientReference = ctx.modules.every( - (mod) => mod.id && manager.clientReferenceMap.has(mod.id), - ); - - if (this.environment.name === "rsc") { - // client reference id is also in react server module graph, - // but we skip RSC HMR for this case to avoid conflicting with Client HMR. - if (ctx.modules.length > 0 && !isClientReference) { - $__global.dev.server.environments.client.hot.send({ - type: "custom", - event: "rsc:update", - data: { - file: ctx.file, - }, - }); - } - } + // @vitejs/plugin-rsc handles rsc:update events for RSC module changes if (this.environment.name === "client") { // css module is not self-accepting, so we filter out // `?direct` module (used for SSR CSS) to avoid browser full reload. - // (see packages/react-server/src/features/assets/css.ts) if (isCSSRequest(ctx.file)) { return ctx.modules.filter((m) => !m.id?.includes("?direct")); } // Server files can be included in client module graph // due to postcss creating dependencies from style.css to all source files. - // In this case, reload all importers (for css hmr), - // and return empty modules to avoid full-reload const reactServerEnv = $__global.dev.server.environments["rsc"]!; - if ( - !isClientReference && - reactServerEnv.moduleGraph.getModulesByFile(ctx.file) - ) { + if (reactServerEnv.moduleGraph.getModulesByFile(ctx.file)) { const importers = ctx.modules.flatMap((m) => [...m.importers]); if ( importers.length > 0 && @@ -288,62 +183,21 @@ export function vitePluginReactServer( }, }; - // orchestrate four builds from a single vite (browser) build - const buildOrchestrationPlugin: Plugin = { - name: vitePluginReactServer.name + ":build", - apply: "build", - async buildStart(_options) { - if (!manager.buildType) { - await createServerPackageJson(manager.outDir); - console.log("▶▶▶ REACT SERVER BUILD (scan) [1/4]"); - manager.buildType = "scan"; - const builder = await createBuilder(); - builder.environments["rsc"]!.config.build.write = false; - await builder.build(builder.environments["rsc"]!); - console.log("▶▶▶ REACT SERVER BUILD (server) [2/4]"); - manager.buildType = "server"; - manager.clientReferenceMap.clear(); - builder.environments["rsc"]!.config.build.write = true; - await builder.build(builder.environments["rsc"]!); - console.log("▶▶▶ REACT SERVER BUILD (browser) [3/4]"); - manager.buildType = "browser"; - } - }, - writeBundle: { - order: "post", - sequential: true, - async handler(_options, _bundle) { - if (manager.buildType === "browser") { - console.log("▶▶▶ REACT SERVER BUILD (ssr) [4/4]"); - manager.buildType = "ssr"; - await build({ - build: { - ssr: true, - }, - }); - } - }, - }, - }; - // plugins for main vite dev server (browser / ssr) return [ - ...rscCore(), - ...vitePluginFindSourceMapURL(), - rscParentPlugin, - buildOrchestrationPlugin, + // @vitejs/plugin-rsc handles: + // - RSC environment setup + // - Build orchestration (rsc -> ssr -> rsc -> client -> ssr) + // - "use client" and "use server" transforms + // - AsyncLocalStorage injection + // - server-only/client-only import validation + // - Server deps config (noExternal, optimizeDeps) + ...rsc(), + frameworkConfigPlugin, // - // react server + // Framework-specific plugins // - ...vitePluginServerUseServer({ - manager, - runtimePath: RUNTIME_SERVER_PATH, - }), - ...vitePluginServerUseClient({ - manager, - runtimePath: RUNTIME_SERVER_PATH, - }), ...routeManifestPluginServer({ manager, routeDir }), createVirtualPlugin("server-routes", () => { return ` @@ -368,85 +222,15 @@ export function vitePluginReactServer( export { router } from "@hiogawa/react-server/entry/server"; `, ), - { - // make `AsyncLocalStorage` available globally for React request context on edge build (e.g. React.cache, ssr preload) - // https://github.com/facebook/react/blob/f14d7f0d2597ea25da12bcf97772e8803f2a394c/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js#L16-L19 - name: "inject-async-local-storage", - async configureServer() { - if (options?.noAsyncLocalStorage) return; - - const __viteRscAyncHooks = await import("node:async_hooks"); - (globalThis as any).AsyncLocalStorage = - __viteRscAyncHooks.AsyncLocalStorage; - }, - banner(chunk) { - if (options?.noAsyncLocalStorage) return ""; - - if ( - (this.environment.name === "ssr" || - this.environment.name === "rsc") && - this.environment.mode === "build" && - chunk.isEntry - ) { - return `\ - import * as __viteRscAyncHooks from "node:async_hooks"; - globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; - `; - } - return ""; - }, - }, - wrapServerPlugin( - validateImportPlugin({ - "client-only": `'client-only' is included in server build`, - "server-only": true, - }), - ), - ...serverAssetsPluginServer({ manager }), - serverDepsConfigPlugin(), // - // react client + // Client-side plugins // - - vitePluginClientUseServer({ - manager, - runtimePath: RUNTIME_BROWSER_PATH, - ssrRuntimePath: RUNTIME_SSR_PATH, - }), - ...vitePluginClientUseClient({ manager }), ...vitePluginServerAssets({ manager, entryBrowser, entryServer }), ...routeManifestPluginClient({ manager }), ...(options?.prerender ? prerenderPlugin({ manager, prerender: options.prerender }) : []), - wrapClientPlugin( - validateImportPlugin({ - "client-only": true, - "server-only": `'server-only' is included in client build`, - }), - ), - { - // externalize `dist/rsc/index.js` import as relative path in ssr build - name: "virtual:react-server-build", - resolveId(source) { - if (source === "virtual:react-server-build") { - return { id: "__VIRTUAL_REACT_SERVER_BUILD__", external: true }; - } - return; - }, - renderChunk(code, chunk) { - if (code.includes("__VIRTUAL_REACT_SERVER_BUILD__")) { - const replacement = path.relative( - path.join(outDir, "server", chunk.fileName, ".."), - path.join(outDir, "rsc", "index.js"), - ); - code = code.replace("__VIRTUAL_REACT_SERVER_BUILD__", replacement); - return { code }; - } - return; - }, - }, createVirtualPlugin("client-routes", () => { return ` @@ -455,11 +239,13 @@ export function vitePluginReactServer( `; }), - createVirtualPlugin(ENTRY_BROWSER_WRAPPER.slice("virtual:".length), () => { - // dev - if (!manager.buildType) { - // wrapper entry to ensure client entry runs after vite/react inititialization - return /* js */ ` + createVirtualPlugin( + ENTRY_BROWSER_WRAPPER.slice("virtual:".length), + function () { + // dev + if (this.environment?.mode === "dev") { + // wrapper entry to ensure client entry runs after vite/react initialization + return /* js */ ` import "${SERVER_CSS_PROXY}"; import RefreshRuntime from "/@react-refresh"; RefreshRuntime.injectIntoGlobalHook(window); @@ -468,99 +254,14 @@ export function vitePluginReactServer( window.__vite_plugin_react_preamble_installed__ = true; await import("${entryBrowser}"); `; - } - // build - if (manager.buildType === "browser") { - // import "runtime/client" for preload + } + // build return /* js */ ` - import "${SERVER_CSS_PROXY}"; - import("@hiogawa/react-server/runtime/client"); - import "${entryBrowser}"; - `; - } - tinyassert(false); - }), + import "${SERVER_CSS_PROXY}"; + import("@hiogawa/react-server/runtime/client"); + import "${entryBrowser}"; + `; + }, + ), ]; } - -// https://github.com/vercel/next.js/blob/90f564d376153fe0b5808eab7b83665ee5e08aaf/packages/next/src/build/webpack-config.ts#L1249-L1280 -// https://github.com/pcattori/vite-env-only/blob/68a0cc8546b9a37c181c0b0a025eb9b62dbedd09/src/deny-imports.ts -// https://github.com/sveltejs/kit/blob/84298477a014ec471839adf7a4448d91bc7949e4/packages/kit/src/exports/vite/index.js#L513 -function validateImportPlugin(entries: Record): Plugin { - return { - name: validateImportPlugin.name, - enforce: "pre", - resolveId(source, importer, options) { - const entry = entries[source]; - if (entry) { - // skip validation during optimizeDeps scan since for now - // we want to allow going through server/client boundary loosely - if ( - entry === true || - manager.buildType === "scan" || - ("scan" in options && options.scan) - ) { - return "\0virtual:validate-import"; - } - throw new Error(entry + ` (importer: ${importer ?? "unknown"})`); - } - return; - }, - load(id, _options) { - if (id === "\0virtual:validate-import") { - return "export {}"; - } - return; - }, - }; -} - -function serverDepsConfigPlugin(): Plugin { - return { - name: serverDepsConfigPlugin.name, - async configEnvironment(name, _config, env) { - if (name !== "rsc" && name !== "ssr") { - return; - } - - // crawl packages with "react" or "next" in "peerDependencies" - // see https://github.com/svitejs/vitefu/blob/d8d82fa121e3b2215ba437107093c77bde51b63b/src/index.js#L95-L101 - const result = await crawlFrameworkPkgs({ - root: process.cwd(), - isBuild: env.command === "build", - isFrameworkPkgByJson(pkgJson) { - const deps = pkgJson["peerDependencies"]; - return deps && ("react" in deps || "next" in deps); - }, - }); - - return { - resolve: { - noExternal: uniq([ - "react", - "react-dom", - "server-only", - "client-only", - ...result.ssr.noExternal, - ]).sort(), - }, - // pre-bundle cjs deps - optimizeDeps: { - include: [ - "react", - "react-dom", - "react/jsx-runtime", - "react/jsx-dev-runtime", - ...(name === "ssr" - ? [ - "react-dom/server.edge", - "@hiogawa/react-server > @hiogawa/vite-rsc/react/ssr", - ] - : ["@hiogawa/react-server > @hiogawa/vite-rsc/react/rsc"]), - ], - exclude: ["@hiogawa/react-server"], - }, - }; - }, - }; -} diff --git a/packages/react-server/tsconfig.json b/packages/react-server/tsconfig.json index c62694458..33643ad5c 100644 --- a/packages/react-server/tsconfig.json +++ b/packages/react-server/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "include": ["src", "e2e", "*.ts"], "compilerOptions": { - "types": ["vite/client"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], "jsx": "react-jsx" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7f5ba485..34648064a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,9 +221,9 @@ importers: '@hiogawa/transforms': specifier: workspace:* version: link:../transforms - '@hiogawa/vite-rsc': - specifier: ^0.4.9 - version: 0.4.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@vitejs/plugin-rsc': + specifier: ^0.5.9 + version: 0.5.9(react-dom@19.1.0(react@19.1.0))(react-server-dom-webpack@19.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.93.0(esbuild@0.24.2)))(react@19.1.0)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) es-module-lexer: specifier: ^1.6.0 version: 1.7.0 @@ -1606,9 +1606,6 @@ packages: peerDependencies: react: ^19.1.0 - '@hiogawa/transforms@0.1.6': - resolution: {integrity: sha512-IWt/CCZ5+ZxOmpm/X7Eyt5t2byqFEnnwNJK1AliIKnN+6Lq8j3fdYB/4goWcTH7WJCelGQoKpvx86eO//IN48w==} - '@hiogawa/unocss-preset-antd@2.2.1-pre.7': resolution: {integrity: sha512-gwY0T8FpFzvHE3I2bShDFKRFAlf1dxgAeaLkfGy7VpNkVr7NkrS3hIUb3An95zAjQWSvgP90t+p1e8AaFW8oXw==} peerDependencies: @@ -1625,14 +1622,6 @@ packages: peerDependencies: vite: ^7.1.5 - '@hiogawa/vite-rsc@0.4.9': - resolution: {integrity: sha512-aV+GTKctMfOw+0dyrzK/K4yn0dy3bQxsZXXkhBS60Q5tFSTbQ671UYOLLBrj2LmJ+t7lJkZtFn/dsgvuFfiZng==} - deprecated: Use @vitejs/plugin-rsc instead - peerDependencies: - react: ^19.1.0 - react-dom: ^19.1.0 - vite: ^7.1.5 - '@iconify-json/ri@1.2.5': resolution: {integrity: sha512-kWGimOXMZrlYusjBKKXYOWcKhbOHusFsmrmRGmjS7rH0BpML5A9/fy8KHZqFOwZfC4M6amObQYbh8BqO5cMC3w==} @@ -1807,9 +1796,6 @@ packages: peerDependencies: rollup: '>=2' - '@mjackson/node-fetch-server@0.6.1': - resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} - '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} @@ -2654,6 +2640,17 @@ packages: peerDependencies: vite: ^7.1.5 + '@vitejs/plugin-rsc@0.5.9': + resolution: {integrity: sha512-DNFFkFDwnJXrwp3eWqjfYZIQQjtMfeqqHCYEECJ7kPV1xqh9tV35qHxWLtO/+D5Hv6dlA88dpBIsOz3rsNls1w==} + peerDependencies: + react: ^19.1.0 + react-dom: ^19.1.0 + react-server-dom-webpack: ^19.1.0 + vite: ^7.1.5 + peerDependenciesMeta: + react-server-dom-webpack: + optional: true + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -3473,6 +3470,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -5737,6 +5737,14 @@ packages: vite: optional: true + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^7.1.5 + peerDependenciesMeta: + vite: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -6548,12 +6556,6 @@ snapshots: dependencies: react: 19.1.0 - '@hiogawa/transforms@0.1.6': - dependencies: - estree-walker: 3.0.3 - magic-string: 0.30.21 - periscopic: 4.0.2 - '@hiogawa/unocss-preset-antd@2.2.1-pre.7(unocss@66.2.1(postcss@8.5.6)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.22(typescript@5.8.3)))': dependencies: unocss: 66.2.1(postcss@8.5.6)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.22(typescript@5.8.3)) @@ -6570,18 +6572,6 @@ snapshots: strip-literal: 3.1.0 vite: 7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - '@hiogawa/vite-rsc@0.4.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@hiogawa/transforms': 0.1.6 - '@mjackson/node-fetch-server': 0.6.1 - es-module-lexer: 1.7.0 - magic-string: 0.30.21 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - turbo-stream: 3.1.0 - vite: 7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - vitefu: 1.0.5(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) - '@iconify-json/ri@1.2.5': dependencies: '@iconify/types': 2.0.0 @@ -6789,8 +6779,6 @@ snapshots: - acorn - supports-color - '@mjackson/node-fetch-server@0.6.1': {} - '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 @@ -7661,6 +7649,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-rsc@0.5.9(react-dom@19.1.0(react@19.1.0))(react-server-dom-webpack@19.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.93.0(esbuild@0.24.2)))(react@19.1.0)(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': + dependencies: + es-module-lexer: 2.0.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + periscopic: 4.0.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + srvx: 0.9.8 + strip-literal: 3.1.0 + turbo-stream: 3.1.0 + vite: 7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vitefu: 1.1.1(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + optionalDependencies: + react-server-dom-webpack: 19.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.93.0(esbuild@0.24.2)) + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -8465,6 +8469,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -11226,6 +11232,10 @@ snapshots: optionalDependencies: vite: 7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vitefu@1.1.1(vite@7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): + optionalDependencies: + vite: 7.1.5(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@types/chai': 5.2.2