Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental.buildAdvancedBaseOptions #8450

Merged
merged 17 commits into from Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/guide/build.md
Expand Up @@ -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`:
Expand Down Expand Up @@ -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.
57 changes: 52 additions & 5 deletions packages/plugin-legacy/src/index.ts
Expand Up @@ -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,
Expand All @@ -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()}}();`
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'
})
Expand All @@ -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: {
Expand All @@ -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'
Expand Down
6 changes: 3 additions & 3 deletions packages/plugin-react/src/index.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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)
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-vue/src/template.ts
Expand Up @@ -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)))
}
}
Expand Down
115 changes: 113 additions & 2 deletions packages/vite/src/node/build.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}