demo
// main.js
import { name, age } from "./msg.js";
let msg = 123;
console.log(name);
// msg.js
export const name = "name";
export const age = "age";
// View the AST syntax tree at https://astexplorer.net/
// rollup.config.js
import babel from "@rollup/plugin-babel";
export default {
input: "./main.js".output: {
file: "dist/bundle.js".format: "es",},plugins: [
babel({
babelHelpers: "bundled".exclude: "node_modules/**".presets: ["@babel/preset-env"],}),]};// npm script
"build": "rollup -c"
Copy the code
1. Start from the entrance
- A rollup -c command
// Package. json bin and main fields
"bin": {
"rollup": "dist/bin/rollup"
}
"main": "dist/rollup.js".Copy the code
- bin/rollup
const rollup = require('.. /shared/rollup.js')
/ / parameters
const command = yargsParser(process.argv.slice(2))
/ / perform a rollup
runRollup(command);
async function runRollup(command) {
const { options } = await getConfigs(command);
for (const inputOptions of options) {
awaitbuild(inputOptions, warnings, command.silent); }}// build performs packaging
async function build(inputOptions) {
The rollup function returns several methods in the bundle
const bundle = await rollup.rollup(inputOptions);
// Call the write method returned by rollup
await Promise.all(outputOptions.map(bundle.write));
}
Copy the code
2. A rollup function
// shared/rollup.js
function rollup(rawInputOptions) {
return rollupInternal(rawInputOptions, null);
}
// The main process does not care watcher
function rollupInternal(rawInputOptions, watcher) {
Standard parameters will have some default parameters
const { options: inputOptions, unsetOptions: unsetInputOptions } = awaitgetInputOptions(rawInputOptions, watcher ! = =null);
// 2. Initialize Graph(the core of rollup) to compile objects like webPack's compiler.
const graph = new Graph(inputOptions, watcher);
// 3. Run the build method to generate module
Rollup is also a basic plugin with many hooks
await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
await graph.build();
await graph.pluginDriver.hookParallel('buildEnd'[]);// 4. Define write function to return result
const result = {
async close() {},
async generate() {},
watchFiles: Object.keys(graph.watchFiles),
// The write method is called after rollup generates the bundle
async write(rawOutputOptions) {
return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph); }}}Copy the code
2.1. GetInputOptions
function getInputOptions(rawInputOptions, watchMode) {
// Make sure plugins are arrays
const rawPlugins = ensureArray(rawInputOptions.plugins);
// Format the configuration rollup.config.js
const { options, unsetOptions } = normalizeInputOptions(
await rawPlugins.reduce(
// Handle the plug-in's options argument wrapped in a Promise array
applyOptionHook(watchMode),
Promise.resolve(rawInputOptions)
)
);
// Set the default plugin name index
normalizePlugins(options.plugins);
return { options, unsetOptions };
}
Copy the code
2.2 Graph
// Initialize many concepts in this
class Graph {
constructor(options, watcher) {
this.modulesById = {}
this.modules =[]
this.options = options
// 1. Path-tracking system skips first
this.deoptimizationTracker = new PathTracker();
/ / cache
this.cachedModules = new Map(a);if(options.cache ! = =false) {}
// Listen mode
if(watcher) {}
// 2. Plug-in driver to inject file operation method plug-in environment context and other operations basePluginDriver
this.pluginDriver = new PluginDriver()
// 3. Global scope There are many types of global scope, block level and so on
this.scope = new GlobalScope();
// 4. js parser acorn
this.acornParser = acorn.Parser.extend(... options.acornInjectPlugins);// 5. Module loader is used for module parsing and loading
this.moduleLoader = newModuleLoader(); }}Copy the code
2.3 PluginDriver
// The plug-in driver uses pluginDriver to execute the hooks in the plug-in
class PluginDriver {
constructor(graph, options, userPlugins, pluginCache, basePluginDriver) {
this.basePluginDriver = basePluginDriver; // Root insert driver
// Set the context for the plug-in
this.pluginContexts.set()
}
hookFirst(){}
hookParallel(){}
// execute the plugin hook.apply()
runHook(){}}Copy the code
2.4 GlobalScope
// The GlobalVariable corresponding to the global scope
class GlobalScope extends Scope {}
class Scope{
// Add to scope
addDeclaration(){}}Copy the code
2.5 ModuleLoader
class ModuleLoader {
constructor(graph, modulesById, options, pluginDriver) {
this.modulesById = modulesById;
}
// Compilation addEntry in webpack?
async addEntryModules(unresolvedEntryModules, isUserDefined){}}Copy the code
3. build
// The build method of executing graph is mainly used to generate modules
function build() {
// 1. Building block diagram
await this.generateModuleGraph();
/ / 2. Sort
// this.sortModules();
// 3. Mark as contained
this.includeStatements();
}
Copy the code
3.1 generateModuleGraph
async generateModuleGraph() {
await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input))
}
function normalizeEntryModules(entryModules) {
return Object.entries(entryModules).map(([name, id]) = > ({
fileName: null,
id,
implicitlyLoadedAfter: [].importer: undefined,
name,
}));
}
Copy the code
3.2 addEntryModules
// name id filename
function addEntryModules(unresolvedEntryModules) {
unresolvedEntryModules.map(({ id, importer }) = >
// Parsing the file depends on the path of the file from the entry, which is main.js
this.loadEntryModule(id, true, importer, null))}Copy the code
3.3 loadEntryModule
function loadEntryModule() {
// Find the absolute path
const resolveIdResult = await resolveId();
return this.fetchModule(this.addDefaultsToResolvedId(resolveIdResult), undefined);
}
function addDefaultsToResolvedId(resolvedId) {
return {
id: resolvedId.id
}
}
Copy the code
3.4 fetchModule
function fetchModule({ id }, importer, isEntry) {
// 1. Generate modules
const module = new Module()
// 2
this.modulesById.set(id, module);
// 3. Add listeners for each module
this.graph.watchFiles[id] = true;
// 4. Get the contents of the file
await this.addModuleSource(id, importer, module);
// 5. Execute hook
await this.pluginDriver.hookParallel("moduleParsed"[module.info]);
// 6. Handle dependency recursive processing
await this.fetchStaticDependencies(module),
await this.fetchDynamicDependencies(module),
// 7. linkImports
module.linkImports();
// 8. Return to module
return module
}
Copy the code
3.4.1 track Module
// Each file is a module
class Module {
constructor(graph, id, options, isEntry) {
this.context = options.moduleContext(id);
this.info = {}
}
}
Copy the code
3.4.2 addModuleSource
function addModuleSource(id, importer, module) {
Load modules Execute the load method or directly read the contents of the file
let source =
(await this.pluginDriver.hookFirst("load", [id])) ?? (await readFile(id))
// 2. Add to the module content
const sourceDescription = { code: source };
module.setSource(
await transform(
sourceDescription,
module.this.pluginDriver,
this.options.onwarn
)
)
}
Copy the code
Rule 3.4.3 transform
function transform() {
return pluginDriver.hookReduceArg0('transform').then(code= > {
return {ast, code}
})
}
Copy the code
3.4.4 setSource
import { nodeConstructors } from "./ast/nodes/index";
function setSource(ast, code) {
Acorn.parser. parse(code) Generates the AST syntax tree
if(! ast) ast =this.graph.contextParse(this.info.code);
// 2. magicString
this.magicString = new MagicString(code, {});
// 3. Ast context
this.astContext = {
addExport: this.addExport.bind(this),
addImport: this.addImport.bind(this),
nodeConstructors,
};
// 4. Module scope
this.scope = new ModuleScope(this.graph.scope, this.astContext);
// 5. Analyze the AST
this.ast = new Program(ast, { context: this.astContext, type: 'Module' }, this.scope)
this.info.ast = ast
}
function addImport() {
const source = node.source.value;
this.sources.add(source);
}
Copy the code
3.4.5 nodeConstructors
// Constructors for different AST nodes are used in Program
import ImportDeclaration from "./ImportDeclaration";
import Program from "./Program";
import Identifier from "./Identifier";
export const nodeConstructors = {
ImportDeclaration,
Program,
Identifier,
};
Copy the code
3.4.6 Program
class Program extends NodeBase {
render(code, options) {
if (this.boy.length) {
renderStatementList(this.body, code, this.start, this.end, options);
} else {
super.render(code, options); }}}Copy the code
3.4.7 NodeBase
class NodeBase extends ExpressionEntity {
constructor(esTreeNode, parent, parentScope) {
// Create scope
this.createScope(parentScope);
// Parse the node
this.parseNode(esTreeNode);
this.initialise();
}
// Our esTreeNode results in something like {start: end: body: []}
parseNode(esTreeNode) {
Parse the constructor for the call in the body
for (const [key, value] of Object.entries(esTreeNode)) {
if (Array.isArray(value)) {
// Body attribute [statement]
for (const child of value) {
this[key].push(
child === null
? null
: // ImportDeclaration class ImportDeclaration
new this.context.nodeConstructors[child.type]()
);
}
}
}
}
}
// ImportDeclaration
class ImportDeclaration extends NodeBase {
// Call addImport in context
initialise() {
// this.sources.add(source); './ MSG 'is added here to sources
this.context.addImport(this); }}Copy the code
3.4.8 fetchStaticDependencies
We added a MSG recursively generated MSG module to import
function fetchStaticDependencies() {
Array.from(module.sources, async (source) =>
// this.fetchModule(resolvedId, importer, false); External dependencies are distinguished
this.fetchResolvedDependency(source, module.id, module.resolvedIds)
)
}
Copy the code
3.5 the result
// We get two modules corresponding to main and MSG
// Graph looks like this
{
modules: [mainModule, msgModule],
modulesById: {main: mainModule, msg: msgModule},
moduleLoader:ModuleLoader,
pluginDriver: PluginDriver.scope:GlobalScope: {children: [moduleScope, moduleScope]}
}
// Attributes in module
{
graph,
id,
name,
filename,
imports: [].exports: [],
scope,
ast,
info
}
Copy the code
4. bundle.write
// After rollup we get modules and then we instantiate bundle and generate chunk from module
// Write the outputBundle to the filesystem
The rollup function returns write
async write(rawOutputOptions) {
return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph);
}
async function handleGenerateWrite() {
Create createOutputPluginDriver
// outputOptions: {file:'dist/bundle.js'}
const { options: outputOptions, outputPluginDriver } =
getOutputOptionsAndPluginDriver();
// create the bundle
const bundle = new Bundle(outputOptions, outputPluginDriver, graph);
Call the bundle generate method
const generated = await bundle.generate();
// 4. Write data to the file system
await Promise.all(
Object.values(generated).map((chunk) = >
// fs.writeFile determines sourcemap
writeOutputFile(chunk, outputOptions)
)
);
/ / 5. Return
return createOutput(generated);
}
Copy the code
4.1 getOutputOptionsAndPluginDriver
function getOutputOptionsAndPluginDriver() {
// new PluginDriver(); Why does fileEmitter need a new one
const outputPluginDriver = inputPluginDriver.createOutputPluginDriver(rawPlugins);
return {
...getOutputOptions(),
outputPluginDriver
}
}
Copy the code
4.2
class Bundle {
constructor(outputOptions, unsetOptions, inputOptions, pluginDriver, graph) {
this.outputOptions = outputOptions;
this.unsetOptions = unsetOptions;
this.inputOptions = inputOptions;
this.pluginDriver = pluginDriver;
this.graph = graph; }}Copy the code
4.3 bundle. The generate
function generate() {
const outputBundle = Object.create(null);
// 1. RenderStart plugin
await this.pluginDriver.hookParallel('renderStart'[this.outputOptions, this.inputOptions]);
// 2. Generate chunks new Chunk()
const chunks = await this.generateChunks()
// 3. Get the public path
const inputBase = commondir(getAbsoluteEntryModulePaths(chunks));
// 4. Pre-render chunks
There is no asynchronous code between pre-rendering and rendering
// {banner: '', footer: '', intro: '', outro: ''}
const addons = await createAddons(this.outputOptions, this.pluginDriver);
// Call the preRender method of chunk
this.prerenderChunks(chunks, inputBase);
// 5. Add chunk to the bundle and call the Render method of Chunk
// Object.assign(outputChunk, await chunk.render())
await this.addFinalizedChunksToBundle(chunks, inputBase, addons, outputBundle);
this.finaliseAssets(outputBundle);
// Where code is handled tree-shaking is handled
// bundle.js: { code:'const name = "name"; \n\nconsole.log(name); \n', fileName:'bundle.js'}
return outputBundle
}
Copy the code
4.3.1 generateChunks
function generateChunks() {
const chunks = []
const chunkByModule = new Map(a)// {alias: null, modules: [main.js, msg.js]} Two modules generated in the previous step
const chunkDefinitions = getChunkAssignments(this.graph.entryModules)
for(const { alias, modules } of chunkDefinitions) {
// Generating chunks is similar to webpack. In general, one entry corresponds to one chunk.
const chunk = new Chunk(modules, this.inputOptions)
chunks.push(chunk)
}
return [...chunks]
}
function addManualChunks(manualChunks) {
addModuleToManualChunk(alias, entry, manualChunkAliasByEntry);
}
function getChunkAssignments() {
for(const entry of entryModules) {
assignEntryToStaticDependencies(entry, null);
}
// dynamicEntryModules
// assignEntryToStaticDependencies()
// {alias: null, modules}chunkDefinitions.push(... createChunks([...entryModules, ...dynamicEntryModules]));return chunkDefinitions;
}
Copy the code
4.3.2 prerenderChunks
function prerenderChunks() {
for (const chunk of chunks) {
chunk.generateExports();
}
for (const chunk of chunks) {
chunk.preRender(this.outputOptions, inputBase); }}Copy the code
4.3.3 addFinalizedChunksToBundle
function addFinalizedChunksToBundle() {
for(const chunk of chunks) {
// Add chunk to outputBundle
outputBundle[chunk.id] = chunk.getChunkInfoWithFileNames();
}
await Promise.all(chunks.map(async (chunk) => {
const outputChunk = outputBundle[chunk.id];
// Call chunk's render method to get the code
Object.assign(outputChunk, await chunk.render(this.outputOptions, addons, outputChunk)); }}))Copy the code
4.3.4 finaliseAssets
function finaliseAssets(outputBundle) {
for(const file of Object.values(outputBundle)) {
if (this.outputOptions.validate && typeof file.code == 'string') {
this.graph.contextParse(file.code)
}
}
this.pluginDriver.finaliseAssets();
}
Copy the code
5. chunk
class Chunk {
constructor(orderedModules, modulesById) {
this.orderedModules = orderedModules
this.modulesById = modulesById;
this.imports = new Set(a);const chunkModules = new Set(orderedModules);
// We generate main MSG with two modules
for(const module of orderedModules) {
}
}
}
Copy the code
5.1 preRender
function preRender(options, inputBase) {
const magicString = new MagicStringBundle()
const renderOptions = {}
for(const module of this.orderedModules) {
// Call the render method on module
const source = module.render(renderOptions)
// We end up with the resource generated by Module Render
this.renderedModuleSources.set(module, source);
magicString.addSource(source);
// [name] [age] removed
const { renderedExports, removedExports } = module.getRenderedExports();
const { renderedModuleSources } = this;
// The final code generated by the rendered module
renderedModules[module.id] = {
get code() {
var _a, _b;
return (_b =
(_a = renderedModuleSources.get(module= = =))null || _a === void 0
? void 0: _a.toString()) ! = =null&& _b ! = =void 0
? _b
: null;
},
originalLength: module.originalCode.length, removedExports, renderedExports, }; }}Copy the code
5.2 render
function render(options, addons, outputChunk) {
// 1. this.dependencies
// 2. finaliseDynamicImports
// 3. finaliseImportMetas
// 4. scope
const format = options.format;
// Different formats we have here is es
const finalise = finalisers[format];
// renderedSource is handled preRender
const magicString = finalize(this.renderedSource, {})
const prevCode = magicString.toString();
let code = await renderChunk({code: prevCode})
if (options.sourcemap) {}
return {code, map}
}
Copy the code
5.3 renderChunk
function renderChunk({code, options, renderChunk}) {
return outputPluginDriver.hookReduceArg0('renderChunk', [code, renderChunk, options], renderChunkReducer);
}
Copy the code
5.4 es
// source: {}
function es(magicString) {
const importBlock = getImportBlock(dependencies, _)
const exportBlock = getExportBlock(exports, _, varOrConst)
return magicString.trim();
}
Copy the code
6. module
// The source we get in chunk is generated by Module
for (const module of this.orderedModules) {
// walk through the two modules we get [main, MSG]
Const name = 'name';
// Get console.log(name) from main.js
// We get the source and write the corresponding code to the file system based on the source
const source = module.render(renderOptions).trim();
}
// We end up with the content generated by the module
function render() {
const magicString = this.magicString.clone();
this.ast.render(magicString, options)
return magicString;
}
// Program processes the AST node from here
function render(code, options) {
renderStatementsList(this.body,code)
}
Copy the code
6.1 the Program
function render(code, options) {
// Start processing the AST tree
renderStatementList(this.body, code, this.start, this.end, options);
}
Copy the code
6.2 renderStatementList
function renderStatementList() {
for (let nextIndex = 1; nextIndex <= statements.length; nextIndex++) {
// Different statements call their own render methods
// We call the render method of ExportNamedDeclaration when we first process MSG
if(currentNode.included) {
currentNode.render(code, options, {})
} else {
// export const age does not include treeShaking.
treeshakeNode(currentNode, code)
}
}
}
Copy the code
6.2.1 ExportNamedDeclaration
function render(code, options) {
// Remove the export keyword
code.remove(this.start, this.declaration.start);
// VariableDeclaration
this.declaration.render(code, options, { end, start });
}
Copy the code
6.2.2 VariableDeclaration
function render() {
// VariableDeclarator const name = 'name'
for (const declarator of this.declarations) { declarator.render(code, options); }}Copy the code
6.2.3 VariableDeclarator
function render(code, options) {
this.id.render(code, options); // Identifier name
this.init.render(code, options) // Literal 'name'
}
Copy the code
6.2.4 ExpressionStatement
// The first ImportDeclaration directly treeshakeNode in main.js
// The second let MSG VariableDeclaration also drops the treeshake directly
// The third one is an ExpressionStatement
function render(code, options) {
super.render(code, options)
this.insertSemicolon(code);
}
Copy the code
6.2.5 NodeBase
function render() {
// ['expression']
for (const key of this.keys) {
value.render(code, options)
}
}
Copy the code
6.2.6 CallExpression
function render() {
// 1. callee MemberExpression
this.callee.render(code, options, {})
// 2. arguments name Identifier
for (const arg of this.arguments) { arg.render(code, options); }}Copy the code
6.2.7 MemberExpression
function render() {
// object, property Identifier
this.object.render();
this.property.render(code, options);
}
Copy the code
6.2.8 Identifier
function render() {
const name = this.variable.getName();
// Don't want to wait for replace
}
Copy the code
6.3 treeshakeNode
function treeshakeNode(node, code, start, end) {
code.remove(start, end)
}
Copy the code
6.4 include
// If we need tree-shaking for ast nodes, it is included
// When did we add this attribute? We got our module by rollup earlier and had the AST property on the module
// return to rollup
// graph.build()
// => generateModuleGraph()
// => addEntryModules()
// => addEntryModules
// => loadEntryModule
// => fetchModule
// => new Module() addModuleSource()
// ==> module.setSource(await transform())
// code: main.js msg.js
function setSource({ast, code}) {
this.astContext = {}
this.scope = new ModuleScope()
this.ast = new Program(ast, { context: this.astContext, type: 'Module' }, this.scope)
}
Ast included:false (false)
// We will recursively rely on the MSG in main
await this.fetchStaticDependencies()
// After the build is complete we will implement the included property modified here
this.includeStatements()
Copy the code
6.4.1 includeStatements
function includeStatements() {
module.includeAllExports(false);
for (const module of this.modules) {
Include shouldBeIncluded = false
Include this.ast. Include (content,false)
// The MSG module will be recursively processed after the name variable is accessed from the main console
module.include()
}
}
// Different types are simply understood to mean that the variables we access will be included
function include() {
const context = createInclusionContext();
// this.ast = new Program
/ / NodeBase return this. The included | | (! content.brokenFlow)
// This. Included = true;
if(this.ast.shouldBeIncluded(context)) {
this.ast.include(context, false)}}// Program
function include(context, includeChildrenRecursively) {
this.included = true;
// Execute node include
for (const node of this.body) {
// console.log() will be executed
// MSG will execute for name but not for age
// How is this attribute determined?
if(includeChildrenRecursively || node.shouldBeIncluded(context)) {
node.include(context)
}
}
}
// How is the brokenFlow attribute determined
function shouldBeIncluded(context) {
return this.included || (! context.brokenFlow &&this.hasEffects(createHasEffectsContext()));
}
// CallExpression
function include() {
this.included = true;
this.callee.include(context, false);
/ / handle the arguments
this.callee.includeCallArguments(context, this.arguments);
}
// callee MemberExpression
function include() {
this.included = true;
this.object.include(context, includeChildrenRecursively);
this.property.include(context, includeChildrenRecursively);
}
// Identifier
function include() {
this.included = true;
// Call module's methods when variable name
this.context.includeVariableInModule(this.variable);
}
function includeVariableInModule() {
// this.graph.needsTreeshakingPass = true; Next time I iterate over MSG
// handle MSG recursively
this.includeVariable(variable);
this.imports.add(variable); // The imports of the main module have a name variable
}
function includeVariable() {
variable.include(); // LocalVariable
}
// GlobalVariable console log
// VariableDeclaration
function include() {
this.include = true
// Name will be included
for (const declarator of this.declarations) {
if(includeChildrenRecursively || declarator.shouldBeIncluded(context)) declarator.include(context, includeChildrenRecursively); }}// VariableDeclarator id init
Copy the code
6.4.2 shouldBeIncluded
// context.brokenFlow
function shouldBeIncluded(context) {
return this.included || (! context.brokenFlow &&this.hasEffects())
}
function include() {
const context = createInclusionContext();
}
function createInclusionContext() {
return {
brokenFlow: BROKEN_FLOW_NONE, / / 0}}function hasEffects() {
for (const node of this.body) {
if (node.hasEffects(context)) {
return (this.hasCachedEffect = true); }}return false;
}
// ExportNamedDeclaration
function hasEffects(context) {
return this.declaration ! = =null && this.declaration.hasEffects(context);
}
// NodeBase
function hasEffects(context) {
child.hasEffects(context)
}
// VariableDeclarator
function hasEffects(context) {
// id is the variable name init is the value
// class LocalVariable
return this.id.hasEffects(context) || (this.init ! = =null && this.init.hasEffects(context));
}
// Identifier
function hasEffects() {
// Console will go here and include
return this.variable instanceof GlobalVariable && this.variable.hasEffectsWhenAccessedAtPath(EMPTY_PATH)
}
// ImportDeclaration
function hasEffects() return false
// ExpressionStatement
function hasEffects() {
child.hasEffects(} / /CallExpression
function hasEffects() {
argument.hasEffects(context)
this.callee.hasEffects(context} / /MemberExpression object property
// LocalVariable
function include() {
this.included = true;
// Change the name of export to include
for(const declaration of this.declarations) {
declaration.include(createInclusionContext(), false); }}Copy the code