Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 3 additions & 0 deletions packages/plugin-rsc/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions packages/plugin-rsc/e2e/starter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,106 @@ test.describe(() => {
})
})

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,
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: ${renderBuiltUrl.toString()}
}
})
`,
},
})
})

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

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
110 changes: 94 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,75 @@ 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
})

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 +1354,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 +1649,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 +1736,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 +1817,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
6 changes: 3 additions & 3 deletions packages/plugin-rsc/src/ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-rsc/src/types/virtual.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare module 'virtual:vite-rsc/assets-manifest' {
const assetsManifest: import('../plugin').AssetsManifest
const assetsManifest: import('../plugin').ResolvedAssetsManifest
export default assetsManifest
}

Expand Down
Loading