This document covers things like how to create the Babel plug-in.

This manual is available in multiple languages. See the readme for a complete list.

directory

  • introduce
  • basis
    • Abstract syntax tree (ASTs)
    • Processing steps for Babel
    • parsing
      • Lexical analysis
      • Syntax analysis
    • conversion
    • generate
    • traverse
    • Visitors
    • Paths
      • Paths in Visitors
    • State of affairs
    • Scopes (Scopes)
      • Bindings (Bindings)
  • API
    • babylon
    • babel-traverse
    • babel-types
    • (Definitions)
    • Builders
    • Validators
    • Converters
    • babel-generator
    • babel-template
  • Write your first Babel plug-in
  • Conversion operations
    • access
    • Gets the Path of the child node
    • Check the Node type
    • Check the Path type
    • Check whether identifiers are referenced
    • Find the specific parent path
    • Get peer path
    • Stop traversal
    • To deal with
    • Replace a node
    • Replace a single node with multiple nodes
    • Replace nodes with string source code
    • Insert sibling node
    • Insert into a container
    • Remove nodes
    • Replace parent node
    • Deleting a parent node
    • Scope (Scope)
    • Check whether local variables are bound
    • Generate the UID
    • Promote variable declarations to the parent scope
    • Renames the binding and its references
  • Plug-in options
    • Preparation and finishing of the plug-in
    • Enable additional syntax in the plug-in
  • Building a node
  • Best practices
    • Avoid traversing abstract syntax trees (AST)
    • Merge visitor objects in a timely manner
    • If you can find it manually, don’t traverse it
    • Optimize nested visitor objects
    • Watch out for nested structures
    • Unit testing

introduce

Babel is a versatile JavaScript compiler. It also has many modules for different forms of static analysis.

Static analysis is the process of analyzing code without executing it (analyzing code while executing code is dynamic analysis). Static analysis has a variety of purposes. It can be used for syntax checking, compilation, code highlighting, code conversion, optimization, compression, and so on.

You can use Babel to create many types of tools to help you be more productive and write better programs.

Follow on Twitter@thejameskyleTo get updates as soon as possible.


basis

Babel is a JavaScript compiler, or rather a source-to-source compiler, commonly known as a “transpiler”. This means that you provide Some JavaScript code to Babel, which Babel changes and returns to you the newly generated code.

Abstract syntax tree (ASTs)

Each step in this process involves creating or manipulating an abstract syntax tree, also known as an AST.

Babel using a ESTree based and modified the AST, its kernel documentation can be in [here] (https://github. com/Babel/Babel/blob/master/doc/AST/spec. Md) found. .

function square(n) { return n * n; } Duplicate codeCopy the code

The AST Explorer gives you a better sense of the AST nodes. Here is a link to an example of the code above.

The program can be represented as a tree like this:

- FunctionDeclaration: - id: - Identifier: - name: square - params [1] - Identifier - name: n - body: - BlockStatement - body [1] - ReturnStatement - argument - BinaryExpression - operator: * - left - Identifier - name: N-right-identifier - name: n Copy codeCopy the code

Or a JavaScript Object like this:

{ type: "FunctionDeclaration ", id: { type: "Identifier ", name: "square " }, params: [{ type: "Identifier ", name: "n " }], body: { type: "BlockStatement ", body: [{ type: "ReturnStatement ", argument: { type: "BinaryExpression ", operator: "* ", left: { type: "Identifier ", name: "n " }, right: { type: "Identifier ", name: "N"}}}]}} copy codeCopy the code

You’ll notice that each layer of the AST has the same structure:

{ type: "FunctionDeclaration ", id: {... }, params: [...] , body: {... }} Copy the codeCopy the code
{ type: "Identifier ", name: ... } Duplicate codeCopy the code
{ type: "BinaryExpression ", operator: ... , left: {... }, right: {... }} Copy the codeCopy the code

Note: Some attributes have been removed for simplification purposes

Each of these layers is also called a Node. An AST can consist of a single node or hundreds or thousands of nodes. Together, they describe program syntax for static analysis.

Each node has the following Interface:

interface Node { type: string; } Duplicate codeCopy the code

The type field is a string representing the type of the node (for example, “FunctionDeclaration “, “Identifier “, or “BinaryExpression “). Each type of node defines additional attributes that further describe the node type.

Babel also generates additional attributes for each node that describe its position in the original code.

{ type: ... , start: 0, end: 38, loc: { start: { line: 1, column: 0 }, end: { line: 3, column: 1 } }, ... } Duplicate codeCopy the code

Each node has attributes like start, end, and LOC.

Processing steps for Babel

The three main processing steps of Babel are: parse, transform and generate. .

parsing

The parsing step receives the code and outputs the AST. This step is broken down into two stages: Lexical Analysis ** and Syntactic Analysis. .

Lexical analysis

The lexical analysis phase transforms the string code into a stream of tokens. .

You can think of tokens as a flat array of syntax fragments:

n * n; Copy the codeCopy the code
[ { type: { ... }, value: "n ", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: "* ", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: "n ", start: 4, end: 5, loc: { ... } }, ... ] Copy the codeCopy the code

Each type has a set of attributes that describe the token:

{ type: { label: 'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... } Duplicate codeCopy the code

Like AST nodes, they have start, end, and LOC attributes. .

Syntax analysis

The parsing phase converts a token flow into the AST form. This phase uses the information in the tokens to transform them into an AST representation structure that makes subsequent operations easier.

conversion

The transformation step receives the AST and traverses it, adding, updating, and removing nodes during this process. This is the most complicated process in Babel or any other compiler and it’s where the plug-in will come in, and it’s going to be a major part of this manual, so let’s take it slow.

generate

The code generation step converts the final AST (after a series of transformations) into string code and creates source maps. .

Code generation is simple: Depth-first traverses the AST and builds a string that represents the transformed code.

traverse

To convert the AST you need to do recursive tree traversal.

Let’s say we have a FunctionDeclaration type. It has several attributes: ID, Params, and Body, each with some embedded nodes.

{ type: "FunctionDeclaration ", id: { type: "Identifier ", name: "square " }, params: [{ type: "Identifier ", name: "n " }], body: { type: "BlockStatement ", body: [{ type: "ReturnStatement ", argument: { type: "BinaryExpression ", operator: "* ", left: { type: "Identifier ", name: "n " }, right: { type: "Identifier ", name: "N"}}}]}} copy codeCopy the code

So we start with FunctionDeclaration and we know its internal properties (that is: ID, params, body), so we visit each property and their child nodes in turn.

Then we come to ID, which is an Identifier. The Identifier does not have any child node attributes, so let’s move on.

And then params, because it’s an array node we access each of them, they’re all single nodes of type Identifier, and then we move on.

At this point we come to the body, which is a BlockStatement and also has a body node, which is also an array node, and we continue to access each of them.

The only property here is the ReturnStatement node, which has an argument, and when we access the argument, we find BinaryExpression. .

BinaryExpression has one operator, one left, and one right. Operator is not a node, it’s just a value so we don’t have to keep going in, we just need to access left and right. .

Babel’s transformation steps are all traversal like this.

Visitors

When we talk about “entering” a node, we really mean that we are accessing them, and we use the term because of the concept of visitor patterns. .

Visitor is a cross-language pattern for AST traversal. They are simply an object that defines a method for obtaining a specific node in a tree structure. That’s a little abstract so let’s look at an example.

const MyVisitor = { Identifier() { console.log("Called! "); }}; // You can also create a visitor object and add methods to it later. let visitor = {}; visitor.MemberExpression = function() {}; The visitor. FunctionDeclaration = function () {} copy codeCopy the code

Note: Identifier() {… } is Identifier: {enter() {… }}. .

This is a simple visitor that, when used for traversal, calls the Identifier() method whenever an Identifier is encountered in the tree.

So the Identifier() method is called four times in the code below (there are four identifiers, including Square). .

function square(n) { return n * n; } Duplicate codeCopy the code
path.traverse(MyVisitor); Called! Called! Called! Called! Copy the codeCopy the code

These calls all occur on entry to the node, although we can sometimes call the visitor method on exit. .

Suppose we have a tree structure:

- FunctionDeclaration - Identifier (id) - Identifier (params[0]) - BlockStatement (body) - ReturnStatement (body) - BinaryExpression (argument) -identifier (left) -identifier (right) Copies codeCopy the code

As we go down each branch of the tree we eventually get to the end, so we need to go up to get to the next node. As we walk down the tree we enter each node, and as we walk up we exit each node.

Let’s walk through this process using the tree above as an example.

  • Enter theFunctionDeclaration
    • Enter theIdentifier (id)
    • Come to an end
    • exitIdentifier (id)
    • Enter theIdentifier (params[0])
    • Come to an end
    • exitIdentifier (params[0])
    • Enter theBlockStatement (body)
    • Enter theReturnStatement (body)
      • Enter theBinaryExpression (argument)
      • Enter theIdentifier (left)
        • Come to an end
      • exitIdentifier (left)
      • Enter theIdentifier (right)
        • Come to an end
      • exitIdentifier (right)
      • exitBinaryExpression (argument)
    • exitReturnStatement (body)
    • exitBlockStatement (body)
  • exitFunctionDeclaration

So you actually have two chances to visit a node when creating visitors.

const MyVisitor = { Identifier: { enter() { console.log("Entered! "); }, exit() { console.log("Exited! "); }}}; Copy the codeCopy the code

If necessary, you can use the method name | divided into Idenfifier | MemberExpression form of string, use the same function in a variety of access nodes. .

An example in the flow-Comments plug-in is as follows:

const MyVisitor = { "ExportNamedDeclaration|Flow "(path) {} }; Copy the codeCopy the code

You can also use aliases (as defined by babel-types) in visitors.

For example,

Function is an alias for FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod.

const MyVisitor = { Function(path) {} }; Copy the codeCopy the code

Paths

An AST usually has many nodes, so how do the nodes relate directly to each other? We can represent relationships between nodes with a large mutable object that can be manipulated and accessed, or we can simplify things with Paths. .

Path is an object that represents the connection between two nodes.

For example, if you have the following node and its children

{ type: "FunctionDeclaration ", id: { type: "Identifier ", name: "square " }, ... } Duplicate codeCopy the code

The child Identifier, expressed as a Path, looks like this:

{ "parent ": { "type ": "FunctionDeclaration ", "id ": {... },... }, "node ": {"type ": "Identifier ", "name ": "square "}} duplicate codeCopy the code

It also contains additional metadata about the path:

{ "parent ": {... }, "node ": {... }, "hub ": {... }, "contexts ": [], "data ": {}, "shouldSkip ": false, "shouldStop ": false, "removed ": false, "state ": null, "opts ": null, "skipKeys ": null, "parentPath ": null, "context ": null, "container ": null, "listKey ": Null, "inList ": false, "parentKey ": NULL, "key ": NULL, "scope ": null, "type ": null, "typeAnnotation ": null} Copy codeCopy the code

Of course, path objects also contain many other methods for adding, updating, moving, and removing nodes, which we’ll look at later.

In a sense, a path is a Reactive representation of a node’s position in the tree and information about that node. When you call a tree modification method, the path information is also updated. Babel manages all this for you, making it as simple and stateless as possible.

Paths in Visitors

When you have a visitor to the Identifier() member method, you are actually accessing the path rather than the node. In this way, you are dealing with the responsive representation of the node rather than the node itself.

const MyVisitor = { Identifier(path) { console.log("Visiting: " + path.node.name); }}; Copy the codeCopy the code
a + b + c; Copy the codeCopy the code
path.traverse(MyVisitor); Visiting: A Visiting: B Visiting: cCopy the code

State of affairs

State is the enemy of abstract syntax tree AST transformations, state management is a constant drag on your energy, and almost all of your assumptions about state will always have some unconsidered syntax that turns out to be wrong.

Consider the following code:

function square(n) { return n * n; } Duplicate codeCopy the code

Let’s write a quick implementation of a visitor that renames n to x.

let paramName; const MyVisitor = { FunctionDeclaration(path) { const param = path.node.params[0]; paramName = param.name; param.name = "x "; }, Identifier(path) { if (path.node.name === paramName) { path.node.name = "x "; }}}; Copy the codeCopy the code

This visitor code might work for the example above, but it’s easy to break:

function square(n) { return n * n; } n; Copy the codeCopy the code

A better way to do this is to use recursion. Let’s put one visitor inside another visitor, like in Christopher Nolan’s movie Inception.

const updateParamNameVisitor = { Identifier(path) { if (path.node.name === this.paramName) { path.node.name = "x "; }}}; const MyVisitor = { FunctionDeclaration(path) { const param = path.node.params[0]; const paramName = param.name; param.name = "x "; path.traverse(updateParamNameVisitor, { paramName }); }}; path.traverse(MyVisitor); Copy the codeCopy the code

Of course, this is just a deliberately written example, but it demonstrates how to eliminate global state from visitors.

Scopes (Scopes)

Next, let’s introduce the concept of scope. JavaScript supports lexical scope, where blocks of code create new scopes in a tree nested structure.

// scope 1 function scopeTwo() {// scope 2}Copy the code

In JavaScript, whenever you create a reference, whether through variables, functions, types, params, module imports, labels, etc., it belongs to the current scope.

var global = "I am in the global scope "; function scopeOne() { var one = "I am in the scope created by `scopeOne()` "; function scopeTwo() { var two = "I am in the scope created by `scopeTwo()` "; }} Copy the codeCopy the code

Deeper inner scope code can use references from outer scopes.

function scopeOne() { var one = "I am in the scope created by `scopeOne()` "; function scopeTwo() { one = "I am updating the reference in `scopeOne` inside `scopeTwo` "; }} Copy the codeCopy the code

An inner scope can also create a reference with the same name as an outer scope.

function scopeOne() { var one = "I am in the scope created by `scopeOne()` "; function scopeTwo() { var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone. "; }} Copy the codeCopy the code

When writing a transformation, you must be careful about scoping. We need to make sure that changing parts of the code doesn’t break existing code.

When we add a new reference, we need to make sure that the name of the newly added reference does not conflict with all existing references. Or we just want to find all references that use a variable, we just want to find those references within a given Scope.

The scope can be expressed as follows:

{ path: path, block: path.node, parentBlock: path.parent, parent: parentScope, bindings: [...] } Duplicate codeCopy the code

When you create a new scope, you need to give its path and parent scope, and then it collects all references (” bindings “) in that scope during traversal.

Once the references are collected, you can use various methods on Scopes, which we’ll learn about later.

Bindings (Bindings)

All references belong to a specific scope, and the relationship between reference and scope is called a binding. .

function scopeOnce() { var ref = "This is a binding "; ref; // This is a reference to a binding function scopeTwo() { ref; // This is a reference to a binding from a lower scope}Copy the code

The individual bindings look like this

Text for Translation { identifier: node, scope: scope, path: path, kind: 'var', referenced: true, references: 3, referencePaths: [PATH, path, path], constant: false, constantViolations: [PATH]} Copy codeCopy the code

With this information you can look up all references to a binding, know what type of binding it is (parameters, definitions, etc.), find its scope, or copy its identifier. You can even tell if it’s constant, and if not, which path modified it.

There are many situations in which it is useful to know if a binding is constant, and one of the most useful situations is when code is compressed.

function scopeOne() { var ref1 = "This is a constant binding "; becauseNothingEverChangesTheValueOf(ref1); function scopeTwo() { var ref2 = "This is *not* a constant binding "; ref2 = "Because this changes the value "; }} Copy the codeCopy the code

API

Babel is actually a collection of modules. In this section we explore some of the major modules, explaining what they do and how to use them.

Note: This section is not a substitute for the detailed API documentation, which will be available shortly.

babylon

Babylon is the parser for Babel. Originally came out of Acorn project Fork. Acorn was fast, easy to use, and designed a plug-in based architecture for non-standard features (and those that will be standard features in the future).

First, let’s install it.

$NPM install --save Babylon copies the codeCopy the code

Start by parsing a code string:

import * as babylon from "babylon "; const code = `function square(n) { return n * n; } `; babylon.parse(code); // Node { // type: "File ", // start: 0, // end: 38, // loc: SourceLocation {... }, // program: Node {... }, // comments: [], // tokens: [...] //} copy codeCopy the code

We can also pass options to the parse() method as follows:

babylon.parse(code, { sourceType: "module ", // default: "script " plugins: ["jsx "] // default: [] }); Copy the codeCopy the code

SourceType can be “module “or “script”, which indicates which mode Babylon should be parsed in. “Module” will parse in strict mode and allow module definition, “script “will not.

Note: sourceType defaults to “script “and generates an error when import or export is found. ScourceType: “module “is used to avoid these errors.

Because Babylon uses a plug-in based architecture, there is a plugins option to switch on and off the built-in plug-ins. Note that Babylon has not yet made this API available to external plug-ins, and it is not ruled out that it will be available in the future.

For a complete list of plug-ins, see the Babylon README file. .

babel-traverse

The Babel Traverse module maintains the state of the entire tree and is responsible for replacing, removing, and adding nodes.

Run the following command to install:

$NPM install --save babel-traverse copy codeCopy the code

We can use Babylon with us to iterate over and update nodes:

import * as babylon from "babylon "; import traverse from "babel-traverse "; const code = `function square(n) { return n * n; } `; const ast = babylon.parse(code); traverse(ast, { enter(path) { if ( path.node.type === "Identifier " && path.node.name === "n " ) { path.node.name = "x "; }}}); Copy the codeCopy the code

babel-types

The Babel Types module is a Lodash library for AST nodes that contains methods for constructing, validating, and transforming AST nodes. This tool library contains thoughtful tool methods that are useful for writing logic that processes AST.

To install it, run the following command:

$NPM install --save babel-typesCopy the code

Then use it as follows:

import traverse from "babel-traverse "; import * as t from "babel-types "; traverse(ast, { enter(path) { if (t.isIdentifier(path.node, { name: "n " })) { path.node.name = "x "; }}}); Copy the codeCopy the code

(Definitions)

The Babel Types module has a definition of each node of a single type, including what attributes the node contains, what legal values are, how to build the node, traverse the node, and the alias of the node.

A single node type is defined as follows:

defineType("BinaryExpression ", { builder: ["operator ", "left ", "right "], fields: { operator: { validate: assertValueType("string ") }, left: { validate: assertNodeType("Expression ") }, right: { validate: assertNodeType("Expression ") } }, visitor: ["left ", "right "], aliases: ["Binary ", "Expression "] }); Copy the codeCopy the code

Builders

You’ll notice that the BinaryExpression definition above has a Builder field. .

Builder: ["operator ", "left ", "right "] Copy codeCopy the code

This is because each node type has a constructor method builder, which is used like this:

t.binaryExpression("* ", t.identifier("a "), t.identifier("b ")); Copy the codeCopy the code

You can create an AST like this:

{ type: "BinaryExpression ", operator: "* ", left: { type: "Identifier ", name: "a " }, right: { type: "Identifier ", name: "b "}} Duplicate codeCopy the code

When printed it looks like this:

A * B copies the codeCopy the code

The constructor also validates the nodes it creates and throws a descriptive error in case of misuse, which leads to the next method type.

Validators

The definition of BinaryExpression also contains information about the node’s fields and how to validate those fields.

fields: { operator: { validate: assertValueType("string ") }, left: { validate: assertNodeType("Expression ") }, right: {validate: assertNodeType("Expression ")}} Copies the codeCopy the code

Two validation methods can be created. The first is isX. .

t.isBinaryExpression(maybeBinaryExpressionNode); Copy the codeCopy the code

This test is used to ensure that the node is a binary expression, and you can also pass in a second argument to ensure that the node contains specific properties and values.

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "* " }); Copy the codeCopy the code

There is also an assertion version of these methods that throws exceptions instead of returning true or false. .

t.assertBinaryExpression(maybeBinaryExpressionNode); t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "* " }); // Error: Expected type "BinaryExpression "with option {"operator ": "*"Copy the code

Converters

[WIP]

babel-generator

The Babel Generator module is a code Generator for Babel that reads the AST and translates it into code and sourcemaps (sourcemaps).

Run the following command to install it:

$NPM install --save babel-generator copy codeCopy the code

Then use it as follows:

import * as babylon from "babylon "; import generate from "babel-generator "; const code = `function square(n) { return n * n; } `; const ast = babylon.parse(code); generate(ast, {}, code); // { // code: "... ", // map: "... "//} copy the codeCopy the code

You can also pass options to the generate() method. .

generate(ast, { retainLines: false, compact: "auto ", concise: false, quotes: "double ", // ... }, code); Copy the codeCopy the code

babel-template

Babel-template is another module that is small but very useful. It lets you write string code with placeholders instead of manual encoding, especially when generating large AST. In computer science, this ability is called quasiquotes.

$NPM install --save babel-templateCopy the code
import template from "babel-template "; import generate from "babel-generator "; import * as t from "babel-types "; const buildRequire = template(` var IMPORT_NAME = require(SOURCE); `); const ast = buildRequire({ IMPORT_NAME: t.identifier("myModule "), SOURCE: t.stringLiteral("my-module ") }); console.log(generate(ast).code); Copy the codeCopy the code
var myModule = require("my-module "); Copy the codeCopy the code

Write your first Babel plug-in

Now that you’re familiar with all the basics of Babel, let’s blend that knowledge with the plug-in API to write our first Babel plug-in.

Start with a function that takes the current Babel object as an argument.

Export default function(Babel) {// plugin contentsCopy the code

Since you will be using babel.types a lot, it is more convenient to simply retrieve babel.types:

Export default function({types: t}) {// plugin contents} copy codeCopy the code

It then returns an object whose Visitor property is the primary visitor to the plug-in.

export default function({ types: t }) { return { visitor: { // visitor contents } }; }; Copy the codeCopy the code

Each function in the Visitor receives two parameters: path and state

export default function({ types: t }) { return { visitor: { Identifier(path, state) {}, ASTNodeTypeHere(path, state) {} } }; }; Copy the codeCopy the code

Let’s quickly write a working plug-in to show how it works. Here is our source code:

foo === bar; Copy the codeCopy the code

Its AST form is as follows:

{ type: "BinaryExpression ", operator: "===", left: { type: "Identifier ", name: "foo " }, right: { type: "Identifier ", name: "bar "}} Duplicate codeCopy the code

We start by adding the BinaryExpression visitor method:

export default function({ types: t }) { return { visitor: { BinaryExpression(path) { // ... }}}; } Duplicate codeCopy the code

Then let’s be more specific and focus only on those that use the BinaryExpression of ===.

visitor: { BinaryExpression(path) { if (path.node.operator ! == "===") { return; } / /... }} Copy the codeCopy the code

Now we replace the left attribute with a new identifier:

BinaryExpression(path) { if (path.node.operator ! == "===") { return; } path.node.left = t.identifier("sebmck "); / /... } Duplicate codeCopy the code

So if we run the plugin we get:

sebmck === bar; Copy the codeCopy the code

Now we just need to replace the right property.

BinaryExpression(path) { if (path.node.operator ! == "===") { return; } path.node.left = t.identifier("sebmck "); path.node.right = t.identifier("dork "); } Duplicate codeCopy the code

This is our final result:

sebmck === dork; Copy the codeCopy the code

Perfect! Our first Babel plugin.


Conversion operations

access

Gets the Path of the child node

To get the property values of an AST node, we typically access the node and use the path.node.property method.

// the BinaryExpression AST node has properties: `left`, `right`, `operator` BinaryExpression(path) { path.node.left; path.node.right; path.node.operator; } Duplicate codeCopy the code

If you want to access the path inside the property, use the get method of the path object, passing the property as a string.

BinaryExpression(path) { path.get('left'); } Program(path) { path.get('body.0'); } Duplicate codeCopy the code

Check the node type

If you want to check the types of nodes, the best way is:

BinaryExpression(path) { if (t.isIdentifier(path.node.left)) { // ... }} Copy the codeCopy the code

You can also perform a shallow check on node attributes:

BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n " })) { // ... }} Copy the codeCopy the code

Functionally equivalent to:

BinaryExpression(path) { if ( path.node.left ! = null && path.node.left.type === "Identifier " && path.node.left.name === "n " ) { // ... }} Copy the codeCopy the code

Check the Path type

A path has the same method to check the type of node:

BinaryExpression(path) { if (path.get('left').isIdentifier({ name: "n " })) { // ... }} Copy the codeCopy the code

Equivalent to:

BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n " })) { // ... }} Copy the codeCopy the code

Check whether identifiers are referenced

Identifier(path) { if (path.isReferencedIdentifier()) { // ... }} Copy the codeCopy the code

Or:

Identifier(path) { if (t.isReferenced(path.node, path.parent)) { // ... }} Copy the codeCopy the code

Find the specific parent path

Sometimes you need to traverse a path up the syntax tree until a condition is met.

For each parent path, callback is called with its NodePath as an argument, and when callback returns true, its NodePath is returned. .

path.findParent((path) => path.isObjectExpression()); Copy the codeCopy the code

If you also need to traverse the current node:

path.find((path) => path.isObjectExpression()); Copy the codeCopy the code

Find the nearest parent function or program:

path.getFunctionParent(); Copy the codeCopy the code

Walk up the syntax tree until you find the parent path in the list

path.getStatementParent(); Copy the codeCopy the code

Get peer path

If a path is in a list of functions/programs, it has sibling nodes.

  • usepath.inListTo determine if the path has sibling nodes,
  • usepath.getSibling(index)To get the sibling path,
  • usepath.keyGets the index of the container where the path resides,
  • usepath.containerA container for getting paths (an array of all sibling nodes)
  • usepath.listKeyGets the key of the container

These apis are used for the transform-merge-Sibling-variables </> plug-in used in babel-minify </>.

Copy the codeCopy the code

var a = 1; // pathA, path.key = 0 var b = 2; // pathB, path.key = 1 var c = 3; // pathC, path.key = 2

```js export default function({ types: t }) { return { visitor: { VariableDeclaration(path) { // if the current path is pathA path.inList // true path.listKey // "body " path.key // 0 path.getSibling(0) // pathA path.getSibling(path.key + 1) // pathB path.container // [pathA, pathB, pathC] } } }; } Duplicate codeCopy the code

Stop traversal

If your plugin needs to stop running in some way, the easiest thing to do is to write it back as soon as possible.

BinaryExpression(path) { if (path.node.operator ! == '**') return; } Duplicate codeCopy the code

If you are subtraversing a top-level path, you can use two provided API methods:

path.skip() skips traversing the children of the current path. path.stop() stops traversal entirely.

outerPath.traverse({ Function(innerPath) { innerPath.skip(); // if checking the children is irrelevant }, ReferencedIdentifier(innerPath, state) { state.iife = true; innerPath.stop(); // if you want to save some state and then stop traversal, or deopt } }); Copy the codeCopy the code

To deal with

Replace a node

BinaryExpression(path) { path.replaceWith( t.binaryExpression("** ", path.node.left, t.numberLiteral(2)) ); } Duplicate codeCopy the code
function square(n) { - return n * n; + return n ** 2; } Duplicate codeCopy the code

Replace a single node with multiple nodes

ReturnStatement(path) { path.replaceWithMultiple([ t.expressionStatement(t.stringLiteral("Is this the real life? ")), t.expressionStatement(t.stringLiteral("Is this just fantasy? ")), t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head) ")), ]); } Duplicate codeCopy the code
function square(n) { - return n * n; + "Is this the real life? "; + "Is this just fantasy? "; + "(Enjoy singing the rest of the song in your head) "; } Duplicate codeCopy the code

** Note: </> When replacing an expression with multiple nodes, they must be declarations. This is because Babel makes extensive use of heuristic algorithms when replacing nodes, which means you can do some pretty crazy transformations that would otherwise be tedious.

Replace nodes with string source code

Copy the codeCopy the code

FunctionDeclaration(path) { path.replaceWithSourceString(function add(a, b) { return a + b; }); }

```diff - function square(n) { - return n * n; + function add(a, b) { + return a + b; } Duplicate codeCopy the code

** Note: </> This API is not recommended unless you are dealing with dynamic source strings, otherwise it is more efficient to parse the code outside the visitor.

Insert sibling node

Copy the codeCopy the code

FunctionDeclaration(path) { path.insertBefore(t.expressionStatement(t.stringLiteral(“Because I ‘m easy come, easy go.”))); path.insertAfter(t.expressionStatement(t.stringLiteral(“A little high, little low.”))); }

```diff + "Because I'm easy come, easy go. "; function square(n) { return n * n; } + "A little high, little low. "; Copy the codeCopy the code

Note: </> should also use declarations or an array of declarations. This uses the same heuristic mentioned in replacing a node with multiple nodes </>. .

Insert into a container

If you want to insert an array like body
in the AST node property. It is similar to insertBefore/insertAfter, but you must specify the listKey (usually the body).

Copy the codeCopy the code

ClassMethod(path) { path.get(‘body’).unshiftContainer(‘body’, t.expressionStatement(t.stringLiteral(‘before’))); path.get(‘body’).pushContainer(‘body’, t.expressionStatement(t.stringLiteral(‘after’))); }

```diff class A { constructor() { + "before " var a = 'middle'; + "after "}} Copy the codeCopy the code

Deleting a node

FunctionDeclaration(path) { path.remove(); } Duplicate codeCopy the code
- function square(n) { - return n * n; -} Copy codeCopy the code

Replace parent node

Simply call replaceWith using parentPath: ‘path.parentPath

BinaryExpression(path) { path.parentPath.replaceWith( t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn 't really matter to me, to me.")) ); } 'copy the codeCopy the code

function square(n) { - return n * n; + "Anyway the wind blows, doesn't really matter to me, to me. "; } Duplicate codeCopy the code

Deleting a parent node

BinaryExpression(path) { path.parentPath.remove(); } Duplicate codeCopy the code

function square(n) { - return n * n; } Duplicate codeCopy the code

Scope (Scope)

Check whether local variables are bound

FunctionDeclaration(path) { if (path.scope.hasBinding("n ")) { // ... }} Copy the codeCopy the code

This will traverse the range tree and check for specific bindings.

You can also check if a scope has its own binding:

FunctionDeclaration(path) { if (path.scope.hasOwnBinding("n ")) { // ... }} Copy the codeCopy the code

Create a UID

This will generate an identifier that will not conflict with any locally defined variables.

FunctionDeclaration(path) { path.scope.generateUidIdentifier("uid "); // Node { type: "Identifier ", name: "_uid " } path.scope.generateUidIdentifier("uid "); // Node {type: "Identifier ", name: "_uid2 "}} Copy codeCopy the code

Promote variable declarations to the parent scope

Sometimes you may want to push a ‘VariableDeclaration’ so that you can assign to it.

FunctionDeclaration(path) { const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id); path.remove(); path.scope.parent.push({ id, init: path.node }); } 'copy the codeCopy the code

- function square(n) { + var _square = function square(n) { return n * n; -} +}; Copy the codeCopy the code

Renames the binding and its references

FunctionDeclaration(path) { path.scope.rename("n ", "x "); } Duplicate codeCopy the code

- function square(n) { - return n * n; + function square(x) { + return x * x; } Duplicate codeCopy the code

Alternatively, you can rename the binding to the generated unique identifier:

FunctionDeclaration(path) { path.scope.rename("n "); } Duplicate codeCopy the code

- function square(n) { - return n * n; + function square(_n) { + return _n * _n; } Duplicate codeCopy the code

Plug-in options

If you want your users to customize the behavior of your Babel plug-in you can accept plugin-specific options that users can specify, as shown below:

Plugins: [["my-plugin ", {"option1 ": true, "option2 ": false}]]} Plugins: [["my-plugin ", {"option1 ": true, "option2 ": false}]Copy the code

These options are passed to the plug-in visitor via the ‘status object:

export default function({ types: t }) { return { visitor: { FunctionDeclaration(path, state) { console.log(state.opts); // {option1: true, option2: false}}}}} 'copies the codeCopy the code

These options are plug-in specific and you do not have access to options in other plug-ins.

Preparation and finishing of the plug-in

A plug-in can have functions that run before or after the plug-in. They can be used for setup or cleaning/analysis purposes.

export default function({ types: t }) { return { pre(state) { this.cache = new Map(); }, visitor: { StringLiteral(path) { this.cache.set(path.node.value, 1); } }, post(state) { console.log(this.cache); }}; } Duplicate codeCopy the code

Enable additional syntax in the plug-in

The plug-in enables Babylon plugins so that users do not need to install/enable them. This prevents parsing errors without inheriting syntax plug-ins.

export default function({ types: t }) { return { inherits: require("babel-plugin-syntax-jsx ") }; } Duplicate codeCopy the code

Throws a syntax error

If you want to throw an error with babel-code-frame and a message:

export default function({ types: t }) { return { visitor: { StringLiteral(path) { throw path.buildCodeFrameError("Error message here "); }}}; } Duplicate codeCopy the code

The error looks like:

file.js: Error message here 7 | 8 | let tips = [ > 9 | "Click on any AST node with a '+' to expand it ", | | | ^ 10 11 "Hovering over a node highlights the 12 \ | corresponding part in the source code", copy the code
                                            
                                            
                                            
                                            Copy the code

Building a node

When writing a transformation, you usually need to build some nodes to insert into the AST. As mentioned earlier, you can use the Builder method in the ‘babel-types package.

The constructor method name is the name of the node type you want, except for the first lowercase letter. For example, if you want to create a MemberExpression you can use t.ember Expression (…). .

The parameters of these builders are determined by the node definitions. There is some work being done to generate easy-to-read file definitions, but now they can all be found here. .

The node definition is as follows:

defineType("MemberExpression", { builder: ["object", "property", "computed"], visitor: ["object", "property"], aliases: ["Expression", "LVal"], fields: { object: { validate: assertNodeType("Expression") }, property: { validate(node, key, val) { let expectedType = node.computed ? "Expression" : "Identifier"; assertNodeType(expectedType)(node, key, val); } }, computed: { default: false } } }); 'Copy codeCopy the code

Here you can see all the information about this particular node type, including how to build it, walk through it, and verify it.

By looking at the generator properties, you can see the three parameters required to call the generator method (T. Conditions).

Generator: ["object", "property", "computed"], copy codeCopy the code

Note that sometimes there are more properties that can be customized on a node than the constructor array contains. This is to prevent the generator from having too many parameters. In these cases, you need to set the properties manually. An example is ClassMethod .

// Example // because the builder doesn't contain `async` as a property var node = t.classMethod( "constructor", t.identifier("constructor"), params, body ) // set it manually after creation node.async = true; Copy the codeCopy the code

You can see the validation for the builder arguments with the fields object.

Copy the codeCopy the code

fields: { object: { validate: assertNodeType("Expression") }, property: { validate(node, key, val) { let expectedType = node.computed ? "Expression" : "Identifier"; assertNodeType(expectedType)(node, key, val); } }, computed: { default: false } }

You can see that `object` needs to be an `Expression`, `property` either needs to be an `Expression` or an `Identifier` depending on if the member expression is `computed` or not and `computed` is simply a boolean that defaults to `false`. So we can construct a `MemberExpression` by doing the following: ```js t.memberExpression( t.identifier('object'), t.identifier('property') // `computed` is optional ); Copy the codeCopy the code

Which will result in:

Object. Property Copies the codeCopy the code

However, we said that object needed to be an Expression so why is Identifier valid?

Well if we look at the definition of Identifier we can see that it has an aliases property which states that it is also an expression.

Aliases: ["Expression", "LVal"], copy codeCopy the code

So since MemberExpression is a type of Expression, we could set it as the object of another MemberExpression:

t.memberExpression( t.memberExpression( t.identifier('member'), t.identifier('expression') ), T. identifier(' Property ')) copies the codeCopy the code

Which will result in:

Member. The expression. The property duplicate codeCopy the code

It's very unlikely that you will ever memorize the builder method signatures for every node type. So you should take some time and understand how they are generated from the node definitions.

You can find all of the actual definitions here and you can see them documented here

Best practices

Create Helper Builders and Checkers

It's pretty simple to extract certain checks (if a node is a certain type) into their own helper functions as well as extracting out helpers for specific node types.

function isAssignment(node) { return node && node.operator === opts.operator + "="; } function buildAssignment(left, right) { return t.assignmentExpression("=", left, right); } Duplicate codeCopy the code

Avoid traversing abstract syntax trees (AST)

Traversing the AST is expensive, and it's easy to accidentally traverse the AST more than necessary. This could be thousands if not tens of thousands of extra operations.

Babel optimizes this as much as possible, merging visitors together if it can in order to do everything in a single traversal.

Merge visitor objects in a timely manner

When writing visitors, it may be tempting to call path.traverse in multiple places where they are logically necessary.

path.traverse({ Identifier(path) { // ... }}); path.traverse({ BinaryExpression(path) { // ... }}); Copy the codeCopy the code

However, it is far better to write these as a single visitor that only gets run once. Otherwise you are traversing the same tree multiple times for no reason.

path.traverse({ Identifier(path) { // ... }, BinaryExpression(path) { // ... }}); Copy the codeCopy the code

If you can find it manually, don't traverse it

It may also be tempting to call path.traverse when looking for a particular node type.

const nestedVisitor = { Identifier(path) { // ... }}; const MyVisitor = { FunctionDeclaration(path) { path.get('params').traverse(nestedVisitor); }}; Copy the codeCopy the code

However, if you are looking for something specific and shallow, there is a good chance you can manually lookup the nodes you need without performing a costly traversal.

const MyVisitor = { FunctionDeclaration(path) { path.node.params.forEach(function() { // ... }); }}; Copy the codeCopy the code

Optimize nested visitor objects

When you nest visitors, it might make sense to nest them in your code.

const MyVisitor = { FunctionDeclaration(path) { path.traverse({ Identifier(path) { // ... }}); }}; Copy the codeCopy the code

However, a new visitor object is created each time ‘FunctionDeclaration() is called. That can be costly, because Babel does some processing each time a new visitor object is passed in (such as exploding keys containing multiple types, performing validation, and adjusting the object structure). Because Babel stores flags on visitor objects indicating that it’s already performed that processing, it’s better to store the visitor in a variable and pass the same object each time.

const nestedVisitor = { Identifier(path) { // ... }}; const MyVisitor = { FunctionDeclaration(path) { path.traverse(nestedVisitor); }}; 'Copy codeCopy the code

If you need some state in nested visitors, like this:

const MyVisitor = { FunctionDeclaration(path) { var exampleState = path.node.params[0].name; path.traverse({ Identifier(path) { if (path.node.name === exampleState) { // ... }}}); }}; Copy the codeCopy the code

You can pass it as a status to the traverse()
method and have access to this in the visitor.

const nestedVisitor = { Identifier(path) { if (path.node.name === this.exampleState) { // ... }}}; const MyVisitor = { FunctionDeclaration(path) { var exampleState = path.node.params[0].name; path.traverse(nestedVisitor, { exampleState }); }}; Copy the codeCopy the code

Watch out for nested structures

Sometimes, when considering a given transformation, it is possible to forget that a given transformation structure can be nested.

For example, imagine that we want to find the constructor ClassMethod Foo ClassDeclaration.

class Foo { constructor() { // ... }} Copy the codeCopy the code

const constructorVisitor = { ClassMethod(path) { if (path.node.name === 'constructor') { // ... } } } const MyVisitor = { ClassDeclaration(path) { if (path.node.id.name === 'Foo') { path.traverse(constructorVisitor); }}} copy the codeCopy the code

We ignore the fact that classes can be nested. Using traversal, we would also get a nested ‘constructor’ above:

class Foo { constructor() { class Bar { constructor() { // ... }}}} 'copy the codeCopy the code

Unit testing

There are several main ways to test the Babel plug-in: snapshot tests, AST tests, and execution tests. For this example, we will use Jest because it supports out-of-box snapshot testing. The example we create here is hosted on this repo.

First we need a Babel plug-in, which we’ll put in SRC/index.js.

<br />module.exports = function testPlugin(babel) { return { visitor: { Identifier(path) { if (path.node.name === 'foo') { path.node.name = 'bar'; }}}}; }; Copy the codeCopy the code

A snapshot of the test

Next, install our dependencies with ‘ ‘NPM install –save-dev babel-core jest , so we can start writing our first test: snapshot. Snapshot testing allows us to visually examine the output of our Babel plug-in. We give it an input, tell it a snapshot, and save it to a file. We check the snapshot into Git. This allows us to see when we affected the output of any of our trial sample tests. It also gives the use of differences when pulling requests. Of course, you can do this with any test framework, but updating the snapshot is as simple as jest-u .

// src/__tests__/index-test.js const babel = require('babel-core'); const plugin = require('.. / '); var example = ` var foo = 1; if (foo) console.log(foo); `; it('works', () => { const {code} = babel.transform(example, {plugins: [plugin]}); expect(code).toMatchSnapshot(); }); Copy the codeCopy the code

This gives us a snapshot file at ‘ ‘SRC / __ tests __ / __ snapshots __ / index-test.js.snap .

exports[`test works 1`] = ` " var bar = 1; if (bar) console.log(bar);" `; Copy the codeCopy the code

If we change “bar” to “baz” in the plug-in and run it again, we get the following results:

The received value does not match the stored snapshot 1. -snapshot + Received @@ -1,3 +1,3 @@ "-var bar = 1; -if (bar) console.log(bar);" +var baz = 1; +if (baz) console.log(baz);" Copy the codeCopy the code

We saw how the changes we made to the plug-in code affected the output of our plug-in. If the output looks good, we can run ‘jest -u to update the snapshot.

AST test

In addition to snapshot testing, we can also check the AST manually. This is a simple but fragile example. For more involved cases, you might want to use babel-traversal. It allows you to specify an object with the visitor key, just as you would with the plug-in itself.

it('contains baz', () => { const {ast} = babel.transform(example, {plugins: [plugin]}); const program = ast.program; const declaration = program.body[0].declarations[0]; assert.equal(declaration.id.name, 'baz'); // or babelTraverse(program, {visitor: ... })}); 'Copy codeCopy the code

Exec Tests

Here, we will transform the code and then evaluate whether it behaves correctly. Note that we did not use assert in our tests. This ensures that if our plug-in does something strange, such as accidentally removing assertion lines, the test still fails.

it('foo is an alias to baz', () => { var input = ` var foo = 1; // test that foo was renamed to baz var res = baz; `; var {code} = babel.transform(input, {plugins: [plugin]}); var f = new Function(` ${code}; return res; `); var res = f(); assert(res === 1, 'res is 1'); }); Copy the codeCopy the code

The Babel core uses a similar method to take snapshots and perform tests.

babel-plugin-tester

This package makes testing plug-ins easier. If you are familiar with ESLint’s RuleTester you should be familiar with this. You can take a look at The Docs to fully understand what’s possible, but here’s a simple example:

import pluginTester from 'babel-plugin-tester'; import identifierReversePlugin from '.. /identifier-reverse-plugin'; pluginTester({ plugin: identifierReversePlugin, fixtures: path.join(__dirname, '__fixtures__'), tests: { 'does not change code with no identifiers': '"hello"; ', 'changes this code': { code: 'var hello = "hi"; ', output: 'var olleh = "hi"; ', }, 'using fixtures files': { fixture: 'changed.js', outputFixture: 'changed-output.js', }, 'using jest snapshots': { code: ` function sayHi(person) { return 'Hello ' + person + '! ' } `, snapshot: true, }, }, }); Copy the codeCopy the code

*** For future updates, follow @thejamesKyle and @babeljs on Twitter.

Reproduced in: https://juejin.cn/post/6844904055945314312