Skip to content

Commit

Permalink
feat: experimental.buildAdvancedBaseOptions (#8450)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev authored Jun 20, 2022
1 parent 15ebe1e commit 8ef7333
Show file tree
Hide file tree
Showing 30 changed files with 773 additions and 151 deletions.
58 changes: 58 additions & 0 deletions docs/guide/build.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
}
}
Loading

0 comments on commit 8ef7333

Please sign in to comment.