Table of Contents generated with DocToc

  • Rollup Tree Shaking
    • preface
    • Contrast webpack
    • How do I use rollup
    • Use the Tree Shaking feature
    • Rollup source code parsing
    • magic-string
    • acorn
      • AST workflow
      • AST parsing process
      • Scope
    • Implement a rollup
    • Realize the tree shaking
    • The dependent variable has been modified
    • Block-level scopes are supported
    • Handle entry tree shaking
    • Implement variable renaming
    • conclusion
    • reference

Rollup Tree Shaking

This article was first published on @careteen/rollup.

preface

Rollup comes out with ES2015 Scripts and Modules.

Next- Generation ES Module Bundler is the Next generation ES module bundler.

Angular/React/Vue, the most popular front-end frameworks, use rollup for packaging.

Rollup relies on ES6 modules to provide tree-shaking functionality. “Tree shaking” means shaking down the dead leaves of a tree. The equivalent in programming is to get rid of useless code. This can play a certain optimization role for large projects.

For example, some of the components in ant-Design are used in the background, but importing ant from ‘antd’ directly packages all of the component code, and the babel-plugin-import plug-in is available for on-demand loading. Reduce the size of the project to some extent to make the page appear faster to the user.

Contrast webpack

  • webpackCan be doneCode separation.Static resource processing.Hot Module Replacement
  • rollupsupportES6 module.tree-shakingPowerful function; butwebpackExport not supportedES6 module.
  • webpackPacking bulky,rollupPackaged compact, closer to source code.

Comparing the two features, webPack is more suitable for applications, while Rollup is more suitable for class libraries.

In the project, webpack was used for development. With the attitude of knowing what it is and why it is, I learned and realized a simple version of Webpack and studied its thermal update principle. Those who are interested can go and read.

How do I use rollup

The following sample code is stored in rollup base for debugging.

Create the rollup.config.dev.js file in the project root directory and do the following

// rollup.config.dev.js
import babel from 'rollup-plugin-babel'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import { terser } from 'rollup-plugin-terser'
import postcss from 'rollup-plugin-postcss'
import serve from 'rollup-plugin-serve'

export default {
  input: './src/index.ts'.output: {
    file: 'dist/index.js'.format: 'es',},plugins: [
    babel({
      exclude: 'node_modules/**',
    }),
    resolve(),
    commonjs(),
    typescript(),
    terser(),
    postcss(),
    serve({
      open: true.port: 2333.contentBase: './dist',})],}Copy the code
  • Rollup-plugin-babel supports compiling output with Babel using the new syntax.
  • @rollup/plugin-node-resolveSupports parsing of third-party modules, i.enode_modulesDirectory.
  • @rollup/plugin-commonjssupportCommonjs specification. (Because the default is only supportedES6 module)
  • @rollup/plugin-typescriptSupport the resolutiontypescript
  • rollup-plugin-terserSupport compressionjs
  • rollup-plugin-postcssSupport the compilationcss
  • Rollup-plugin-serve Supports local server startup

Create a new SRC /index.ts file

// src/index.ts
console.log('hello rollup')
Copy the code

Configure in package.json file

// package.json
"scripts": {
  "dev": "rollup --config rollup.config.dev.js -w",},Copy the code

Run the script NPM run dev

Use the Tree Shaking feature

Create a new file SRC /userinfo.ts

// src/userinfo.ts
export const name = 'careteen'
export const age = 25

Copy the code

Change the SRC/index. Ts

// src/index.ts
import { name, age } from './userinfo'
console.log(name)
Copy the code

Run the script NPM run dev to view dist/index.js

const name = 'careteen'
console.log(name)
Copy the code

As you can see, rollup combines the two files into a single file output, removing the garbage export const age = 25 and also removing the import/export statement.

Rollup source code parsing

The latest rollup is rich in functionality and complex in architecture. This article is about tree-shaking, so scroll through the rollup commit record and find the original version available at [email protected]. The number of files and lines of code are relatively small, and we can easily read.

. ├ ─ ─ Bundle │ └ ─ ─ index. The js # responsible for packaging ├ ─ ─ the Module │ └ ─ ─ index. The js # is responsible for parsing Module ├ ─ ─ the ast │ ├ ─ ─ the Scope. The js # Scope chain │ ├ ─ ─ analyse. Js # Parsing the ast syntax tree │ └ ─ ─ walk. Js # traversal ast syntax tree ├ ─ ─ finalisers # output type │ ├ ─ ─ micro js │ ├ ─ ─ CJS. Js │ ├ ─ ─ es6. Js │ ├ ─ ─ index. The js │ └ ─ ─ Umd. Js ├ ─ ─ a rollup. Js # entrance └ ─ ─ utils # tool function ├ ─ ─ the map - helpers. Js ├ ─ ─ object. The js ├ ─ ─ promise. Js └ ─ ─ replaceIdentifiers. JsCopy the code
  • rollup.jsPackage entry file
  • Bundle/index.jsPackage tool, which generates one when you packageBundleInstance, collect all the dependent modules, and finally package the code together for output
  • Module/index.jsEach file is a module
  • ast/Scope.jsBuild scopes and scope chains
  • ast/analyse.jsAnalysis of theAstScope and dependencies
  • ast/walk.jstraverseAst

A quick look at the third-party libraries that the code relies on to see magic-String and Acorn, let’s take a quick look.

magic-string

It is possible to make minor modifications and substitutions to the source code.

const MagicString = require('magic-string')

const s = new MagicString(`export var name = 'careteen'`)
// Returns the clipped character
console.log(s.snip(0.6).toString(), s.toString()) // export, export var name = 'careteen'
// Remove the specified position interval character
console.log(s.remove(0.7).toString(), s.toString()) // var name = 'careteen', var name = 'careteen'

// Concatenate source code with the specified delimiter
const b = new MagicString.Bundle()
b.addSource({
  content: `var name = 'careteen'`.separator: '\n',
})
b.addSource({
  content: `var age = 25`.separator: '\n',})console.log(b.toString()) // var name = 'careteen' \n var age = 25
Copy the code

acorn

The following code is stored at @careteen/ rollup-prepos. If you are interested, go to debug.

JavaScript parser that parses source code into an abstract syntax tree AST.

The tree defines the structure of the code. By manipulating the tree, you can accurately locate statements, assignment statements, operation statements, and so on. 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

AST parsing process

Astexplorer lets you preview the results of source code parsing in real time. And set the Parser Settings to Acorn.

The following code parsing results are shown in the figure

import $ from 'jquery';
Copy the code

So how do you traverse the abstract syntax tree and manipulate it at the right time?

Depth-first traversal is used.

For those of you who don’t know, go ahead and illustrate depth-first traversal

// walk.js
// DFS
function walk (node, { enter, leave }) {
  visit(node, null, enter, leave)
}

function visit (node, parent, enter, leave) {
  if (enter) {
    enter.call(null, node, parent)
  }
  const keys = Object.keys(node).filter(key= > typeof node[key] === 'object')
  keys.forEach(key= > {
    const value = node[key]
    if (Array.isArray(value)) {
      value.forEach(val= > {
        visit(val, node, enter, leave)
      })
    } else if (value && value.type) {
      visit(value, node, enter, leave)
    }
  })
  if (leave) {
    leave.call(null, node, parent)
  }
}

module.exports = walk
Copy the code

This logic is in source rollup/ast/walk.js

Output abstract syntax tree

// ast.js
const walk = require('./walk')
const acorn = require('acorn')

const ast = acorn.parse(
  `import $ from 'jquery'; `,
  {
    locations: true.ranges: true.sourceType: 'module'.ecmaVersion: 8,})let ident = 0
const padding = () = > ' '.repeat(ident)

// Test
ast.body.forEach(statement= > {
  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)
      }
    }
  })
})

// Output the result
/** ImportDeclaration ImportDefaultSpecifier Identifier Identifier ImportDefaultSpecifier Literal Literal ImportDeclaration */
Copy the code

The expected output looks like the structure above.

Scope

This file rollup/ast/ scope.js is a relatively independent and simple implementation of creating scopes.

// scope.js
class Scope {
  constructor(options = {}) {
    this.name = options.name
    this.parent = options.parent
    this.names = options.params || []
  }
  add(name) {
    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

This logic is in the source rollup/ast/scope.js

See how to use it

// useScope.js
const Scope = require('./scope')

var a = 1
function one() {
  var b = 2
  function two() {
    var c = 3
    console.log(a, b, c)
  }
  two()
}
one()

// Build scope chain
const globalScope = new Scope({
  name: 'global'.parent: null,})const oneScope = new Scope({
  name: 'one'.parent: globalScope,
})
const twoScope = new Scope({
  name: 'two'.parent: oneScope,
})

globalScope.add('a')
oneScope.add('b')
twoScope.add('c')

console.log(twoScope.findDefiningScope('a'))
console.log(oneScope.findDefiningScope('c'))
console.log(globalScope.findDefiningScope('d'))

// Output the result
/ / 1 2 3
// Scope { name: 'global', parent: null, names: [ 'a' ] }
// null
// null
Copy the code

This file primarily creates scopes and scope chains, mounts declared variables to the corresponding scope, and also provides the method findDefiningScope to find the scope in which a specific variable resides.

This is significant enough that Rollup can use it to determine if a variable is defined in the current file, otherwise import, recurse until it finds the scope of the variable definition, and then write the dependency.

The ES Module feature exports references to values. If it is modified after import, it will change the source, that is, it is only a shallow copy.

Once you find the scope of a variable, you can directly cut its source code and output it. (Detailed implementation later)

Implement a rollup

Create a new tunable configuration file, use SRC /index.js as the entry file, package it and export it to dest/bundle.js

// ./example/myRollup/rollup.config.js
const path = require('path')
const rollup = require('.. /.. /src/rollup')

const entry = path.resolve(__dirname, 'src/index.js')
rollup(entry, 'dest/bundle.js')
Copy the code

The import file relies on the bundle to actually compile and package it.

// .src/rollup.js
const Bundle = require('./bundle')

function rollup(entry, filename) {
  const bundle = new Bundle({
    entry,
  })
  bundle.build(filename)
}

module.exports = rollup
Copy the code

The initial idea is to parse the content of entryPath, parse the source code, and output it to the target file.

// .src/bundle.js
const path = require('path')

class Bundle {
  constructor(options) {
    this.entryPath = path.resolve(options.entry.replace(/\.js$/.' ') + '.js')
    this.modules = {}
  }
  build(filename) {
    console.log(this.entryPath, filename)
  }
}

module.exports = Bundle
Copy the code

The general process is

    1. Get the contents of the import file, wrapped intomoduleGenerate an abstract syntax tree
    1. Dependency parsing of the entry file abstract syntax tree
    1. Generate the final code
    1. Write to target file
// .src/bundle.js
const { readFileSync, writeFileSync } = require('fs')
const { resolve } = require('path')
const Module = require('./module')
const MagicString = require('magic-string')

class Bundle {
  constructor(options) {
    this.entryPath = resolve(options.entry.replace(/\.js$/.' ') + '.js')
    this.modules = {}
    this.statements = []
  }
  build(filename) {
    // 1. Get the contents of the entry file, wrap it as' module ', and generate an abstract syntax tree
    const entryModule = this.fetchModule(this.entryPath)
    // 2. Perform dependency parsing on the entry file abstract syntax tree
    this.statements = entryModule.expandAllStatements()
    // 3. Generate the final code
    const { code } = this.generate()
    // 4. Write to the target file
    writeFileSync(filename, code)
  }
  fetchModule(importee) {
    let route = importee
    if (route) {
      const code = readFileSync(route, 'utf-8')
      const module = new Module({
        code,
        path: importee,
        bundle: this,})return module}}generate() {
    const ms = new MagicString.Bundle()
    this.statements.forEach(statement= > {
      const source = statement._source.clone()
      ms.addSource({
        content: source,
        separator: '\n',})})return {
      code: ms.toString()
    }
  }
}

module.exports = Bundle
Copy the code

Each file is a Module that parses the source code into an abstract syntax tree, mounts the source code to nodes in the tree, and provides a way to expand and modify it.

// ./src/module.js
const { parse } = require('acorn')
const MagicString = require('magic-string')
const analyse = require('./ast/analyse')

class Module {
  constructor({ code, path, bundle, }) {
    this.code = new MagicString(code, {
      filename: path,
    })
    this.path = path
    this.bundle = bundle
    this.ast = parse(code, {
      ecmaVersion: 7.sourceType: 'module',})this.analyse()
  }
  analyse() {
    analyse(this.ast, this.code, this)}expandAllStatements() {
    const allStatements = []
    this.ast.body.forEach(statement= > {
      const statements = this.expandStatement(statement) allStatements.push(... statements) })return allStatements
  }
  expandStatement(statement) {
    statement._included = true
    const result = []
    result.push(statement)
    return result
  }
}

module.exports = Module
Copy the code

Mount the source code to a node in the tree.

// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement= > {
    Object.defineProperties(statement, {
      _source: {
        value: ms.snip(statement.start, statement.end)
      }
    })
  })
}

module.exports = analyse
Copy the code

Realize the tree shaking

Modify the debugging file content as follows to test the tree-shaking function

// ./example/myRollup/src/index.js
import { name, age } from './userinfo'

function say() {
  console.log('hi ', name)
}

say()
Copy the code

Dependent userinfo file

// ./example/myRollup/src/userinfo.js
export var name = 'careteen'
export var age = 25
Copy the code

The expected packing result is

var name = 'careteen'
function say() {
  console.log('hi ', name)
}

say()
Copy the code

Need to make the following // + additions and modifications

// ./src/bundle.js
const { readFileSync, writeFileSync } = require('fs')
const { resolve, isAbsolute, dirname } = require('path') // +
const Module = require('./module')
const MagicString = require('magic-string')

class Bundle {
  constructor(options) {
    this.entryPath = resolve(options.entry.replace(/\.js$/.' ') + '.js')
    this.modules = {}
    this.statements = []
  }
  build(filename) {
    const entryModule = this.fetchModule(this.entryPath)
    this.statements = entryModule.expandAllStatements()
    const { code } = this.generate()
    writeFileSync(filename, code)
  }
  fetchModule(importee, importer) { // +
    let route // +
    if(! importer) {// +
      route = importee // +
    } else { // +
      if (isAbsolute(importee)) { // +
        route = importee // +
      } else if (importee[0= = ='. ') { // +
        route = resolve(dirname(importer), importee.replace(/\.js$/.' ') + '.js') // +
      } // +
    } // +
    if (route) {
      const code = readFileSync(route, 'utf-8')
      const module = new Module({
        code,
        path: importee,
        bundle: this,})return module}}generate() {
    const ms = new MagicString.Bundle()
    this.statements.forEach(statement= > {
      const source = statement._source.clone()
      if (/^Export/.test(statement.type)) { // +
        if (statement.type === 'ExportNamedDeclaration') { // +
          source.remove(statement.start, statement.declaration.start) // +
        } // +
      } // +
      ms.addSource({
        content: source,
        separator: '\n',})})return {
      code: ms.toString()
    }
  }
}

module.exports = Bundle
Copy the code

The fetchModule method is modified to handle cases such as import {name} from ‘./ userInfo ‘, where userInfo defines variables or depends on other file variables.

The ExportNamedDeclaration statement is filtered out of the generate method and the variable definition is output directly.

- export var name = 'careteen'
+ var name = 'careteen'
Copy the code

Do something about the Module

// ./src/module.js
const { parse } = require('acorn')
const MagicString = require('magic-string')
const analyse = require('./ast/analyse')

function hasOwn(obj, prop) { // +
  return Object.prototype.hasOwnProperty.call(obj, prop) // +
} // +

class Module {
  constructor({ code, path, bundle, }) {
    this.code = new MagicString(code, {
      filename: path,
    })
    this.path = path
    this.bundle = bundle
    this.ast = parse(code, {
      ecmaVersion: 7.sourceType: 'module',})// +++ start +++
    this.imports = {} // Import variables
    this.exports = {} // The exported variable
    this.definitions = {} // The variable definition statement
    // +++ end +++
    this.analyse()
  }
  analyse() {
    // +++ start +++
    // Collect import and export variables
    this.ast.body.forEach(node= > {
      if (node.type === 'ImportDeclaration') {
        const source = node.source.value
        node.specifiers.forEach(specifier= > {
          const { name: localName} = specifier.local
          const { name } = specifier.imported
          this.imports[localName] = {
            source,
            name,
            localName,
          }
        })
      } else if (node.type === 'ExportNamedDeclaration') {
        const { declaration } = node
        if (declaration.type === 'VariableDeclaration') {
          const { name } = declaration.declarations[0].id
          this.exports[name] = {
            node,
            localName: name,
            expression: declaration,
          }
        }
      }
    })
    // +++ end +++
    analyse(this.ast, this.code, this)
    // +++ start +++
    // Collect all the variables defined by the statement and establish the corresponding relationship between the variables and the declaration statement
    this.ast.body.forEach(statement= > {
      Object.keys(statement._defines).forEach(name= > {
        this.definitions[name] = statement
      })
    })
    // +++ end +++
  }
  expandAllStatements() {
    const allStatements = []
    this.ast.body.forEach(statement= > {
      // +++ start +++
      // Filter 'import' statements
      if (statement.type === 'ImportDeclaration') {
        return
      }
      // +++ end +++
      const statements = this.expandStatement(statement) allStatements.push(... statements) })return allStatements
  }
  expandStatement(statement) {
    statement._included = true
    const result = []
    // +++ start +++
    const dependencies = Object.keys(statement._dependsOn)
    dependencies.forEach(name= > {
      const definition = this.define(name) result.push(... definition) })// +++ end +++
    result.push(statement)
    return result
  }
  // +++ start +++
  define(name) {
    if (hasOwn(this.imports, name)) {
      const importDeclaration = this.imports[name]
      const mod = this.bundle.fetchModule(importDeclaration.source, this.path)
      const exportDeclaration = mod.exports[importDeclaration.name]
      if(! exportDeclaration) {throw new Error(`Module ${mod.path} does not export ${importDeclaration.name} (imported by The ${this.path}) `)}return mod.define(exportDeclaration.localName)
    } else {
      let statement = this.definitions[name]
      if(statement && ! statement._included) {return this.expandStatement(statement)
      } else {
        return[]}}}// +++ end +++
}

module.exports = Module
Copy the code

A couple of things need to happen

  • Collect import and export variables

    • Create a mapping for future use
  • Collect variables defined by all statements

    • Establish mappings between variables and declaration statements for subsequent use
  • Filter import statements

    • Delete keywords
  • When printing the statement, determine whether the variable is import

    • If you need to recursively collect variables that depend on files again
    • Otherwise just output
  • Build dependencies, create a chain of scopes, and turn it into a./ SRC /ast/ skill.js file

    • Mounts on each statement in the abstract syntax tree_source(source code), _defines(variable of current module), _dependsOn(variable of external dependence), _included
    • Collect the variables defined on each statement, creating a chain of scopes
    • Collect external dependent variables
// ./src/ast/analyse.js
// +++ start +++
const Scope = require('./scope')
const walk = require('./walk')

function analyse(ast, ms) {
  let scope = new Scope()
  // create scope chain,
  ast.body.forEach(statement= > {
    function addToScope(declarator) {
      const { name } = declarator.id
      scope.add(name)
      if(! scope.parent) {// If there is no upper-level scope, it is a sizing scope within the module
        statement._defines[name] = true}}Object.defineProperties(statement, {
      _source: { / / the source code
        value: ms.snip(statement.start, statement.end),
      },
      _defines: { // A variable defined by the current module
        value: {},},_dependsOn: { // Variables that are not defined by the current module, i.e. external dependent variables
        value: {},},_included: { // Whether it is already included in the output statement
        value: false.writable: true,}})// Collect the variables defined on each statement, creating a chain of scopes
    walk(statement, {
      enter(node) {
        let newScope
        switch (node.type) {
          case 'FunctionDeclaration':
            const params = node.params.map(p= > p.name)
            addToScope(node)
            newScope = new Scope({
              parent: scope,
              params,
            })
            break;
          case 'VariableDeclaration':
            node.declarations.forEach(addToScope)
            break;
        }
        if (newScope) {
          Object.defineProperty(node, '_scope', {
            value: newScope,
          })
          scope = newScope
        }
      },
      leave(node) {
        if (node._scope) {
          scope = scope.parent
        }
      },
    })
  })
  ast._scope = scope
  // Collect external dependent variables
  ast.body.forEach(statement= > {
    walk(statement, {
      enter(node) {
        if (node.type === 'Identifier') {
          const { name } = node
          const definingScope = scope.findDefiningScope(name)
          // External dependencies are not found in the scope chain
          if(! definingScope) { statement._dependsOn[name] =true}}},})})}module.exports = analyse
// +++ end +++
Copy the code

The traversal and Scope of dependencies can be implemented directly using the walk and Scope implementations mentioned above.

The dependent variable has been modified

After the above processing, we have basically implemented the simple tree-shaking function, but we still need to deal with the changes of exported variables in the following dependent files

// ./example/myRollup/src/userinfo.js
export var name = 'careteen'
name += 'lan'
name ++
export var age = 25
Copy the code

You need to add the following // + changes to the./ SRC /module.js file

// ./src/module.js
class Module {
  constructor() {
    // ...
    this.definitions = {} // The variable definition statement
    this.modifications = {} // Change the variable // +
    this.analyse()    
  }
  analyse() {
    // ...
    // Collect all the variables defined by the statement and establish the corresponding relationship between the variables and the declaration statement
    this.ast.body.forEach(statement= > {
      Object.keys(statement._defines).forEach(name= > {
        this.definitions[name] = statement
      })
      // +++ start +++
      Object.keys(statement._modifies).forEach(name= > {
        if(! hasOwn(this.modifications, name)) {
          this.modifications[name] = []
        }
        // There may be several changes
        this.modifications[name].push(statement)
      })
      // +++ end +++})}expandStatement(statement) {
    statement._included = true
    const result = []
    const dependencies = Object.keys(statement._dependsOn)
    dependencies.forEach(name= > {
      const definition = this.define(name) result.push(... definition) }) result.push(statement)// +++ start +++
    // Add result to variables defined under the current module if they change
    const defines = Object.keys(statement._defines)
    defines.forEach(name= > {
      const modifications = hasOwn(this.modifications, name) && this.modifications[name]
      if (modifications) {
        modifications.forEach(modif= > {
          if(! modif._included) {const statements = this.expandStatement(modif) result.push(... statements) } }) } })// +++ end +++
    return result
  }  
}
Copy the code

A couple of things need to happen

  • Define modified variables in each statement of the abstract syntax tree_modifies(tosrc/ast/analyse.jsProcessing)
    • Collect external dependent variables (already implemented above)
    • Collect statements for variable modifications
  • Store all variables of the modified statement tomodifications
  • When the statement is output, the variable defined is judged_definesIf yes, output is required
// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement= > {
    // ...
    Object.defineProperties(statement, {
      // ...
      _modifies: { // Modify the variable
        value: {}, // +
      },
    })    
  })
  ast.body.forEach(statement= > {
    // +++ start +++
    // Collect external dependent variables
    function checkForReads(node) {
      if (node.type === 'Identifier') {
        const { name } = node
        const definingScope = scope.findDefiningScope(name)
        // External dependencies are not found in the scope chain
        if(! definingScope) { statement._dependsOn[name] =true}}}// Collect statements for variable modifications
    function checkForWrites(node) {
      function addNode(n) {
        while (n.type === 'MemberExpression') { // var a = 1; var obj = { c: 3 }; a += obj.c;
          n = n.object
        }
        if(n.type ! = ='Identifier') {
          return
        }
        statement._modifies[n.name] = true
      }
      if (node.type === 'AssignmentExpression') {
        addNode(node.left)
      } else if (node.type === 'UpdateExpression') { // var a = 1; a++
        addNode(node.argument)
      } else if (node.type === 'CallExpression') {
        node.arguments.forEach(addNode)
      }
    }
    // // +++ end +++
    walk(statement, {
      enter(node) {
        // +++ start +++
        if (node._scope) {
          scope = node._scope
        }
        checkForReads(node)
        checkForWrites(node)
      },
      leave(node) {
        if (node._scope) {
          scope = scope.parent
        }
      }
      // +++ end +++})})}Copy the code

After the above processing, the desired output results can be obtained

// ./example/myRollup/dest/bundle.js
var name = 'careteen'
name += 'lan'
name ++
function say() {
  console.log('hi ', name)
}
say()
Copy the code

Block-level scopes are supported

Support is also required for the following statements

if(true) {
  var blockVariable = 25
}
console.log(blockVariable)
Copy the code

You need to perform the following operations

// ./src/ast/scope.js
class Scope {
  constructor(options = {}) {
    // ...
    this.isBlockScope = !! options.block// Whether it is block-scoped
  }
  // +++ start +++
  add(name, isBlockDeclaration) {
    if (this.isBlockScope && ! isBlockDeclaration) {// The current scope is block-level scope && This statement is var or declare function
      this.parent.add(name, isBlockDeclaration)
    } else {
      this.names.push(name)
    }
  }
  // +++ end +++
}

Copy the code
  • When creating scopes, distinguish between block-level scopes and plain variable definitions
// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement= > {
    function addToScope(declarator, isBlockDeclaration = false) { // +
      const { name } = declarator.id
      scope.add(name, isBlockDeclaration) // +
      // ...
    }
    // ...
    // Collect the variables defined on each statement, creating a chain of scopes
    walk(statement, {
      enter(node) {
        let newScope
        switch (node.type) {
          // +++ start +++
          case 'FunctionExpression':
          case 'FunctionDeclaration':
            const params = node.params.map(p= > p.name)
            if (node.type === 'FunctionDeclaration') {
              addToScope(node)
            } else if (node.type === 'FunctionExpression' && node.id) {
              params.push(node.id.name)
            }
            newScope = new Scope({
              parent: scope,
              params,
              block: true,})break;
          case 'BlockStatement':
            newScope = new Scope({
              parent: scope,
              block: true,})break;
          case 'VariableDeclaration':
            node.declarations.forEach(variableDeclarator= > {
              if (node.kind === 'let' || node.kind === 'const') {
                addToScope(variableDeclarator, true)}else {
                addToScope(variableDeclarator, false)}})break;
          // +++ end +++
        }
      }
    } 
  }
}
Copy the code

The system variable console.log is used to output statements, so as not to output them twice.

// ./src/module.js
const SYSTEM_VARIABLE = ['console'.'log']
class Module {
  // ...
  define(name) {
    if (hasOwn(this.imports, name)) {
      // ...
    } else {
      let statement = this.definitions[name]
      // +++ start +++
      if(statement && ! statement._included) {return this.expandStatement(statement)
      } else if (SYSTEM_VARIABLE.includes(name)) {
        return[]}else {
        throw new Error(`variable '${name}' is not exist`)}// +++ end +++}}}Copy the code

Handle entry tree shaking

The above tree-shaking is for import statements, and for entry files that are defined but do not use variables

var company = 'sohu focus'
var companyAge = 23
console.log(company)
Copy the code
  • Filter variables defined but not used
  • When collecting a defined variable, it is no longer printed if the variable is already printed
// ./src/module.js
class Module {
  // ...
  expandAllStatements() {
    if (statement.type === 'ImportDeclaration') {
      return
    }
    // +++ start +++
    // Filter variables defined but not used
    if (statement.type === 'VariableDeclaration') {
      return
    }
    // +++ end +++
  }
  define(name) {
    if (hasOwn(this.imports, name)) {
    // ...
    } else {
      let statement = this.definitions[name]
      // +++ start +++
      if (statement) {
        if (statement._included) {
          return[]}else {
          return this.expandStatement(statement)
        }
        // +++ end +++
      } else if (SYSTEM_VARIABLE.includes(name)) {
        return[]}else {
        throw new Error(`variable '${name}' is not exist`)}}}}Copy the code

Implement variable renaming

There are cases where multiple modules have a variable named company

// ./example/myRollup/src/compay1.ts
const company = 'qunar'
export const company1 = company + '1'

// ./example/myRollup/src/compay2.ts
const company = 'sohu'
export const company2 = company + '2'
Copy the code
// ./example/myRollup/src/index.ts
import { company1 } from './compay1'
import { company2 } from './compay2'
console.log(company1, company2)
Copy the code

In this case, you need to rename the same variable appropriately during packaging and output it

First extract and prepare the utility functions

// ./src/utils.js
const walk = require('./ast/walk')

function hasOwn(obj, prop) {
  return Object.prototype.hasOwnProperty.call(obj, prop)
}

function replaceIdentifiers(statement, source, replacements) {
  walk(statement, {
    enter(node) {
      if (node.type === 'Identifier') {
        if (node.name && replacements[node.name]) {
          source.overwrite(node.start, node.end, replacements[node.name])
        }
      }
    }
  })
}

module.exports = {
  hasOwn,
  replaceIdentifiers,
}
Copy the code

Mount an instance of the current module on each statement in the abstract syntax tree

// ./src/ast/analyse.js
function analyse(ast, ms, module) { // +
  ast.body.forEach(statement= > {
    Object.defineProperties(statement, {
      _module: { / / the module instance
        value: module.// +
      },
      // ...}}}Copy the code

Provide renaming methods on the Module

// ./src/module.js
class Module {
  constructor() {
    // ...
    this.canonicalNames = {} // a variable without the same name
    this.analyse()
  }
  // +++ start ++
  rename(name, replacement) {
    this.canonicalNames[name] = replacement
  }
  getCanonicalName(localName) {
    if(! hasOwn(this.canonicalNames, localName)) {
      this.canonicalNames[localName] = localName
    }
    return this.canonicalNames[localName]
  }
  // +++ end ++
}
Copy the code
  • Collect duplicate named variables
  • Renames duplicate variables
// ./src/bundle.js
const { hasOwn, replaceIdentifiers } = require('./utils')

class Bundle {
  constructor(options) {
  build(filename) {
    const entryModule = this.fetchModule(this.entryPath)
    this.statements = entryModule.expandAllStatements()
    this.definesConflict() // +
    const { code } = this.generate()
    writeFileSync(filename, code)
  }
  // +++ start +++
  definesConflict() {
    const defines = {}
    const conflicts = {}
    this.statements.forEach(statement= > {
      Object.keys(statement._defines).forEach(name= > {
        if (hasOwn(defines, name)) {
          conflicts[name] = true
        } else {
          defines[name] = []
        }
        defines[name].push(statement._module)
      })
    })
    Object.keys(conflicts).forEach(name= > {
      const modules = defines[name]
      modules.pop() // The last one with the same name is not processed
      modules.forEach(module= > {
        const replacement = getSafeName(name)
        module.rename(name,replacement)
      })
    })
    function getSafeName(name) {
      while (hasOwn(conflicts, name)) {
        name = ` _${name}`
      }
      conflicts[name] = true
      return name
    }
  }
  // +++ end +++
  generate() {
    const ms = new MagicString.Bundle()
    this.statements.forEach(statement= > {
      // +++ start +++
      let replacements = {}
      Object.keys(statement._dependsOn)
        .concat(Object.keys(statement._defines))
        .forEach(name= > {
          const canonicalName = statement._module.getCanonicalName(name)
          if(name ! == canonicalName) { replacements[name] = canonicalName } })// +++ end +++
      const source = statement._source.clone()
      if (/^Export/.test(statement.type)) {
        if (statement.type === 'ExportNamedDeclaration') {
          source.remove(statement.start, statement.declaration.start)
        }
      }
      replaceIdentifiers(statement, source, replacements) // +}}}Copy the code

After the above processing, the following output results can be obtained

// ./example/myRollup/dest/bundle.js
const _company = 'qunar'
const company1 = _company + '1'
const company = 'sohu'
const company2 = company + '2'
console.log(company1, company2)
Copy the code

✌ 🏻 😁 ✌ 🏻

conclusion

This article implements tree-shaking simplicity from rollup usage to source code disclosure. All code is stored in @careteen/rollup. Interested students can go to debug.

reference

  • The Rollup’s official website
  • ECMA Module
  • Working principle of ES Module
  • Webpack is easy to implement
  • Commonjs specification principles
  • Online Parsing AST