The introduction

Babel is a very powerful tool that does much more than just ES6 -> ES5 syntax conversions. Along the way, learning about Babel and its flexible plug-in pattern will open up more possibilities for the front end.

This article is the use of Babel, through the writing of Babel plug-in to solve the problem in a real project.

The code for this article is hosted on Github: babel-plugin-import-customized-require

1. Problems encountered

Recently, we encountered a problem in the project: We knew that using WebPack as a build tool would automatically help us with dependency builds by default; However, some of the dependencies in the project code are run-time/compile-time dependencies (which can be interpreted as pure front-end modularity like RequireJS and SeaJS), and not dealing with these dependencies can cause webPack compilation errors.

Why do you need non-compile time dependencies? For example, in my current business module (a separate Webpack repository), I rely on the dotting code of a common business module

// This is the home business module code
// Code that depends on the common business module
import log from 'common:util/log.js'

log('act-1');
Copy the code

However, this could be due to a disunity of the technology stack, a legacy problem with common business code that cannot be refactored, or simply a divide-and-conquer of business modules… In short, these module dependencies cannot be resolved at WebPack compile time and need to be addressed by the front-end runtime framework.

To solve the problem of webPack not being able to resolve such module dependencies at compile time, we can introduce a new syntax for such non-compile time dependencies, such as the following:

// __my_require__ is our custom front-end require method
var log = __my_require__('common:util/log.js')

log('act-1');
Copy the code

But this led to a split in the form of our code, and embracing the specification made us wish we could still use the ESM’s standard syntax to treat everyone equally.

We still want to write code like this:

// Standard ESM syntax
import * as log from 'common:util/log.js';

log('act-1');
Copy the code

Also, consider using webPack to provide an externals configuration to avoid some modules being packaged by Webpack. One important issue, however, is that there is a front-end modularity syntax in the existing Common code, and there are some problems integrating webpack-compiled code with the existing schema. Therefore, this method also has shortcomings.

In view of the above description, to sum up, our purpose is:

  • The ability to use ESM syntax in code for non-compile-time analysis of module references
  • Since WebPack will try to package this dependency, it needs to be compile-time without errors

2. Solution

With the above goals in mind, first of all, we need a way to identify run-time dependencies that do not require compilation. For example, the util/record module, if it is a runtime dependency, can refer to the standard syntax and add the module name identifier: Runtime :util/record. The effect is as follows:

The following two lines are normal compile-time dependencies
import React from 'react';
import Nav from './component/nav';

// There are two modules below that we don't want WebPack to handle at compile time
import record from 'runtime:util/record';
import {Banner, List} from 'runtime:ui/layout/component';
Copy the code

Second, although identification already lets developers know which modules in the code are dependencies that WebPack needs to package and which are non-compile-time dependencies; What WebPack doesn’t know is that it just takes the module source, analyzes the import syntax to get the dependency, and then tries to load the dependency module. But webpack is blindfolded at this point, because modules like Runtime :util/ Record are runtime dependencies and cannot be found at compile time. Then you need a way to make non-compile-time dependencies invisible to WebPack.

Finally, we get the non-compile-time dependencies. Since the browser does not currently support the ESM import syntax, we need to change it to our own custom module-dependent syntax at the front run time.

3. Analyze the source code using Babel

3.1. Introduction of Babel related tools

For those who are not familiar with Babel and the plugin mechanism, you can read this section for a brief overview.

Babel is a powerful javascript compiler that can transform source code into an AST (abstract syntax tree) through lexical analysis and parsing. By transforming the AST, you can modify the source code, and finally transform the modified AST into object code.

Due to space constraints, THIS article does not cover compiler or AST much, but if you have learned the principles of compilation, you should be familiar with lexical analysis, syntax analysis, token, and AST. Babel is a compiler that translates javascript source code into a particular kind of data structure, called a tree, or AST, that represents the source code well. Babel’s AST is based on ESTree.

For example, the var alienZhou = ‘happy’ statement, processed by Babel, has an AST that looks something like this

{
    type: 'VariableDeclaration'.kind: 'var'./ /... Other attributes
    decolarations: [{
        type: 'VariableDeclarator'.id: {
            type: 'Identifier'.name: 'alienzhou'./ /... Other attributes
        },
        init: {
            type: 'StringLiteral'.value: 'happy'./ /... Other attributes}}}],Copy the code

This part of the AST node represents a variable declaration statement using the var keyword, where the ID and init properties are two AST nodes. These are an Identifier named alienzhou and a StringLiteral with a value of happy.

Here is a brief overview of how to use Babel and some of the libraries it provides for AST analysis and modification. The AST can be generated using methods in babel-core, such as:

const babel = require('babel-core');
const {ast} = babel.transform(`var alienzhou = 'happy'`);
Copy the code

You can then traverse the AST to find specific nodes to modify. Babel also provides us with traverse methods to traverse the AST:

const traverse = require('babel-traverse').default;
Copy the code

To access AST nodes in Babel, vistor mode is used. You can specify AST node type as follows to access desired AST nodes:

traverse(ast, {
    StringLiteral(path) {
        console.log(path.node.value)
        // ...}})Copy the code

This gives you all the string literals, and of course you can replace the contents of this node:

let visitor = {
    StringLiteral(path) {
        console.log(path.node.value)
        path.replaceWith(
            t.stringLiteral('excited'); }}; traverse(ast, visitor);Copy the code

Note that the AST is a mutable object, and all node operations are modified on the original AST.

This article will not cover the API for Babel-Core and Babel-traverse in detail. Instead, it will help you quickly understand the API for babel-core and Babel-traverse.

Since most Webpack projects use Babel in the Loader, you only need to provide a plug-in for Babel to handle non-compile-time dependent syntax. The Babel plug-in simply exports a method that returns the visitor object we mentioned above.

So next we focus on the writing of visitor.

3.2 Write a Babel plug-in to resolve non-compile time dependencies

The ESM import syntax in AST node type is ImportDeclaration:

export default function () {
    return {
        ImportDeclaration: {
            enter(path) {
                // ...
            }
            exit(path) {
                let source = path.node.source;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    // ...
                }
            }
        }
    }
}
Copy the code

In the Enter method, you need to collect information about the ImportDeclaration syntax; In the exit method, determine whether the current ImportDeclaration is a non-compile-time dependency and perform syntactic conversions if so.

Collecting ImportDeclaration syntax information Note that different import Specifier types require different analysis methods. The following five import specifier types are listed:

import util from 'runtime:util';
import * as util from 'runtime:util';
import {util} from 'runtime:util';
import {util as u} from 'runtime:util';
import 'runtime:util';
Copy the code

There are three specifiers:

  • ImportSpecifier:import {util} from 'runtime:util'.import {util as u} from 'runtime:util';
  • ImportDefaultSpecifier:import util from 'runtime:util'
  • ImportNamespaceSpecifier:import * as util from 'runtime:util'

Import ‘Runtime :util’ without specifier

The child nodes can be traversed based on the ImportDeclaration, where a visitor is created to visit the Specifier and collect for different syntax:

const specifierVisitor = {
    ImportNamespaceSpecifier(_path) {
        let data = {
            type: 'NAMESPACE'.local: _path.node.local.name
        };

        this.specifiers.push(data);
    },

    ImportSpecifier(_path) {
        let data = {
            type: 'COMMON'.local: _path.node.local.name,
            imported: _path.node.imported ? _path.node.imported.name : null
        };

        this.specifiers.push(data);
    },

    ImportDefaultSpecifier(_path) {
        let data = {
            type: 'DEFAULT'.local: _path.node.local.name
        };

        this.specifiers.push(data); }}Copy the code

Traversal using specifierVisitor in ImportDeclaration:

export default function () {
    // store the specifiers in one importDeclaration
    let specifiers = [];
    return {
        ImportDeclaration: {
            enter(path) {
                path.traverse(specifierVisitor, { specifiers });
            }
            exit(path) {
                let source = path.node.source;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    // ...
                }
            }
        }
    }
}
Copy the code

So far, we have collected information about import statements when entering the ImportDeclaration node, and when exiting the node, we can determine whether the node is currently non-compile-time dependent. Therefore, if there is a non-compile-time dependency, you simply replace the node syntax based on the information collected.

New nodes can be generated using babel-types. However, using babel-template is recommended to make the code simpler and clearer. The following method, which generates different runtime code based on different import information, assumes that the __my_require__ method is the custom front-end module require method.

const template = require('babel-template');

function constructRequireModule({ local, type, imported, moduleName }) {

    /* using template instead of origin type functions */
    const namespaceTemplate = template(` var LOCAL = __my_require__(MODULE_NAME); `);

    const commonTemplate = template(` var LOCAL = __my_require__(MODULE_NAME)[IMPORTED]; `);

    const defaultTemplate = template(` var LOCAL = __my_require__(MODULE_NAME)['default']; `);

    const sideTemplate = template(` __my_require__(MODULE_NAME); `);
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

    let declaration;
    switch (type) {
        case 'NAMESPACE':
            declaration = namespaceTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName)
            });
            break;

        case 'COMMON':
            imported = imported || local;
            declaration = commonTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName),
                IMPORTED: t.stringLiteral(imported)
            });
            break;

        case 'DEFAULT':
            declaration = defaultTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName)
            });
            break;

        case 'SIDE':
            declaration = sideTemplate({
                MODULE_NAME: t.stringLiteral(moduleName)
            })

        default:
            break;
    }

    return declaration;
}
Copy the code

Finally integrated into the original visitor:

export default function () {
    // store the specifiers in one importDeclaration
    let specifiers = [];
    return {
        ImportDeclaration: {
            enter(path) {
                path.traverse(specifierVisitor, { specifiers });
            }
            exit(path) {
                let source = path.node.source;
                let moduleName = path.node.source.value;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    let nodes;
                    if (specifiers.length === 0) {
                        nodes = constructRequireModule({
                            moduleName,
                            type: 'SIDE'
                        });
                        nodes = [nodes]
                    }
                    else{ nodes = specifiers.map(constructRequireModule); } path.replaceWithMultiple(nodes); } specifiers = []; }}}}Copy the code

Var util = require(‘runtime:util’)[‘default’] var util = require(‘runtime:util’)[‘default’] This code is also printed directly by Webpack.

Thus, with the Babel plug-in, we have accomplished our goal from the beginning of this article.

4. Process dynamic import

Careful readers will notice that we only solved the static import problem above, so dynamic import like the following will still have the above problems.

import('runtime:util').then(u= > {
    u.record(1);
});
Copy the code

Yes, there will still be problems. Therefore, we further need to deal with the syntax of dynamic import. All you need to do is add a new Node type to the visitor:

{
    Import: {
        enter(path) {
            let callNode = path.parentPath.node;
            let nameNode = callNode.arguments && callNode.arguments[0]? callNode.arguments[0] : null;

            if (t.isCallExpression(callNode)
                && t.isStringLiteral(nameNode)
                && /^runtime:/.test(nameNode.value)
            ) {
                let args = callNode.arguments;
                path.parentPath.replaceWith(
                    t.callExpression(
                        t.memberExpression(
                            t.identifier('__my_require__'), t.identifier('async'), false), args )); }}}}Copy the code

The dynamic import code above is replaced with:

__my_require__.async('runtime:util').then(u= > {
    u.record(1);
});
Copy the code

Very convenient.

5. Write at the end

The code for this article is hosted on Github: babel-plugin-import-customized-require

This article starts with a requirement for WebPack compile time, and uses Babel to make some module dependencies in your code not processed at WebPack compile time. In fact, Babel gives us a lot of possibilities.

The problem solved in the article is only a small demand, maybe you will have a better solution; But it’s more about how flexible and powerful Babel is, how much space and possibility it brings to the front end, and how it can be found in many other areas. Hopefully this article will serve as a primer for you to develop another way of thinking about the problem.

The resources

  • Babel AST spec
  • visitor pattern
  • Babel Plugin Handbook
  • AST Explorer