bundle.generate
Bundle 类定义在 src/Bundle.ts:
class Bundle {
private readonly facadeChunkByModule = new Map<Module, Chunk>();
private readonly includedNamespaces = new Set<Module>();
constructor(
private readonly outputOptions: NormalizedOutputOptions,
private readonly unsetOptions: ReadonlySet<string>,
private readonly inputOptions: NormalizedInputOptions,
private readonly pluginDriver: PluginDriver,
private readonly graph: Graph
) {}
async generate(isWrite: boolean): Promise<OutputBundle> {
const outputBundleBase: OutputBundle = Object.create(null);
// outputBundle 是一个 proxy
const outputBundle = getOutputBundle(outputBundleBase);
//...
try {
//...
const getHashPlaceholder = getHashPlaceholderGenerator();
// chunks => [chunk]
const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
if (chunks.length > 1) {
//校验 outputOptions 选项是否合法
validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
}
//...
for (const chunk of chunks) {
chunk.generateExports();
}
/**
* renderChunks 主要作用是生成最终的 outputBundle。
* 例如:{index.js: {…}, acorn-bf6b1c54.js: {…}}
*
*/
await renderChunks(
chunks,
outputBundle,
this.pluginDriver,
this.outputOptions,
this.inputOptions.onwarn
);
} catch (error_: any) {
await this.pluginDriver.hookParallel('renderError', [error_]);
throw error_;
}
//...
return outputBundleBase;
}
private async addManualChunks(
manualChunks: Record<string, readonly string[]>
): Promise<Map<Module, string>> {
const manualChunkAliasByEntry = new Map<Module, string>();
/**
* alias 就是我们定义manualChunks 的 key,例如 acorn
* await this.graph.moduleLoader.addAdditionalModules(files) 会得到一个 module[]
* chunkEntries
[{…}]
0: {alias: 'acorn', entries: Array(1)}
length: 1
[[Prototype]]: Array(0)
[[Prototype]]: Object
*/
const chunkEntries = await Promise.all(
Object.entries(manualChunks).map(async ([alias, files]) => ({
alias,
entries: await this.graph.moduleLoader.addAdditionalModules(files)
}))
);
for (const { alias, entries } of chunkEntries) {
for (const entry of entries) {
//内部调用 manualChunkAliasByEntry.set(module, alias)
addModuleToManualChunk(alias, entry, manualChunkAliasByEntry);
}
}
return manualChunkAliasByEntry;
}
private assignManualChunks(getManualChunk: GetManualChunk): Map<Module, string> {
// eslint-disable-next-line unicorn/prefer-module
const manualChunkAliasesWithEntry: [alias: string, module: Module][] = [];
const manualChunksApi = {
getModuleIds: () => this.graph.modulesById.keys(),
getModuleInfo: this.graph.getModuleInfo
};
for (const module of this.graph.modulesById.values()) {
if (module instanceof Module) {
const manualChunkAlias = getManualChunk(module.id, manualChunksApi);
if (typeof manualChunkAlias === 'string') {
manualChunkAliasesWithEntry.push([manualChunkAlias, module]);
}
}
}
manualChunkAliasesWithEntry.sort(([aliasA], [aliasB]) =>
aliasA > aliasB ? 1 : aliasA < aliasB ? -1 : 0
);
const manualChunkAliasByEntry = new Map<Module, string>();
for (const [alias, module] of manualChunkAliasesWithEntry) {
addModuleToManualChunk(alias, module, manualChunkAliasByEntry);
}
return manualChunkAliasByEntry;
}
private finaliseAssets(bundle: OutputBundleWithPlaceholders): void {
if (this.outputOptions.validate) {
for (const file of Object.values(bundle)) {
if ('code' in file) {
try {
this.graph.contextParse(file.code, {
allowHashBang: true,
ecmaVersion: 'latest'
});
} catch (error_: any) {
this.inputOptions.onwarn(errorChunkInvalid(file, error_));
}
}
}
}
this.pluginDriver.finaliseAssets();
}
private async generateChunks(
bundle: OutputBundleWithPlaceholders,
getHashPlaceholder: HashPlaceholderGenerator
): Promise<Chunk[]> {
/**
* inlineDynamicImports: 该选项用于内联动态引入,而不是用于创建包含新 Chunk 的独立 bundle。它只在单一输入源时产生作用。
* manualChunks: 该选项允许你创建自定义的公共模块。
* preserveModules: 该选项将使用原始模块名作为文件名,为所有模块创建单独的 chunk,而不是创建尽可能少的 chunk。
*
*/
const { inlineDynamicImports, manualChunks, preserveModules } = this.outputOptions;
// outputOption.manualChunks 既可以是对象,也可以是函数
/**
* this.addManualChunks(manualChunks) 或者 this.assignManualChunks(manualChunks) 都会返回一个 Map<Module, string> 结构的对象。
* manualChunkAliasByEntry => [Module, 'acorn']
*/
const manualChunkAliasByEntry =
typeof manualChunks === 'object'
? await this.addManualChunks(manualChunks)
: this.assignManualChunks(manualChunks);
const snippets = getGenerateCodeSnippets(this.outputOptions);
const includedModules = getIncludedModules(this.graph.modulesById);
/**
* getAbsoluteEntryModulePaths(includedModules, false) 返回入口模块并且为绝对路径的一个字符串((module.info.isEntry || preserveModules) && isAbsolute(module.id))
* commondir 会返回路径中代表文件夹的部分
* inputBase 就是根路径的意思
*/
const inputBase = commondir(getAbsoluteEntryModulePaths(includedModules, preserveModules));
const externalChunkByModule = getExternalChunkByModule(
this.graph.modulesById,
this.outputOptions,
inputBase
);
const chunks: Chunk[] = [];
const chunkByModule = new Map<Module, Chunk>();
/**
* 如果没有在配置文件中手动设置 preserveModules 和 inlineDynamicImports 则他们都默认为 false 。
* 因此执行的是 getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)
*/
for (const { alias, modules } of inlineDynamicImports
? [{ alias: null, modules: includedModules }]
: preserveModules
? includedModules.map(module => ({ alias: null, modules: [module] }))
: getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) {
sortByExecutionOrder(modules);
const chunk = new Chunk(
modules,
this.inputOptions,
this.outputOptions,
this.unsetOptions,
this.pluginDriver,
this.graph.modulesById,
chunkByModule,
externalChunkByModule,
this.facadeChunkByModule,
this.includedNamespaces,
alias,
getHashPlaceholder,
bundle,
inputBase,
snippets
);
chunks.push(chunk);
}
for (const chunk of chunks) {
chunk.link();
}
const facades: Chunk[] = [];
for (const chunk of chunks) {
facades.push(...chunk.generateFacades());
}
return [...chunks, ...facades];
}
}
bundle.generate 方法内部主要是执行 "code generate" 阶段的逻辑。
它首先定义了一个 outputBundleBase 对象用于记录打包输出的 fileName 和 chunk。如下是 OutputBundle 的定义:
interface OutputBundle {
[fileName: string]: OutputAsset | OutputChunk;
}
接着执行 const outputBundle = getOutputBundle(outputBundleBase); 这个 outputBundle 就是一个 proxy 实例。const chunks = await this.generateChunks(outputBundle, getHashPlaceholder); 就是调用 bundle.generateChunks 方法。
bundle.generateChunks
bundle.generateChunks 方法内部首先通过 const { inlineDynamicImports, manualChunks, preserveModules } = this.outputOptions; 获取传入的 output 配置。因为 output.manualChunks 配置既可以是对象也可以是函数。
例如我们在配置文件中像这样设置了 manualChunks 选项:
//rollup.config.js
import nodeResolve from '@rollup/plugin-node-resolve';
export default {
input: 'example/index.js',
output: {
dir: 'example/dest',
format: 'es',
manualChunks: {
acorn: ['acorn']
}
},
plugins: [nodeResolve()]
};
因此在获取 manualChunkAliasByEntry 的时候会首先判断 typeof manualChunks === 'object' ,如果是对象结构的话则调用 this.addManualChunks(manualChunks),否则调用 this.assignManualChunks(manualChunks)方法。 无论如何,最终都会返回 manualChunkAliasByEntry 的一个 map 对象。这个 manualChunkAliasByEntry 的对象结构的示意如下:
manualChunkAliasByEntry: {
Module, 'acorn';
}
const snippets = getGenerateCodeSnippets(this.outputOptions); 这个 snippets 中定义了一些生成打包代码时所用到的公共方法.
export interface GenerateCodeSnippets {
_: string;
cnst: string;
n: string;
s: string;
getDirectReturnFunction(
parameters: string[],
options: {
functionReturn: boolean;
lineBreakIndent: { base: string; t: string } | null;
name: string | null;
}
): [left: string, right: string];
getDirectReturnIifeLeft(
parameters: string[],
returned: string,
options: {
needsArrowReturnParens: boolean | undefined;
needsWrappedFunction: boolean | undefined;
}
): string;
getFunctionIntro(
parameters: string[],
options: { isAsync: boolean; name: string | null }
): string;
getNonArrowFunctionIntro(
parameters: string[],
options: { isAsync: boolean; name: string | null }
): string;
getObject(
fields: [key: string | null, value: string][],
options: { lineBreakIndent: { base: string; t: string } | null }
): string;
getPropertyAccess(name: string): string;
}
举个例子: getNonArrowFunctionIntro 方法可以获取一个非箭头函数的模版(不包含函数体部分)。
snippets.getNonArrowFunctionIntro([1, 2], { isAsync: false, name: 'test' });
// 'function test (1, 2) '
const includedModules = getIncludedModules(this.graph.modulesById) 就是应该被包含到打包输出的 modules。
const inputBase = commondir(getAbsoluteEntryModulePaths(includedModules, preserveModules)); inputBase 表示输出文件的根路径。getAbsoluteEntryModulePaths(includedModules, preserveModules) 会返回一个包含路径字符串的数组。例如:
getAbsoluteEntryModulePaths(includedModules, preserveModules); // ['c:\Users\**\Desktop\study\rollup-master\rollup\example\index.js']
commondir 会返回路径中代表文件夹的那部分。因此 inputBase 大概就是 'c:\Users**\Desktop\study\rollup-master\rollup\example' 这样。
const externalChunkByModule = getExternalChunkByModule( this.graph.modulesById, this.outputOptions, inputBase ) 的目的是为了获取我们配置文件中设置了 external 的模块。
例如下面例子中设置了 'acorn' 为 externalModule :
export default {
input: 'example/index.js',
external: ['acorn'],
output: {
file: 'dest/bundle.js',
format: 'es'
},
plugins: []
};
接着分别定义了 chunks(代表需要打包的区块)、chunkByModule(module => chunk 组成的 map 对象)。
如果我们没有手动设置配置文件中的 preserveModules 和 inlineDynamicImports 则他们默认都是 false。因此 for 循环的是 getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry) 执行后的返回值。因此我们看下 getChunkAssignments 函数的定义:
src/utils/chunkAssignment.ts
export function getChunkAssignments(
entryModules: readonly Module[],
manualChunkAliasByEntry: ReadonlyMap<Module, string>
): ChunkDefinitions {
const chunkDefinitions: ChunkDefinitions = [];
const modulesInManualChunks = new Set(manualChunkAliasByEntry.keys());
const manualChunkModulesByAlias: Record<string, Module[]> = Object.create(null);
/**
*下面的for循环逻辑中主要作用是将 manualChunkAliasByEntry 的结构({ Module {graph, …} => acorn})
* 进行 key-value 的反转然后添加到 manualChunkModulesByAlias 对象中
*/
for (const [entry, alias] of manualChunkAliasByEntry) {
const chunkModules = (manualChunkModulesByAlias[alias] =
manualChunkModulesByAlias[alias] || []);
addStaticDependenciesToManualChunk(entry, chunkModules, modulesInManualChunks);
}
for (const [alias, modules] of Object.entries(manualChunkModulesByAlias)) {
chunkDefinitions.push({ alias, modules });
}
const assignedEntryPointsByModule: DependentModuleMap = new Map();
const { dependentEntryPointsByModule, dynamicEntryModules } = analyzeModuleGraph(entryModules);
/**
* getDynamicDependentEntryPoints 的作用是获取动态 import 所对应的 importer
* 在这个例子中就是为了获取 user 模块对应的 importer (index模块)
* {size: 1, Module {graph, …} => Set(1) {…}}
*/
const dynamicallyDependentEntryPointsByDynamicEntry: DependentModuleMap =
getDynamicDependentEntryPoints(dependentEntryPointsByModule, dynamicEntryModules);
const staticEntries = new Set(entryModules);
function assignEntryToStaticDependencies(
entry: Module,
dynamicDependentEntryPoints: ReadonlySet<Module> | null
) {
const modulesToHandle = new Set([entry]);
for (const module of modulesToHandle) {
const assignedEntryPoints = getOrCreate(assignedEntryPointsByModule, module, () => new Set());
if (
dynamicDependentEntryPoints &&
areEntryPointsContainedOrDynamicallyDependent(
dynamicDependentEntryPoints,
dependentEntryPointsByModule.get(module)!
)
) {
continue;
} else {
assignedEntryPoints.add(entry);
}
for (const dependency of module.getDependenciesToBeIncluded()) {
if (!(dependency instanceof ExternalModule || modulesInManualChunks.has(dependency))) {
modulesToHandle.add(dependency);
}
}
}
}
function areEntryPointsContainedOrDynamicallyDependent(
entryPoints: ReadonlySet<Module>,
containedIn: ReadonlySet<Module>
): boolean {
const entriesToCheck = new Set(entryPoints);
for (const entry of entriesToCheck) {
if (!containedIn.has(entry)) {
if (staticEntries.has(entry)) return false;
const dynamicallyDependentEntryPoints =
dynamicallyDependentEntryPointsByDynamicEntry.get(entry)!;
for (const dependentEntry of dynamicallyDependentEntryPoints) {
entriesToCheck.add(dependentEntry);
}
}
}
return true;
}
for (const entry of entryModules) {
if (!modulesInManualChunks.has(entry)) {
assignEntryToStaticDependencies(entry, null);
}
}
for (const entry of dynamicEntryModules) {
if (!modulesInManualChunks.has(entry)) {
assignEntryToStaticDependencies(
entry,
dynamicallyDependentEntryPointsByDynamicEntry.get(entry)!
);
}
}
chunkDefinitions.push(
...createChunks([...entryModules, ...dynamicEntryModules], assignedEntryPointsByModule)
);
return chunkDefinitions;
}
getChunkAssignments 函数返回值是一个 ChunkDefinitions,它里面保存了区块的别名和对应的 module 信息。ChunkDefinitions 如下:
type ChunkDefinitions = { alias: string | null; modules: Module[] }[];
所以最终通过 for (const { alias, modules } of getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) 循环执行 chunks.push(new Chunk()) 得到 chunks。再执行 for (const chunk of chunks) { chunk.link(); } 为每个 ckunk 设置 this.dependencies 等。
再之后就是 for (const chunk of chunks) { facades.push(...chunk.generateFacades()); }。 chunk.generateFacades() 也会生成 chunk 并返回。
class Chunk {
//...
generateFacades(): Chunk[] {
const facades: Chunk[] = [];
const entryModules = new Set([...this.entryModules, ...this.implicitEntryModules]);
const exposedVariables = new Set<Variable>(
this.dynamicEntryModules.map(({ namespace }) => namespace)
);
//收集需要导出的变量
for (const module of entryModules) {
if (module.preserveSignature) {
for (const exportedVariable of module.getExportNamesByVariable().keys()) {
exposedVariables.add(exportedVariable);
}
}
}
for (const module of entryModules) {
// eslint-disable-next-line unicorn/prefer-spread
const requiredFacades: FacadeName[] = Array.from(
new Set(
module.chunkNames.filter(({ isUserDefined }) => isUserDefined).map(({ name }) => name)
),
// mapping must run after Set 'name' dedupe
name => ({
name
})
);
if (requiredFacades.length === 0 && module.isUserDefinedEntryPoint) {
requiredFacades.push({});
}
// eslint-disable-next-line unicorn/prefer-spread
requiredFacades.push(...Array.from(module.chunkFileNames, fileName => ({ fileName })));
if (requiredFacades.length === 0) {
requiredFacades.push({});
}
if (!this.facadeModule) {
const needsStrictFacade =
module.preserveSignature === 'strict' ||
(module.preserveSignature === 'exports-only' &&
module.getExportNamesByVariable().size > 0);
if (
!needsStrictFacade ||
this.outputOptions.preserveModules ||
this.canModuleBeFacade(module, exposedVariables)
) {
this.facadeModule = module;
this.facadeChunkByModule.set(module, this);
//module.preserveSignature = 'exports-only'
if (module.preserveSignature) {
this.strictFacade = needsStrictFacade;
}
//设置this.fileName 或者 this.name
this.assignFacadeName(
requiredFacades.shift()!,
module,
this.outputOptions.preserveModules
);
}
}
for (const facadeName of requiredFacades) {
facades.push(
Chunk.generateFacade(
this.inputOptions,
this.outputOptions,
this.unsetOptions,
this.pluginDriver,
this.modulesById,
this.chunkByModule,
this.externalChunkByModule,
this.facadeChunkByModule,
this.includedNamespaces,
module,
facadeName,
this.getPlaceholder,
this.bundle,
this.inputBase,
this.snippets
)
);
}
}
for (const module of this.dynamicEntryModules) {
if (module.info.syntheticNamedExports) continue;
if (!this.facadeModule && this.canModuleBeFacade(module, exposedVariables)) {
this.facadeModule = module;
this.facadeChunkByModule.set(module, this);
this.strictFacade = true;
this.dynamicName = getChunkNameFromModule(module);
} else if (
this.facadeModule === module &&
!this.strictFacade &&
this.canModuleBeFacade(module, exposedVariables)
) {
this.strictFacade = true;
} else if (!this.facadeChunkByModule.get(module)?.strictFacade) {
this.includedNamespaces.add(module);
this.exports.add(module.namespace);
}
}
if (!this.outputOptions.preserveModules) {
this.addNecessaryImportsForFacades();
}
return facades;
}
}
最终 generateChunks 方法执行了 return [...chunks, ...facades]; 也就是所有的区块集合。
我们继续回到 Bundle.generate 方法:
async generate(isWrite: boolean): Promise<OutputBundle> {
//...
const outputBundleBase: OutputBundle = Object.create(null);
// outputBundle 是一个 proxy
const outputBundle = getOutputBundle(outputBundleBase);
this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions);
try {
//...
const getHashPlaceholder = getHashPlaceholderGenerator();
// chunks => [chunk]
const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
if (chunks.length > 1) {
//校验 outputOptions 选项是否合法
validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
}
/**
* chunk.generateExports 主要做了以下几件事:
* 1. 设置 chunk.exportNamesByVariable
* 2. 设置 chunk.exportsByName
* 3. 压缩变量名称(默认情况下)
* 4. 设置 chunk.exportMode
*/
for (const chunk of chunks) {
chunk.generateExports();
}
/**
* renderChunks 主要作用是生成最终的 outputBundle。
* 例如:{index.js: {…}, acorn-bf6b1c54.js: {…}}
* 主要可以分为以下几步:
* 1. 设置入口 chunk 的 preliminaryFileName
* 2. 执行chunk.render()
* 3. 生成 chunkGraph
* 4. addChunksToBundle
*/
await renderChunks(
chunks,
outputBundle,
this.pluginDriver,
this.outputOptions,
this.inputOptions.onwarn
);
} catch (error_: any) {
await this.pluginDriver.hookParallel('renderError', [error_]);
throw error_;
}
//...
return outputBundleBase;
}
在执行 const chunks = await this.generateChunks(outputBundle, getHashPlaceholder); 的时候这个 chunks 就是刚才 bundle.generateChunks 返回的 chunks。
chunk.generateExports() 主要做了以下4件事:
- 设置 chunk.exportNamesByVariable
- 设置 chunk.exportsByName
- 压缩变量名称(默认情况下)
- 设置 chunk.exportMode
如下是 chunk.generateExports 代码定义:
generateExports(): void {
this.sortedExportNames = null;
const remainingExports = new Set(this.exports);
if (
this.facadeModule !== null &&
(this.facadeModule.preserveSignature !== false || this.strictFacade)
) {
const exportNamesByVariable = this.facadeModule.getExportNamesByVariable();
for (const [variable, exportNames] of exportNamesByVariable) {
this.exportNamesByVariable.set(variable, [...exportNames]);
for (const exportName of exportNames) {
this.exportsByName.set(exportName, variable);
}
remainingExports.delete(variable);
}
}
if (this.outputOptions.minifyInternalExports) {
//压缩变量名称
assignExportsToMangledNames(remainingExports, this.exportsByName, this.exportNamesByVariable);
} else {
assignExportsToNames(remainingExports, this.exportsByName, this.exportNamesByVariable);
}
if (this.outputOptions.preserveModules || (this.facadeModule && this.facadeModule.info.isEntry))
this.exportMode = getExportMode(
this,
this.outputOptions,
this.facadeModule!.id,
this.inputOptions.onwarn
);
}
await renderChunks(chunks, outputBundle, this.pluginDriver, this.outputOptions, this.inputOptions.onwarn); 函数内部主要逻辑可以分为以下几步:
- 设置入口 chunk 的 preliminaryFileName
- 执行chunk.render()
- 生成 chunkGraph
- addChunksToBundle
renderChunks 主要作用是生成最终的 outputBundle。例如:
{index.js: {…}, acorn-bf6b1c54.js: {…}}
最终 bundle.generate 函数内部将 outputBundleBase 返回。
generate 总结
generate 内部主要分为以下几个步骤:
- 定义 outputBundle - const outputBundle = getOutputBundle(outputBundleBase); 。
- 获取 chunks - const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
- 校验 outputOptions 选项是否合法 - validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
- 设置 chunk.exportMode、chunk.exportsByName、chunk.exportNamesByVariable 等 - chunk.generateExports();
- 生成最终的 outputBundle。 - await renderChunks(chunks, outputBundle, this.pluginDriver, this.outputOptions, this.inputOptions.onwarn)
- 将 outputBundleBase 返回。
下一节我们继续深入分析 renderChunks 内部逻辑。
rollup 揭秘相关文章
- rollup 技术揭秘系列一 准备篇(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列二 源码目录结构及打包入口分析(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列三 rollup 函数(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列四 graph.build(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列五 构建依赖图谱(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列六 模块排序(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列七 includeStatements(可能是全网最系统性的 rollup 源码分析文章
- rollup 技术揭秘系列八 node.hasEffects(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列九 module.include()(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十 includeStatements 总结(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十一 rollup 打包配置选项整理(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十二 handleGenerateWrite(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十三 bundle.generate(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十四 renderChunks(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十五 renderModules(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十六 Rollup 插件开发指南(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十七 rollup-cli 的开发(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十八 Rollup 打包流程示意图(可能是全网最系统性的 rollup 源码分析文章)