Super Simplified WebPack with Babel Handlift

This article describes how to parse, compile and package files and their dependencies using Babel. See Babeltry


The directory structure

SRC: Packaged project for testing

Dist: Used to store the generated file, test.html used to test the generated file effect

Babeltry.config. js: configuration file

Index.js: entry for the packaging tool

Lib: The dependency method of the packaging tool

Effect of packaging

The source file

src/index.js

import { greeting } from "./greeting.js";
document.write(greeting('world'));
Copy the code

src/greeting.js

import { str } from "./hello.js";
var greeting = function(name) {
    return str + ' ' + name;
}
export { greeting }
Copy the code

src/hello.js

var str = 'hello';
export { str }
Copy the code

The packaged file

dist/main.js

            (function(modules){
                function require(filepath){
                    const fn = modules[filepath];
                    
                    const moudle = { exports: {}}; fn(require, moudle, moudle.exports);

                    return moudle.exports
                }
                require('E: item folder item Data Other babeltry SRC index.js') ({})'E: item folder item Data Other babeltry SRC index.js': function (require, moudle, exports) {"use strict";

var _greeting = require("./greeting.js");

document.write((0, _greeting.greeting)('world')); },'./greeting.js': function (require, moudle, exports) {"use strict";

Object.defineProperty(exports."__esModule", {
  value: true
});
exports.greeting = undefined;

var _hello = require("./hello.js");

var greeting = function greeting(name) {
  return _hello.str + ' ' + name;
};
exports.greeting = greeting; },'./hello.js': function (require, moudle, exports) {"use strict";

Object.defineProperty(exports."__esModule", {
  value: true
});
var str = 'hello';
exports.str = str;},})
        
Copy the code

dist/test.html

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
    <script src="./main.js"></script>
</head>
<body>
</body>
</html>
Copy the code

Start writing a packaging tool

package.json

Required dependencies, which we’ll explain in the parser section

"devDependencies": {
    "babel-core": "^ 6.26.3"."babel-preset-env": "^ 1.7.0"."babel-traverse": "^ 6.26.0"."babylon": "^ 6.18.0"
}
Copy the code

babeltry.config.js

'use strict'
const path = require('path');
module.exports = {
    entry: path.join(__dirname, '/src/index.js'),
    output: {
        path: path.join(__dirname, '/dist'),
        filename: 'main.js'}};Copy the code

Set the entry of the item to be packaged and the exit location and file name of the packaged result

index.js

const Compiler = require('./lib/compiler');
const config = require('./babeltry.config');
new Compiler(config).run();
Copy the code

The contents of the index.js file are fairly simple: load the configuration and compiler, instantiate a compiler, and execute the run method

Parser. Js parser

Before we write the compiler, let’s look at how the parser is implemented

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const core = require('babel-core');

module.exports = {
    getAST: (path) = > {
        const file = fs.readFileSync(path, 'utf-8');
        return babylon.parse(file, {
            sourceType: 'module'})},getNode: (ast) = > {
        const nodes = [];
        traverse(ast, {
            ImportDeclaration: ({ node }) = > {
                nodes.push(node.source.value)
            }
        });
        return nodes
    },
    transform: (ast) = > {
        const { code } = core.transformFromAst(ast, null, {
            presets: ["env"]});return code
    }
}
Copy the code

Introducing dependencies:

Babylon: The AST abstract syntax tree is generated using the Parse method of Babylon. The abstract syntax tree is not explained here. There are many related articles available online

Babel-traverse: traverse the AST with babel-traverse. ImportDeclaration is used to fetch dependent nodes

Babel-core: generate source code through babel-core’s transformFromAst method

The parser exports three methods:

GetAST: receives a file path as an input parameter, reads the file and parses out the AST

GetNode: Takes the AST as an input parameter and returns an array that depends on the file path

Transform: Receives the AST as an input parameter and returns the source code

Compiler. Js compiler

const fs = require('fs');
const path = require('path');
const { getAST, getNode, transform } = require('./parser');

module.exports = class Compiler {
    constructor(options) {
        const { entry, output } = options;
        this.entry = entry;
        this.output = output;
        this.modules = [];
    }

    run(){}buildMoudle(){}fillFile(){}}Copy the code

The constructor of a compiler class takes a configuration item and declares three variables, which are an entry, an exit, and an array representing a list of dependent files.

The compiler class has three methods. The run method, the compiler’s body method, does the parsing and compilation, the buildMoudle method takes the dependency path and generates an object representing the dependency file, which is each item stored in the Modules array, and the fillFile method generates the package file.

Look at the buildMoudle method first

buildMoudle(filepath, isEntry) {
    let ast;
    if (isEntry) {
        ast = getAST(filepath);
    } else {
        let absolutePath = path.join(process.cwd(), '/src', filepath);
        ast = getAST(absolutePath);
    }
    return {
        filepath,
        nodes: getNode(ast),
        code: transform(ast)
    }
}
Copy the code

BuildMoudle takes two arguments, filepath for dependent filepath and isEntry for whether it is an entry file.

It mainly does three actions, namely calls the three methods of the parser, first obtains the AST abstract syntax tree through getAST, then calls getNode and Transform as the input parameter, respectively obtains the dependent file path array and source code of the current parsed file.

Returns an object representing the current dependent file. Filepath is the dependent filepath, nodes is the array of dependent file paths for the current parsed file, and code is the source file.

run() {
    const entryModule = this.buildMoudle(this.entry, true);
    this.modules.push(entryModule);
    // Deep traversal dependency
    for(let i = 0; i < this.modules.length; i++){let _moudle = this.modules[i];
        _moudle.nodes.map((node) = >{
            this.modules.push(this.buildMoudle(node));
        });
    }
    this.fillFile();
}
Copy the code

The run method starts with the entry file, walks through the modules dependency array, deeply walks through all dependency files, builds all dependency file objects and stores them in modules, and finally calls fillFile to generate the package file.

fillFile() {
    let moudles = ' ';
    this.modules.map((_moudle) = >{
        moudles += ` '${_moudle.filepath}': function (require, moudle, exports) {${_moudle.code}}, `
    })
    const bundle = `
        (function(modules){
            function require(filepath){
                const fn = modules[filepath];
                
                const moudle = { exports: {} };

                fn(require, moudle, moudle.exports);

                return moudle.exports
            }
            require('The ${this.entry}') ({})${moudles}})
    `;

    const outpath = path.join(this.output.path, this.output.filename);
    fs.writeFileSync(outpath, bundle, 'utf-8');
}
Copy the code

FillFile generates a package file whose body calls an anonymous method.

The anonymous method receives an object generated by our list of dependent files. Each attribute of the object is a dependent file path and the value is a method (called fn for easy memory). The FN method receives three parameters (require, moudle, exports) and the method content is the source of the dependent file.

The body of the method declares the require method and calls require with the entry file path as an input, thus entering the recursive loop.

The require method takes a path as an entry that we can pass through the closure to get the corresponding FN method in the anonymous method entry object, declare a moudle object with exports, and then call fn to return moudle.exports.

It’s a little bit confusing to say that fn’s contents are the dependency files that we compiled, like greeting.js. Whenever we get a require dependency file, we execute that dependency file and return the exported result.

The execution logic of the sample main.js should make sense.