This is the sixth day of my participation in Gwen Challenge

Lynne, a front-end development engineer who can cry, love and laugh forever. In the Internet wave, love life and technology.

preface

Since we learned about tree-shaking in Webpack, it’s time to release a repository for a simple implementation of Rollup

  • Where does the useless code go? Tree-shaking project rollup
  • Rollup Practice series relearning rollup at the beginning

The specific implementation function can refer to the original version of rollup source code, only to achieve variables and function methods of non-nested tree-shaknig, the main purpose is to achieve the basic rollup packaging process, easy for novice to understand the rollup construction and packaging principle.

After all, I am only half a year, this article is more for novices, if you have opinions, welcome to give advice.

GitHub repository address: Lynn-zuo/rollup-demo, other than the ability to run through simple tree-shking and pack, other guarantees are not guaranteed.

Front knowledge

Before we go through the whole process, let’s take a look at some of the basic functions of the pre-knowledge code tool block.

1. magic-string

A tool for manipulating strings and generating source-maps, written by Rollup authors.

A piece of code to understand the use of magic-string basic methods.

var MagicString = require('magic-string'); var magicString = new MagicString('export var name = "Lynne"'); Console.log (magicString.snip(0, 6).toString())); // Return a copy of magicString, removing the contents before the beginning and end of the original string. // Remove the string from start to finish (the original string instead of the generated string) console.log(magicString.remove(0, 7).toString()); Let bundleString = new magicString.bundle (); bundleString.addSource({ content: 'var name = Lynne1', separator: '\n' }) bundleString.addSource({ content: 'var name = Lynne2', separator: '\n' }) console.log(bundleString.toString());Copy the code

2. AST

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

AST workflow:

  • Parser – Converts the source code into an abstract syntax tree with many ESTree nodes;
  • Transform – Transforms the abstract syntax tree;
  • Generation code Generation – Generates new code from the abstract syntax tree transformed in the previous step.

Acorn-rollup uses this library

Astexplorer can convert code into a syntax tree, and Acorn parses The results in accordance with The Estree Spec. It has The same functionality as Babel and is lighter than Babel.

The basic flow of acorn traversal to generate the syntax tree is as follows, where Walk implements the traversal method of the syntax tree.

// let shouldSkip; // let shouldAbort; /* * @param {*} ast Syntax tree to traverse * @param {*} param1 Configuration object */ function walk(ast, {enter, leave}) {visit(ast, null, enter, Leave)} /** * Access to this node * @param {*} node traversal node * @param {*} parent node * @param {*} enter method * @param {*} leave */ function visit (node, parent, enter, leave) {if (! node) return; Call (null, node, parent) // Specify this in Enter} // iterate over the child node again, Key => typeof node[key] === 'Object'; let keys = object.keys (node).filter(key => typeof node[key] === 'Object '); keys.forEach(key => { let 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)}}); Exports = walk if (leave) {leave(node, parent)}} module.exports = walkCopy the code

3. The scope

In JS, scope defines the rules of variable access scope. The scope chain is composed of a series of variable objects of the current execution environment and the upper execution environment to ensure the orderly access of the variables and functions that meet the access permission of the current execution environment

scope.js

class Scope { constructor(options = {}) { this.name = options.name; this.parent = options.parent; / / parent attribute to its frontal parent scope enclosing names = options. Params | | [] / / stored within the scope of all variables} the 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

Usescope.js – How to use and traverse ast

let Scope = require('./scope.js')

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

one();

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

let aScope = twoScope.findDefiningScope('a');
console.log('----1', aScope.name);
let bScope = twoScope.findDefiningScope('b');
console.log('----2', bScope.name);
let cScope = twoScope.findDefiningScope('c');
console.log('----3', cScope.name);
let dScope = twoScope.findDefiningScope('d');
console.log('----4', dScope && dScope.name);
Copy the code

Overview of the basic build process

  • Through an entry file – usually index.js, which Rollup reads and parses using Acorn – we are returned with the structural content of an abstract syntax tree (AST).
  • Once we have an AST, we can manipulate the tree to pinpoint declarations, assignments, operations, and so on to analyze, optimize, and change code.

In this case, rollup checks to see if the node has called a function or read a variable. If it does, it checks to see if it is in the current scope. If not, it looks up until it finds 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. If it finds a method in another module that depends on another module, it will recursively read the other module, and so on until it does not depend on another module, find out where these variables or methods are defined, include the definition statement, and leave out all other irrelevant code.

  • The AST will be analyzed and optimized and then packaged and compressed for output.

Basic build process implementation

As you work your way through the layers of the outermost build process, build packaging isn’t all that mysterious

Package rollup package compilation

Encapsulates the methods that rollup calls externally, exposing the entry file and output file paths.

Internally, the bundle is called, the bundle package object is generated, and the output file is compiled with bundle.build().

let Bundle = require('./bundle.js'); Const Bundle = new Bundle({entry}); function rollup(entry, outputFileName){const Bundle = new Bundle({entry}); // Call the build method to compile bundle.build(outputFileName); } module.exports = rollup;Copy the code

Bundle Packages the internal implementation of an object

Inside a Bundle object

  • First, analyze the entry path, according to the entry path to get the Module information to build and read the Module code – through fetchModule() method implementation, internal call Module object;
  • Second, expand the read internal module code statements and return an array – with the expandAllStatements() method;
  • The final expanded statement generates code and merges it with magicString(). – the generate ().
const fs = require('fs'); const path = require('path'); const { default: MagicString } = require('magic-string'); const Module = require('./module.js'); Class Bundle{constructor(options){constructor(options){this.entrypath = options.entry.replace(/\.js$/, ") + '.js'; this.module = {}; } build(outputFileName){// Find the module definition let entryModule = from the absolute path of the entry file this.fetchModule(this.entryPath); / / all the statements of the entry module, returns an array of all of the statements of enclosing statements. = entryModule expandAllStatements (); const {code} = this.generate(); fs.writeFileSync(outputFileName, code, 'utf8'); FetchModule (import_path, importer) {// let route = import_path; // let route; if (! Importer) {// If there is no module to import this module, this is the import module route = import_path; } else {if (path.isabsolute (import_path)) {route = import_path // absolute path} else if (import_path[0] == '.') {// Relative path route = path.resolve(path.dirname(importer), import_path.replace(/\.js$/, '') + '.js'); }} if(route) {let code = fs.readFileSync(route, 'utf8'); Let Module = new Module({code, // module source code path: route, // module absolute path: bundle: this // belongs to which bundle}); 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

The Module instance

When you package a file, each file is a Module, and each Module has an instance of Module. We actually walk through each file /Module.

let MagicString = require('magic-string'); const {parse} = require('acorn'); const analyse = require('./ast/analyse.js'); / / determine whether obj Object function prop attributes hasOwnProperty (obj, prop) {return Object. The prototype. The hasOwnProperty. Call (obj, /* * Each file is a module, / / Class Module {constructor({code, path, bundle}) {this.code = new MagicString(code, {filename: path}); this.path = path; // Module path this.bundle = bundle; This. ast = parse(code, {// Convert source code to abstract syntax tree ecmaVersion: 6, sourceType: 'module'}); this.analyse(); } analyse(){this.exports = [];} analyse(){this.exports = [];} analyse(){this.exports = []; If (node.type === 'ImportDeclaration'){// Let source = node.source.value; //./test.js from which module to import let speciFIERS = node.specifiers; debugger specifiers.forEach(specifier => { let name = specifier.imported ? specifier.imported.name : '' // name let localName = specifier.local ? specifier.local.name : // this.imports. Age = {name: 'age', localName: "age", source: {name: 'age', localName: "age", source: './test.js} this.imports[localName || name] = {name, localName, source} }) } else if (/^Export/.test(node.type)) { let declaration = node.declaration; if (! // let name = declar.declarations [0].id; // let name = declar.declarations [0].id; // this.exports['age'] = {node, localName: // exports['age'] = {node, localName: // exports['age'] = {node, localName: name, expression} this.exports[name] = { node, localName: name, expression: declaration } } }) analyse(this.ast, this.code, this); // This. Definitions = {}; This.ast.body. ForEach (statement => {object.keys (statement._defines). ForEach (name => { this.definitions[name] = statement; ExpandAllStatements (){let allStatements = []; expandAllStatements(){let allStatements = []; this.ast.body.forEach(statement => { if(statement.type === 'ImportDeclaration') return; Let statements = this.expandStatement(statement); allStatements.push(... statements); }); return allStatements; } // expandStatement(statement) {let result = []; let result = []; let result = []; const dependencies = Object.keys(statement._dependsOn); ForEach (name=> {let definition = this.define(name); result.push(... definition); }) if (! statement._included){ console.log('set --- statement._included') // statement._included = true; // This object has been added to the result, so it does not need to be added again: TODO: include not allowed to change assignment // tree-shaking core is here result.push(statement); } return result; } define(name) {if(hasOwnProperty(this.imports, name)) {// this.imports. Age = {name: 'age', localName: "age", source: './test.js} const importDeclaration = this.imports[name] // Get dependency module const module = this.bundle.fetchModule(importDeclaration.source, this.path) // this.exports['age'] = {node, localName: ExportData = module.exports[importdeclaration. name] // const exportData= module.exports[importdeclaration. name] // Exportdata.localname return module.define(name)} else {// key is the current module variable name, Let statement = this. Definitions [name]; Console. log('define--log', statement && statement._included) if (statement &&! statement._included) { return this.expandStatement(statement); } else { return [] } } } } module.exports = ModuleCopy the code

Internal references to Magi-String, Acorn, etc., are not repeated, but are basically the basics of pre-knowledge.

Rollup in development

As mentioned in the previous article, the hot vite build tool of late leverages the packaging capabilities of Rollup. One is its tree-shaking and pure JS code handling, and the other is probably rollup lightweight (thanks to its focus on function code, Easy to integrate) and ongoing maintenance (although the community is probably less active because it is lightweight and not complex).

One of the bugs that the vue/ Vite core recently fixed while maintaining Vite was due to an innocuous option added to the latest version of Rollup — the main purpose of which is to generate one less helper function. God, the rollup package code is so streamlined that it is still being optimized. Again, I admire the dedication of the contributors.

conclusion

Rollup is simple but worth learning ~ understanding how it is built helps in two scenarios:

  • Build pure JS function library project;
  • Use new generation build tools like Vite.