diff --git a/packages/react-server/examples/basic/package.json b/packages/react-server/examples/basic/package.json index 477d17b1c..30c337974 100644 --- a/packages/react-server/examples/basic/package.json +++ b/packages/react-server/examples/basic/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "vite build --ssr", "preview": "vite preview", "test-e2e": "playwright test", "test-e2e-preview": "E2E_PREVIEW=1 playwright test", diff --git a/packages/react-server/examples/starter/package.json b/packages/react-server/examples/starter/package.json index 95ba2d191..be3852637 100644 --- a/packages/react-server/examples/starter/package.json +++ b/packages/react-server/examples/starter/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "vite build --ssr", "preview": "vite preview", "cf-build": "SSR_ENTRY=/src/adapters/cloudflare-workers.ts pnpm build && bash misc/cloudflare-workers/build.sh", "cf-preview": "cd misc/cloudflare-workers && wrangler dev", diff --git a/packages/react-server/src/features/router/plugin.ts b/packages/react-server/src/features/router/plugin.ts index 7f865f55c..8977f3770 100644 --- a/packages/react-server/src/features/router/plugin.ts +++ b/packages/react-server/src/features/router/plugin.ts @@ -62,7 +62,7 @@ export function routeManifestPluginClient({ } const routeToAssetDeps = objectMapValues( manager.routeToClientReferences, - // facade module might not exist when dynamic import is also imported statically + // facade module might not exist when dynamic import is also imported statically? (ids) => mergeAssetDeps( ids.map((id) => facadeModuleDeps[id]).filter(typedBoolean), diff --git a/packages/react-server/src/features/server-action/plugin.tsx b/packages/react-server/src/features/server-action/plugin.tsx index 5ce663cf3..8745e6250 100644 --- a/packages/react-server/src/features/server-action/plugin.tsx +++ b/packages/react-server/src/features/server-action/plugin.tsx @@ -6,6 +6,7 @@ import { createDebug, tinyassert } from "@hiogawa/utils"; import { type Plugin, type PluginOption, parseAstAsync } from "vite"; import type { PluginStateManager } from "../../plugin"; import { + type CustomModuleMeta, USE_SERVER, createVirtualPlugin, hashString, @@ -56,7 +57,15 @@ const $$proxy = (id, name) => createServerReference(id + "#" + name); id, outCode: output.toString(), }); - return { code: output.toString(), map: output.generateMap() }; + return { + code: output.toString(), + map: output.generateMap(), + meta: { + $$rsc: { + type: "server", + }, + } satisfies CustomModuleMeta, + }; }, }; } @@ -100,10 +109,44 @@ export function vitePluginServerUseServer({ id, outCode: output.toString(), }); - return { code: output.toString(), map: output.generateMap() }; + if (manager.buildType === "rsc") { + manager.serverReferenceMap[id] = this.emitFile({ + type: "chunk", + id, + }); + } + return { + code: output.toString(), + map: output.generateMap(), + meta: { + $$rsc: { + type: "server", + }, + } satisfies CustomModuleMeta, + }; } return; }, + generateBundle(_options, bundle) { + if (manager.buildType === "rsc") { + let result = `export default {\n`; + for (const [id, refId] of Object.entries(manager.serverReferenceMap)) { + const fileName = this.getFileName(refId); + result += ` "${hashString(id)}": () => import("../${fileName}"),\n`; + } + result += "};\n"; + for (const [_k, v] of Object.entries(bundle)) { + if ( + v.type === "chunk" && + v.facadeModuleId === "\0virtual:server-references" + ) { + // TODO: how to content hash? + // server reference virtual is fine, but it's critical for client reference + v.code = result; + } + } + } + }, }; // expose server references for RSC build via virtual module @@ -112,6 +155,9 @@ export function vitePluginServerUseServer({ return `export default {}`; } tinyassert(manager.buildType === "rsc"); + if (1) { + return `export default "** PLACEHOLDER **"`; + } let result = `export default {\n`; for (const id of manager.rscUseServerIds) { let key = manager.buildType ? hashString(id) : id; diff --git a/packages/react-server/src/plugin/index.ts b/packages/react-server/src/plugin/index.ts index a5d6a9c12..01b6778a6 100644 --- a/packages/react-server/src/plugin/index.ts +++ b/packages/react-server/src/plugin/index.ts @@ -65,6 +65,7 @@ class PluginStateManager { routeToClientReferences: Record = {}; routeManifest?: RouteManifest; + serverReferenceMap: Record = {}; // expose "use client" node modules to client via virtual modules // to avoid dual package due to deps optimization hash during dev @@ -297,7 +298,7 @@ export function vitePluginReactServer(options?: { }, }; - // orchestrate four builds from a single vite (browser) build + // orchestrate four builds from a single vite (ssr) build const buildOrchestrationPlugin: Plugin = { name: vitePluginReactServer.name + ":build", apply: "build", @@ -306,24 +307,18 @@ export function vitePluginReactServer(options?: { console.log("▶▶▶ REACT SERVER BUILD (scan) [1/4]"); manager.buildType = "scan"; await build(reactServerViteConfig); + console.log("▶▶▶ REACT SERVER BUILD (server) [2/4]"); manager.buildType = "rsc"; manager.rscUseClientIds.clear(); await build(reactServerViteConfig); + console.log("▶▶▶ REACT SERVER BUILD (browser) [3/4]"); manager.buildType = "client"; - } - }, - async closeBundle() { - // TODO: build ssr only when client build succeeds - if (manager.buildType === "client") { + await build(); + console.log("▶▶▶ REACT SERVER BUILD (ssr) [4/4]"); manager.buildType = "ssr"; - await build({ - build: { - ssr: true, - }, - }); } }, }; diff --git a/packages/react-server/src/plugin/utils.tsx b/packages/react-server/src/plugin/utils.tsx index e9a25655a..9910e3ea3 100644 --- a/packages/react-server/src/plugin/utils.tsx +++ b/packages/react-server/src/plugin/utils.tsx @@ -11,7 +11,7 @@ export function invalidateModule(server: ViteDevServer, id: string) { // cf. https://github.com/vercel/next.js/blob/5ae286ffd664e5c76841ed64f6e2da85a0835922/packages/next/src/build/webpack/loaders/get-module-build-info.ts#L8 export type CustomModuleMeta = { $$rsc?: { - type: "client"; + type: "client" | "server"; }; };