Skip to content

Commit 09648c2

Browse files
authored
feat!: relative base (#7644)
1 parent 04046ea commit 09648c2

29 files changed

+768
-102
lines changed

packages/plugin-legacy/src/index.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ const legacyPolyfillId = 'vite-legacy-polyfill'
3737
const legacyEntryId = 'vite-legacy-entry'
3838
const systemJSInlineCode = `System.import(document.getElementById('${legacyEntryId}').getAttribute('data-src'))`
3939

40-
const detectDynamicImportVarName = '__vite_is_dynamic_import_support'
41-
const detectDynamicImportCode = `try{import("_").catch(()=>1);}catch(e){}window.${detectDynamicImportVarName}=true;`
42-
const dynamicFallbackInlineCode = `!function(){if(window.${detectDynamicImportVarName})return;console.warn("vite: loading legacy build because dynamic import is unsupported, syntax error above should be ignored");var e=document.getElementById("${legacyPolyfillId}"),n=document.createElement("script");n.src=e.src,n.onload=function(){${systemJSInlineCode}},document.body.appendChild(n)}();`
40+
const detectModernBrowserVarName = '__vite_is_modern_browser'
41+
const detectModernBrowserCode = `try{import(new URL(import.meta.url).href).catch(()=>1);}catch(e){}window.${detectModernBrowserVarName}=true;`
42+
const dynamicFallbackInlineCode = `!function(){if(window.${detectModernBrowserVarName})return;console.warn("vite: loading legacy build because dynamic import or import.meta.url is unsupported, syntax error above should be ignored");var e=document.getElementById("${legacyPolyfillId}"),n=document.createElement("script");n.src=e.src,n.onload=function(){${systemJSInlineCode}},document.body.appendChild(n)}();`
4343

4444
const forceDynamicImportUsage = `export function __vite_legacy_guard(){import('data:text/javascript,')};`
4545

@@ -438,7 +438,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
438438
tags.push({
439439
tag: 'script',
440440
attrs: { type: 'module' },
441-
children: detectDynamicImportCode,
441+
children: detectModernBrowserCode,
442442
injectTo: 'head'
443443
})
444444
tags.push({
@@ -686,7 +686,7 @@ function wrapIIFEBabelPlugin(): BabelPlugin {
686686
export const cspHashes = [
687687
createHash('sha256').update(safari10NoModuleFix).digest('base64'),
688688
createHash('sha256').update(systemJSInlineCode).digest('base64'),
689-
createHash('sha256').update(detectDynamicImportCode).digest('base64'),
689+
createHash('sha256').update(detectModernBrowserCode).digest('base64'),
690690
createHash('sha256').update(dynamicFallbackInlineCode).digest('base64')
691691
]
692692

packages/vite/src/node/build.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions {
245245
// Support browserslist
246246
// "defaults and supports es6-module and supports es6-module-dynamic-import",
247247
resolved.target = [
248-
'es2019',
248+
'es2020', // support import.meta.url
249249
'edge88',
250250
'firefox78',
251251
'chrome87',
252-
'safari13.1'
252+
'safari13' // transpile nullish coalescing
253253
]
254254
} else if (resolved.target === 'esnext' && resolved.minify === 'terser') {
255255
// esnext + terser: limit to es2019 so it can be minified by terser

packages/vite/src/node/plugins/asset.ts

+57-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { OutputOptions, PluginContext } from 'rollup'
66
import MagicString from 'magic-string'
77
import type { Plugin } from '../plugin'
88
import type { ResolvedConfig } from '../config'
9-
import { cleanUrl, getHash, normalizePath } from '../utils'
9+
import { cleanUrl, getHash, isRelativeBase, normalizePath } from '../utils'
1010
import { FS_PREFIX } from '../constants'
1111

1212
export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g
@@ -29,6 +29,7 @@ const emittedHashMap = new WeakMap<ResolvedConfig, Set<string>>()
2929
export function assetPlugin(config: ResolvedConfig): Plugin {
3030
// assetHashToFilenameMap initialization in buildStart causes getAssetFilename to return undefined
3131
assetHashToFilenameMap.set(config, new Map())
32+
const relativeBase = isRelativeBase(config.base)
3233

3334
// add own dictionary entry by directly assigning mrmine
3435
// https://github.com/lukeed/mrmime/issues/3
@@ -82,8 +83,13 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
8283
let match: RegExpExecArray | null
8384
let s: MagicString | undefined
8485

86+
const absoluteUrlPathInterpolation = (filename: string) =>
87+
`"+new URL(${JSON.stringify(
88+
path.posix.relative(path.dirname(chunk.fileName), filename)
89+
)},import.meta.url).href+"`
90+
8591
// Urls added with JS using e.g.
86-
// imgElement.src = "my/file.png" are using quotes
92+
// imgElement.src = "__VITE_ASSET__5aa0ddc0__" are using quotes
8793

8894
// Urls added in CSS that is imported in JS end up like
8995
// var inlined = ".inlined{color:green;background:url(__VITE_ASSET__5aa0ddc0__)}\n";
@@ -94,15 +100,33 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
94100
s = s || (s = new MagicString(code))
95101
const [full, hash, postfix = ''] = match
96102
// some internal plugins may still need to emit chunks (e.g. worker) so
97-
// fallback to this.getFileName for that.
103+
// fallback to this.getFileName for that. TODO: remove, not needed
98104
const file = getAssetFilename(hash, config) || this.getFileName(hash)
99105
chunk.viteMetadata.importedAssets.add(cleanUrl(file))
100-
const outputFilepath = config.base + file + postfix
106+
const filename = file + postfix
107+
const outputFilepath = relativeBase
108+
? absoluteUrlPathInterpolation(filename)
109+
: JSON.stringify(config.base + filename).slice(1, -1)
101110
s.overwrite(match.index, match.index + full.length, outputFilepath, {
102111
contentOnly: true
103112
})
104113
}
105114

115+
// Replace __VITE_PUBLIC_ASSET__5aa0ddc0__ with absolute paths
116+
117+
if (relativeBase) {
118+
const publicAssetUrlMap = publicAssetUrlCache.get(config)!
119+
while ((match = publicAssetUrlRE.exec(code))) {
120+
s = s || (s = new MagicString(code))
121+
const [full, hash] = match
122+
const publicUrl = publicAssetUrlMap.get(hash)!
123+
const replacement = absoluteUrlPathInterpolation(publicUrl.slice(1))
124+
s.overwrite(match.index, match.index + full.length, replacement, {
125+
contentOnly: true
126+
})
127+
}
128+
}
129+
106130
if (s) {
107131
return {
108132
code: s.toString(),
@@ -258,6 +282,33 @@ export function assetFileNamesToFileName(
258282
return fileName
259283
}
260284

285+
export const publicAssetUrlCache = new WeakMap<
286+
ResolvedConfig,
287+
// hash -> url
288+
Map<string, string>
289+
>()
290+
291+
export const publicAssetUrlRE = /__VITE_PUBLIC_ASSET__([a-z\d]{8})__/g
292+
293+
export function publicFileToBuiltUrl(
294+
url: string,
295+
config: ResolvedConfig
296+
): string {
297+
if (!isRelativeBase(config.base)) {
298+
return config.base + url.slice(1)
299+
}
300+
const hash = getHash(url)
301+
let cache = publicAssetUrlCache.get(config)
302+
if (!cache) {
303+
cache = new Map<string, string>()
304+
publicAssetUrlCache.set(config, cache)
305+
}
306+
if (!cache.get(hash)) {
307+
cache.set(hash, url)
308+
}
309+
return `__VITE_PUBLIC_ASSET__${hash}__`
310+
}
311+
261312
/**
262313
* Register an asset to be emitted as part of the bundle (if necessary)
263314
* and returns the resolved public URL
@@ -269,7 +320,7 @@ async function fileToBuiltUrl(
269320
skipPublicCheck = false
270321
): Promise<string> {
271322
if (!skipPublicCheck && checkPublicFile(id, config)) {
272-
return config.base + id.slice(1)
323+
return publicFileToBuiltUrl(id, config)
273324
}
274325

275326
const cache = assetCache.get(config)!
@@ -342,7 +393,7 @@ export async function urlToBuiltUrl(
342393
pluginContext: PluginContext
343394
): Promise<string> {
344395
if (checkPublicFile(url, config)) {
345-
return config.base + url.slice(1)
396+
return publicFileToBuiltUrl(url, config)
346397
}
347398
const file = url.startsWith('/')
348399
? path.join(config.root, url)

packages/vite/src/node/plugins/assetImportMetaUrl.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { stripLiteral } from 'strip-literal'
44
import type { Plugin } from '../plugin'
55
import type { ResolvedConfig } from '../config'
66
import { fileToUrl } from './asset'
7+
import { preloadHelperId } from './importAnalysisBuild'
78

89
/**
910
* Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL
@@ -21,6 +22,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
2122
async transform(code, id, options) {
2223
if (
2324
!options?.ssr &&
25+
id !== preloadHelperId &&
2426
code.includes('new URL') &&
2527
code.includes(`import.meta.url`)
2628
) {

packages/vite/src/node/plugins/css.ts

+83-41
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
isDataUrl,
3939
isExternalUrl,
4040
isObject,
41+
isRelativeBase,
4142
normalizePath,
4243
parseRequest,
4344
processSrcSet
@@ -48,7 +49,10 @@ import {
4849
assetUrlRE,
4950
checkPublicFile,
5051
fileToUrl,
51-
getAssetFilename
52+
getAssetFilename,
53+
publicAssetUrlCache,
54+
publicAssetUrlRE,
55+
publicFileToBuiltUrl
5256
} from './asset'
5357

5458
// const debug = createDebugger('vite:css')
@@ -106,6 +110,8 @@ const inlineCSSRE = /(\?|&)inline-css\b/
106110
const usedRE = /(\?|&)used\b/
107111
const varRE = /^var\(/i
108112

113+
const cssBundleName = 'style.css'
114+
109115
const enum PreprocessLang {
110116
less = 'less',
111117
sass = 'sass',
@@ -183,7 +189,11 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
183189

184190
const urlReplacer: CssUrlReplacer = async (url, importer) => {
185191
if (checkPublicFile(url, config)) {
186-
return config.base + url.slice(1)
192+
if (isRelativeBase(config.base)) {
193+
return publicFileToBuiltUrl(url, config)
194+
} else {
195+
return config.base + url.slice(1)
196+
}
187197
}
188198
const resolved = await resolveUrl(url, importer)
189199
if (resolved) {
@@ -283,6 +293,30 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
283293
let outputToExtractedCSSMap: Map<NormalizedOutputOptions, string>
284294
let hasEmitted = false
285295

296+
const relativeBase = isRelativeBase(config.base)
297+
298+
const rollupOptionsOutput = config.build.rollupOptions.output
299+
const assetFileNames = (
300+
Array.isArray(rollupOptionsOutput)
301+
? rollupOptionsOutput[0]
302+
: rollupOptionsOutput
303+
)?.assetFileNames
304+
const getCssAssetDirname = (cssAssetName: string) => {
305+
if (!assetFileNames) {
306+
return config.build.assetsDir
307+
} else if (typeof assetFileNames === 'string') {
308+
return path.dirname(assetFileNames)
309+
} else {
310+
return path.dirname(
311+
assetFileNames({
312+
name: cssAssetName,
313+
type: 'asset',
314+
source: '/* vite internal call, ignore */'
315+
})
316+
)
317+
}
318+
}
319+
286320
return {
287321
name: 'vite:css-post',
288322

@@ -415,35 +449,42 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
415449
return null
416450
}
417451

418-
// resolve asset URL placeholders to their built file URLs and perform
419-
// minification if necessary
420-
const processChunkCSS = async (
421-
css: string,
422-
{
423-
inlined,
424-
minify
425-
}: {
426-
inlined: boolean
427-
minify: boolean
428-
}
429-
) => {
452+
const publicAssetUrlMap = publicAssetUrlCache.get(config)!
453+
454+
// resolve asset URL placeholders to their built file URLs
455+
function resolveAssetUrlsInCss(chunkCSS: string, cssAssetName: string) {
456+
const cssAssetDirname = relativeBase
457+
? getCssAssetDirname(cssAssetName)
458+
: undefined
459+
430460
// replace asset url references with resolved url.
431-
const isRelativeBase = config.base === '' || config.base.startsWith('.')
432-
css = css.replace(assetUrlRE, (_, fileHash, postfix = '') => {
461+
chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => {
433462
const filename = getAssetFilename(fileHash, config) + postfix
434463
chunk.viteMetadata.importedAssets.add(cleanUrl(filename))
435-
if (!isRelativeBase || inlined) {
436-
// absolute base or relative base but inlined (injected as style tag into
437-
// index.html) use the base as-is
438-
return config.base + filename
464+
if (relativeBase) {
465+
// relative base + extracted CSS
466+
const relativePath = path.posix.relative(cssAssetDirname!, filename)
467+
return relativePath.startsWith('.')
468+
? relativePath
469+
: './' + relativePath
439470
} else {
440-
// relative base + extracted CSS - asset file will be in the same dir
441-
return `./${path.posix.basename(filename)}`
471+
// absolute base
472+
return config.base + filename
442473
}
443474
})
444-
// only external @imports and @charset should exist at this point
445-
css = await finalizeCss(css, minify, config)
446-
return css
475+
// resolve public URL from CSS paths
476+
if (relativeBase) {
477+
const relativePathToPublicFromCSS = path.posix.relative(
478+
cssAssetDirname!,
479+
''
480+
)
481+
chunkCSS = chunkCSS.replace(
482+
publicAssetUrlRE,
483+
(_, hash) =>
484+
relativePathToPublicFromCSS + publicAssetUrlMap.get(hash)!
485+
)
486+
}
487+
return chunkCSS
447488
}
448489

449490
if (config.build.cssCodeSplit) {
@@ -456,23 +497,25 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
456497
opts.format === 'cjs' ||
457498
opts.format === 'system'
458499
) {
459-
chunkCSS = await processChunkCSS(chunkCSS, {
460-
inlined: false,
461-
minify: true
462-
})
500+
const cssAssetName = chunk.name + '.css'
501+
502+
chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName)
503+
chunkCSS = await finalizeCss(chunkCSS, true, config)
504+
463505
// emit corresponding css file
464506
const fileHandle = this.emitFile({
465-
name: chunk.name + '.css',
507+
name: cssAssetName,
466508
type: 'asset',
467509
source: chunkCSS
468510
})
469511
chunk.viteMetadata.importedCss.add(this.getFileName(fileHandle))
470512
} else if (!config.build.ssr) {
471-
// legacy build, inline css
472-
chunkCSS = await processChunkCSS(chunkCSS, {
473-
inlined: true,
474-
minify: true
475-
})
513+
// legacy build and inline css
514+
515+
// __VITE_ASSET__ and __VITE_PUBLIC_ASSET__ urls are processed by
516+
// the vite:asset plugin, don't call resolveAssetUrlsInCss here
517+
chunkCSS = await finalizeCss(chunkCSS, true, config)
518+
476519
const style = `__vite_style__`
477520
const injectCode =
478521
`var ${style} = document.createElement('style');` +
@@ -481,6 +524,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
481524
if (config.build.sourcemap) {
482525
const s = new MagicString(code)
483526
s.prepend(injectCode)
527+
// resolve public URL from CSS paths, we need to use absolute paths
484528
return {
485529
code: s.toString(),
486530
map: s.generateMap({ hires: true })
@@ -490,11 +534,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
490534
}
491535
}
492536
} else {
493-
// non-split extracted CSS will be minified together
494-
chunkCSS = await processChunkCSS(chunkCSS, {
495-
inlined: false,
496-
minify: false
497-
})
537+
chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssBundleName)
538+
// finalizeCss is called for the aggregated chunk in generateBundle
539+
498540
outputToExtractedCSSMap.set(
499541
opts,
500542
(outputToExtractedCSSMap.get(opts) || '') + chunkCSS
@@ -558,7 +600,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
558600
hasEmitted = true
559601
extractedCss = await finalizeCss(extractedCss, true, config)
560602
this.emitFile({
561-
name: 'style.css',
603+
name: cssBundleName,
562604
type: 'asset',
563605
source: extractedCss
564606
})

0 commit comments

Comments
 (0)