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
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 19 additions & 30 deletions docs/guide/build.md
Original file line number Diff line number Diff line change
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.renderBuiltAssetUrl`.

```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: {
renderBuiltAssetUrl: (filename: string, importer: string) => {
if (path.extname(importer) === '.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}`
renderBuiltAssetUrl(filename: string, importer: string, { 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.
72 changes: 49 additions & 23 deletions packages/plugin-legacy/src/index.ts
Original file line number Diff line number Diff line change
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,65 @@ 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
export function toOutputFilePathInHtml(
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
filename: string,
type: 'asset' | 'public',
importer: string,
config: ResolvedConfig,
toRelative: (filename: string, importer: string) => string
): string {
const { renderBuiltAssetUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltAssetUrl) {
const result = renderBuiltAssetUrl(filename, importer, {
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
throw new Error(
`{ runtime: ${
result.runtime
} } is not supported for assets in ${path.extname(
importer
)} 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, importer)
} 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, importer: string) =>
getBaseInHTML(relativeUrlPath, config) + filename
return toOutputFilePathInHtml(filename, 'asset', htmlPath, config, toRelative)
}

// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
Expand Down
172 changes: 73 additions & 99 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, 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,83 @@ 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 type RenderBuiltAssetUrl = (
filename: string,
importer: string,
type: { type: 'asset' | 'public'; ssr: boolean }
) => string | { relative?: boolean; runtime?: string } | undefined

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 function toOutputFilePathInString(
filename: string,
type: 'asset' | 'public',
importer: string,
config: ResolvedConfig,
toRelative: (
filename: string,
importer: string
) => string | { runtime: string }
): string | { runtime: string } {
const { renderBuiltAssetUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltAssetUrl) {
const result = renderBuiltAssetUrl(filename, importer, {
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
return { runtime: result.runtime }
}
if (typeof result.relative === 'boolean') {
relative = true
}
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
} 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, importer)
}
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',
importer: string,
config: ResolvedConfig,
toRelative: (filename: string, importer: string) => string
): string {
const { renderBuiltAssetUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltAssetUrl) {
const result = renderBuiltAssetUrl(filename, importer, {
type,
ssr: !!config.build.ssr
})
if (typeof result === 'object') {
if (result.runtime) {
throw new Error(
`{ runtime: ${
result.runtime
} } is not supported for assets in ${path.extname(
importer
)} files: ${filename}`
)
}
if (typeof result.relative === 'boolean') {
relative = true
}
} 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, importer)
} else {
return config.base + filename
}
}

export const toOutputFilePathInCss = toOutputFilePathWithoutRuntime
export const toOutputFilePathInHtml = toOutputFilePathWithoutRuntime
Loading