Rollup packaging principle

preface

Rollup has been introduced in detail in the last article. This article will learn the rollup principle. Due to space constraints, the code for the original version of Rollup (version 0.3.0) was pulled. My goal is to learn how rollup is packaged, how to do tree-shaking. The original source code already implements both of these functions (semi-finished), so it is sufficient to look at the original source code.

Front knowledge

Rollup uses the Acorn and Magic-String libraries. To read the rollup source code, you must know something about them.

"dependencies": {
  "acorn": "^ 1.1.0." "."magic-string": "^ 0.5.1"."sander": "^ 0.3.3"
},
Copy the code

magic-string

Magic-string is a tool for manipulating strings and generating source-maps. Magic-string is a library of string manipulations written by rollup authors. Here’s an example on Github:

var MagicString = require('magic-string');
var magicString = new MagicString('export var name = "beijing"');
// Similar to intercepting a string
console.log(magicString.snip(0.6).toString()); // export
// Delete string from start to finish (index is always based on original string, not changed)
console.log(magicString.remove(0.7).toString()); // var name = "beijing"

// Many modules, to package them in one file, requires merging the source code of many files together
let bundleString = new MagicString.Bundle();
bundleString.addSource({
    content:'var a = 1; '.separator:'\n'
});
bundleString.addSource({
    content:'var b = 2; '.separator:'\n'
});
/* let str = ''; str += 'var a = 1; \n' str += 'var b = 2; \n' console.log(str); * /
console.log(bundleString.toString());
// var a = 1;
//var b = 2;

Copy the code

AST

JavaScript Parser can transform the code into an abstract syntax tree AST, which defines the structure of the code. By manipulating this tree, we can accurately locate declaration statements, assignment statements, operation statements, etc., and realize the code analysis, optimization, change and other operations

AST workflow

  • Parse converts source code into an abstract syntax tree with many ESTree nodes
  • Transform Transforms the abstract syntax tree
  • Generate generates new code from the transformed abstract syntax tree of the previous step

acorn

  • Astexplorer can turn code into a syntax tree

  • Acorn results comply with The Estree Spec

Import $from ‘jquery ast

You can see that the AST is of type Program, indicating that this is a program. The body contains the AST child nodes for all the following statements of the program.

Each node has a type of type, such as Identifier, indicating that the node is an Identifier.

For more details on AST nodes, see the article parsing JavaScript with Acorn.

How does rollup pack

Let’s use it briefly and analyze the packaging results

The directory structure is as follows:

├─ ├─ ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txt ├─ download.txtCopy the code

Install rollup and configure the execute script statement

npm install rollup -S -D
Copy the code

package.json

{
  "name": "rollup-demo"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "build": "rollup --config"
  },

  "keywords": []."author": ""."devDependencies": {
    "rollup": "^ 2.46.0"}}Copy the code

rollup.config.js

export default {
  input: './src/main.js'.// Import file
  output: {
    file: './dist/bundle.js'.// Save the file after packing
    format: 'cjs'.// Output format AMD ES6 IIFE UMD CJS
    name: 'bundleName'If iife, umD needs to specify a global variable}}Copy the code

src/mian.js

import { name, age } from './modules/myModule'

function say() {
  console.log(`my name is ${name}`);
}

console.log(age);
say();

Copy the code

modules/myModule.js

export const name = 'jiahang'
export const age = 18
export const height = 180
Copy the code

Execute NPM run build, open dist directory, and see bundle.js as follows:

'use strict';

const name = 'jiahang';
const age = 18;

function say() {
  console.log(`my name is ${name}`);
}

console.log(age);
say();

Copy the code

As you can see, rollup is very compact, and unused variables and methods are not packed, so development libraries and the like use rollup to reduce the size of their code.

In rollup, a file is a module. Each module generates an AST syntax abstraction tree based on the code of the file, and rollup needs to analyze each AST node. Parsing an AST node is to see if it calls any functions or methods. If so, check to see if the called function or method is in the current scope, and if not, look up until you find the module’s top-level scope. If this module is not found, it indicates that the function or method depends on other modules and needs to be imported from other modules.

Simple rollup implementation

The easy version does not currently consider module dependencies and variable scope

Main.js entry file

console.log('hello');
console.log('world');
Copy the code

lib/rollup.js

let Bundle = require('./bundle');
function rollup(entry,outputFileName){
    //Bundle represents the package object, which contains all module information
    const bundle = new Bundle({entry});
    // Call the build method to start compiling
    bundle.build(outputFileName);
}
module.exports = rollup;
Copy the code

This is the rollup package entry, takes the entry path and packaged name, instantiates a Bundle, calls the instance’s build method, and the core logic is the Bundle class.

const fs = require('fs');
const MagicString = require('magic-string');
const Module = require('./module');

class Bundle {
  constructor(options) {
    // The absolute path to the entry file, including the suffix
    this.entryPath = options.entry.replace(/\.js$/.' ') + '.js';
    this.modules = {};// Store all module entry files and their dependent modules
  }

  build(outputFileName) {
    // Find the module definition from the absolute path of the entry file
    let entryModule = this.fetchModule(this.entryPath);
    // Expand all statements in the entry module to return an array of all statements
    this.statements = entryModule.expandAllStatements();
    const { code } = this.generate();
    fs.writeFileSync(outputFileName, code, 'utf8');
  }

  // Get module information
  fetchModule(importee) {
    let route = importee;// The absolute path to the entry file
    if (route) {
      // Read the source code of this module from the hard disk
      let code = fs.readFileSync(route, 'utf8');
      let module = new Module({
        code,// Module source code
        path: route,// The absolute path of the module
        bundle: this// Which Bundle does it belong to
      });
      return module; }}// generate code for this.statements
  generate() {
    let magicString = new MagicString.Bundle();
    this.statements.forEach(statement= > {
      const source = statement._source;
      magicString.addSource({
        content: source,
        separator: '\n'
      });
    });
    return { code: magicString.toString() }; }}module.exports = Bundle;

Copy the code

Each file will generate a Module instance, including the source code of the module, the path of the module, and the abstract syntax tree ast of the module. Then, the syntax tree statements will be expanded to return an array of all statements. Finally, generate will be called to generate the final code.

The Module class

let MagicString = require('magic-string');
const { parse } = require('acorn');
const analyse = require('./ast/analyse');

/** * Each file is a Module, and each Module corresponds to a Module instance */
class Module {
  constructor({ code, path, bundle }) {
    this.code = new MagicString(code, { filename: path });
    this.path = path;// The path to the module
    this.bundle = bundle;// Which bundle instance belongs to
    this.ast = parse(code, {// Convert the source code into an abstract syntax tree
      ecmaVersion: 7.sourceType: 'module'
    });
    this.analyse();
  }

  analyse() {
    analyse(this.ast, this.code, this);
  }

  // Expand the statements in this module and place all statements for variables defined in those statements into the result
  expandAllStatements() {
    let allStatements = [];
    this.ast.body.forEach(statement= > {
      let statements = this.expandStatement(statement); allStatements.push(... statements); });return allStatements;
  }

  // Expand a node
  // Find the variables that the current node depends on, the variables it accesses, and the declarations for those variables.
  // These statements may be declared in the current module or in the imported module
  expandStatement(statement) {
    let result = [];
    if(! statement._included) { statement._included =true;// Indicates that the node is already included in the result and does not need to be added again
      // Tree shaking core is here
      result.push(statement);
    }

    returnresult; }}module.exports = Module;
Copy the code

ast/analyse.js

function analyse(ast, magicString, module) {
  ast.body.forEach(statement= > {// The top-level node below the body
    Object.defineProperties(statement, {
      //start refers to the starting index of the node in the source code, and end refers to the end index
      // MagicString. snip returns the magicString instance clone
      _source: { value: magicString.snip(statement.start, statement.end) }
    });
  });
}

module.exports = analyse;
Copy the code

Now let’s test out the simple version

debugger.js

const path = require('path');
const rollup = require('./lib/rollup');
// The absolute path to the entry file
let entry = path.resolve(__dirname,'src/main.js');
rollup(entry,'bundle.js');
Copy the code

The result of packing

console.log('hello');
console.log('world');
Copy the code

You can see the complete output package.

The full rollup implementation

The easy version of rollup simply copies the code together, leaving the module’s variable methods untouched.

Bundle. build(outputFileName) calls the fetchModule method of the Bundle instance after the build instance is instantiated. So let’s take a look at this method and do the above.

fetchModule(importee) {
  let route = importee;// The absolute path to the entry file
  if (route) {
    // Read the source code of this module from the hard disk
    let code = fs.readFileSync(route, 'utf8');
    let module = new Module({
      code,// Module source code
      path: route,// The absolute path of the module
      bundle: this// Which Bundle does it belong to
    });
    return module; }}Copy the code

Start from the entry file, each file will generate a Module instance, in the instantiation time will be to analyze the code, this time is to analyze the AST tree, the analysis of the time to find the Module import and export.

1. Process ast import and export

import {age} from './title';
age++;
export {
    age
};
Copy the code

Let’s take a look at what the AST of this code looks like

Methods in the Module class perfect the analyse method

analyse() {
  this.imports = {};// Store all the imports of the current module
  this.exports = {};// Store all exports of the current module
  this.ast.body.forEach(node= > {
    if (node.type === 'ImportDeclaration') {// this is an import statement
      let source = node.source.value;//./ MSG which module is imported from
      let specifiers = node.specifiers;
      specifiers.forEach(specifier= > {
        const name = specifier.imported.name;//name
        const localName = specifier.local.name;//name
        // Which local variable is exported from which module variable
        //this.imports.age = {name:'age',localName:"age",source:'./msg'};
        this.imports[localName] = { name, localName, source }
      });
    } else if (node.type === 'ExportNamedDeclaration') {
      let declaration = node.declaration;//VariableDeclaration
      if (declaration.type === 'VariableDeclaration') {
        let name = declaration.declarations[0].id.name;//age
        // Make a note of the expression through which the age is created for the current module export
        //this.exports['age']={node,localName:age,expression}
        this.exports[name] = {
          node, localName: name, expression: declaration
        }
      }
    }
  });
  analyse(this.ast, this.code, this);
}
Copy the code

After this process, each file module import/export is included in imports and exports.

Scope Scope class

Analyze the Scope between each AST node, find out the variables defined by each AST node, and generate a Scope instance for each AST node traversed.

The scope chain is composed of a series of variable objects of the current execution environment and the upper execution environment. It guarantees the orderly access of the variables and functions that meet the access permissions of the current execution environment. The core of tree-shaking principle is based on such a scope chain.

class Scope {
  constructor(options = {}) {
    this.name = options.name;// The scope name is not useful, just to help people know
    this.parent = options.parent;// Parent scope
    this.depth = this.parent ? this.parent.depth + 1 : 0 // Scope level
    this.names = options.params || [];// What are the variables in this action
  }

  add(name, isBlockDeclaration) {
		if(! isBlockDeclaration &&this.isBlockScope) {
			this.parent.add(name, isBlockDeclaration)
		} else {
			this.names.push(name)
		}
	}

  findDefiningScope(name) {
    if (this.names.includes(name)) {
      return this;
    }
    if (this.parent) {
      return this.parent.findDefiningScope(name);
    }
    return null; }}module.exports = Scope;
Copy the code

Just to test this out,

let Scope = require('./scope');
let a = 1;

function one() {
  let b = 2;

  function two(age) {
    let c = 3;
    console.log(a, b, c, age);
  }

  two();
}

one();
let globalScope = new Scope({
  name: 'globalScope'.params: [].parent: null
});
globalScope.add('a');
let oneScope = new Scope({
  name: 'oneScope'.params: [].parent: globalScope
});
oneScope.add('b');
let twoScope = new Scope({
  name: 'twoScope'.params: ['age'].parent: oneScope
});
twoScope.add('c');

let aScope = twoScope.findDefiningScope('a');
console.log(aScope.name);

let bScope = twoScope.findDefiningScope('b');
console.log(bScope.name);

let cScope = twoScope.findDefiningScope('c');
console.log(cScope.name);

let ageScope = twoScope.findDefiningScope('age');
console.log(ageScope.name);

let xxxScope = twoScope.findDefiningScope('xxx');
console.log(xxxScope);

Copy the code

The print result is as follows:

// 1 2 3 undefined
// globalScope
// oneScope
// twoScope
// twoScope
// null
Copy the code

The Scope is simple in that it has an array of Names attributes that hold variables within this AST node. Rollup builds the Scope of the variable from the Scope chain.

3. Implementation of walk method

/ * * * *@param {*} Ast Syntax tree to traverse *@param {*} Param1 Configures the object */
function walk(ast, { enter, leave }) {
  visit(ast, null, enter, leave);
}

/** * Access the node *@param {*} node
 * @param {*} parent
 * @param {*} enter
 * @param {*} leave* /
function visit(node, parent, enter, leave) {
  if (enter) {// Execute the Enter method on this node first
    enter(node, parent);// If you don't care about this, you can write it like this
    //enter.call(null,node,parent); // If you want to specify this in Enter
  }
  // Iterate over the child nodes to find which are the child nodes of the object
  let childKeys = Object.keys(node).filter(key= > typeof node[key] === 'object');
  childKeys.forEach(childKey= > {//childKey=specifiers value=[]
    let value = node[childKey];
    if (Array.isArray(value)) {
      value.forEach((val) = > visit(val, node, enter, leave));
    } else {
      visit(value, node, enter, leave)
    }
  });
  // Execute the exit method again
  if(leave) { leave(node, parent); }}module.exports = walk;
Copy the code

Test use of the walk method

let acorn = require('acorn');
let walk = require('./walk');
The parse method converts source code to an abstract syntax tree
let astTree = acorn.parse(`import $ from 'jquery'; `, {
  locations: true.ranges: true.sourceType: 'module'.ecmaVersion: 8
});
let ident = 0;
const padding = () = > ' '.repeat(ident);
//console.log(astTree.body);
// Walk through each statement in the syntax tree
astTree.body.forEach(statement= > {
  // Each statement is passed to the walk method, which iterates through the sentence elements
  // Use the depth-first method for traversal
  walk(statement, {
    enter(node) {
      if (node.type) {
        console.log(padding() + node.type + '进入');
        ident += 2; }},leave(node) {
      if (node.type) {
        ident -= 2;
        console.log(padding() + node.type + 'leave'); }}}); });Copy the code

Depth traversal is used, as shown

The print result is as follows

ImportDeclaration Enter ImportDefaultSpecifier enter Identifier Enter Identifier Leave ImportDefaultSpecifier leave Literal enter Literal leave ImportDeclaration leaveCopy the code

4. Analyze identifiers and find their dependencies

What is an identifier? Variable names, function names, and attribute names are all classified as identifiers. When an identifier is resolved, rollup traverses its current scope to see if it exists. If not, look for its parent scope. If the top-level scope of the module is not found, the function or method depends on other modules and needs to be imported from other modules. If a function or method needs to be imported, add it to the _dependsOn object of the Module. That’s the tree-shaking principle of rollup, rollup doesn’t look at what functions you bring in, it looks at what functions you call. If the function called is not in this module, it is imported from another module. In other words, if you manually introduce a function at the top of the module but don’t call it.

In Module instantiations, we’ve collected imports and exports for each Module and executed analyse(this.ast, this.code, this). In analyse, we use walk and scope to analyse identifiers and find their dependencies.

let Scope = require('./scope');
let walk = require('./walk');

/** * Find out which variables are used by the current module * also know which variables are declared by the current module and which variables are imported from other modules *@param {*} Ast Syntax tree *@param {*} MagicString source code@param {*} Module Belongs to the */ module
function analyse(ast, magicString, module) {
  let scope = new Scope();// Create a global scope within the module
  // Traverses all the top nodes of the current syntax tree
  ast.body.forEach(statement= > {
    // Add a variable to scope var function const let variable declaration
    function addToScope(declaration) {
      var name = declaration.id.name;// Get the declared variable
      scope.add(name);Add the say variable to the current global scope
      if(! scope.parent) {If the current scope is global
        statement._defines[name] = true;Declare a global variable say in the global scope}}Object.defineProperties(statement, {
      _defines: { value: {}},// Store all global variables defined by the current module
      _dependsOn: { value: {}},// External variables that are not defined but used by the current module
      _included: { value: false.writable: true },// Whether this statement is already included in the package result
      //start refers to the starting index of the node in the source code, and end refers to the end index
      // MagicString. snip returns the magicString instance clone
      _source: { value: magicString.snip(statement.start, statement.end) }
    });
    // This step builds our scope chain
    walk(statement, {
      enter(node) {
        let newScope;
        switch (node.type) {
          case 'FunctionDeclaration':
            const params = node.params.map(x= > x.name);
            if (node.type === 'FunctionDeclaration') {
              addToScope(node);
            }
            // If a function declaration is iterated over, I create a new scope object
            newScope = new Scope({
              parent: scope,// Parent scope is the current scope
              params
            });
            break;
          case 'VariableDeclaration': // A new scope is not generated
            node.declarations.forEach(addToScope);
            break;
        }
        if (newScope) {// The current node declares a new scope
          // If this node generates a new scope, a _scope is placed on the node, pointing to the new scope
          Object.defineProperty(node, '_scope', { value: newScope }); scope = newScope; }},leave(node) {
        if (node._scope) {If this node produces a new scope, the scope returns to the parent scope after leaving the nodescope = scope.parent; }}}); });console.log('First pass', scope);
  ast._scope = scope;
  // Find external dependency _dependsOn
  ast.body.forEach(statement= > {
    walk(statement, {
      enter(node) {
        if (node._scope) {
          scope = node._scope;
        } // If the node has a scope community, the node generates a new scope
        if (node.type === 'Identifier') {
          // Recurse up from the current scope to find out in which scope this variable is defined
          const definingScope = scope.findDefiningScope(node.name);
          if(! definingScope) { statement._dependsOn[node.name] =true;// indicates that this is an externally dependent variable}}},leave(node) {
        if(node._scope) { scope = scope.parent; }}}); }); }module.exports = analyse;
Copy the code

After the analysis, you need to find the statements that define the global variables

this.definitions = {};// Define all global variables
this.ast.body.forEach(statement= > {
  Object.keys(statement._defines).forEach(name= > {
    // Key is the name of the global variable and the value is the statement that defines the global variable
    this.definitions[name] = statement;
  });
});
Copy the code

The full analyse method is as follows:

analyse() {
  this.imports = {};// Store all the imports of the current module
  this.exports = {};// Store all exports of the current module
  this.ast.body.forEach(node= > {
    if (node.type === 'ImportDeclaration') {// this is an import statement
      let source = node.source.value;//./ MSG which module is imported from
      let specifiers = node.specifiers;
      specifiers.forEach(specifier= > {
        const name = specifier.imported.name;//name
        const localName = specifier.local.name;//name
        // Which local variable is exported from which module variable
        //this.imports.age = {name:'age',localName:"age",source:'./msg'};
        this.imports[localName] = { name, localName, source }
      });
      //}else if(/^Export/.test(node.type)){
    } else if (node.type === 'ExportNamedDeclaration') {
      let declaration = node.declaration;//VariableDeclaration
      if (declaration.type === 'VariableDeclaration') {
        let name = declaration.declarations[0].id.name;//age
        // Make a note of the expression through which the age is created for the current module export
        //this.exports['age']={node,localName:age,expression}
        this.exports[name] = {
          node, localName: name, expression: declaration
        }
      }
    }
  });
  analyse(this.ast, this.code, this);// Find a dependson and defines their relationship
  this.definitions = {};// Define all global variables
  this.ast.body.forEach(statement= > {
    Object.keys(statement._defines).forEach(name= > {
      // Key is the name of the global variable and the value is the statement that defines the global variable
      this.definitions[name] = statement;
    });
  });

}
Copy the code

5. Optimize other methods

// Expand the statements in this module and place all statements for variables defined in those statements into the result
expandAllStatements() {
  let allStatements = [];
  this.ast.body.forEach(statement= > {
    if (statement.type === 'ImportDeclaration') {return}
    let statements = this.expandStatement(statement); allStatements.push(... statements); });return allStatements;
}

// Expand a node
// Find the variables that the current node depends on, the variables it accesses, and the declarations for those variables.
// These statements may be declared in the current module or in the imported module
expandStatement(statement) {
  let result = [];
  const dependencies = Object.keys(statement._dependsOn);// External dependencies [name]
  dependencies.forEach(name= > {
    // Find the declaration node that defines this variable, either in the current module or in a dependent module
    let definition = this.define(name); result.push(... definition); });if(! statement._included) { statement._included =true;// Indicates that the node is already included in the result and does not need to be added again
    result.push(statement);
  }
  return result;
}

define(name) {
  // Check if there is a name in the imported variable
  if (hasOwnProperty(this.imports, name)) {
    //this.imports.age = {name:'age',localName:"age",source:'./msg'};
    const importData = this.imports[name];
    Exports imports MSG module
    const module = this.bundle.fetchModule(importData.source, this.path);
    //this.exports['age']={node,localName:age,expression}
    const exportData = module.exports[importData.name];
    // Call the define method of the MSG module. The parameter is the local variable name of the MSG module. The purpose is to return the statement that defines the age variable
    return module.define(exportData.localName);
  } else {
    // Definitions is the name of the variable in the current module, and the value is the statement that defines the variable
    let statement = this.definitions[name];
    if(statement && ! statement._included) {return this.expandStatement(statement);
    } else {
      return[]; }}}Copy the code

6. Generate code

You need to call the Bundle’s generate() method to generate code. Also, during the packaging process, you need to do some extra work on the introduced functions,

  • Remove extra code

For example, the code for the foo1() function imported from foo.js looks like this: export function foo1() {}. Rollup removes export and becomes function foo1() {}. Because they are going to be packaged together, there is no need for export.

  • rename

For example, if two modules have a function foo() with the same name, when packaged together, one of the functions is renamed _foo() to avoid collisions.

// Get module information
fetchModule(importee, importer) {
  let route;
  if(! importer) {// If no module imports this module, it is said to be an entry module
    route = importee;
  } else {
    if (path.isAbsolute(importee)) {// If it is an absolute path
      route = importee;
    } else if (importee[0] = ='. ') {// If relative path
      route = path.resolve(path.dirname(importer), importee.replace(/\.js$/.' ') + '.js'); }}if (route) {
    // Read the source code of this module from the hard disk
    let code = fs.readFileSync(route, 'utf8');
    let module = new Module({
      code,// Module source code
      path: route,// The absolute path of the module
      bundle: this// Which Bundle does it belong to
    });
    return module; }}// generate code for this.statements
generate() {
  let magicString = new MagicString.Bundle();
  this.statements.forEach(statement= > {
    const source = statement._source;
    if (statement.type === 'ExportNamedDeclaration') {
      source.remove(statement.start, statement.declaration.start);
    }
    magicString.addSource({
      content: source,
      separator: '\n'
    });
  });
  return { code: magicString.toString() };
}
Copy the code

The complete code

module.js

const fs = require('fs');
const path = require('path');
const MagicString = require('magic-string');
const Module = require('./module');

class Bundle {
  constructor(options) {
    // The absolute path to the entry file, including the suffix
    this.entryPath = options.entry.replace(/\.js$/.' ') + '.js';
    this.modules = {};// Store all module entry files and their dependent modules
  }

  build(outputFileName) {
    // Find the module definition from the absolute path of the entry file
    let entryModule = this.fetchModule(this.entryPath);
    // Expand all statements in the entry module to return an array of all statements
    this.statements = entryModule.expandAllStatements();
    const { code } = this.generate();
    fs.writeFileSync(outputFileName, code, 'utf8');
  }

  // Get module information
  fetchModule(importee, importer) {
    let route;
    if(! importer) {// If no module imports this module, it is said to be an entry module
      route = importee;
    } else {
      if (path.isAbsolute(importee)) {// If it is an absolute path
        route = importee;
      } else if (importee[0] = ='. ') {// If relative path
        route = path.resolve(path.dirname(importer), importee.replace(/\.js$/.' ') + '.js'); }}if (route) {
      // Read the source code of this module from the hard disk
      let code = fs.readFileSync(route, 'utf8');
      let module = new Module({
        code,// Module source code
        path: route,// The absolute path of the module
        bundle: this// Which Bundle does it belong to
      });
      return module; }}// generate code for this.statements
  generate() {
    let magicString = new MagicString.Bundle();
    this.statements.forEach(statement= > {
      const source = statement._source;
      if (statement.type === 'ExportNamedDeclaration') {
        source.remove(statement.start, statement.declaration.start);
      }
      magicString.addSource({
        content: source,
        separator: '\n'
      });
    });
    return { code: magicString.toString() }; }}module.exports = Bundle;
Copy the code

bundle.js

let MagicString = require('magic-string');
const { parse } = require('acorn');
const analyse = require('./ast/analyse');

// Check whether the obj object has a prop property
function hasOwnProperty(obj, prop) {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

/** * Each file is a Module, and each Module corresponds to a Module instance */
class Module {
  constructor({ code, path, bundle }) {
    this.code = new MagicString(code, { filename: path });
    this.path = path;// The path to the module
    this.bundle = bundle;// Which bundle instance belongs to
    this.ast = parse(code, {// Convert the source code into an abstract syntax tree
      ecmaVersion: 7.sourceType: 'module'
    });
    this.analyse();
  }

  analyse() {
    this.imports = {};// Store all the imports of the current module
    this.exports = {};// Store all exports of the current module
    this.ast.body.forEach(node= > {
      if (node.type === 'ImportDeclaration') {// this is an import statement
        let source = node.source.value;//./ MSG which module is imported from
        let specifiers = node.specifiers;
        specifiers.forEach(specifier= > {
          const name = specifier.imported.name;//name
          const localName = specifier.local.name;//name
          // Which local variable is exported from which module variable
          //this.imports.age = {name:'age',localName:"age",source:'./msg'};
          this.imports[localName] = { name, localName, source }
        });
        //}else if(/^Export/.test(node.type)){
      } else if (node.type === 'ExportNamedDeclaration') {
        let declaration = node.declaration;//VariableDeclaration
        if (declaration.type === 'VariableDeclaration') {
          let name = declaration.declarations[0].id.name;//age
          // Make a note of the expression through which the age is created for the current module export
          //this.exports['age']={node,localName:age,expression}
          this.exports[name] = {
            node, localName: name, expression: declaration
          }
        }
      }
    });
    analyse(this.ast, this.code, this);// Find a dependson and defines their relationship
    this.definitions = {};// Define all global variables
    this.ast.body.forEach(statement= > {
      Object.keys(statement._defines).forEach(name= > {
        // Key is the name of the global variable and the value is the statement that defines the global variable
        this.definitions[name] = statement;
      });
    });

  }

  // Expand the statements in this module and place all statements for variables defined in those statements into the result
  expandAllStatements() {
    let allStatements = [];
    this.ast.body.forEach(statement= > {
      if (statement.type === 'ImportDeclaration') {return}
      let statements = this.expandStatement(statement); allStatements.push(... statements); });return allStatements;
  }

  // Expand a node
  // Find the variables that the current node depends on, the variables it accesses, and the declarations for those variables.
  // These statements may be declared in the current module or in the imported module
  expandStatement(statement) {
    let result = [];
    const dependencies = Object.keys(statement._dependsOn);// External dependencies [name]
    dependencies.forEach(name= > {
      // Find the declaration node that defines this variable, either in the current module or in a dependent module
      let definition = this.define(name); result.push(... definition); });if(! statement._included) { statement._included =true;// Indicates that the node is already included in the result and does not need to be added again
      result.push(statement);
    }
    return result;
  }

  define(name) {
    // Check if there is a name in the imported variable
    if (hasOwnProperty(this.imports, name)) {
      //this.imports.age = {name:'age',localName:"age",source:'./msg'};
      const importData = this.imports[name];
      Exports imports MSG module
      const module = this.bundle.fetchModule(importData.source, this.path);
      //this.exports['age']={node,localName:age,expression}
      const exportData = module.exports[importData.name];
      // Call the define method of the MSG module. The parameter is the local variable name of the MSG module. The purpose is to return the statement that defines the age variable
      return module.define(exportData.localName);
    } else {
      // Definitions is the name of the variable in the current module, and the value is the statement that defines the variable
      let statement = this.definitions[name];
      if(statement && ! statement._included) {return this.expandStatement(statement);
      } else {
        return[]; }}}}module.exports = Module;
Copy the code

annlyse.js

let Scope = require('./scope');
let walk = require('./walk');

/** * Find out which variables are used by the current module * also know which variables are declared by the current module and which variables are imported from other modules *@param {*} Ast Syntax tree *@param {*} MagicString source code@param {*} Module Belongs to the */ module
function analyse(ast, magicString, module) {
  let scope = new Scope();// Create a global scope within the module
  // Traverses all the top nodes of the current syntax tree
  ast.body.forEach(statement= > {
    // Add a variable to scope var function const let variable declaration
    function addToScope(declaration) {
      var name = declaration.id.name;// Get the declared variable
      scope.add(name);Add the say variable to the current global scope
      if(! scope.parent) {If the current scope is global
        statement._defines[name] = true;Declare a global variable say in the global scope}}Object.defineProperties(statement, {
      _defines: { value: {}},// Store all global variables defined by the current module
      _dependsOn: { value: {}},// External variables that are not defined but used by the current module
      _included: { value: false.writable: true },// Whether this statement is already included in the package result
      //start refers to the starting index of the node in the source code, and end refers to the end index
      // MagicString. snip returns the magicString instance clone
      _source: { value: magicString.snip(statement.start, statement.end) }
    });
    // This step builds our scope chain
    walk(statement, {
      enter(node) {
        let newScope;
        switch (node.type) {
          case 'FunctionDeclaration':
            const params = node.params.map(x= > x.name);
            if (node.type === 'FunctionDeclaration') {
              addToScope(node);
            }
            // If a function declaration is iterated over, I create a new scope object
            newScope = new Scope({
              parent: scope,// Parent scope is the current scope
              params
            });
            break;
          case 'VariableDeclaration': // A new scope is not generated
            node.declarations.forEach(addToScope);
            break;
        }
        if (newScope) {// The current node declares a new scope
          // If this node generates a new scope, a _scope is placed on the node, pointing to the new scope
          Object.defineProperty(node, '_scope', { value: newScope }); scope = newScope; }},leave(node) {
        if (node._scope) {If this node produces a new scope, the scope returns to the parent scope after leaving the nodescope = scope.parent; }}}); });console.log('First pass', scope);
  ast._scope = scope;
  // Find external dependency _dependsOn
  ast.body.forEach(statement= > {
    walk(statement, {
      enter(node) {
        if (node._scope) {
          scope = node._scope;
        } // If the node has a scope community, the node generates a new scope
        if (node.type === 'Identifier') {
          // Recurse up from the current scope to find out in which scope this variable is defined
          const definingScope = scope.findDefiningScope(node.name);
          if(! definingScope) { statement._dependsOn[node.name] =true;// indicates that this is an externally dependent variable}}},leave(node) {
        if(node._scope) { scope = scope.parent; }}}); }); }module.exports = analyse;
Copy the code

rollup.js

let Bundle = require('./bundle');

function rollup(entry, outputFileName) {
  //Bundle represents the package object, which contains all module information
  const bundle = new Bundle({ entry });
  // Call the build method to start compiling
  bundle.build(outputFileName);
}

module.exports = rollup;
Copy the code

Other file code already, will not post.

conclusion

Rollup is packaged and tree-shaking. Some details are not covered, such as the scope block, the rollup command line, and the watch rollup watch. For those interested, see the rollup source code.

Refer to the link

  • Use Acorn to parse JavaScript
  • magic-string