Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/vite/src/node/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const ROLLUP_HOOKS: RollupPluginHooks[] = [
'augmentChunkHash',
'outputOptions',
// 'renderDynamicImport',
// 'resolveFileUrl',
'resolveFileUrl',
// 'resolveImportMeta',
'intro',
'outro',
Expand Down
163 changes: 148 additions & 15 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Environment, Map<string, string>>()
/**
* 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_<referenceId>` (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<Environment, Map<string, FileToBuiltUrlResult>>()

/**
* Emitted asset file names referenced from each chunk (keyed by preliminary
* chunk name) via `import.meta.ROLLDOWN_FILE_URL_<referenceId>`.
*/
const importedAssetsFromFileUrl = new WeakMap<
Environment,
Map<string, Set<string>>
>()

/** a set of referenceId for entry CSS assets for each environment */
export const cssEntriesMap: WeakMap<
Expand Down Expand Up @@ -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:
Expand All @@ -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) {
Expand Down Expand Up @@ -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')
}
}

Expand Down Expand Up @@ -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<string> {
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<FileToBuiltUrlResult> {
const environment = pluginContext.environment
const topLevelConfig = environment.getTopLevelConfig()
if (!skipPublicCheck) {
Expand All @@ -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),
}
}
}
}
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -533,6 +665,7 @@ export async function urlToBuiltUrl(
return fileToBuiltUrl(
pluginContext,
file,
'string',
// skip public check since we just did it above
true,
forceInline,
Expand Down
Loading