feat: build.modulePreload options (#9938)

This commit is contained in:
patak 2022-09-24 16:09:40 +02:00 committed by GitHub
parent 66c90585e2
commit e223f84af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 409 additions and 67 deletions

View File

@ -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`

View File

@ -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<BuildOptions>
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<Omit<BuildOptions, 'polyfillModulePreload'>> {
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]

View File

@ -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.

View File

@ -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'

View File

@ -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 <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)

View File

@ -16,6 +16,7 @@ import {
import type { Plugin } from '../plugin'
import { getDepOptimizationConfig } from '../config'
import type { ResolvedConfig } from '../config'
import { toOutputFilePathInJS } from '../build'
import { genSourceMapUrl } from '../server/sourcemap'
import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer'
import { isCSSRequest, removedPureCssFilesCache } from './css'
@ -40,6 +41,11 @@ const dynamicImportPrefixRE = /import\s*\(/
const optimizedDepChunkRE = /\/chunk-[A-Z0-9]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z0-9]{8}\.js/
function toRelativePath(filename: string, importer: string) {
const relPath = path.relative(path.dirname(importer), filename)
return relPath.startsWith('.') ? relPath : `./${relPath}`
}
/**
* Helper for preloading CSS and direct imports of async chunks in parallel to
* the async chunk itself.
@ -124,16 +130,49 @@ function preload(
export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
const ssr = !!config.build.ssr
const isWorker = config.isWorker
const insertPreload = !(ssr || !!config.build.lib || isWorker)
const insertPreload = !(
ssr ||
!!config.build.lib ||
isWorker ||
config.build.modulePreload === false
)
const relativePreloadUrls = config.base === './' || config.base === ''
const resolveModulePreloadDependencies =
config.build.modulePreload && config.build.modulePreload.resolveDependencies
const renderBuiltUrl = config.experimental.renderBuiltUrl
const customModulePreloadPaths = !!(
resolveModulePreloadDependencies || renderBuiltUrl
)
const isRelativeBase = config.base === './' || config.base === ''
const optimizeModulePreloadRelativePaths =
isRelativeBase && !customModulePreloadPaths
const scriptRel = config.build.polyfillModulePreload
? `'modulepreload'`
: `(${detectScriptRel.toString()})()`
const assetsURL = relativePreloadUrls
? `function(dep,importerUrl) { return new URL(dep, importerUrl).href }`
: `function(dep) { return ${JSON.stringify(config.base)}+dep }`
const { modulePreload } = config.build
const scriptRel =
modulePreload && modulePreload.polyfill
? `'modulepreload'`
: `(${detectScriptRel.toString()})()`
// There are three different cases for the preload list format in __vitePreload
//
// __vitePreload(() => import(asyncChunk), [ ...deps... ])
//
// This is maintained to keep backwards compatibility as some users developed plugins
// using regex over this list to workaround the fact that module preload wasn't
// configurable.
const assetsURL = customModulePreloadPaths
? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used
// the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)`
// a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized.
`function(dep, importerUrl) { return dep.startsWith('.') ? new URL(dep, importerUrl).href : dep }`
: optimizeModulePreloadRelativePaths
? // If there isn't custom resolvers affecting the deps list, deps in the list are relative
// to the current chunk and are resolved to absolute URL by the __vitePreload helper itself.
// The importerUrl is passed as third parameter to __vitePreload in this case
`function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
: // If the base isn't relative, then the deps are relative to the projects `outDir` and the base
// is appendended inside __vitePreload too.
`function(dep) { return ${JSON.stringify(config.base)}+dep }`
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
return {
@ -258,7 +297,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
str().appendRight(
expEnd,
`,${isModernFlag}?"${preloadMarker}":void 0${
relativePreloadUrls ? ',import.meta.url' : ''
optimizeModulePreloadRelativePaths || customModulePreloadPaths
? ',import.meta.url'
: ''
})`
)
}
@ -383,7 +424,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
},
generateBundle({ format }, bundle) {
if (format !== 'es' || ssr || isWorker) {
if (
format !== 'es' ||
ssr ||
isWorker ||
config.build.modulePreload === false
) {
return
}
@ -423,7 +469,14 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
const deps: Set<string> = new Set()
let hasRemovedPureCssChunk = false
let normalizedFile: string | undefined = undefined
if (url) {
normalizedFile = path.posix.join(
path.posix.dirname(chunk.fileName),
url
)
const ownerFilename = chunk.fileName
// literal import - trace direct imports and add to deps
const analyzed: Set<string> = new Set<string>()
@ -458,10 +511,6 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
}
}
}
const normalizedFile = path.posix.join(
path.posix.dirname(chunk.fileName),
url
)
addDeps(normalizedFile)
}
@ -472,25 +521,71 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
}
if (markerStartPos > 0) {
// the dep list includes the main chunk, so only need to reload when there are actual other deps.
const depsArray =
deps.size > 1 ||
// main chunk is removed
(hasRemovedPureCssChunk && deps.size > 0)
? [...deps]
: []
let renderedDeps: string[]
if (normalizedFile && customModulePreloadPaths) {
const { modulePreload } = config.build
const resolveDependencies =
modulePreload && modulePreload.resolveDependencies
let resolvedDeps: string[]
if (resolveDependencies) {
// We can't let the user remove css deps as these aren't really preloads, they are just using
// the same mechanism as module preloads for this chunk
const cssDeps: string[] = []
const otherDeps: string[] = []
for (const dep of depsArray) {
;(dep.endsWith('.css') ? cssDeps : otherDeps).push(dep)
}
resolvedDeps = [
...resolveDependencies(normalizedFile, otherDeps, {
hostId: file,
hostType: 'js'
}),
...cssDeps
]
} else {
resolvedDeps = depsArray
}
renderedDeps = resolvedDeps.map((dep: string) => {
const replacement = toOutputFilePathInJS(
dep,
'asset',
chunk.fileName,
'js',
config,
toRelativePath
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement)
: replacement.runtime
return replacementString
})
} else {
renderedDeps = depsArray.map((d) =>
// Don't include the assets dir if the default asset file names
// are used, the path will be reconstructed by the import preload helper
JSON.stringify(
optimizeModulePreloadRelativePaths
? toRelativePath(d, file)
: d
)
)
}
s.overwrite(
markerStartPos,
markerStartPos + preloadMarkerWithQuote.length,
// the dep list includes the main chunk, so only need to reload when there are
// actual other deps. Don't include the assets dir if the default asset file names
// are used, the path will be reconstructed by the import preload helper
deps.size > 1 ||
// main chunk is removed
(hasRemovedPureCssChunk && deps.size > 0)
? `[${[...deps]
.map((d) =>
JSON.stringify(
relativePreloadUrls
? path.relative(path.dirname(file), d)
: d
)
)
.join(',')}]`
: `[]`,
`[${renderedDeps.join(',')}]`,
{ contentOnly: true }
)
rewroteMarkerStartPos.add(markerStartPos)

View File

@ -37,6 +37,7 @@ export async function resolvePlugins(
const buildPlugins = isBuild
? (await import('../build')).resolveBuildPlugins(config)
: { pre: [], post: [] }
const { modulePreload } = config.build
return [
isWatch ? ensureWatchPlugin() : null,
@ -44,7 +45,8 @@ export async function resolvePlugins(
preAliasPlugin(config),
aliasPlugin({ entries: config.resolve.alias }),
...prePlugins,
config.build.polyfillModulePreload
modulePreload === true ||
(typeof modulePreload === 'object' && modulePreload.polyfill)
? modulePreloadPolyfillPlugin(config)
: null,
...(isDepsOptimizerEnabled(config, false) ||

View File

@ -6,7 +6,11 @@ import type { Plugin } from '../plugin'
import type { ViteDevServer } from '../server'
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
import { cleanUrl, getHash, injectQuery, parseRequest } from '../utils'
import { onRollupWarning, toOutputFilePathInString } from '../build'
import {
createToImportMetaURLBasedRelativeRuntime,
onRollupWarning,
toOutputFilePathInJS
} from '../build'
import { getDepsOptimizer } from '../optimizer'
import { fileToUrl } from './asset'
@ -318,6 +322,10 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
)
}
if (code.match(workerAssetUrlRE) || code.includes('import.meta.url')) {
const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime(
outputOptions.format
)
let match: RegExpExecArray | null
s = new MagicString(code)
@ -328,13 +336,13 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
while ((match = workerAssetUrlRE.exec(code))) {
const [full, hash] = match
const filename = fileNameHash.get(hash)!
const replacement = toOutputFilePathInString(
const replacement = toOutputFilePathInJS(
filename,
'asset',
chunk.fileName,
'js',
config,
outputOptions.format
toRelativeRuntime
)
const replacementString =
typeof replacement === 'string'

View File

@ -0,0 +1,22 @@
import { describe, expect, test } from 'vitest'
import { browserLogs, isBuild, page, viteTestUrl } from '~utils'
test('should have no 404s', () => {
browserLogs.forEach((msg) => {
expect(msg).not.toMatch('404')
})
})
describe.runIf(isBuild)('build', () => {
test('dynamic import', async () => {
const appHtml = await page.content()
expect(appHtml).toMatch('This is <b>home</b> page.')
})
test('dynamic import with comments', async () => {
await page.goto(viteTestUrl + '/#/hello')
const html = await page.content()
expect(html).not.toMatch(/link rel="modulepreload"/)
expect(html).not.toMatch(/link rel="stylesheet"/)
})
})

View File

@ -0,0 +1 @@
module.exports = require('../../vite.config-preload-disabled')

View File

@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest'
import { browserLogs, isBuild, page, viteTestUrl } from '~utils'
test('should have no 404s', () => {
browserLogs.forEach((msg) => {
expect(msg).not.toMatch('404')
})
})
describe.runIf(isBuild)('build', () => {
test('dynamic import', async () => {
const appHtml = await page.content()
expect(appHtml).toMatch('This is <b>home</b> page.')
})
test('dynamic import with comments', async () => {
await page.goto(viteTestUrl + '/#/hello')
const html = await page.content()
expect(html).toMatch(
/link rel="modulepreload".*?href="http.*?\/Hello\.\w{8}\.js"/
)
expect(html).toMatch(/link rel="modulepreload".*?href="\/preloaded.js"/)
expect(html).toMatch(
/link rel="stylesheet".*?href="http.*?\/Hello\.\w{8}\.css"/
)
})
})

View File

@ -0,0 +1 @@
module.exports = require('../../vite.config-resolve-deps')

View File

@ -6,7 +6,15 @@
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
"preview": "vite preview"
"preview": "vite preview",
"dev:resolve-deps": "vite --config vite.config-resolve-deps.ts",
"build:resolve-deps": "vite build --config vite.config-resolve-deps.ts",
"debug:resolve-deps": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-resolve-deps.ts",
"preview:resolve-deps": "vite preview --config vite.config-resolve-deps.ts",
"dev:preload-disabled": "vite --config vite.config-preload-disabled.ts",
"build:preload-disabled": "vite build --config vite.config-preload-disabled.ts",
"debug:preload-disabled": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-preload-disabled.ts",
"preview:preload-disabled": "vite preview --config vite.config-preload-disabled.ts"
},
"dependencies": {
"vue": "^3.2.39",

View File

@ -0,0 +1 @@
console.log('preloaded')

View File

@ -0,0 +1,18 @@
import vuePlugin from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vuePlugin()],
build: {
minify: 'terser',
terserOptions: {
format: {
beautify: true
},
compress: {
passes: 3
}
},
modulePreload: false
}
})

View File

@ -0,0 +1,33 @@
import vuePlugin from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vuePlugin()],
build: {
minify: 'terser',
terserOptions: {
format: {
beautify: true
},
compress: {
passes: 3
}
},
modulePreload: {
resolveDependencies(filename, deps, { hostId, hostType }) {
if (filename.includes('Hello')) {
return [...deps, 'preloaded.js']
}
return deps
}
}
},
experimental: {
renderBuiltUrl(filename, { hostId, hostType }) {
if (filename.includes('preloaded')) {
return { runtime: `""+${JSON.stringify('/' + filename)}` }
}
return { relative: true }
}
}
})