Skip to content

Commit 5314ed6

Browse files
HenriqueLimashlimashi-ogawa
authored
feat(rsc): add support for experimental.renderBuiltUrl on assets metadata (#612)
Co-authored-by: hlimas <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent d9cb926 commit 5314ed6

File tree

5 files changed

+267
-21
lines changed

5 files changed

+267
-21
lines changed

packages/plugin-rsc/e2e/fixture.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ function editFileJson(filepath: string, edit: (s: string) => string) {
200200
export async function setupInlineFixture(options: {
201201
src: string
202202
dest: string
203-
files?: Record<string, string | { cp: string }>
203+
files?: Record<
204+
string,
205+
string | { cp: string } | { edit: (s: string) => string }
206+
>
204207
}) {
205208
fs.rmSync(options.dest, { recursive: true, force: true })
206209
fs.mkdirSync(options.dest, { recursive: true })
@@ -223,6 +226,11 @@ export async function setupInlineFixture(options: {
223226
fs.copyFileSync(srcFile, destFile)
224227
continue
225228
}
229+
if (typeof contents === 'object' && 'edit' in contents) {
230+
const editted = contents.edit(fs.readFileSync(destFile, 'utf-8'))
231+
fs.writeFileSync(destFile, editted)
232+
continue
233+
}
226234

227235
// write a new file
228236
contents = contents.replace(/^\n*/, '').replace(/\s*$/, '\n')

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

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,165 @@ test.describe(() => {
229229
})
230230
})
231231

232+
test.describe(() => {
233+
const root = 'examples/e2e/temp/renderBuiltUrl-runtime'
234+
235+
test.beforeAll(async () => {
236+
const renderBuiltUrl = (filename: string) => {
237+
return {
238+
runtime: `__dynamicBase + ${JSON.stringify(filename)}`,
239+
}
240+
}
241+
await setupInlineFixture({
242+
src: 'examples/starter',
243+
dest: root,
244+
files: {
245+
'vite.config.ts': /* js */ `
246+
import rsc from '@vitejs/plugin-rsc'
247+
import react from '@vitejs/plugin-react'
248+
import { defineConfig } from 'vite'
249+
250+
export default defineConfig({
251+
plugins: [
252+
react(),
253+
rsc({
254+
entries: {
255+
client: './src/framework/entry.browser.tsx',
256+
ssr: './src/framework/entry.ssr.tsx',
257+
rsc: './src/framework/entry.rsc.tsx',
258+
}
259+
}),
260+
{
261+
// simulate custom asset server
262+
name: 'custom-server',
263+
config(_config, env) {
264+
if (env.isPreview) {
265+
globalThis.__dynamicBase = '/custom-server/';
266+
}
267+
},
268+
configurePreviewServer(server) {
269+
server.middlewares.use((req, res, next) => {
270+
const url = new URL(req.url ?? '', "http://localhost");
271+
if (url.pathname.startsWith('/custom-server/')) {
272+
req.url = url.pathname.replace('/custom-server/', '/');
273+
}
274+
next();
275+
});
276+
}
277+
}
278+
],
279+
// tweak chunks to test "__dynamicBase" used on browser for "__vite__mapDeps"
280+
environments: {
281+
client: {
282+
build: {
283+
rollupOptions: {
284+
output: {
285+
manualChunks: (id) => {
286+
if (id.includes('node_modules/react/')) {
287+
return 'lib-react';
288+
}
289+
}
290+
},
291+
}
292+
}
293+
}
294+
},
295+
experimental: {
296+
renderBuiltUrl: ${renderBuiltUrl.toString()}
297+
},
298+
})
299+
`,
300+
'src/root.tsx': {
301+
// define __dynamicBase on browser via head script
302+
edit: (s: string) =>
303+
s.replace(
304+
'</head>',
305+
() =>
306+
`<script>{\`globalThis.__dynamicBase = $\{JSON.stringify(globalThis.__dynamicBase ?? "/")}\`}</script></head>`,
307+
),
308+
},
309+
},
310+
})
311+
})
312+
313+
test.describe('dev-renderBuiltUrl-runtime', () => {
314+
const f = useFixture({ root, mode: 'dev' })
315+
316+
test('basic', async ({ page }) => {
317+
using _ = expectNoPageError(page)
318+
await page.goto(f.url())
319+
await waitForHydration_(page)
320+
})
321+
})
322+
323+
test.describe('build-renderBuiltUrl-runtime', () => {
324+
const f = useFixture({ root, mode: 'build' })
325+
defineTest(f)
326+
327+
test('verify runtime url', () => {
328+
const manifestFileContent = fs.readFileSync(
329+
f.root + '/dist/ssr/__vite_rsc_assets_manifest.js',
330+
'utf-8',
331+
)
332+
expect(manifestFileContent).toContain(`__dynamicBase + "assets/client-`)
333+
})
334+
})
335+
})
336+
337+
test.describe(() => {
338+
const root = 'examples/e2e/temp/renderBuiltUrl-string'
339+
340+
test.beforeAll(async () => {
341+
await setupInlineFixture({
342+
src: 'examples/starter',
343+
dest: root,
344+
files: {
345+
'vite.config.ts': /* js */ `
346+
import rsc from '@vitejs/plugin-rsc'
347+
import react from '@vitejs/plugin-react'
348+
import { defineConfig } from 'vite'
349+
350+
export default defineConfig({
351+
plugins: [
352+
react(),
353+
rsc({
354+
entries: {
355+
client: './src/framework/entry.browser.tsx',
356+
ssr: './src/framework/entry.ssr.tsx',
357+
rsc: './src/framework/entry.rsc.tsx',
358+
}
359+
}),
360+
{
361+
// simulate custom asset server
362+
name: 'custom-server',
363+
configurePreviewServer(server) {
364+
server.middlewares.use((req, res, next) => {
365+
const url = new URL(req.url ?? '', "http://localhost");
366+
if (url.pathname.startsWith('/custom-server/')) {
367+
req.url = url.pathname.replace('/custom-server/', '/');
368+
}
369+
next();
370+
});
371+
}
372+
}
373+
],
374+
experimental: {
375+
renderBuiltUrl(filename) {
376+
return '/custom-server/' + filename;
377+
}
378+
}
379+
})
380+
`,
381+
},
382+
})
383+
})
384+
385+
test.describe('build-renderBuiltUrl-string', () => {
386+
const f = useFixture({ root, mode: 'build' })
387+
defineTest(f)
388+
})
389+
})
390+
232391
function defineTest(f: Fixture, variant?: 'no-ssr' | 'dev-production') {
233392
const waitForHydration: typeof waitForHydration_ = (page) =>
234393
waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body')

packages/plugin-rsc/src/plugin.ts

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,8 @@ export default function vitePluginRsc(
242242
serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
243243
await builder.build(builder.environments.client!)
244244

245-
const assetsManifestCode = `export default ${JSON.stringify(
245+
const assetsManifestCode = `export default ${serializeValueWithRuntime(
246246
buildAssetsManifest,
247-
null,
248-
2,
249247
)}`
250248
const manifestPath = path.join(
251249
builder.environments!.rsc!.config.build!.outDir!,
@@ -586,7 +584,7 @@ export default function vitePluginRsc(
586584
assert(this.environment.mode === 'dev')
587585
const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser)
588586
const manifest: AssetsManifest = {
589-
bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`,
587+
bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`,
590588
clientReferenceDeps: {},
591589
}
592590
return `export default ${JSON.stringify(manifest, null, 2)}`
@@ -640,8 +638,16 @@ export default function vitePluginRsc(
640638
mergeAssetDeps(deps, entry.deps),
641639
)
642640
}
641+
let bootstrapScriptContent: string | RuntimeAsset
642+
if (typeof entryUrl === 'string') {
643+
bootstrapScriptContent = `import(${JSON.stringify(entryUrl)})`
644+
} else {
645+
bootstrapScriptContent = new RuntimeAsset(
646+
`"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`,
647+
)
648+
}
643649
buildAssetsManifest = {
644-
bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`,
650+
bootstrapScriptContent,
645651
clientReferenceDeps,
646652
serverResources,
647653
}
@@ -671,10 +677,8 @@ export default function vitePluginRsc(
671677
if (this.environment.name === 'ssr') {
672678
// output client manifest to non-client build directly.
673679
// this makes server build to be self-contained and deploy-able for cloudflare.
674-
const assetsManifestCode = `export default ${JSON.stringify(
680+
const assetsManifestCode = `export default ${serializeValueWithRuntime(
675681
buildAssetsManifest,
676-
null,
677-
2,
678682
)}`
679683
for (const name of ['ssr', 'rsc']) {
680684
const manifestPath = path.join(
@@ -1273,15 +1277,76 @@ function generateDynamicImportCode(map: Record<string, string>) {
12731277
return `export default {${code}};\n`
12741278
}
12751279

1276-
// // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230
1280+
class RuntimeAsset {
1281+
runtime: string
1282+
constructor(value: string) {
1283+
this.runtime = value
1284+
}
1285+
}
1286+
1287+
function serializeValueWithRuntime(value: any) {
1288+
const replacements: [string, string][] = []
1289+
let result = JSON.stringify(
1290+
value,
1291+
(_key, value) => {
1292+
if (value instanceof RuntimeAsset) {
1293+
const placeholder = `__runtime_placeholder_${replacements.length}__`
1294+
replacements.push([placeholder, value.runtime])
1295+
return placeholder
1296+
}
1297+
1298+
return value
1299+
},
1300+
2,
1301+
)
1302+
1303+
for (const [placeholder, runtime] of replacements) {
1304+
result = result.replace(`"${placeholder}"`, runtime)
1305+
}
1306+
1307+
return result
1308+
}
1309+
12771310
function assetsURL(url: string) {
1311+
if (
1312+
config.command === 'build' &&
1313+
typeof config.experimental?.renderBuiltUrl === 'function'
1314+
) {
1315+
// https://github.com/vitejs/vite/blob/bdde0f9e5077ca1a21a04eefc30abad055047226/packages/vite/src/node/build.ts#L1369
1316+
const result = config.experimental.renderBuiltUrl(url, {
1317+
type: 'asset',
1318+
hostType: 'js',
1319+
ssr: true,
1320+
hostId: '',
1321+
})
1322+
1323+
if (typeof result === 'object') {
1324+
if (result.runtime) {
1325+
return new RuntimeAsset(result.runtime)
1326+
}
1327+
assert(
1328+
!result.relative,
1329+
'"result.relative" not supported on renderBuiltUrl() for RSC',
1330+
)
1331+
} else if (result) {
1332+
return result satisfies string
1333+
}
1334+
}
1335+
1336+
// https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230
12781337
return config.base + url
12791338
}
12801339

12811340
function assetsURLOfDeps(deps: AssetDeps) {
12821341
return {
1283-
js: deps.js.map((href) => assetsURL(href)),
1284-
css: deps.css.map((href) => assetsURL(href)),
1342+
js: deps.js.map((href) => {
1343+
assert(typeof href === 'string')
1344+
return assetsURL(href)
1345+
}),
1346+
css: deps.css.map((href) => {
1347+
assert(typeof href === 'string')
1348+
return assetsURL(href)
1349+
}),
12851350
}
12861351
}
12871352

@@ -1290,12 +1355,23 @@ function assetsURLOfDeps(deps: AssetDeps) {
12901355
//
12911356

12921357
export type AssetsManifest = {
1293-
bootstrapScriptContent: string
1358+
bootstrapScriptContent: string | RuntimeAsset
12941359
clientReferenceDeps: Record<string, AssetDeps>
1295-
serverResources?: Record<string, { css: string[] }>
1360+
serverResources?: Record<string, Pick<AssetDeps, 'css'>>
12961361
}
12971362

12981363
export type AssetDeps = {
1364+
js: (string | RuntimeAsset)[]
1365+
css: (string | RuntimeAsset)[]
1366+
}
1367+
1368+
export type ResolvedAssetsManifest = {
1369+
bootstrapScriptContent: string
1370+
clientReferenceDeps: Record<string, ResolvedAssetDeps>
1371+
serverResources?: Record<string, Pick<ResolvedAssetDeps, 'css'>>
1372+
}
1373+
1374+
export type ResolvedAssetDeps = {
12991375
js: string[]
13001376
css: string[]
13011377
}
@@ -1574,7 +1650,7 @@ export function vitePluginRscCss(
15741650
this.addWatchFile(file)
15751651
}
15761652
const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1)))
1577-
return `export default ${JSON.stringify(hrefs)}`
1653+
return `export default ${serializeValueWithRuntime(hrefs)}`
15781654
}
15791655
},
15801656
},
@@ -1661,7 +1737,7 @@ export function vitePluginRscCss(
16611737
encodeURIComponent(importer),
16621738
]
16631739
const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs })
1664-
return generateResourcesCode(JSON.stringify(deps, null, 2))
1740+
return generateResourcesCode(serializeValueWithRuntime(deps))
16651741
} else {
16661742
const key = normalizePath(path.relative(config.root, importer))
16671743
serverResourcesMetaMap[importer] = { key }
@@ -1742,7 +1818,10 @@ function collectModuleDependents(mods: EnvironmentModuleNode[]) {
17421818
}
17431819

17441820
function generateResourcesCode(depsCode: string) {
1745-
const ResourcesFn = (React: typeof import('react'), deps: AssetDeps) => {
1821+
const ResourcesFn = (
1822+
React: typeof import('react'),
1823+
deps: ResolvedAssetDeps,
1824+
) => {
17461825
return function Resources() {
17471826
return React.createElement(React.Fragment, null, [
17481827
...deps.css.map((href: string) =>

0 commit comments

Comments
 (0)