Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
45 changes: 45 additions & 0 deletions packages/plugin-rsc/e2e/import-asset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { test } from '@playwright/test'
import { setupInlineFixture, useFixture } from './fixture'
import { defineStarterTest } from './starter'

test.describe('viteRsc.importAsset', () => {
const root = 'examples/e2e/temp/import-asset'
test.beforeAll(async () => {
await setupInlineFixture({
src: 'examples/starter',
dest: root,
files: {
'src/framework/entry.ssr.tsx': {
edit: (s) =>
s.replace(
`\
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')
`,
`\
const asset = await import.meta.viteRsc.importAsset('./entry.browser.tsx', { entry: true })
const bootstrapScriptContent = \`import(\${JSON.stringify(asset.url)})\`
`,
),
},
// Remove "index" client entry to test importAsset replacing the convention
'vite.config.base.ts': { cp: 'vite.config.ts' },
'vite.config.ts': /* js */ `
import baseConfig from './vite.config.base.ts'
delete baseConfig.environments.client.build.rollupOptions.input;
export default baseConfig;
`,
},
})
})

test.describe('dev', () => {
const f = useFixture({ root, mode: 'dev' })
defineStarterTest(f)
})

test.describe('build', () => {
const f = useFixture({ root, mode: 'build' })
defineStarterTest(f)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export async function renderHTML(
}

// render html (traditional SSR)
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')
const asset = await import.meta.viteRsc.importAsset('./entry.browser.tsx', {
entry: true,
})
const bootstrapScriptContent = `import(${JSON.stringify(asset.url)})`
let htmlStream: ReadableStream<Uint8Array>
let status: number | undefined
try {
Expand Down
126 changes: 107 additions & 19 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ import { crawlFrameworkPkgs } from 'vitefu'
import vitePluginRscCore from './core/plugin'
import { cjsModuleRunnerPlugin } from './plugins/cjs'
import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url'
import {
ensureAssetImportsClientEntry,
vitePluginImportAsset,
type AssetImportMeta,
} from './plugins/import-asset'
import {
ensureEnvironmentImportsEntryFallback,
vitePluginImportEnvironment,
Expand Down Expand Up @@ -136,6 +141,13 @@ class RscPluginManager {
>
>
> = {}
assetImportMetaMap: Record<
string, // sourceEnv
Record<
string, // resolvedId
AssetImportMeta
>
> = {}

stabilize(): void {
// sort for stable build
Expand Down Expand Up @@ -394,6 +406,7 @@ export default function vitePluginRsc(

// rsc -> ssr -> rsc -> client -> ssr
ensureEnvironmentImportsEntryFallback(builder.config)
ensureAssetImportsClientEntry(builder.config)
manager.isScanBuild = true
builder.environments.rsc!.config.build.write = false
builder.environments.ssr!.config.build.write = false
Expand Down Expand Up @@ -1065,35 +1078,107 @@ export function createRpcClient(params) {
}

const assetDeps = collectAssetDeps(bundle)
const entry = Object.values(assetDeps).find(
(v) => v.chunk.name === 'index' && v.chunk.isEntry,
)
assert(entry)
const entryUrl = assetsURL(entry.chunk.fileName, manager)
const clientReferenceDeps: Record<string, AssetDeps> = {}
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
const deps: AssetDeps = assetDeps[meta.groupChunkId!]?.deps ?? {
js: [],
css: [],

// Check if there are any importAsset entries with isEntry: true
const importAssetEntries: Array<{
resolvedId: string
chunk: Rollup.OutputChunk
deps: AssetDeps
}> = []
for (const metas of Object.values(manager.assetImportMetaMap)) {
for (const [resolvedId, meta] of Object.entries(metas)) {
if (meta.isEntry) {
const chunk = Object.values(bundle).find(
(c): c is Rollup.OutputChunk =>
c.type === 'chunk' && c.facadeModuleId === resolvedId,
)
if (chunk) {
const chunkDeps = assetDeps[chunk.fileName]
if (chunkDeps) {
importAssetEntries.push({
resolvedId,
chunk,
deps: chunkDeps.deps,
})
}
}
}
}
}

// Compute importAssets from assetImportMetaMap
const importAssets: Record<string, { url: string | RuntimeAsset }> =
{}
for (const metas of Object.values(manager.assetImportMetaMap)) {
for (const resolvedId of Object.keys(metas)) {
const chunk = Object.values(bundle).find(
(c) => c.type === 'chunk' && c.facadeModuleId === resolvedId,
)
if (chunk) {
const relativeId = manager.toRelativeId(resolvedId)
importAssets[relativeId] = {
url: assetsURL(chunk.fileName, manager),
}
}
}
clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps(
mergeAssetDeps(deps, entry.deps),
manager,
)
}
let bootstrapScriptContent: string | RuntimeAsset
if (typeof entryUrl === 'string') {
bootstrapScriptContent = `import(${JSON.stringify(entryUrl)})`

let bootstrapScriptContent: string | RuntimeAsset = ''
const clientReferenceDeps: Record<string, AssetDeps> = {}

if (importAssetEntries.length > 0) {
// Use importAsset entries for merging deps into client references
// Merge all entry deps together
let entryDeps: AssetDeps = { js: [], css: [] }
for (const entry of importAssetEntries) {
entryDeps = mergeAssetDeps(entryDeps, entry.deps)
}
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
const deps: AssetDeps = assetDeps[meta.groupChunkId!]?.deps ?? {
js: [],
css: [],
}
clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps(
mergeAssetDeps(deps, entryDeps),
manager,
)
}
} else {
bootstrapScriptContent = new RuntimeAsset(
`"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`,
// Fall back to "index" entry convention
const entry = Object.values(assetDeps).find(
(v) => v.chunk.name === 'index' && v.chunk.isEntry,
)
assert(
entry,
`[vite-rsc] missing "index" entry. Use importAsset with { entry: true } or configure client entry.`,
)
const entryUrl = assetsURL(entry.chunk.fileName, manager)
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
const deps: AssetDeps = assetDeps[meta.groupChunkId!]?.deps ?? {
js: [],
css: [],
}
clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps(
mergeAssetDeps(deps, entry.deps),
manager,
)
}
if (typeof entryUrl === 'string') {
bootstrapScriptContent = `import(${JSON.stringify(entryUrl)})`
} else {
bootstrapScriptContent = new RuntimeAsset(
`"import(" + JSON.stringify(${entryUrl.runtime}) + ")"`,
)
}
}

manager.buildAssetsManifest = {
bootstrapScriptContent,
clientReferenceDeps,
serverResources,
cssLinkPrecedence: rscPluginOptions.cssLinkPrecedence,
importAssets:
Object.keys(importAssets).length > 0 ? importAssets : undefined,
}
}
},
Expand Down Expand Up @@ -1220,6 +1305,7 @@ import.meta.hot.on("rsc:update", () => {
),
...vitePluginRscMinimal(rscPluginOptions, manager),
...vitePluginImportEnvironment(manager),
...vitePluginImportAsset(manager),
...vitePluginFindSourceMapURL(),
...vitePluginRscCss(rscPluginOptions, manager),
{
Expand Down Expand Up @@ -1990,6 +2076,7 @@ export type AssetsManifest = {
clientReferenceDeps: Record<string, AssetDeps>
serverResources?: Record<string, Pick<AssetDeps, 'css'>>
cssLinkPrecedence?: boolean
importAssets?: Record<string, { url: string | RuntimeAsset }>
}

export type AssetDeps = {
Expand All @@ -2002,6 +2089,7 @@ export type ResolvedAssetsManifest = {
clientReferenceDeps: Record<string, ResolvedAssetDeps>
serverResources?: Record<string, Pick<ResolvedAssetDeps, 'css'>>
cssLinkPrecedence?: boolean
importAssets?: Record<string, { url: string }>
}

export type ResolvedAssetDeps = {
Expand Down
Loading
Loading