Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/plugin-rsc/e2e/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ function editFileJson(filepath: string, edit: (s: string) => string) {
export async function setupInlineFixture(options: {
src: string
dest: string
files?: Record<string, string | { cp: string }>
files?: Record<
string,
string | { cp: string } | { edit: (s: string) => string }
>
}) {
fs.rmSync(options.dest, { recursive: true, force: true })
fs.mkdirSync(options.dest, { recursive: true })
Expand All @@ -223,6 +226,11 @@ export async function setupInlineFixture(options: {
fs.copyFileSync(srcFile, destFile)
continue
}
if (typeof contents === 'object' && 'edit' in contents) {
const editted = contents.edit(fs.readFileSync(destFile, 'utf-8'))
fs.writeFileSync(destFile, editted)
continue
}

// write a new file
contents = contents.replace(/^\n*/, '').replace(/\s*$/, '\n')
Expand Down
159 changes: 159 additions & 0 deletions packages/plugin-rsc/e2e/starter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,165 @@ test.describe(() => {
})
})

test.describe(() => {
const root = 'examples/e2e/temp/renderBuiltUrl-runtime'

test.beforeAll(async () => {
const renderBuiltUrl = (filename: string) => {
return {
runtime: `__dynamicBase + ${JSON.stringify(filename)}`,
}
}
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',
}
}),
{
// simulate custom asset server
name: 'custom-server',
config(_config, env) {
if (env.isPreview) {
globalThis.__dynamicBase = '/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();
});
}
}
],
// 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(
'</head>',
() =>
`<script>{\`globalThis.__dynamicBase = $\{JSON.stringify(globalThis.__dynamicBase ?? "/")}\`}</script></head>`,
),
},
},
})
})

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)

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-`)
})
})
})

test.describe(() => {
const root = 'examples/e2e/temp/renderBuiltUrl-string'

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',
}
}),
{
// 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 '/custom-server/' + filename;
}
}
})
`,
},
})
})

test.describe('build-renderBuiltUrl-string', () => {
const f = useFixture({ root, mode: 'build' })
defineTest(f)
})
})

function defineTest(f: Fixture, variant?: 'no-ssr' | 'dev-production') {
const waitForHydration: typeof waitForHydration_ = (page) =>
waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body')
Expand Down
114 changes: 98 additions & 16 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,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!,
Expand Down Expand Up @@ -586,7 +584,7 @@ export default function vitePluginRsc(
assert(this.environment.mode === 'dev')
const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser)
const manifest: AssetsManifest = {
bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`,
bootstrapScriptContent: `import(${serializeValueWithRuntime(entryUrl)})`,
clientReferenceDeps: {},
}
return `export default ${JSON.stringify(manifest, null, 2)}`
Expand Down Expand Up @@ -640,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}) + ")"`,
)
}
Comment on lines +641 to +648
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously bootstrapScriptContent's import url runtime is resolved on browser:

{
  "bootstrapScriptContent":"import(globalThis.__dynamicBase + \"assets/index-BxtkZ08f.js\")",

But, I think this should be aligned to other assets, so the runtime is resolved on server:

{
  "bootstrapScriptContent": "import(" + JSON.stringify(globalThis.__dynamicBase + "assets/index-BxtkZ08f.js") + ")",

buildAssetsManifest = {
bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`,
bootstrapScriptContent,
clientReferenceDeps,
serverResources,
}
Expand Down Expand Up @@ -671,10 +677,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(
Expand Down Expand Up @@ -1273,15 +1277,79 @@ function generateDynamicImportCode(map: Record<string, string>) {
return `export default {${code}};\n`
}

// // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230
class RuntimeAsset {
runtime: string
constructor(value: string) {
this.runtime = value
}
}

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
}

return value
},
2,
)

for (let [placeholder, runtime] of replacements) {
result = result.replace(`"${placeholder}"`, runtime)
}

return result
}

function assetsURL(url: string) {
if (
config.command === 'build' &&
typeof config.experimental?.renderBuiltUrl === 'function'
) {
const result = config.experimental.renderBuiltUrl(url, {
type: 'asset',
hostType: 'js',
ssr: true,
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) {
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)
}),
css: deps.css.map((href) => {
assert(typeof href === 'string')
return assetsURL(href)
}),
}
}

Expand All @@ -1290,12 +1358,23 @@ function assetsURLOfDeps(deps: AssetDeps) {
//

export type AssetsManifest = {
bootstrapScriptContent: string
bootstrapScriptContent: string | RuntimeAsset
clientReferenceDeps: Record<string, AssetDeps>
serverResources?: Record<string, { css: string[] }>
serverResources?: Record<string, Pick<AssetDeps, 'css'>>
}

export type AssetDeps = {
js: (string | RuntimeAsset)[]
css: (string | RuntimeAsset)[]
}

export type ResolvedAssetsManifest = {
bootstrapScriptContent: string
clientReferenceDeps: Record<string, ResolvedAssetDeps>
serverResources?: Record<string, Pick<ResolvedAssetDeps, 'css'>>
}

export type ResolvedAssetDeps = {
js: string[]
css: string[]
}
Expand Down Expand Up @@ -1574,7 +1653,7 @@ export function vitePluginRscCss(
this.addWatchFile(file)
}
const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1)))
return `export default ${JSON.stringify(hrefs)}`
return `export default ${serializeValueWithRuntime(hrefs)}`
}
},
},
Expand Down Expand Up @@ -1661,7 +1740,7 @@ export function vitePluginRscCss(
encodeURIComponent(importer),
]
const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs })
return generateResourcesCode(JSON.stringify(deps, null, 2))
return generateResourcesCode(serializeValueWithRuntime(deps))
} else {
const key = normalizePath(path.relative(config.root, importer))
serverResourcesMetaMap[importer] = { key }
Expand Down Expand Up @@ -1742,7 +1821,10 @@ 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: string) =>
Expand Down
Loading
Loading