Skip to content

Commit

Permalink
feat: experimental.renderBuiltUrl (revised build base options) (#8762)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev committed Jun 27, 2022
1 parent d90409e commit 895a7d6
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 347 deletions.
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 = result.relative
}
} 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

0 comments on commit 895a7d6

Please sign in to comment.