From 103fc2c7bc9c90b245558a111259c98dd2df994e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 11:54:10 +0900 Subject: [PATCH 01/15] fix(rsc): isolate plugin state per plugin --- packages/plugin-rsc/src/plugin.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 543a63622..f43701c98 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -80,6 +80,14 @@ function resolvePackage(name: string) { return pathToFileURL(require.resolve(name)).href } +export type { RscPluginManager } + +class RscPluginManager { + serverReferences: Record = {} + clientReferenceMetaMap: Record = {} + serverResourcesMetaMap: Record = {} +} + export type RscPluginOptions = { /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` @@ -201,6 +209,9 @@ export function vitePluginRscMinimal( export default function vitePluginRsc( rscPluginOptions: RscPluginOptions = {}, ): Plugin[] { + const manager = new RscPluginManager() + manager.clientReferenceMetaMap + const buildApp: NonNullable = async (builder) => { // no-ssr case // rsc -> client -> rsc -> client From 8f29031ab14833a37e05c7f18f435b5a256edb4e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 11:59:26 +0900 Subject: [PATCH 02/15] wip --- packages/plugin-rsc/src/plugin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f43701c98..30465a2f9 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -83,6 +83,11 @@ function resolvePackage(name: string) { export type { RscPluginManager } class RscPluginManager { + server!: ViteDevServer + config!: ResolvedConfig + rscBundle!: Rollup.OutputBundle + buildAssetsManifest: AssetsManifest | undefined + isScanBuild = false serverReferences: Record = {} clientReferenceMetaMap: Record = {} serverResourcesMetaMap: Record = {} From 43766bd591e920508af918fbd5d8cc26af3bdbe8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:06:41 +0900 Subject: [PATCH 03/15] refactor: isScanBuild --- packages/plugin-rsc/src/plugin.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 30465a2f9..7b5508990 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -50,7 +50,7 @@ let server: ViteDevServer let config: ResolvedConfig let rscBundle: Rollup.OutputBundle let buildAssetsManifest: AssetsManifest | undefined -let isScanBuild = false +// let isScanBuild = false const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' type ClientReferenceMeta = { @@ -170,8 +170,11 @@ export type RscPluginOptions = { /** @experimental */ export function vitePluginRscMinimal( - rscPluginOptions: RscPluginOptions = {}, + rscPluginOptions: RscPluginOptions & { manager?: RscPluginManager } = {}, ): Plugin[] { + const manager = rscPluginOptions.manager ?? new RscPluginManager() + manager + return [ { name: 'rsc:minimal', @@ -215,18 +218,17 @@ export default function vitePluginRsc( rscPluginOptions: RscPluginOptions = {}, ): Plugin[] { const manager = new RscPluginManager() - manager.clientReferenceMetaMap const buildApp: NonNullable = async (builder) => { // no-ssr case // rsc -> client -> rsc -> client if (!builder.environments.ssr?.config.build.rollupOptions.input) { - isScanBuild = true + manager.isScanBuild = true builder.environments.rsc!.config.build.write = false builder.environments.client!.config.build.write = false await builder.build(builder.environments.rsc!) await builder.build(builder.environments.client!) - isScanBuild = false + manager.isScanBuild = false builder.environments.rsc!.config.build.write = true builder.environments.client!.config.build.write = true await builder.build(builder.environments.rsc!) @@ -239,12 +241,12 @@ export default function vitePluginRsc( } // rsc -> ssr -> rsc -> client -> ssr - isScanBuild = true + manager.isScanBuild = true builder.environments.rsc!.config.build.write = false builder.environments.ssr!.config.build.write = false await builder.build(builder.environments.rsc!) await builder.build(builder.environments.ssr!) - isScanBuild = false + manager.isScanBuild = false builder.environments.rsc!.config.build.write = true builder.environments.ssr!.config.build.write = true await builder.build(builder.environments.rsc!) @@ -909,26 +911,30 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; return '' }, }, - ...vitePluginRscMinimal(rscPluginOptions), + ...vitePluginRscMinimal({ ...rscPluginOptions, manager }), ...vitePluginFindSourceMapURL(), ...vitePluginRscCss({ rscCssTransform: rscPluginOptions.rscCssTransform }), ...(rscPluginOptions.validateImports !== false ? [validateImportPlugin()] : []), - scanBuildStripPlugin(), + scanBuildStripPlugin({ manager }), ...cjsModuleRunnerPlugin(), ] } // During scan build, we strip all code but imports to // traverse module graph faster and just discover client/server references. -function scanBuildStripPlugin(): Plugin { +function scanBuildStripPlugin({ + manager, +}: { + manager: RscPluginManager +}): Plugin { return { name: 'rsc:scan-strip', apply: 'build', enforce: 'post', async transform(code, _id, _options) { - if (!isScanBuild) return + if (!manager.isScanBuild) return const output = await transformScanBuildStrip(code) return { code: output, map: { mappings: '' } } }, From 3de554490ae48434398fed0c6e11b22b76316b8c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:07:53 +0900 Subject: [PATCH 04/15] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 7b5508990..7573e80f2 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -87,9 +87,9 @@ class RscPluginManager { config!: ResolvedConfig rscBundle!: Rollup.OutputBundle buildAssetsManifest: AssetsManifest | undefined - isScanBuild = false + isScanBuild: boolean = false serverReferences: Record = {} - clientReferenceMetaMap: Record = {} + clientReferenceMetaMap: Record = {} serverResourcesMetaMap: Record = {} } From cbb04f3989abce1889560d223c80884b82fac507 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:13:20 +0900 Subject: [PATCH 05/15] refactor: buildAssetsManifest --- packages/plugin-rsc/src/plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 7573e80f2..7517bb06f 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -49,7 +49,7 @@ let serverReferences: Record = {} let server: ViteDevServer let config: ResolvedConfig let rscBundle: Rollup.OutputBundle -let buildAssetsManifest: AssetsManifest | undefined +// let buildAssetsManifest: AssetsManifest | undefined // let isScanBuild = false const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' @@ -262,7 +262,7 @@ export default function vitePluginRsc( // output client manifest to non-client build directly. // this makes server build to be self-contained and deploy-able for cloudflare. const assetsManifestCode = `export default ${serializeValueWithRuntime( - buildAssetsManifest, + manager.buildAssetsManifest, )}` for (const name of environmentNames) { const manifestPath = path.join( @@ -771,7 +771,7 @@ export default function vitePluginRsc( `"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`, ) } - buildAssetsManifest = { + manager.buildAssetsManifest = { bootstrapScriptContent, clientReferenceDeps, serverResources, From c63e50827b3f4a3a88fcc3d73c827ad31cef9001 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:14:01 +0900 Subject: [PATCH 06/15] refactor: rscBundle --- packages/plugin-rsc/src/plugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 7517bb06f..74a043abb 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -48,7 +48,7 @@ import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' let serverReferences: Record = {} let server: ViteDevServer let config: ResolvedConfig -let rscBundle: Rollup.OutputBundle +// let rscBundle: Rollup.OutputBundle // let buildAssetsManifest: AssetsManifest | undefined // let isScanBuild = false const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' @@ -719,7 +719,7 @@ export default function vitePluginRsc( generateBundle(_options, bundle) { // copy assets from rsc build to client build if (this.environment.name === 'rsc') { - rscBundle = bundle + manager.rscBundle = bundle } if (this.environment.name === 'client') { @@ -730,7 +730,7 @@ export default function vitePluginRsc( typeof rscBuildOptions.manifest === 'string' ? rscBuildOptions.manifest : rscBuildOptions.manifest && '.vite/manifest.json' - for (const asset of Object.values(rscBundle)) { + for (const asset of Object.values(manager.rscBundle)) { if (asset.fileName === rscViteManifest) continue if (asset.type === 'asset' && filterAssets(asset.fileName)) { this.emitFile({ @@ -742,7 +742,7 @@ export default function vitePluginRsc( } const serverResources: Record = {} - const rscAssetDeps = collectAssetDeps(rscBundle) + const rscAssetDeps = collectAssetDeps(manager.rscBundle) for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { serverResources[meta.key] = assetsURLOfDeps({ js: [], From 5f837e5ae9b9340aedf44e103df9f6d5c2b2f029 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:27:52 +0900 Subject: [PATCH 07/15] refactor: config --- packages/plugin-rsc/src/plugin.ts | 101 ++++++++++++++++++------------ 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 74a043abb..92e9de6ea 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -47,7 +47,7 @@ import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' // state for build orchestration let serverReferences: Record = {} let server: ViteDevServer -let config: ResolvedConfig +// let config: ResolvedConfig // let rscBundle: Rollup.OutputBundle // let buildAssetsManifest: AssetsManifest | undefined // let isScanBuild = false @@ -170,11 +170,9 @@ export type RscPluginOptions = { /** @experimental */ export function vitePluginRscMinimal( - rscPluginOptions: RscPluginOptions & { manager?: RscPluginManager } = {}, + rscPluginOptions: RscPluginOptions = {}, + manager: RscPluginManager = new RscPluginManager(), ): Plugin[] { - const manager = rscPluginOptions.manager ?? new RscPluginManager() - manager - return [ { name: 'rsc:minimal', @@ -182,8 +180,8 @@ export function vitePluginRscMinimal( async config() { await esModuleLexer.init }, - configResolved(config_) { - config = config_ + configResolved(config) { + manager.config = config // ensure outDir is fully resolved to take custom root into account // https://github.com/vitejs/vite/blob/946831f986cb797009b8178659d2b31f570c44ff/packages/vite/src/node/build.ts#L574 for (const e of Object.values(config.environments)) { @@ -208,8 +206,8 @@ export function vitePluginRscMinimal( }, }, ...vitePluginRscCore(), - ...vitePluginUseClient(rscPluginOptions), - ...vitePluginUseServer(rscPluginOptions), + ...vitePluginUseClient(rscPluginOptions, manager), + ...vitePluginUseServer(rscPluginOptions, manager), ...vitePluginDefineEncryptionKey(rscPluginOptions), ] } @@ -266,7 +264,7 @@ export default function vitePluginRsc( )}` for (const name of environmentNames) { const manifestPath = path.join( - config.environments[name]!.build.outDir, + manager.config.environments[name]!.build.outDir, BUILD_ASSETS_MANIFEST_NAME, ) fs.writeFileSync(manifestPath, assetsManifestCode) @@ -427,7 +425,7 @@ export default function vitePluginRsc( } const entryFile = path.join( - config.environments[options.environmentName]!.build.outDir, + manager.config.environments[options.environmentName]!.build.outDir, `${options.entryName}.js`, ) const entry = pathToFileURL(entryFile).href @@ -618,12 +616,12 @@ export default function vitePluginRsc( const importPath = normalizeRelativePath( path.relative( path.join( - config.environments[fromEnv!]!.build.outDir, + manager.config.environments[fromEnv!]!.build.outDir, chunk.fileName, '..', ), path.join( - config.environments[toEnv!]!.build.outDir, + manager.config.environments[toEnv!]!.build.outDir, // TODO: this breaks when custom entyFileNames `${entryName}.js`, ), @@ -707,7 +705,10 @@ export default function vitePluginRsc( if (id === '\0virtual:vite-rsc/assets-manifest') { assert(this.environment.name !== 'client') assert(this.environment.mode === 'dev') - const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser) + const entryUrl = assetsURL( + '@id/__x00__' + VIRTUAL_ENTRIES.browser, + manager, + ) const manifest: AssetsManifest = { bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`, clientReferenceDeps: {}, @@ -725,7 +726,7 @@ export default function vitePluginRsc( if (this.environment.name === 'client') { const filterAssets = rscPluginOptions.copyServerAssetsToClient ?? (() => true) - const rscBuildOptions = config.environments.rsc!.build + const rscBuildOptions = manager.config.environments.rsc!.build const rscViteManifest = typeof rscBuildOptions.manifest === 'string' ? rscBuildOptions.manifest @@ -744,10 +745,13 @@ export default function vitePluginRsc( const serverResources: Record = {} const rscAssetDeps = collectAssetDeps(manager.rscBundle) for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { - serverResources[meta.key] = assetsURLOfDeps({ - js: [], - css: rscAssetDeps[id]?.deps.css ?? [], - }) + serverResources[meta.key] = assetsURLOfDeps( + { + js: [], + css: rscAssetDeps[id]?.deps.css ?? [], + }, + manager, + ) } const assetDeps = collectAssetDeps(bundle) @@ -755,12 +759,13 @@ export default function vitePluginRsc( (v) => v.chunk.name === 'index', ) assert(entry) - const entryUrl = assetsURL(entry.chunk.fileName) + const entryUrl = assetsURL(entry.chunk.fileName, manager) const clientReferenceDeps: Record = {} for (const [id, meta] of Object.entries(clientReferenceMetaMap)) { const deps: AssetDeps = assetDeps[id]?.deps ?? { js: [], css: [] } clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps( mergeAssetDeps(deps, entry.deps), + manager, ) } let bootstrapScriptContent: string | RuntimeAsset @@ -911,9 +916,9 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; return '' }, }, - ...vitePluginRscMinimal({ ...rscPluginOptions, manager }), + ...vitePluginRscMinimal(rscPluginOptions, manager), ...vitePluginFindSourceMapURL(), - ...vitePluginRscCss({ rscCssTransform: rscPluginOptions.rscCssTransform }), + ...vitePluginRscCss(rscPluginOptions, manager), ...(rscPluginOptions.validateImports !== false ? [validateImportPlugin()] : []), @@ -970,6 +975,7 @@ function vitePluginUseClient( RscPluginOptions, 'keepUseCientProxy' | 'environment' >, + manager: RscPluginManager, ): Plugin[] { const packageSources = new Map() @@ -1048,7 +1054,7 @@ function vitePluginUseClient( } else { importId = id referenceKey = hashString( - normalizePath(path.relative(config.root, id)), + normalizePath(path.relative(manager.config.root, id)), ) } } @@ -1354,6 +1360,7 @@ function vitePluginUseServer( RscPluginOptions, 'enableActionEncryption' | 'environment' >, + manager: RscPluginManager, ): Plugin[] { const serverEnvironmentName = useServerPluginOptions.environment?.rsc ?? 'rsc' const browserEnvironmentName = @@ -1383,8 +1390,8 @@ function vitePluginUseServer( ) id = cleanUrl(id) } - if (config.command === 'build') { - normalizedId_ = hashString(path.relative(config.root, id)) + if (manager.config.command === 'build') { + normalizedId_ = hashString(path.relative(manager.config.root, id)) } else { normalizedId_ = normalizeViteImportAnalysisUrl( server.environments[serverEnvironmentName]!, @@ -1556,13 +1563,13 @@ function serializeValueWithRuntime(value: any) { return result } -function assetsURL(url: string) { +function assetsURL(url: string, manager: RscPluginManager) { if ( - config.command === 'build' && - typeof config.experimental?.renderBuiltUrl === 'function' + manager.config.command === 'build' && + typeof manager.config.experimental?.renderBuiltUrl === 'function' ) { // https://github.com/vitejs/vite/blob/bdde0f9e5077ca1a21a04eefc30abad055047226/packages/vite/src/node/build.ts#L1369 - const result = config.experimental.renderBuiltUrl(url, { + const result = manager.config.experimental.renderBuiltUrl(url, { type: 'asset', hostType: 'js', ssr: true, @@ -1583,18 +1590,18 @@ function assetsURL(url: string) { } // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 - return config.base + url + return manager.config.base + url } -function assetsURLOfDeps(deps: AssetDeps) { +function assetsURLOfDeps(deps: AssetDeps, manager: RscPluginManager) { return { js: deps.js.map((href) => { assert(typeof href === 'string') - return assetsURL(href) + return assetsURL(href, manager) }), css: deps.css.map((href) => { assert(typeof href === 'string') - return assetsURL(href) + return assetsURL(href, manager) }), } } @@ -1685,8 +1692,9 @@ function collectAssetDepsInner( // css support // -export function vitePluginRscCss( - rscCssOptions?: Pick, +function vitePluginRscCss( + rscCssOptions: Pick = {}, + manager: RscPluginManager, ): Plugin[] { function hasSpecialCssQuery(id: string): boolean { return /[?&](url|inline|raw)(\b|=|&|$)/.test(id) @@ -1814,7 +1822,9 @@ export function vitePluginRscCss( for (const file of [mod.file, ...result.visitedFiles]) { this.addWatchFile(file) } - const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1))) + const hrefs = result.hrefs.map((href) => + assetsURL(href.slice(1), manager), + ) return `export default ${serializeValueWithRuntime(hrefs)}` } }, @@ -1906,10 +1916,18 @@ export function vitePluginRscCss( '@id/__x00__virtual:vite-rsc/importer-resources-browser?importer=' + encodeURIComponent(importer), ] - const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs }) - return generateResourcesCode(serializeValueWithRuntime(deps)) + const deps = assetsURLOfDeps( + { css: cssHrefs, js: jsHrefs }, + manager, + ) + return generateResourcesCode( + serializeValueWithRuntime(deps), + manager, + ) } else { - const key = normalizePath(path.relative(config.root, importer)) + const key = normalizePath( + path.relative(manager.config.root, importer), + ) serverResourcesMetaMap[importer] = { key } return ` import __vite_rsc_assets_manifest__ from "virtual:vite-rsc/assets-manifest"; @@ -1917,6 +1935,7 @@ export function vitePluginRscCss( `__vite_rsc_assets_manifest__.serverResources[${JSON.stringify( key, )}]`, + manager, )} ` } @@ -2017,7 +2036,7 @@ function collectModuleDependents(mods: EnvironmentModuleNode[]) { return [...visited] } -function generateResourcesCode(depsCode: string) { +function generateResourcesCode(depsCode: string, manager: RscPluginManager) { const ResourcesFn = ( React: typeof import('react'), deps: ResolvedAssetDeps, @@ -2054,7 +2073,7 @@ function generateResourcesCode(depsCode: string) { import __vite_rsc_react__ from "react"; ${ - config.command === 'serve' + manager.config.command === 'serve' ? `import RemoveDuplicateServerCss from "virtual:vite-rsc/remove-duplicate-server-css";` : `const RemoveDuplicateServerCss = undefined;` } From 48c17b1bc8554137e670a35253bdc770c780ffd3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:30:29 +0900 Subject: [PATCH 08/15] refactor: server --- packages/plugin-rsc/src/plugin.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 92e9de6ea..6f04f61c8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -46,7 +46,7 @@ import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' // state for build orchestration let serverReferences: Record = {} -let server: ViteDevServer +// let server: ViteDevServer // let config: ResolvedConfig // let rscBundle: Rollup.OutputBundle // let buildAssetsManifest: AssetsManifest | undefined @@ -189,7 +189,7 @@ export function vitePluginRscMinimal( } }, configureServer(server_) { - server = server_ + manager.server = server_ }, }, { @@ -382,7 +382,7 @@ export default function vitePluginRsc( } }, buildApp: rscPluginOptions.useBuildAppHook ? buildApp : undefined, - configureServer() { + configureServer(server) { ;(globalThis as any).__viteRscDevServer = server if (rscPluginOptions.disableServerHandler) return @@ -501,7 +501,7 @@ export default function vitePluginRsc( try { await this.environment.transformRequest(mod.url) } catch (e) { - server.environments.client.hot.send({ + manager.server.environments.client.hot.send({ type: 'error', err: prepareError(e as any), }) @@ -559,6 +559,7 @@ export default function vitePluginRsc( name: 'rsc:load-environment-module', async transform(code) { if (!code.includes('import.meta.viteRsc.loadModule')) return + const { server } = manager const s = new MagicString(code) for (const match of code.matchAll( /import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg, @@ -1047,7 +1048,7 @@ function vitePluginUseClient( } else { if (this.environment.mode === 'dev') { importId = normalizeViteImportAnalysisUrl( - server.environments[browserEnvironmentName]!, + manager.server.environments[browserEnvironmentName]!, id, ) referenceKey = importId @@ -1394,7 +1395,7 @@ function vitePluginUseServer( normalizedId_ = hashString(path.relative(manager.config.root, id)) } else { normalizedId_ = normalizeViteImportAnalysisUrl( - server.environments[serverEnvironmentName]!, + manager.server.environments[serverEnvironmentName]!, id, ) } @@ -1812,6 +1813,7 @@ function vitePluginRscCss( async load(id) { if (id.startsWith('\0virtual:vite-rsc/css/dev-ssr/')) { id = id.slice('\0virtual:vite-rsc/css/dev-ssr/'.length) + const { server } = manager const mod = await server.environments.ssr.moduleGraph.getModuleByUrl(id) if (!mod?.id || !mod?.file) { @@ -1905,6 +1907,7 @@ function vitePluginRscCss( } }, load(id) { + const { server } = manager if (id.startsWith('\0virtual:vite-rsc/importer-resources?importer=')) { const importer = decodeURIComponent( parseIdQuery(id).query['importer']!, @@ -1963,6 +1966,7 @@ function vitePluginRscCss( }, hotUpdate(ctx) { if (this.environment.name === 'rsc') { + const { server } = manager const mods = collectModuleDependents(ctx.modules) for (const mod of mods) { if (mod.id) { From 84493176efebbb753f9a39c5780b5e555c299852 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:34:02 +0900 Subject: [PATCH 09/15] refactor: serverReferences --- packages/plugin-rsc/src/plugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 6f04f61c8..8c7c86577 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -45,7 +45,7 @@ import { validateImportPlugin } from './plugins/validate-import' import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' // state for build orchestration -let serverReferences: Record = {} +// let serverReferences: Record = {} // let server: ViteDevServer // let config: ResolvedConfig // let rscBundle: Rollup.OutputBundle @@ -1426,7 +1426,7 @@ function vitePluginUseServer( : undefined, }) if (!output.hasChanged()) return - serverReferences[getNormalizedId()] = id + manager.serverReferences[getNormalizedId()] = id const importSource = resolvePackage(`${PKG_NAME}/react/rsc`) output.prepend(`import * as $$ReactServer from "${importSource}";\n`) if (enableEncryption) { @@ -1463,7 +1463,7 @@ function vitePluginUseServer( }) const output = result?.output if (!output?.hasChanged()) return - serverReferences[getNormalizedId()] = id + manager.serverReferences[getNormalizedId()] = id const name = this.environment.name === browserEnvironmentName ? 'browser' : 'ssr' const importSource = resolvePackage(`${PKG_NAME}/react/${name}`) @@ -1479,7 +1479,7 @@ function vitePluginUseServer( if (this.environment.mode === 'dev') { return { code: `export {}`, map: null } } - const code = generateDynamicImportCode(serverReferences) + const code = generateDynamicImportCode(manager.serverReferences) return { code, map: null } }), ] From 6a00ffd0815cd03a5aaf88d53f4b54b8dd909ff8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:35:21 +0900 Subject: [PATCH 10/15] refactor: serverResourcesMetaMap --- packages/plugin-rsc/src/plugin.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 8c7c86577..f95b9138c 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -64,7 +64,7 @@ type ClientReferenceMeta = { } let clientReferenceMetaMap: Record = {} -let serverResourcesMetaMap: Record = {} +// let serverResourcesMetaMap: Record = {} const PKG_NAME = '@vitejs/plugin-rsc' const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom` @@ -232,7 +232,9 @@ export default function vitePluginRsc( await builder.build(builder.environments.rsc!) // sort for stable build clientReferenceMetaMap = sortObject(clientReferenceMetaMap) - serverResourcesMetaMap = sortObject(serverResourcesMetaMap) + manager.serverResourcesMetaMap = sortObject( + manager.serverResourcesMetaMap, + ) await builder.build(builder.environments.client!) writeAssetsManifest(['rsc']) return @@ -250,7 +252,7 @@ export default function vitePluginRsc( await builder.build(builder.environments.rsc!) // sort for stable build clientReferenceMetaMap = sortObject(clientReferenceMetaMap) - serverResourcesMetaMap = sortObject(serverResourcesMetaMap) + manager.serverResourcesMetaMap = sortObject(manager.serverResourcesMetaMap) await builder.build(builder.environments.client!) await builder.build(builder.environments.ssr!) writeAssetsManifest(['ssr', 'rsc']) @@ -745,7 +747,9 @@ export default function vitePluginRsc( const serverResources: Record = {} const rscAssetDeps = collectAssetDeps(manager.rscBundle) - for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { + for (const [id, meta] of Object.entries( + manager.serverResourcesMetaMap, + )) { serverResources[meta.key] = assetsURLOfDeps( { js: [], @@ -1931,7 +1935,7 @@ function vitePluginRscCss( const key = normalizePath( path.relative(manager.config.root, importer), ) - serverResourcesMetaMap[importer] = { key } + manager.serverResourcesMetaMap[importer] = { key } return ` import __vite_rsc_assets_manifest__ from "virtual:vite-rsc/assets-manifest"; ${generateResourcesCode( From eaed94df428b133312340c9665160450a66088e3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 12:38:36 +0900 Subject: [PATCH 11/15] refactor: clientReferenceMetaMap --- packages/plugin-rsc/src/plugin.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f95b9138c..19e6c05cd 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -62,7 +62,7 @@ type ClientReferenceMeta = { exportNames: string[] renderedExports: string[] } -let clientReferenceMetaMap: Record = {} +// let clientReferenceMetaMap: Record = {} // let serverResourcesMetaMap: Record = {} @@ -231,7 +231,9 @@ export default function vitePluginRsc( builder.environments.client!.config.build.write = true await builder.build(builder.environments.rsc!) // sort for stable build - clientReferenceMetaMap = sortObject(clientReferenceMetaMap) + manager.clientReferenceMetaMap = sortObject( + manager.clientReferenceMetaMap, + ) manager.serverResourcesMetaMap = sortObject( manager.serverResourcesMetaMap, ) @@ -251,7 +253,7 @@ export default function vitePluginRsc( builder.environments.ssr!.config.build.write = true await builder.build(builder.environments.rsc!) // sort for stable build - clientReferenceMetaMap = sortObject(clientReferenceMetaMap) + manager.clientReferenceMetaMap = sortObject(manager.clientReferenceMetaMap) manager.serverResourcesMetaMap = sortObject(manager.serverResourcesMetaMap) await builder.build(builder.environments.client!) await builder.build(builder.environments.ssr!) @@ -470,7 +472,7 @@ export default function vitePluginRsc( const visited = new Set() function recurse(mod: EnvironmentModuleNode): boolean { if (!mod.id) return false - if (clientReferenceMetaMap[mod.id]) return true + if (manager.clientReferenceMetaMap[mod.id]) return true if (visited.has(mod.id)) return false visited.add(mod.id) for (const importer of mod.importers) { @@ -766,7 +768,9 @@ export default function vitePluginRsc( assert(entry) const entryUrl = assetsURL(entry.chunk.fileName, manager) const clientReferenceDeps: Record = {} - for (const [id, meta] of Object.entries(clientReferenceMetaMap)) { + for (const [id, meta] of Object.entries( + manager.clientReferenceMetaMap, + )) { const deps: AssetDeps = assetDeps[id]?.deps ?? { js: [], css: [] } clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps( mergeAssetDeps(deps, entry.deps), @@ -1090,7 +1094,7 @@ function vitePluginUseClient( }) if (!result) return const { output, exportNames } = result - clientReferenceMetaMap[id] = { + manager.clientReferenceMetaMap[id] = { importId, referenceKey, packageSource, @@ -1107,7 +1111,7 @@ function vitePluginUseClient( return { code: `export default {}`, map: null } } let code = '' - for (const meta of Object.values(clientReferenceMetaMap)) { + for (const meta of Object.values(manager.clientReferenceMetaMap)) { // vite/rollup can apply tree-shaking to dynamic import of this form const key = JSON.stringify(meta.referenceKey) const id = JSON.stringify(meta.importId) @@ -1169,7 +1173,7 @@ function vitePluginUseClient( const source = id.slice( '\0virtual:vite-rsc/client-package-proxy/'.length, ) - const meta = Object.values(clientReferenceMetaMap).find( + const meta = Object.values(manager.clientReferenceMetaMap).find( (v) => v.packageSource === source, )! const exportNames = meta.exportNames @@ -1186,7 +1190,7 @@ function vitePluginUseClient( for (const chunk of Object.values(bundle)) { if (chunk.type === 'chunk') { for (const [id, mod] of Object.entries(chunk.modules)) { - const meta = clientReferenceMetaMap[id] + const meta = manager.clientReferenceMetaMap[id] if (meta) { meta.renderedExports = mod.renderedExports } From 4f59f9f158354fc225971b08c84098784867d9ce Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 13:25:27 +0900 Subject: [PATCH 12/15] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 32 ++++++++++--------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 19e6c05cd..e4a3bfa29 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -44,28 +44,8 @@ import { transformScanBuildStrip } from './plugins/scan' import { validateImportPlugin } from './plugins/validate-import' import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' -// state for build orchestration -// let serverReferences: Record = {} -// let server: ViteDevServer -// let config: ResolvedConfig -// let rscBundle: Rollup.OutputBundle -// let buildAssetsManifest: AssetsManifest | undefined -// let isScanBuild = false const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' -type ClientReferenceMeta = { - importId: string - // same as `importId` during dev. hashed id during build. - referenceKey: string - packageSource?: string - // build only for tree-shaking unused export - exportNames: string[] - renderedExports: string[] -} -// let clientReferenceMetaMap: Record = {} - -// let serverResourcesMetaMap: Record = {} - const PKG_NAME = '@vitejs/plugin-rsc' const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom` @@ -80,8 +60,6 @@ function resolvePackage(name: string) { return pathToFileURL(require.resolve(name)).href } -export type { RscPluginManager } - class RscPluginManager { server!: ViteDevServer config!: ResolvedConfig @@ -93,6 +71,16 @@ class RscPluginManager { serverResourcesMetaMap: Record = {} } +type ClientReferenceMeta = { + importId: string + // same as `importId` during dev. hashed id during build. + referenceKey: string + packageSource?: string + // build only for tree-shaking unused export + exportNames: string[] + renderedExports: string[] +} + export type RscPluginOptions = { /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` From fc519eb4bb1862eb32ee838e3617a1dc1d009ed1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 14:37:41 +0900 Subject: [PATCH 13/15] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index e4a3bfa29..708f7e655 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -46,6 +46,16 @@ import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' +type ClientReferenceMeta = { + importId: string + // same as `importId` during dev. hashed id during build. + referenceKey: string + packageSource?: string + // build only for tree-shaking unused export + exportNames: string[] + renderedExports: string[] +} + const PKG_NAME = '@vitejs/plugin-rsc' const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom` @@ -71,16 +81,6 @@ class RscPluginManager { serverResourcesMetaMap: Record = {} } -type ClientReferenceMeta = { - importId: string - // same as `importId` during dev. hashed id during build. - referenceKey: string - packageSource?: string - // build only for tree-shaking unused export - exportNames: string[] - renderedExports: string[] -} - export type RscPluginOptions = { /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` From 7daccf13c815ad334bf147d6e35aa5d000ec31f8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 15:22:20 +0900 Subject: [PATCH 14/15] test: add e2e --- packages/plugin-rsc/e2e/starter.test.ts | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 42f09e714..3c64d5470 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test' import { setupInlineFixture, useFixture, type Fixture } from './fixture' import { defineStarterTest } from './starter' import { expectNoPageError, waitForHydration } from './helper' +import { x } from 'tinyexec' test.describe('dev-default', () => { const f = useFixture({ root: 'examples/starter', mode: 'dev' }) @@ -94,3 +95,49 @@ test.describe('duplicate loadCss', () => { }) } }) + +test.describe('isolated build', () => { + const root = 'examples/e2e/temp/isolated-build' + + test.beforeAll(async () => { + // build twice programmatically to verify two plugin states are independent + async function testFn() { + const vite = await import('vite') + const fs = await import('node:fs') + + console.log('======== first build ========') + const builder1 = await vite.createBuilder() + await builder1.buildApp() + + // edit files to remove client references + fs.rmSync(`src/client.tsx`) + fs.writeFileSync( + `src/root.tsx`, + fs + .readFileSync(`src/root.tsx`, 'utf-8') + .replace(`import { ClientCounter } from './client.tsx'`, '') + .replace(``, ''), + ) + + console.log('======== second build ========') + const builder2 = await vite.createBuilder() + await builder2.buildApp() + } + + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'test.js': `await (${testFn.toString()})();\n`, + }, + }) + }) + + test('build', async () => { + const result = await x('node', ['./test.js'], { + nodeOptions: { cwd: root }, + }) + expect(result.stderr).not.toContain('Build failed') + expect(result.exitCode).toBe(0) + }) +}) From 6189de76513fd7b0491cd359eb365602f23ae7b5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 21 Aug 2025 15:26:11 +0900 Subject: [PATCH 15/15] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 708f7e655..d00343015 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -601,6 +601,7 @@ export default function vitePluginRsc( }, renderChunk(code, chunk) { if (!code.includes('__vite_rsc_load_module')) return + const { config } = manager const s = new MagicString(code) for (const match of code.matchAll( /['"]__vite_rsc_load_module:(\w+):(\w+):(\w+)['"]/dg, @@ -609,12 +610,12 @@ export default function vitePluginRsc( const importPath = normalizeRelativePath( path.relative( path.join( - manager.config.environments[fromEnv!]!.build.outDir, + config.environments[fromEnv!]!.build.outDir, chunk.fileName, '..', ), path.join( - manager.config.environments[toEnv!]!.build.outDir, + config.environments[toEnv!]!.build.outDir, // TODO: this breaks when custom entyFileNames `${entryName}.js`, ), @@ -1561,12 +1562,13 @@ function serializeValueWithRuntime(value: any) { } function assetsURL(url: string, manager: RscPluginManager) { + const { config } = manager if ( - manager.config.command === 'build' && - typeof manager.config.experimental?.renderBuiltUrl === 'function' + config.command === 'build' && + typeof config.experimental?.renderBuiltUrl === 'function' ) { // https://github.com/vitejs/vite/blob/bdde0f9e5077ca1a21a04eefc30abad055047226/packages/vite/src/node/build.ts#L1369 - const result = manager.config.experimental.renderBuiltUrl(url, { + const result = config.experimental.renderBuiltUrl(url, { type: 'asset', hostType: 'js', ssr: true, @@ -1587,7 +1589,7 @@ function assetsURL(url: string, manager: RscPluginManager) { } // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 - return manager.config.base + url + return config.base + url } function assetsURLOfDeps(deps: AssetDeps, manager: RscPluginManager) {