From 1282d86490c2011c915060bdacc5afd8757aee28 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Fri, 25 Sep 2026 19:45:52 +0900 Subject: [PATCH] fix: pass queries to `renderBuiltUrl` --- .../vite/src/node/__tests__/build.spec.ts | 36 +++++++++++- packages/vite/src/node/plugins/asset.ts | 58 ++++++++++++++----- packages/vite/src/node/plugins/css.ts | 7 ++- packages/vite/src/node/plugins/html.ts | 8 ++- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index c194760f856866..7aa05ca0b94604 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -25,6 +25,7 @@ import { } from '../build' import type { Logger } from '../logger' import { createLogger } from '../logger' +import { injectQuery } from '../utils' const dirname = import.meta.dirname @@ -112,6 +113,26 @@ describe('build', () => { assertOutputHashContentChange(result[0], result[1]) }) + test('renderBuiltUrl receives asset postfixes in JS and CSS', async () => { + const result = await buildProjectWithRenderBuiltUrl( + (filename) => injectQuery(filename, 'dpl=id'), + true, + ) + const entry = result.output.find( + (output): output is OutputChunk => + output.type === 'chunk' && output.isEntry, + ) + const css = result.output.find( + (output): output is OutputAsset => + output.type === 'asset' && output.fileName.endsWith('.css'), + ) + + expect(entry?.code).toContain('?dpl=id&marker=value') + expect(entry?.code).toContain('?dpl=id&marker=other') + expect(css?.source.toString()).toContain('?dpl=id&marker=value') + expect(css?.source.toString()).toContain('?dpl=id&marker=other') + }) + test('top-level input is used as the default build entry', async () => { const result = (await build({ root: resolve(dirname, 'packages/build-project'), @@ -1564,6 +1585,7 @@ test('copies public directory after building same environment with write false f async function buildProjectWithRenderBuiltUrl( renderBuiltUrl: (filename: string) => string, + includePostfixes = false, ) { return (await build({ root: resolve(dirname, 'packages/build-project'), @@ -1579,21 +1601,29 @@ async function buildProjectWithRenderBuiltUrl( { name: 'test', resolveId(id) { - if (id === 'entry.js' || id === 'subentry.js') { + if (id === 'entry.js' || id === 'subentry.js' || id === 'style.css') { return '\0' + id } }, load(id) { if (id === '\0entry.js') { return ` - import assetUrl from '/asset.txt?url' - console.log(assetUrl) + import assetUrl from '/asset.txt?url${includePostfixes ? '&marker=value' : ''}' + ${includePostfixes ? `import otherAssetUrl from '/asset.txt?url&marker=other'` : ''} + ${includePostfixes ? `import 'style.css'` : ''} + console.log(assetUrl${includePostfixes ? `, otherAssetUrl` : ''}) window.addEventListener('click', () => { import('subentry.js') }) ` } if (id === '\0subentry.js') { return `export default 'subentry'` } + if (id === '\0style.css') { + return ` + .asset-a { background: url('/asset.txt?marker=value') } + .asset-b { background: url('/asset.txt?marker=other') } + ` + } }, }, ], diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 85e12b89d722fa..fade538a50a666 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -45,8 +45,9 @@ import { } from '../utils' import { getImportMapFilename } from './html' -// referenceId is base64url but replaces - with $ -export const assetUrlRE: RegExp = /__VITE_ASSET__([\w$]+)__/g +// referenceId is a 22-character base64url string but replaces - with $ +export const assetUrlRE: RegExp = + /__VITE_ASSET__([\w$]{22})(?:_([a-f\d]{24}))?__/g const encodedHashPlaceholderRE = /!~%7B([\w$]{1,17})%7D~/g function unescapeHashPlaceholders(uri: string): string { @@ -55,6 +56,7 @@ function unescapeHashPlaceholders(uri: string): string { interface FileUrlMetadata { asFileUrl: boolean + postfix: string } const fileUrlMetadata = new WeakMap>() @@ -140,12 +142,12 @@ export function renderAssetUrlInJS( assetUrlRE.lastIndex = 0 while ((match = assetUrlRE.exec(code))) { s ||= new MagicString(code) - const [full, referenceId] = match + const [full, referenceId, urlId] = match const file = pluginContext.getFileName(referenceId) chunk.viteMetadata!.importedAssets.add(cleanUrl(file)) const replacement = toOutputFilePathInJS( environment, - file, + file + getAssetUrlPostfix(environment, urlId), 'asset', chunk.fileName, 'js', @@ -285,7 +287,11 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } return { - code: `export default ${formatBuiltAsset(resolved, 'js')}`, + code: `export default ${formatBuiltAsset( + resolved, + 'js', + addFileUrlMetadataForAsset(this.environment, 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: @@ -327,7 +333,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } const replacement = toOutputFilePathInJS( environment, - fileName, + fileName + (metadata?.postfix ?? ''), 'asset', chunkId, 'js', @@ -555,13 +561,30 @@ async function fileToBuiltUrl( skipPublicCheck, forceInline, ) - const urlId = - resolved.type === 'reference' && format === 'js' && asFileUrl - ? addFileUrlMetadata(pluginContext.environment, { asFileUrl }) - : undefined + const urlId = addFileUrlMetadataForAsset( + pluginContext.environment, + resolved, + format, + asFileUrl, + ) return formatBuiltAsset(resolved, format, urlId) } +function addFileUrlMetadataForAsset( + environment: Environment, + resolved: FileToBuiltUrlResult, + format: AssetUrlFormat, + useFileUrl = false, +): string | undefined { + if (resolved.type !== 'reference') return + + const asFileUrl = format === 'js' && useFileUrl + const { postfix } = resolved + if (!asFileUrl && !postfix) return + + return addFileUrlMetadata(environment, { asFileUrl, postfix }) +} + function addFileUrlMetadata( environment: Environment, metadata: FileUrlMetadata, @@ -580,6 +603,15 @@ function addFileUrlMetadata( return urlId } +export function getAssetUrlPostfix( + environment: Environment, + urlId: string | undefined, +): string { + return urlId + ? (fileUrlMetadata.get(environment)?.get(urlId)?.postfix ?? '') + : '' +} + /** Format a resolved asset as either a JS expression or a plain-text string. */ function formatBuiltAsset( resolved: FileToBuiltUrlResult, @@ -591,11 +623,9 @@ function formatBuiltAsset( const base = urlId ? `import.meta.ROLLDOWN_FILE_URL_${resolved.referenceId}_${urlId}` : `import.meta.ROLLDOWN_FILE_URL_${resolved.referenceId}` - return resolved.postfix - ? `${base} + ${JSON.stringify(resolved.postfix)}` - : base + return base } - return `__VITE_ASSET__${resolved.referenceId}__${resolved.postfix}` + return `__VITE_ASSET__${resolved.referenceId}${urlId ? `_${urlId}` : ''}__` } return format === 'js' ? JSON.stringify(encodeURIPath(resolved.value)) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 6830215563f5fa..61494751dfc1f7 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -100,6 +100,7 @@ import { assetUrlRE, cssEntriesMap, fileToUrl, + getAssetUrlPostfix, publicAssetUrlCache, publicAssetUrlRE, publicFileToBuiltUrl, @@ -751,8 +752,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } // replace asset url references with resolved url. - chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash) => { - const filename = this.getFileName(fileHash) + chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, urlId) => { + const filename = + this.getFileName(fileHash) + + getAssetUrlPostfix(this.environment, urlId) chunk.viteMetadata!.importedAssets.add(cleanUrl(filename)) return encodeURIPath( toOutputFilePathInCss( diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index c09d5067086c37..031c21a814f8f7 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -44,6 +44,7 @@ import { } from '../utils' import { assetUrlRE, + getAssetUrlPostfix, getPublicAssetFilename, publicAssetUrlRE, urlToBuiltUrl, @@ -1074,12 +1075,15 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, ) // resolve asset url references - result = result.replace(assetUrlRE, (_, fileHash) => { + result = result.replace(assetUrlRE, (_, fileHash, urlId) => { const file = this.getFileName(fileHash) if (chunk) { chunk.viteMetadata!.importedAssets.add(cleanUrl(file)) } - return encodeURIPath(toOutputAssetFilePath(file)) + return ( + encodeURIPath(toOutputAssetFilePath(file)) + + getAssetUrlPostfix(this.environment, urlId) + ) }) result = result.replace(publicAssetUrlRE, (_, fileHash) => {