Skip to content

Commit 6924db4

Browse files
authored
chore(rsc): custom client chunks example (#765)
1 parent bf512e1 commit 6924db4

File tree

3 files changed

+152
-15
lines changed

3 files changed

+152
-15
lines changed

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { createHash } from 'node:crypto'
22
import { readFileSync } from 'node:fs'
33
import { type Page, expect, test } from '@playwright/test'
4-
import { type Fixture, useFixture } from './fixture'
4+
import { type Fixture, useCreateEditor, useFixture } from './fixture'
55
import {
66
expectNoPageError,
77
expectNoReload,
88
testNoJs,
99
waitForHydration,
1010
} from './helper'
11+
import { x } from 'tinyexec'
1112

1213
test.describe('dev-default', () => {
1314
const f = useFixture({ root: 'examples/basic', mode: 'dev' })
@@ -98,6 +99,56 @@ test.describe('dev-inconsistent-client-optimization', () => {
9899
})
99100
})
100101

102+
test.describe('build-stable-chunks', () => {
103+
const root = 'examples/basic'
104+
const createEditor = useCreateEditor(root)
105+
106+
test('basic', async () => {
107+
// 1st build
108+
await x('pnpm', ['build'], {
109+
throwOnError: true,
110+
nodeOptions: {
111+
cwd: root,
112+
},
113+
})
114+
const manifest1: import('vite').Manifest = JSON.parse(
115+
createEditor('dist/client/.vite/manifest.json').read(),
116+
)
117+
118+
// edit src/routes/client.tsx
119+
const editor = createEditor('src/routes/client.tsx')
120+
editor.edit((s) => s.replace('client-counter', 'client-counter-v2'))
121+
122+
// 2nd build
123+
await x('pnpm', ['build'], {
124+
throwOnError: true,
125+
nodeOptions: {
126+
cwd: root,
127+
},
128+
})
129+
const manifest2: import('vite').Manifest = JSON.parse(
130+
createEditor('dist/client/.vite/manifest.json').read(),
131+
)
132+
133+
// compare two mainfest.json
134+
const files1 = new Set(Object.values(manifest1).map((v) => v.file))
135+
const files2 = new Set(Object.values(manifest2).map((v) => v.file))
136+
const oldChunks = Object.entries(manifest2)
137+
.filter(([_k, v]) => !files1.has(v.file))
138+
.map(([k]) => k)
139+
.sort()
140+
const newChunks = Object.entries(manifest1)
141+
.filter(([_k, v]) => !files2.has(v.file))
142+
.map(([k]) => k)
143+
.sort()
144+
expect(newChunks).toEqual([
145+
'src/framework/entry.browser.tsx',
146+
'src/routes/client.tsx',
147+
])
148+
expect(oldChunks).toEqual(newChunks)
149+
})
150+
})
151+
101152
function defineTest(f: Fixture) {
102153
test('basic', async ({ page }) => {
103154
using _ = expectNoPageError(page)

packages/plugin-rsc/e2e/fixture.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,33 @@ export function useFixture(options: {
125125
await cleanup?.()
126126
})
127127

128+
const createEditor = useCreateEditor(cwd)
129+
130+
return {
131+
mode: options.mode,
132+
root: cwd,
133+
url: (url: string = './') => new URL(url, baseURL).href,
134+
createEditor,
135+
proc: () => proc,
136+
}
137+
}
138+
139+
export function useCreateEditor(cwd: string) {
128140
const originalFiles: Record<string, string> = {}
129141

142+
test.afterAll(async () => {
143+
for (const [filepath, content] of Object.entries(originalFiles)) {
144+
fs.writeFileSync(filepath, content)
145+
}
146+
})
147+
130148
function createEditor(filepath: string) {
131149
filepath = path.resolve(cwd, filepath)
132150
const init = fs.readFileSync(filepath, 'utf-8')
133151
originalFiles[filepath] ??= init
134152
let current = init
135153
return {
154+
read: () => current,
136155
edit(editFn: (data: string) => string): void {
137156
const next = editFn(current)
138157
assert(next !== current, 'Edit function did not change the content')
@@ -148,19 +167,7 @@ export function useFixture(options: {
148167
}
149168
}
150169

151-
test.afterAll(async () => {
152-
for (const [filepath, content] of Object.entries(originalFiles)) {
153-
fs.writeFileSync(filepath, content)
154-
}
155-
})
156-
157-
return {
158-
mode: options.mode,
159-
root: cwd,
160-
url: (url: string = './') => new URL(url, baseURL).href,
161-
createEditor,
162-
proc: () => proc,
163-
}
170+
return createEditor
164171
}
165172

166173
export async function setupIsolatedFixture(options: {

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import assert from 'node:assert'
22
import rsc, { transformHoistInlineDirective } from '@vitejs/plugin-rsc'
33
import tailwindcss from '@tailwindcss/vite'
44
import react from '@vitejs/plugin-react'
5-
import { type Plugin, defineConfig, normalizePath, parseAstAsync } from 'vite'
5+
import {
6+
type Plugin,
7+
type Rollup,
8+
defineConfig,
9+
normalizePath,
10+
parseAstAsync,
11+
} from 'vite'
612
// import inspect from 'vite-plugin-inspect'
713
import path from 'node:path'
14+
import { fileURLToPath } from 'node:url'
815

916
export default defineConfig({
1017
clearScreen: false,
@@ -88,6 +95,78 @@ export default defineConfig({
8895
}
8996
},
9097
},
98+
{
99+
name: 'optimize-chunks',
100+
apply: 'build',
101+
config() {
102+
const resolvePackageSource = (source: string) =>
103+
normalizePath(fileURLToPath(import.meta.resolve(source)))
104+
105+
// TODO: this package entry isn't a public API.
106+
const reactServerDom = resolvePackageSource(
107+
'@vitejs/plugin-rsc/react/browser',
108+
)
109+
110+
return {
111+
environments: {
112+
client: {
113+
build: {
114+
rollupOptions: {
115+
output: {
116+
manualChunks: (id) => {
117+
// need to use functional form to handle commonjs plugin proxy module
118+
// e.g. `(id)?commonjs-es-import`
119+
if (
120+
id.includes('node_modules/react/') ||
121+
id.includes('node_modules/react-dom/') ||
122+
id.includes(reactServerDom)
123+
) {
124+
return 'lib-react'
125+
}
126+
if (id === '\0vite/preload-helper.js') {
127+
return 'lib-vite'
128+
}
129+
},
130+
},
131+
},
132+
},
133+
},
134+
},
135+
}
136+
},
137+
// verify chunks are "stable"
138+
writeBundle(_options, bundle) {
139+
if (this.environment.name === 'client') {
140+
const entryChunks: Rollup.OutputChunk[] = []
141+
const libChunks: Record<string, Rollup.OutputChunk[]> = {}
142+
for (const chunk of Object.values(bundle)) {
143+
if (chunk.type === 'chunk') {
144+
if (chunk.isEntry) {
145+
entryChunks.push(chunk)
146+
}
147+
if (chunk.name.startsWith('lib-')) {
148+
;(libChunks[chunk.name] ??= []).push(chunk)
149+
}
150+
}
151+
}
152+
153+
// react vendor chunk has no import
154+
assert.equal(libChunks['lib-react'].length, 1)
155+
assert.deepEqual(
156+
// https://rolldown.rs/guide/in-depth/advanced-chunks#why-there-s-always-a-runtime-js-chunk
157+
libChunks['lib-react'][0].imports.filter(
158+
(f) => !f.includes('rolldown-runtime'),
159+
),
160+
[],
161+
)
162+
assert.deepEqual(libChunks['lib-react'][0].dynamicImports, [])
163+
164+
// entry chunk has no export
165+
assert.equal(entryChunks.length, 1)
166+
assert.deepEqual(entryChunks[0].exports, [])
167+
}
168+
},
169+
},
91170
{
92171
name: 'cf-build',
93172
enforce: 'post',

0 commit comments

Comments
 (0)