Skip to content

Commit c40234e

Browse files
hi-ogawaclaude
andauthored
feat(rsc): ability to merge client reference chunks (#766)
Co-authored-by: Claude <[email protected]>
1 parent 6924db4 commit c40234e

File tree

8 files changed

+168
-24
lines changed

8 files changed

+168
-24
lines changed

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
waitForHydration,
1010
} from './helper'
1111
import { x } from 'tinyexec'
12+
import { normalizePath, type Rollup } from 'vite'
13+
import path from 'node:path'
1214

1315
test.describe('dev-default', () => {
1416
const f = useFixture({ root: 'examples/basic', mode: 'dev' })
@@ -45,6 +47,17 @@ test.describe('dev-initial', () => {
4547
test.describe('build-default', () => {
4648
const f = useFixture({ root: 'examples/basic', mode: 'build' })
4749
defineTest(f)
50+
51+
test('custom client chunk', async () => {
52+
const { chunks }: { chunks: Rollup.OutputChunk[] } = JSON.parse(
53+
f.createEditor('dist/client/.vite/test.json').read(),
54+
)
55+
const chunk = chunks.find((c) => c.name === 'custom-chunk')
56+
const expected = [1, 2, 3].map((i) =>
57+
normalizePath(path.join(f.root, `src/routes/chunk/client${i}.tsx`)),
58+
)
59+
expect(chunk?.moduleIds).toEqual(expect.arrayContaining(expected))
60+
})
4861
})
4962

5063
test.describe('dev-non-optimized-cjs', () => {
@@ -143,7 +156,7 @@ test.describe('build-stable-chunks', () => {
143156
.sort()
144157
expect(newChunks).toEqual([
145158
'src/framework/entry.browser.tsx',
146-
'src/routes/client.tsx',
159+
'virtual:vite-rsc/client-references/group/src/routes/client.tsx',
147160
])
148161
expect(oldChunks).toEqual(newChunks)
149162
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client'
2+
3+
export function TestClientChunk1() {
4+
return <span>test-chunk1</span>
5+
}
6+
7+
export function TestClientChunkConflict() {
8+
return <span>test-chunk-conflict1</span>
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export function TestClientChunk2() {
4+
return <span>test-chunk2</span>
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client'
2+
3+
export function TestClientChunk3() {
4+
return <span>test-chunk3</span>
5+
}
6+
7+
export function TestClientChunkConflict() {
8+
return <span>test-chunk-conflict3</span>
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
TestClientChunk1,
3+
TestClientChunkConflict as TestClientChunkConflict1,
4+
} from './client1'
5+
import { TestClientChunk2 } from './client2'
6+
import {
7+
TestClientChunk3,
8+
TestClientChunkConflict as TestClientChunkConflict3,
9+
} from './client3'
10+
11+
export function TestClientChunkServer() {
12+
return (
13+
<div data-testid="test-client-chunk">
14+
<TestClientChunk1 />|
15+
<TestClientChunkConflict1 />|
16+
<TestClientChunk2 />|
17+
<TestClientChunk3 />|
18+
<TestClientChunkConflict3 />
19+
</div>
20+
)
21+
}

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { TestAssetsServer } from './assets/server'
4141
import { TestHmrSwitchServer } from './hmr-switch/server'
4242
import { TestHmrSwitchClient } from './hmr-switch/client'
4343
import { TestTreeShakeServer } from './tree-shake/server'
44+
import { TestClientChunkServer } from './chunk/server'
4445

4546
export function Root(props: { url: URL }) {
4647
return (
@@ -95,6 +96,7 @@ export function Root(props: { url: URL }) {
9596
<TestImportMetaGlob />
9697
<TestAssetsServer />
9798
<TestTreeShakeServer />
99+
<TestClientChunkServer />
98100
</body>
99101
</html>
100102
)

packages/plugin-rsc/examples/basic/vite.config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from 'vite'
1212
// import inspect from 'vite-plugin-inspect'
1313
import path from 'node:path'
14+
import fs from 'node:fs'
1415
import { fileURLToPath } from 'node:url'
1516

1617
export default defineConfig({
@@ -30,6 +31,11 @@ export default defineConfig({
3031
rscCssTransform: false,
3132
copyServerAssetsToClient: (fileName) =>
3233
fileName !== '__server_secret.txt',
34+
clientChunks(id) {
35+
if (id.includes('/src/routes/chunk/')) {
36+
return 'custom-chunk'
37+
}
38+
},
3339
}),
3440
{
3541
name: 'test-tree-shake',
@@ -43,6 +49,23 @@ export default defineConfig({
4349
}
4450
},
4551
},
52+
{
53+
// dump entire bundle to analyze build output for e2e
54+
name: 'test-metadata',
55+
enforce: 'post',
56+
writeBundle(options, bundle) {
57+
const chunks: Rollup.OutputChunk[] = []
58+
for (const chunk of Object.values(bundle)) {
59+
if (chunk.type === 'chunk') {
60+
chunks.push(chunk)
61+
}
62+
}
63+
fs.writeFileSync(
64+
path.join(options.dir!, '.vite/test.json'),
65+
JSON.stringify({ chunks }, null, 2),
66+
)
67+
},
68+
},
4669
{
4770
name: 'test-server-assets-security',
4871
buildStart() {

packages/plugin-rsc/src/plugin.ts

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class RscPluginManager {
9393
buildAssetsManifest: AssetsManifest | undefined
9494
isScanBuild: boolean = false
9595
clientReferenceMetaMap: Record<string, ClientReferenceMeta> = {}
96+
clientReferenceGroups: Record</* group name*/ string, ClientReferenceMeta[]> =
97+
{}
9698
serverReferenceMetaMap: Record<string, ServerRerferenceMeta> = {}
9799
serverResourcesMetaMap: Record<string, { key: string }> = {}
98100

@@ -176,6 +178,17 @@ export type RscPluginOptions = {
176178
ssr?: string
177179
rsc?: string
178180
}
181+
182+
/**
183+
* Custom chunking strategy for client reference modules.
184+
*
185+
* This function allows you to group multiple client components into
186+
* custom chunks instead of having each module in its own chunk.
187+
*
188+
* @param id - The absolute path of the client module
189+
* @returns The chunk name to group this module with, or undefined to use default behavior
190+
*/
191+
clientChunks?: (id: string) => string | undefined
179192
}
180193

181194
/** @experimental */
@@ -977,7 +990,7 @@ function scanBuildStripPlugin({
977990
function vitePluginUseClient(
978991
useClientPluginOptions: Pick<
979992
RscPluginOptions,
980-
'keepUseCientProxy' | 'environment'
993+
'keepUseCientProxy' | 'environment' | 'clientChunks'
981994
>,
982995
manager: RscPluginManager,
983996
): Plugin[] {
@@ -1107,28 +1120,77 @@ function vitePluginUseClient(
11071120
return { code: output.toString(), map: { mappings: '' } }
11081121
},
11091122
},
1110-
createVirtualPlugin('vite-rsc/client-references', function () {
1111-
if (this.environment.mode === 'dev') {
1112-
return { code: `export default {}`, map: null }
1113-
}
1114-
let code = ''
1115-
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
1116-
// vite/rollup can apply tree-shaking to dynamic import of this form
1117-
const key = JSON.stringify(meta.referenceKey)
1118-
const id = JSON.stringify(meta.importId)
1119-
const exports = meta.renderedExports
1120-
.map((name) => (name === 'default' ? 'default: _default' : name))
1121-
.sort()
1122-
code += `
1123-
${key}: async () => {
1124-
const {${exports}} = await import(${id});
1125-
return {${exports}};
1126-
},
1127-
`
1128-
}
1129-
code = `export default {${code}};\n`
1130-
return { code, map: null }
1131-
}),
1123+
{
1124+
name: 'rsc:use-client/build-references',
1125+
resolveId(source) {
1126+
if (source.startsWith('virtual:vite-rsc/client-references')) {
1127+
return '\0' + source
1128+
}
1129+
},
1130+
load(id) {
1131+
if (id === '\0virtual:vite-rsc/client-references') {
1132+
// not used during dev
1133+
if (this.environment.mode === 'dev') {
1134+
return { code: `export default {}`, map: null }
1135+
}
1136+
// no custom chunking needed for scan
1137+
if (manager.isScanBuild) {
1138+
let code = ``
1139+
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
1140+
code += `import ${JSON.stringify(meta.importId)};\n`
1141+
}
1142+
return { code, map: null }
1143+
}
1144+
let code = ''
1145+
// group client reference modules by `clientChunks` option
1146+
manager.clientReferenceGroups = {}
1147+
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
1148+
const name =
1149+
useClientPluginOptions.clientChunks?.(meta.importId) ||
1150+
// use original module id as name by default
1151+
normalizePath(path.relative(manager.config.root, meta.importId))
1152+
const group = (manager.clientReferenceGroups[name] ??= [])
1153+
group.push(meta)
1154+
}
1155+
for (const [name, metas] of Object.entries(
1156+
manager.clientReferenceGroups,
1157+
)) {
1158+
const groupVirtual = `virtual:vite-rsc/client-references/group/${name}`
1159+
for (const meta of metas) {
1160+
code += `\
1161+
${JSON.stringify(meta.referenceKey)}: async () => {
1162+
const m = await import(${JSON.stringify(groupVirtual)});
1163+
return m.export_${meta.referenceKey};
1164+
},
1165+
`
1166+
}
1167+
}
1168+
code = `export default {${code}};\n`
1169+
return { code, map: null }
1170+
}
1171+
// re-export client reference modules from each group
1172+
if (id.startsWith('\0virtual:vite-rsc/client-references/group/')) {
1173+
const name = id.slice(
1174+
'\0virtual:vite-rsc/client-references/group/'.length,
1175+
)
1176+
const metas = manager.clientReferenceGroups[name]
1177+
assert(metas, `unknown client reference group: ${name}`)
1178+
let code = ``
1179+
for (const meta of metas) {
1180+
// pick only renderedExports to tree-shake unused client references
1181+
const exports = meta.renderedExports
1182+
.map((name) => `${name}: import_${meta.referenceKey}.${name},\n`)
1183+
.sort()
1184+
.join('')
1185+
code += `
1186+
import * as import_${meta.referenceKey} from ${JSON.stringify(meta.importId)};
1187+
export const export_${meta.referenceKey} = {${exports}};
1188+
`
1189+
}
1190+
return { code, map: null }
1191+
}
1192+
},
1193+
},
11321194
{
11331195
name: 'rsc:virtual-client-in-server-package',
11341196
async load(id) {

0 commit comments

Comments
 (0)