From 70b20a1ed4eb1b4c36b5873e485d304fc506d909 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 16:52:27 +0900 Subject: [PATCH 1/8] feat(rsc): enable server-chunk-based client chunks --- packages/plugin-rsc/src/plugin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 5951ef80..81e43886 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -196,6 +196,8 @@ export type RscPluginOptions = { clientChunks?: (meta: { /** client reference module id */ id: string + /** normalized client reference module id */ + relativeId: string /** server chunk which includes a corresponding client reference proxy module */ serverChunk: string }) => string | undefined @@ -1190,10 +1192,9 @@ function vitePluginUseClient( let name = useClientPluginOptions.clientChunks?.({ id: meta.importId, + relativeId: manager.toRelativeId(meta.importId), serverChunk: meta.serverChunk!, - }) ?? - // use original module id as name by default - manager.toRelativeId(meta.importId) + }) ?? meta.serverChunk! // ensure clean virtual id to avoid interfering with other plugins name = cleanUrl(name.replaceAll('..', '__')) const group = (manager.clientReferenceGroups[name] ??= []) From 40cd4e10438e2c182baf2168b845c786f13793de Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 16:54:41 +0900 Subject: [PATCH 2/8] chore: jsdoc --- packages/plugin-rsc/src/plugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 81e43886..002445c8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -192,6 +192,7 @@ export type RscPluginOptions = { * * This function allows you to group multiple client components into * custom chunks instead of having each module in its own chunk. + * By default, client chunks are grouped by `meta.serverChunk`. */ clientChunks?: (meta: { /** client reference module id */ From 9f5107b2e2694f42028989e8bb5bcc697aebd059 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 17:07:21 +0900 Subject: [PATCH 3/8] tweak --- 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 002445c8..00385f55 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -198,7 +198,7 @@ export type RscPluginOptions = { /** client reference module id */ id: string /** normalized client reference module id */ - relativeId: string + normalizedId: string /** server chunk which includes a corresponding client reference proxy module */ serverChunk: string }) => string | undefined @@ -1193,7 +1193,7 @@ function vitePluginUseClient( let name = useClientPluginOptions.clientChunks?.({ id: meta.importId, - relativeId: manager.toRelativeId(meta.importId), + normalizedId: manager.toRelativeId(meta.importId), serverChunk: meta.serverChunk!, }) ?? meta.serverChunk! // ensure clean virtual id to avoid interfering with other plugins From 31411d9e1ff52435893ec825be47a23363c6fddd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 17:16:37 +0900 Subject: [PATCH 4/8] test: update --- packages/plugin-rsc/e2e/basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index a4378638..8d50ec00 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -181,7 +181,7 @@ test.describe('build-stable-chunks', () => { .sort() expect(newChunks).toEqual([ 'src/framework/entry.browser.tsx', - 'virtual:vite-rsc/client-references/group/src/routes/client.tsx', + 'virtual:vite-rsc/client-references/group/facade:src/routes/root.tsx', ]) expect(oldChunks).toEqual(newChunks) }) From 0d22354c93cdf57378f007db0f8b8b3e8ad336c8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 17:21:55 +0900 Subject: [PATCH 5/8] test: update --- packages/plugin-rsc/e2e/render-built-url.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/render-built-url.test.ts b/packages/plugin-rsc/e2e/render-built-url.test.ts index 565bb499..66721515 100644 --- a/packages/plugin-rsc/e2e/render-built-url.test.ts +++ b/packages/plugin-rsc/e2e/render-built-url.test.ts @@ -104,7 +104,9 @@ test.describe(() => { f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', 'utf-8', ) - expect(manifestFileContent).toContain(`__dynamicBase + "assets/client-`) + expect(manifestFileContent).toContain( + `__dynamicBase + "assets/entry.rsc-`, + ) }) }) }) From 97f92717ec825596cab109e518d707df24673462 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 17:23:07 +0900 Subject: [PATCH 6/8] test: update --- packages/plugin-rsc/e2e/starter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 3c64d547..1caff94d 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -45,7 +45,7 @@ test.describe('build-development', () => { test('verify development', async ({ page }) => { let output!: string page.on('response', async (response) => { - if (response.url().match(/\/assets\/client-[\w-]+\.js$/)) { + if (response.url().match(/\/assets\/entry.rsc-[\w-]+\.js$/)) { output = await response.text() } }) From b343287b3d0d4061424482edf2aae0a9a372f42b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 17:44:40 +0900 Subject: [PATCH 7/8] test: update --- packages/plugin-rsc/e2e/basic.test.ts | 21 ++++--------------- .../plugin-rsc/examples/basic/vite.config.ts | 9 ++++---- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 8d50ec00..02cec846 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -47,37 +47,24 @@ test.describe('dev-initial', () => { test.describe('build-default', () => { const f = useFixture({ root: 'examples/basic', mode: 'build' }) defineTest(f) - - test('custom client chunk', async () => { - const { chunks }: { chunks: Rollup.OutputChunk[] } = JSON.parse( - f.createEditor('dist/client/.vite/test.json').read(), - ) - const chunk = chunks.find((c) => c.name === 'custom-chunk') - const expected = [1, 2, 3].map((i) => - normalizePath(path.join(f.root, `src/routes/chunk/client${i}.tsx`)), - ) - expect(chunk?.moduleIds).toEqual(expect.arrayContaining(expected)) - }) }) -test.describe('build-server-client-chunks', () => { +test.describe('custom-client-chunks', () => { const f = useFixture({ root: 'examples/basic', mode: 'build', cliOptions: { env: { - TEST_SERVER_CLIENT_CHUNKS: 'true', + TEST_CUSTOM_CLIENT_CHUNKS: 'true', }, }, }) - defineTest(f) - - test('custom client chunk', async () => { + test('basic', async () => { const { chunks }: { chunks: Rollup.OutputChunk[] } = JSON.parse( f.createEditor('dist/client/.vite/test.json').read(), ) - const chunk = chunks.find((c) => c.name === 'root') + const chunk = chunks.find((c) => c.name === 'custom-chunk') const expected = [1, 2, 3].map((i) => normalizePath(path.join(f.root, `src/routes/chunk/client${i}.tsx`)), ) diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index a3c76c7e..144117f2 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -32,11 +32,10 @@ export default defineConfig({ copyServerAssetsToClient: (fileName) => fileName !== '__server_secret.txt', clientChunks(meta) { - if (process.env.TEST_SERVER_CLIENT_CHUNKS) { - return meta.serverChunk - } - if (meta.id.includes('/src/routes/chunk/')) { - return 'custom-chunk' + if (process.env.TEST_CUSTOM_CLIENT_CHUNKS) { + if (meta.id.includes('/src/routes/chunk/')) { + return 'custom-chunk' + } } }, }), From f8b5335150f898fb9283ea38b314f84abbf3d8ec Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 28 Aug 2025 18:39:37 +0900 Subject: [PATCH 8/8] test: e2e --- packages/plugin-rsc/e2e/basic.test.ts | 33 +++++++++++++++++++ .../basic/src/routes/chunk2/client1.tsx | 5 +++ .../basic/src/routes/chunk2/client2.tsx | 5 +++ .../basic/src/routes/chunk2/client2b.tsx | 5 +++ .../basic/src/routes/chunk2/client3.tsx | 5 +++ .../basic/src/routes/chunk2/server.tsx | 17 ++++++++++ .../basic/src/routes/chunk2/server2.tsx | 11 +++++++ .../basic/src/routes/chunk2/server3.tsx | 5 +++ .../basic/src/routes/chunk2/server4.tsx | 5 +++ .../examples/basic/src/routes/root.tsx | 2 ++ 10 files changed, 93 insertions(+) create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/client1.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/client2.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/client2b.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/client3.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/server.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/server2.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/server3.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/chunk2/server4.tsx diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 02cec846..99163871 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -47,6 +47,31 @@ test.describe('dev-initial', () => { test.describe('build-default', () => { const f = useFixture({ root: 'examples/basic', mode: 'build' }) defineTest(f) + + test('server-chunk-based client chunks', async () => { + const { chunks }: { chunks: Rollup.OutputChunk[] } = JSON.parse( + f.createEditor('dist/client/.vite/test.json').read(), + ) + const expectedGroups = { + 'facade:src/routes/chunk2/client1.tsx': ['src/routes/chunk2/client1.tsx'], + 'facade:src/routes/chunk2/server2.tsx': [ + 'src/routes/chunk2/client2.tsx', + 'src/routes/chunk2/client2b.tsx', + ], + 'shared:src/routes/chunk2/client3.tsx': ['src/routes/chunk2/client3.tsx'], + } + const actualGroups: Record = {} + for (const key in expectedGroups) { + const groupId = `\0virtual:vite-rsc/client-references/group/${key}` + const groupChunk = chunks.find((c) => c.facadeModuleId === groupId) + if (groupChunk) { + actualGroups[key] = groupChunk.moduleIds + .filter((id) => id !== groupId) + .map((id) => normalizePath(path.relative(f.root, id))) + } + } + expect(actualGroups).toEqual(expectedGroups) + }) }) test.describe('custom-client-chunks', () => { @@ -1421,4 +1446,12 @@ function defineTest(f: Fixture) { await testBackgroundImage('.test-assets-server-css') await testBackgroundImage('.test-assets-client-css') }) + + test('lazy', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('test-chunk2')).toHaveText( + 'test-chunk1|test-chunk2|test-chunk2b|test-chunk3|test-chunk3', + ) + }) } diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/client1.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client1.tsx new file mode 100644 index 00000000..0ffe9aad --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client1.tsx @@ -0,0 +1,5 @@ +'use client' + +export default function TestChunkClient1() { + return test-chunk1 +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2.tsx new file mode 100644 index 00000000..dc7dacf5 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2.tsx @@ -0,0 +1,5 @@ +'use client' + +export default function TestChunkClient2() { + return test-chunk2 +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2b.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2b.tsx new file mode 100644 index 00000000..9d03f2a0 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client2b.tsx @@ -0,0 +1,5 @@ +'use client' + +export default function TestChunkClient2b() { + return test-chunk2b +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/client3.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client3.tsx new file mode 100644 index 00000000..1baadec9 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/client3.tsx @@ -0,0 +1,5 @@ +'use client' + +export default function TestChunkClient3() { + return test-chunk3 +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server.tsx new file mode 100644 index 00000000..96039706 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +const TestChunkClient1 = React.lazy(() => import('./client1')) +const TestChunkServer2 = React.lazy(() => import('./server2')) +const TestChunkServer3 = React.lazy(() => import('./server3')) +const TestChunkServer4 = React.lazy(() => import('./server4')) + +export function TestChunk2() { + return ( +
+ | + | + | + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/server2.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server2.tsx new file mode 100644 index 00000000..693a828c --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server2.tsx @@ -0,0 +1,11 @@ +import TestChunkClient2 from './client2' +import TestChunkClient2b from './client2b' + +export default function TestChunkServer2() { + return ( + <> + | + + + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/server3.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server3.tsx new file mode 100644 index 00000000..e8a829b0 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server3.tsx @@ -0,0 +1,5 @@ +import TestChunkClient3 from './client3' + +export default function TestChunkServer3() { + return +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/chunk2/server4.tsx b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server4.tsx new file mode 100644 index 00000000..69ee9e37 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/chunk2/server4.tsx @@ -0,0 +1,5 @@ +import TestChunkClient3 from './client3' + +export default function TestChunkServer4() { + return +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index cc266a54..59c633b7 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -43,6 +43,7 @@ import { TestClientChunkServer } from './chunk/server' import { TestTailwind } from './tailwind' import { TestHmrClientDep2 } from './hmr-client-dep2/client' import { TestHmrClientDep3 } from './hmr-client-dep3/server' +import { TestChunk2 } from './chunk2/server' export function Root(props: { url: URL }) { return ( @@ -99,6 +100,7 @@ export function Root(props: { url: URL }) { + )