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

  1. A rollup -c command
// Package. json bin and main fields
"bin": {
  "rollup": "dist/bin/rollup"
}
"main": "dist/rollup.js".Copy the code
  1. 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