首页文章关于

vite-@vitejs/plugin-legacy源码阅读

vitelegacypolyfill

@vitejs/plugin-legacy作为兼容不支持原生ESM的方案插件,其根据构建目标生成对应polyfill的插件设计思路很值得学习特此记录。

用法及参数说明

ts
// ...
export default {
plugins: [
legacy({
// 构建目标,最终由@babel/preset-env处理
// 自定义可使用browserslist数组形式: ['last 2 versions nad not dead', > 0.3%, 'Firefox ESR']
targets: 'defaults',
// 默认true,使用@babel/preset的useBuiltIns: 'usage'
// 自定义可使用polyfill specifiers数组形式:['es.promise.finally', 'es/map', 'es/set', ...]
// false时则生成任何polyfill
polyfills: true,
// 添加自定义导入至基于es语言功能生成的polyfill中,例如额外的DOM API polyfill
additionalLegacyPolyfills: ['resize-observer-polyfill'],
// 忽略@babel/preset-env的browserslist检测
ignoreBrowserslistConfig: false,
// 默认为false,为true时会会分开生成modern polyfill
// 自定义可使用polyfill specifiers数组形式:['es.promise.finally', 'es/map', 'es/set', ...]
modernPolyfills: false,
// 默认为true,一般用在使用modernPolyfill为现代语法构建注入polyfill时设置为false
renderLegacyChunks: true,
// 默认为false,为true时systemjs/dist/s.min.js将不会包含在polyfills-legacy块中
externalSystemJS: false
})
]
}

viteLegacyPlugin流程

从入口文件默认导出了由三个内部插件组成分别处理对应阶段分别config阶段的legacyConfigPlugin > generateBundle阶段的legacyGenerateBundlePlugin > 以及后置处理的legacyPostPlugin,以下会按照流程顺序依次解释:

ts
// ...
function viteLegacyPlugin(options: Options = {}): Plugin[] {
// ...
const legacyConfigPlugin: Plugin = {
// ...
}
const legacyGenerateBundlePlugin: Plugin = {
// ...
}
const legacyPostPlugin: Plugin = {
// ...
}
return [legacyConfigPlugin, legacyGenerateBundlePlugin, legacyPostPlugin]
}
// ...
export default viteLegacyPlugin

在vite-config.ts中调用实际形式如下:

ts
export default {
plugins: [
// legacy({ targets: 'last 2 versions' })
{
name: 'vite:legacy-config',
config: () => {/**/},
configResolved: () => {/**/}
},
{
name: 'vite:legacy-generate-polyfill-chunk',
apply: 'build',
generateBundle: () => {/**/}
},
{
name: 'vite:legacy-post-process',
enforce: 'post',
apply: 'build',
configResolved: () => {/**/},
renderChunk: () => {/**/}
}
]
}

legacyConfigPlugin

此步骤插件在vite配置为解析前后(configconfigResolved)进行了处理:

  1. config钩子中对build配置进行设值cssTargettarget
  2. 并对是否会覆盖原有构建目标进行判断
  3. 使用define定义环境变量import.meta.env.LEGACY=__VITE_IS_LEGACY__并返回
  4. 最终在configResolved中判断有无用户自定义的构建目标,有的话就提示将会被覆盖,应该在legacy插件中定义构建目标
ts
const legacyConfigPlugin = {
name: "vite:legacy-config",
config(config2, env) {
if (env.command === "build") {
if (!config2.build) {
config2.build = {};
}
if (!config2.build.cssTarget) {
config2.build.cssTarget = "chrome61";
}
if (genLegacy) {
overriddenBuildTarget = config2.build.target !== void 0;
config2.build.target = [
"es2020",
"edge79",
"firefox67",
"chrome64",
"safari12"
];
}
}
return {
define: {
"import.meta.env.LEGACY": env.command === "serve" || config2.build?.ssr ? false : legacyEnvVarMarker
}
};
},
configResolved(config2) {
if (overriddenBuildTarget) {
config2.logger.warn(
picocolorsExports.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.`
)
);
}
}
}

legacyGenerateBundlePlug

此步骤插件在vite构建产物的generateBundle钩子中进行处理:

  1. 如果不是legacy类型的bundle则走modernpolyfill构建处理
  2. 将在renderChunk中处理后的polyfill集合信息单独构建polyfillChunk并追加到原有的bundle信息中,在bundle.write时真正输出
polyfillsPlugin
ts
function polyfillsPlugin(imports, excludeSystemJS) {
return {
name: "vite:legacy-polyfills",
resolveId(id) {
if (id === polyfillId) {
return id;
}
},
// polyfillsPlugin将legacyPolyfill在load钩子中遍历拼接import "systemjs/dist/s.min.js注入到入口文件中
load(id) {
if (id === polyfillId) {
return [...imports].map((i) => `import ${JSON.stringify(i)};`).join("") + (excludeSystemJS ? "" : `import "systemjs/dist/s.min.js";`);
}
}
};
}
buildPolyfillChunk
ts
async function buildPolyfillChunk(mode, imports, bundle, facadeToChunkMap, buildOptions, format, rollupOutputOptions, excludeSystemJS) {
let { minify, assetsDir } = buildOptions;
minify = minify ? "terser" : false;
const res = await vite.build({
mode,
// so that everything is resolved from here
root: path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)))),
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;
// polyfillChunk信息:以legacyPolyfills作为入口文件内容构建的产物
const polyfillChunk = _polyfillChunk.output[0];
for (const key in bundle) {
const chunk = bundle[key];
// 在整体bundle中查找chunk类型,并将以chunk的实际磁盘位置作为key,polyfillChunk的fileName作为value保存
// 后后置transformIndexHtml钩子中会用到
if (chunk.type === "chunk" && chunk.facadeModuleId) {
facadeToChunkMap.set(chunk.facadeModuleId, polyfillChunk.fileName);
}
}
// 将上面构建出的polyfillChunk追加到原有的bundle中,在后续的实际产出中将会打包出来
bundle[polyfillChunk.fileName] = polyfillChunk;
}
legacyGenerateBundlePlugin
ts
const legacyGenerateBundlePlugin = {
name: "vite:legacy-generate-polyfill-chunk",
apply: "build",
async generateBundle(opts, bundle) {
if (config.build.ssr) return;
// isLegacyBundle会对format为system的chunk进行是否包含-legacy字段的判断
// 如果是false则会进行以format为es的形式对chunk进行modern polyfill构建
if (!isLegacyBundle(bundle, opts)) {
if (!modernPolyfills.size) return;
isDebug && console.log(
`[@vitejs/plugin-legacy] modern polyfills:`,
modernPolyfills
);
await buildPolyfillChunk(
config.mode,
modernPolyfills,
bundle,
facadeToModernPolyfillMap,
config.build,
"es",
opts,
true
);
return;
}
// 如果genLegacy为true则return,不将生成
if (!genLegacy) return;
// 此时经过renderChunk的babel处理后,legacyPolyfills中已经记录了需要的polyfill信息(ImportDeclaration、ExpressionStatement等)
if (legacyPolyfills.size) {
// 根据输入语法检测在legacyPolyfills中增加对应ImportDeclaration类型的polyfill模块名称
await detectPolyfills(
`Promise.resolve(); Promise.all();`,
targets,
legacyPolyfills
);
isDebug && console.log(
`[@vitejs/plugin-legacy] legacy polyfills:`,
legacyPolyfills
);
// 进行vite函数式构建
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
);
}
}
}

legacyPostPlugin

此步骤插件仅在构建时对相同configResolvedrenderChunktransformIndexHtmlgenerateBundle钩子做后置处理:

ts
const legacyPostPlugin = {
name: "vite:legacy-post-process",
enforce: "post",
apply: "build",
configResolved,
renderChunk,
transformIndexHtml,
generateBundle
}
  1. configResolved钩子中对产出的chunklegacy命名化处理(chunkName-legacy-[hash].js)
ts
function configResolved(_config) {
if (_config.build.lib) throw new Error("@vitejs/plugin-legacy does not support library mode.");
config = _config; // 将修改后的config保存至顶层变量中
if (!genLegacy || config.build.ssr) return;
// 优先legacy-plugin传过来的targets,其次从项目根目录查找,否则使用默认预设目标
targets = options.targets || browserslistLoadConfig({ path: config.root }) || "last 2 versions and not dead, > 0.3%, Firefox ESR";
// chunk legacy命名化文件名称处理
const getLegacyOutputFileName = (fileNames, defaultFileName = "[name]-legacy-[hash].js") => {
// 没有文件名称时返回assets路径和默认文件名称的拼接路径
// 本例中的情况是: createLegacyOutput => { ..., createLegacyOutput(entryFileNames) }
// assets/[name]-legacy-[hash].js
if (!fileNames) {
return path.posix.join(config.build.assetsDir, defaultFileName);
}
// 有fileName时处理,会犯chunkFileNames方法
// 本例中的情况是: createLegacyOutput => { ..., createLegacyOutput(chunkFileNames) }
// 以供generateBundle时获取legacy命名化后的chunk信息
// 对应的chunk名称将会变成:index-legacy-xxx.js
return (chunkInfo) => {
let fileName = typeof fileNames === "function" ? fileNames(chunkInfo) : fileNames;
if (fileName.includes("[name]")) {
fileName = fileName.replace("[name]", "[name]-legacy");
} else {
fileName = fileName.replace(/(.+)\.(.+)/, "$1-legacy.$2");
}
return fileName;
};
}
// 创建legacy化rollupOptions.output配置
// 在rollup的build阶段会根据这个配置进行transform和moduleParsed处理
// 后续被renderChunk处理的代码都将是system格式的
const createLegacyOutput = (options2 = {}) => {
return {
...options2,
format: "system",
entryFileNames: getLegacyOutputFileName(options2.entryFileNames),
chunkFileNames: getLegacyOutputFileName(options2.chunkFileNames)
};
};
const { rollupOptions } = config.build;
const { output } = rollupOptions;
// 多出口配置判断
if (Array.isArray(output)) {
rollupOptions.output = [...output.map(createLegacyOutput), ...output];
} else {
// 本例中是单出口
rollupOptions.output = [createLegacyOutput(output), output || {}];
}
}
  1. renderChunk钩子中对每个system格式的chunk通过babel transform进行ast分析后,将根据targets生成的polyfill记录在legacyPolyfills中,以供generateBundle时生成
ts
// raw为system格式处理后的代码字符串
// chunk为当前renderChunk信息
// opts为处理后的rollupOptions
async function renderChunk(raw, chunk, opts) {
if (config.build.ssr) return null;
// 判断是否为*.+-legacy的chunk
if (!isLegacyChunk(chunk, opts)) {
// 如果modernPolyfill为true时就通过@babel/preset-env对代码进行polyfill的检测
// 并将需要polyfill的模块名称添加到对应的modernPolyfills Map体中
if (options.modernPolyfills && !Array.isArray(options.modernPolyfills)) {
await detectPolyfills(raw, { esmodules: true }, modernPolyfills);
}
const ms = new MagicString(raw);
if (genLegacy && chunk.isEntry) {
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: true })
};
}
return {
code: ms.toString()
};
}
if (!genLegacy) return null;
// vite内部流程私有标识设置
opts.__vite_skip_esbuild__ = true;
opts.__vite_force_terser__ = true;
opts.__vite_skip_asset_emit__ = true;
// 是否需要polyfill用于createBabelPresetEnvOptions创建@babel/preset-env配置
const needPolyfills = options.polyfills !== false && !Array.isArray(options.polyfills);
// 是否需要sourcemap
const sourceMaps = !!config.build.sourcemap;
const babel2 = await loadBabel();
const result = babel2.transform(raw, {
babelrc: false,
configFile: false,
compact: !!config.build.minify,
sourceMaps,
inputSourceMap: void 0,
// 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...
[
() => ({
// 自定义的babel transform plugin
plugins: [
// 后置插件: 将babel polyfill移除并添加到legacyPolyfills Map体中
recordAndRemovePolyfillBabelPlugin(legacyPolyfills),
// 访问器插件: 遍历替换节点
replaceLegacyEnvBabelPlugin(),
// 后置插件: iife包裹处理node.body
wrapIIFEBabelPlugin()
]
})
],
[
"@babel/preset-env",
// 根据targets创建env对应配置项
createBabelPresetEnvOptions(targets, {
needPolyfills,
ignoreBrowserslistConfig: options.ignoreBrowserslistConfig
})
]
]
});
// 有编译结果则输出对应sourcemap
if (result) return { code: result.code, map: result.map };
return null;
}
  1. transformIndexHtml钩子中增加资源引用以及现代浏览器能力判断:
ts
function transformIndexHtml(html, { chunk }) {
if (config.build.ssr) return;
if (!chunk) return;
// 记录legacy chunk
if (chunk.fileName.includes("-legacy")) {
facadeToLegacyChunkMap.set(chunk.facadeModuleId, chunk.fileName);
return;
}
const tags = [];
const htmlFilename = chunk.facadeModuleId?.replace(/\?.*$/, "");
const modernPolyfillFilename = facadeToModernPolyfillMap.get(
chunk.facadeModuleId
);
// 如果用户设置了 modern polyfill的话就加到body当中
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}`
);
}
// 如果不生成legacy则交由其他钩子处理
if (!genLegacy) {
return { html, tags };
}
// safari兼容处理
tags.push({
tag: "script",
attrs: { nomodule: true },
children: safari10NoModuleFix,
injectTo: "body"
});
const legacyPolyfillFilename = facadeToLegacyPolyfillMap.get(
chunk.facadeModuleId
);
// legacy polyfill 注入
if (legacyPolyfillFilename) {
tags.push({
tag: "script",
attrs: {
nomodule: true,
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}`
);
}
const legacyEntryFilename = facadeToLegacyChunkMap.get(
chunk.facadeModuleId
);
// legacy入口文件注入
if (legacyEntryFilename) {
tags.push({
tag: "script",
attrs: {
nomodule: true,
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}`
);
}
// 增加现代浏览器能力检测,如果支持则使用现代构建产物否则使用system格式产物
if (genLegacy && legacyPolyfillFilename && legacyEntryFilename) {
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
};
}
  1. generateBundle钩子中做了服务端渲染返回以及删除非.map文件的asset类型bundle
ts
function generateBundle(opts, bundle) {
if (config.build.ssr) {
return;
}
if (isLegacyBundle(bundle, opts)) {
for (const name in bundle) {
if (bundle[name].type === "asset" && !/.+\.map$/.test(name)) {
delete bundle[name];
}
}
}
}

至此整个legacy代码处理流程结束,也就是通过插件来讲转换后的代码通过babel再转换为指定浏览器目标的systemjs模块化形式.再通过入口文件运行时判断是否支持原生module来选择使用哪套代码。

Copyright © 2023, Daily learning records and sharing. Build by