From 09648c220a67852c38da0ba742501a15837e16c2 Mon Sep 17 00:00:00 2001 From: patak Date: Wed, 18 May 2022 07:07:41 +0200 Subject: [PATCH] feat!: relative base (#7644) --- packages/plugin-legacy/src/index.ts | 10 +- packages/vite/src/node/build.ts | 4 +- packages/vite/src/node/plugins/asset.ts | 63 ++++- .../src/node/plugins/assetImportMetaUrl.ts | 2 + packages/vite/src/node/plugins/css.ts | 124 ++++++--- packages/vite/src/node/plugins/html.ts | 61 +++-- .../src/node/plugins/importAnalysisBuild.ts | 40 ++- packages/vite/src/node/plugins/worker.ts | 82 +++++- .../src/node/plugins/workerImportMetaUrl.ts | 6 +- .../src/node/server/middlewares/indexHtml.ts | 4 +- packages/vite/src/node/utils.ts | 4 + .../relative-base-assets.spec.ts | 235 ++++++++++++++++++ .../__tests__/relative-base/vite.config.js | 1 + ...-\346\270\254\350\251\246-white space.png" | Bin 3395 -> 4986 bytes playground/assets/package.json | 5 +- .../assets/vite.config-relative-base.js | 23 ++ playground/assets/vite.config.js | 1 + playground/css/vite.config.js | 1 + playground/html/vite.config.js | 1 + playground/legacy/vite.config.js | 1 + playground/preload/__tests__/preload.spec.ts | 4 +- playground/test-utils.ts | 8 +- playground/vitestSetup.ts | 8 +- .../relative-base-worker.spec.ts | 126 ++++++++++ .../__tests__/relative-base/vite.config.js | 1 + playground/worker/classic-shared-worker.js | 5 +- playground/worker/classic-worker.js | 5 +- playground/worker/package.json | 3 + .../worker/vite.config-relative-base.js | 42 ++++ 29 files changed, 768 insertions(+), 102 deletions(-) create mode 100644 playground/assets/__tests__/relative-base/relative-base-assets.spec.ts create mode 100644 playground/assets/__tests__/relative-base/vite.config.js create mode 100644 playground/assets/vite.config-relative-base.js create mode 100644 playground/worker/__tests__/relative-base/relative-base-worker.spec.ts create mode 100644 playground/worker/__tests__/relative-base/vite.config.js create mode 100644 playground/worker/vite.config-relative-base.js diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index d107a2d66ac619..8bd3c6811f6a9b 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -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,')};` @@ -438,7 +438,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { tags.push({ tag: 'script', attrs: { type: 'module' }, - children: detectDynamicImportCode, + children: detectModernBrowserCode, injectTo: 'head' }) tags.push({ @@ -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') ] diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index ea40914fdb44c8..550af9d13af389 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -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 diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 650a6630fc6b8f..4c1bcfea0fd50b 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -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 @@ -29,6 +29,7 @@ const emittedHashMap = new WeakMap>() 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 @@ -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"; @@ -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(), @@ -258,6 +282,33 @@ export function assetFileNamesToFileName( return fileName } +export const publicAssetUrlCache = new WeakMap< + ResolvedConfig, + // hash -> url + Map +>() + +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() + 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 @@ -269,7 +320,7 @@ async function fileToBuiltUrl( skipPublicCheck = false ): Promise { if (!skipPublicCheck && checkPublicFile(id, config)) { - return config.base + id.slice(1) + return publicFileToBuiltUrl(id, config) } const cache = assetCache.get(config)! @@ -342,7 +393,7 @@ export async function urlToBuiltUrl( pluginContext: PluginContext ): Promise { if (checkPublicFile(url, config)) { - return config.base + url.slice(1) + return publicFileToBuiltUrl(url, config) } const file = url.startsWith('/') ? path.join(config.root, url) diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index 2bff4f5e7b682e..636234736cc32c 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -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 @@ -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`) ) { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index d5a48658493ce7..eeafc46feafee0 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -38,6 +38,7 @@ import { isDataUrl, isExternalUrl, isObject, + isRelativeBase, normalizePath, parseRequest, processSrcSet @@ -48,7 +49,10 @@ import { assetUrlRE, checkPublicFile, fileToUrl, - getAssetFilename + getAssetFilename, + publicAssetUrlCache, + publicAssetUrlRE, + publicFileToBuiltUrl } from './asset' // const debug = createDebugger('vite:css') @@ -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', @@ -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) { @@ -283,6 +293,30 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { let outputToExtractedCSSMap: Map 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', @@ -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) { @@ -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');` + @@ -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 }) @@ -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 @@ -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 }) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 8e48a023d021cc..9d06d6e168f2e1 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -24,6 +24,7 @@ import { getHash, isDataUrl, isExternalUrl, + isRelativeBase, normalizePath, processSrcSet, slash @@ -236,7 +237,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { async transform(html, id) { if (id.endsWith('.html')) { - const publicPath = `/${slash(path.relative(config.root, id))}` + const relativeUrlPath = slash(path.relative(config.root, id)) + const publicPath = `/${relativeUrlPath}` + const publicBase = getPublicBase(relativeUrlPath, config) + // pre-transform html = await applyHtmlTransforms(html, preHooks, { path: publicPath, @@ -272,7 +276,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.overwrite( src!.value!.loc.start.offset, src!.value!.loc.end.offset, - `"${config.base + url.slice(1)}"`, + `"${normalizePublicPath(url, publicBase)}"`, { contentOnly: true } ) } @@ -354,7 +358,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.overwrite( p.value.loc.start.offset, p.value.loc.end.offset, - `"${config.base + url.slice(1)}"`, + `"${normalizePublicPath(url, publicBase)}"`, { contentOnly: true } ) } @@ -470,7 +474,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { { contentOnly: true } ) } else if (checkPublicFile(url, config)) { - s.overwrite(start, end, config.base + url.slice(1), { + s.overwrite(start, end, normalizePublicPath(url, publicBase), { contentOnly: true }) } @@ -531,6 +535,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const toScriptTag = ( chunk: OutputChunk, + publicBase: string, isAsync: boolean ): HtmlTagDescriptor => ({ tag: 'script', @@ -538,21 +543,25 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { ...(isAsync ? { async: true } : {}), type: 'module', crossorigin: true, - src: toPublicPath(chunk.fileName, config) + src: toPublicPath(chunk.fileName, publicBase) } }) - const toPreloadTag = (chunk: OutputChunk): HtmlTagDescriptor => ({ + const toPreloadTag = ( + chunk: OutputChunk, + publicBase: string + ): HtmlTagDescriptor => ({ tag: 'link', attrs: { rel: 'modulepreload', crossorigin: true, - href: toPublicPath(chunk.fileName, config) + href: toPublicPath(chunk.fileName, publicBase) } }) const getCssTagsForChunk = ( chunk: OutputChunk, + publicBase: string, seen: Set = new Set() ): HtmlTagDescriptor[] => { const tags: HtmlTagDescriptor[] = [] @@ -561,7 +570,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { chunk.imports.forEach((file) => { const importee = bundle[file] if (importee?.type === 'chunk') { - tags.push(...getCssTagsForChunk(importee, seen)) + tags.push(...getCssTagsForChunk(importee, publicBase, seen)) } }) } @@ -573,7 +582,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { tag: 'link', attrs: { rel: 'stylesheet', - href: toPublicPath(file, config) + href: toPublicPath(file, publicBase) } }) } @@ -583,6 +592,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { } for (const [id, html] of processedHtml) { + const relativeUrlPath = path.posix.relative(config.root, id) + const publicBase = getPublicBase(relativeUrlPath, config) + const isAsync = isAsyncScriptMap.get(config)!.get(id)! let result = html @@ -610,10 +622,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // when inlined, discard entry chunk and inject