From cb42862a1a6f58b084136592c0ec0e3023ecf9fa Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 8 Apr 2022 00:16:15 +0200 Subject: [PATCH 01/33] feat: relative base --- packages/playground/assets/package.json | 5 +- .../playground/assets/vite.config-portable.js | 14 +++++ packages/playground/css/vite.config.js | 1 + packages/playground/html/vite.config.js | 1 + packages/playground/legacy/vite.config.js | 1 + .../preload/__tests__/preload.spec.ts | 4 +- packages/plugin-legacy/index.js | 17 +++--- packages/vite/src/node/build.ts | 29 ++++++++- packages/vite/src/node/plugins/asset.ts | 31 +++++++--- .../src/node/plugins/assetImportMetaUrl.ts | 4 +- packages/vite/src/node/plugins/css.ts | 27 ++++---- packages/vite/src/node/plugins/html.ts | 61 +++++++++++++------ .../src/node/plugins/importAnalysisBuild.ts | 32 +++++++--- packages/vite/src/node/plugins/wasm.ts | 2 +- packages/vite/src/node/plugins/worker.ts | 10 +-- .../src/node/plugins/workerImportMetaUrl.ts | 2 +- .../src/node/server/middlewares/indexHtml.ts | 4 +- scripts/jestPerTestSetup.ts | 12 ++-- 18 files changed, 181 insertions(+), 76 deletions(-) create mode 100644 packages/playground/assets/vite.config-portable.js diff --git a/packages/playground/assets/package.json b/packages/playground/assets/package.json index 33f64cef771525..8fc79569f55b41 100644 --- a/packages/playground/assets/package.json +++ b/packages/playground/assets/package.json @@ -6,6 +6,9 @@ "dev": "vite", "build": "vite build", "debug": "node --inspect-brk ../../vite/bin/vite", - "preview": "vite preview" + "preview": "vite preview", + "dev:portable": "vite --config ./vite.config-portable.js dev", + "build:portable": "vite --config ./vite.config-portable.js build", + "preview:portable": "vite --config ./vite.config-portable.js preview" } } diff --git a/packages/playground/assets/vite.config-portable.js b/packages/playground/assets/vite.config-portable.js new file mode 100644 index 00000000000000..dbfa9f6dee5ee0 --- /dev/null +++ b/packages/playground/assets/vite.config-portable.js @@ -0,0 +1,14 @@ +/** + * @type {import('vite').UserConfig} + */ + +const baseConfig = require('./vite.config.js') +module.exports = { + ...baseConfig, + base: './', // relative base to make dist portable + build: { + ...baseConfig.build, + outDir: 'dist', + watch: false + } +} diff --git a/packages/playground/css/vite.config.js b/packages/playground/css/vite.config.js index 639a1302debb88..0c2432ec40cf38 100644 --- a/packages/playground/css/vite.config.js +++ b/packages/playground/css/vite.config.js @@ -4,6 +4,7 @@ const path = require('path') * @type {import('vite').UserConfig} */ module.exports = { + base: './', build: { cssTarget: 'chrome61' }, diff --git a/packages/playground/html/vite.config.js b/packages/playground/html/vite.config.js index 1703e02cc05366..8e70727eab6f48 100644 --- a/packages/playground/html/vite.config.js +++ b/packages/playground/html/vite.config.js @@ -4,6 +4,7 @@ const { resolve } = require('path') * @type {import('vite').UserConfig} */ module.exports = { + base: './', build: { rollupOptions: { input: { diff --git a/packages/playground/legacy/vite.config.js b/packages/playground/legacy/vite.config.js index 90d3be7f7c56a0..e1d68245604346 100644 --- a/packages/playground/legacy/vite.config.js +++ b/packages/playground/legacy/vite.config.js @@ -3,6 +3,7 @@ const path = require('path') const legacy = require('@vitejs/plugin-legacy').default module.exports = { + base: './', plugins: [ legacy({ targets: 'IE 11' diff --git a/packages/playground/preload/__tests__/preload.spec.ts b/packages/playground/preload/__tests__/preload.spec.ts index 27a64930487797..8e5fedb2ee7fff 100644 --- a/packages/playground/preload/__tests__/preload.spec.ts +++ b/packages/playground/preload/__tests__/preload.spec.ts @@ -16,10 +16,10 @@ if (isBuild) { await page.goto(viteTestUrl + '/#/hello') const html = await page.content() expect(html).toMatch( - /link rel="modulepreload".*?href="\/assets\/Hello\.\w{8}\.js"/ + /link rel="modulepreload".*?href=".*?\/assets\/Hello\.\w{8}\.js"/ ) expect(html).toMatch( - /link rel="stylesheet".*?href="\/assets\/Hello\.\w{8}\.css"/ + /link rel="stylesheet".*?href=".*?\/assets\/Hello\.\w{8}\.css"/ ) }) } diff --git a/packages/plugin-legacy/index.js b/packages/plugin-legacy/index.js index 41f7157ebfc533..85098cd7cb89d6 100644 --- a/packages/plugin-legacy/index.js +++ b/packages/plugin-legacy/index.js @@ -19,10 +19,11 @@ 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 detectDynamicImportVarInitCode = `var ${detectDynamicImportVarName}=false;` -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 detectModernBrowserVarInitCode = `var ${detectModernBrowserVarName}=false;` +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,')};` @@ -440,13 +441,13 @@ function viteLegacyPlugin(options = {}) { tags.push({ tag: 'script', attrs: { type: 'module' }, - children: detectDynamicImportVarInitCode, + children: detectModernBrowserVarInitCode, injectTo: 'head' }) tags.push({ tag: 'script', attrs: { type: 'module' }, - children: detectDynamicImportCode, + children: detectModernBrowserCode, injectTo: 'head' }) tags.push({ @@ -714,7 +715,7 @@ viteLegacyPlugin.default = viteLegacyPlugin viteLegacyPlugin.cspHashes = [ createHash('sha256').update(safari10NoModuleFix).digest('base64'), createHash('sha256').update(systemJSInlineCode).digest('base64'), - createHash('sha256').update(detectDynamicImportVarInitCode).digest('base64'), - createHash('sha256').update(detectDynamicImportCode).digest('base64'), + createHash('sha256').update(detectModernBrowserVarInitCode).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 3541cc377dc411..d9ed080ccc5dbb 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -273,11 +273,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 coallesing ] } else if (resolved.target === 'esnext' && resolved.minify === 'terser') { // esnext + terser: limit to es2019 so it can be minified by terser @@ -778,3 +778,28 @@ function injectSsrFlag>( ): T & { ssr: boolean } { return { ...(options ?? {}), ssr: true } as T & { ssr: boolean } } + +export function isRelativeBase(base: string): boolean { + return base === '' || base.startsWith('.') +} + +export function assetFilenameWithBase(filename: string, base: string): string { + if (isRelativeBase(base)) { + // relative base - asset file will be in the same dir + return `./${path.posix.basename(filename)}` + } else { + return base + filename + } +} + +export function publicURLfromAsset( + url: string, + config: ResolvedConfig +): string { + if (isRelativeBase(config.base)) { + // Assume flat assets structure + return path.posix.relative(config.build.assetsDir, '') + url + } else { + return config.base + url.slice(1) + } +} diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 633438cf3cb0d4..3321a1c9985348 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -4,12 +4,16 @@ import fs, { promises as fsp } from 'fs' import * as mrmime from 'mrmime' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { cleanUrl } from '../utils' +import { cleanUrl, normalizePath } from '../utils' +import { + assetFilenameWithBase, + isRelativeBase, + publicURLfromAsset +} from '../build' import { FS_PREFIX } from '../constants' import type { OutputOptions, PluginContext } from 'rollup' import MagicString from 'magic-string' import { createHash } from 'crypto' -import { normalizePath } from '../utils' export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g @@ -77,7 +81,8 @@ export function assetPlugin(config: ResolvedConfig): Plugin { id = id.replace(urlRE, '$1').replace(/[\?&]$/, '') const url = await fileToUrl(id, config, this) - return `export default ${JSON.stringify(url)}` + // Return a template string so we can use interpolation when replacing __VITE_ASSET__ + return `export default \`${url}\`` }, renderChunk(code, chunk) { @@ -85,12 +90,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin { let s: MagicString | undefined // 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"; + // var inlined = `.inlined{color:green;background:url(__VITE_ASSET__5aa0ddc0__)}\n`; - // In both cases, the wrapping should already be fine + // In both cases, the wrapping allows us to use interpolation while ((match = assetUrlRE.exec(code))) { s = s || (s = new MagicString(code)) @@ -99,8 +104,14 @@ export function assetPlugin(config: ResolvedConfig): Plugin { // fallback to this.getFileName for that. const file = getAssetFilename(hash, config) || this.getFileName(hash) chunk.viteMetadata.importedAssets.add(cleanUrl(file)) - const outputFilepath = config.base + file + postfix - s.overwrite(match.index, match.index + full.length, outputFilepath, { + const outputFilepath = assetFilenameWithBase( + file + postfix, + config.base + ) + const replacement = isRelativeBase(config.base) + ? `\${new URL(${JSON.stringify(outputFilepath)},import.meta.url)}` + : outputFilepath + s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true }) } @@ -271,7 +282,7 @@ async function fileToBuiltUrl( skipPublicCheck = false ): Promise { if (!skipPublicCheck && checkPublicFile(id, config)) { - return config.base + id.slice(1) + return publicURLfromAsset(id, config) } const cache = assetCache.get(config)! @@ -348,7 +359,7 @@ export async function urlToBuiltUrl( pluginContext: PluginContext ): Promise { if (checkPublicFile(url, config)) { - return config.base + url.slice(1) + return publicURLfromAsset(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 a3f8e441b0f933..3bc4f6a47a56d1 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -9,6 +9,7 @@ import { stringsRE, blankReplacer } from '../utils' +import { preloadHelperId } from './importAnalysisBuild' /** * Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL @@ -26,6 +27,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`) ) { @@ -82,7 +84,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { s.overwrite( index, index + exp.length, - `new URL(${JSON.stringify(builtUrl)}, self.location)`, + `new URL(\`${builtUrl}\`, self.location)`, { contentOnly: true } ) } diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index dbe9e7d1dfa00f..2ee686234fbf79 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -48,6 +48,7 @@ import { transform, formatMessages } from 'esbuild' import { addToHTMLProxyTransformResult } from './html' import { injectSourcesContent, getCodeWithSourcemap } from '../server/sourcemap' import type { RawSourceMap } from '@ampproject/remapping' +import { assetFilenameWithBase, publicURLfromAsset } from '../build' // const debug = createDebugger('vite:css') @@ -180,7 +181,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { const urlReplacer: CssUrlReplacer = async (url, importer) => { if (checkPublicFile(url, config)) { - return config.base + url.slice(1) + return publicURLfromAsset(url, config) } const resolved = await resolveUrl(url, importer) if (resolved) { @@ -309,10 +310,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } else { // server only if (options?.ssr) { - return modulesCode || `export default ${JSON.stringify(css)}` + return modulesCode || `export default \`${css}\`` } if (inlined) { - return `export default ${JSON.stringify(css)}` + return `export default \`${css}\`` } let cssContent = css @@ -327,7 +328,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { path.posix.join(config.base, CLIENT_PUBLIC_PATH) )}`, `const __vite__id = ${JSON.stringify(id)}`, - `const __vite__css = ${JSON.stringify(cssContent)}`, + `const __vite__css = \`${cssContent}\``, `__vite__updateStyle(__vite__id, __vite__css)`, // css modules exports change on edit so it can't self accept `${ @@ -361,11 +362,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { let code: string if (usedRE.test(id)) { if (inlined) { - code = `export default ${JSON.stringify( - await minifyCSS(css, config) - )}` + code = `export default \`${await minifyCSS(css, config)}\`` } else { - code = modulesCode || `export default ${JSON.stringify(css)}` + code = modulesCode || `export default \`${css}\`` } } else { code = `export default ''` @@ -414,18 +413,14 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } ) => { // replace asset url references with resolved url. - const isRelativeBase = config.base === '' || config.base.startsWith('.') css = css.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 + if (inlined) { + // inlined (injected as style tag into index.html) use the base as-is return config.base + filename - } else { - // relative base + extracted CSS - asset file will be in the same dir - return `./${path.posix.basename(filename)}` } + return assetFilenameWithBase(filename, config.base) }) // only external @imports should exist at this point - and they need to // be hoisted to the top of the CSS chunk per spec (#1845) @@ -464,7 +459,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const style = `__vite_style__` const injectCode = `var ${style} = document.createElement('style');` + - `${style}.innerHTML = ${JSON.stringify(chunkCSS)};` + + `${style}.innerHTML = \`${chunkCSS}\`;` + `document.head.appendChild(${style});` if (config.build.sourcemap) { const s = new MagicString(code) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 25ad91582140c3..8186f66dcb5024 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -17,6 +17,7 @@ import { processSrcSet, slash } from '../utils' +import { isRelativeBase } from '../build' import type { ResolvedConfig } from '../config' import MagicString from 'magic-string' import { @@ -232,7 +233,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, @@ -267,7 +271,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 } ) } @@ -339,7 +343,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 } ) } @@ -448,7 +452,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 }) } @@ -490,6 +494,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const toScriptTag = ( chunk: OutputChunk, + publicBase: string, isAsync: boolean ): HtmlTagDescriptor => ({ tag: 'script', @@ -497,20 +502,24 @@ 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', - href: toPublicPath(chunk.fileName, config) + href: toPublicPath(chunk.fileName, publicBase) } }) const getCssTagsForChunk = ( chunk: OutputChunk, + publicBase: string, seen: Set = new Set() ): HtmlTagDescriptor[] => { const tags: HtmlTagDescriptor[] = [] @@ -519,7 +528,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)) } }) } @@ -531,7 +540,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { tag: 'link', attrs: { rel: 'stylesheet', - href: toPublicPath(file, config) + href: toPublicPath(file, publicBase) } }) } @@ -541,6 +550,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 @@ -568,10 +580,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // when inlined, discard entry chunk and inject