From fdd92ca5ad2ba64fd0c7e47f67ffcea9ad4397c1 Mon Sep 17 00:00:00 2001
From: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
Date: Tue, 7 Jul 2026 18:47:17 +0900
Subject: [PATCH 1/3] feat: use `import.meta.ROLLUP_FILE_URL_*` for assets in
JS
---
packages/vite/src/node/plugin.ts | 15 +++
packages/vite/src/node/plugins/asset.ts | 163 +++++++++++++++++++++---
2 files changed, 163 insertions(+), 15 deletions(-)
diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts
index bed86871a58aa0..c5bb26675f2a53 100644
--- a/packages/vite/src/node/plugin.ts
+++ b/packages/vite/src/node/plugin.ts
@@ -1,6 +1,7 @@
import type {
CustomPluginOptions,
ImportKind,
+ InternalModuleFormat,
LoadResult,
MinimalPluginContext,
ModuleType,
@@ -173,6 +174,20 @@ export interface Plugin extends RolldownPlugin {
}
}
>
+ /**
+ * TODO: implement in Rolldown. Add a type for now.
+ */
+ resolveFileUrl?: (
+ this: PluginContext,
+ options: {
+ chunkId: string
+ fileName: string
+ format: InternalModuleFormat
+ moduleId: string
+ referenceId: string
+ relativePath: string
+ },
+ ) => string | null | undefined
/**
* Opt-in this plugin into the shared plugins pipeline.
* For backward-compatibility, plugins are re-recreated for each environment
diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts
index 058249090eb0be..14d759356bf74a 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.ROLLUP_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.ROLLUP_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.ROLLUP_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,
From 048120284357132e3102e6649863e241f4be293a Mon Sep 17 00:00:00 2001
From: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
Date: Wed, 15 Jul 2026 18:33:08 +0900
Subject: [PATCH 2/3] chore: update
---
packages/vite/src/node/constants.ts | 2 +-
packages/vite/src/node/plugin.ts | 15 ---------------
2 files changed, 1 insertion(+), 16 deletions(-)
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/plugin.ts b/packages/vite/src/node/plugin.ts
index c5bb26675f2a53..bed86871a58aa0 100644
--- a/packages/vite/src/node/plugin.ts
+++ b/packages/vite/src/node/plugin.ts
@@ -1,7 +1,6 @@
import type {
CustomPluginOptions,
ImportKind,
- InternalModuleFormat,
LoadResult,
MinimalPluginContext,
ModuleType,
@@ -174,20 +173,6 @@ export interface Plugin extends RolldownPlugin {
}
}
>
- /**
- * TODO: implement in Rolldown. Add a type for now.
- */
- resolveFileUrl?: (
- this: PluginContext,
- options: {
- chunkId: string
- fileName: string
- format: InternalModuleFormat
- moduleId: string
- referenceId: string
- relativePath: string
- },
- ) => string | null | undefined
/**
* Opt-in this plugin into the shared plugins pipeline.
* For backward-compatibility, plugins are re-recreated for each environment
From 28d91650aa85e48338c8c5aee9915a993139d871 Mon Sep 17 00:00:00 2001
From: sapphi-red
Date: Fri, 17 Jul 2026 16:15:15 +0900
Subject: [PATCH 3/3] chore: use `ROLLDOWN_FILE_URL`
---
packages/vite/src/node/plugins/asset.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts
index 14d759356bf74a..8a4e653d699aae 100644
--- a/packages/vite/src/node/plugins/asset.ts
+++ b/packages/vite/src/node/plugins/asset.ts
@@ -57,7 +57,7 @@ export const inlineRE: RegExp = /[?&]inline\b/
* - `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.ROLLUP_FILE_URL_` (JS) or a `__VITE_ASSET__`
+ * 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 =
@@ -75,7 +75,7 @@ const assetCache = new WeakMap>()
/**
* Emitted asset file names referenced from each chunk (keyed by preliminary
- * chunk name) via `import.meta.ROLLUP_FILE_URL_`.
+ * chunk name) via `import.meta.ROLLDOWN_FILE_URL_`.
*/
const importedAssetsFromFileUrl = new WeakMap<
Environment,
@@ -535,7 +535,7 @@ function formatBuiltAsset(
): string {
if (resolved.type === 'reference') {
if (format === 'js') {
- const base = `import.meta.ROLLUP_FILE_URL_${resolved.referenceId}`
+ const base = `import.meta.ROLLDOWN_FILE_URL_${resolved.referenceId}`
return resolved.postfix
? `${base} + ${JSON.stringify(resolved.postfix)}`
: base