Skip to content
Merged
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
215 changes: 113 additions & 102 deletions packages/vite/src/node/plugins/assetImportMetaUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import path from 'node:path'
import MagicString from 'magic-string'
import { stripLiteral } from 'strip-literal'
import { parseAst } from 'rollup/parseAst'
import type { Plugin } from '../plugin'
// import type { Plugin } from '../plugin'
import type { RolldownPlugin } from 'rolldown'
import type { ResolvedConfig } from '../config'
import type { ResolveFn } from '../'
import { injectQuery, isParentDirectory, transformStableResult } from '../utils'
Expand All @@ -23,7 +24,9 @@ import { hasViteIgnoreRE } from './importAnalysis'
* import.meta.glob('./dir/**.png', { eager: true, import: 'default' })[`./dir/${name}.png`]
* ```
*/
export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
export function assetImportMetaUrlPlugin(
config: ResolvedConfig,
): RolldownPlugin {
const { publicDir } = config
let assetResolver: ResolveFn

Expand All @@ -39,120 +42,128 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {

return {
name: 'vite:asset-import-meta-url',
async transform(code, id, options) {
if (
!options?.ssr &&
id !== preloadHelperId &&
id !== CLIENT_ENTRY &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
let s: MagicString | undefined
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg
const cleanString = stripLiteral(code)
transform: {
filter: {
code: {
include: ['new URL', 'import.meta.url'],
},
},
async handler(code, id, options) {
if (
// @ts-expect-error needs rolldown plugin type compatible
!options?.ssr &&
id !== preloadHelperId &&
id !== CLIENT_ENTRY &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
let s: MagicString | undefined
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg
const cleanString = stripLiteral(code)

let match: RegExpExecArray | null
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices!
if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue
let match: RegExpExecArray | null
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices!
if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue

const rawUrl = code.slice(urlStart, urlEnd)
const rawUrl = code.slice(urlStart, urlEnd)

if (!s) s = new MagicString(code)
if (!s) s = new MagicString(code)

// potential dynamic template string
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl)
const hasQueryDelimiter = queryDelimiterIndex !== -1
const pureUrl = hasQueryDelimiter
? rawUrl.slice(0, queryDelimiterIndex) + '`'
: rawUrl
const queryString = hasQueryDelimiter
? rawUrl.slice(queryDelimiterIndex, -1)
: ''
const ast = parseAst(pureUrl)
const templateLiteral = (ast as any).body[0].expression
if (templateLiteral.expressions.length) {
const pattern = buildGlobPattern(templateLiteral)
if (pattern.startsWith('**')) {
// don't transform for patterns like this
// because users won't intend to do that in most cases
continue
}
// potential dynamic template string
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl)
const hasQueryDelimiter = queryDelimiterIndex !== -1
const pureUrl = hasQueryDelimiter
? rawUrl.slice(0, queryDelimiterIndex) + '`'
: rawUrl
const queryString = hasQueryDelimiter
? rawUrl.slice(queryDelimiterIndex, -1)
: ''
const ast = parseAst(pureUrl)
const templateLiteral = (ast as any).body[0].expression
if (templateLiteral.expressions.length) {
const pattern = buildGlobPattern(templateLiteral)
if (pattern.startsWith('**')) {
// don't transform for patterns like this
// because users won't intend to do that in most cases
continue
}

const globOptions = {
eager: true,
import: 'default',
// A hack to allow 'as' & 'query' exist at the same time
query: injectQuery(queryString, 'url'),
const globOptions = {
eager: true,
import: 'default',
// A hack to allow 'as' & 'query' exist at the same time
query: injectQuery(queryString, 'url'),
}
s.update(
startIndex,
endIndex,
`new URL((import.meta.glob(${JSON.stringify(
pattern,
)}, ${JSON.stringify(
globOptions,
)}))[${pureUrl}], import.meta.url)`,
)
continue
}
s.update(
startIndex,
endIndex,
`new URL((import.meta.glob(${JSON.stringify(
pattern,
)}, ${JSON.stringify(
globOptions,
)}))[${pureUrl}], import.meta.url)`,
)
continue
}
}

const url = rawUrl.slice(1, -1)
let file: string | undefined
if (url[0] === '.') {
file = slash(path.resolve(path.dirname(id), url))
file = tryFsResolve(file, fsResolveOptions) ?? file
} else {
assetResolver ??= config.createResolver({
extensions: [],
mainFields: [],
tryIndex: false,
preferRelative: true,
})
file = await assetResolver(url, id)
file ??=
url[0] === '/'
? slash(path.join(publicDir, url))
: slash(path.resolve(path.dirname(id), url))
}
const url = rawUrl.slice(1, -1)
let file: string | undefined
if (url[0] === '.') {
file = slash(path.resolve(path.dirname(id), url))
file = tryFsResolve(file, fsResolveOptions) ?? file
} else {
assetResolver ??= config.createResolver({
extensions: [],
mainFields: [],
tryIndex: false,
preferRelative: true,
})
file = await assetResolver(url, id)
file ??=
url[0] === '/'
? slash(path.join(publicDir, url))
: slash(path.resolve(path.dirname(id), url))
}

// Get final asset URL. If the file does not exist,
// we fall back to the initial URL and let it resolve in runtime
let builtUrl: string | undefined
if (file) {
try {
if (publicDir && isParentDirectory(publicDir, file)) {
const publicPath = '/' + path.posix.relative(publicDir, file)
builtUrl = await fileToUrl(publicPath, config, this)
} else {
builtUrl = await fileToUrl(file, config, this)
// Get final asset URL. If the file does not exist,
// we fall back to the initial URL and let it resolve in runtime
let builtUrl: string | undefined
if (file) {
try {
if (publicDir && isParentDirectory(publicDir, file)) {
const publicPath = '/' + path.posix.relative(publicDir, file)
builtUrl = await fileToUrl(publicPath, config, this)
} else {
builtUrl = await fileToUrl(file, config, this)
}
} catch {
// do nothing, we'll log a warning after this
}
} catch {
// do nothing, we'll log a warning after this
}
}
if (!builtUrl) {
const rawExp = code.slice(startIndex, endIndex)
config.logger.warnOnce(
`\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` +
`If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`,
if (!builtUrl) {
const rawExp = code.slice(startIndex, endIndex)
config.logger.warnOnce(
`\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` +
`If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`,
)
builtUrl = url
}
s.update(
startIndex,
endIndex,
`new URL(${JSON.stringify(builtUrl)}, import.meta.url)`,
)
builtUrl = url
}
s.update(
startIndex,
endIndex,
`new URL(${JSON.stringify(builtUrl)}, import.meta.url)`,
)
}
if (s) {
return transformStableResult(s, id, config)
if (s) {
return transformStableResult(s, id, config)
}
}
}
return null
return null
},
},
}
}
Expand Down