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)
+ })
+})
diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts
index 543a63622..d00343015 100644
--- a/packages/plugin-rsc/src/plugin.ts
+++ b/packages/plugin-rsc/src/plugin.ts
@@ -44,13 +44,6 @@ 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 = {
@@ -62,9 +55,6 @@ type ClientReferenceMeta = {
exportNames: string[]
renderedExports: string[]
}
-let clientReferenceMetaMap: Record* id */ string, ClientReferenceMeta> = {}
-
-let serverResourcesMetaMap: Record = {}
const PKG_NAME = '@vitejs/plugin-rsc'
const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom`
@@ -80,6 +70,17 @@ function resolvePackage(name: string) {
return pathToFileURL(require.resolve(name)).href
}
+class RscPluginManager {
+ server!: ViteDevServer
+ config!: ResolvedConfig
+ rscBundle!: Rollup.OutputBundle
+ buildAssetsManifest: AssetsManifest | undefined
+ isScanBuild: boolean = false
+ serverReferences: Record = {}
+ clientReferenceMetaMap: Record = {}
+ serverResourcesMetaMap: Record = {}
+}
+
export type RscPluginOptions = {
/**
* shorthand for configuring `environments.(name).build.rollupOptions.input.index`
@@ -158,6 +159,7 @@ export type RscPluginOptions = {
/** @experimental */
export function vitePluginRscMinimal(
rscPluginOptions: RscPluginOptions = {},
+ manager: RscPluginManager = new RscPluginManager(),
): Plugin[] {
return [
{
@@ -166,8 +168,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)) {
@@ -175,7 +177,7 @@ export function vitePluginRscMinimal(
}
},
configureServer(server_) {
- server = server_
+ manager.server = server_
},
},
{
@@ -192,8 +194,8 @@ export function vitePluginRscMinimal(
},
},
...vitePluginRscCore(),
- ...vitePluginUseClient(rscPluginOptions),
- ...vitePluginUseServer(rscPluginOptions),
+ ...vitePluginUseClient(rscPluginOptions, manager),
+ ...vitePluginUseServer(rscPluginOptions, manager),
...vitePluginDefineEncryptionKey(rscPluginOptions),
]
}
@@ -201,40 +203,46 @@ export function vitePluginRscMinimal(
export default function vitePluginRsc(
rscPluginOptions: RscPluginOptions = {},
): Plugin[] {
+ const manager = new RscPluginManager()
+
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!)
// sort for stable build
- clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
- serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
+ manager.clientReferenceMetaMap = sortObject(
+ manager.clientReferenceMetaMap,
+ )
+ manager.serverResourcesMetaMap = sortObject(
+ manager.serverResourcesMetaMap,
+ )
await builder.build(builder.environments.client!)
writeAssetsManifest(['rsc'])
return
}
// 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!)
// sort for stable build
- clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
- serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
+ manager.clientReferenceMetaMap = sortObject(manager.clientReferenceMetaMap)
+ manager.serverResourcesMetaMap = sortObject(manager.serverResourcesMetaMap)
await builder.build(builder.environments.client!)
await builder.build(builder.environments.ssr!)
writeAssetsManifest(['ssr', 'rsc'])
@@ -244,11 +252,11 @@ 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(
- config.environments[name]!.build.outDir,
+ manager.config.environments[name]!.build.outDir,
BUILD_ASSETS_MANIFEST_NAME,
)
fs.writeFileSync(manifestPath, assetsManifestCode)
@@ -366,7 +374,7 @@ export default function vitePluginRsc(
}
},
buildApp: rscPluginOptions.useBuildAppHook ? buildApp : undefined,
- configureServer() {
+ configureServer(server) {
;(globalThis as any).__viteRscDevServer = server
if (rscPluginOptions.disableServerHandler) return
@@ -409,7 +417,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
@@ -452,7 +460,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) {
@@ -485,7 +493,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),
})
@@ -543,6 +551,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,
@@ -592,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,
@@ -689,7 +699,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: {},
@@ -701,18 +714,18 @@ 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') {
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
: 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({
@@ -724,12 +737,17 @@ export default function vitePluginRsc(
}
const serverResources: Record = {}
- const rscAssetDeps = collectAssetDeps(rscBundle)
- for (const [id, meta] of Object.entries(serverResourcesMetaMap)) {
- serverResources[meta.key] = assetsURLOfDeps({
- js: [],
- css: rscAssetDeps[id]?.deps.css ?? [],
- })
+ const rscAssetDeps = collectAssetDeps(manager.rscBundle)
+ for (const [id, meta] of Object.entries(
+ manager.serverResourcesMetaMap,
+ )) {
+ serverResources[meta.key] = assetsURLOfDeps(
+ {
+ js: [],
+ css: rscAssetDeps[id]?.deps.css ?? [],
+ },
+ manager,
+ )
}
const assetDeps = collectAssetDeps(bundle)
@@ -737,12 +755,15 @@ 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)) {
+ 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),
+ manager,
)
}
let bootstrapScriptContent: string | RuntimeAsset
@@ -753,7 +774,7 @@ export default function vitePluginRsc(
`"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`,
)
}
- buildAssetsManifest = {
+ manager.buildAssetsManifest = {
bootstrapScriptContent,
clientReferenceDeps,
serverResources,
@@ -893,26 +914,30 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage;
return ''
},
},
- ...vitePluginRscMinimal(rscPluginOptions),
+ ...vitePluginRscMinimal(rscPluginOptions, manager),
...vitePluginFindSourceMapURL(),
- ...vitePluginRscCss({ rscCssTransform: rscPluginOptions.rscCssTransform }),
+ ...vitePluginRscCss(rscPluginOptions, manager),
...(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: '' } }
},
@@ -948,6 +973,7 @@ function vitePluginUseClient(
RscPluginOptions,
'keepUseCientProxy' | 'environment'
>,
+ manager: RscPluginManager,
): Plugin[] {
const packageSources = new Map()
@@ -1019,14 +1045,14 @@ function vitePluginUseClient(
} else {
if (this.environment.mode === 'dev') {
importId = normalizeViteImportAnalysisUrl(
- server.environments[browserEnvironmentName]!,
+ manager.server.environments[browserEnvironmentName]!,
id,
)
referenceKey = importId
} else {
importId = id
referenceKey = hashString(
- normalizePath(path.relative(config.root, id)),
+ normalizePath(path.relative(manager.config.root, id)),
)
}
}
@@ -1057,7 +1083,7 @@ function vitePluginUseClient(
})
if (!result) return
const { output, exportNames } = result
- clientReferenceMetaMap[id] = {
+ manager.clientReferenceMetaMap[id] = {
importId,
referenceKey,
packageSource,
@@ -1074,7 +1100,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)
@@ -1136,7 +1162,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
@@ -1153,7 +1179,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
}
@@ -1332,6 +1358,7 @@ function vitePluginUseServer(
RscPluginOptions,
'enableActionEncryption' | 'environment'
>,
+ manager: RscPluginManager,
): Plugin[] {
const serverEnvironmentName = useServerPluginOptions.environment?.rsc ?? 'rsc'
const browserEnvironmentName =
@@ -1361,11 +1388,11 @@ 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]!,
+ manager.server.environments[serverEnvironmentName]!,
id,
)
}
@@ -1396,7 +1423,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) {
@@ -1433,7 +1460,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}`)
@@ -1449,7 +1476,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 }
}),
]
@@ -1534,7 +1561,8 @@ function serializeValueWithRuntime(value: any) {
return result
}
-function assetsURL(url: string) {
+function assetsURL(url: string, manager: RscPluginManager) {
+ const { config } = manager
if (
config.command === 'build' &&
typeof config.experimental?.renderBuiltUrl === 'function'
@@ -1564,15 +1592,15 @@ function assetsURL(url: string) {
return 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)
}),
}
}
@@ -1663,8 +1691,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)
@@ -1782,6 +1811,7 @@ export 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) {
@@ -1792,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)}`
}
},
@@ -1873,6 +1905,7 @@ export function vitePluginRscCss(
}
},
load(id) {
+ const { server } = manager
if (id.startsWith('\0virtual:vite-rsc/importer-resources?importer=')) {
const importer = decodeURIComponent(
parseIdQuery(id).query['importer']!,
@@ -1884,17 +1917,26 @@ 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))
- serverResourcesMetaMap[importer] = { key }
+ const key = normalizePath(
+ path.relative(manager.config.root, importer),
+ )
+ manager.serverResourcesMetaMap[importer] = { key }
return `
import __vite_rsc_assets_manifest__ from "virtual:vite-rsc/assets-manifest";
${generateResourcesCode(
`__vite_rsc_assets_manifest__.serverResources[${JSON.stringify(
key,
)}]`,
+ manager,
)}
`
}
@@ -1922,6 +1964,7 @@ export 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) {
@@ -1995,7 +2038,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,
@@ -2032,7 +2075,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;`
}