feat: experimental.buildAdvancedBaseOptions (#8450)

This commit is contained in:
patak 2022-06-20 17:27:39 +02:00 committed by GitHub
parent 15ebe1e6df
commit 8ef733368f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 773 additions and 151 deletions

View File

@ -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.

View File

@ -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'

View File

@ -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)
}
]
}

View File

@ -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)))
}
}

View File

@ -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
}
}

View File

@ -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 += '/'
}

View File

@ -23,7 +23,10 @@ export type {
BuildOptions,
LibraryOptions,
LibraryFormats,
ResolvedBuildOptions
ResolvedBuildOptions,
BuildAdvancedBaseConfig,
ResolvedBuildAdvancedBaseConfig,
BuildAdvancedBaseOptions
} from './build'
export type {
PreviewOptions,

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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))

View File

@ -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!,
''

View File

@ -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) {

View File

@ -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
)

View File

@ -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) {

View File

@ -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) {

View File

@ -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
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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)
}

View File

@ -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,

View File

@ -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')
})

View File

@ -0,0 +1 @@
module.exports = require('../../vite.config-runtime-base')

View File

@ -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"
}
}

View 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}`
}
}
}
}

View File

@ -0,0 +1,2 @@
<h1 id="nested html"></h1>
<script type="module" src="../main.js"></script>

View File

@ -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`
}
}

View File

@ -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

View File

@ -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'