1. Rollup is used for basic purposes

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/main.js".output: {
    file: "dist/rollup_bundle_1.js".format: "es".// Many modes amd/ ES/Iife/umD/CJS
    // name: 'bundleName' // iife when needed
  },
  plugins: [
    babel({
      babelHelpers: "bundled".exclude: "node_modules/**",
    }),
    resolve(),
    commonjs(),
    typescript(),
    terser(),
    postcss(),
    serve({})
  ],
};
Copy the code

2. Pre-knowledge

  1. Magic-string (operation string generates sourcemap)
  2. Acorn (JavaScript Parser Ast Syntax tree Transform Generate)
  3. Scope chain

3. Rollup the basic process

function rollup(entry, filename) {
  // Generate a bundle from the entry
  const bundle = new Bundle({ entry });
  // build generates code and sourceMap
  const {code} = bundle.build(filename);
  // Write to the file
  fs.writeFileSync(filename, code);
}
Copy the code

4. Merge module code

  1. So let’s write a demo
//index.js
import { name, age } from './msg'
let city = 'sz'
var sex = 'boy'
console.log(city, name)
// msg.js
export const name = 'name'
export const age = 28
Copy the code
  1. Merge code
// bundle.js let's simply merge the code together
// replace import {name, age} from './ MSG '
const name = 'name'
const age = 28
let city = 'sz'
var sex = 'boy'
console.log(city, name)
Copy the code
  1. Simple implementation of module merge
// rollup
function rollup() {
  const bundle = new Bundle({ entry });
  bundle.build(filename);
}

// bundle.js 
// The simplest idea is to find the import node and copy the code
// https://astexplorer.net/ View the AST node
class Bundle {
  constructor(options) {
    // Import file data
    this.entryPath = path.resolve(options.entry.replace(/\.js$/."") + ".js");
  }
  build(filename) {
    let magicString = new MagicString.Bundle();
    const code = fs.readFileSync(this.entryPath);
    magicString.addSource({
      content: code,
      separator: "\n"});// Turn code into an AST syntax tree. Find the import statement and copy the code
    // Each file is actually a module
    this.ast = parse(code, {
      ecmaVersion: 7.sourceType: "module"});let sourceList = [];
    // Parse the syntax tree through the AST to find the import statement and get the code from source
    this.ast.body.forEach((node) = > {
      if (node.type === "ImportDeclaration") {
        let source = node.source.value; // ./msgsourceList.push(source); }});// Copy the code directly from sourceList
    for (let i = 0; i < sourceList.length; i++) {
      // The relative path becomes the absolute path
      let pathName = sourceList[i];
      if(! path.isAbsolute(pathName)) { pathName = path.resolve( path.dirname(this.entryPath),
          pathName.replace(/\.js$/."") + ".js"
        );
      }
      const code = fs.readFileSync(pathName);
      magicString.addSource({
        content: code,
        separator: "\n"}); } fs.writeFileSync(filename, magicString.toString()); }}Copy the code

  1. result
// All the code is consolidated by the above actions
import { name, age } from "./msg";
let city = "sz";
var sex = "boy";
console.log(city, name);

export const name = "name";
export const age = 28;
Copy the code
  1. The rollup results
// 1. Each file is a module. We need a module.js
// 2. Isolate ast syntax tree processing
// 3. We need to remove the import statement export does not need it
// 4. We need to remove variables not used tree-shaking
const name = "name";

let city = "sz";
console.log(city, name);
Copy the code
  1. Optimize the code
// Basic directory structureHeavy Exercises ── heavy exercises ─ skill.jsAst / / analysis│ ├ ─ ─ Scope. Js/ / scope│ └ ─ ─ walk. Js// Walk through the AST syntax tree├ ─ ─ bundle. Js// Bundle collects dependent modules, and finally packages all the code together for output│ └ ─ ─ index. Js ├ ─ ─module.js / / Each file is a module ├─ rollup// Packaged entry module└ ─ ─ utils. Js// Some auxiliary functions
Copy the code
  • bundle.js
// We add to the result in terms of statements instead of files
class Bundle {
  constructor(options) {
    // Import file data
    this.entryPath = path.resolve(options.entry.replace(/\.js$/."") + ".js");
  }
  build(filename) {
    // Get the entry module
    let entryModule = this.fetchModule(this.entryPath);
    // Expand all statements we add statements one by one instead of adding them from a file perspective
    this.statements = entryModule.expandAllStatements(true);
    const { code } = this.generate({});
    fs.writeFileSync(filename, code);
  }
  // The entry module is index.js and takes two arguments when an import is encountered
  fetchModule(importee, importer) {
    let route;
    if(! importer) route = importee;else {
      if (path.isAbsolute(importee)) route = importee;
      // relative path./ MSG
      else
        route = path.resolve(
          path.dirname(importer),
          importee.replace(/\.js$/."") + ".js"
        );
    }
    if (route) {
      let code = fs.readFileSync(route, "utf8");
      const module = new Module({
        code,
        path: importee,
        bundle: this.// Bundle instance
      });
      return module; }}// Generate code
  generate() {
    let magicString = new MagicString.Bundle();
    // The traversal statement is added to the result
    this.statements.forEach((statement) = > {
      const source = statement._source.clone();
      if (/^Export/.test(statement.type)) {
        if (
          statement.type === "ExportNamedDeclaration" &&
          statement.declaration.type === "VariableDeclaration"
        ) {
          // 2
          source.remove(statement.start, statement.declaration.start);
        }
      }
      magicString.addSource({
        content: source,
        separator: "\n"}); });return { code: magicString.toString() }; }}Copy the code
  • module.js
class Module {
  constructor({ code, path, bundle }) {
    this.code = new MagicString(code, { filename: path });
    this.path = path;
    this.bundle = bundle;
    // Get the AST syntax tree
    this.ast = parse(code, {
      ecmaVersion: 7.sourceType: "module"});// Parse the AST syntax tree
    // 1. Add a _source attribute to execute the code for the current statement
    analyse(this.ast, this.code, this);
  }
  // Expand all statements
  expandAllStatements() {
    let allStatements = [];
    // We now have a traversal expansion with four statements
    this.ast.body.forEach((statement) = > {
      let statements = this.expandStatement(statement); allStatements.push(... statements); });return allStatements;
  }
  expandStatement(statement) {
    // mark it as processed
    statement._included = true;
    let result = [];
    // We need to expand the statement import to generate a new module
    if (statement.type === "ImportDeclaration") {
      // Recursive create module imports either relative or absolute paths from index.js
      let module = this.bundle.fetchModule(statement.source.value, this.path);
      const statements = module.expandAllStatements(); result.push(... statements); }else {
      // 1. We do not need an import statement
      result.push(statement);
    }
    returnresult; }}Copy the code
  • analyse
function analyse(ast, magicString) {
  ast.body.forEach((statement) = > {
    Object.defineProperties(statement, {
      _source: { value: magicString.snip(statement.start, statement.end) },
    });
  });
}
Copy the code
  • output
// We get the result of the module merge
const name = "name";
const age = 28;
let city = "sz";
var sex = "boy";
console.log(city, name);
Copy the code

5. tree-shaking

// How do I know if a variable is used? What are the possibilities for using variables
// 1. Assignment statement name = XXX name += xx AssignmentExpression
// 2. Function calls such as console
// Determine which variables are used in each statement as we expand it
// We don't need FunctionDeclaration VariableDeclaration

// After the exclusion, our definition does not need to solve several problems
// 1. How do I know which variables are used
Console. log(name, age) uses name age to analyze statements that may use variables
// Use _dependsOn to save the dependent variable (this module may also be import)
// 2. How to get the definition statement of the used variable
// We use definitions to store all the statements for the variables
// 3. Are variables defined by this module or imported by import
// We need to check by scope if it exists in this module it belongs to this module otherwise it belongs to import
// We also use imports to hold all import variables
Copy the code
  1. When we expand the statement, we exclude what we don’t need
// ImportDeclaration FunctionDeclaration VariableDeclaration
// expandAllStatements are not handled when expanded in module.js
this.ast.body.forEach((statement) = > {
  if (statement.type === "ImportDeclaration") return;
  if (statement.type === "VariableDeclaration") return;
  if (statement.type === "FunctionDeclaration") return;
  // Only console.log(city, name) will be expanded. All other statements will be skipped
  let statements = this.expandStatement(statement); allStatements.push(... statements); });Where is our variable definition
Copy the code
  1. Walk through the AST syntax tree
// 1. Add several properties to collect defined variable dependent variables
Object.defineProperties(statement, {
  _defines: { value: {}},// Define variables to distinguish imports
  _dependsOn: { value: {}},// The dependent variable
  _included: { value: false.writable: true }, // Whether it is already included in the output statement
  _source: { value: magicString.snip(statement.start, statement.end) }, // The corresponding code
});
// 2. Block-level domains are supported by the definition of variables between CHCS and BMCS
scope.add(name, isBlockDeclaration)
// If BlockStatement is used
new Scope()
// 3. Find the dependent variable _dependsOn
// iterate over the add
if (node.type === "Identifier") {
  statement._dependsOn[node.name] = true;
}
// 4. Find all variables defined in this module.
// We can get the corresponding statement from the variable
Object.keys(statement._defines).forEach((name) = > {
  this.definitions[name] = statement;
});
Copy the code
  1. A statement
function expandStatement(statement) {
  statement._included = true; // The tag has already been added
  let result = [];
  // 1. Add statements corresponding to dependent variables
  // console.log(city, name) depends on [console, log, city, name]
  const dependencies = Object.keys(statement._dependsOn);
  dependencies.forEach((name) = > {
    // Find the defined statement to add because variables may be import (we need to recursively create module)
    let definition = this.define(name); result.push(... definition); });// 2. Add yourself to console.log(city, name)
  result.push(statement);
  return result
}
// Find the statement corresponding to the variable
function define(name) {
  if (hasOwnProperty(this.imports, name)) {
    // indicate that it is import
    let module = this.bundle.fetchModule(this.imports[name].source, this.path);
    // go to the MSG module to find the corresponding statement to find the name statement
    return module.define(exportDeclaration.localName);
  } else { // Own variables in this module
    statement = this.definitions[name];
    // find the statement corresponding to city
    return this.expandStatement(statement); }}// It is easy to implement tree-shaking
let city = "sz";
const name = "name";
console.log(city, name);
Copy the code

6. Handle AssignmentExpression

// Except for CallExpression, which accesses variables
// AssignmentExpression also calls the variable name = 123. We need to add these statements to the result as well
// Modify the MSG code
export let name = "name";
export const age = 28;
name = "name-";
name += "AssignmentExpression";
// Look at the structure of the AST
Copy the code

// We need to add the modified statement to the result
// 1. Add a _modifies attribute while iterating through the statement
Object.defineProperties(statement, {
  _modifies: {value: {}},/ / modify
})
// 2. Collect _modifies (read and write) when collecting _dependsOn
statement._modifies[node.name] = true;
// 3. Define a modifications variable to hold a module modification of this.modifications = {};
// There may be more than one modification statement
this.modifications[name].push(statement);
// 4. Add the modified statement to the result when expanding the statement
function expandStatement(statement) {
  // 1. Handle _dependsOn
  // 2. Add yourself to the result
  // 3. Add the modified statement to the result
  const defines = Object.keys(statement._defines);
  let statements = this.expandStatement(statement); result.push(... statements); }// The result contains the statement we modified
let city = "sz";
city = "city";
let name = "name";
name = "name-";
name += "AssignmentExpression";
console.log(city, name);
Copy the code

7. Block-level scopes are supported

// Change the scope of the demo rollup is not supported in earlier versions.
import { name, age } from "./msg";
if (age > 10) {
  let block = "block";
  console.log(block);
} else if (age > 100) {
  console.log(name);
} else {
  console.log("test");
}
// Get the result
{
  let block = "block";
  console.log(block);
}
// The ast structure determines which BlockStatement is generated based on the result of the test
// Determine the logic in the BlockStatement

Copy the code

// Let's make it simple and change it to
{
  let block = "block";
  let name = 'name'
  console.log(block);
}
// Expect results
{
  let block = "block";
  console.log(block);
}
// version 0.3.1 is actually not processed to get the result
{
  let block = "block";
  let name = "name";
  var test = "var";
  console.log(block);
}
//# sourceMappingURL=bundle.js.map
Copy the code

// We simply add the var variable declaration to the parent scope
// We add a judgment while traversing the node
function addToScope(declarator, isBlockDeclaration = false) {
  if(! scope.parent || ! isBlockDeclaration) {// Add the variable declared by var
    statement._defines[name] = true; }}Our code does not use this scope at all
Copy the code

8. Deal with variable names

// Change the name of the variable if it is repeated

// We modify the demo code
// index.js was copied in with the same name and we need to rename it
import { name1 } from "./name1"; // const name = 'name1'
import { name2 } from "./name2"; // const name = 'name2'
import { name3 } from "./name3"; // const name = 'name3'
console.log(name1, name2, name3);
// names1
const name = 'name1'
export const name1 = name
// names2
const name = 'name2'
export const name2 = name
// names3
const name = 'name3'
export const name3 = name

// Get the result
const name$2 = "name1";
const name1 = name$2;

const name$1 = "name2";
const name2 = name$1;

const name = "name3";
const name3 = name;

console.log(name1, name2, name3); // The principle is to rename

// 1. Process the variable names after the statements are generated in the bundle's build process
this.deConflict();
function deConflict() {
  const conflicts = {}; // Command conflict
  conflicts[name] = true // Record conflicting variables
  // Record the corresponding module
  defines[name].push(statement._module)
  // Iterate through the conflicts rename
  const replacement = name + ` $${index + 1}`;
  module.rename(name, replacement);
}
// 2. Add a _module attribute to statement traversal
Object.defineProperties(statement, {
  _module: { value: module }, // The corresponding module
})
// 3. module defines the rename method
this.canonicalNames = {}; // store the corresponding relationship
function rename(name, replacement) {
  this.canonicalNames[name] = replacement;
}
// 4. We need to modify the contents of the AST node during code generation
function generate() {
  Object.keys(statement._dependsOn)
    .concat(Object.keys(statement._defines))
    .forEach((name) = > {
      const canonicalName = statement._module.getCanonicalName(name);
      if(name ! == canonicalName) replacements[name] = canonicalName; });// Replace the old name with the new name
  replaceIdentifiers(statement, source, replacements);
}
// 5. Replace the ast content
function replaceIdentifiers(statement, source, replacements) {
  walk(statement, {
    enter(node) {
      if (node.type === "Identifier") {
        / / renamed
        if(node.name && replacements[node.name]) { source.overwrite(node.start, node.end, replacements[node.name]); }}}}); }// Get the result
const name = "name1";
const name1 = name;
const name$1 = "name2";
const name2 = name$1;
const name$2 = "name3";
const name3 = name$2;
console.log(name1, name2, name3);
Copy the code

9. Sourcemap file

// Let's restore the demo code first
import { name, age } from "./hello";
let msg = "123";
console.log(age);
function fn() {
  console.log(msg);
}
fn();
// rollup packages the generated results
const age = "age";

let msg = "123";
console.log(age);

function fn() {
  console.log(msg);
}

fn();
//# sourceMappingURL=rollup_bundle_map.js.map

/ / the map file
{
	"version": 3."file": "rollup_bundle_map.js"."sources": [".. /src/hello.js".".. /src/map.js"]."sourcesContent": ["export let name = \"name\"; \nexport const age = \"age\"; \nname = \"test\"; \nname += 20; \n"."import { name, age } from \"./hello\"; \nlet msg = \"123\"; \nconsole.log(age); \nfunction fn() {\n console.log(msg); \n}\nfn(); \n"]."names": ["age"."msg"."console"."log"."fn"]."mappings": "AACO,MAAMA,GAAG,GAAG,KAAZ;; ACAP,IAAIC,GAAG,GAAG,KAAV; AACAC,OAAO,CAACC,GAAR,CAAYH,GAAZ;; AACA,SAASI,EAAT,GAAc; AACZF,EAAAA,OAAO,CAACC,GAAR,CAAYF,GAAZ; AACD;; AACDG,EAAE"
}

// We need to do two things based on the results generated by the rollup package
// 1. Add the sourceMappingURL address to the end of the source code before writing the file to the code concatenation string
let SOURCEMAPPING_URL = "sourceMa";
SOURCEMAPPING_URL += "ppingURL";
code += `\n//# ${SOURCEMAPPING_URL}=${path.basename(filename)}.map`;
2. Generate the sourcemap file
// generate to return map content
const {code, map} = this.generate({})
fs.writeFileSync(filename + ".map", map.toString());
// We directly use the generateMap method of magicString to generate the map file
map: magicString.generateMap({
  includeContent: true.file: options.dest,
  // TODO
}),
// There is another issue here: the file for 0.3.1 is inconsistent with the latest rollup build
{
	"version": 3."file": "bundle_map.js"."sources": [".. /.. / / Users/xueshuai. Liu/Desktop/rollup - study/rollup - 0.3.1 / main js. "".".. /.. / / Users/xueshuai. Liu/Desktop/rollup - study/rollup - 0.3.1 / hello. Js. ""]."sourcesContent": ["import { name, age } from \"./hello\"; \nlet msg = \"123\"; \nconsole.log(age); \nfunction fn() {\n console.log(msg); \n}\nfn(); \n"."export let name = \"name\"; \nexport const age = \"age\"; \nname = \"test\"; \nname += 20; \n"]."names": []."mappings": "AACA; AAEA; AACA; AACA; ACJO; ADCP; AAIA"
}
Copy the code

10. Source debugging

/ / reference https://juejin.cn/post/6898865993289105415

/ / version 0.3.1├── ├─ class.exe # ├─ class.exe # ├─ class.exe # 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

conclusion

Rollup simple merge module code. Analyze ast to find import statements and copy the code directly. In fact, all files are a module. By analyzing ast, you can know the dependencies and modifications of each statement in the module (latitude of the module). Recursively expand each statement from the entry file to add to the best output 4. How do we tree-shaking? 1. We do not deal with declarations such as import let function until they are used. 2. When expanding, add the statement corresponding to the dependent variable to the variable that may be import. We can add the var variable to the scope of the parent. We can handle the scope as if it is not useful. 6Copy the code