diff --git a/docs/guide/build.md b/docs/guide/build.md index fe3bdb370a07fb..5069d566d889ef 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -27,6 +27,8 @@ JS-imported asset URLs, CSS `url()` references, and asset references in your `.h The exception is when you need to dynamically concatenate URLs on the fly. In this case, you can use the globally injected `import.meta.env.BASE_URL` variable which will be the public base path. Note this variable is statically replaced during build so it must appear exactly as-is (i.e. `import.meta.env['BASE_URL']` won't work). +For advanced base path control, check out [Advanced Base Options](#advanced-base-options). + ## Customizing the Build The build can be customized via various [build config options](/config/build-options.md). Specifically, you can directly adjust the underlying [Rollup options](https://rollupjs.org/guide/en/#big-list-of-options) via `build.rollupOptions`: @@ -181,3 +183,59 @@ Recommended `package.json` for your lib: } } ``` + +## Advanced Base Options + +::: warning +This feature is experimental, the API may change in a future minor without following semver. Please fix the minor version of Vite when using it. +::: + +For advanced use cases, the deployed assets and public files may be in different paths, for example to use different cache strategies. +A user may choose to deploy in three different paths: + +- The generated entry HTML files (which may be processed during SSR) +- 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`. + +```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})` + }, + } +``` + +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: + +```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}` + } + } + } +``` + +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 add2384935d836..0f0fa668c8a502 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -3,9 +3,10 @@ import path from 'node:path' import { createHash } from 'node:crypto' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' -import { build } from 'vite' +import { build, normalizePath } from 'vite' import MagicString from 'magic-string' import type { + BuildAdvancedBaseOptions, BuildOptions, HtmlTagDescriptor, Plugin, @@ -31,6 +32,40 @@ async function loadBabel() { return babel } +function getBaseInHTML( + urlRelativePath: string, + baseOptions: BuildAdvancedBaseOptions, + 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) + ) +} + +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 +} + // https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc // DO NOT ALTER THIS CONTENT const safari10NoModuleFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();` @@ -355,13 +390,18 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { const modernPolyfillFilename = facadeToModernPolyfillMap.get( chunk.facadeModuleId ) + if (modernPolyfillFilename) { tags.push({ tag: 'script', attrs: { type: 'module', crossorigin: true, - src: `${config.base}${modernPolyfillFilename}` + src: toAssetPathFromHtml( + modernPolyfillFilename, + chunk.facadeModuleId!, + config + ) } }) } else if (modernPolyfills.size) { @@ -393,7 +433,11 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { nomodule: true, crossorigin: true, id: legacyPolyfillId, - src: `${config.base}${legacyPolyfillFilename}` + src: toAssetPathFromHtml( + legacyPolyfillFilename, + chunk.facadeModuleId!, + config + ) }, injectTo: 'body' }) @@ -409,7 +453,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { ) if (legacyEntryFilename) { // `assets/foo.js` means importing "named register" in SystemJS - const nonBareBase = config.base === '' ? './' : config.base tags.push({ tag: 'script', attrs: { @@ -419,7 +462,11 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { // script content will stay consistent - which allows using a constant // hash value for CSP. id: legacyEntryId, - 'data-src': nonBareBase + legacyEntryFilename + 'data-src': toAssetPathFromHtml( + legacyEntryFilename, + chunk.facadeModuleId!, + config + ) }, children: systemJSInlineCode, injectTo: 'body' diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 387959dbdb2f33..3e7ef4e9c4e384 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -90,7 +90,7 @@ declare module 'vite' { export default function viteReact(opts: Options = {}): PluginOption[] { // Provide default values for Rollup compat. - let base = '/' + let devBase = '/' let resolvedCacheDir: string let filter = createFilter(opts.include, opts.exclude) let isProduction = true @@ -129,7 +129,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { } }, configResolved(config) { - base = config.base + devBase = config.base projectRoot = config.root resolvedCacheDir = normalizePath(path.resolve(config.cacheDir)) filter = createFilter(opts.include, opts.exclude, { @@ -365,7 +365,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { { tag: 'script', attrs: { type: 'module' }, - children: preambleCode.replace(`__BASE__`, base) + children: preambleCode.replace(`__BASE__`, devBase) } ] } diff --git a/packages/plugin-vue/src/template.ts b/packages/plugin-vue/src/template.ts index d603debe042b9c..1d0bddadea1c1b 100644 --- a/packages/plugin-vue/src/template.ts +++ b/packages/plugin-vue/src/template.ts @@ -116,10 +116,11 @@ export function resolveTemplateCompilerOptions( // relative paths directly to absolute paths without incurring an extra import // request if (filename.startsWith(options.root)) { + const devBase = options.devServer.config.base assetUrlOptions = { base: (options.devServer.config.server?.origin ?? '') + - options.devServer.config.base + + devBase + slash(path.relative(options.root, path.dirname(filename))) } } diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index d1694c295638bf..52096a476f7391 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, resolveConfig } from './config' +import { isDepsOptimizerEnabled, resolveBaseUrl, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' import { terserPlugin } from './plugins/terser' @@ -229,7 +229,11 @@ export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' export type ResolvedBuildOptions = Required -export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions { +export function resolveBuildOptions( + raw: BuildOptions | undefined, + isBuild: boolean, + logger: Logger +): ResolvedBuildOptions { const resolved: ResolvedBuildOptions = { target: 'modules', polyfillModulePreload: true, @@ -826,3 +830,110 @@ function injectSsrFlag>( ): T & { ssr: boolean } { 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 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 + } + + return { + ...resolved, + assets: resolveBuildBaseSpecificOptions( + baseConfig?.assets, + resolved, + isBuild, + logger, + 'assets' + ), + public: resolveBuildBaseSpecificOptions( + baseConfig?.public, + resolved, + isBuild, + logger, + 'public' + ) + } +} + +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 } + } + return { + relative: options?.relative ?? parent.relative, + url: options?.url + ? resolveBaseUrl(options?.url, isBuild, logger, urlConfigPath) + : parent.url, + runtime: options?.runtime ?? parent.runtime + } +} diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 61c8dd5bdfae4f..c8f3d104ec4e17 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -9,8 +9,13 @@ import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' import type { RollupOptions } from 'rollup' import type { Plugin } from './plugin' -import type { BuildOptions, ResolvedBuildOptions } from './build' -import { resolveBuildOptions } from './build' +import type { + BuildAdvancedBaseConfig, + BuildOptions, + ResolvedBuildAdvancedBaseConfig, + ResolvedBuildOptions +} from './build' +import { resolveBuildAdvancedBaseConfig, resolveBuildOptions } from './build' import type { ResolvedServerOptions, ServerOptions } from './server' import { resolveServerOptions } from './server' import type { PreviewOptions, ResolvedPreviewOptions } from './preview' @@ -47,6 +52,8 @@ import { resolveSSROptions } from './ssr' const debug = createDebugger('vite:config') +export type { BuildAdvancedBaseOptions, BuildAdvancedBaseConfig } from './build' + // NOTE: every export in this file is re-exported from ./index.ts so it will // be part of the public API. export interface ConfigEnv { @@ -247,7 +254,12 @@ export interface ExperimentalOptions { * @default false */ importGlobRestoreExtension?: boolean - + /** + * Build advanced base options. Allow finegrain contol over assets and public files base + * + * @experimental + */ + buildAdvancedBaseOptions?: BuildAdvancedBaseConfig /** * Enables support of HMR partial accept via `import.meta.hot.acceptExports`. * @@ -257,6 +269,10 @@ 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. @@ -328,6 +344,7 @@ export type ResolvedConfig = Readonly< packageCache: PackageCache worker: ResolveWorkerOptions appType: AppType + experimental: ResolvedExperimentalOptions } > @@ -464,8 +481,31 @@ export async function resolveConfig( } // resolve public base url - const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger) - const resolvedBuildOptions = resolveBuildOptions(config.build) + const isBuild = command === 'build' + const relativeBaseShortcut = config.base === '' || config.base === './' + const base = relativeBaseShortcut && !isBuild ? '/' : 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 + ) + + const resolvedBuildOptions = resolveBuildOptions( + config.build, + isBuild, + logger + ) // resolve cache directory const pkgPath = lookupFile(resolvedRoot, [`package.json`], { pathOnly: true }) @@ -538,6 +578,8 @@ export async function resolveConfig( const optimizeDeps = config.optimizeDeps || {} + const BASE_URL = resolvedBase + const resolved: ResolvedConfig = { ...config, configFile: configFile ? normalizePath(configFile) : undefined, @@ -546,7 +588,7 @@ export async function resolveConfig( ), inlineConfig, root: resolvedRoot, - base: BASE_URL, + base: resolvedBase, resolve: resolveOptions, publicDir: resolvedPublicDir, cacheDir, @@ -581,7 +623,13 @@ export async function resolveConfig( } }, worker: resolvedWorkerOptions, - appType: config.appType ?? middlewareMode === 'ssr' ? 'custom' : 'spa' + appType: config.appType ?? middlewareMode === 'ssr' ? 'custom' : 'spa', + experimental: { + importGlobRestoreExtension: false, + hmrPartialAccept: false, + ...config.experimental, + buildAdvancedBaseOptions: resolvedBuildAdvancedBaseOptions + } } if (middlewareMode === 'ssr') { @@ -682,23 +730,20 @@ assetFileNames isn't equal for every build.rollupOptions.output. A single patter } /** - * Resolve base. Note that some users use Vite to build for non-web targets like + * Resolve base url. Note that some users use Vite to build for non-web targets like * electron or expects to deploy */ -function resolveBaseUrl( +export function resolveBaseUrl( base: UserConfig['base'] = '/', isBuild: boolean, - logger: Logger + logger: Logger, + optionName: string ): string { - // #1669 special treatment for empty for same dir relative base - if (base === '' || base === './') { - return isBuild ? base : '/' - } if (base.startsWith('.')) { logger.warn( colors.yellow( colors.bold( - `(!) invalid "base" option: ${base}. The value can only be an absolute ` + + `(!) invalid "${optionName}" option: ${base}. The value can only be an absolute ` + `URL, ./, or an empty string.` ) ) @@ -718,7 +763,7 @@ function resolveBaseUrl( if (!base.startsWith('/')) { logger.warn( colors.yellow( - colors.bold(`(!) "base" option should start with a slash.`) + colors.bold(`(!) "${optionName}" option should start with a slash.`) ) ) base = '/' + base @@ -728,7 +773,9 @@ function resolveBaseUrl( // ensure ending slash if (!base.endsWith('/')) { logger.warn( - colors.yellow(colors.bold(`(!) "base" option should end with a slash.`)) + colors.yellow( + colors.bold(`(!) "${optionName}" option should end with a slash.`) + ) ) base += '/' } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 38952e8a466b62..4655b4881b7650 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -23,7 +23,10 @@ export type { BuildOptions, LibraryOptions, LibraryFormats, - ResolvedBuildOptions + ResolvedBuildOptions, + BuildAdvancedBaseConfig, + ResolvedBuildAdvancedBaseConfig, + BuildAdvancedBaseOptions } from './build' export type { PreviewOptions, diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index edb57b02fc2cdb..597de2358f43e3 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -155,13 +155,8 @@ export async function printCommonServerUrls( if (isAddressInfo(address)) { const hostname = await resolveHostname(options.host) const protocol = options.https ? 'https' : 'http' - printServerUrls( - hostname, - protocol, - address.port, - config.base, - config.logger.info - ) + const base = config.base === './' || config.base === '' ? '/' : config.base + printServerUrls(hostname, protocol, address.port, base, config.logger.info) } } diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 3b4554daff02ca..16e73361cdd44f 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -4,9 +4,10 @@ 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 type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { cleanUrl, getHash, isRelativeBase, normalizePath } from '../utils' +import { cleanUrl, getHash, normalizePath } from '../utils' import { FS_PREFIX } from '../constants' export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g @@ -41,7 +42,6 @@ export function registerCustomMime(): void { 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) registerCustomMime() @@ -99,6 +99,17 @@ export function assetPlugin(config: ResolvedConfig): Plugin { 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 + ? absoluteUrlPathInterpolation(filename) + : JSON.stringify((base.url ?? config.base) + filename).slice(1, -1) + } + // Urls added with JS using e.g. // imgElement.src = "__VITE_ASSET__5aa0ddc0__" are using quotes @@ -115,27 +126,29 @@ export function assetPlugin(config: ResolvedConfig): Plugin { const file = getAssetFilename(hash, config) || this.getFileName(hash) chunk.viteMetadata.importedAssets.add(cleanUrl(file)) 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, { + const replacement = toOutputFilePathInString( + filename, + config.experimental.buildAdvancedBaseOptions.assets + ) + s.overwrite(match.index, match.index + full.length, replacement, { 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 - }) - } + 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)!.slice(1) + const replacement = toOutputFilePathInString( + publicUrl, + config.experimental.buildAdvancedBaseOptions.public + ) + s.overwrite(match.index, match.index + full.length, replacement, { + contentOnly: true + }) } if (s) { @@ -207,7 +220,8 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { rtn = path.posix.join(FS_PREFIX + id) } const origin = config.server?.origin ?? '' - return origin + config.base + rtn.replace(/^\//, '') + const devBase = config.base + return origin + devBase + rtn.replace(/^\//, '') } export function getAssetFilename( @@ -326,7 +340,8 @@ export function publicFileToBuiltUrl( url: string, config: ResolvedConfig ): string { - if (!isRelativeBase(config.base)) { + if (config.command !== 'build') { + // We don't need relative base or buildAdvancedBaseOptions support during dev return config.base + url.slice(1) } const hash = getHash(url) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 10859c8f75d27d..a363288e26282c 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -32,7 +32,8 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { } else { port = String(port || options.port || config.server.port!) } - let hmrBase = config.base + const devBase = config.base + let hmrBase = devBase if (options.path) { hmrBase = path.posix.join(hmrBase, options.path) } @@ -42,7 +43,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { return code .replace(`__MODE__`, JSON.stringify(config.mode)) - .replace(`__BASE__`, JSON.stringify(config.base)) + .replace(`__BASE__`, JSON.stringify(devBase)) .replace(`__DEFINES__`, serializeDefine(config.define || {})) .replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)) .replace(`__HMR_HOSTNAME__`, JSON.stringify(host)) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index eaa8ee4b51b32f..ef112d7083cca5 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -37,7 +37,6 @@ import { isDataUrl, isExternalUrl, isObject, - isRelativeBase, normalizePath, parseRequest, processSrcSet, @@ -149,6 +148,10 @@ const postcssConfigCache = new WeakMap< PostCSSConfigResult | null >() +function encodePublicUrlsInCSS(config: ResolvedConfig) { + return config.command === 'build' +} + /** * Plugin applied before user plugins */ @@ -190,7 +193,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { const urlReplacer: CssUrlReplacer = async (url, importer) => { if (checkPublicFile(url, config)) { - if (isRelativeBase(config.base)) { + if (encodePublicUrlsInCSS(config)) { return publicFileToBuiltUrl(url, config) } else { return config.base + url.slice(1) @@ -232,6 +235,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { // server only logic for handling CSS @import dependency hmr const { moduleGraph } = server const thisModule = moduleGraph.getModuleById(id) + const devBase = config.base if (thisModule) { // CSS modules cannot self-accept since it exports values const isSelfAccepting = @@ -247,10 +251,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { : await moduleGraph.ensureEntryFromUrl( ( await fileToUrl(file, config, this) - ).replace( - (config.server?.origin ?? '') + config.base, - '/' - ), + ).replace((config.server?.origin ?? '') + devBase, '/'), ssr ) ) @@ -296,8 +297,6 @@ 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) @@ -372,9 +371,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } const cssContent = await getContentWithSourcemap(css) + const devBase = config.base return [ `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( - path.posix.join(config.base, CLIENT_PUBLIC_PATH) + path.posix.join(devBase, CLIENT_PUBLIC_PATH) )}`, `const __vite__id = ${JSON.stringify(id)}`, `const __vite__css = ${JSON.stringify(cssContent)}`, @@ -457,27 +457,32 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // resolve asset URL placeholders to their built file URLs function resolveAssetUrlsInCss(chunkCSS: string, cssAssetName: string) { - const cssAssetDirname = relativeBase - ? getCssAssetDirname(cssAssetName) - : undefined + const encodedPublicUrls = encodePublicUrlsInCSS(config) + const assetsBase = config.experimental.buildAdvancedBaseOptions.assets + const cssAssetDirname = + encodedPublicUrls || assetsBase.relative + ? getCssAssetDirname(cssAssetName) + : undefined // 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 (relativeBase) { + if (assetsBase.relative) { // relative base + extracted CSS const relativePath = path.posix.relative(cssAssetDirname!, filename) return relativePath.startsWith('.') ? relativePath : './' + relativePath } else { - // absolute base - return config.base + filename + if (assetsBase.runtime) { + // config.logger.error('Error TODO:base')... absolute + runtime + } + return (assetsBase.url ?? config.base) + filename } }) - // resolve public URL from CSS paths - if (relativeBase) { + // resolve public URL from CSS paths, TODO:base + if (encodedPublicUrls) { const relativePathToPublicFromCSS = path.posix.relative( cssAssetDirname!, '' diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index d387fd99a658ab..9473afc19bff8e 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -24,12 +24,12 @@ import { getHash, isDataUrl, isExternalUrl, - isRelativeBase, normalizePath, processSrcSet, slash } from '../utils' import type { ResolvedConfig } from '../config' +import type { BuildAdvancedBaseOptions } from '../build' import { assetUrlRE, checkPublicFile, @@ -535,7 +535,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const toScriptTag = ( chunk: OutputChunk, - publicBase: string, + assetsBase: string, isAsync: boolean ): HtmlTagDescriptor => ({ tag: 'script', @@ -543,25 +543,25 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { ...(isAsync ? { async: true } : {}), type: 'module', crossorigin: true, - src: toPublicPath(chunk.fileName, publicBase) + src: toPublicPath(chunk.fileName, assetsBase) } }) const toPreloadTag = ( chunk: OutputChunk, - publicBase: string + assetsBase: string ): HtmlTagDescriptor => ({ tag: 'link', attrs: { rel: 'modulepreload', crossorigin: true, - href: toPublicPath(chunk.fileName, publicBase) + href: toPublicPath(chunk.fileName, assetsBase) } }) const getCssTagsForChunk = ( chunk: OutputChunk, - publicBase: string, + assetsBase: string, seen: Set = new Set() ): HtmlTagDescriptor[] => { const tags: HtmlTagDescriptor[] = [] @@ -570,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, publicBase, seen)) + tags.push(...getCssTagsForChunk(importee, assetsBase, seen)) } }) } @@ -582,7 +582,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { tag: 'link', attrs: { rel: 'stylesheet', - href: toPublicPath(file, publicBase) + href: toPublicPath(file, assetsBase) } }) } @@ -593,7 +593,7 @@ 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 assetsBase = getAssetsBase(relativeUrlPath, config) const isAsync = isAsyncScriptMap.get(config)!.get(id)! @@ -622,13 +622,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // when inlined, discard entry chunk and inject diff --git a/playground/legacy/vite.config.js b/playground/legacy/vite.config.js index c21813971716b5..f793980f365887 100644 --- a/playground/legacy/vite.config.js +++ b/playground/legacy/vite.config.js @@ -15,12 +15,15 @@ module.exports = { cssCodeSplit: false, manifest: true, rollupOptions: { + input: { + index: path.resolve(__dirname, 'index.html'), + nested: path.resolve(__dirname, 'nested/index.html') + }, output: { chunkFileNames(chunkInfo) { if (chunkInfo.name === 'immutable-chunk') { return `assets/${chunkInfo.name}.js` } - return `assets/chunk-[name].[hash].js` } } diff --git a/playground/lib/__tests__/serve.ts b/playground/lib/__tests__/serve.ts index 7f390e20a18abe..02a2bb04d3348d 100644 --- a/playground/lib/__tests__/serve.ts +++ b/playground/lib/__tests__/serve.ts @@ -42,8 +42,8 @@ export async function serve(): Promise<{ close(): Promise }> { }) ).listen() // use resolved port/base from server - const base = viteServer.config.base === '/' ? '' : viteServer.config.base - setViteUrl(`http://localhost:${viteServer.config.server.port}${base}`) + const devBase = viteServer.config.base === '/' ? '' : viteServer.config.base + setViteUrl(`http://localhost:${viteServer.config.server.port}${devBase}`) await page.goto(viteTestUrl) return viteServer diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index 1dc5e102edf0f7..de0877302e5118 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -214,8 +214,10 @@ export async function startDefaultServe(): Promise { viteConfig = testConfig server = await (await createServer(testConfig)).listen() // use resolved port/base from server - const base = server.config.base === '/' ? '' : server.config.base - viteTestUrl = `http://localhost:${server.config.server.port}${base}` + const devBase = server.config.base + viteTestUrl = `http://localhost:${server.config.server.port}${ + devBase === '/' ? '' : devBase + }` await page.goto(viteTestUrl) } else { process.env.VITE_INLINE = 'inline-build'