diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 291135045560ce..42e12d03b16cb2 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -17,14 +17,12 @@ The transform is performed with esbuild and the value should be a valid [esbuild Note the build will fail if the code contains features that cannot be safely transpiled by esbuild. See [esbuild docs](https://esbuild.github.io/content-types/#javascript) for more details. -## build.polyfillModulePreload +## build.modulePreload -- **Type:** `boolean` +- **Type:** `boolean | { polyfill?: boolean, resolveDependencies?: ResolveModulePreloadDependenciesFn }` - **Default:** `true` -Whether to automatically inject [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill). - -If set to `true`, the polyfill is auto injected into the proxy module of each `index.html` entry. If the build is configured to use a non-html custom entry via `build.rollupOptions.input`, then it is necessary to manually import the polyfill in your custom entry: +By default, a [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill) is automatically injected. The polyfill is auto injected into the proxy module of each `index.html` entry. If the build is configured to use a non-HTML custom entry via `build.rollupOptions.input`, then it is necessary to manually import the polyfill in your custom entry: ```js import 'vite/modulepreload-polyfill' @@ -32,6 +30,42 @@ import 'vite/modulepreload-polyfill' Note: the polyfill does **not** apply to [Library Mode](/guide/build#library-mode). If you need to support browsers without native dynamic import, you should probably avoid using it in your library. +The polyfill can be disabled using `{ polyfill: false }`. + +The list of chunks to preload for each dynamic import is computed by Vite. By default, an absolute path including the `base` will be used when loading these dependencies. If the `base` is relative (`''` or `'./'`), `import.meta.url` is used at runtime to avoid absolute paths that depend on the final deployed base. + +There is experimental support for fine grained control over the dependencies list and their paths using the `resolveDependencies` function. It expects a function of type `ResolveModulePreloadDependenciesFn`: + +```ts +type ResolveModulePreloadDependenciesFn = ( + url: string, + deps: string[], + context: { + importer: string + } +) => (string | { runtime?: string })[] +``` + +The `resolveDependencies` function will be called for each dynamic import with a list of the chunks it depends on, and it will also be called for each chunk imported in entry HTML files. A new dependencies array can be returned with these filtered or more dependencies injected, and their paths modified. The `deps` paths are relative to the `build.outDir`. Returning a relative path to the `hostId` for `hostType === 'js'` is allowed, in which case `new URL(dep, import.meta.url)` is used to get an absolute path when injecting this module preload in the HTML head. + +```js +modulePreload: { + resolveDependencies: (filename, deps, { hostId, hostType }) => { + return deps.filter(condition) + } +} +``` + +The resolved dependency paths can be further modified using [`experimental.renderBuiltUrl`](../guide/build.md#advanced-base-options). + +## build.polyfillModulePreload + +- **Type:** `boolean` +- **Default:** `true` +- **Deprecated** use `build.modulePreload.polyfill` instead + +Whether to automatically inject a [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill). + ## build.outDir - **Type:** `string` diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 41b5d0524aad04..3ef16ec019c6d5 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -72,8 +72,15 @@ export interface BuildOptions { * whether to inject module preload polyfill. * Note: does not apply to library mode. * @default true + * @deprecated use `modulePreload.polyfill` instead */ polyfillModulePreload?: boolean + /** + * Configure module preload + * Note: does not apply to library mode. + * @default true + */ + modulePreload?: boolean | ModulePreloadOptions /** * Directory relative from `root` where build output will be placed. If the * directory exists, it will be removed before the build. @@ -228,16 +235,67 @@ export interface LibraryOptions { export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' -export type ResolvedBuildOptions = Required +export interface ModulePreloadOptions { + /** + * Whether to inject a module preload polyfill. + * Note: does not apply to library mode. + * @default true + */ + polyfill?: boolean + /** + * Resolve the list of dependencies to preload for a given dynamic import + * @experimental + */ + resolveDependencies?: ResolveModulePreloadDependenciesFn +} +export interface ResolvedModulePreloadOptions { + polyfill: boolean + resolveDependencies?: ResolveModulePreloadDependenciesFn +} + +export type ResolveModulePreloadDependenciesFn = ( + filename: string, + deps: string[], + context: { + hostId: string + hostType: 'html' | 'js' + } +) => string[] + +export interface ResolvedBuildOptions + extends Required> { + modulePreload: false | ResolvedModulePreloadOptions +} export function resolveBuildOptions( raw: BuildOptions | undefined, isBuild: boolean, logger: Logger ): ResolvedBuildOptions { + const deprecatedPolyfillModulePreload = raw?.polyfillModulePreload + if (raw) { + const { polyfillModulePreload, ...rest } = raw + raw = rest + if (deprecatedPolyfillModulePreload !== undefined) { + logger.warn( + 'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.' + ) + } + if ( + deprecatedPolyfillModulePreload === false && + raw.modulePreload === undefined + ) { + raw.modulePreload = { polyfill: false } + } + } + + const modulePreload = raw?.modulePreload + const defaultModulePreload = { + polyfill: true + } + const resolved: ResolvedBuildOptions = { target: 'modules', - polyfillModulePreload: true, outDir: 'dist', assetsDir: 'assets', assetsInlineLimit: 4096, @@ -266,7 +324,17 @@ export function resolveBuildOptions( warnOnError: true, exclude: [/node_modules/], ...raw?.dynamicImportVarsOptions - } + }, + // Resolve to false | object + modulePreload: + modulePreload === false + ? false + : typeof modulePreload === 'object' + ? { + ...defaultModulePreload, + ...modulePreload + } + : defaultModulePreload } // handle special build targets @@ -903,19 +971,16 @@ export type RenderBuiltAssetUrl = ( } ) => string | { relative?: boolean; runtime?: string } | undefined -export function toOutputFilePathInString( +export function toOutputFilePathInJS( filename: string, type: 'asset' | 'public', hostId: string, hostType: 'js' | 'css' | 'html', config: ResolvedConfig, - format: InternalModuleFormat, toRelative: ( filename: string, hostType: string - ) => string | { runtime: string } = getToImportMetaURLBasedRelativePath( - format - ) + ) => string | { runtime: string } ): string | { runtime: string } { const { renderBuiltUrl } = config.experimental let relative = config.base === '' || config.base === './' @@ -943,7 +1008,7 @@ export function toOutputFilePathInString( return config.base + filename } -function getToImportMetaURLBasedRelativePath( +export function createToImportMetaURLBasedRelativeRuntime( format: InternalModuleFormat ): (filename: string, importer: string) => { runtime: string } { const toRelativePath = relativeUrlMechanisms[format] diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a760a06fb86601..806f0f8275de4f 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -62,7 +62,12 @@ import { resolveSSROptions } from './ssr' const debug = createDebugger('vite:config') -export type { RenderBuiltAssetUrl } from './build' +export type { + RenderBuiltAssetUrl, + ModulePreloadOptions, + ResolvedModulePreloadOptions, + ResolveModulePreloadDependenciesFn +} from './build' // NOTE: every export in this file is re-exported from ./index.ts so it will // be part of the public API. diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index db2f9d7a7cb613..3839b54ef3365b 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -13,7 +13,10 @@ import type { } from 'rollup' import MagicString from 'magic-string' import colors from 'picocolors' -import { toOutputFilePathInString } from '../build' +import { + createToImportMetaURLBasedRelativeRuntime, + toOutputFilePathInJS +} from '../build' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { cleanUrl, getHash, normalizePath } from '../utils' @@ -57,6 +60,10 @@ export function renderAssetUrlInJS( opts: NormalizedOutputOptions, code: string ): MagicString | undefined { + const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( + opts.format + ) + let match: RegExpExecArray | null let s: MagicString | undefined @@ -76,13 +83,13 @@ export function renderAssetUrlInJS( const file = getAssetFilename(hash, config) || ctx.getFileName(hash) chunk.viteMetadata.importedAssets.add(cleanUrl(file)) const filename = file + postfix - const replacement = toOutputFilePathInString( + const replacement = toOutputFilePathInJS( filename, 'asset', chunk.fileName, 'js', config, - opts.format + toRelativeRuntime ) const replacementString = typeof replacement === 'string' @@ -100,13 +107,13 @@ export function renderAssetUrlInJS( s ||= new MagicString(code) const [full, hash] = match const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) - const replacement = toOutputFilePathInString( + const replacement = toOutputFilePathInJS( publicUrl, 'public', chunk.fileName, 'js', config, - opts.format + toRelativeRuntime ) const replacementString = typeof replacement === 'string' diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index f09c2a6fcd89b0..5285fab26cd0b4 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -581,8 +581,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { processedHtml.set(id, s.toString()) // inject module preload polyfill only when configured and needed + const { modulePreload } = config.build if ( - config.build.polyfillModulePreload && + (modulePreload === true || + (typeof modulePreload === 'object' && modulePreload.polyfill)) && (someScriptsAreAsync || someScriptsAreDefer) ) { js = `import "${modulePreloadPolyfillId}";\n${js}` @@ -627,14 +629,14 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }) const toPreloadTag = ( - chunk: OutputChunk, + filename: string, toOutputPath: (filename: string) => string ): HtmlTagDescriptor => ({ tag: 'link', attrs: { rel: 'modulepreload', crossorigin: true, - href: toOutputPath(chunk.fileName) + href: toOutputPath(filename) } }) @@ -726,15 +728,28 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // when not inlined, inject