Skip to content

Commit e223f84

Browse files
authored
feat: build.modulePreload options (#9938)
1 parent 66c9058 commit e223f84

File tree

16 files changed

+411
-69
lines changed

16 files changed

+411
-69
lines changed

docs/config/build-options.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,55 @@ The transform is performed with esbuild and the value should be a valid [esbuild
1717

1818
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.
1919

20-
## build.polyfillModulePreload
20+
## build.modulePreload
2121

22-
- **Type:** `boolean`
22+
- **Type:** `boolean | { polyfill?: boolean, resolveDependencies?: ResolveModulePreloadDependenciesFn }`
2323
- **Default:** `true`
2424

25-
Whether to automatically inject [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill).
26-
27-
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:
25+
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:
2826

2927
```js
3028
import 'vite/modulepreload-polyfill'
3129
```
3230

3331
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.
3432

33+
The polyfill can be disabled using `{ polyfill: false }`.
34+
35+
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.
36+
37+
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`:
38+
39+
```ts
40+
type ResolveModulePreloadDependenciesFn = (
41+
url: string,
42+
deps: string[],
43+
context: {
44+
importer: string
45+
}
46+
) => (string | { runtime?: string })[]
47+
```
48+
49+
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.
50+
51+
```js
52+
modulePreload: {
53+
resolveDependencies: (filename, deps, { hostId, hostType }) => {
54+
return deps.filter(condition)
55+
}
56+
}
57+
```
58+
59+
The resolved dependency paths can be further modified using [`experimental.renderBuiltUrl`](../guide/build.md#advanced-base-options).
60+
61+
## build.polyfillModulePreload
62+
63+
- **Type:** `boolean`
64+
- **Default:** `true`
65+
- **Deprecated** use `build.modulePreload.polyfill` instead
66+
67+
Whether to automatically inject a [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill).
68+
3569
## build.outDir
3670
3771
- **Type:** `string`

packages/vite/src/node/build.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,15 @@ export interface BuildOptions {
7272
* whether to inject module preload polyfill.
7373
* Note: does not apply to library mode.
7474
* @default true
75+
* @deprecated use `modulePreload.polyfill` instead
7576
*/
7677
polyfillModulePreload?: boolean
78+
/**
79+
* Configure module preload
80+
* Note: does not apply to library mode.
81+
* @default true
82+
*/
83+
modulePreload?: boolean | ModulePreloadOptions
7784
/**
7885
* Directory relative from `root` where build output will be placed. If the
7986
* directory exists, it will be removed before the build.
@@ -228,16 +235,67 @@ export interface LibraryOptions {
228235

229236
export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife'
230237

231-
export type ResolvedBuildOptions = Required<BuildOptions>
238+
export interface ModulePreloadOptions {
239+
/**
240+
* Whether to inject a module preload polyfill.
241+
* Note: does not apply to library mode.
242+
* @default true
243+
*/
244+
polyfill?: boolean
245+
/**
246+
* Resolve the list of dependencies to preload for a given dynamic import
247+
* @experimental
248+
*/
249+
resolveDependencies?: ResolveModulePreloadDependenciesFn
250+
}
251+
export interface ResolvedModulePreloadOptions {
252+
polyfill: boolean
253+
resolveDependencies?: ResolveModulePreloadDependenciesFn
254+
}
255+
256+
export type ResolveModulePreloadDependenciesFn = (
257+
filename: string,
258+
deps: string[],
259+
context: {
260+
hostId: string
261+
hostType: 'html' | 'js'
262+
}
263+
) => string[]
264+
265+
export interface ResolvedBuildOptions
266+
extends Required<Omit<BuildOptions, 'polyfillModulePreload'>> {
267+
modulePreload: false | ResolvedModulePreloadOptions
268+
}
232269

233270
export function resolveBuildOptions(
234271
raw: BuildOptions | undefined,
235272
isBuild: boolean,
236273
logger: Logger
237274
): ResolvedBuildOptions {
275+
const deprecatedPolyfillModulePreload = raw?.polyfillModulePreload
276+
if (raw) {
277+
const { polyfillModulePreload, ...rest } = raw
278+
raw = rest
279+
if (deprecatedPolyfillModulePreload !== undefined) {
280+
logger.warn(
281+
'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.'
282+
)
283+
}
284+
if (
285+
deprecatedPolyfillModulePreload === false &&
286+
raw.modulePreload === undefined
287+
) {
288+
raw.modulePreload = { polyfill: false }
289+
}
290+
}
291+
292+
const modulePreload = raw?.modulePreload
293+
const defaultModulePreload = {
294+
polyfill: true
295+
}
296+
238297
const resolved: ResolvedBuildOptions = {
239298
target: 'modules',
240-
polyfillModulePreload: true,
241299
outDir: 'dist',
242300
assetsDir: 'assets',
243301
assetsInlineLimit: 4096,
@@ -266,7 +324,17 @@ export function resolveBuildOptions(
266324
warnOnError: true,
267325
exclude: [/node_modules/],
268326
...raw?.dynamicImportVarsOptions
269-
}
327+
},
328+
// Resolve to false | object
329+
modulePreload:
330+
modulePreload === false
331+
? false
332+
: typeof modulePreload === 'object'
333+
? {
334+
...defaultModulePreload,
335+
...modulePreload
336+
}
337+
: defaultModulePreload
270338
}
271339

272340
// handle special build targets
@@ -903,19 +971,16 @@ export type RenderBuiltAssetUrl = (
903971
}
904972
) => string | { relative?: boolean; runtime?: string } | undefined
905973

906-
export function toOutputFilePathInString(
974+
export function toOutputFilePathInJS(
907975
filename: string,
908976
type: 'asset' | 'public',
909977
hostId: string,
910978
hostType: 'js' | 'css' | 'html',
911979
config: ResolvedConfig,
912-
format: InternalModuleFormat,
913980
toRelative: (
914981
filename: string,
915982
hostType: string
916-
) => string | { runtime: string } = getToImportMetaURLBasedRelativePath(
917-
format
918-
)
983+
) => string | { runtime: string }
919984
): string | { runtime: string } {
920985
const { renderBuiltUrl } = config.experimental
921986
let relative = config.base === '' || config.base === './'
@@ -943,7 +1008,7 @@ export function toOutputFilePathInString(
9431008
return config.base + filename
9441009
}
9451010

946-
function getToImportMetaURLBasedRelativePath(
1011+
export function createToImportMetaURLBasedRelativeRuntime(
9471012
format: InternalModuleFormat
9481013
): (filename: string, importer: string) => { runtime: string } {
9491014
const toRelativePath = relativeUrlMechanisms[format]

packages/vite/src/node/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ import { resolveSSROptions } from './ssr'
6262

6363
const debug = createDebugger('vite:config')
6464

65-
export type { RenderBuiltAssetUrl } from './build'
65+
export type {
66+
RenderBuiltAssetUrl,
67+
ModulePreloadOptions,
68+
ResolvedModulePreloadOptions,
69+
ResolveModulePreloadDependenciesFn
70+
} from './build'
6671

6772
// NOTE: every export in this file is re-exported from ./index.ts so it will
6873
// be part of the public API.

packages/vite/src/node/plugins/asset.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import type {
1313
} from 'rollup'
1414
import MagicString from 'magic-string'
1515
import colors from 'picocolors'
16-
import { toOutputFilePathInString } from '../build'
16+
import {
17+
createToImportMetaURLBasedRelativeRuntime,
18+
toOutputFilePathInJS
19+
} from '../build'
1720
import type { Plugin } from '../plugin'
1821
import type { ResolvedConfig } from '../config'
1922
import { cleanUrl, getHash, normalizePath } from '../utils'
@@ -57,6 +60,10 @@ export function renderAssetUrlInJS(
5760
opts: NormalizedOutputOptions,
5861
code: string
5962
): MagicString | undefined {
63+
const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime(
64+
opts.format
65+
)
66+
6067
let match: RegExpExecArray | null
6168
let s: MagicString | undefined
6269

@@ -76,13 +83,13 @@ export function renderAssetUrlInJS(
7683
const file = getAssetFilename(hash, config) || ctx.getFileName(hash)
7784
chunk.viteMetadata.importedAssets.add(cleanUrl(file))
7885
const filename = file + postfix
79-
const replacement = toOutputFilePathInString(
86+
const replacement = toOutputFilePathInJS(
8087
filename,
8188
'asset',
8289
chunk.fileName,
8390
'js',
8491
config,
85-
opts.format
92+
toRelativeRuntime
8693
)
8794
const replacementString =
8895
typeof replacement === 'string'
@@ -100,13 +107,13 @@ export function renderAssetUrlInJS(
100107
s ||= new MagicString(code)
101108
const [full, hash] = match
102109
const publicUrl = publicAssetUrlMap.get(hash)!.slice(1)
103-
const replacement = toOutputFilePathInString(
110+
const replacement = toOutputFilePathInJS(
104111
publicUrl,
105112
'public',
106113
chunk.fileName,
107114
'js',
108115
config,
109-
opts.format
116+
toRelativeRuntime
110117
)
111118
const replacementString =
112119
typeof replacement === 'string'

packages/vite/src/node/plugins/html.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
581581
processedHtml.set(id, s.toString())
582582

583583
// inject module preload polyfill only when configured and needed
584+
const { modulePreload } = config.build
584585
if (
585-
config.build.polyfillModulePreload &&
586+
(modulePreload === true ||
587+
(typeof modulePreload === 'object' && modulePreload.polyfill)) &&
586588
(someScriptsAreAsync || someScriptsAreDefer)
587589
) {
588590
js = `import "${modulePreloadPolyfillId}";\n${js}`
@@ -627,14 +629,14 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
627629
})
628630

629631
const toPreloadTag = (
630-
chunk: OutputChunk,
632+
filename: string,
631633
toOutputPath: (filename: string) => string
632634
): HtmlTagDescriptor => ({
633635
tag: 'link',
634636
attrs: {
635637
rel: 'modulepreload',
636638
crossorigin: true,
637-
href: toOutputPath(chunk.fileName)
639+
href: toOutputPath(filename)
638640
}
639641
})
640642

@@ -726,15 +728,28 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
726728
// when not inlined, inject <script> for entry and modulepreload its dependencies
727729
// when inlined, discard entry chunk and inject <script> for everything in post-order
728730
const imports = getImportedChunks(chunk)
729-
const assetTags = canInlineEntry
730-
? imports.map((chunk) =>
731-
toScriptTag(chunk, toOutputAssetFilePath, isAsync)
732-
)
733-
: [
734-
toScriptTag(chunk, toOutputAssetFilePath, isAsync),
735-
...imports.map((i) => toPreloadTag(i, toOutputAssetFilePath))
736-
]
737-
731+
let assetTags: HtmlTagDescriptor[]
732+
if (canInlineEntry) {
733+
assetTags = imports.map((chunk) =>
734+
toScriptTag(chunk, toOutputAssetFilePath, isAsync)
735+
)
736+
} else {
737+
const { modulePreload } = config.build
738+
const resolveDependencies =
739+
typeof modulePreload === 'object' &&
740+
modulePreload.resolveDependencies
741+
const importsFileNames = imports.map((chunk) => chunk.fileName)
742+
const resolvedDeps = resolveDependencies
743+
? resolveDependencies(chunk.fileName, importsFileNames, {
744+
hostId: relativeUrlPath,
745+
hostType: 'html'
746+
})
747+
: importsFileNames
748+
assetTags = [
749+
toScriptTag(chunk, toOutputAssetFilePath, isAsync),
750+
...resolvedDeps.map((i) => toPreloadTag(i, toOutputAssetFilePath))
751+
]
752+
}
738753
assetTags.push(...getCssTagsForChunk(chunk, toOutputAssetFilePath))
739754

740755
result = injectToHead(result, assetTags)

0 commit comments

Comments
 (0)