diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 200d076f76334e..7600888c0b64e3 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -17,7 +17,7 @@ export const ROLLUP_HOOKS: RollupPluginHooks[] = [ 'augmentChunkHash', 'outputOptions', // 'renderDynamicImport', - // 'resolveFileUrl', + 'resolveFileUrl', // 'resolveImportMeta', 'intro', 'outro', diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 058249090eb0be..8a4e653d699aae 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -52,7 +52,35 @@ const jsSourceMapRE = /\.[cm]?js\.map$/ export const noInlineRE: RegExp = /[?&]no-inline\b/ export const inlineRE: RegExp = /[?&]inline\b/ -const assetCache = new WeakMap>() +/** + * The resolved form of an asset request during build. + * - `string`: a URL usable as-is (an inlined `data:` URL, a + * `__VITE_PUBLIC_ASSET__` token, or a bundled-dev output URL). + * - `reference`: an emitted file referenced by its `referenceId`, to be turned + * into `import.meta.ROLLDOWN_FILE_URL_` (JS) or a `__VITE_ASSET__` + * token (CSS/HTML). The `postfix` is the query/hash appended after the URL. + */ +type FileToBuiltUrlResult = + | { type: 'string'; value: string } + | { type: 'reference'; referenceId: string; postfix: string } + +/** + * How an asset URL should be embedded by the caller: + * - `'string'`: plain text (CSS/HTML and other text consumers) + * - `'js'`: a JavaScript expression (embedded in generated JS) + */ +type AssetUrlFormat = 'string' | 'js' + +const assetCache = new WeakMap>() + +/** + * Emitted asset file names referenced from each chunk (keyed by preliminary + * chunk name) via `import.meta.ROLLDOWN_FILE_URL_`. + */ +const importedAssetsFromFileUrl = new WeakMap< + Environment, + Map> +>() /** a set of referenceId for entry CSS assets for each environment */ export const cssEntriesMap: WeakMap< @@ -219,18 +247,33 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } id = removeUrlQuery(id) - let url = await fileToUrl(this, id) + let resolved: FileToBuiltUrlResult + if (!this.environment.config.isBundled) { + resolved = { + type: 'string', + value: await fileToDevUrl(this.environment, id), + } + } else { + resolved = await resolveBuiltAsset(this, id) + } // Inherit HMR timestamp if this asset was invalidated - if (!url.startsWith('data:') && this.environment.mode === 'dev') { + if ( + resolved.type === 'string' && + !resolved.value.startsWith('data:') && + this.environment.mode === 'dev' + ) { const mod = this.environment.moduleGraph.getModuleById(id) if (mod && mod.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) + resolved = { + type: 'string', + value: injectQuery(resolved.value, `t=${mod.lastHMRTimestamp}`), + } } } return { - code: `export default ${JSON.stringify(encodeURIPath(url))}`, + code: `export default ${formatBuiltAsset(resolved, 'js')}`, // Force rollup to keep this module from being shared between other entry points if it's an entrypoint. // If the resulting chunk is empty, it will be removed in generateBundle. moduleSideEffects: @@ -245,7 +288,48 @@ export function assetPlugin(config: ResolvedConfig): Plugin { ...(config.command === 'build' ? { + resolveFileUrl({ fileName, chunkId, format }) { + const { environment } = this + + let importedByChunk = importedAssetsFromFileUrl.get(environment) + if (!importedByChunk) { + importedByChunk = new Map() + importedAssetsFromFileUrl.set(environment, importedByChunk) + } + let files = importedByChunk.get(chunkId) + if (!files) { + files = new Set() + importedByChunk.set(chunkId, files) + } + files.add(cleanUrl(fileName)) + + const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( + format, + environment.config.isWorker, + ) + const replacement = toOutputFilePathInJS( + environment, + fileName, + 'asset', + chunkId, + 'js', + toRelativeRuntime, + ) + return typeof replacement === 'string' + ? JSON.stringify(encodeURIPath(replacement)) + : replacement.runtime + }, + renderChunk(code, chunk, opts) { + const importedFromFileUrl = importedAssetsFromFileUrl + .get(this.environment) + ?.get(chunk.fileName) + if (importedFromFileUrl) { + for (const file of importedFromFileUrl) { + chunk.viteMetadata!.importedAssets.add(file) + } + } + const s = renderAssetUrlInJS(this, chunk, opts, code) if (s) { @@ -332,7 +416,7 @@ export async function fileToUrl( if (!environment.config.isBundled) { return fileToDevUrl(environment, id, asFileUrl) } else { - return fileToBuiltUrl(pluginContext, id) + return fileToBuiltUrl(pluginContext, id, 'string') } } @@ -425,15 +509,54 @@ function isGitLfsPlaceholder(content: Buffer): boolean { } /** - * Register an asset to be emitted as part of the bundle (if necessary) - * and returns the resolved public URL + * Register an asset to be emitted as part of the bundle (if necessary) and + * return its resolved URL in the requested `format`. */ async function fileToBuiltUrl( pluginContext: PluginContext, id: string, + format: AssetUrlFormat, skipPublicCheck = false, forceInline?: boolean, ): Promise { + const resolved = await resolveBuiltAsset( + pluginContext, + id, + skipPublicCheck, + forceInline, + ) + return formatBuiltAsset(resolved, format) +} + +/** Format a resolved asset as either a JS expression or a plain-text string. */ +function formatBuiltAsset( + resolved: FileToBuiltUrlResult, + format: AssetUrlFormat, +): string { + if (resolved.type === 'reference') { + if (format === 'js') { + const base = `import.meta.ROLLDOWN_FILE_URL_${resolved.referenceId}` + return resolved.postfix + ? `${base} + ${JSON.stringify(resolved.postfix)}` + : base + } + return `__VITE_ASSET__${resolved.referenceId}__${resolved.postfix}` + } + return format === 'js' + ? JSON.stringify(encodeURIPath(resolved.value)) + : resolved.value +} + +/** + * Register an asset to be emitted (if necessary) and return the structured result, + * cached per id so the emitted file is shared. + */ +async function resolveBuiltAsset( + pluginContext: PluginContext, + id: string, + skipPublicCheck = false, + forceInline?: boolean, +): Promise { const environment = pluginContext.environment const topLevelConfig = environment.getTopLevelConfig() if (!skipPublicCheck) { @@ -443,7 +566,10 @@ async function fileToBuiltUrl( // If inline via query, re-assign the id so it can be read by the fs and inlined id = publicFile } else { - return publicFileToBuiltUrl(id, topLevelConfig) + return { + type: 'string', + value: publicFileToBuiltUrl(id, topLevelConfig), + } } } } @@ -457,11 +583,14 @@ async function fileToBuiltUrl( let { file, postfix } = splitFileAndPostfix(id) const content = await fsp.readFile(file) - let url: string + let result: FileToBuiltUrlResult if ( shouldInline(environment, file, id, content, pluginContext, forceInline) ) { - url = assetToDataURL(environment, file, content) + result = { + type: 'string', + value: assetToDataURL(environment, file, content), + } } else { // emit as asset const originalFileName = normalizePath( @@ -484,14 +613,17 @@ async function fileToBuiltUrl( environment.config.isBundled ) { const outputFilename = pluginContext.getFileName(referenceId) - url = toOutputFilePathInJSForBundledDev(environment, outputFilename) + result = { + type: 'string', + value: toOutputFilePathInJSForBundledDev(environment, outputFilename), + } } else { - url = `__VITE_ASSET__${referenceId}__${postfix}` + result = { type: 'reference', referenceId, postfix } } } - cache.set(id, url) - return url + cache.set(id, result) + return result } export function toOutputFilePathInJSForBundledDev( @@ -533,6 +665,7 @@ export async function urlToBuiltUrl( return fileToBuiltUrl( pluginContext, file, + 'string', // skip public check since we just did it above true, forceInline,