From 98e75df987076b8235390de92e2c079abdd680f7 Mon Sep 17 00:00:00 2001 From: hlimas Date: Tue, 22 Jul 2025 17:59:22 -0700 Subject: [PATCH 01/17] feat(rsc): add support for renderBuiltInUrl on assets metadata --- packages/plugin-rsc/e2e/basic.test.ts | 11 +- packages/plugin-rsc/e2e/helper.ts | 14 ++ packages/plugin-rsc/e2e/react-router.test.ts | 17 ++- packages/plugin-rsc/e2e/starter.test.ts | 106 +++++++++++++ packages/plugin-rsc/src/plugin.ts | 152 +++++++++++++++---- 5 files changed, 257 insertions(+), 43 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index d463b983b..569329817 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1,10 +1,10 @@ import { createHash } from 'node:crypto' -import { readFileSync } from 'node:fs' import { type Page, expect, test } from '@playwright/test' import { type Fixture, setupIsolatedFixture, useFixture } from './fixture' import { expectNoPageError, expectNoReload, + loadRSCManifest, testNoJs, waitForHydration, } from './helper' @@ -177,12 +177,9 @@ function defineTest(f: Fixture) { .evaluateAll((elements) => elements.map((el) => el.getAttribute('href')), ) - const manifest = JSON.parse( - readFileSync( - f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', - 'utf-8', - ).slice('export default '.length), - ) + + const manifest = await loadRSCManifest(f.root) + const hashString = (v: string) => createHash('sha256').update(v).digest().toString('hex').slice(0, 12) const deps = diff --git a/packages/plugin-rsc/e2e/helper.ts b/packages/plugin-rsc/e2e/helper.ts index 702c5b7ec..d5019699f 100644 --- a/packages/plugin-rsc/e2e/helper.ts +++ b/packages/plugin-rsc/e2e/helper.ts @@ -1,4 +1,5 @@ import test, { type Page, expect } from '@playwright/test' +import { readFileSync } from 'node:fs' export const testNoJs = test.extend({ javaScriptEnabled: ({}, use) => use(false), @@ -54,3 +55,16 @@ export function expectNoPageError(page: Page) { }, } } + +export async function loadRSCManifest(root: string) { + // Use dynamic "data:" import instead of URL path imports so it is + // not cached by the runtime. + const manifestFileContent = readFileSync( + root + '/dist/ssr/__vite_rsc_assets_manifest.js', + 'utf-8', + ) + const manifest = (await import('data:text/javascript,' + manifestFileContent)) + .default + + return manifest +} diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts index 01637fb58..e09757d7f 100644 --- a/packages/plugin-rsc/e2e/react-router.test.ts +++ b/packages/plugin-rsc/e2e/react-router.test.ts @@ -1,8 +1,12 @@ import { createHash } from 'node:crypto' import { expect, test } from '@playwright/test' import { type Fixture, useFixture } from './fixture' -import { expectNoReload, testNoJs, waitForHydration } from './helper' -import { readFileSync } from 'node:fs' +import { + expectNoReload, + loadRSCManifest, + testNoJs, + waitForHydration, +} from './helper' test.describe('dev-default', () => { const f = useFixture({ root: 'examples/react-router', mode: 'dev' }) @@ -74,12 +78,9 @@ function defineTest(f: Fixture) { .evaluateAll((elements) => elements.map((el) => el.getAttribute('href')), ) - const manifest = JSON.parse( - readFileSync( - f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', - 'utf-8', - ).slice('export default '.length), - ) + + const manifest = await loadRSCManifest(f.root) + const hashString = (v: string) => createHash('sha256').update(v).digest().toString('hex').slice(0, 12) const deps = diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 86752aa64..12a220523 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -245,6 +245,112 @@ test.describe(() => { }) }) +test.describe(() => { + const root = 'examples/e2e/temp/base' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'vite.config.ts': /* js */ ` + import rsc from '@vitejs/plugin-rsc' + import react from '@vitejs/plugin-react' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [ + react(), + rsc({ + entries: { + client: './src/framework/entry.browser.tsx', + ssr: './src/framework/entry.ssr.tsx', + rsc: './src/framework/entry.rsc.tsx', + } + }), + ], + experimental: { + renderBuiltUrl(filename) { + return { + runtime: \`'/' + \${JSON.stringify(filename)}\` + } + } + } + }) + `, + }, + }) + }) + + test.describe('dev-render-built-url-runtime', () => { + const f = useFixture({ root, mode: 'dev' }) + defineTest({ + ...f, + url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, + }) + }) + + test.describe('build-render-built-url-runtime', () => { + const f = useFixture({ root, mode: 'build' }) + defineTest({ + ...f, + url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, + }) + }) +}) + +test.describe(() => { + const root = 'examples/e2e/temp/base' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'vite.config.ts': /* js */ ` + import rsc from '@vitejs/plugin-rsc' + import react from '@vitejs/plugin-react' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [ + react(), + rsc({ + entries: { + client: './src/framework/entry.browser.tsx', + ssr: './src/framework/entry.ssr.tsx', + rsc: './src/framework/entry.rsc.tsx', + } + }), + ], + experimental: { + renderBuiltUrl(filename) { + return '/' + filename; + } + } + }) + `, + }, + }) + }) + + test.describe('dev-render-built-url-string', () => { + const f = useFixture({ root, mode: 'dev' }) + defineTest({ + ...f, + url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, + }) + }) + + test.describe('build-render-built-url-string', () => { + const f = useFixture({ root, mode: 'build' }) + defineTest({ + ...f, + url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, + }) + }) +}) + function defineTest(f: Fixture, variant?: 'no-ssr' | 'dev-production') { const waitForHydration: typeof waitForHydration_ = (page) => waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body') diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 8f7f7f0f3..af2484e75 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -9,6 +9,7 @@ import * as esModuleLexer from 'es-module-lexer' import MagicString from 'magic-string' import { type DevEnvironment, + type Environment, type EnvironmentModuleNode, type Plugin, type ResolvedConfig, @@ -242,10 +243,8 @@ export default function vitePluginRsc( serverResourcesMetaMap = sortObject(serverResourcesMetaMap) await builder.build(builder.environments.client!) - const assetsManifestCode = `export default ${JSON.stringify( + const assetsManifestCode = `export default ${serializeValueWithRuntime( buildAssetsManifest, - null, - 2, )}` const manifestPath = path.join( builder.environments!.rsc!.config.build!.outDir!, @@ -584,9 +583,11 @@ export default function vitePluginRsc( if (id === '\0virtual:vite-rsc/assets-manifest') { assert(this.environment.name !== 'client') assert(this.environment.mode === 'dev') - const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser) + const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser, { + environment: this.environment, + }) const manifest: AssetsManifest = { - bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`, + bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`, clientReferenceDeps: {}, } return `export default ${JSON.stringify(manifest, null, 2)}` @@ -621,10 +622,16 @@ export default function vitePluginRsc( const serverResources: Record = {} const rscAssetDeps = collectAssetDeps(rscBundle) for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { - serverResources[meta.key] = assetsURLOfDeps({ - js: [], - css: rscAssetDeps[id]?.deps.css ?? [], - }) + serverResources[meta.key] = assetsURLOfDeps( + { + js: [], + css: rscAssetDeps[id]?.deps.css ?? [], + }, + { + environment: this.environment, + enableRuntimeValue: true, + }, + ) } const assetDeps = collectAssetDeps(bundle) @@ -632,16 +639,23 @@ export default function vitePluginRsc( (v) => v.chunk.name === 'index', ) assert(entry) - const entryUrl = assetsURL(entry.chunk.fileName) + const entryUrl = assetsURL(entry.chunk.fileName, { + environment: this.environment, + enableRuntimeValue: true, + }) const clientReferenceDeps: Record = {} for (const [id, meta] of Object.entries(clientReferenceMetaMap)) { const deps: AssetDeps = assetDeps[id]?.deps ?? { js: [], css: [] } clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps( mergeAssetDeps(deps, entry.deps), + { + environment: this.environment, + enableRuntimeValue: true, + }, ) } buildAssetsManifest = { - bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`, + bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`, clientReferenceDeps, serverResources, } @@ -671,10 +685,8 @@ export default function vitePluginRsc( if (this.environment.name === 'ssr') { // output client manifest to non-client build directly. // this makes server build to be self-contained and deploy-able for cloudflare. - const assetsManifestCode = `export default ${JSON.stringify( + const assetsManifestCode = `export default ${serializeValueWithRuntime( buildAssetsManifest, - null, - 2, )}` for (const name of ['ssr', 'rsc']) { const manifestPath = path.join( @@ -1273,15 +1285,94 @@ function generateDynamicImportCode(map: Record) { return `export default {${code}};\n` } -// // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 -function assetsURL(url: string) { +class RuntimeAsset { + runtime: string + constructor(value: string) { + this.runtime = value + } +} + +function serializeValueWithRuntime(value: any) { + const replacements = [] + let result = JSON.stringify(value, (_key, value) => { + if (value instanceof RuntimeAsset) { + const placeholder = `__runtime_placeholder_${replacements.length}__` + replacements.push([placeholder, value.runtime]) + return placeholder + } + + return value + }) + + for (let [placeholder, runtime] of replacements) { + result = result.replace(`"${placeholder}"`, runtime) + } + + return result +} + +function assetsURL( + url: string, + { + environment, + enableRuntimeValue = false, + }: { + environment: Environment + enableRuntimeValue?: boolean + }, +) { + if ( + enableRuntimeValue && + environment.mode === 'build' && + typeof config.experimental?.renderBuiltUrl === 'function' + ) { + const result = config.experimental.renderBuiltUrl(url, { + type: 'asset', + hostType: 'js', + ssr: environment.name === 'ssr', + hostId: '', + }) + + if (typeof result === 'object') { + if (result.runtime) { + return new RuntimeAsset(result.runtime) + } + assert( + !result.relative, + '"result.relative" not supported on renderBuiltUrl() for RSC', + ) + } else if (result) { + assert( + typeof result === 'string', + '"renderBuiltUrl" should return a string!', + ) + return result + } + } + + // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 return config.base + url } -function assetsURLOfDeps(deps: AssetDeps) { +function assetsURLOfDeps( + deps: AssetDeps, + { + environment, + enableRuntimeValue = false, + }: { + environment: Environment + enableRuntimeValue?: boolean + }, +) { return { - js: deps.js.map((href) => assetsURL(href)), - css: deps.css.map((href) => assetsURL(href)), + js: deps.js.map((href) => { + assert(typeof href === 'string') + return assetsURL(href, { environment, enableRuntimeValue }) + }), + css: deps.css.map((href) => { + assert(typeof href === 'string') + return assetsURL(href, { environment, enableRuntimeValue }) + }), } } @@ -1292,12 +1383,12 @@ function assetsURLOfDeps(deps: AssetDeps) { export type AssetsManifest = { bootstrapScriptContent: string clientReferenceDeps: Record - serverResources?: Record + serverResources?: Record> } export type AssetDeps = { - js: string[] - css: string[] + js: (string | RuntimeAsset)[] + css: (string | RuntimeAsset)[] } function mergeAssetDeps(a: AssetDeps, b: AssetDeps): AssetDeps { @@ -1573,8 +1664,10 @@ export function vitePluginRscCss( for (const file of [mod.file, ...result.visitedFiles]) { this.addWatchFile(file) } - const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1))) - return `export default ${JSON.stringify(hrefs)}` + const hrefs = result.hrefs.map((href) => + assetsURL(href.slice(1), { environment: this.environment }), + ) + return `export default ${serializeValueWithRuntime(hrefs)}` } }, }, @@ -1660,8 +1753,11 @@ export function vitePluginRscCss( '@id/__x00__virtual:vite-rsc/importer-resources-browser?importer=' + encodeURIComponent(importer), ] - const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs }) - return generateResourcesCode(JSON.stringify(deps, null, 2)) + const deps = assetsURLOfDeps( + { css: cssHrefs, js: jsHrefs }, + { environment: this.environment }, + ) + return generateResourcesCode(serializeValueWithRuntime(deps)) } else { const key = normalizePath(path.relative(config.root, importer)) serverResourcesMetaMap[importer] = { key } @@ -1745,7 +1841,7 @@ function generateResourcesCode(depsCode: string) { const ResourcesFn = (React: typeof import('react'), deps: AssetDeps) => { return function Resources() { return React.createElement(React.Fragment, null, [ - ...deps.css.map((href: string) => + ...deps.css.map((href) => React.createElement('link', { key: 'css:' + href, rel: 'stylesheet', @@ -1754,7 +1850,7 @@ function generateResourcesCode(depsCode: string) { }), ), // js is only for dev to forward css import on browser to have hmr - ...deps.js.map((href: string) => + ...deps.js.map((href) => React.createElement('script', { key: 'js:' + href, type: 'module', From 3fdf2bc2a5929e7c02ddfc6806f7bab842f51505 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 12:04:24 +0900 Subject: [PATCH 02/17] refactor: fix types by adding `ResolvedAssetDeps` --- packages/plugin-rsc/CONTRIBUTING.md | 3 +++ packages/plugin-rsc/src/plugin.ts | 22 ++++++++++++++++++---- packages/plugin-rsc/src/ssr.tsx | 6 +++--- packages/plugin-rsc/src/types/virtual.d.ts | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/CONTRIBUTING.md b/packages/plugin-rsc/CONTRIBUTING.md index 74f5bdf1a..7de848fb2 100644 --- a/packages/plugin-rsc/CONTRIBUTING.md +++ b/packages/plugin-rsc/CONTRIBUTING.md @@ -36,6 +36,9 @@ Best for testing specific edge cases or isolated features. See `e2e/ssr-thenable # Build packages pnpm dev # pnpm -C packages/plugin-rsc dev +# Type check +pnpm -C packages/plugin-rsc tsc-dev + # Run examples pnpm -C packages/plugin-rsc/examples/basic dev # build / preview pnpm -C packages/plugin-rsc/examples/starter dev # build / preview diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index af2484e75..45df12271 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1293,7 +1293,7 @@ class RuntimeAsset { } function serializeValueWithRuntime(value: any) { - const replacements = [] + const replacements: [string, string][] = [] let result = JSON.stringify(value, (_key, value) => { if (value instanceof RuntimeAsset) { const placeholder = `__runtime_placeholder_${replacements.length}__` @@ -1391,6 +1391,17 @@ export type AssetDeps = { css: (string | RuntimeAsset)[] } +export type ResolvedAssetsManifest = { + bootstrapScriptContent: string + clientReferenceDeps: Record + serverResources?: Record> +} + +export type ResolvedAssetDeps = { + js: string[] + css: string[] +} + function mergeAssetDeps(a: AssetDeps, b: AssetDeps): AssetDeps { return { js: [...new Set([...a.js, ...b.js])], @@ -1838,10 +1849,13 @@ function collectModuleDependents(mods: EnvironmentModuleNode[]) { } function generateResourcesCode(depsCode: string) { - const ResourcesFn = (React: typeof import('react'), deps: AssetDeps) => { + const ResourcesFn = ( + React: typeof import('react'), + deps: ResolvedAssetDeps, + ) => { return function Resources() { return React.createElement(React.Fragment, null, [ - ...deps.css.map((href) => + ...deps.css.map((href: string) => React.createElement('link', { key: 'css:' + href, rel: 'stylesheet', @@ -1850,7 +1864,7 @@ function generateResourcesCode(depsCode: string) { }), ), // js is only for dev to forward css import on browser to have hmr - ...deps.js.map((href) => + ...deps.js.map((href: string) => React.createElement('script', { key: 'js:' + href, type: 'module', diff --git a/packages/plugin-rsc/src/ssr.tsx b/packages/plugin-rsc/src/ssr.tsx index ecd0b18fb..dec5e6c17 100644 --- a/packages/plugin-rsc/src/ssr.tsx +++ b/packages/plugin-rsc/src/ssr.tsx @@ -2,7 +2,7 @@ import assetsManifest from 'virtual:vite-rsc/assets-manifest' import * as clientReferences from 'virtual:vite-rsc/client-references' import * as ReactDOM from 'react-dom' import { setRequireModule } from './core/ssr' -import type { AssetDeps } from './plugin' +import type { ResolvedAssetDeps } from './plugin' export { createServerConsumerManifest } from './core/ssr' @@ -37,7 +37,7 @@ function initialize(): void { } // preload/preinit during getter access since `load` is cached on production -function wrapResourceProxy(mod: any, deps?: AssetDeps) { +function wrapResourceProxy(mod: any, deps?: ResolvedAssetDeps) { return new Proxy(mod, { get(target, p, receiver) { if (p in mod) { @@ -50,7 +50,7 @@ function wrapResourceProxy(mod: any, deps?: AssetDeps) { }) } -function preloadDeps(deps: AssetDeps) { +function preloadDeps(deps: ResolvedAssetDeps) { for (const href of deps.js) { ReactDOM.preloadModule(href, { as: 'script', diff --git a/packages/plugin-rsc/src/types/virtual.d.ts b/packages/plugin-rsc/src/types/virtual.d.ts index 81bbc242d..eb2108382 100644 --- a/packages/plugin-rsc/src/types/virtual.d.ts +++ b/packages/plugin-rsc/src/types/virtual.d.ts @@ -1,5 +1,5 @@ declare module 'virtual:vite-rsc/assets-manifest' { - const assetsManifest: import('../plugin').AssetsManifest + const assetsManifest: import('../plugin').ResolvedAssetsManifest export default assetsManifest } From b791ba3b2e31ef97c4e171a6fcc498117054e095 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 12:08:57 +0900 Subject: [PATCH 03/17] test: rename root --- packages/plugin-rsc/e2e/starter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 12a220523..0062894c3 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -246,7 +246,7 @@ test.describe(() => { }) test.describe(() => { - const root = 'examples/e2e/temp/base' + const root = 'examples/e2e/temp/render-built-url-runtime' test.beforeAll(async () => { await setupInlineFixture({ @@ -300,7 +300,7 @@ test.describe(() => { }) test.describe(() => { - const root = 'examples/e2e/temp/base' + const root = 'examples/e2e/temp/render-built-url-string' test.beforeAll(async () => { await setupInlineFixture({ From 9a00e4d2a9297fd701d12987c5195981ed17aac6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 12:31:42 +0900 Subject: [PATCH 04/17] test: revert unneeded change for now --- packages/plugin-rsc/e2e/basic.test.ts | 11 +++++++---- packages/plugin-rsc/e2e/helper.ts | 14 -------------- packages/plugin-rsc/e2e/react-router.test.ts | 17 ++++++++--------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 569329817..d463b983b 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1,10 +1,10 @@ import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' import { type Page, expect, test } from '@playwright/test' import { type Fixture, setupIsolatedFixture, useFixture } from './fixture' import { expectNoPageError, expectNoReload, - loadRSCManifest, testNoJs, waitForHydration, } from './helper' @@ -177,9 +177,12 @@ function defineTest(f: Fixture) { .evaluateAll((elements) => elements.map((el) => el.getAttribute('href')), ) - - const manifest = await loadRSCManifest(f.root) - + const manifest = JSON.parse( + readFileSync( + f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', + 'utf-8', + ).slice('export default '.length), + ) const hashString = (v: string) => createHash('sha256').update(v).digest().toString('hex').slice(0, 12) const deps = diff --git a/packages/plugin-rsc/e2e/helper.ts b/packages/plugin-rsc/e2e/helper.ts index d5019699f..702c5b7ec 100644 --- a/packages/plugin-rsc/e2e/helper.ts +++ b/packages/plugin-rsc/e2e/helper.ts @@ -1,5 +1,4 @@ import test, { type Page, expect } from '@playwright/test' -import { readFileSync } from 'node:fs' export const testNoJs = test.extend({ javaScriptEnabled: ({}, use) => use(false), @@ -55,16 +54,3 @@ export function expectNoPageError(page: Page) { }, } } - -export async function loadRSCManifest(root: string) { - // Use dynamic "data:" import instead of URL path imports so it is - // not cached by the runtime. - const manifestFileContent = readFileSync( - root + '/dist/ssr/__vite_rsc_assets_manifest.js', - 'utf-8', - ) - const manifest = (await import('data:text/javascript,' + manifestFileContent)) - .default - - return manifest -} diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts index e09757d7f..01637fb58 100644 --- a/packages/plugin-rsc/e2e/react-router.test.ts +++ b/packages/plugin-rsc/e2e/react-router.test.ts @@ -1,12 +1,8 @@ import { createHash } from 'node:crypto' import { expect, test } from '@playwright/test' import { type Fixture, useFixture } from './fixture' -import { - expectNoReload, - loadRSCManifest, - testNoJs, - waitForHydration, -} from './helper' +import { expectNoReload, testNoJs, waitForHydration } from './helper' +import { readFileSync } from 'node:fs' test.describe('dev-default', () => { const f = useFixture({ root: 'examples/react-router', mode: 'dev' }) @@ -78,9 +74,12 @@ function defineTest(f: Fixture) { .evaluateAll((elements) => elements.map((el) => el.getAttribute('href')), ) - - const manifest = await loadRSCManifest(f.root) - + const manifest = JSON.parse( + readFileSync( + f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', + 'utf-8', + ).slice('export default '.length), + ) const hashString = (v: string) => createHash('sha256').update(v).digest().toString('hex').slice(0, 12) const deps = From 99cb01417ba9083a9f54e37c9d2eb79b6def4bee Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 12:34:58 +0900 Subject: [PATCH 05/17] test: tweak --- packages/plugin-rsc/e2e/starter.test.ts | 34 +++++-------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 0062894c3..4a96cbbc3 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -246,7 +246,7 @@ test.describe(() => { }) test.describe(() => { - const root = 'examples/e2e/temp/render-built-url-runtime' + const root = 'examples/e2e/temp/renderBuiltUrl-runtime' test.beforeAll(async () => { await setupInlineFixture({ @@ -282,25 +282,14 @@ test.describe(() => { }) }) - test.describe('dev-render-built-url-runtime', () => { - const f = useFixture({ root, mode: 'dev' }) - defineTest({ - ...f, - url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) - }) - - test.describe('build-render-built-url-runtime', () => { + test.describe('build-renderBuiltUrl-runtime', () => { const f = useFixture({ root, mode: 'build' }) - defineTest({ - ...f, - url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) + defineTest(f) }) }) test.describe(() => { - const root = 'examples/e2e/temp/render-built-url-string' + const root = 'examples/e2e/temp/renderBuiltUrl-string' test.beforeAll(async () => { await setupInlineFixture({ @@ -334,20 +323,9 @@ test.describe(() => { }) }) - test.describe('dev-render-built-url-string', () => { - const f = useFixture({ root, mode: 'dev' }) - defineTest({ - ...f, - url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) - }) - - test.describe('build-render-built-url-string', () => { + test.describe('build-renderBuiltUrl-string', () => { const f = useFixture({ root, mode: 'build' }) - defineTest({ - ...f, - url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) + defineTest(f) }) }) From 8fd23dfb5b89b80cb82a15bd77d619a4d3099df4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 12:45:17 +0900 Subject: [PATCH 06/17] test: tweak renderBuiltUrl-string test --- packages/plugin-rsc/e2e/starter.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 4a96cbbc3..69dcd0114 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -311,10 +311,23 @@ test.describe(() => { rsc: './src/framework/entry.rsc.tsx', } }), + { + // simulate custom asset server + name: 'custom-server', + configurePreviewServer(server) { + server.middlewares.use((req, res, next) => { + const url = new URL(req.url ?? '', "http://localhost"); + if (url.pathname.startsWith('/custom-server/')) { + req.url = url.pathname.replace('/custom-server/', '/'); + } + next(); + }); + } + } ], experimental: { renderBuiltUrl(filename) { - return '/' + filename; + return '/custom-server/' + filename; } } }) From bff5ea1b42577ab93131ded193ca8e0a6810eb27 Mon Sep 17 00:00:00 2001 From: HenriqueLimas Date: Wed, 23 Jul 2025 21:39:37 -0700 Subject: [PATCH 07/17] refactor: remove unecessary params --- packages/plugin-rsc/src/plugin.ts | 68 +++++++------------------------ 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 45df12271..3283e7243 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -9,7 +9,6 @@ import * as esModuleLexer from 'es-module-lexer' import MagicString from 'magic-string' import { type DevEnvironment, - type Environment, type EnvironmentModuleNode, type Plugin, type ResolvedConfig, @@ -583,9 +582,7 @@ export default function vitePluginRsc( if (id === '\0virtual:vite-rsc/assets-manifest') { assert(this.environment.name !== 'client') assert(this.environment.mode === 'dev') - const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser, { - environment: this.environment, - }) + const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser) const manifest: AssetsManifest = { bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`, clientReferenceDeps: {}, @@ -622,16 +619,10 @@ export default function vitePluginRsc( const serverResources: Record = {} const rscAssetDeps = collectAssetDeps(rscBundle) for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { - serverResources[meta.key] = assetsURLOfDeps( - { - js: [], - css: rscAssetDeps[id]?.deps.css ?? [], - }, - { - environment: this.environment, - enableRuntimeValue: true, - }, - ) + serverResources[meta.key] = assetsURLOfDeps({ + js: [], + css: rscAssetDeps[id]?.deps.css ?? [], + }) } const assetDeps = collectAssetDeps(bundle) @@ -639,19 +630,12 @@ export default function vitePluginRsc( (v) => v.chunk.name === 'index', ) assert(entry) - const entryUrl = assetsURL(entry.chunk.fileName, { - environment: this.environment, - enableRuntimeValue: true, - }) + const entryUrl = assetsURL(entry.chunk.fileName) const clientReferenceDeps: Record = {} for (const [id, meta] of Object.entries(clientReferenceMetaMap)) { const deps: AssetDeps = assetDeps[id]?.deps ?? { js: [], css: [] } clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps( mergeAssetDeps(deps, entry.deps), - { - environment: this.environment, - enableRuntimeValue: true, - }, ) } buildAssetsManifest = { @@ -1311,25 +1295,15 @@ function serializeValueWithRuntime(value: any) { return result } -function assetsURL( - url: string, - { - environment, - enableRuntimeValue = false, - }: { - environment: Environment - enableRuntimeValue?: boolean - }, -) { +function assetsURL(url: string) { if ( - enableRuntimeValue && - environment.mode === 'build' && + config.command === 'build' && typeof config.experimental?.renderBuiltUrl === 'function' ) { const result = config.experimental.renderBuiltUrl(url, { type: 'asset', hostType: 'js', - ssr: environment.name === 'ssr', + ssr: true, hostId: '', }) @@ -1354,24 +1328,15 @@ function assetsURL( return config.base + url } -function assetsURLOfDeps( - deps: AssetDeps, - { - environment, - enableRuntimeValue = false, - }: { - environment: Environment - enableRuntimeValue?: boolean - }, -) { +function assetsURLOfDeps(deps: AssetDeps) { return { js: deps.js.map((href) => { assert(typeof href === 'string') - return assetsURL(href, { environment, enableRuntimeValue }) + return assetsURL(href) }), css: deps.css.map((href) => { assert(typeof href === 'string') - return assetsURL(href, { environment, enableRuntimeValue }) + return assetsURL(href) }), } } @@ -1675,9 +1640,7 @@ export function vitePluginRscCss( for (const file of [mod.file, ...result.visitedFiles]) { this.addWatchFile(file) } - const hrefs = result.hrefs.map((href) => - assetsURL(href.slice(1), { environment: this.environment }), - ) + const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1))) return `export default ${serializeValueWithRuntime(hrefs)}` } }, @@ -1764,10 +1727,7 @@ export function vitePluginRscCss( '@id/__x00__virtual:vite-rsc/importer-resources-browser?importer=' + encodeURIComponent(importer), ] - const deps = assetsURLOfDeps( - { css: cssHrefs, js: jsHrefs }, - { environment: this.environment }, - ) + const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs }) return generateResourcesCode(serializeValueWithRuntime(deps)) } else { const key = normalizePath(path.relative(config.root, importer)) From 3eda7090fabfa024a216df311292b48fab524c35 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 14:43:29 +0900 Subject: [PATCH 08/17] fix: evaluate bootstrapScriptContent import url on server --- packages/plugin-rsc/src/plugin.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 3283e7243..2845063c0 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -638,8 +638,16 @@ export default function vitePluginRsc( mergeAssetDeps(deps, entry.deps), ) } + let bootstrapScriptContent: string | RuntimeAsset + if (typeof entryUrl === 'string') { + bootstrapScriptContent = `import(${JSON.stringify(entryUrl)})` + } else { + bootstrapScriptContent = new RuntimeAsset( + `"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`, + ) + } buildAssetsManifest = { - bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`, + bootstrapScriptContent, clientReferenceDeps, serverResources, } @@ -1346,7 +1354,7 @@ function assetsURLOfDeps(deps: AssetDeps) { // export type AssetsManifest = { - bootstrapScriptContent: string + bootstrapScriptContent: string | RuntimeAsset clientReferenceDeps: Record serverResources?: Record> } From 74cbe3100899d15679a431fe4993335faffc2207 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 14:44:01 +0900 Subject: [PATCH 09/17] test: wip renderBuiltUrl runtime --- packages/plugin-rsc/e2e/starter.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 69dcd0114..9e57c6785 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -249,6 +249,13 @@ test.describe(() => { const root = 'examples/e2e/temp/renderBuiltUrl-runtime' test.beforeAll(async () => { + // TODO: test `globalThis`-based dynamic base + // TODO: test client shared chunk + const renderBuiltUrl = (filename: string) => { + return { + runtime: `"/" + ${JSON.stringify(filename)}`, + } + }; await setupInlineFixture({ src: 'examples/starter', dest: root, @@ -270,11 +277,7 @@ test.describe(() => { }), ], experimental: { - renderBuiltUrl(filename) { - return { - runtime: \`'/' + \${JSON.stringify(filename)}\` - } - } + renderBuiltUrl: ${renderBuiltUrl.toString()} } }) `, From 87c9d0c32eece3591e2d0db0bbd92e42494e0a9a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 14:47:01 +0900 Subject: [PATCH 10/17] chore: pretty json --- packages/plugin-rsc/src/plugin.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 2845063c0..e170af308 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1286,15 +1286,19 @@ class RuntimeAsset { function serializeValueWithRuntime(value: any) { const replacements: [string, string][] = [] - let result = JSON.stringify(value, (_key, value) => { - if (value instanceof RuntimeAsset) { - const placeholder = `__runtime_placeholder_${replacements.length}__` - replacements.push([placeholder, value.runtime]) - return placeholder - } + let result = JSON.stringify( + value, + (_key, value) => { + if (value instanceof RuntimeAsset) { + const placeholder = `__runtime_placeholder_${replacements.length}__` + replacements.push([placeholder, value.runtime]) + return placeholder + } - return value - }) + return value + }, + 2, + ) for (let [placeholder, runtime] of replacements) { result = result.replace(`"${placeholder}"`, runtime) From 6fbeba24f9fccf24b1110325cafd4de2fb7e90d0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 14:56:25 +0900 Subject: [PATCH 11/17] test: test dynamic base --- packages/plugin-rsc/e2e/starter.test.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 9e57c6785..c12a348d8 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -249,13 +249,12 @@ test.describe(() => { const root = 'examples/e2e/temp/renderBuiltUrl-runtime' test.beforeAll(async () => { - // TODO: test `globalThis`-based dynamic base // TODO: test client shared chunk const renderBuiltUrl = (filename: string) => { return { - runtime: `"/" + ${JSON.stringify(filename)}`, + runtime: `globalThis.__dynamicBase + ${JSON.stringify(filename)}`, } - }; + } await setupInlineFixture({ src: 'examples/starter', dest: root, @@ -275,6 +274,25 @@ test.describe(() => { rsc: './src/framework/entry.rsc.tsx', } }), + { + // simulate custom asset server + name: 'custom-server', + config(_config, env) { + if (env.isPreview) { + globalThis.__dynamicBase = '/custom-server/'; + } + }, + configurePreviewServer(server) { + globalThis.__dynamicBase = '/custom-server/'; + server.middlewares.use((req, res, next) => { + const url = new URL(req.url ?? '', "http://localhost"); + if (url.pathname.startsWith('/custom-server/')) { + req.url = url.pathname.replace('/custom-server/', '/'); + } + next(); + }); + } + } ], experimental: { renderBuiltUrl: ${renderBuiltUrl.toString()} From 7bc75326be27dc8c86244f21129703f665bc3913 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 14:59:44 +0900 Subject: [PATCH 12/17] test: put back minimal dev test --- packages/plugin-rsc/e2e/starter.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index c12a348d8..e4d3c7c56 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -303,6 +303,16 @@ test.describe(() => { }) }) + test.describe('dev-renderBuiltUrl-runtime', () => { + const f = useFixture({ root, mode: 'dev' }) + + test('basic', async ({ page }) => { + using _ = expectNoPageError(page) + await page.goto(f.url()) + await waitForHydration_(page) + }) + }) + test.describe('build-renderBuiltUrl-runtime', () => { const f = useFixture({ root, mode: 'build' }) defineTest(f) From 9ea817c40c2100bc5cbfa55dd54f858332145976 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 15:01:19 +0900 Subject: [PATCH 13/17] chore: cleanup --- packages/plugin-rsc/e2e/starter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index e4d3c7c56..e7d342922 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -283,7 +283,6 @@ test.describe(() => { } }, configurePreviewServer(server) { - globalThis.__dynamicBase = '/custom-server/'; server.middlewares.use((req, res, next) => { const url = new URL(req.url ?? '', "http://localhost"); if (url.pathname.startsWith('/custom-server/')) { From 51a3aa2ca0794344b9b8f610585e2c3c22941ba5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 15:35:30 +0900 Subject: [PATCH 14/17] test: test dynamic base on browser --- packages/plugin-rsc/e2e/fixture.ts | 8 ++++++- packages/plugin-rsc/e2e/starter.test.ts | 30 ++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts index ac1d7385d..d88f19ed0 100644 --- a/packages/plugin-rsc/e2e/fixture.ts +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -200,7 +200,7 @@ function editFileJson(filepath: string, edit: (s: string) => string) { export async function setupInlineFixture(options: { src: string dest: string - files?: Record + files?: Record string }> }) { fs.rmSync(options.dest, { recursive: true, force: true }) fs.mkdirSync(options.dest, { recursive: true }) @@ -214,6 +214,12 @@ export async function setupInlineFixture(options: { // write additional files if (options.files) { for (let [filename, contents] of Object.entries(options.files)) { + if (typeof contents === 'object' && 'edit' in contents) { + const file = path.join(options.dest, filename) + const editted = contents.edit(fs.readFileSync(file, 'utf-8')) + fs.writeFileSync(file, editted) + continue + } let filepath = path.join(options.dest, filename) fs.mkdirSync(path.dirname(filepath), { recursive: true }) // strip indent diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index e7d342922..e0b3c9817 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -249,10 +249,9 @@ test.describe(() => { const root = 'examples/e2e/temp/renderBuiltUrl-runtime' test.beforeAll(async () => { - // TODO: test client shared chunk const renderBuiltUrl = (filename: string) => { return { - runtime: `globalThis.__dynamicBase + ${JSON.stringify(filename)}`, + runtime: `__dynamicBase + ${JSON.stringify(filename)}`, } } await setupInlineFixture({ @@ -293,11 +292,36 @@ test.describe(() => { } } ], + // tweak chunks to test "__dynamicBase" used on browser for "__vite__mapDeps" + environments: { + client: { + build: { + rollupOptions: { + output: { + manualChunks: (id) => { + if (id.includes('node_modules/react/')) { + return 'lib-react'; + } + } + }, + } + } + } + }, experimental: { renderBuiltUrl: ${renderBuiltUrl.toString()} - } + }, }) `, + 'src/root.tsx': { + // define __dynamicBase on browser via head script + edit: (s: string) => + s.replace( + '', + () => + ``, + ), + }, }, }) }) From 37844eaebe92a657dbe2902cde85d44b96242391 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 18:10:03 +0900 Subject: [PATCH 15/17] test: verify runtime url in built manifest --- packages/plugin-rsc/e2e/starter.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 74e3b2798..df0d93b0b 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -323,6 +323,14 @@ test.describe(() => { test.describe('build-renderBuiltUrl-runtime', () => { const f = useFixture({ root, mode: 'build' }) defineTest(f) + + test('verify runtime url', () => { + const manifestFileContent = fs.readFileSync( + f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', + 'utf-8', + ) + expect(manifestFileContent).toContain(`__dynamicBase + "assets/client-`) + }) }) }) From 9ae24599ac1cc6246f5436a159480fae554fe3f2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 18:48:54 +0900 Subject: [PATCH 16/17] refactor: remove trivial assertion --- packages/plugin-rsc/src/plugin.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index e170af308..c6fa50595 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1312,6 +1312,7 @@ function assetsURL(url: string) { config.command === 'build' && typeof config.experimental?.renderBuiltUrl === 'function' ) { + // https://github.com/vitejs/vite/blob/bdde0f9e5077ca1a21a04eefc30abad055047226/packages/vite/src/node/build.ts#L1369 const result = config.experimental.renderBuiltUrl(url, { type: 'asset', hostType: 'js', @@ -1328,11 +1329,7 @@ function assetsURL(url: string) { '"result.relative" not supported on renderBuiltUrl() for RSC', ) } else if (result) { - assert( - typeof result === 'string', - '"renderBuiltUrl" should return a string!', - ) - return result + return result satisfies string } } From 96268360b30573af06e8d915315997d4587310d8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 24 Jul 2025 18:49:23 +0900 Subject: [PATCH 17/17] chore: const --- packages/plugin-rsc/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index c6fa50595..0ef7db0b8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1300,7 +1300,7 @@ function serializeValueWithRuntime(value: any) { 2, ) - for (let [placeholder, runtime] of replacements) { + for (const [placeholder, runtime] of replacements) { result = result.replace(`"${placeholder}"`, runtime) }