From 51b476de86dc98870ecb5ef9acda845b86badde9 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 Jun 2022 14:21:23 +0200 Subject: [PATCH 01/10] feat: experimental.renderBuiltAssetUrl (revised advanced build base options) --- docs/guide/build.md | 49 ++--- packages/plugin-legacy/src/index.ts | 72 +++++--- packages/vite/src/node/build.ts | 172 ++++++++---------- packages/vite/src/node/config.ts | 57 ++---- packages/vite/src/node/index.ts | 6 +- packages/vite/src/node/plugins/asset.ts | 46 +++-- packages/vite/src/node/plugins/css.ts | 47 +++-- packages/vite/src/node/plugins/html.ts | 111 ++++++----- .../src/node/plugins/importAnalysisBuild.ts | 9 +- packages/vite/src/node/plugins/worker.ts | 56 +++--- playground/assets/package.json | 6 +- playground/assets/vite.config-runtime-base.js | 30 +-- 12 files changed, 318 insertions(+), 343 deletions(-) diff --git a/docs/guide/build.md b/docs/guide/build.md index abf852479b6321..9813bbbd7011a6 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -197,45 +197,34 @@ A user may choose to deploy in three different paths: - The generated hashed assets (JS, CSS, and other file types like images) - The copied [public files](assets.md#the-public-directory) -A single static [base](#public-base-path) isn't enough in these scenarios. Vite provides experimental support for advanced base options during build, using `experimental.buildAdvancedBaseOptions`. +A single static [base](#public-base-path) isn't enough in these scenarios. Vite provides experimental support for advanced base options during build, using `experimental.renderBuiltAssetUrl`. ```js - experimental: { - buildAdvancedBaseOptions: { - // Same as base: './' - // type: boolean, default: false - relative: true - // Static base - // type: string, default: undefined - url: 'https://cdn.domain.com/' - // Dynamic base to be used for paths inside JS - // type: (url: string) => string, default: undefined - runtime: (url: string) => `window.__toCdnUrl(${url})` - }, +experimental: { + renderBuiltAssetUrl: (filename: string, importer: string) => { + if (path.extname(importer) === '.js') { + return { runtime: `window.__toCdnUrl(${JSON.stringify(filename)})` } + } else { + return { relative: true } + } } +} ``` -When `runtime` is defined, it will be used for hashed assets and public files paths inside JS assets. Inside CSS and HTML generated files, paths will use `url` if defined or fallback to `config.base`. - -If `relative` is true and `url` is defined, relative paths will be prefered for assets inside the same group (for example a hashed image referenced from a JS file). And `url` will be used for the paths in HTML entries and for paths between different groups (a public file referenced from a CSS file). - -If the hashed assets and public files aren't deployed together, options for each group can be defined independently: +If the hashed assets and public files aren't deployed together, options for each group can be defined independently using asset `type` included in the third `context` param given to the function. ```js experimental: { - buildAdvancedBaseOptions: { - assets: { - relative: true - url: 'https://cdn.domain.com/assets', - runtime: (url: string) => `window.__assetsPath(${url})` - }, - public: { - relative: false - url: 'https://www.domain.com/', - runtime: (url: string) => `window.__publicPath + ${url}` + renderBuiltAssetUrl(filename: string, importer: string, { type: 'public' | 'asset' }) { + if (type === 'public') { + return 'https://www.domain.com/' + filename + } + else if (path.extname(importer) === '.js') { + return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` } + } + else { + return 'https://cdn.domain.com/assets/' + filename } } } ``` - -Any option that isn't defined in the `public` or `assets` entry will be inherited from the main `buildAdvancedBaseOptions` config. diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index 0f0fa668c8a502..05c49ce2dc773a 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url' import { build, normalizePath } from 'vite' import MagicString from 'magic-string' import type { - BuildAdvancedBaseOptions, BuildOptions, HtmlTagDescriptor, Plugin, @@ -32,38 +31,65 @@ async function loadBabel() { return babel } -function getBaseInHTML( - urlRelativePath: string, - baseOptions: BuildAdvancedBaseOptions, - config: ResolvedConfig -) { +// Duplicated from build.ts in Vite Core, at least while the feature is experimental +// We should later expose this helper for other plugins to use +export function toOutputFilePathInHtml( + filename: string, + type: 'asset' | 'public', + importer: string, + config: ResolvedConfig, + toRelative: (filename: string, importer: string) => string +): string { + const { renderBuiltAssetUrl } = config.experimental + let relative = config.base === '' || config.base === './' + if (renderBuiltAssetUrl) { + const result = renderBuiltAssetUrl(filename, importer, { + type, + ssr: !!config.build.ssr + }) + if (typeof result === 'object') { + if (result.runtime) { + throw new Error( + `{ runtime: ${ + result.runtime + } } is not supported for assets in ${path.extname( + importer + )} files: ${filename}` + ) + } + if (typeof result.relative === 'boolean') { + relative = true + } + } else if (result) { + return result + } + } + if (relative && !config.build.ssr) { + return toRelative(filename, importer) + } else { + return config.base + filename + } +} +function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) { // Prefer explicit URL if defined for linking to assets and public files from HTML, // even when base relative is specified - return ( - baseOptions.url ?? - (baseOptions.relative - ? path.posix.join( - path.posix.relative(urlRelativePath, '').slice(0, -2), - './' - ) - : config.base) - ) + return config.base === './' || config.base === '' + ? path.posix.join( + path.posix.relative(urlRelativePath, '').slice(0, -2), + './' + ) + : config.base } -function getAssetsBase(urlRelativePath: string, config: ResolvedConfig) { - return getBaseInHTML( - urlRelativePath, - config.experimental.buildAdvancedBaseOptions.assets, - config - ) -} function toAssetPathFromHtml( filename: string, htmlPath: string, config: ResolvedConfig ): string { const relativeUrlPath = normalizePath(path.relative(config.root, htmlPath)) - return getAssetsBase(relativeUrlPath, config) + filename + const toRelative = (filename: string, importer: string) => + getBaseInHTML(relativeUrlPath, config) + filename + return toOutputFilePathInHtml(filename, 'asset', htmlPath, config, toRelative) } // https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 52096a476f7391..98e9bb82347fd9 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -22,7 +22,7 @@ import type { RollupCommonJSOptions } from 'types/commonjs' import type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars' import type { TransformOptions } from 'esbuild' import type { InlineConfig, ResolvedConfig } from './config' -import { isDepsOptimizerEnabled, resolveBaseUrl, resolveConfig } from './config' +import { isDepsOptimizerEnabled, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' import { terserPlugin } from './plugins/terser' @@ -831,109 +831,83 @@ function injectSsrFlag>( return { ...(options ?? {}), ssr: true } as T & { ssr: boolean } } -/* - * If defined, these functions will be called for assets and public files - * paths which are generated in JS assets. Examples: - * - * assets: { runtime: (url: string) => `window.__assetsPath(${url})` } - * public: { runtime: (url: string) => `window.__publicPath + ${url}` } - * - * For assets and public files paths in CSS or HTML, the corresponding - * `assets.url` and `public.url` base urls or global base will be used. - * - * When using relative base, the assets.runtime function isn't needed as - * all the asset paths will be computed using import.meta.url - * The public.runtime function is still useful if the public files aren't - * deployed in the same base as the hashed assets - */ +export type RenderBuiltAssetUrl = ( + filename: string, + importer: string, + type: { type: 'asset' | 'public'; ssr: boolean } +) => string | { relative?: boolean; runtime?: string } | undefined -export interface BuildAdvancedBaseOptions { - /** - * Relative base. If true, every generated URL is relative and the dist folder - * can be deployed to any base or subdomain. Use this option when the base - * is unkown at build time - * @default false - */ - relative?: boolean - url?: string - runtime?: (filename: string) => string -} - -export type BuildAdvancedBaseConfig = BuildAdvancedBaseOptions & { - /** - * Base for assets and public files in case they should be different - */ - assets?: string | BuildAdvancedBaseOptions - public?: string | BuildAdvancedBaseOptions -} - -export type ResolvedBuildAdvancedBaseConfig = BuildAdvancedBaseOptions & { - assets: BuildAdvancedBaseOptions - public: BuildAdvancedBaseOptions -} - -/** - * Resolve base. Note that some users use Vite to build for non-web targets like - * electron or expects to deploy - */ -export function resolveBuildAdvancedBaseConfig( - baseConfig: BuildAdvancedBaseConfig | undefined, - resolvedBase: string, - isBuild: boolean, - logger: Logger -): ResolvedBuildAdvancedBaseConfig { - baseConfig ??= {} - - const relativeBaseShortcut = resolvedBase === '' || resolvedBase === './' - - const resolved = { - relative: baseConfig?.relative ?? relativeBaseShortcut, - url: baseConfig?.url - ? resolveBaseUrl( - baseConfig?.url, - isBuild, - logger, - 'experimental.buildAdvancedBaseOptions.url' - ) - : undefined, - runtime: baseConfig?.runtime +export function toOutputFilePathInString( + filename: string, + type: 'asset' | 'public', + importer: string, + config: ResolvedConfig, + toRelative: ( + filename: string, + importer: string + ) => string | { runtime: string } +): string | { runtime: string } { + const { renderBuiltAssetUrl } = config.experimental + let relative = config.base === '' || config.base === './' + if (renderBuiltAssetUrl) { + const result = renderBuiltAssetUrl(filename, importer, { + type, + ssr: !!config.build.ssr + }) + if (typeof result === 'object') { + if (result.runtime) { + return { runtime: result.runtime } + } + if (typeof result.relative === 'boolean') { + relative = true + } + } else if (result) { + return result + } } - - return { - ...resolved, - assets: resolveBuildBaseSpecificOptions( - baseConfig?.assets, - resolved, - isBuild, - logger, - 'assets' - ), - public: resolveBuildBaseSpecificOptions( - baseConfig?.public, - resolved, - isBuild, - logger, - 'public' - ) + if (relative && !config.build.ssr) { + return toRelative(filename, importer) } + return config.base + filename } -function resolveBuildBaseSpecificOptions( - options: BuildAdvancedBaseOptions | string | undefined, - parent: BuildAdvancedBaseOptions, - isBuild: boolean, - logger: Logger, - optionName: string -): BuildAdvancedBaseOptions { - const urlConfigPath = `experimental.buildAdvancedBaseOptions.${optionName}.url` - if (typeof options === 'string') { - options = { url: options } +export function toOutputFilePathWithoutRuntime( + filename: string, + type: 'asset' | 'public', + importer: string, + config: ResolvedConfig, + toRelative: (filename: string, importer: string) => string +): string { + const { renderBuiltAssetUrl } = config.experimental + let relative = config.base === '' || config.base === './' + if (renderBuiltAssetUrl) { + const result = renderBuiltAssetUrl(filename, importer, { + type, + ssr: !!config.build.ssr + }) + if (typeof result === 'object') { + if (result.runtime) { + throw new Error( + `{ runtime: ${ + result.runtime + } } is not supported for assets in ${path.extname( + importer + )} files: ${filename}` + ) + } + if (typeof result.relative === 'boolean') { + relative = true + } + } else if (result) { + return result + } } - return { - relative: options?.relative ?? parent.relative, - url: options?.url - ? resolveBaseUrl(options?.url, isBuild, logger, urlConfigPath) - : parent.url, - runtime: options?.runtime ?? parent.runtime + if (relative && !config.build.ssr) { + return toRelative(filename, importer) + } else { + return config.base + filename } } + +export const toOutputFilePathInCss = toOutputFilePathWithoutRuntime +export const toOutputFilePathInHtml = toOutputFilePathWithoutRuntime diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 06b222736667c8..dfd43aa5c9f010 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -11,12 +11,11 @@ import type { Plugin as ESBuildPlugin } from 'esbuild' import type { RollupOptions } from 'rollup' import type { Plugin } from './plugin' import type { - BuildAdvancedBaseConfig, + RenderBuiltAssetUrl, BuildOptions, - ResolvedBuildAdvancedBaseConfig, ResolvedBuildOptions } from './build' -import { resolveBuildAdvancedBaseConfig, resolveBuildOptions } from './build' +import { resolveBuildOptions } from './build' import type { ResolvedServerOptions, ServerOptions } from './server' import { resolveServerOptions } from './server' import type { PreviewOptions, ResolvedPreviewOptions } from './preview' @@ -53,7 +52,7 @@ import { resolveSSROptions } from './ssr' const debug = createDebugger('vite:config') -export type { BuildAdvancedBaseOptions, BuildAdvancedBaseConfig } from './build' +export type { RenderBuiltAssetUrl } from './build' // NOTE: every export in this file is re-exported from ./index.ts so it will // be part of the public API. @@ -256,11 +255,11 @@ export interface ExperimentalOptions { */ importGlobRestoreExtension?: boolean /** - * Build advanced base options. Allow finegrain contol over assets and public files base + * Allow finegrain contol over assets and public files paths * * @experimental */ - buildAdvancedBaseOptions?: BuildAdvancedBaseConfig + renderBuiltAssetUrl?: RenderBuiltAssetUrl /** * Enables support of HMR partial accept via `import.meta.hot.acceptExports`. * @@ -270,10 +269,6 @@ export interface ExperimentalOptions { hmrPartialAccept?: boolean } -export type ResolvedExperimentalOptions = Required & { - buildAdvancedBaseOptions: ResolvedBuildAdvancedBaseConfig -} - export interface LegacyOptions { /** * Revert vite dev to the v2.9 strategy. Enable esbuild based deps scanner. @@ -345,7 +340,7 @@ export type ResolvedConfig = Readonly< packageCache: PackageCache worker: ResolveWorkerOptions appType: AppType - experimental: ResolvedExperimentalOptions + experimental: ExperimentalOptions } > @@ -487,28 +482,12 @@ export async function resolveConfig( // During dev, we ignore relative base and fallback to '/' // For the SSR build, relative base isn't possible by means - // of import.meta.url. The user will be able to work out a setup - // using experimental.buildAdvancedBaseOptions - const base = - relativeBaseShortcut && (!isBuild || config.build?.ssr) + // of import.meta.url. + const resolvedBase = relativeBaseShortcut + ? !isBuild || config.build?.ssr ? '/' - : config.base ?? '/' - let resolvedBase = relativeBaseShortcut - ? base - : resolveBaseUrl(base, isBuild, logger, 'base') - if ( - config.experimental?.buildAdvancedBaseOptions?.relative && - config.base === undefined - ) { - resolvedBase = './' - } - - const resolvedBuildAdvancedBaseOptions = resolveBuildAdvancedBaseConfig( - config.experimental?.buildAdvancedBaseOptions, - resolvedBase, - isBuild, - logger - ) + : './' + : resolveBaseUrl(config.base, isBuild, logger) ?? '/' const resolvedBuildOptions = resolveBuildOptions( config.build, @@ -637,8 +616,7 @@ export async function resolveConfig( experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false, - ...config.experimental, - buildAdvancedBaseOptions: resolvedBuildAdvancedBaseOptions + ...config.experimental } } @@ -746,14 +724,13 @@ assetFileNames isn't equal for every build.rollupOptions.output. A single patter export function resolveBaseUrl( base: UserConfig['base'] = '/', isBuild: boolean, - logger: Logger, - optionName: string + logger: Logger ): string { if (base.startsWith('.')) { logger.warn( colors.yellow( colors.bold( - `(!) invalid "${optionName}" option: ${base}. The value can only be an absolute ` + + `(!) invalid "base" option: ${base}. The value can only be an absolute ` + `URL, ./, or an empty string.` ) ) @@ -773,7 +750,7 @@ export function resolveBaseUrl( if (!base.startsWith('/')) { logger.warn( colors.yellow( - colors.bold(`(!) "${optionName}" option should start with a slash.`) + colors.bold(`(!) "base" option should start with a slash.`) ) ) base = '/' + base @@ -783,9 +760,7 @@ export function resolveBaseUrl( // ensure ending slash if (!base.endsWith('/')) { logger.warn( - colors.yellow( - colors.bold(`(!) "${optionName}" option should end with a slash.`) - ) + colors.yellow(colors.bold(`(!) "base" option should end with a slash.`)) ) base += '/' } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 4655b4881b7650..e1a9de026ffc1e 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -23,10 +23,8 @@ export type { BuildOptions, LibraryOptions, LibraryFormats, - ResolvedBuildOptions, - BuildAdvancedBaseConfig, - ResolvedBuildAdvancedBaseConfig, - BuildAdvancedBaseOptions + RenderBuiltAssetUrl, + ResolvedBuildOptions } from './build' export type { PreviewOptions, diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index cab050cbdfa652..68ef4a2d8da13a 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -4,7 +4,7 @@ import fs, { promises as fsp } from 'node:fs' import * as mrmime from 'mrmime' import type { OutputOptions, PluginContext, PreRenderedAsset } from 'rollup' import MagicString from 'magic-string' -import type { BuildAdvancedBaseOptions } from '../build' +import { toOutputFilePathInString } from '../build' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { cleanUrl, getHash, normalizePath } from '../utils' @@ -94,20 +94,12 @@ 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+"` - - const toOutputFilePathInString = ( - filename: string, - base: BuildAdvancedBaseOptions - ) => { - return base.runtime - ? `"+${base.runtime(JSON.stringify(filename))}+"` - : base.relative && !config.build.ssr - ? absoluteUrlPathInterpolation(filename) - : JSON.stringify((base.url ?? config.base) + filename).slice(1, -1) + const toRelative = (filename: string, importer: string) => { + return { + runtime: `new URL(${JSON.stringify( + path.posix.relative(path.dirname(importer), filename) + )},import.meta.url).href` + } } // Urls added with JS using e.g. @@ -128,9 +120,16 @@ export function assetPlugin(config: ResolvedConfig): Plugin { const filename = file + postfix const replacement = toOutputFilePathInString( filename, - config.experimental.buildAdvancedBaseOptions.assets + 'asset', + chunk.fileName, + config, + toRelative ) - s.overwrite(match.index, match.index + full.length, replacement, { + const replacementString = + typeof replacement === 'string' + ? JSON.stringify(replacement).slice(1, -1) + : `"+${replacement.runtime}+"` + s.overwrite(match.index, match.index + full.length, replacementString, { contentOnly: true }) } @@ -144,9 +143,16 @@ export function assetPlugin(config: ResolvedConfig): Plugin { const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) const replacement = toOutputFilePathInString( publicUrl, - config.experimental.buildAdvancedBaseOptions.public + 'public', + chunk.fileName, + config, + toRelative ) - s.overwrite(match.index, match.index + full.length, replacement, { + const replacementString = + typeof replacement === 'string' + ? JSON.stringify(replacement).slice(1, -1) + : `"+${replacement.runtime}+"` + s.overwrite(match.index, match.index + full.length, replacementString, { contentOnly: true }) } @@ -341,7 +347,7 @@ export function publicFileToBuiltUrl( config: ResolvedConfig ): string { if (config.command !== 'build') { - // We don't need relative base or buildAdvancedBaseOptions support during dev + // We don't need relative base or renderBuiltAssetUrl support during dev return config.base + url.slice(1) } const hash = getHash(url) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index a362fc124c4150..acaf59789b7725 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -24,6 +24,7 @@ import type { RawSourceMap } from '@ampproject/remapping' import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap' import type { ModuleNode } from '../server/moduleGraph' import type { ResolveFn, ViteDevServer } from '../' +import { toOutputFilePathInCss } from '../build' import { CLIENT_PUBLIC_PATH, SPECIAL_QUERY_RE } from '../constants' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' @@ -458,27 +459,32 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // resolve asset URL placeholders to their built file URLs function resolveAssetUrlsInCss(chunkCSS: string, cssAssetName: string) { const encodedPublicUrls = encodePublicUrlsInCSS(config) - const { assets: assetsBase, public: publicBase } = - config.experimental.buildAdvancedBaseOptions + + const relative = config.base === './' || config.base === '' const cssAssetDirname = - encodedPublicUrls || assetsBase.relative + encodedPublicUrls || relative ? getCssAssetDirname(cssAssetName) : undefined + const toRelative = (filename: string, importer: string) => { + // relative base + extracted CSS + const relativePath = path.posix.relative(cssAssetDirname!, filename) + return relativePath.startsWith('.') + ? relativePath + : './' + relativePath + } + // replace asset url references with resolved url. chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => { const filename = getAssetFilename(fileHash, config) + postfix chunk.viteMetadata.importedAssets.add(cleanUrl(filename)) - if (assetsBase.relative && !config.build.ssr) { - // relative base + extracted CSS - const relativePath = path.posix.relative(cssAssetDirname!, filename) - return relativePath.startsWith('.') - ? relativePath - : './' + relativePath - } else { - // assetsBase.runtime has no effect for assets in CSS - return (assetsBase.url ?? config.base) + filename - } + return toOutputFilePathInCss( + filename, + 'asset', + cssAssetName, + config, + toRelative + ) }) // resolve public URL from CSS paths if (encodedPublicUrls) { @@ -487,13 +493,14 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { '' ) chunkCSS = chunkCSS.replace(publicAssetUrlRE, (_, hash) => { - const publicUrl = publicAssetUrlMap.get(hash)! - if (publicBase.relative && !config.build.ssr) { - return relativePathToPublicFromCSS + publicUrl - } else { - // publicBase.runtime has no effect for assets in CSS - return (publicBase.url ?? config.base) + publicUrl.slice(1) - } + const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) + return toOutputFilePathInCss( + publicUrl, + 'public', + cssAssetName, + config, + () => `${relativePathToPublicFromCSS}/${publicUrl}` + ) }) } return chunkCSS diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 9473afc19bff8e..bc5d4d0117b4b4 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -29,7 +29,7 @@ import { slash } from '../utils' import type { ResolvedConfig } from '../config' -import type { BuildAdvancedBaseOptions } from '../build' +import { toOutputFilePathInHtml } from '../build' import { assetUrlRE, checkPublicFile, @@ -239,7 +239,18 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { if (id.endsWith('.html')) { const relativeUrlPath = slash(path.relative(config.root, id)) const publicPath = `/${relativeUrlPath}` - const publicBase = getPublicBase(relativeUrlPath, config) + const publicBase = getBaseInHTML(relativeUrlPath, config) + + const publicToRelative = (filename: string, importer: string) => + publicBase + filename + const toOutputPublicFilePath = (url: string) => + toOutputFilePathInHtml( + url.slice(1), + 'public', + relativeUrlPath, + config, + publicToRelative + ) // pre-transform html = await applyHtmlTransforms(html, preHooks, { @@ -276,7 +287,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.overwrite( src!.value!.loc.start.offset, src!.value!.loc.end.offset, - `"${normalizePublicPath(url, publicBase)}"`, + `"${toOutputPublicFilePath(url)}"`, { contentOnly: true } ) } @@ -358,7 +369,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.overwrite( p.value.loc.start.offset, p.value.loc.end.offset, - `"${normalizePublicPath(url, publicBase)}"`, + `"${toOutputPublicFilePath(url)}"`, { contentOnly: true } ) } @@ -474,7 +485,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { { contentOnly: true } ) } else if (checkPublicFile(url, config)) { - s.overwrite(start, end, normalizePublicPath(url, publicBase), { + s.overwrite(start, end, toOutputPublicFilePath(url), { contentOnly: true }) } @@ -535,7 +546,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const toScriptTag = ( chunk: OutputChunk, - assetsBase: string, + toOutputPath: (filename: string) => string, isAsync: boolean ): HtmlTagDescriptor => ({ tag: 'script', @@ -543,25 +554,25 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { ...(isAsync ? { async: true } : {}), type: 'module', crossorigin: true, - src: toPublicPath(chunk.fileName, assetsBase) + src: toOutputPath(chunk.fileName) } }) const toPreloadTag = ( chunk: OutputChunk, - assetsBase: string + toOutputPath: (filename: string) => string ): HtmlTagDescriptor => ({ tag: 'link', attrs: { rel: 'modulepreload', crossorigin: true, - href: toPublicPath(chunk.fileName, assetsBase) + href: toOutputPath(chunk.fileName) } }) const getCssTagsForChunk = ( chunk: OutputChunk, - assetsBase: string, + toOutputPath: (filename: string) => string, seen: Set = new Set() ): HtmlTagDescriptor[] => { const tags: HtmlTagDescriptor[] = [] @@ -570,7 +581,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { chunk.imports.forEach((file) => { const importee = bundle[file] if (importee?.type === 'chunk') { - tags.push(...getCssTagsForChunk(importee, assetsBase, seen)) + tags.push(...getCssTagsForChunk(importee, toOutputPath, seen)) } }) } @@ -582,7 +593,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { tag: 'link', attrs: { rel: 'stylesheet', - href: toPublicPath(file, assetsBase) + href: toOutputPath(file) } }) } @@ -593,7 +604,20 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { for (const [id, html] of processedHtml) { const relativeUrlPath = path.posix.relative(config.root, id) - const assetsBase = getAssetsBase(relativeUrlPath, config) + const assetsBase = getBaseInHTML(relativeUrlPath, config) + const toOutputAssetFilePath = (filename: string) => { + if (isExternalUrl(filename)) { + return filename + } else { + return toOutputFilePathInHtml( + filename, + 'asset', + relativeUrlPath, + config, + (filename: string, importer: string) => assetsBase + filename + ) + } + } const isAsync = isAsyncScriptMap.get(config)!.get(id)! @@ -622,13 +646,15 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // when inlined, discard entry chunk and inject