Note: This article is not intended to be a beginner’s tutorial on Babel; you need to have some basic knowledge. You should have a basic knowledge of Babel or read the user manual before reading this article

First published in: Zhihu Column,

This article mainly introduces the workflow of Babel, the working principle of the plug-in, and some details that need to be paid attention to when implementing the AST tree operation in real life. I hope this article can help you get familiar with babel-Plugin as soon as possible. Your support is my motivation.

Babel process flow

Readers familiar with compilers know that most compilers have three procedures, and Babel is no exception:

  • Parsing: Parsing code strings into an abstract syntax tree (AST) through word Parsing and Parsing. AST is the core data structure of Babel translation, and subsequent operations depend on AST

  • Word segmentation: A lexical analyzer translates strings of code into Tokens, which are arrays of grammatical fragments. Const a=1 is decomposed into four elements of the token array, each of which contains more detailed descriptions

Const a=1 token array

  • Syntax analysis: Establish the relationship between parsing syntax units and form AST tree

  • Transformation: Transforms the abstract syntax tree. Plug-ins are applied to this process

  • Code Generation: Generates Code strings from the transformed abstract syntax tree

Babel does a pretty good job of parsing and generating. To change the generated results, add the corresponding plug-in operations to the **_ Transformation. **

Babel core

Babel, Webpack, SLATE and Qiankun all adopt microkernel architecture, which means their core functions are very small and most of the functions are realized through plug-in extensions.

@babel/core is the kernel in the “microkernel” architecture. The kernel handles these things:

  • Load and process the configuration
  • Load the plug-in
  • Convert the code to AST via Parse
  • Call the Traverser to traverse the AST and convert the AST using the visitor pattern
  • Generate code, including SourceMap transformation and source code generation

The nature of the Babel plug-in

Babel’s plug-ins are essentially functions that intervene in Babel’s transformation of the AST syntax tree and, by doing so, change the resulting results.

The presets in.babelrc are actually a collection of plugins, and Babel does not “translate” the code unless the presets are added to.babelrc or the plugins tell Babel what to do. Meanwhile, parser does not support extension and is maintained by the authorities. You can use @ to distinguish whether plug-ins are officially maintained.

Babel tool set

Core support classes:

@babel/parser

It is the JavaScript parser used in Babel. ES2017 enabled by default, JSX, Flow, TypeScript support, experimental language proposals (at least stage-0)

@babel/traverse

It implements the visitor pattern, traversing the AST, through which plug-ins can obtain the corresponding AST nodes, and perform specific operations on the corresponding nodes.

@babel/generator

It converts the AST into source code and also supports SourceMap

Plug-in development helper classes:

@babel/template

It is a small but very useful module. It allows you to write string code with placeholders instead of manual encoding, especially useful when generating large-scale AST.

@babel/types

It is a Lodash library for AST nodes, including methods for constructing nodes, validating nodes, and transforming AST nodes. Plug-in development is used frequently.

@babel/helper

It is used for auxiliary code that may not work with simple syntactic conversions, such as the class keyword that is not recognized by older browsers, so you need to add auxiliary code to emulate the class.

Visitor pattern

The visitor pattern is a design pattern that separates data operations from data structures. Also known as the most complex design pattern, and use frequency is not high. The authors of Design Patterns comment that most of the time you don’t need to use the visitor pattern, but when you do, you do.

Why does Babel need observer mode? Imagine that there are so many types of nodes in the AST. If each plug-in traverses the AST itself, it is not only inefficient, but also makes the whole system very unreadable and difficult to maintain.

So the benefits of using the visitor pattern to manipulate the AST are obvious, with the following major advantages

  1. Nodes can be traversed uniformly
  2. Excellent scalability and flexibility
  3. Provides the operation methods of nodes

Plug-in to write

Our goal is to implement an on-demand plug-in, so before we start, what do we need to do?

Of course, our first priority is to make the referenced components load on demand, but we also need to consider:

  • If the user uses alias import, be very careful with Scope if you use alias import. We know that JavaScript supports lexical Scope, where code blocks create new scopes in a tree nested structure. When using alias imports, you need to pay special attention to all variables ** that Reference the alias Binding ** to ensure that you do not break the existing code logic when manipulating parts of the code.
  • If the user uses the default import or namespace import format, stop the conversion and report an error
  • Depending on the scope, you can determine whether a component has been referenced after being introduced. If not, you should remove the node when operating on the AST.
  • Whether the name of the component is concatenated with more than one word, and how the corresponding rules for loading addresses on demand should be translated. It should be converted to a large hump, and the small hump should still be splice with a “-” line
  • Plug-in design should not be limited to a few libraries, but should be designed to be more extensible, providing some parameter Settings for the plug-in to make it more flexible.

The specific effect is as follows:

import {Button as Btn,Input,TimePicker,ConfigProvider,Haaaa} from 'antd'
Copy the code

To:

import _ConfigProvider from "antd/lib/config-provider";
import _Button from "antd/lib/button";
import _Input from "antd/lib/input";
import _TimePicker from "antd/lib/time-picker";
Copy the code

After sorting out what we need to achieve, let’s look at the AST node structure of the following code

import {Button as Btn, Input} from 'antd'
Copy the code

After AST conversion, the structure is (some parameters have been omitted) :

{
    "type": "ImportDeclaration",
    "specifiers": [
        {
            "type": "ImportSpecifier",
            "imported": {
                "type": "Identifier",
                "loc": {
                    "identifierName": "Button"
                },
                "name": "Button"
            },
            "importKind": null,
            "local": {
                "type": "Identifier",
                "loc": {
                    "identifierName": "Btn"
                },
                "name": "Btn"
            }
        },
        {
            "type": "ImportSpecifier",
            "imported": {
                "type": "Identifier",
                "loc": {
                    "identifierName": "Input"
                },
                "name": "Input"
            },
            "importKind": null,
            "local": {
                "type": "Identifier",
                "start": 23,
                "end": 28,
                "loc": {
                    "identifierName": "Input"
                },
                "name": "Input"
            }
        }
    ],
    "importKind": "value",
    "source": {
        "type": "StringLiteral",
        "value": "antd"
    }
}
Copy the code

From the AST structure above, we know that the plug-in needs to process nodes and iterate over their speciFIERS. If non-importDeclaration nodes are found during the iterate, the user is using default imports or namespace imports and should stop the conversion.

The specific implementation of on-demand plug-in is as follows:

module.exports = function ({ type }) { return { visitor: { ImportDeclaration(path, state = { opts }) { const { node } = path; if (! node) return; const { source: { value: libName }, } = node; /** * initializes the plugin's parameters * libraryName: required parameter, libraryName * libraryDirectory: default lib * nameForm: The default is to convert to the "-" link form, with large for converting large humps and small for converting small humps. Give the plug-in key-value for individual cases in the transformation, / const {libraryName, libraryDirectory = 'lib', nameForm = 'default', toImportQueue = {}, } = state.opts; /** * Check whether the user submitted the parameter is valid */ libraryName is mandatory */ if (! libraryName || typeof libraryName ! == 'string' || typeof libraryDirectory ! == 'string' || typeof nameForm ! == 'string' || Object.prototype.toString.call(toImportQueue) ! == '[object Object]' ) assert(libraryName, 'libraryName should be provided'); /** * traverses the specifiers to handle the corresponding nodes */ const ids = {}; const imports = []; if (libName === libraryName) { node.specifiers.forEach(item => { if (t.isImportSpecifier(item)) { const { local: { name: localName = undefined }, imported: { name: importedName = undefined }, } = item; if (! localName || ! importedName) throw path.buildCodeFrameError( 'An error occurred in parsing the abstract syntax tree', ); /** * If (path.scope.getBinding(localName).references === 0) return; */ const id = path.scope.generateUid(' _${localName} '); ids[localName] = id; let horizontal; /** * If the user specified the result of the address translation, the user supplied */ if (! JSON.stringify(toImportQueue) === '{}') { Object.keys(toImportQueue).forEach(key => { if (key === importedName) { horizontal = toImportQueue[key]; }}); } if (! horizontal) { switch (nameForm) { case 'large': { horizontal = importedName[0].toUpperCase() + importedName.substr(1); break; } case 'small': { horizontal = importedName[0].toLowerCase() + importedName.substr(1); break; } default: horizontal = importedName.replace(/([a-zA-Z]+)([A-Z])/g, '$1-$2').toLowerCase(); } } imports.push( t.importDeclaration( [t.importDefaultSpecifier(t.identifier(id))], t.StringLiteral(`${libraryName}/${libraryDirectory}/${horizontal}/index.js`), ), ); /** * Find all references to the target binding and replace them */ const currentBinding = path.scope.getBinding(localName); currentBinding.referencePaths.forEach(scopePath => { const { type } = scopePath; if (type === 'JSXIdentifier') { scopePath.replaceWith(t.jSXIdentifier(id)); } else { scopePath.replaceWith(t.identifier(id)); }}); } else { throw path.buildCodeFrameError('Cannot use default import or namespace import'); }}); path.replaceWithMultiple(imports); ,}}}}; };Copy the code

How is the babel-Plugin applied

Once the plug-in is written, we reference it in.babelrc.

{ "plugins": [ [ "dtImport", { "libraryName": "Antd ", // required /** * libraryDirectory: defaults to lib * nameForm: defaults to '-' link form, large to convert big humps, small to replace bricks small humps * toImportQueue: Give the plug-in key-value for each case in the transformation, with priority greater than nameForm */}]]}Copy the code

Note that Babel executes in the order in which plug-ins are registered, and most plug-ins do not care about the order in which they are defined. In general, new or experimental plugins (as in this example) should be placed first in the array, and officially maintained or stable plugins should be defined later, but some need to be in order, such as modifiers and classes. If the class is compiled first, the modifier compilation will not work properly:

{
  "plugins": [
    "@babel/plugin-proposal-decorators",
    "@babel/plugin-proposal-class-properties"
  ]
}
Copy the code

Note: Preset configurations in the Babel configuration run in the reverse order

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
Copy the code

Plug-in verification

Now we are in the verification phase, testing each of the points we thought about before. The following code is our use case for transformation:

import ReactDOM from 'react-dom';
import React from 'react';
import { Button as Btn, Input } from 'antd';

ReactDOM.render(() => {
  return (
    <>
      <Btn>Default</Btn>
      <Input placeholder="Basic usage" />
    </>
  );
}, document.getElementById('app'));
Copy the code

After the transformation:

import ReactDOM from 'react-dom';
import React from 'react';
import _Btn from 'antd/lib/button/index.js';
import _Input from 'antd/lib/input/index.js';

ReactDOM.render(() => {
  return /* #__PURE__ */ React.createElement(
    React.Fragment,
    null,
    /* #__PURE__ */ React.createElement(_Btn, null, 'Default'),
    /* #__PURE__ */ React.createElement(_Input, {
      placeholder: 'Basic usage',
    }),
  );
}, document.getElementById('app')); // // 'a'==='b'
Copy the code

As you can see from the above verification, if we change the code and add a closure to the stateless component with the variable names _Btn and _Input to see if our plugin breaks the code logic, By the way, introduce an unused component and a composite component (TimePicker and ConfigProvider) in import to see how the plug-in works.

import ReactDOM from 'react-dom';
import React from 'react';
import { Button as Btn, Input, TimePicker, ConfigProvider } from 'antd';

ReactDOM.render(() => {
  function changeInput(e) {
    const _Btn = 'test';
    const _Input = 'test';
    console.log(e.target.value, ' ', _Btn, ' ', _Input);
  }
  return (
    <ConfigProvider>
      <Btn>Default</Btn>
      <Input placeholder="Basic usage" onChange={e => changeInput(e)} />
    </ConfigProvider>
  );
}, document.getElementById('app'));
Copy the code

After the transformation:

import ReactDOM from 'react-dom';
import React from 'react';
import _Btn2 from 'antd/lib/button/index.js';
import _Input2 from 'antd/lib/input/index.js';
import _ConfigProvider from 'antd/lib/config-provider/index.js';

ReactDOM.render(({ a }) => {
  function changeInput(e) {
    const _Btn = 'test';
    const _Input = 'test';
    console.log(e.target.value, ' ', _Btn, ' ', _Input);
  }

  return /* #__PURE__ */ React.createElement(
    _ConfigProvider,
    {
      csp: {
        nonce: 'YourNonceCode',
      },
    },
    /* #__PURE__ */ React.createElement(_Btn2, null, 'Default'),
    /* #__PURE__ */ React.createElement(_Input2, {
      placeholder: 'Basic usage',
      onChange: e => changeInput(e),
    }),
  );
}, document.getElementById('app'));
Copy the code

Because we use scope’s generateUid method to generate the binding when we operate on the AST, we can ensure that the variable name is unique. In the example above, we declared _Btn and _Input in the closure, and changed the name to _Btn2 and _Input2 without affecting the original logic of the code. In addition, the plugin removes unreferenced components based on scope and separates the combination words with a “-” sign, for example: ConfigProvider -> config-provider.

The generateUid method is implemented as follows:

generateUid(name: string = "temp") {
  name = t
    .toIdentifier(name)
    .replace(/^_+/, "")
    .replace(/[0-9]+$/g, "");

  let uid;
  let i = 0;
  do {
    uid = this._generateUid(name, i);
    i++;
  } while (
    this.hasLabel(uid) ||
    this.hasBinding(uid) ||
    this.hasGlobal(uid) ||
    this.hasReference(uid)
  );

  const program = this.getProgramParent();
  program.references[uid] = true;
  program.uids[uid] = true;

  return uid;
}
Copy the code

It uses a while loop to determine whether the current scope, parent scope, etc., have already declared the same variable. If so, i++ is used to ensure uniqueness.

Finally, we implemented the plug-in into the project, packaging a component library within the company. The plug-in will participate in the wBEPack packaging process. We can determine whether the plug-in is effective by analyzing the package structure after webPack is packaged. First, let’s take a look at the analysis before using the plug-in:

After using the plug-in:

You can see that the dT-React-Component already points to its internal counterpart (see chromeDownload), which indicates that the plugin was successfully tested

Write in the last

At present, it has demonstrated the use and results of plug-ins under various circumstances. Due to the limited level of the author, readers are also welcome to point out any bugs in plug-ins in the comment section. If there are explanations inconsistent with the facts in this article, readers are also welcome to point out and improve this article. Readers can also load CSS on demand with plug-ins, completing the import plug-in as an exercise. Finally, let’s take a look at our dT-React-Component library, which is reliable and refined from online business modules with high single-test coverage.