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.renderBuiltUrl (revised build base options) #8762

Merged
merged 10 commits into from Jun 27, 2022
49 changes: 19 additions & 30 deletions docs/guide/build.md
Expand Up @@ -197,45 +197,34 @@ A user may choose to deploy in three different paths:
- 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`.
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.renderBuiltUrl`.

```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})`
},
experimental: {
renderBuiltUrl: (filename: string, { hostType: 'js' | 'css' | 'html' }) => {
if (hostType === 'js') {
return { runtime: `window.__toCdnUrl(${JSON.stringify(filename)})` }
} else {
return { relative: true }
}
}
}
```

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:
If the hashed assets and public files aren't deployed together, options for each group can be defined independently using asset `type` included in the third `context` param given to the function.

```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}`
renderBuiltUrl(filename: string, { hostType: 'js' | 'css' | 'html', type: 'public' | 'asset' }) {
if (type === 'public') {
return 'https://www.domain.com/' + filename
}
else if (path.extname(importer) === '.js') {
return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
}
else {
return 'https://cdn.domain.com/assets/' + filename
}
}
}
```

Any option that isn't defined in the `public` or `assets` entry will be inherited from the main `buildAdvancedBaseOptions` config.
78 changes: 55 additions & 23 deletions packages/plugin-legacy/src/index.ts
Expand Up @@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url'
import { build, normalizePath } from 'vite'
import MagicString from 'magic-string'
import type {
BuildAdvancedBaseOptions,
BuildOptions,
HtmlTagDescriptor,
Plugin,
Expand All @@ -32,38 +31,71 @@ async function loadBabel() {
return babel
}

function getBaseInHTML(
urlRelativePath: string,
baseOptions: BuildAdvancedBaseOptions,
config: ResolvedConfig
) {
// Duplicated from build.ts in Vite Core, at least while the feature is experimental
// We should later expose this helper for other plugins to use
function toOutputFilePathInHtml(
filename: string,
type: 'asset' | 'public',
hostId: string,
hostType: 'js' | 'css' | 'html',
config: ResolvedConfig,
toRelative: (filename: string, importer: string) => string
): string {
const { renderBuiltUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltUrl) {
const result = renderBuiltUrl(filename, {
hostId,
hostType,
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
throw new Error(
`{ runtime: "${result.runtime}" } is not supported for assets in ${hostType} files: ${filename}`
)
}
if (typeof result.relative === 'boolean') {
relative = true
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (result) {
return result
}
}
if (relative && !config.build.ssr) {
return toRelative(filename, hostId)
} else {
return config.base + filename
}
}
function getBaseInHTML(urlRelativePath: string, 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)
)
return config.base === './' || config.base === ''
? 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
const toRelative = (filename: string, hostId: string) =>
getBaseInHTML(relativeUrlPath, config) + filename
return toOutputFilePathInHtml(
filename,
'asset',
htmlPath,
'html',
config,
toRelative
)
}

// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
Expand Down
180 changes: 80 additions & 100 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, resolveBaseUrl, resolveConfig } from './config'
import { isDepsOptimizerEnabled, resolveConfig } from './config'
import { buildReporterPlugin } from './plugins/reporter'
import { buildEsbuildPlugin } from './plugins/esbuild'
import { terserPlugin } from './plugins/terser'
Expand Down Expand Up @@ -831,109 +831,89 @@ function injectSsrFlag<T extends Record<string, any>>(
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
export type RenderBuiltAssetUrl = (
filename: string,
type: {
type: 'asset' | 'public'
hostId: string
hostType: 'js' | 'css' | 'html'
ssr: boolean
}
) => string | { relative?: boolean; runtime?: string } | undefined

export function toOutputFilePathInString(
filename: string,
type: 'asset' | 'public',
hostId: string,
hostType: 'js' | 'css' | 'html',
config: ResolvedConfig,
toRelative: (
filename: string,
hostType: string
) => string | { runtime: string }
): string | { runtime: string } {
const { renderBuiltUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltUrl) {
const result = renderBuiltUrl(filename, {
hostId,
hostType,
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
return { runtime: result.runtime }
}
if (typeof result.relative === 'boolean') {
relative = result.relative
}
} else if (result) {
return result
}
}

return {
...resolved,
assets: resolveBuildBaseSpecificOptions(
baseConfig?.assets,
resolved,
isBuild,
logger,
'assets'
),
public: resolveBuildBaseSpecificOptions(
baseConfig?.public,
resolved,
isBuild,
logger,
'public'
)
if (relative && !config.build.ssr) {
return toRelative(filename, hostId)
}
return config.base + filename
}

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 }
export function toOutputFilePathWithoutRuntime(
filename: string,
type: 'asset' | 'public',
hostId: string,
hostType: 'js' | 'css' | 'html',
config: ResolvedConfig,
toRelative: (filename: string, hostId: string) => string
): string {
const { renderBuiltUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltUrl) {
const result = renderBuiltUrl(filename, {
hostId,
hostType,
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
throw new Error(
`{ runtime: "${result.runtime} }" is not supported for assets in ${hostType} files: ${filename}`
)
}
if (typeof result.relative === 'boolean') {
relative = result.relative
}
} else if (result) {
return result
}
}
return {
relative: options?.relative ?? parent.relative,
url: options?.url
? resolveBaseUrl(options?.url, isBuild, logger, urlConfigPath)
: parent.url,
runtime: options?.runtime ?? parent.runtime
if (relative && !config.build.ssr) {
return toRelative(filename, hostId)
} else {
return config.base + filename
}
}

export const toOutputFilePathInCss = toOutputFilePathWithoutRuntime
export const toOutputFilePathInHtml = toOutputFilePathWithoutRuntime