mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 22:59:10 +00:00
feat: experimental.buildAdvancedBaseOptions (#8450)
This commit is contained in:
parent
15ebe1e6df
commit
8ef733368f
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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<BuildOptions>
|
||||
|
||||
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 extends Record<string, any>>(
|
||||
): 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
|
||||
}
|
||||
}
|
||||
|
@ -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<ExperimentalOptions> & {
|
||||
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 += '/'
|
||||
}
|
||||
|
@ -23,7 +23,10 @@ export type {
|
||||
BuildOptions,
|
||||
LibraryOptions,
|
||||
LibraryFormats,
|
||||
ResolvedBuildOptions
|
||||
ResolvedBuildOptions,
|
||||
BuildAdvancedBaseConfig,
|
||||
ResolvedBuildAdvancedBaseConfig,
|
||||
BuildAdvancedBaseOptions
|
||||
} from './build'
|
||||
export type {
|
||||
PreviewOptions,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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<NormalizedOutputOptions, string>
|
||||
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!,
|
||||
''
|
||||
|
@ -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<string> = 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 <script> for everything in post-order
|
||||
const imports = getImportedChunks(chunk)
|
||||
const assetTags = canInlineEntry
|
||||
? imports.map((chunk) => toScriptTag(chunk, publicBase, isAsync))
|
||||
? imports.map((chunk) => toScriptTag(chunk, assetsBase, isAsync))
|
||||
: [
|
||||
toScriptTag(chunk, publicBase, isAsync),
|
||||
...imports.map((i) => toPreloadTag(i, publicBase))
|
||||
toScriptTag(chunk, assetsBase, isAsync),
|
||||
...imports.map((i) => toPreloadTag(i, assetsBase))
|
||||
]
|
||||
|
||||
assetTags.push(...getCssTagsForChunk(chunk, publicBase))
|
||||
assetTags.push(...getCssTagsForChunk(chunk, assetsBase))
|
||||
|
||||
result = injectToHead(result, assetTags)
|
||||
}
|
||||
@ -644,7 +644,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
href: toPublicPath(cssChunk.fileName, publicBase)
|
||||
href: toPublicPath(cssChunk.fileName, assetsBase)
|
||||
}
|
||||
}
|
||||
])
|
||||
@ -676,7 +676,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
|
||||
})
|
||||
// resolve asset url references
|
||||
result = result.replace(assetUrlRE, (_, fileHash, postfix = '') => {
|
||||
return publicBase + getAssetFilename(fileHash, config) + postfix
|
||||
return assetsBase + getAssetFilename(fileHash, config) + postfix
|
||||
})
|
||||
|
||||
if (chunk && canInlineEntry) {
|
||||
@ -818,13 +818,38 @@ function isEntirelyImport(code: string) {
|
||||
return !code.replace(importRE, '').replace(commentRE, '').trim().length
|
||||
}
|
||||
|
||||
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 getPublicBase(urlRelativePath: string, config: ResolvedConfig) {
|
||||
return isRelativeBase(config.base)
|
||||
? path.posix.join(
|
||||
path.posix.relative(urlRelativePath, '').slice(0, -2),
|
||||
'./'
|
||||
)
|
||||
: config.base
|
||||
return getBaseInHTML(
|
||||
urlRelativePath,
|
||||
config.experimental.buildAdvancedBaseOptions.public,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
function getAssetsBase(urlRelativePath: string, config: ResolvedConfig) {
|
||||
return getBaseInHTML(
|
||||
urlRelativePath,
|
||||
config.experimental.buildAdvancedBaseOptions.assets,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
function toPublicPath(filename: string, publicBase: string) {
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
combineSourcemaps,
|
||||
isDataUrl,
|
||||
isExternalUrl,
|
||||
isRelativeBase,
|
||||
moduleListContains
|
||||
} from '../utils'
|
||||
import type { Plugin } from '../plugin'
|
||||
@ -109,14 +108,19 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
const isWorker = config.isWorker
|
||||
const insertPreload = !(ssr || !!config.build.lib || isWorker)
|
||||
|
||||
const relativeBase = isRelativeBase(config.base)
|
||||
const assetsBase = config.experimental.buildAdvancedBaseOptions.assets
|
||||
const relativePreloadUrls = !(assetsBase.url || assetsBase.runtime)
|
||||
|
||||
const scriptRel = config.build.polyfillModulePreload
|
||||
? `'modulepreload'`
|
||||
: `(${detectScriptRel.toString()})()`
|
||||
const assetsURL = relativeBase
|
||||
const assetsURL = relativePreloadUrls
|
||||
? `function(dep,importerUrl) { return new URL(dep, importerUrl).href }`
|
||||
: `function(dep) { return ${JSON.stringify(config.base)}+dep }`
|
||||
: `function(dep) { return ${
|
||||
assetsBase.runtime
|
||||
? assetsBase.runtime('dep')
|
||||
: `${JSON.stringify(assetsBase.url ?? config.base)}+dep`
|
||||
}}`
|
||||
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
|
||||
|
||||
return {
|
||||
@ -235,7 +239,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
str().appendRight(
|
||||
expEnd,
|
||||
`,${isModernFlag}?"${preloadMarker}":void 0${
|
||||
relativeBase ? ',import.meta.url' : ''
|
||||
relativePreloadUrls ? ',import.meta.url' : ''
|
||||
})`
|
||||
)
|
||||
}
|
||||
@ -444,7 +448,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
? `[${[...deps]
|
||||
.map((d) =>
|
||||
JSON.stringify(
|
||||
relativeBase
|
||||
relativePreloadUrls
|
||||
? path.relative(path.dirname(file), d)
|
||||
: d
|
||||
)
|
||||
|
@ -64,7 +64,7 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin {
|
||||
id,
|
||||
config.root,
|
||||
(im) => this.resolve(im, id).then((i) => i?.id || im),
|
||||
config.experimental?.importGlobRestoreExtension
|
||||
config.experimental.importGlobRestoreExtension
|
||||
)
|
||||
if (result) {
|
||||
if (server) {
|
||||
|
@ -86,14 +86,14 @@ export interface InternalResolveOptions extends ResolveOptions {
|
||||
shouldExternalize?: (id: string) => boolean | undefined
|
||||
}
|
||||
|
||||
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
|
||||
export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
|
||||
const {
|
||||
root,
|
||||
isProduction,
|
||||
asSrc,
|
||||
ssrConfig,
|
||||
preferRelative = false
|
||||
} = baseOptions
|
||||
} = resolveOptions
|
||||
|
||||
const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {}
|
||||
|
||||
@ -103,7 +103,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
|
||||
async resolveId(id, importer, resolveOpts) {
|
||||
// We need to delay depsOptimizer until here instead of passing it as an option
|
||||
// the resolvePlugin because the optimizer is created on server listen during dev
|
||||
const depsOptimizer = baseOptions.getDepsOptimizer?.()
|
||||
const depsOptimizer = resolveOptions.getDepsOptimizer?.()
|
||||
|
||||
const ssr = resolveOpts?.ssr === true
|
||||
|
||||
@ -124,8 +124,8 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
|
||||
|
||||
const options: InternalResolveOptions = {
|
||||
isRequire,
|
||||
...baseOptions,
|
||||
scan: resolveOpts?.scan ?? baseOptions.scan
|
||||
...resolveOptions,
|
||||
scan: resolveOpts?.scan ?? resolveOptions.scan
|
||||
}
|
||||
|
||||
if (importer) {
|
||||
|
@ -5,13 +5,7 @@ import type { ResolvedConfig } from '../config'
|
||||
import type { Plugin } from '../plugin'
|
||||
import type { ViteDevServer } from '../server'
|
||||
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
|
||||
import {
|
||||
cleanUrl,
|
||||
getHash,
|
||||
injectQuery,
|
||||
isRelativeBase,
|
||||
parseRequest
|
||||
} from '../utils'
|
||||
import { cleanUrl, getHash, injectQuery, parseRequest } from '../utils'
|
||||
import { onRollupWarning } from '../build'
|
||||
import { getDepsOptimizer } from '../optimizer'
|
||||
import { fileToUrl } from './asset'
|
||||
@ -181,10 +175,10 @@ export async function workerFileToUrl(
|
||||
})
|
||||
workerMap.bundle.set(id, fileName)
|
||||
}
|
||||
|
||||
return isRelativeBase(config.base)
|
||||
const assetsBase = config.experimental.buildAdvancedBaseOptions.assets
|
||||
return assetsBase.relative || assetsBase.runtime
|
||||
? encodeWorkerAssetFileName(fileName, workerMap)
|
||||
: config.base + fileName
|
||||
: (assetsBase.url ?? config.base) + fileName
|
||||
}
|
||||
|
||||
export function webWorkerPlugin(config: ResolvedConfig): Plugin {
|
||||
@ -333,18 +327,30 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
|
||||
// Replace "__VITE_WORKER_ASSET__5aa0ddc0__" using relative paths
|
||||
const workerMap = workerCache.get(config.mainConfig || config)!
|
||||
const { fileNameHash } = workerMap
|
||||
const assetsBase = config.experimental.buildAdvancedBaseOptions.assets
|
||||
|
||||
while ((match = workerAssetUrlRE.exec(code))) {
|
||||
const [full, hash] = match
|
||||
const filename = fileNameHash.get(hash)!
|
||||
let outputFilepath = path.posix.relative(
|
||||
path.dirname(chunk.fileName),
|
||||
filename
|
||||
)
|
||||
if (!outputFilepath.startsWith('.')) {
|
||||
outputFilepath = './' + outputFilepath
|
||||
let replacement: string
|
||||
if (assetsBase.runtime) {
|
||||
replacement = `"+${assetsBase.runtime(JSON.stringify(filename))}+"`
|
||||
} else {
|
||||
// Relative base
|
||||
let outputFilepath: string
|
||||
if (assetsBase.relative) {
|
||||
outputFilepath = path.posix.relative(
|
||||
path.dirname(chunk.fileName),
|
||||
filename
|
||||
)
|
||||
if (!outputFilepath.startsWith('.')) {
|
||||
outputFilepath = './' + outputFilepath
|
||||
}
|
||||
} else {
|
||||
outputFilepath = (assetsBase.url ?? config.base) + filename
|
||||
}
|
||||
replacement = JSON.stringify(outputFilepath).slice(1, -1)
|
||||
}
|
||||
const replacement = JSON.stringify(outputFilepath).slice(1, -1)
|
||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||
contentOnly: true
|
||||
})
|
||||
|
@ -97,10 +97,13 @@ export async function preview(
|
||||
|
||||
app.use(compression())
|
||||
|
||||
const previewBase =
|
||||
config.base === './' || config.base === '' ? '/' : config.base
|
||||
|
||||
// static assets
|
||||
const distDir = path.resolve(config.root, config.build.outDir)
|
||||
app.use(
|
||||
config.base,
|
||||
previewBase,
|
||||
sirv(distDir, {
|
||||
etag: true,
|
||||
dev: true,
|
||||
@ -116,7 +119,6 @@ export async function preview(
|
||||
const port = options.port ?? 4173
|
||||
const protocol = options.https ? 'https' : 'http'
|
||||
const logger = config.logger
|
||||
const base = config.base
|
||||
|
||||
const serverPort = await httpServerStart(httpServer, {
|
||||
port,
|
||||
@ -126,7 +128,7 @@ export async function preview(
|
||||
})
|
||||
|
||||
if (options.open) {
|
||||
const path = typeof options.open === 'string' ? options.open : base
|
||||
const path = typeof options.open === 'string' ? options.open : previewBase
|
||||
openBrowser(
|
||||
path.startsWith('http')
|
||||
? path
|
||||
|
@ -464,7 +464,8 @@ export async function createServer(
|
||||
}
|
||||
|
||||
// base
|
||||
if (config.base !== '/') {
|
||||
const devBase = config.base
|
||||
if (devBase !== '/') {
|
||||
middlewares.use(baseMiddleware(server))
|
||||
}
|
||||
|
||||
@ -559,7 +560,7 @@ async function startServer(
|
||||
|
||||
const protocol = options.https ? 'https' : 'http'
|
||||
const info = server.config.logger.info
|
||||
const base = server.config.base
|
||||
const devBase = server.config.base
|
||||
|
||||
const serverPort = await httpServerStart(httpServer, {
|
||||
port,
|
||||
@ -588,7 +589,7 @@ async function startServer(
|
||||
}
|
||||
|
||||
if (options.open && !isRestart) {
|
||||
const path = typeof options.open === 'string' ? options.open : base
|
||||
const path = typeof options.open === 'string' ? options.open : devBase
|
||||
openBrowser(
|
||||
path.startsWith('http')
|
||||
? path
|
||||
|
@ -6,7 +6,7 @@ import type { ViteDevServer } from '..'
|
||||
export function baseMiddleware({
|
||||
config
|
||||
}: ViteDevServer): Connect.NextHandleFunction {
|
||||
const base = config.base
|
||||
const devBase = config.base
|
||||
|
||||
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
|
||||
return function viteBaseMiddleware(req, res, next) {
|
||||
@ -14,10 +14,10 @@ export function baseMiddleware({
|
||||
const parsed = new URL(url, 'http://vitejs.dev')
|
||||
const path = parsed.pathname || '/'
|
||||
|
||||
if (path.startsWith(base)) {
|
||||
if (path.startsWith(devBase)) {
|
||||
// rewrite url to remove base.. this ensures that other middleware does
|
||||
// not need to consider base being prepended or not
|
||||
req.url = url.replace(base, '/')
|
||||
req.url = url.replace(devBase, '/')
|
||||
return next()
|
||||
}
|
||||
|
||||
@ -29,18 +29,18 @@ export function baseMiddleware({
|
||||
if (path === '/' || path === '/index.html') {
|
||||
// redirect root visit to based url with search and hash
|
||||
res.writeHead(302, {
|
||||
Location: base + (parsed.search || '') + (parsed.hash || '')
|
||||
Location: devBase + (parsed.search || '') + (parsed.hash || '')
|
||||
})
|
||||
res.end()
|
||||
return
|
||||
} else if (req.headers.accept?.includes('text/html')) {
|
||||
// non-based page visit
|
||||
const redirectPath = base + url.slice(1)
|
||||
const redirectPath = devBase + url.slice(1)
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
res.end(
|
||||
`The server is configured with a public base URL of ${base} - ` +
|
||||
`The server is configured with a public base URL of ${devBase} - ` +
|
||||
`did you mean to visit <a href="${redirectPath}">${redirectPath}</a> instead?`
|
||||
)
|
||||
return
|
||||
|
@ -79,12 +79,13 @@ const processNodeUrl = (
|
||||
url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
|
||||
}
|
||||
}
|
||||
const devBase = config.base
|
||||
if (startsWithSingleSlashRE.test(url)) {
|
||||
// prefix with base (dev only, base is never relative)
|
||||
s.overwrite(
|
||||
node.value!.loc.start.offset,
|
||||
node.value!.loc.end.offset,
|
||||
`"${config.base + url.slice(1)}"`,
|
||||
`"${devBase + url.slice(1)}"`,
|
||||
{ contentOnly: true }
|
||||
)
|
||||
} else if (
|
||||
@ -95,8 +96,8 @@ const processNodeUrl = (
|
||||
) {
|
||||
const replacer = (url: string) =>
|
||||
path.posix.join(
|
||||
config.base,
|
||||
path.posix.relative(originalUrl, config.base),
|
||||
devBase,
|
||||
path.posix.relative(originalUrl, devBase),
|
||||
url.slice(1)
|
||||
)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { normalizePath } from '../utils'
|
||||
export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
|
||||
// module id => preload assets mapping
|
||||
const ssrManifest: Record<string, string[]> = {}
|
||||
const base = config.base
|
||||
const base = config.base // TODO:base
|
||||
|
||||
return {
|
||||
name: 'vite:ssr-manifest',
|
||||
@ -59,7 +59,7 @@ export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
|
||||
const chunk = bundle[filename] as OutputChunk | undefined
|
||||
if (chunk) {
|
||||
chunk.viteMetadata.importedCss.forEach((file) => {
|
||||
deps.push(join(config.base, file))
|
||||
deps.push(join(base, file)) // TODO:base
|
||||
})
|
||||
chunk.imports.forEach(addDeps)
|
||||
}
|
||||
|
@ -309,10 +309,6 @@ export function removeTimestampQuery(url: string): string {
|
||||
return url.replace(timestampRE, '').replace(trailingSeparatorRE, '')
|
||||
}
|
||||
|
||||
export function isRelativeBase(base: string): boolean {
|
||||
return base === '' || base.startsWith('.')
|
||||
}
|
||||
|
||||
export async function asyncReplace(
|
||||
input: string,
|
||||
re: RegExp,
|
||||
|
@ -0,0 +1,235 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import {
|
||||
browserLogs,
|
||||
findAssetFile,
|
||||
getBg,
|
||||
getColor,
|
||||
isBuild,
|
||||
page
|
||||
} from '~utils'
|
||||
|
||||
const absoluteAssetMatch = isBuild
|
||||
? /\/other-assets\/asset\.\w{8}\.png/
|
||||
: '/nested/asset.png'
|
||||
|
||||
// Asset URLs in CSS are relative to the same dir, the computed
|
||||
// style returns the absolute URL in the test
|
||||
const cssBgAssetMatch = absoluteAssetMatch
|
||||
|
||||
const iconMatch = `/icon.png`
|
||||
|
||||
const absoluteIconMatch = isBuild
|
||||
? /\/other-assets\/icon\.\w{8}\.png/
|
||||
: '/nested/icon.png'
|
||||
|
||||
const absolutePublicIconMatch = isBuild ? /\/icon\.png/ : '/icon.png'
|
||||
|
||||
test('should have no 404s', () => {
|
||||
browserLogs.forEach((msg) => {
|
||||
expect(msg).not.toMatch('404')
|
||||
})
|
||||
})
|
||||
|
||||
describe('raw references from /public', () => {
|
||||
test('load raw js from /public', async () => {
|
||||
expect(await page.textContent('.raw-js')).toMatch('[success]')
|
||||
})
|
||||
|
||||
test('load raw css from /public', async () => {
|
||||
expect(await getColor('.raw-css')).toBe('red')
|
||||
})
|
||||
})
|
||||
|
||||
test('import-expression from simple script', async () => {
|
||||
expect(await page.textContent('.import-expression')).toMatch(
|
||||
'[success][success]'
|
||||
)
|
||||
})
|
||||
|
||||
describe('asset imports from js', () => {
|
||||
test('relative', async () => {
|
||||
expect(await page.textContent('.asset-import-relative')).toMatch(
|
||||
cssBgAssetMatch
|
||||
)
|
||||
})
|
||||
|
||||
test('absolute', async () => {
|
||||
expect(await page.textContent('.asset-import-absolute')).toMatch(
|
||||
cssBgAssetMatch
|
||||
)
|
||||
})
|
||||
|
||||
test('from /public', async () => {
|
||||
expect(await page.textContent('.public-import')).toMatch(
|
||||
absolutePublicIconMatch
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('css url() references', () => {
|
||||
test('fonts', async () => {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (document as any).fonts.check('700 32px Inter')
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('relative', async () => {
|
||||
const bg = await getBg('.css-url-relative')
|
||||
expect(bg).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
|
||||
test('image-set relative', async () => {
|
||||
const imageSet = await getBg('.css-image-set-relative')
|
||||
imageSet.split(', ').forEach((s) => {
|
||||
expect(s).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
})
|
||||
|
||||
test('image-set without the url() call', async () => {
|
||||
const imageSet = await getBg('.css-image-set-without-url-call')
|
||||
imageSet.split(', ').forEach((s) => {
|
||||
expect(s).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
})
|
||||
|
||||
test('image-set with var', async () => {
|
||||
const imageSet = await getBg('.css-image-set-with-var')
|
||||
imageSet.split(', ').forEach((s) => {
|
||||
expect(s).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
})
|
||||
|
||||
test('image-set with mix', async () => {
|
||||
const imageSet = await getBg('.css-image-set-mix-url-var')
|
||||
imageSet.split(', ').forEach((s) => {
|
||||
expect(s).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
})
|
||||
|
||||
test('relative in @import', async () => {
|
||||
expect(await getBg('.css-url-relative-at-imported')).toMatch(
|
||||
cssBgAssetMatch
|
||||
)
|
||||
})
|
||||
|
||||
test('absolute', async () => {
|
||||
expect(await getBg('.css-url-absolute')).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
|
||||
test('from /public', async () => {
|
||||
expect(await getBg('.css-url-public')).toMatch(iconMatch)
|
||||
})
|
||||
|
||||
test('multiple urls on the same line', async () => {
|
||||
const bg = await getBg('.css-url-same-line')
|
||||
expect(bg).toMatch(cssBgAssetMatch)
|
||||
expect(bg).toMatch(iconMatch)
|
||||
})
|
||||
|
||||
test('aliased', async () => {
|
||||
const bg = await getBg('.css-url-aliased')
|
||||
expect(bg).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
})
|
||||
|
||||
describe.runIf(isBuild)('index.css URLs', () => {
|
||||
let css: string
|
||||
beforeAll(() => {
|
||||
css = findAssetFile(/index.*\.css$/, '', 'other-assets')
|
||||
})
|
||||
|
||||
test('relative asset URL', () => {
|
||||
expect(css).toMatch(`./asset.`)
|
||||
})
|
||||
|
||||
test('preserve postfix query/hash', () => {
|
||||
expect(css).toMatch(`woff2?#iefix`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('image', () => {
|
||||
test('srcset', async () => {
|
||||
const img = await page.$('.img-src-set')
|
||||
const srcset = await img.getAttribute('srcset')
|
||||
srcset.split(', ').forEach((s) => {
|
||||
expect(s).toMatch(
|
||||
isBuild
|
||||
? /other-assets\/asset\.\w{8}\.png \d{1}x/
|
||||
: /\.\/nested\/asset\.png \d{1}x/
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('svg fragments', () => {
|
||||
// 404 is checked already, so here we just ensure the urls end with #fragment
|
||||
test('img url', async () => {
|
||||
const img = await page.$('.svg-frag-img')
|
||||
expect(await img.getAttribute('src')).toMatch(/svg#icon-clock-view$/)
|
||||
})
|
||||
|
||||
test('via css url()', async () => {
|
||||
const bg = await page.evaluate(() => {
|
||||
return getComputedStyle(document.querySelector('.icon')).backgroundImage
|
||||
})
|
||||
expect(bg).toMatch(/svg#icon-clock-view"\)$/)
|
||||
})
|
||||
|
||||
test('from js import', async () => {
|
||||
const img = await page.$('.svg-frag-import')
|
||||
expect(await img.getAttribute('src')).toMatch(/svg#icon-heart-view$/)
|
||||
})
|
||||
})
|
||||
|
||||
test('?raw import', async () => {
|
||||
expect(await page.textContent('.raw')).toMatch('SVG')
|
||||
})
|
||||
|
||||
test('?url import', async () => {
|
||||
expect(await page.textContent('.url')).toMatch(
|
||||
isBuild ? /\/other-assets\/foo\.\w{8}\.js/ : `/foo.js`
|
||||
)
|
||||
})
|
||||
|
||||
test('?url import on css', async () => {
|
||||
const txt = await page.textContent('.url-css')
|
||||
expect(txt).toMatch(
|
||||
isBuild ? /\/other-assets\/icons\.\w{8}\.css/ : '/css/icons.css'
|
||||
)
|
||||
})
|
||||
|
||||
test('new URL(..., import.meta.url)', async () => {
|
||||
expect(await page.textContent('.import-meta-url')).toMatch(absoluteAssetMatch)
|
||||
})
|
||||
|
||||
test('new URL(`${dynamic}`, import.meta.url)', async () => {
|
||||
const dynamic1 = await page.textContent('.dynamic-import-meta-url-1')
|
||||
expect(dynamic1).toMatch(absoluteIconMatch)
|
||||
const dynamic2 = await page.textContent('.dynamic-import-meta-url-2')
|
||||
expect(dynamic2).toMatch(absoluteAssetMatch)
|
||||
})
|
||||
|
||||
test('new URL(`non-existent`, import.meta.url)', async () => {
|
||||
expect(await page.textContent('.non-existent-import-meta-url')).toMatch(
|
||||
'/non-existent'
|
||||
)
|
||||
})
|
||||
|
||||
test('inline style test', async () => {
|
||||
expect(await getBg('.inline-style')).toMatch(cssBgAssetMatch)
|
||||
expect(await getBg('.style-url-assets')).toMatch(cssBgAssetMatch)
|
||||
})
|
||||
|
||||
test('html import word boundary', async () => {
|
||||
expect(await page.textContent('.obj-import-express')).toMatch(
|
||||
'ignore object import prop'
|
||||
)
|
||||
expect(await page.textContent('.string-import-express')).toMatch('no load')
|
||||
})
|
||||
|
||||
test('relative path in html asset', async () => {
|
||||
expect(await page.textContent('.relative-js')).toMatch('hello')
|
||||
expect(await getColor('.relative-css')).toMatch('red')
|
||||
})
|
1
playground/assets/__tests__/runtime-base/vite.config.js
Normal file
1
playground/assets/__tests__/runtime-base/vite.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../../vite.config-runtime-base')
|
@ -9,6 +9,9 @@
|
||||
"preview": "vite preview",
|
||||
"dev:relative-base": "vite --config ./vite.config-relative-base.js dev",
|
||||
"build:relative-base": "vite --config ./vite.config-relative-base.js build",
|
||||
"preview:relative-base": "vite --config ./vite.config-relative-base.js preview"
|
||||
"preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
|
||||
"dev:dynamic-base": "vite --config ./vite.config-dynamic-base.js dev",
|
||||
"build:dynamic-base": "vite --config ./vite.config-dynamic-base.js build",
|
||||
"preview:dynamic-base": "vite --config ./vite.config-dynamic-base.js preview"
|
||||
}
|
||||
}
|
||||
|
58
playground/assets/vite.config-runtime-base.js
Normal file
58
playground/assets/vite.config-runtime-base.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
*/
|
||||
|
||||
const dynamicBaseAssetsCode = `
|
||||
globalThis.__toAssetUrl = url => '/' + url
|
||||
globalThis.__publicBase = '/'
|
||||
`
|
||||
|
||||
const baseConfig = require('./vite.config.js')
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
base: './', // overwrite the original base: '/foo/'
|
||||
build: {
|
||||
...baseConfig.build,
|
||||
outDir: 'dist',
|
||||
watch: false,
|
||||
minify: false,
|
||||
assetsInlineLimit: 0,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'entries/[name].js',
|
||||
chunkFileNames: 'chunks/[name].[hash].js',
|
||||
assetFileNames: 'other-assets/[name].[hash][extname]'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'dynamic-base-assets-globals',
|
||||
transformIndexHtml(_, ctx) {
|
||||
if (ctx.bundle) {
|
||||
// Only inject during build
|
||||
return [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: { type: 'module' },
|
||||
children: dynamicBaseAssetsCode
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
experimental: {
|
||||
buildAdvancedBaseOptions: {
|
||||
relative: true,
|
||||
assets: {
|
||||
url: '/',
|
||||
runtime: (url) => `globalThis.__toAssetUrl(${url})`
|
||||
},
|
||||
public: {
|
||||
url: '/',
|
||||
runtime: (url) => `globalThis.__publicBase+${url}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
playground/legacy/nested/index.html
Normal file
2
playground/legacy/nested/index.html
Normal file
@ -0,0 +1,2 @@
|
||||
<h1 id="nested html"></h1>
|
||||
<script type="module" src="../main.js"></script>
|
@ -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`
|
||||
}
|
||||
}
|
||||
|
@ -42,8 +42,8 @@ export async function serve(): Promise<{ close(): Promise<void> }> {
|
||||
})
|
||||
).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
|
||||
|
@ -214,8 +214,10 @@ export async function startDefaultServe(): Promise<void> {
|
||||
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'
|
||||
|
Loading…
Reference in New Issue
Block a user