Skip to content

Commit 35004e3

Browse files
hi-ogawaclaude
andauthored
fix(rsc): support nested RSC outDir inside SSR outDir (#1053)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8527360 commit 35004e3

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { expect, test } from '@playwright/test'
4+
import { setupInlineFixture, useFixture } from './fixture'
5+
import { defineStarterTest } from './starter'
6+
7+
test.describe(() => {
8+
const root = 'examples/e2e/temp/nested-outDir'
9+
10+
test.beforeAll(async () => {
11+
await setupInlineFixture({
12+
src: 'examples/starter',
13+
dest: root,
14+
files: {
15+
'vite.config.base.ts': { cp: 'vite.config.ts' },
16+
'vite.config.ts': /* js */ `
17+
import baseConfig from './vite.config.base.ts'
18+
19+
// Modify baseConfig to use nested outDir (rsc inside ssr)
20+
baseConfig.environments.rsc.build.outDir = './dist/server/rsc'
21+
baseConfig.environments.ssr.build.outDir = './dist/server'
22+
23+
export default baseConfig
24+
`,
25+
},
26+
})
27+
})
28+
29+
test.describe('build-nested-outDir', () => {
30+
const f = useFixture({ root, mode: 'build' })
31+
defineStarterTest(f)
32+
33+
test('verify nested outDir structure', () => {
34+
// RSC output exists inside SSR outDir
35+
expect(fs.existsSync(path.join(f.root, 'dist/server/rsc/index.js'))).toBe(
36+
true,
37+
)
38+
expect(
39+
fs.existsSync(
40+
path.join(f.root, 'dist/server/rsc/__vite_rsc_assets_manifest.js'),
41+
),
42+
).toBe(true)
43+
// SSR output exists
44+
expect(fs.existsSync(path.join(f.root, 'dist/server/index.js'))).toBe(
45+
true,
46+
)
47+
expect(
48+
fs.existsSync(
49+
path.join(f.root, 'dist/server/__vite_rsc_assets_manifest.js'),
50+
),
51+
).toBe(true)
52+
})
53+
})
54+
})

packages/plugin-rsc/src/plugin.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,20 @@ export default function vitePluginRsc(
347347
return
348348
}
349349

350+
// Check if RSC outDir is inside SSR outDir to avoid SSR build overwriting RSC output
351+
const rscOutDir = builder.environments.rsc!.config.build.outDir
352+
const ssrOutDir = builder.environments.ssr!.config.build.outDir
353+
const rscInsideSsr = path
354+
.normalize(rscOutDir)
355+
.startsWith(path.normalize(ssrOutDir) + path.sep)
356+
357+
const tempRscOutDir = path.join(
358+
builder.config.root,
359+
'node_modules',
360+
'.vite-rsc-temp',
361+
'rsc',
362+
)
363+
350364
// rsc -> ssr -> rsc -> client -> ssr
351365
manager.isScanBuild = true
352366
builder.environments.rsc!.config.build.write = false
@@ -360,11 +374,31 @@ export default function vitePluginRsc(
360374
builder.environments.ssr!.config.build.write = true
361375
logStep('[3/5] build rsc environment...')
362376
await builder.build(builder.environments.rsc!)
377+
378+
// Evacuate RSC output to temp before SSR build overwrites it
379+
if (rscInsideSsr) {
380+
if (fs.existsSync(tempRscOutDir)) {
381+
fs.rmSync(tempRscOutDir, { recursive: true })
382+
}
383+
fs.mkdirSync(path.dirname(tempRscOutDir), { recursive: true })
384+
fs.renameSync(rscOutDir, tempRscOutDir)
385+
}
386+
363387
manager.stabilize()
364388
logStep('[4/5] build client environment...')
365389
await builder.build(builder.environments.client!)
366390
logStep('[5/5] build ssr environment...')
367391
await builder.build(builder.environments.ssr!)
392+
393+
// Restore RSC output from temp after SSR build
394+
if (rscInsideSsr) {
395+
if (fs.existsSync(rscOutDir)) {
396+
fs.rmSync(rscOutDir, { recursive: true })
397+
}
398+
fs.mkdirSync(path.dirname(rscOutDir), { recursive: true })
399+
fs.renameSync(tempRscOutDir, rscOutDir)
400+
}
401+
368402
writeAssetsManifest(['ssr', 'rsc'])
369403
}
370404

0 commit comments

Comments
 (0)