@babel/core is a versatile option if you need to modify js code in JS.

At @babel/core, it’s already integrated

  • astTree Building (parse)
  • astTree traverse and Modification (traverse)
  • astNode types and creation (types)
  • astOutput code (transfromFromAst)

And several common functions, and plug-in mode to achieve business logic decoupling.

In addition, @babel/ Generator is a separate AST code generation tool. It differs from transfromFromAst in that:

generator transfrom
Can be based on any typeastNode output code Can only be based on integrityastThe tree
Code compression, quotation mark selection and other simple configuration Complete code configuration, plug-in support

In addition to @babel/core, another lightweight option is Acorn, and Babel uses Acorn for AST parsing.

The smaller Acorn, compressed to about 80KB, can be used on the Web; But the function is relatively simple, only to achieve AST parsing. Traversal of the AST tree needs to be implemented separately, and modification is performed directly on the AST tree.

Understand the AST syntax tree

Let’s first look at what an AST tree looks like:

const { parse, traverse, types, transformFromAstSync } = require('@babel/core')

const ast = parse('const a = 1')
console.log(JSON.stringify(ast, null.' '))
Copy the code

To make it easier to see, I’ve removed the number of lines of code, the start and end positions, and so on.

{
    "type": "File"."errors": []."program": {
        "type": "Program"."sourceType": "module"."interpreter": null."body": [{"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
                            "type": "Identifier"."identifierName": "a"
                            },
                            "name": "a"
                        },
                        "init": {
                            "type": "NumericLiteral"."extra": {
                                "rawValue": 1."raw": "1"
                            },
                            "value": 1}}]."kind": "const"}]."directives": []},"comments": []}Copy the code

All nodes have a type attribute, and different types have different node structures.

Where, File -> Program is the inherent information of the complete AST document, so we can ignore it temporarily. Start with program. body, which is the body of the code.

  • VariableDeclarationStart declaring variables, types (kind) forconstContains the following variables
    • VariableDeclarator
      • idA variable calleda(id.name)
      • initHas an initial value (init is not null) of typenumber(NumericLiteral)
        • NumericLiteral
          • valueA value of1
    • VariableDeclarator
    • VariableDeclarator
    • (Other variables of this declaration…)

Convert from AST to code

Since we have the complete AST documentation, we can use Transfrom directly for code generation:

const ast = parse('const a = 1')
const { code } = transformFromAstSync(ast)
console.log(code)
Copy the code

The output

% node babel.js
const a = 1;
Copy the code

I put a semicolon on it.

AST traversal

When we need to collect code information, such as analyzing dependencies to do tree-shaking, or when we need to change code, such as const/let to var, we walk through the AST to find and manipulate nodes.

const ast = parse('const a = 1')
traverse(ast, {
    enter(nodePath) {
        console.log(nodePath.type)
    }
})
Copy the code

The output

% node babel.js
Program
VariableDeclaration
VariableDeclarator
Identifier
NumericLiteral
Copy the code

Node type method

The second argument to traverse is a map of the various node manipulation methods: name a method according to the node type, and the corresponding type of nodes will enter the method.

The special one is the Enter method, which all nodes enter before executing the method of their type.

traverse(ast, {
    enter(nodePath) {
        console.log(1, nodePath.type)
    },
    // Node type naming method
    Identifier(nodePath) {
        console.log(2, nodePath.type)
    }
})
Copy the code

The output

% node babel.js
1 Program
1 VariableDeclaration
1 VariableDeclarator
1 Identifier
The node type method is executed after the Enter method
2 Identifier
1 NumericLiteral
Copy the code

NodePath

NodePath is a structure that describes node relationships, exposed through the node method on traverse.

Common attributes/methods include:

  • NodePath. Node The node itself
  • NodePath. ParentPath Indicates the parent node
  • NodePath. Scope syntax context, can get global variables and other information
  • Since the traversal NodePath. Traverse by ()
  • NodePath. ReplaceWith () node replacement
  • Remove () The node is deleted

AST changes

With traverse and exposed NodePath, we can modify the AST.

Modify Node directly

The Node Node is a reference-value property in the NodePath, and manipulating Node directly affects the final code generation. For example, we rename variable A to a:

const ast = parse('const a = 1')
traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        node.name = 'A'}})Copy the code

run

% node babel.js
const A = 1;
Copy the code

Even delete nodes

traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        node.name = 'A'
    },
    VariableDeclarator({ node }) {
        delete node.init // node.init = null also works
    },
    VariableDeclaration({ node }) {
        node.kind = 'var'}})Var A; '
Copy the code

According to practical experience, direct operation of nodes, especially complex nodes, will affect the traversal process and cause some unexpected problems. Therefore, it is recommended to operate complex nodes

Use NodePath

We use the method provided by NodePath to achieve the above effect:

traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        if(node.name === 'a') {
            const newId = types.identifier('A')
            nodePath.replaceWith(newId)
        }
    },
    VariableDeclaration(nodePath) {
        const { node } = nodePath
        const { declarations, kind } = node
        if(kind === 'const') {
            const newNode = types.variableDeclaration('var', declarations)
            nodePath.replaceWith(newNode)
        }
    },
    NumericLiteral(nodePath) {
        if(nodePath.parentPath && nodePath.parentPath.type === 'VariableDeclarator') {
            nodePath.remove()
        }
    }
})
Copy the code

It looks more rigorous and elegant.

Create an AST node using types

After types., the first letter is lowercase, corresponding to the creation method of the node type; The first letter is uppercase, corresponding to the node type definition. @babel/core is very friendly to type and attribute hints and auto-complete.

// types.Identifier
types.identifier('A')
// types.VariableDeclaration
types.variableDeclaration('var', declarations)
Copy the code

Necessary condition judgment

Notice that in each node method, modification criteria are added.

Every time a new node is added or replaced, the traverse process will also go through and the loop will fall into an infinite loop without conditional judgment.

Maximum call stack size exceeded
Copy the code

The plug-in

Write a Babel plug-in

A plug-in is a function that returns a structure:

function plugin() {
    return {
        visitor: {
            // ...}}}Copy the code

That visitor, that’s the second parameter to traverse.

const plugin = () = > {
    return {
        visitor: {
            Identifier(nodePath) {
                const { node } = nodePath
                if(node.name === 'a') {
                    const newId = types.identifier('A')
                    nodePath.replaceWith(newId)
                }
            },
            VariableDeclaration(nodePath) {
                const { node } = nodePath
                const { declarations, kind } = node
                if(kind === 'const') {
                    const newNode = types.variableDeclaration('var', declarations)
                    nodePath.replaceWith(newNode)
                }
            },
            NumericLiteral(nodePath) {
                if(
                    nodePath.parentPath
                    && nodePath.parentPath.type === 'VariableDeclarator'
                ) {
                    nodePath.remove()
                }
            }
        }
    }
}
Copy the code

The plug-in call

const ast = parse('const a = 1')
const { code } = transformFromAstSync(
    ast,
    null,
    { plugins: [ plugin ] }  / / here
)
console.log(code)
Copy the code

run

% node babel.js
var A;
Copy the code

The Babel plugin skill get.

The official plug-in

For common code modifications, many plug-ins have been officially released, except for specific scenarios, generally for scaffolding and other applications, basically enough:

Babeljs. IO/docs/en/plu…

practice

@babel/plugin-transform-async-to-generator

The installation

# https://babeljs.io/docs/en/babel-plugin-transform-async-to-generator
npm i -D @babel/plugin-transform-async-to-generator
Copy the code

code

const ast = parse('const fn = async () => { }')
const { code } = transformFromAstSync(
    ast,
    null,
    { plugins: ['@babel/plugin-transform-async-to-generator']}// Here, string calls.
)
console.log(code)
Copy the code

The output

% node babel.js
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); }}function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

const fn = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator(function* () {});

  return function fn() {
    return_ref.apply(this, arguments); }; } ();Copy the code

The above