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;
}Copy 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: nCopy 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 the code
You’ll notice that each layer of the AST has the same structure:
{ type: "FunctionDeclaration", id: {... }, params: [...] , body: {... }}Copy the code
{
type: "Identifier",
name: ...
}Copy the code
{ type: "BinaryExpression", operator: ... , left: {... }, right: {... }}Copy 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;
}Copy 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 } }, ... }Copy 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 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 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
},
...
}Copy 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 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() {}; visitor.FunctionDeclaration = function() {}Copy 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;
}Copy the code
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!Copy 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)Copy 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 the
FunctionDeclaration
- Enter the
Identifier (id)
- Come to an end
- exit
Identifier (id)
- Enter the
Identifier (params[0])
- Come to an end
- exit
Identifier (params[0])
- Enter the
BlockStatement (body)
- Enter the
ReturnStatement (body)
- Enter the
BinaryExpression (argument)
- Enter the
Identifier (left)
- Come to an end
- exit
Identifier (left)
- Enter the
Identifier (right)
- Come to an end
- exit
Identifier (right)
- exit
BinaryExpression (argument)
- Enter the
- exit
ReturnStatement (body)
- exit
BlockStatement (body)
- Enter the
- exit
FunctionDeclaration
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 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 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 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"
},
...
}Copy the code
The child Identifier, expressed as a Path, looks like this:
{ "parent": { "type": "FunctionDeclaration", "id": {... },... }, "node": { "type": "Identifier", "name": "square" } }Copy 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 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 code
a + b + c;Copy 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;
}Copy 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 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 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 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.
// global scope
function scopeOne() {
// 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 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 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 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: [...]
}Copy 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 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 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 babylonCopy 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 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 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-traverseCopy 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 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 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 code
Builders
You’ll notice that the BinaryExpression definition above has a Builder field. .
builder: ["operator", "left", "right"]Copy 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 code
You can create an AST like this:
{
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "a"
},
right: {
type: "Identifier",
name: "b"
}
}Copy the code
When printed it looks like this:
a * bCopy 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")
}
}Copy the code
Two validation methods can be created. The first is isX. .
t.isBinaryExpression(maybeBinaryExpressionNode);Copy 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 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-generatorCopy 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 code
You can also pass options to the generate() method. .
generate(ast, {
retainLines: false,
compact: "auto",
concise: false,
quotes: "double",
// ...
}, code);Copy 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 code
var myModule = require("my-module");Copy 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 contents
}Copy 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 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 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 code
Let’s quickly write a working plug-in to show how it works. Here is our source code:
foo === bar;Copy the code
Its AST form is as follows:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}Copy the code
We start by adding the BinaryExpression visitor method:
export default function({ types: t }) { return { visitor: { BinaryExpression(path) { // ... }}}; }Copy 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 code
Now we replace the left attribute with a new identifier:
BinaryExpression(path) { if (path.node.operator ! == "===") { return; } path.node.left = t.identifier("sebmck"); / /... }Copy the code
So if we run the plugin we get:
sebmck === bar;Copy 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"); }Copy the code
This is our final result:
sebmck === dork;Copy 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;
}Copy 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');
}Copy 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 code
You can also perform a shallow check on node attributes:
BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n" })) { // ... }}Copy the code
Functionally equivalent to:
BinaryExpression(path) {
if (
path.node.left != null &&
path.node.left.type === "Identifier" &&
path.node.left.name === "n"
) {
// ...
}
}Copy 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 code
Equivalent to:
BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n" })) { // ... }}Copy the code
Check whether identifiers are referenced
Identifier(path) { if (path.isReferencedIdentifier()) { // ... }}Copy the code
Or:
Identifier(path) { if (t.isReferenced(path.node, path.parent)) { // ... }}Copy 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 code
If you also need to traverse the current node:
path.find((path) => path.isObjectExpression());Copy the code
Find the nearest parent function or program:
path.getFunctionParent();Copy the code
Walk up the syntax tree until you find the parent path in the list
path.getStatementParent();Copy the code
Get peer path
If a path is in a list of functions/programs, it has sibling nodes.
- use
path.inList
To determine if the path has sibling nodes, - use
path.getSibling(index)
To get the sibling path, - use
path.key
Gets the index of the container where the path resides, - use
path.container
A container for getting paths (an array of all sibling nodes) - use
path.listKey
Gets the key of the container
These apis are used for the transform-merge-Sibling-variables </> plug-in used in babel-minify </>.
Copy 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]
}
}
};
}
Copy 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; }Copy 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 code
To deal with
Replace a node
BinaryExpression(path) {
path.replaceWith(
t.binaryExpression("**", path.node.left, t.numberLiteral(2))
);
}Copy the code
function square(n) {
- return n * n;
+ return n ** 2;
}Copy 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)")),
]);
}Copy 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)"; }Copy 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 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;
}
Copy 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 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 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 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 code
Deleting a node
FunctionDeclaration(path) {
path.remove();
}Copy the code
- function square(n) { - return n * n; -}Copy 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 code
function square(n) {
- return n * n;
+ "Anyway the wind blows, doesn't really matter to me, to me.";
}Copy the code
Deleting a parent node
BinaryExpression(path) {
path.parentPath.remove();
}Copy the code
function square(n) {
- return n * n;
}Copy the code
Scope (Scope)
Check whether local variables are bound
FunctionDeclaration(path) { if (path.scope.hasBinding("n")) { // ... }}Copy 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 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 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 code
- function square(n) { + var _square = function square(n) { return n * n; -} +};Copy the code
Renames the binding and its references
FunctionDeclaration(path) {
path.scope.rename("n", "x");
}Copy the code
- function square(n) {
- return n * n;
+ function square(x) {
+ return x * x;
}Copy the code
Alternatively, you can rename the binding to the generated unique identifier:
FunctionDeclaration(path) {
path.scope.rename("n");
}Copy the code
- function square(n) {
- return n * n;
+ function square(_n) {
+ return _n * _n;
}Copy the code
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
}]
]
}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 }
}
}
}
}
`
Copy 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); }}; }Copy 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")
};
}Copy 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"); }}}; }Copy 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
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 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).
Generators: ["object", "property", "computed"],Copy 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 code
You can see the validation for the builder arguments with the
fields
object.Copy 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 code
Which will result in:
object.propertyCopy 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 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')
)Copy the code
Which will result in:
member.expression.propertyCopy 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
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);
}Copy 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 code
*** For future updates, follow @thejamesKyle </> and @babeljs </> on Twitter.