Skip to content

Commit 8ef7333

Browse files
authored
feat: experimental.buildAdvancedBaseOptions (#8450)
1 parent 15ebe1e commit 8ef7333

30 files changed

+773
-151
lines changed

docs/guide/build.md

+58
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ JS-imported asset URLs, CSS `url()` references, and asset references in your `.h
2727

2828
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).
2929

30+
For advanced base path control, check out [Advanced Base Options](#advanced-base-options).
31+
3032
## Customizing the Build
3133

3234
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:
181183
}
182184
}
183185
```
186+
187+
## Advanced Base Options
188+
189+
::: warning
190+
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.
191+
:::
192+
193+
For advanced use cases, the deployed assets and public files may be in different paths, for example to use different cache strategies.
194+
A user may choose to deploy in three different paths:
195+
196+
- The generated entry HTML files (which may be processed during SSR)
197+
- The generated hashed assets (JS, CSS, and other file types like images)
198+
- The copied [public files](assets.md#the-public-directory)
199+
200+
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`.
201+
202+
```js
203+
experimental: {
204+
buildAdvancedBaseOptions: {
205+
// Same as base: './'
206+
// type: boolean, default: false
207+
relative: true
208+
// Static base
209+
// type: string, default: undefined
210+
url: 'https:/cdn.domain.com/'
211+
// Dynamic base to be used for paths inside JS
212+
// type: (url: string) => string, default: undefined
213+
runtime: (url: string) => `window.__toCdnUrl(${url})`
214+
},
215+
}
216+
```
217+
218+
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`.
219+
220+
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).
221+
222+
If the hashed assets and public files aren't deployed together, options for each group can be defined independently:
223+
224+
```js
225+
experimental: {
226+
buildAdvancedBaseOptions: {
227+
assets: {
228+
relative: true
229+
url: 'https:/cdn.domain.com/assets',
230+
runtime: (url: string) => `window.__assetsPath(${url})`
231+
},
232+
public: {
233+
relative: false
234+
url: 'https:/www.domain.com/',
235+
runtime: (url: string) => `window.__publicPath + ${url}`
236+
}
237+
}
238+
}
239+
```
240+
241+
Any option that isn't defined in the `public` or `assets` entry will be inherited from the main `buildAdvancedBaseOptions` config.

packages/plugin-legacy/src/index.ts

+52-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import path from 'node:path'
33
import { createHash } from 'node:crypto'
44
import { createRequire } from 'node:module'
55
import { fileURLToPath } from 'node:url'
6-
import { build } from 'vite'
6+
import { build, normalizePath } from 'vite'
77
import MagicString from 'magic-string'
88
import type {
9+
BuildAdvancedBaseOptions,
910
BuildOptions,
1011
HtmlTagDescriptor,
1112
Plugin,
@@ -31,6 +32,40 @@ async function loadBabel() {
3132
return babel
3233
}
3334

35+
function getBaseInHTML(
36+
urlRelativePath: string,
37+
baseOptions: BuildAdvancedBaseOptions,
38+
config: ResolvedConfig
39+
) {
40+
// Prefer explicit URL if defined for linking to assets and public files from HTML,
41+
// even when base relative is specified
42+
return (
43+
baseOptions.url ??
44+
(baseOptions.relative
45+
? path.posix.join(
46+
path.posix.relative(urlRelativePath, '').slice(0, -2),
47+
'./'
48+
)
49+
: config.base)
50+
)
51+
}
52+
53+
function getAssetsBase(urlRelativePath: string, config: ResolvedConfig) {
54+
return getBaseInHTML(
55+
urlRelativePath,
56+
config.experimental.buildAdvancedBaseOptions.assets,
57+
config
58+
)
59+
}
60+
function toAssetPathFromHtml(
61+
filename: string,
62+
htmlPath: string,
63+
config: ResolvedConfig
64+
): string {
65+
const relativeUrlPath = normalizePath(path.relative(config.root, htmlPath))
66+
return getAssetsBase(relativeUrlPath, config) + filename
67+
}
68+
3469
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
3570
// DO NOT ALTER THIS CONTENT
3671
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[] {
355390
const modernPolyfillFilename = facadeToModernPolyfillMap.get(
356391
chunk.facadeModuleId
357392
)
393+
358394
if (modernPolyfillFilename) {
359395
tags.push({
360396
tag: 'script',
361397
attrs: {
362398
type: 'module',
363399
crossorigin: true,
364-
src: `${config.base}${modernPolyfillFilename}`
400+
src: toAssetPathFromHtml(
401+
modernPolyfillFilename,
402+
chunk.facadeModuleId!,
403+
config
404+
)
365405
}
366406
})
367407
} else if (modernPolyfills.size) {
@@ -393,7 +433,11 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
393433
nomodule: true,
394434
crossorigin: true,
395435
id: legacyPolyfillId,
396-
src: `${config.base}${legacyPolyfillFilename}`
436+
src: toAssetPathFromHtml(
437+
legacyPolyfillFilename,
438+
chunk.facadeModuleId!,
439+
config
440+
)
397441
},
398442
injectTo: 'body'
399443
})
@@ -409,7 +453,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
409453
)
410454
if (legacyEntryFilename) {
411455
// `assets/foo.js` means importing "named register" in SystemJS
412-
const nonBareBase = config.base === '' ? './' : config.base
413456
tags.push({
414457
tag: 'script',
415458
attrs: {
@@ -419,7 +462,11 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
419462
// script content will stay consistent - which allows using a constant
420463
// hash value for CSP.
421464
id: legacyEntryId,
422-
'data-src': nonBareBase + legacyEntryFilename
465+
'data-src': toAssetPathFromHtml(
466+
legacyEntryFilename,
467+
chunk.facadeModuleId!,
468+
config
469+
)
423470
},
424471
children: systemJSInlineCode,
425472
injectTo: 'body'

packages/plugin-react/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ declare module 'vite' {
9090

9191
export default function viteReact(opts: Options = {}): PluginOption[] {
9292
// Provide default values for Rollup compat.
93-
let base = '/'
93+
let devBase = '/'
9494
let resolvedCacheDir: string
9595
let filter = createFilter(opts.include, opts.exclude)
9696
let isProduction = true
@@ -129,7 +129,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
129129
}
130130
},
131131
configResolved(config) {
132-
base = config.base
132+
devBase = config.base
133133
projectRoot = config.root
134134
resolvedCacheDir = normalizePath(path.resolve(config.cacheDir))
135135
filter = createFilter(opts.include, opts.exclude, {
@@ -365,7 +365,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
365365
{
366366
tag: 'script',
367367
attrs: { type: 'module' },
368-
children: preambleCode.replace(`__BASE__`, base)
368+
children: preambleCode.replace(`__BASE__`, devBase)
369369
}
370370
]
371371
}

packages/plugin-vue/src/template.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ export function resolveTemplateCompilerOptions(
116116
// relative paths directly to absolute paths without incurring an extra import
117117
// request
118118
if (filename.startsWith(options.root)) {
119+
const devBase = options.devServer.config.base
119120
assetUrlOptions = {
120121
base:
121122
(options.devServer.config.server?.origin ?? '') +
122-
options.devServer.config.base +
123+
devBase +
123124
slash(path.relative(options.root, path.dirname(filename)))
124125
}
125126
}

packages/vite/src/node/build.ts

+113-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { RollupCommonJSOptions } from 'types/commonjs'
2222
import type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars'
2323
import type { TransformOptions } from 'esbuild'
2424
import type { InlineConfig, ResolvedConfig } from './config'
25-
import { isDepsOptimizerEnabled, resolveConfig } from './config'
25+
import { isDepsOptimizerEnabled, resolveBaseUrl, resolveConfig } from './config'
2626
import { buildReporterPlugin } from './plugins/reporter'
2727
import { buildEsbuildPlugin } from './plugins/esbuild'
2828
import { terserPlugin } from './plugins/terser'
@@ -229,7 +229,11 @@ export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife'
229229

230230
export type ResolvedBuildOptions = Required<BuildOptions>
231231

232-
export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions {
232+
export function resolveBuildOptions(
233+
raw: BuildOptions | undefined,
234+
isBuild: boolean,
235+
logger: Logger
236+
): ResolvedBuildOptions {
233237
const resolved: ResolvedBuildOptions = {
234238
target: 'modules',
235239
polyfillModulePreload: true,
@@ -826,3 +830,110 @@ function injectSsrFlag<T extends Record<string, any>>(
826830
): T & { ssr: boolean } {
827831
return { ...(options ?? {}), ssr: true } as T & { ssr: boolean }
828832
}
833+
834+
/*
835+
* If defined, these functions will be called for assets and public files
836+
* paths which are generated in JS assets. Examples:
837+
*
838+
* assets: { runtime: (url: string) => `window.__assetsPath(${url})` }
839+
* public: { runtime: (url: string) => `window.__publicPath + ${url}` }
840+
*
841+
* For assets and public files paths in CSS or HTML, the corresponding
842+
* `assets.url` and `public.url` base urls or global base will be used.
843+
*
844+
* When using relative base, the assets.runtime function isn't needed as
845+
* all the asset paths will be computed using import.meta.url
846+
* The public.runtime function is still useful if the public files aren't
847+
* deployed in the same base as the hashed assets
848+
*/
849+
850+
export interface BuildAdvancedBaseOptions {
851+
/**
852+
* Relative base. If true, every generated URL is relative and the dist folder
853+
* can be deployed to any base or subdomain. Use this option when the base
854+
* is unkown at build time
855+
* @default false
856+
*/
857+
relative?: boolean
858+
url?: string
859+
runtime?: (filename: string) => string
860+
}
861+
862+
export type BuildAdvancedBaseConfig = BuildAdvancedBaseOptions & {
863+
/**
864+
* Base for assets and public files in case they should be different
865+
*/
866+
assets?: string | BuildAdvancedBaseOptions
867+
public?: string | BuildAdvancedBaseOptions
868+
}
869+
870+
export type ResolvedBuildAdvancedBaseConfig = BuildAdvancedBaseOptions & {
871+
assets: BuildAdvancedBaseOptions
872+
public: BuildAdvancedBaseOptions
873+
}
874+
875+
/**
876+
* Resolve base. Note that some users use Vite to build for non-web targets like
877+
* electron or expects to deploy
878+
*/
879+
export function resolveBuildAdvancedBaseConfig(
880+
baseConfig: BuildAdvancedBaseConfig | undefined,
881+
resolvedBase: string,
882+
isBuild: boolean,
883+
logger: Logger
884+
): ResolvedBuildAdvancedBaseConfig {
885+
baseConfig ??= {}
886+
887+
const relativeBaseShortcut = resolvedBase === '' || resolvedBase === './'
888+
889+
const resolved = {
890+
relative: baseConfig?.relative ?? relativeBaseShortcut,
891+
url: baseConfig?.url
892+
? resolveBaseUrl(
893+
baseConfig?.url,
894+
isBuild,
895+
logger,
896+
'experimental.buildAdvancedBaseOptions.url'
897+
)
898+
: undefined,
899+
runtime: baseConfig?.runtime
900+
}
901+
902+
return {
903+
...resolved,
904+
assets: resolveBuildBaseSpecificOptions(
905+
baseConfig?.assets,
906+
resolved,
907+
isBuild,
908+
logger,
909+
'assets'
910+
),
911+
public: resolveBuildBaseSpecificOptions(
912+
baseConfig?.public,
913+
resolved,
914+
isBuild,
915+
logger,
916+
'public'
917+
)
918+
}
919+
}
920+
921+
function resolveBuildBaseSpecificOptions(
922+
options: BuildAdvancedBaseOptions | string | undefined,
923+
parent: BuildAdvancedBaseOptions,
924+
isBuild: boolean,
925+
logger: Logger,
926+
optionName: string
927+
): BuildAdvancedBaseOptions {
928+
const urlConfigPath = `experimental.buildAdvancedBaseOptions.${optionName}.url`
929+
if (typeof options === 'string') {
930+
options = { url: options }
931+
}
932+
return {
933+
relative: options?.relative ?? parent.relative,
934+
url: options?.url
935+
? resolveBaseUrl(options?.url, isBuild, logger, urlConfigPath)
936+
: parent.url,
937+
runtime: options?.runtime ?? parent.runtime
938+
}
939+
}

0 commit comments

Comments
 (0)