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: build.modulePreload options #9938

Merged
merged 9 commits into from Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 39 additions & 5 deletions docs/config/build-options.md
Expand Up @@ -17,21 +17,55 @@ 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'
```

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 [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill).
patak-dev marked this conversation as resolved.
Show resolved Hide resolved

## build.outDir

- **Type:** `string`
Expand Down
73 changes: 64 additions & 9 deletions packages/vite/src/node/build.ts
Expand Up @@ -73,8 +73,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.
Expand Down Expand Up @@ -229,16 +236,60 @@ export interface LibraryOptions {

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

export type ResolvedBuildOptions = Required<BuildOptions>
export interface ModulePreloadOptions {
/**
* whether to inject module preload polyfill.
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
* Note: does not apply to library mode.
* @default true
*/
polyfill?: boolean
/**
* Resolve the list of dependencies to preload for
* a given dynamic import
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
* @experimental
*/
resolveDependencies?: ResolveModulePreloadDependenciesFn
}

export type ResolveModulePreloadDependenciesFn = (
filename: string,
deps: string[],
context: {
hostId: string
hostType: 'html' | 'js'
}
) => string[]

export type ResolvedBuildOptions = Required<
Omit<BuildOptions, 'polyfillModulePreload'>
>

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 resolved: ResolvedBuildOptions = {
target: 'modules',
polyfillModulePreload: true,
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
Expand Down Expand Up @@ -267,7 +318,14 @@ export function resolveBuildOptions(
warnOnError: true,
exclude: [/node_modules/],
...raw?.dynamicImportVarsOptions
}
},
modulePreload:
typeof modulePreload === 'object'
? {
polyfill: true,
...modulePreload
}
: modulePreload ?? true
}

// handle special build targets
Expand Down Expand Up @@ -905,19 +963,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 === './'
Expand Down Expand Up @@ -945,7 +1000,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]
Expand Down
6 changes: 5 additions & 1 deletion packages/vite/src/node/config.ts
Expand Up @@ -60,7 +60,11 @@ import { resolveSSROptions } from './ssr'

const debug = createDebugger('vite:config')

export type { RenderBuiltAssetUrl } from './build'
export type {
RenderBuiltAssetUrl,
ModulePreloadOptions,
ResolveModulePreloadDependenciesFn
} from './build'

// NOTE: every export in this file is re-exported from ./index.ts so it will
// be part of the public API.
Expand Down
17 changes: 12 additions & 5 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -12,7 +12,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'
Expand Down Expand Up @@ -51,6 +54,10 @@ export function renderAssetUrlInJS(
opts: NormalizedOutputOptions,
code: string
): MagicString | undefined {
const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime(
opts.format
)

let match: RegExpExecArray | null
let s: MagicString | undefined

Expand All @@ -70,13 +77,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'
Expand All @@ -94,13 +101,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'
Expand Down
39 changes: 27 additions & 12 deletions packages/vite/src/node/plugins/html.ts
Expand Up @@ -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}`
Expand Down Expand Up @@ -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)
}
})

Expand Down Expand Up @@ -726,15 +728,28 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
// when not inlined, inject <script> for entry and modulepreload its dependencies
// when inlined, discard entry chunk and inject <script> for everything in post-order
const imports = getImportedChunks(chunk)
const assetTags = canInlineEntry
? imports.map((chunk) =>
toScriptTag(chunk, toOutputAssetFilePath, isAsync)
)
: [
toScriptTag(chunk, toOutputAssetFilePath, isAsync),
...imports.map((i) => toPreloadTag(i, toOutputAssetFilePath))
]

let assetTags: HtmlTagDescriptor[]
if (canInlineEntry) {
assetTags = imports.map((chunk) =>
toScriptTag(chunk, toOutputAssetFilePath, isAsync)
)
} else {
const { modulePreload } = config.build
const resolveDependencies =
typeof modulePreload === 'object' &&
modulePreload.resolveDependencies
const importsFileNames = imports.map((chunk) => chunk.fileName)
const resolvedDeps = resolveDependencies
? resolveDependencies(chunk.fileName, importsFileNames, {
hostId: relativeUrlPath,
hostType: 'html'
})
: importsFileNames
assetTags = [
toScriptTag(chunk, toOutputAssetFilePath, isAsync),
...resolvedDeps.map((i) => toPreloadTag(i, toOutputAssetFilePath))
]
}
assetTags.push(...getCssTagsForChunk(chunk, toOutputAssetFilePath))

result = injectToHead(result, assetTags)
Expand Down