Skip to content

Commit

Permalink
feat!: relative base (#7644)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev committed May 18, 2022
1 parent 04046ea commit 09648c2
Show file tree
Hide file tree
Showing 29 changed files with 768 additions and 102 deletions.
10 changes: 5 additions & 5 deletions packages/plugin-legacy/src/index.ts
Expand Up @@ -37,9 +37,9 @@ const legacyPolyfillId = 'vite-legacy-polyfill'
const legacyEntryId = 'vite-legacy-entry'
const systemJSInlineCode = `System.import(document.getElementById('${legacyEntryId}').getAttribute('data-src'))`

const detectDynamicImportVarName = '__vite_is_dynamic_import_support'
const detectDynamicImportCode = `try{import("_").catch(()=>1);}catch(e){}window.${detectDynamicImportVarName}=true;`
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)}();`
const detectModernBrowserVarName = '__vite_is_modern_browser'
const detectModernBrowserCode = `try{import(new URL(import.meta.url).href).catch(()=>1);}catch(e){}window.${detectModernBrowserVarName}=true;`
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)}();`

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

Expand Down Expand Up @@ -438,7 +438,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
tags.push({
tag: 'script',
attrs: { type: 'module' },
children: detectDynamicImportCode,
children: detectModernBrowserCode,
injectTo: 'head'
})
tags.push({
Expand Down Expand Up @@ -686,7 +686,7 @@ function wrapIIFEBabelPlugin(): BabelPlugin {
export const cspHashes = [
createHash('sha256').update(safari10NoModuleFix).digest('base64'),
createHash('sha256').update(systemJSInlineCode).digest('base64'),
createHash('sha256').update(detectDynamicImportCode).digest('base64'),
createHash('sha256').update(detectModernBrowserCode).digest('base64'),
createHash('sha256').update(dynamicFallbackInlineCode).digest('base64')
]

Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/build.ts
Expand Up @@ -245,11 +245,11 @@ export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions {
// Support browserslist
// "defaults and supports es6-module and supports es6-module-dynamic-import",
resolved.target = [
'es2019',
'es2020', // support import.meta.url
'edge88',
'firefox78',
'chrome87',
'safari13.1'
'safari13' // transpile nullish coalescing
]
} else if (resolved.target === 'esnext' && resolved.minify === 'terser') {
// esnext + terser: limit to es2019 so it can be minified by terser
Expand Down
63 changes: 57 additions & 6 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -6,7 +6,7 @@ import type { OutputOptions, PluginContext } from 'rollup'
import MagicString from 'magic-string'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { cleanUrl, getHash, normalizePath } from '../utils'
import { cleanUrl, getHash, isRelativeBase, normalizePath } from '../utils'
import { FS_PREFIX } from '../constants'

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

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

const absoluteUrlPathInterpolation = (filename: string) =>
`"+new URL(${JSON.stringify(
path.posix.relative(path.dirname(chunk.fileName), filename)
)},import.meta.url).href+"`

// Urls added with JS using e.g.
// imgElement.src = "my/file.png" are using quotes
// imgElement.src = "__VITE_ASSET__5aa0ddc0__" are using quotes

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

// Replace __VITE_PUBLIC_ASSET__5aa0ddc0__ with absolute paths

if (relativeBase) {
const publicAssetUrlMap = publicAssetUrlCache.get(config)!
while ((match = publicAssetUrlRE.exec(code))) {
s = s || (s = new MagicString(code))
const [full, hash] = match
const publicUrl = publicAssetUrlMap.get(hash)!
const replacement = absoluteUrlPathInterpolation(publicUrl.slice(1))
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
}

if (s) {
return {
code: s.toString(),
Expand Down Expand Up @@ -258,6 +282,33 @@ export function assetFileNamesToFileName(
return fileName
}

export const publicAssetUrlCache = new WeakMap<
ResolvedConfig,
// hash -> url
Map<string, string>
>()

export const publicAssetUrlRE = /__VITE_PUBLIC_ASSET__([a-z\d]{8})__/g

export function publicFileToBuiltUrl(
url: string,
config: ResolvedConfig
): string {
if (!isRelativeBase(config.base)) {
return config.base + url.slice(1)
}
const hash = getHash(url)
let cache = publicAssetUrlCache.get(config)
if (!cache) {
cache = new Map<string, string>()
publicAssetUrlCache.set(config, cache)
}
if (!cache.get(hash)) {
cache.set(hash, url)
}
return `__VITE_PUBLIC_ASSET__${hash}__`
}

/**
* Register an asset to be emitted as part of the bundle (if necessary)
* and returns the resolved public URL
Expand All @@ -269,7 +320,7 @@ async function fileToBuiltUrl(
skipPublicCheck = false
): Promise<string> {
if (!skipPublicCheck && checkPublicFile(id, config)) {
return config.base + id.slice(1)
return publicFileToBuiltUrl(id, config)
}

const cache = assetCache.get(config)!
Expand Down Expand Up @@ -342,7 +393,7 @@ export async function urlToBuiltUrl(
pluginContext: PluginContext
): Promise<string> {
if (checkPublicFile(url, config)) {
return config.base + url.slice(1)
return publicFileToBuiltUrl(url, config)
}
const file = url.startsWith('/')
? path.join(config.root, url)
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/node/plugins/assetImportMetaUrl.ts
Expand Up @@ -4,6 +4,7 @@ import { stripLiteral } from 'strip-literal'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { fileToUrl } from './asset'
import { preloadHelperId } from './importAnalysisBuild'

/**
* Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL
Expand All @@ -21,6 +22,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
async transform(code, id, options) {
if (
!options?.ssr &&
id !== preloadHelperId &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
Expand Down
124 changes: 83 additions & 41 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -38,6 +38,7 @@ import {
isDataUrl,
isExternalUrl,
isObject,
isRelativeBase,
normalizePath,
parseRequest,
processSrcSet
Expand All @@ -48,7 +49,10 @@ import {
assetUrlRE,
checkPublicFile,
fileToUrl,
getAssetFilename
getAssetFilename,
publicAssetUrlCache,
publicAssetUrlRE,
publicFileToBuiltUrl
} from './asset'

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

const cssBundleName = 'style.css'

const enum PreprocessLang {
less = 'less',
sass = 'sass',
Expand Down Expand Up @@ -183,7 +189,11 @@ export function cssPlugin(config: ResolvedConfig): Plugin {

const urlReplacer: CssUrlReplacer = async (url, importer) => {
if (checkPublicFile(url, config)) {
return config.base + url.slice(1)
if (isRelativeBase(config.base)) {
return publicFileToBuiltUrl(url, config)
} else {
return config.base + url.slice(1)
}
}
const resolved = await resolveUrl(url, importer)
if (resolved) {
Expand Down Expand Up @@ -283,6 +293,30 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
let outputToExtractedCSSMap: Map<NormalizedOutputOptions, string>
let hasEmitted = false

const relativeBase = isRelativeBase(config.base)

const rollupOptionsOutput = config.build.rollupOptions.output
const assetFileNames = (
Array.isArray(rollupOptionsOutput)
? rollupOptionsOutput[0]
: rollupOptionsOutput
)?.assetFileNames
const getCssAssetDirname = (cssAssetName: string) => {
if (!assetFileNames) {
return config.build.assetsDir
} else if (typeof assetFileNames === 'string') {
return path.dirname(assetFileNames)
} else {
return path.dirname(
assetFileNames({
name: cssAssetName,
type: 'asset',
source: '/* vite internal call, ignore */'
})
)
}
}

return {
name: 'vite:css-post',

Expand Down Expand Up @@ -415,35 +449,42 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
return null
}

// resolve asset URL placeholders to their built file URLs and perform
// minification if necessary
const processChunkCSS = async (
css: string,
{
inlined,
minify
}: {
inlined: boolean
minify: boolean
}
) => {
const publicAssetUrlMap = publicAssetUrlCache.get(config)!

// resolve asset URL placeholders to their built file URLs
function resolveAssetUrlsInCss(chunkCSS: string, cssAssetName: string) {
const cssAssetDirname = relativeBase
? getCssAssetDirname(cssAssetName)
: undefined

// replace asset url references with resolved url.
const isRelativeBase = config.base === '' || config.base.startsWith('.')
css = css.replace(assetUrlRE, (_, fileHash, postfix = '') => {
chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => {
const filename = getAssetFilename(fileHash, config) + postfix
chunk.viteMetadata.importedAssets.add(cleanUrl(filename))
if (!isRelativeBase || inlined) {
// absolute base or relative base but inlined (injected as style tag into
// index.html) use the base as-is
return config.base + filename
if (relativeBase) {
// relative base + extracted CSS
const relativePath = path.posix.relative(cssAssetDirname!, filename)
return relativePath.startsWith('.')
? relativePath
: './' + relativePath
} else {
// relative base + extracted CSS - asset file will be in the same dir
return `./${path.posix.basename(filename)}`
// absolute base
return config.base + filename
}
})
// only external @imports and @charset should exist at this point
css = await finalizeCss(css, minify, config)
return css
// resolve public URL from CSS paths
if (relativeBase) {
const relativePathToPublicFromCSS = path.posix.relative(
cssAssetDirname!,
''
)
chunkCSS = chunkCSS.replace(
publicAssetUrlRE,
(_, hash) =>
relativePathToPublicFromCSS + publicAssetUrlMap.get(hash)!
)
}
return chunkCSS
}

if (config.build.cssCodeSplit) {
Expand All @@ -456,23 +497,25 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
opts.format === 'cjs' ||
opts.format === 'system'
) {
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: false,
minify: true
})
const cssAssetName = chunk.name + '.css'

chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName)
chunkCSS = await finalizeCss(chunkCSS, true, config)

// emit corresponding css file
const fileHandle = this.emitFile({
name: chunk.name + '.css',
name: cssAssetName,
type: 'asset',
source: chunkCSS
})
chunk.viteMetadata.importedCss.add(this.getFileName(fileHandle))
} else if (!config.build.ssr) {
// legacy build, inline css
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: true,
minify: true
})
// legacy build and inline css

// __VITE_ASSET__ and __VITE_PUBLIC_ASSET__ urls are processed by
// the vite:asset plugin, don't call resolveAssetUrlsInCss here
chunkCSS = await finalizeCss(chunkCSS, true, config)

const style = `__vite_style__`
const injectCode =
`var ${style} = document.createElement('style');` +
Expand All @@ -481,6 +524,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
if (config.build.sourcemap) {
const s = new MagicString(code)
s.prepend(injectCode)
// resolve public URL from CSS paths, we need to use absolute paths
return {
code: s.toString(),
map: s.generateMap({ hires: true })
Expand All @@ -490,11 +534,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
}
}
} else {
// non-split extracted CSS will be minified together
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: false,
minify: false
})
chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssBundleName)
// finalizeCss is called for the aggregated chunk in generateBundle

outputToExtractedCSSMap.set(
opts,
(outputToExtractedCSSMap.get(opts) || '') + chunkCSS
Expand Down Expand Up @@ -558,7 +600,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
hasEmitted = true
extractedCss = await finalizeCss(extractedCss, true, config)
this.emitFile({
name: 'style.css',
name: cssBundleName,
type: 'asset',
source: extractedCss
})
Expand Down

0 comments on commit 09648c2

Please sign in to comment.