[Vite]Vite-legacy插件原理了解
兼容低版本浏览器用它就对了
作用
- 检测旧浏览器:插件需要能够检测到用户的浏览器是否需要转换代码。这通常是通过用户代理字符串来实现的。
- 代码转换:对于需要支持的旧浏览器,插件会使用Babel等工具将ES6+代码转换为ES5代码。
- Polyfills注入:为了支持旧浏览器中不存在的功能,插件会注入必要的polyfills。
- 配置调整:插件可能需要调整Vite的配置,例如修改入口文件,以便为旧浏览器生成额外的构建版本。
- 条件加载:在构建过程中,插件会生成多个版本的资源,包括一个为现代浏览器优化的版本和一个为旧浏览器优化的版本。在服务端,它将根据用户的浏览器类型来提供相应的资源。
- 服务端渲染:在某些情况下,插件可能还会涉及到服务端渲染(SSR),以进一步优化旧浏览器的兼容性。
为什么它会增加大量的打包时间?
- 额外的转换步骤:插件需要将现代JavaScript代码转换为ES5,这需要额外的处理时间。Babel等工具在转换过程中需要解析、转换和生成代码。
- 多版本构建:为了支持旧浏览器,插件需要生成额外的构建版本。这意味着同样的代码可能需要被打包多次,一次为现代浏览器,一次或多次为旧浏览器。
- Polyfills的加载:插件需要加载额外的polyfills来支持旧浏览器中缺失的功能,这会增加最终打包文件的大小,并且可能需要额外的时间来解析和应用这些polyfills。
- 条件加载逻辑:服务端需要根据用户的浏览器类型来决定加载哪个版本的资源,这可能涉及到额外的逻辑处理,从而增加服务端的响应时间。
- 资源分割:为了优化性能,现代前端构建工具通常会对代码进行分割。但是,如果
plugin-legacy需要为旧浏览器生成额外的资源,这可能会导致更多的分割和更多的处理时间。 - 依赖管理:如果插件需要处理或解决依赖之间的兼容性问题,这也会增加处理时间。
- 构建缓存:Vite的快速开发体验部分依赖于其构建缓存。但是,如果
plugin-legacy引入了额外的转换步骤,可能会影响缓存的效率。 - 服务端渲染(SSR):如果插件涉及到SSR,这可能会增加额外的计算和渲染时间。
- 测试和验证:为了保证转换后的代码在旧浏览器中正确运行,插件可能需要进行额外的测试和验证,这也会增加时间。
源码地址
解读:
这段代码是 Vite 的 viteLegacyPlugin 插件的实现部分,它用于支持旧版浏览器。下面是代码的主要组成部分和功能的解释:
-
导入依赖:代码开始部分导入了所需的 Node.js 内置模块和第三方库,如
path、crypto、module、url、magic-string等,以及 Vite 和 Rollup 的类型定义。 -
加载 Babel:
loadBabel函数用于按需加载 Babel,以避免在开发过程中使用时的额外负担。 -
Browserslist 配置:使用
browserslist来确定目标浏览器,并据此决定需要包含哪些 polyfills。 -
插件选项:
Options类型定义了插件的配置选项,如targets、modernTargets、polyfills等。 -
生成插件数组:
viteLegacyPlugin函数返回一个插件数组,这些插件在 Vite 构建过程中执行不同的任务。 -
配置插件:
legacyConfigPlugin插件在 Vite 配置阶段运行,它可能修改 Vite 的构建目标和定义环境变量。 -
生成 Bundle 插件:
legacyGenerateBundlePlugin在 Vite 生成 Bundle 时运行,负责处理 polyfills 的生成和注入。 -
后处理插件:
legacyPostPlugin是一个 'post' 插件,它在其他插件运行后执行,负责处理 JavaScript 代码的转换,注入 polyfills,修改 HTML 以适应旧版浏览器。 -
检测 Polyfills:
detectPolyfills函数使用 Babel 来分析代码,并确定需要哪些 polyfills。 -
创建 Babel 预设选项:
createBabelPresetEnvOptions函数创建了 Babel@babel/preset-env插件的配置选项。 -
构建 Polyfill Chunk:
buildPolyfillChunk函数使用 Rollup 来构建包含所有所需 polyfills 的单独 chunk。 -
Polyfills 插件:
polyfillsPlugin提供了一个 Rollup 插件,用于在构建过程中包含 polyfills。 -
识别旧版 Chunk 和 Bundle:
isLegacyChunk和isLegacyBundle函数用于识别是否正在处理旧版浏览器的代码。 -
Babel 插件:
recordAndRemovePolyfillBabelPlugin、replaceLegacyEnvBabelPlugin和wrapIIFEBabelPlugin是用于处理代码的 Babel 插件,它们分别用于记录 polyfill 导入、替换环境变量和包裹 IIFE。 -
CSP 哈希:
cspHashes用于内容安全策略,包含了一些内联脚本的哈希值。
这个插件的目的是创建一个兼容旧版浏览器的构建版本,同时保持对现代浏览器的优化。它通过条件加载 polyfills 和使用 SystemJS 作为模块加载器来实现这一点。代码中使用了一些技巧,如动态导入 polyfills、修改文件名来区分现代和旧版构建,以及在 HTML 中注入额外的脚本标签。
/* eslint-disable n/no-extraneous-import */
import path from 'node:path'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { build, normalizePath } from 'vite'
import MagicString from 'magic-string'
import type {
BuildOptions,
HtmlTagDescriptor,
Plugin,
ResolvedConfig,
} from 'vite'
import type {
NormalizedOutputOptions,
OutputBundle,
OutputOptions,
PreRenderedChunk,
RenderedChunk,
} from 'rollup'
import type {
PluginItem as BabelPlugin,
types as BabelTypes,
} from '@babel/core'
import colors from 'picocolors'
import browserslist from 'browserslist'
import type { Options } from './types'
import {
detectModernBrowserCode,
dynamicFallbackInlineCode,
legacyEntryId,
legacyPolyfillId,
modernChunkLegacyGuard,
safari10NoModuleFix,
systemJSInlineCode,
} from './snippets'
// lazy load babel since it's not used during dev
let babel: typeof import('@babel/core') | undefined
async function loadBabel() {
if (!babel) {
babel = await import('@babel/core')
}
return babel
}
// The requested module 'browserslist' is a CommonJS module
// which may not support all module.exports as named exports
const { loadConfig: browserslistLoadConfig } = browserslist
// 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
function toOutputFilePathInHtml(
filename: string,
type: 'asset' | 'public',
hostId: string,
hostType: 'js' | 'css' | 'html',
config: ResolvedConfig,
toRelative: (filename: string, importer: string) => string,
): string {
const { renderBuiltUrl } = config.experimental
let relative = config.base === '' || config.base === './'
if (renderBuiltUrl) {
const result = renderBuiltUrl(filename, {
hostId,
hostType,
type,
ssr: !!config.build.ssr,
})
if (typeof result === 'object') {
if (result.runtime) {
throw new Error(
`{ runtime: "${result.runtime}" } is not supported for assets in ${hostType} files: ${filename}`,
)
}
if (typeof result.relative === 'boolean') {
relative = result.relative
}
} else if (result) {
return result
}
}
if (relative && !config.build.ssr) {
return toRelative(filename, hostId)
} 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 config.base === './' || config.base === ''
? path.posix.join(
path.posix.relative(urlRelativePath, '').slice(0, -2),
'./',
)
: config.base
}
function toAssetPathFromHtml(
filename: string,
htmlPath: string,
config: ResolvedConfig,
): string {
const relativeUrlPath = normalizePath(path.relative(config.root, htmlPath))
const toRelative = (filename: string, hostId: string) =>
getBaseInHTML(relativeUrlPath, config) + filename
return toOutputFilePathInHtml(
filename,
'asset',
htmlPath,
'html',
config,
toRelative,
)
}
const legacyEnvVarMarker = `__VITE_IS_LEGACY__`
const _require = createRequire(import.meta.url)
const nonLeadingHashInFileNameRE = /[^/]+\[hash(?::\d+)?\]/
const prefixedHashInFileNameRE = /\W?\[hash(:\d+)?\]/
function viteLegacyPlugin(options: Options = {}): Plugin[] {
let config: ResolvedConfig
let targets: Options['targets']
let modernTargets: Options['modernTargets']
// browsers supporting ESM + dynamic import + import.meta + async generator
const modernTargetsEsbuild = [
'es2020',
'edge79',
'firefox67',
'chrome64',
'safari12',
]
// same with above but by browserslist syntax
// es2020 = chrome 80+, safari 13.1+, firefox 72+, edge 80+
// https://github.com/evanw/esbuild/issues/121#issuecomment-646956379
const modernTargetsBabel =
'edge>=79, firefox>=67, chrome>=64, safari>=12, chromeAndroid>=64, iOS>=12'
const genLegacy = options.renderLegacyChunks !== false
const genModern = options.renderModernChunks !== false
if (!genLegacy && !genModern) {
throw new Error(
'`renderLegacyChunks` and `renderModernChunks` cannot be both false',
)
}
const debugFlags = (process.env.DEBUG || '').split(',')
const isDebug =
debugFlags.includes('vite:*') || debugFlags.includes('vite:legacy')
const facadeToLegacyChunkMap = new Map()
const facadeToLegacyPolyfillMap = new Map()
const facadeToModernPolyfillMap = new Map()
const modernPolyfills = new Set<string>()
const legacyPolyfills = new Set<string>()
// When discovering polyfills in `renderChunk`, the hook may be non-deterministic, so we group the
// modern and legacy polyfills in a sorted chunks map for each rendered outputs before merging them.
const outputToChunkFileNameToPolyfills = new WeakMap<
NormalizedOutputOptions,
Map<string, { modern: Set<string>; legacy: Set<string> }> | null
>()
if (Array.isArray(options.modernPolyfills) && genModern) {
options.modernPolyfills.forEach((i) => {
modernPolyfills.add(
i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`,
)
})
}
if (Array.isArray(options.additionalModernPolyfills)) {
options.additionalModernPolyfills.forEach((i) => {
modernPolyfills.add(i)
})
}
if (Array.isArray(options.polyfills)) {
options.polyfills.forEach((i) => {
if (i.startsWith(`regenerator`)) {
legacyPolyfills.add(`regenerator-runtime/runtime.js`)
} else {
legacyPolyfills.add(
i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`,
)
}
})
}
if (Array.isArray(options.additionalLegacyPolyfills)) {
options.additionalLegacyPolyfills.forEach((i) => {
legacyPolyfills.add(i)
})
}
let overriddenBuildTarget = false
let overriddenDefaultModernTargets = false
const legacyConfigPlugin: Plugin = {
name: 'vite:legacy-config',
async config(config, env) {
if (env.command === 'build' && !config.build?.ssr) {
if (!config.build) {
config.build = {}
}
if (!config.build.cssTarget) {
// Hint for esbuild that we are targeting legacy browsers when minifying CSS.
// Full CSS compat table available at https://github.com/evanw/esbuild/blob/78e04680228cf989bdd7d471e02bbc2c8d345dc9/internal/compat/css_table.go
// But note that only the `HexRGBA` feature affects the minify outcome.
// HSL & rebeccapurple values will be minified away regardless the target.
// So targeting `chrome61` suffices to fix the compatibility issue.
config.build.cssTarget = 'chrome61'
}
if (genLegacy) {
// Vite's default target browsers are **not** the same.
// See https://github.com/vitejs/vite/pull/10052#issuecomment-1242076461
overriddenBuildTarget = config.build.target !== undefined
overriddenDefaultModernTargets = options.modernTargets !== undefined
if (options.modernTargets) {
// Package is ESM only
const { default: browserslistToEsbuild } = await import(
'browserslist-to-esbuild'
)
config.build.target = browserslistToEsbuild(options.modernTargets)
} else {
config.build.target = modernTargetsEsbuild
}
}
}
return {
define: {
'import.meta.env.LEGACY':
env.command === 'serve' || config.build?.ssr
? false
: legacyEnvVarMarker,
},
}
},
configResolved(config) {
if (overriddenBuildTarget) {
config.logger.warn(
colors.yellow(
`plugin-legacy overrode 'build.target'. You should pass 'targets' as an option to this plugin with the list of legacy browsers to support instead.`,
),
)
}
if (overriddenDefaultModernTargets) {
config.logger.warn(
colors.yellow(
`plugin-legacy 'modernTargets' option overrode the builtin targets of modern chunks. Some versions of browsers between legacy and modern may not be supported.`,
),
)
}
},
}
const legacyGenerateBundlePlugin: Plugin = {
name: 'vite:legacy-generate-polyfill-chunk',
apply: 'build',
async generateBundle(opts, bundle) {
if (config.build.ssr) {
return
}
const chunkFileNameToPolyfills =
outputToChunkFileNameToPolyfills.get(opts)
if (chunkFileNameToPolyfills == null) {
throw new Error(
'Internal @vitejs/plugin-legacy error: discovered polyfills should exist',
)
}
if (!isLegacyBundle(bundle, opts)) {
// Merge discovered modern polyfills to `modernPolyfills`
for (const { modern } of chunkFileNameToPolyfills.values()) {
modern.forEach((p) => modernPolyfills.add(p))
}
if (!modernPolyfills.size) {
return
}
isDebug &&
console.log(
`[@vitejs/plugin-legacy] modern polyfills:`,
modernPolyfills,
)
const polyfillChunk = await buildPolyfillChunk(
config.mode,
modernPolyfills,
bundle,
facadeToModernPolyfillMap,
config.build,
'es',
opts,
true,
)
if (genLegacy && polyfillChunk) {
polyfillChunk.code = modernChunkLegacyGuard + polyfillChunk.code
}
return
}
if (!genLegacy) {
return
}
// Merge discovered legacy polyfills to `legacyPolyfills`
for (const { legacy } of chunkFileNameToPolyfills.values()) {
legacy.forEach((p) => legacyPolyfills.add(p))
}
// legacy bundle
if (options.polyfills !== false) {
// check if the target needs Promise polyfill because SystemJS relies on it
// https://github.com/systemjs/systemjs#ie11-support
await detectPolyfills(
`Promise.resolve(); Promise.all();`,
targets,
legacyPolyfills,
)
}
if (legacyPolyfills.size || !options.externalSystemJS) {
isDebug &&
console.log(
`[@vitejs/plugin-legacy] legacy polyfills:`,
legacyPolyfills,
)
await buildPolyfillChunk(
config.mode,
legacyPolyfills,
bundle,
facadeToLegacyPolyfillMap,
// force using terser for legacy polyfill minification, since esbuild
// isn't legacy-safe
config.build,
'iife',
opts,
options.externalSystemJS,
)
}
},
}
const legacyPostPlugin: Plugin = {
name: 'vite:legacy-post-process',
enforce: 'post',
apply: 'build',
renderStart(opts) {
// Empty the nested map for this output
outputToChunkFileNameToPolyfills.set(opts, null)
},
configResolved(_config) {
if (_config.build.lib) {
throw new Error('@vitejs/plugin-legacy does not support library mode.')
}
config = _config
modernTargets = options.modernTargets || modernTargetsBabel
isDebug &&
console.log(`[@vitejs/plugin-legacy] modernTargets:`, modernTargets)
if (!genLegacy || config.build.ssr) {
return
}
targets =
options.targets ||
browserslistLoadConfig({ path: config.root }) ||
'last 2 versions and not dead, > 0.3%, Firefox ESR'
isDebug && console.log(`[@vitejs/plugin-legacy] targets:`, targets)
const getLegacyOutputFileName = (
fileNames:
| string
| ((chunkInfo: PreRenderedChunk) => string)
| undefined,
defaultFileName = '[name]-legacy-[hash].js',
): string | ((chunkInfo: PreRenderedChunk) => string) => {
if (!fileNames) {
return path.posix.join(config.build.assetsDir, defaultFileName)
}
return (chunkInfo) => {
let fileName =
typeof fileNames === 'function' ? fileNames(chunkInfo) : fileNames
if (fileName.includes('[name]')) {
// [name]-[hash].[format] -> [name]-legacy-[hash].[format]
fileName = fileName.replace('[name]', '[name]-legacy')
} else if (nonLeadingHashInFileNameRE.test(fileName)) {
// custom[hash].[format] -> [name]-legacy[hash].[format]
// custom-[hash].[format] -> [name]-legacy-[hash].[format]
// custom.[hash].[format] -> [name]-legacy.[hash].[format]
// custom.[hash:10].[format] -> custom-legacy.[hash:10].[format]
fileName = fileName.replace(prefixedHashInFileNameRE, '-legacy$&')
} else {
// entry.js -> entry-legacy.js
// entry.min.js -> entry-legacy.min.js
fileName = fileName.replace(/(.+?)\.(.+)/, '$1-legacy.$2')
}
return fileName
}
}
const createLegacyOutput = (
options: OutputOptions = {},
): OutputOptions => {
return {
...options,
format: 'system',
entryFileNames: getLegacyOutputFileName(options.entryFileNames),
chunkFileNames: getLegacyOutputFileName(options.chunkFileNames),
}
}
const { rollupOptions } = config.build
const { output } = rollupOptions
if (Array.isArray(output)) {
rollupOptions.output = [
...output.map(createLegacyOutput),
...(genModern ? output : []),
]
} else {
rollupOptions.output = [
createLegacyOutput(output),
...(genModern ? [output || {}] : []),
]
}
},
async renderChunk(raw, chunk, opts, { chunks }) {
if (config.build.ssr) {
return null
}
// On first run, intialize the map with sorted chunk file names
let chunkFileNameToPolyfills = outputToChunkFileNameToPolyfills.get(opts)
if (chunkFileNameToPolyfills == null) {
chunkFileNameToPolyfills = new Map()
for (const fileName in chunks) {
chunkFileNameToPolyfills.set(fileName, {
modern: new Set(),
legacy: new Set(),
})
}
outputToChunkFileNameToPolyfills.set(opts, chunkFileNameToPolyfills)
}
const polyfillsDiscovered = chunkFileNameToPolyfills.get(chunk.fileName)
if (polyfillsDiscovered == null) {
throw new Error(
`Internal @vitejs/plugin-legacy error: discovered polyfills for ${chunk.fileName} should exist`,
)
}
if (!isLegacyChunk(chunk, opts)) {
if (
options.modernPolyfills &&
!Array.isArray(options.modernPolyfills) &&
genModern
) {
// analyze and record modern polyfills
await detectPolyfills(raw, modernTargets, polyfillsDiscovered.modern)
}
const ms = new MagicString(raw)
if (genLegacy && chunk.isEntry) {
// append this code to avoid modern chunks running on legacy targeted browsers
ms.prepend(modernChunkLegacyGuard)
}
if (raw.includes(legacyEnvVarMarker)) {
const re = new RegExp(legacyEnvVarMarker, 'g')
let match
while ((match = re.exec(raw))) {
ms.overwrite(
match.index,
match.index + legacyEnvVarMarker.length,
`false`,
)
}
}
if (config.build.sourcemap) {
return {
code: ms.toString(),
map: ms.generateMap({ hires: 'boundary' }),
}
}
return {
code: ms.toString(),
}
}
if (!genLegacy) {
return null
}
// @ts-expect-error avoid esbuild transform on legacy chunks since it produces
// legacy-unsafe code - e.g. rewriting object properties into shorthands
opts.__vite_skip_esbuild__ = true
// @ts-expect-error force terser for legacy chunks. This only takes effect if
// minification isn't disabled, because that leaves out the terser plugin
// entirely.
opts.__vite_force_terser__ = true
// @ts-expect-error In the `generateBundle` hook,
// we'll delete the assets from the legacy bundle to avoid emitting duplicate assets.
// But that's still a waste of computing resource.
// So we add this flag to avoid emitting the asset in the first place whenever possible.
opts.__vite_skip_asset_emit__ = true
// avoid emitting assets for legacy bundle
const needPolyfills =
options.polyfills !== false && !Array.isArray(options.polyfills)
// transform the legacy chunk with @babel/preset-env
const sourceMaps = !!config.build.sourcemap
const babel = await loadBabel()
const result = babel.transform(raw, {
babelrc: false,
configFile: false,
compact: !!config.build.minify,
sourceMaps,
inputSourceMap: undefined, // sourceMaps ? chunk.map : undefined, `.map` TODO: moved to OutputChunk?
presets: [
// forcing our plugin to run before preset-env by wrapping it in a
// preset so we can catch the injected import statements...
[
() => ({
plugins: [
recordAndRemovePolyfillBabelPlugin(polyfillsDiscovered.legacy),
replaceLegacyEnvBabelPlugin(),
wrapIIFEBabelPlugin(),
],
}),
],
[
(await import('@babel/preset-env')).default,
createBabelPresetEnvOptions(targets, { needPolyfills }),
],
],
})
if (result) return { code: result.code!, map: result.map }
return null
},
transformIndexHtml(html, { chunk }) {
if (config.build.ssr) return
if (!chunk) return
if (chunk.fileName.includes('-legacy')) {
// The legacy bundle is built first, and its index.html isn't actually emitted if
// modern bundle will be generated. Here we simply record its corresponding legacy chunk.
facadeToLegacyChunkMap.set(chunk.facadeModuleId, chunk.fileName)
if (genModern) {
return
}
}
if (!genModern) {
html = html.replace(/<script type="module".*?<\/script>/g, '')
}
const tags: HtmlTagDescriptor[] = []
const htmlFilename = chunk.facadeModuleId?.replace(/\?.*$/, '')
// 1. inject modern polyfills
if (genModern) {
const modernPolyfillFilename = facadeToModernPolyfillMap.get(
chunk.facadeModuleId,
)
if (modernPolyfillFilename) {
tags.push({
tag: 'script',
attrs: {
type: 'module',
crossorigin: true,
src: toAssetPathFromHtml(
modernPolyfillFilename,
chunk.facadeModuleId!,
config,
),
},
})
} else if (modernPolyfills.size) {
throw new Error(
`No corresponding modern polyfill chunk found for ${htmlFilename}`,
)
}
}
if (!genLegacy) {
return { html, tags }
}
// 2. inject Safari 10 nomodule fix
if (genModern) {
tags.push({
tag: 'script',
attrs: { nomodule: genModern },
children: safari10NoModuleFix,
injectTo: 'body',
})
}
// 3. inject legacy polyfills
const legacyPolyfillFilename = facadeToLegacyPolyfillMap.get(
chunk.facadeModuleId,
)
if (legacyPolyfillFilename) {
tags.push({
tag: 'script',
attrs: {
nomodule: genModern,
crossorigin: true,
id: legacyPolyfillId,
src: toAssetPathFromHtml(
legacyPolyfillFilename,
chunk.facadeModuleId!,
config,
),
},
injectTo: 'body',
})
} else if (legacyPolyfills.size) {
throw new Error(
`No corresponding legacy polyfill chunk found for ${htmlFilename}`,
)
}
// 4. inject legacy entry
const legacyEntryFilename = facadeToLegacyChunkMap.get(
chunk.facadeModuleId,
)
if (legacyEntryFilename) {
// `assets/foo.js` means importing "named register" in SystemJS
tags.push({
tag: 'script',
attrs: {
nomodule: genModern,
crossorigin: true,
// we set the entry path on the element as an attribute so that the
// script content will stay consistent - which allows using a constant
// hash value for CSP.
id: legacyEntryId,
'data-src': toAssetPathFromHtml(
legacyEntryFilename,
chunk.facadeModuleId!,
config,
),
},
children: systemJSInlineCode,
injectTo: 'body',
})
} else {
throw new Error(
`No corresponding legacy entry chunk found for ${htmlFilename}`,
)
}
// 5. inject dynamic import fallback entry
if (legacyPolyfillFilename && legacyEntryFilename && genModern) {
tags.push({
tag: 'script',
attrs: { type: 'module' },
children: detectModernBrowserCode,
injectTo: 'head',
})
tags.push({
tag: 'script',
attrs: { type: 'module' },
children: dynamicFallbackInlineCode,
injectTo: 'head',
})
}
return {
html,
tags,
}
},
generateBundle(opts, bundle) {
if (config.build.ssr) {
return
}
if (isLegacyBundle(bundle, opts) && genModern) {
// avoid emitting duplicate assets
for (const name in bundle) {
if (bundle[name].type === 'asset' && !/.+\.map$/.test(name)) {
delete bundle[name]
}
}
}
},
}
return [legacyConfigPlugin, legacyGenerateBundlePlugin, legacyPostPlugin]
}
export async function detectPolyfills(
code: string,
targets: any,
list: Set<string>,
): Promise<void> {
const babel = await loadBabel()
const result = babel.transform(code, {
ast: true,
babelrc: false,
configFile: false,
compact: false,
presets: [
[
(await import('@babel/preset-env')).default,
createBabelPresetEnvOptions(targets, {}),
],
],
})
for (const node of result!.ast!.program.body) {
if (node.type === 'ImportDeclaration') {
const source = node.source.value
if (
source.startsWith('core-js/') ||
source.startsWith('regenerator-runtime/')
) {
list.add(source)
}
}
}
}
function createBabelPresetEnvOptions(
targets: any,
{ needPolyfills = true }: { needPolyfills?: boolean },
) {
return {
targets,
bugfixes: true,
loose: false,
modules: false,
useBuiltIns: needPolyfills ? 'usage' : false,
corejs: needPolyfills
? {
version: _require('core-js/package.json').version,
proposals: false,
}
: undefined,
shippedProposals: true,
ignoreBrowserslistConfig: true,
}
}
async function buildPolyfillChunk(
mode: string,
imports: Set<string>,
bundle: OutputBundle,
facadeToChunkMap: Map<string, string>,
buildOptions: BuildOptions,
format: 'iife' | 'es',
rollupOutputOptions: NormalizedOutputOptions,
excludeSystemJS?: boolean,
) {
let { minify, assetsDir } = buildOptions
minify = minify ? 'terser' : false
const res = await build({
mode,
// so that everything is resolved from here
root: path.dirname(fileURLToPath(import.meta.url)),
configFile: false,
logLevel: 'error',
plugins: [polyfillsPlugin(imports, excludeSystemJS)],
build: {
write: false,
minify,
assetsDir,
rollupOptions: {
input: {
polyfills: polyfillId,
},
output: {
format,
entryFileNames: rollupOutputOptions.entryFileNames,
},
},
},
// Don't run esbuild for transpilation or minification
// because we don't want to transpile code.
esbuild: false,
optimizeDeps: {
esbuildOptions: {
// If a value above 'es5' is set, esbuild injects helper functions which uses es2015 features.
// This limits the input code not to include es2015+ codes.
// But core-js is the only dependency which includes commonjs code
// and core-js doesn't include es2015+ codes.
target: 'es5',
},
},
})
const _polyfillChunk = Array.isArray(res) ? res[0] : res
if (!('output' in _polyfillChunk)) return
const polyfillChunk = _polyfillChunk.output[0]
// associate the polyfill chunk to every entry chunk so that we can retrieve
// the polyfill filename in index html transform
for (const key in bundle) {
const chunk = bundle[key]
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
facadeToChunkMap.set(chunk.facadeModuleId, polyfillChunk.fileName)
}
}
// add the chunk to the bundle
bundle[polyfillChunk.fileName] = polyfillChunk
return polyfillChunk
}
const polyfillId = '\0vite/legacy-polyfills'
function polyfillsPlugin(
imports: Set<string>,
excludeSystemJS?: boolean,
): Plugin {
return {
name: 'vite:legacy-polyfills',
resolveId(id) {
if (id === polyfillId) {
return id
}
},
load(id) {
if (id === polyfillId) {
return (
[...imports].map((i) => `import ${JSON.stringify(i)};`).join('') +
(excludeSystemJS ? '' : `import "systemjs/dist/s.min.js";`)
)
}
},
}
}
function isLegacyChunk(chunk: RenderedChunk, options: NormalizedOutputOptions) {
return options.format === 'system' && chunk.fileName.includes('-legacy')
}
function isLegacyBundle(
bundle: OutputBundle,
options: NormalizedOutputOptions,
) {
if (options.format === 'system') {
const entryChunk = Object.values(bundle).find(
(output) => output.type === 'chunk' && output.isEntry,
)
return !!entryChunk && entryChunk.fileName.includes('-legacy')
}
return false
}
function recordAndRemovePolyfillBabelPlugin(
polyfills: Set<string>,
): BabelPlugin {
return ({ types: t }: { types: typeof BabelTypes }): BabelPlugin => ({
name: 'vite-remove-polyfill-import',
post({ path }) {
path.get('body').forEach((p) => {
if (t.isImportDeclaration(p.node)) {
polyfills.add(p.node.source.value)
p.remove()
}
})
},
})
}
function replaceLegacyEnvBabelPlugin(): BabelPlugin {
return ({ types: t }): BabelPlugin => ({
name: 'vite-replace-env-legacy',
visitor: {
Identifier(path) {
if (path.node.name === legacyEnvVarMarker) {
path.replaceWith(t.booleanLiteral(true))
}
},
},
})
}
function wrapIIFEBabelPlugin(): BabelPlugin {
return ({ types: t, template }): BabelPlugin => {
const buildIIFE = template(';(function(){%%body%%})();')
return {
name: 'vite-wrap-iife',
post({ path }) {
if (!this.isWrapped) {
this.isWrapped = true
path.replaceWith(t.program(buildIIFE({ body: path.node.body })))
}
},
}
}
}
export const cspHashes = [
safari10NoModuleFix,
systemJSInlineCode,
detectModernBrowserCode,
dynamicFallbackInlineCode,
].map((i) => createHash('sha256').update(i).digest('base64'))
export type { Options }
export default viteLegacyPlugin