Skip to content
Permalink
Branch: master
Find file Copy path
2 contributors

Users who have contributed to this file

@thejameskylebot @jamiebuilds
2163 lines (1687 sloc) 58.3 KB

Babel Plugin Handbook

このドキュメントではBabelプラグインを作る方法を解説します。.

cc-by-4.0

This handbook is available in other languages, see the README for a complete list.

目次

イントロダクション

BabelはJavaScriptのための汎用的で多目的に使用できるコンパイラです。また、様々な静的コード解析に利用するためのモジュールのコレクションでもあります。

静的コード解析とは、実行すること無くコードの分析を行うプロセスです。 (コードの実行中にそれを分析するのは動的コード解析と呼ばれます。) 静的コード解析の目的は様々です。 Lint、コンパイル、コードハイライト、トランスフォーム、最適化、縮小など、様々な目的で利用することができます。

Babelを利用することで、より生産的で、より良いコードを書くためのツールを作ることができます。

最新の情報を受け取るには、Twitterで@thejameskyleをフォローしてください。


基本

BabelはJavaScriptのコンパイラ、特にソースからソースへ変換する「トランスパイラ(Transpiler)」と呼ばれる種類のコンパイラです。 つまり、BabelにJavaScriptのコードを与えることで、Babelはコードを変更し新しいコードを生成します。

抽象構文木(ASTs)

コードの変換の各ステップでは抽象構文木(ASTs)またはASTを利用します。

Babel uses an AST modified from ESTree, with the core spec located here.

function square(n) {
  return n * n;
}

ASTノードについて理解を深めたい場合はAST Explorerを使ってみてください。 上記のサンプルコードの例はここで確認することができます。

上記のサンプルコードをASTノードとして表すと以下の様になります。

- 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

またはJavaScriptのオブジェクトして表現すると、以下の様になります。

{
  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"
        }
      }
    }]
  }
}

ASTの各階層は同じような構造をしていることに気付くでしょう。

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
{
  type: "Identifier",
  name: ...
}
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

注) いくつかのプロパティは、単純化のため省略しています。

これらは**ノード(Node)**と呼ばれます。 ASTは単一のノード、または何百、何千のノードから構成することができます。 これらを利用し、静的コード解析に利用するプログラムの文法を説明することができるのです。

全てのノードはインターフェイスを持ちます。

interface Node {
  type: string;
}

typeフィールドは、オブジェクトのノードの型を表す文字列です(例えば、 "FunctionDeclaration""Identifier""BinaryExpression"などがあります。) ノードの種類は特定のノードの型を記述するためのプロパティのセットを追加して定義します。

Babelが生成したノードには、元のソースコード上のノードの位置を記述した追加のプロパティがセットされます。

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}

これらのプロパティにはstartendlocが一つのノードに出現します。

Babelのステージ

Babelには大きく分けて3つのステージが存在します。すなわち、parsetransform、そしてgenerateです。.

パーサー

parseは、コードを入力として受け取り、ASTを出力するステージです。 さらに、parseは2つのフェーズに分けることができます。すなわち、 Lexical AnalysisSyntactic Analysisです。.

字句解析

Lexical Analysisは、コードの文字列をtokenのストリームへ変換するフェーズを指します。.

tokenは言語の構文の個々の部品であり、tokenのストリームはそれらがフラットに並んだものと考えてください。

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

上記はtokenのストリームですが、それぞれのtokenはtypeを持ち、それは以下の様なプロパティから構成されています。

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

ASTのノードと同様、typeもまたstartendlocといったプロパティを持ちます。.

構文解析

一方、Syntactic Analysisは、tokenのストリームをASTに変換するフェーズを指します。 ここでは、tokenの情報をベースにそれらを再構成して、コードの構造をより加工しやすい形(AST)で表現します。

変換

transform ステージでは、ASTのツリーを走査して、ノードの追加、変更、削除といった処理を施します。 このステージこそが最も複雑なステージであり、それはBabelのみならず、他のコンパイラにおいても同様です。 また、このステージこそがプラグインに関わる部分であるため、言わばこのハンドブックの大半はtransformに関して書かれています。 したがって、ここでは簡単に説明するだけに留めたいと思います。

ジェネレーター

generate(code generation)ステージは、ASTをふたたびコードの文字列に変換するステージです。さらに、このステージはsource mapも生成します。.

code generationの処理は単純明快です。それは、ASTのツリーをdepth-firstの順番で走査することで、変換結果としてのコードの文字列を構築します。

Traversal

transformのステージでは、ASTのツリーを再帰的に走査(traverse)する必要があります。

たとえば、typeがFunctionDeclarationのASTがあるとしましょう。このASTは idparams、そしてbodyという3つのネストしたノードを含みます。

{
  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"
        }
      }
    }]
  }
}

ご覧のとおり、FunctionDeclaration以下のノードはさらに子ノードを持つため、それらひとつひとつを走査しなければいけません。

まずは、idを見てみましょう。これはIdentifierノードであり、自身の直接の子ノードのみを持ちます。

次に、paramsを見てみましょう。これはノードの配列で、それぞれのノードはまたしても、Identifierです。

最後にbody を見てみましょう。これはBlockStatement であり、bodyというプロパティを持ちます。<0>body</0>はノードの配列なので、ひとつづつ辿ってみましょう。

配列は単一のノード(ReturnStatement)から構成されており、argumentというプロパティを持ちます。argumentBinaryExpressionであり、さらに子ノードを持ちます。.

BinaryExpressionoperatorleft、そしてrightの3つのプロパティを持ちます。 operatorはノードではなく、ただの値です。一方、leftrightはノードです。.

この操作(traversal)のプロセスは、Babelのtransformステージ全体を通じて行われます。

Visitors

When we talk about "going" to a node, we actually mean we are visiting them. The reason we use that term is because there is this concept of a visitor.

Visitors are a pattern used in AST traversal across languages. Simply put they are an object with methods defined for accepting particular node types in a tree. That's a bit abstract so let's look at an example.

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// You can also create a visitor and add methods on it later
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

Note: Identifier() { ... } is shorthand for Identifier: { enter() { ... } }.

This is a basic visitor that when used during a traversal will call the Identifier() method for every Identifier in the tree.

So with this code the Identifier() method will be called four times with each Identifier (including square).

function square(n) {
  return n * n;
}
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!

These calls are all on node enter. However there is also the possibility of calling a visitor method when on exit.

Imagine we have this tree structure:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)

As we traverse down each branch of the tree we eventually hit dead ends where we need to traverse back up the tree to get to the next node. Going down the tree we enter each node, then going back up we exit each node.

Let's walk through what this process looks like for the above tree.

  • Enter FunctionDeclaration
    • Enter Identifier (id)
    • Hit dead end
    • Exit Identifier (id)
    • Enter Identifier (params[0])
    • Hit dead end
    • Exit Identifier (params[0])
    • Enter BlockStatement (body)
    • Enter ReturnStatement (body)
      • Enter BinaryExpression (argument)
      • Enter Identifier (left)
        • Hit dead end
      • Exit Identifier (left)
      • Enter Identifier (right)
        • Hit dead end
      • Exit Identifier (right)
      • Exit BinaryExpression (argument)
    • Exit ReturnStatement (body)
    • Exit BlockStatement (body)
  • Exit FunctionDeclaration

So when creating a visitor you have two opportunities to visit a node.

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

If necessary, you can also apply the same function for multiple visitor nodes by separating them with a | in the method name as a string like Identifier|MemberExpression.

Example usage in the flow-comments plugin

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};

You can also use aliases as visitor nodes (as defined in babel-types).

For example,

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

const MyVisitor = {
  Function(path) {}
};

Paths

An AST generally has many Nodes, but how do Nodes relate to one another? We could have one giant mutable object that you manipulate and have full access to, or we can simplify this with Paths.

A Path is an object representation of the link between two nodes.

For example if we take the following node and its child:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}

And represent the child Identifier as a path, it looks something like this:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}

It also has 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
}

As well as tons and tons of methods related to adding, updating, moving, and removing nodes, but we'll get into those later.

In a sense, paths are a reactive representation of a node's position in the tree and all sorts of information about the node. Whenever you call a method that modifies the tree, this information is updated. Babel manages all of this for you to make working with nodes easy and as stateless as possible.

Paths in Visitors

When you have a visitor that has a Identifier() method, you're actually visiting the path instead of the node. This way you are mostly working with the reactive representation of a node instead of the node itself.

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
a + b + c;
path.traverse(MyVisitor);
Visiting: a
Visiting: b
Visiting: c

State

State is the enemy of AST transformation. State will bite you over and over again and your assumptions about state will almost always be proven wrong by some syntax that you didn't consider.

Take the following code:

function square(n) {
  return n * n;
}

Let's write a quick hacky visitor that will rename 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";
    }
  }
};

This might work for the above code, but we can easily break that by doing this:

function square(n) {
  return n * n;
}
n;

The better way to deal with this is recursion. So let's make like a Christopher Nolan film and put a visitor inside of a visitor.

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);

Of course, this is a contrived example but it demonstrates how to eliminate global state from your visitors.

Scopes

Next let's introduce the concept of a scope. JavaScript has lexical scoping, which is a tree structure where blocks create new scope.

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {
    // scope 2
  }
}

Whenever you create a reference in JavaScript, whether that be by a variable, function, class, param, import, label, 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()`";
  }
}

Code within a deeper scope may use a reference from a higher scope.

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
  }
}

A lower scope might also create a reference of the same name without modifying it.

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.";
  }
}

When writing a transform, we want to be wary of scope. We need to make sure we don't break existing code while modifying different parts of it.

We may want to add new references and make sure they don't collide with existing ones. Or maybe we just want to find where a variable is referenced. We want to be able to track these references within a given scope.

A scope can be represented as:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

When you create a new scope you do so by giving it a path and a parent scope. Then during the traversal process it collects all the references ("bindings") within that scope.

Once that's done, there's all sorts of methods you can use on scopes. We'll get into those later though.

Bindings

References all belong to a particular scope; this relationship is known as 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
  }
}

A single binding looks like this:

{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}

With this information you can find all the references to a binding, see what type of binding it is (parameter, declaration, etc.), lookup what scope it belongs to, or get a copy of its identifier. You can even tell if it's constant and if not, see what paths are causing it to be non-constant.

Being able to tell if a binding is constant is useful for many purposes, the largest of which is minification.

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";
  }
}

API

Babel is actually a collection of modules. In this section we'll walk through the major ones, explaining what they do and how to use them.

Note: This is not a replacement for detailed API documentation which will be available elsewhere shortly.

babylon

Babylon is Babel's parser. Started as a fork of Acorn, it's fast, simple to use, has plugin-based architecture for non-standard features (as well as future standards).

First, let's install it.

$ npm install --save babylon

Let's start by simply parsing a string of code:

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: [...]
// }

We can also pass options to parse() like so:

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

sourceType can either be "module" or "script" which is the mode that Babylon should parse in. "module" will parse in strict mode and allow module declarations, "script" will not.

Note: sourceType defaults to "script" and will error when it finds import or export. Pass sourceType: "module" to get rid of these errors.

Since Babylon is built with a plugin-based architecture, there is also a plugins option which will enable the internal plugins. Note that Babylon has not yet opened this API to external plugins, although may do so in the future.

To see a full list of plugins, see the Babylon README.

babel-traverse

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

Install it by running:

$ npm install --save babel-traverse

We can use it alongside Babylon to traverse 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";
    }
  }
});

babel-types

Babel Types is a Lodash-esque utility library for AST nodes. It contains methods for building, validating, and converting AST nodes. It's useful for cleaning up AST logic with well thought out utility methods.

You can install it by running:

$ npm install --save babel-types

Then start using it:

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";
    }
  }
});

Definitions

Babel Types has definitions for every single type of node, with information on what properties belong where, what values are valid, how to build that node, how the node should be traversed, and aliases of the Node.

A single node type definition looks like this:

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"]
});

Builders

You'll notice the above definition for BinaryExpression has a field for a builder.

builder: ["operator", "left", "right"]

This is because each node type gets a builder method, which when used looks like this:

t.binaryExpression("*", t.identifier("a"), t.identifier("b"));

Which creates an AST like this:

{
  type: "BinaryExpression",
  operator: "*",
  left: {
    type: "Identifier",
    name: "a"
  },
  right: {
    type: "Identifier",
    name: "b"
  }
}

Which when printed looks like this:

a * b

Builders will also validate the nodes they are creating and throw descriptive errors if used improperly. Which leads into the next type of method.

Validators

The definition for BinaryExpression also includes information on the fields of a node and how to validate them.

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

This is used to create two types of validating methods. The first of which is isX.

t.isBinaryExpression(maybeBinaryExpressionNode);

This tests to make sure that the node is a binary expression, but you can also pass a second parameter to ensure that the node contains certain properties and values.

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });

There is also the more, ehem, assertive version of these methods, which will throw errors instead of returning true or false.

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

Converters

[WIP]

babel-generator

Babel Generator is the code generator for Babel. It takes an AST and turns it into code with sourcemaps.

Run the following to install it:

$ npm install --save babel-generator

Then use it

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: "..."
// }

You can also pass options to generate().

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

babel-template

Babel Template is another tiny but incredibly useful module. It allows you to write strings of code with placeholders that you can use instead of manually building up a massive AST. In computer science, this capability is called quasiquotes.

$ npm install --save babel-template
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);
var myModule = require("my-module");

Writing your first Babel Plugin

Now that you're familiar with all the basics of Babel, let's tie it together with the plugin API.

Start off with a function that gets passed the current babel object.

export default function(babel) {
  // plugin contents
}

Since you'll be using it so often, you'll likely want to grab just babel.types like so:

export default function({ types: t }) {
  // plugin contents
}

Then you return an object with a property visitor which is the primary visitor for the plugin.

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

Each function in the visitor receives 2 arguments: path and state

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

Let's write a quick plugin to show off how it works. Here's our source code:

foo === bar;

Or in AST form:

{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}

We'll start off by adding a BinaryExpression visitor method.

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

Then let's narrow it down to just BinaryExpressions that are using the === operator.

visitor: {
  BinaryExpression(path) {
    if (path.node.operator !== "===") {
      return;
    }

    // ...
  }
}

Now let's replace the left property with a new identifier:

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  // ...
}

Already if we run this plugin we would get:

sebmck === bar;

Now let's just replace the right property.

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  path.node.right = t.identifier("dork");
}

And now for our final result:

sebmck === dork;

Awesome! Our very first Babel plugin.


Transformation Operations

Visiting

Get the Path of Sub-Node

To access an AST node's property you normally access the node and then the property. path.node.property

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

If you need to access the path of that property instead, use the get method of a path, passing in the string to the property.

BinaryExpression(path) {
  path.get('left');
}
Program(path) {
  path.get('body.0');
}

Check if a node is a certain type

If you want to check what the type of a node is, the preferred way to do so is:

BinaryExpression(path) {
  if (t.isIdentifier(path.node.left)) {
    // ...
  }
}

You can also do a shallow check for properties on that node:

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

This is functionally equivalent to:

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

Check if a path is a certain type

A path has the same methods for checking the type of a node:

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

is equivalent to doing:

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

Check if an identifier is referenced

Identifier(path) {
  if (path.isReferencedIdentifier()) {
    // ...
  }
}

Alternatively:

Identifier(path) {
  if (t.isReferenced(path.node, path.parent)) {
    // ...
  }
}

Find a specific parent path

Sometimes you will need to traverse the tree upwards from a path until a condition is satisfied.

Call the provided callback with the NodePaths of all the parents. When the callback returns a truthy value, we return that NodePath.

path.findParent((path) => path.isObjectExpression());

If the current path should be included as well:

path.find((path) => path.isObjectExpression());

Find the closest parent function or program:

path.getFunctionParent();

Walk up the tree until we hit a parent node path in a list

path.getStatementParent();

Get Sibling Paths

If a path is in a list like in the body of a Function/Program, it will have "siblings".

  • Check if a path is part of a list with path.inList
  • You can get the surrounding siblings with path.getSibling(index),
  • The current path's index in the container with path.key,
  • The path's container (an array of all sibling nodes) with path.container
  • Get the name of the key of the list container with path.listKey

These APIs are used in the transform-merge-sibling-variables plugin used in babel-minify.

var a = 1; // pathA, path.key = 0
var b = 2; // pathB, path.key = 1
var c = 3; // pathC, path.key = 2
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]
      }
    }
  };
}

Stopping Traversal

If your plugin needs to not run in a certain situation, the simpliest thing to do is to write an early return.

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

If you are doing a sub-traversal in a top level path, you can use 2 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
  }
});

Manipulation

Replacing a node

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

Replacing a 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)")),
  ]);
}
  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)";
  }

Note: When replacing an expression with multiple nodes, they must be statements. This is because Babel uses heuristics extensively when replacing nodes which means that you can do some pretty crazy transformations that would be extremely verbose otherwise.

Replacing a node with a source string

FunctionDeclaration(path) {
  path.replaceWithSourceString(`function add(a, b) {
    return a + b;
  }`);
}
- function square(n) {
-   return n * n;
+ function add(a, b) {
+   return a + b;
  }

Note: It's not recommended to use this API unless you're dealing with dynamic source strings, otherwise it's more efficient to parse the code outside of the visitor.

Inserting a sibling node

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.")));
}
+ "Because I'm easy come, easy go.";
  function square(n) {
    return n * n;
  }
+ "A little high, little low.";

Note: This should always be a statement or an array of statements. This uses the same heuristics mentioned in Replacing a node with multiple nodes.

Inserting into a container

If you want to insert into a AST node property like that is an array like body. It is similar to insertBefore/insertAfter other than you having to specify the listKey which is usually body.

ClassMethod(path) {
  path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));
  path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}
 class A {
  constructor() {
+   "before"
    var a = 'middle';
+   "after"
  }
 }

Removing a node

FunctionDeclaration(path) {
  path.remove();
}
- function square(n) {
-   return n * n;
- }

Replacing a parent

Just call replaceWith with the parentPath: path.parentPath

BinaryExpression(path) {
  path.parentPath.replaceWith(
    t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
  );
}
  function square(n) {
-   return n * n;
+   "Anyway the wind blows, doesn't really matter to me, to me.";
  }

Removing a parent

BinaryExpression(path) {
  path.parentPath.remove();
}
  function square(n) {
-   return n * n;
  }

スコープ

Checking if a local variable is bound

FunctionDeclaration(path) {
  if (path.scope.hasBinding("n")) {
    // ...
  }
}

This will walk up the scope tree and check for that particular binding.

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

FunctionDeclaration(path) {
  if (path.scope.hasOwnBinding("n")) {
    // ...
  }
}

Generating a UID

This will generate an identifier that doesn't collide 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" }
}

Pushing a variable declaration to a parent scope

Sometimes you may want to push a VariableDeclaration so 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 });
}
- function square(n) {
+ var _square = function square(n) {
    return n * n;
- }
+ };

Rename a binding and its references

FunctionDeclaration(path) {
  path.scope.rename("n", "x");
}
- function square(n) {
-   return n * n;
+ function square(x) {
+   return x * x;
  }

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

FunctionDeclaration(path) {
  path.scope.rename("n");
}
- function square(n) {
-   return n * n;
+ function square(_n) {
+   return _n * _n;
  }

プラグインのオプション

If you would like to let your users customize the behavior of your Babel plugin you can accept plugin specific options which users can specify like this:

{
  plugins: [
    ["my-plugin", {
      "option1": true,
      "option2": false
    }]
  ]
}

These options then get passed into plugin visitors through the state object:

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

These options are plugin-specific and you cannot access options from other plugins.

Pre and Post in Plugins

Plugins can have functions that are run before or after plugins. They can be used for setup or cleanup/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);
    }
  };
}

Enabling Syntax in Plugins

Plugins can enable babylon plugins so that users don't need to install/enable them. This prevents a parsing error without inheriting the syntax plugin.

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

Throwing 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");
      }
    }
  };
}

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",

Building Nodes

When writing transformations you'll often want to build up some nodes to insert into the AST. As mentioned previously, you can do this using the builder methods in the babel-types package.

The method name for a builder is simply the name of the node type you want to build except with the first letter lowercased. For example if you wanted to build a MemberExpression you would use t.memberExpression(...).

The arguments of these builders are decided by the node definition. There's some work that's being done to generate easy-to-read documentation on the definitions, but for now they can all be found here.

A node definition looks like the following:

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
    }
  }
});

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

By looking at the builder property, you can see the 3 arguments that will be needed to call the builder method (t.memberExpression).

builder: ["object", "property", "computed"],

Note that sometimes there are more properties that you can customize on the node than the builder array contains. This is to keep the builder from having too many arguments. In these cases you need to set the properties manually. An example of this 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;

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

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:

t.memberExpression(
  t.identifier('object'),
  t.identifier('property')
  // `computed` is optional
);

Which will result in:

object.property

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"],

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')
)

Which will result in:

member.expression.property

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);
}

Avoid traversing the AST as much as possible

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 visitors whenever possible

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) {
    // ...
  }
});

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) {
    // ...
  }
});

Do not traverse when manual lookup will do

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);
  }
};

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() {
      // ...
    });
  }
};

Optimizing nested visitors

When you are nesting visitors, it might make sense to write them nested in your code.

const MyVisitor = {
  FunctionDeclaration(path) {
    path.traverse({
      Identifier(path) {
        // ...
      }
    });
  }
};

However, this creates a new visitor object every 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);
  }
};

If you need some state within the nested visitor, like so:

const MyVisitor = {
  FunctionDeclaration(path) {
    var exampleState = path.node.params[0].name;

    path.traverse({
      Identifier(path) {
        if (path.node.name === exampleState) {
          // ...
        }
      }
    });
  }
};

You can pass it in as state to the traverse() method and have access to it on 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 });
  }
};

Being aware of nested structures

Sometimes when thinking about a given transform, you might forget that the given structure can be nested.

For example, imagine we want to lookup the constructor ClassMethod from the Foo ClassDeclaration.

class Foo {
  constructor() {
    // ...
  }
}
const constructorVisitor = {
  ClassMethod(path) {
    if (path.node.name === 'constructor') {
      // ...
    }
  }
}

const MyVisitor = {
  ClassDeclaration(path) {
    if (path.node.id.name === 'Foo') {
      path.traverse(constructorVisitor);
    }
  }
}

We are ignoring the fact that classes can be nested and using the traversal above we will hit a nested constructor as well:

class Foo {
  constructor() {
    class Bar {
      constructor() {
        // ...
      }
    }
  }
}

Unit Testing

There are a few primary ways to test babel plugins: snapshot tests, AST tests, and exec tests. We'll use jest for this example because it supports snapshot testing out of the box. The example we're creating here is hosted in this repo.

First we need a babel plugin, we'll put this in src/index.js.

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

Snapshot Tests

Next, install our dependencies with npm install --save-dev babel-core jest, and then we can begin writing our first test: the snapshot. Snapshot tests allow us to visually inspect the output of our babel plugin. We give it an input, tell it to make a snapshot, and it saves it to a file. We check in the snapshots into git. This allows us to see when we've affected the output of any of our test cases. It also gives use a diff in pull requests. Of course you could do this with any test framework, but with jest updating the snapshots is as easy 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();
});

This gives us a snapshot file in src/__tests__/__snapshots__/index-test.js.snap.

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

If we change 'bar' to 'baz' in our plugin and run jest again, we get this:

Received value does not match 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);"

We see how our change to the plugin code affected the output of our plugin, and if the output looks good to us, we can run jest -u to update the snapshot.

AST Tests

In addition to snapshot testing, we can manually inspect the AST. This is a simple but brittle example. For more involved situations you may wish to leverage babel-traverse. It allows you to specify an object with a visitor key, exactly like you use for the plugin 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: ...})
});

Exec Tests

Here we'll be transforming the code, and then evaluating that it behaves correctly. Note that we're not using assert in the test. This ensures that if our plugin does weird stuff like removing the assert line by accident, the test will still fail.

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');
});

Babel core uses a similar approach to snapshot and exec tests.

babel-plugin-tester

This package makes testing plugins easier. If you're familiar with ESLint's RuleTester this should be familiar. You can look at the docs to get a full sense of 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,
    },
  },
});

For future updates, follow @thejameskyle and @babeljs on Twitter.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.