Skip to content

Instantly share code, notes, and snippets.

@wafuwafu13
Last active January 29, 2024 08:33
Show Gist options
  • Save wafuwafu13/d424f2eabb870ec4f338f10006eadff0 to your computer and use it in GitHub Desktop.
Save wafuwafu13/d424f2eabb870ec4f338f10006eadff0 to your computer and use it in GitHub Desktop.

Babel Plugin Handbook

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

cc-by-4.0

このハンドブックは他の言語でも閲覧可能です。READMEをご覧ください。

目次

イントロダクション

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

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

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

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


基本事項

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

抽象構文木(ASTs)

コードの変換の各ステップではAbstract Syntax Tree、すなわちAST(抽象構文木)を利用します。

Babelは、ESTreeをベースにしたASTを使用しています。 コアスペックはこちらです。

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

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

上記のサンプルコードは、次のようなツリーで表現できます。

- 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が1つのノードに出現します。

Babelのステージ

Babelには大きく分けて3つのステージが存在します。すなわち、パース(Parse)変換(Transform)、そして**生成(generate)**です。

パース(Parse)

**パース(Parse)**は、コードを入力として受け取り、ASTを出力するステージです。 さらに、Parseは2つのフェーズに分けることができます。すなわち、 字句解析(Lexical Analysis)構文解析(Syntactic Analysis)です。.

字句解析(Lexical Analysis)

字句解析(Lexical Analysis)は、コードの文字列を**トークン(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: { ... } },
  ...
]

上記はトークンのストリームですが、それぞれのトークンは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)

一方、構文解析(Syntactic Analysis)は、トークンのストリームをASTに変換するフェーズを指します。 このフェーズでは、トークンの情報を利用して、コードの構造を表すASTとして再構成し、作業をしやすくします。

変換(Transform)

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

ジェネレーター

コード生成(Code Generate)のステージは、ASTをふたたびコードの文字列に変換するステージです。さらに、このステージはソースマップ(Source Map)も生成します。

コード生成の処理は単純明快です。それは、ASTを深さ順に走査して、変換後のコードを表す文字列を構築します。

走査(Traversal)

ASTを変換するには、ツリーを再帰的に走査(Traversal)する必要があります。

たとえば、typeFunctionDeclarationの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から始めて、その内部のプロパティがわかるので、それぞれとその子(Children)を順番に見ていきます。

次に、Identifierであるidに進みます。Identifierは子ノード(Child Node)のプロパティを持っていないので、次に進みます。

続いて、ノードの配列であるparamsがあるので、それぞれのノードを訪問します。この場合は、またしてもIdentifierという単一のノードなので、次に進みます。

続いて、BlockStatementであるbodyに、ノードの配列であるpropertyがあるので、それぞれにアクセスします。

ここには、引数(Argument)を持つReturnStatementノードしかないので、argumentに訪問してBinaryExpressionを見つけます。

BinaryExpressionoperatorleft、そしてrightの3つのプロパティを持ちます。 operatorはノードではなく単なる値なので、そこにはアクセスせず、leftrightにアクセスします。

この走査(Traversal)プロセスは、Babelの変換(Transform)ステージを通して行われます。

ビジター(Visitors)

私たちがノードに「行く」というとき、実際には 訪問(Visiting) していることを意味します。この言葉を使うのは、ビジター(Visitor)という概念があるからです。

ビジターは、言語を問わずAST走査で使われるパターンです。簡単に言えば,木(Tree)の中の特定のノードタイプ(Node Types)を受け入れるためのメソッドが定義されたオブジェクトです。少し抽象的なので、例を見てみましょう。

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

// ビジターを作成して、後からメソッドを追加することも可能です。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

注) Identifier() { ... }Identifier: { enter() { ... } }の簡略。

これは基本的なビジターで、走査中に使用されると、ツリー内のすべてのIdentifierに対してIdentifier()メソッドを呼び出します。

つまりこのコードでは、各Identifiersquareを含む)に対して、Identifier()メソッドが4回呼ばれます。

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

これらの呼び出しはすべてノードの enter で行われます。しかし、 exit の時にvisitorメソッドを呼び出す可能性もあります。

このようなツリー構造があるとします。

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

木の各枝(each Branch of the Tree)をたどっていくと、最終的には行き止まりになり、次のノードに行くためには木をさかのぼらなければなりません。木を下っていくと各ノードに enter し、上に戻ると各ノードから exit します。

上の木の場合、このプロセスがどのように見えるか 歩いて みましょう。

  • 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

そのため、ビジターを作成する際には、ノードを訪問する機会が2回あります。

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

必要に応じて、メソッド名を | で区切って、Identifier|MemberExpression のような文字列として、複数のビジターノードに同じ関数を適用することもできます。

flow-commentsプラグインで以下のように使われています。

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

また、エイリアス(Aliases)をビジターノードとして使用することもできます(babel-typesで定義されています).

例えば,

FunctionFunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod, ClassMethodのエイリアスです.

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

パス(Paths)

ASTは一般的に多くのノードを持ちますが、ノードはどうやってお互いに関係するのでしょうか?巨大な可変型オブジェクト(Giant Mutable Object)を用意して、それを操作したり、完全にアクセスできるようにすることもできますが、 パス(Path) を使ってこれを単純化することもできます。

パス(Path) とは、2つのノード間のリンクをオブジェクトで表現したものです。

例えば、次のようなノードとその子を考えてみましょう。

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

そして、子のIdentifierをパスで表すと、以下のようになります。

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

また、パスに関する追加のメタデータも持っています。

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

また、ノードの追加、更新、移動、削除に関連する膨大な数のメソッドがありますが、それらについては後ほど説明します。

ある意味で、パスはツリー内のノードの位置とノードに関するあらゆる情報を リアクティブ(Reactive) に表現しています。ツリーを変更するメソッドを呼び出すたびに、この情報は更新されます。Babelは、ノードの操作を簡単にし、可能な限りステートレスにするために、これらすべてを管理します。

ビジターにおけるパス(Paths in Visitors)

ビジターが Identifier()メソッドを持っている場合、実際にはノードではなくパスを訪れていることになります。この方法では、ほとんどの場合ノードそのものではなく、ノードのリアクティブな表現を扱うことになります。

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

状態(State)

状態(State)はAST変換(AST Transformation)の敵です。状態は何度も何度も手を煩わしてきますし、状態に関する仮定はほとんどの場合、考慮していなかった何らかの構文によって間違っていることが証明されます。

次のコードを見てみましょう。

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

それでは、nxにリネームする簡単なハッキーなビジター(Hacky Visitor)を書いてみましょう。

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

これは上記のコードではうまくいくかもしれませんが、次のようにすれば簡単に壊すことができます。

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

これに対処するためのより良い方法は再帰(Recursion)です。では、クリストファー・ノーランの映画のように、ビジターの中にビジターを入れてみましょう。

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

もちろん、これは作為的な例ですが、ビジターからグローバルステートを排除する方法を示しています。

スコープ(Scopes)

次に、スコープ(Scope)という概念を紹介します。 JavaScriptには、字句スコープ(Lexical Scoping) JavaScript has lexical scopingという、ブロックが新しいスコープを作るツリー構造があります。

// グローバルスコープ

function scopeOne() {
  // スコープ 1

  function scopeTwo() {
    // スコープ 2
  }
}

JavaScriptでは、変数、関数、クラス、param、import、labelなどで参照を作成すると、それは現在のスコープに属します。

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

より深いスコープ内のコードは、より高いスコープからの参照を使用することができます。

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

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

下位のスコープでは、同じ名前の参照を変更せずに作成することもあります。

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

変換(Transform)を書くときには、スコープに注意したいものです。既存のコードの様々な部分を修正する際に、既存のコードを壊してしまわないようにする必要があります。

新しい参照を追加して、それが既存のものと衝突しないようにしたいこともあるでしょう。また、ある変数がどこで参照されているかを調べたいこともあるでしょう。そのためには、特定のスコープ内の参照を追跡できるようにする必要があります。

スコープは次のように表すことができます。

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

新しいスコープを作成するときは、パスと親スコープ(Parent Scope)を与えて行います。そして走査(Traversal)処理の間に、そのスコープ内のすべての参照(「バインディング(Bindings)」)を集めます。

これが完了すると、スコープで使用できるあらゆる種類のメソッドがあります。それらについては後ほどご紹介します。

バインディング(Bindings)

参照(References)はすべて特定のスコープに属しており、この関係は バインディング(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
  }
}

シングルバインディング(Single Binding)の場合は以下のようになります。

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

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

  constant: false,
  constantViolations: [path]
}

この情報を使って、あなたはバインディングへのすべての参照を見つけ、それがどのようなタイプのバインディングであるか(パラメータ(Parameter)、宣言(Declaration)など)を確認し、それがどのスコープに属しているかを調べ、またはその識別子(Identifier)のコピーを得ることができます。定数(Constant)であるかどうかもわかりますし、定数でない場合には、どのようなパスが原因で定数でないのかもわかります。

バインディングが定数であるかどうかを知ることができるのは、多くの目的に役立ちますが、その中でも最大の目的はミニマイズ(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は、実際にはモジュールのコレクション(a Collection of Modules)です。このセクションでは、主要なモジュールについて、それらが何をするのか、どのように使用するのかを説明します。

注) これは、間もなく他の場所で利用可能になる詳細なAPIドキュメントの代わりではありません。

BabylonはBabelのパーサーです。Acornのフォークとして始まり、高速で使いやすく、非標準的な機能(将来の標準的な機能も含む)のためのプラグインベースのアーキテクチャ(Plugin-Based Architecture)を備えています。

まずは、インストールしてみましょう。

$ npm install --save @babel/parser

まずは、単純にコードの文字列をパース(Parsing)してみましょう。

import parser from "@babel/parser";

const code = `function square(n) {
  return n * n;
}`;

parser.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }

また、以下のようにparse()にオプションを渡すこともできます。

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

sourceType"module""script" のどちらかで、Babylonがパースする際のモードを表します。"module"はstrictモードでパースし、モジュールの宣言(Module Declarations)を許可する一方で、"script"は許可しません。

注) sourceTypeのデフォルトは"script"で、importexportを見つけるとエラーになります。sourceType: "module"を渡すことで、これらのエラーを取り除くことができます。

Babylonはプラグインベースのアーキテクチャで構築されているので、内部プラグインを有効にするpluginsオプションもあります。BabylonはまだこのAPIを外部プラグインに開放していませんが、将来的には開放する可能性があることに注意してください。

プラグインの全リストは、Babylon READMEで見られます。

babel-traverseモジュールは、ツリー全体の状態を維持し、ノードの交換、削除、追加を行います。

次を実行してインストールしてください。

$ npm install --save @babel/traverse

Babylonと一緒に使って、ノードを走査(Traverse)して更新することができます。

import parser from "@babel/parser";
import traverse from "@babel/traverse";

const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});

babel-typesは、Lodash風のASTノード用ユーティリティーライブラリ(Utility Library for AST Nodes)です。ASTノードを構築、検証、変換するためのメソッドが含まれています。よく考えられたユーティリティーメソッド(Utility Methods)を使って、ASTのロジックをきれいにするのに便利です。

次のコマンドでインストールできます。

$ npm install --save @babel/types

では、使ってみましょう。

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には、どのプロパティがどこに属しているか、どの値が有効か、そのノードをどのように構築するか、そのノードをどのように走査(Traverse)するか、ノードのエイリアスはなにか、などの情報を含む、ノードのすべてのタイプ(Type)の定義があります。

1つのノードタイプの定義(Single Node Type Definition)は以下のようになります。

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)

上記のBinaryExpressionの定義には、builderフィールドがあることに気づくでしょう。

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

これは、各ノードタイプにビルダーメソッドが用意されているからで、これを使うと次のようになります。

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

これは次のようなASTを生成します。

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

これを出力すると次のようになります。

a * b

また、ビルダーは作成中のノードを検証し、不適切な使い方をした場合は記述エラーを出します。これが次のタイプのメソッド(Type of Method)につながります。

バリデーター(Validators)

BinaryExpressionの定義には、ノードのfieldsとその検証方法についての情報も含まれています。

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

これを使って、2種類のバリデーションメソッド(Validating Methods)を作成します。1つ目は、isXです。

t.isBinaryExpression(maybeBinaryExpressionNode);

これは、ノードがバイナリ式(Binary Expression)であることを確認するためのテストですが、2番目のパラメータを渡して、ノードに特定のプロパティや値が含まれていることを確認することもできます。

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

また、これらのメソッドには、truefalseを返すのではなく、エラーを発生させる、より賢いものもあります。

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

コンバーター(Converters)

[WIP]

babel-generatorは、Babelのコードジェネレータ(Code Generator)です。ASTをソースマップ付きのコード(Code with Sourcemaps)に変換します。

以下を実行してインストールしてください。

$ npm install --save @babel/generator

では、使ってみましょう。

import parser from "@babel/parser";
import generate from "@babel/generator";

const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

generate(ast, {}, code);
// {
//   code: "...",
//   map: "..."
// }

generate()にオプションを渡すこともできます。

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

babel-templateは、小さいですが、非常に便利なモジュールです。これにより、膨大なASTを手動で構築する代わりに、プレースホルダー(Placeholders)を使用してコードの文字列を書くことができます。コンピュータサイエンスの世界では、この機能を「quasiquote」と呼びます。

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

はじめてのBabelプラグイン作成

Babelの基本をすべて理解したところで、それをプラグインAPIと結びつけてみましょう。

まずは、現在のbabelオブジェクトを受け取る function から始めましょう。

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

頻繁に使用することになるので、次のようにbabel.typesだけを取得したいと思うでしょう。

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

そして、プラグインの主要なビジター(Visitor)であるプロパティvisitorを持つオブジェクトを返します。

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

ビジターの各関数は2つの引数を受け取ります。pathstateです。

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

それでは、簡単なプラグインを作成して、その仕組みを紹介しましょう。ここにソースコードがあります。

foo === bar;

またはASTの形で。

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

まずは、BinaryExpressionのビジターメソッド(Visitor Method)を追加することから始めましょう。

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

そこで、===演算子を使っているBinaryExpressionだけに絞ってみましょう。

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

    // ...
  }
}

では、leftプロパティを新しい識別子(Identifier)に置き換えてみましょう。

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

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

すでにこのプラグインを実行すると、次のようになります。

sebmck === bar;

では、rightプロパティだけを置き換えてみましょう。

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

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

最終結果は次のようになります。

sebmck === dork;

すごい!私たちの最初のBabelプラグインです。


変換作業(Transformation Operations)

ビジティング(Visiting)

サブノード(Sub Node)のパス(Path)の取得

ASTノードのプロパティにアクセスするには、通常、ノードにアクセスしてからプロパティにアクセスします(path.node.property)。

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

代わりにそのプロパティのpathにアクセスする必要がある場合は、パスのgetメソッドを使用して、プロパティに文字列を渡します。

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

ノード(Node)が特定のタイプ(Type)か調べる

ノードのタイプが何であるかを確認したい場合は、次が好ましい方法です。

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

また、そのノードのプロパティを浅くチェック(Shallow Check)することもできます。

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

これは機能的には次と同等です。

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

パス(Path)が特定のタイプ(Type)か調べる

パスには、ノードのタイプを確認する方法があります。

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

これは次と同等です。

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

識別子(Identifier)が参照されているか調べる

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

他の方法として次があります。

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

特定の親パス(Parent Path)を探す

ある条件が満たされるまで、あるパスからツリーを上方向に走査(Traverse)する必要がある場合があります。

すべての親のNodePathを指定して、指定されたcallbackを呼び出します。callbackが真の値(Truthy Value)を返したら、そのNodePathを返します。

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

現在のパスも含めたい場合は次のようにします。

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

最も近い親の関数やプログラム(Parent Function or Program)を探す場合は次のようにします。

path.getFunctionParent();

リストの親ノードのパスが見つかるまでASTを上方向に走査するには次のようにします。

path.getStatementParent();

兄弟パス(Sibling Paths)を取得する

パスが Function/Programのボディ(Body)といったリストの中にある場合、そのパスには「兄弟(Siblings)」が存在します。

  • パスがリストの一部であるかどうかを path.inListでチェックします。
  • path.getSibling(index)で周囲の兄弟(Surrounding Siblings)を取得することができます。
  • コンテナ(Container)内の現在のパスのインデックスをpath.keyで取得します。
  • パスのコンテナ(すべての兄弟ノードの配列)をpath.containerで取得します。
  • path.listKeyで、リストコンテナ(List Container)のキーの名前を取得します。

これらのAPIはbabel-minifyで使用されているtransform-merge-sibling-variablesプラグインで使用されています。

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

走査(Traversal)を停止する

ある状況下でプラグインを動作させない必要がある場合、最もシンプルなのはアーリーリターン(Early Return)を書くことです。

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

トップレベルのパスでサブトラバーサル(Sub Traversal)を行う場合、2つの提供されたAPIメソッドを使用できます。

path.skip()は、現在のパスの子の走査をスキップします。path.stop()は、走査を完全に停止します。

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

操作方法

ノード(Node)を置き換える

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

1つのノード(Node)を複数のノード(Node)で置き換える

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

注) 複数のノードで式(Expression)を置き換える場合、それらはステートメント(Statement)でなければなりません。これは、Babelがノードを置き換える際にヒューリスティック(Heuristics)を広範囲に使用するためで、そうでなければ非常に冗長になってしまうような、かなりクレイジーな変換をすることになります。

ノード(Node)をソースの文字列で置き換える

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

注) 動的なソースの文字列(Dynamic Source Strings)を扱う場合を除き、このAPIの使用は推奨されません。そうでない場合は、ビジター(Visitor)の外部でコードを解析する方が効率的です。

兄弟ノード(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.";

注) これは常にステートメント(Statement)またはステートメント(Statement)の配列でなければなりません。これは[Replacing a node with multiple nodes](#replacing a node-with-multiple-nodes)で述べられているのと同じヒューリスティック(Heuristics)を使用しています。

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

ノード(Node)を削除する

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

親(Parent)を置き換える

親パスを指定してreplaceWithを呼び出すだけです: 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.";
  }

親(Parent)を削除する

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

スコープ(Scope)

ローカル変数がバインド(Bind)されているかどうかの確認

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

これにより、スコープツリー(Scope Tree)をさかのぼり、その特定のバインディング(Binding)をチェックします。

また、あるスコープが 独自の バインディングを持っているかどうかをチェックすることもできます。

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

UIDの生成

これにより、ローカルに定義された変数(Locally Defined Variables)と衝突しない識別子(Identifier)が生成されます。

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

変数宣言(Variable Declaration)の親スコープ(Parent Scope)へのプッシュ

時には、VariableDeclarationをプッシュして、代入できるようにしたいこともあります。

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

バインディング(Binding)とその参照(References)の名称変更

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

また、バインディングの名前を、生成された一意の識別子に変更することもできます。

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

プラグインのオプション

ユーザーにBabelプラグインの動作をカスタマイズさせたい場合には、ユーザーが以下のように指定できるプラグイン固有のオプションを受け入れることができます。

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

これらのオプションは、stateオブジェクトを通じて、プラグインのビジター(Visitors)に渡されます。

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

これらのオプションはプラグイン固有のもので、他のプラグインのオプションにはアクセスできません。

プラグインのPreとPost

プラグインは、プラグインの前または後に実行される関数を持つことができます。これらは、セットアップやクリーンアップ、分析のために使用することができます。

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

プラグインのシンタックス(Syntax)を有効にする

プラグインはbabylonプラグインを有効にすることで、ユーザーがインストール/有効化する必要がありません。これにより、シンタックスプラグイン(Syntax Plugin)を継承しなくても、パースエラー(Parsing Error)を防ぐことができます。

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

シンタックスエラー(Syntax Error)を投げる

babel-code-frameとメッセージでエラーを投げたい場合は次のようにします。

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

エラーは次のように表示されます。

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

ノード(Node)の構築

変換(Transformations)のコードを書いていると、ASTに挿入するノードを構築したくなることがよくあります。前述のように、babel-types`パッケージのbuilderメソッドを使ってこれを行うことができます。

ビルダー(Builder)のメソッド名は、構築したいノードタイプの名前を、最初の文字を除いて小文字にしたものになります。例えば、MemberExpressionを構築したい場合は、t.memberExpression(...)となります。

これらのビルダーの引数は、ノード定義(Node Definition)によって決定されます。ノード定義についての読みやすいドキュメントを作成する作業が行われていますが、現時点ではすべてここで見ることができます。

ノード定義は次のようなものです。

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

ここには、この特定のノードタイプに関するすべての情報(構築方法、走査(Traverse)方法、検証(Validate)方法など)が表示されます。

builderプロパティを見ると、ビルダーメソッド(t.memberExpression)を呼び出すのに必要な3つの引数を見ることができます。

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

なお、ノード上でカスタマイズできるプロパティは、builderの配列に含まれる数よりも多い場合があります。これはビルダーの引数が多くなりすぎないようにするためです。このような場合には、手動でプロパティを設定する必要があります。この例としては、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;

ビルダーの引数に対するバリデーションは、fieldsオブジェクトで確認できます。

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

objectExpressionである必要があり、propertyMemberExpressioncomputed であるかどうかに応じて ExpressionまたはIdentifierである必要があり、computedは単なるブール値で、デフォルトではfalseであることがわかります。

つまり、以下のようにしてMemberExpressionを構築することができます。

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

これは次のようになります。

object.property

しかし、objectExpressionである必要があると言いましたが、なぜIdentifierは有効なのでしょうか?

さて、Identifierの定義を見てみると、aliasesというプロパティがあり、これはExpressionでもあることを示しています。

aliases: ["Expression", "LVal"],

つまり、MemberExpressionExpressionの一種なので、別のMemberExpressionobjectとして設定することができます。

t.memberExpression(
  t.memberExpression(
    t.identifier('member'),
    t.identifier('expression')
  ),
  t.identifier('property')
)

これは次のようになります。

member.expression.property

すべてのノードタイプのビルダーメソッドシグネチャ(Builder Method Signatures)を記憶することはまずないでしょう。そのため、時間をかけて、ノード定義からどのように生成されるかを理解する必要があります。

実際の全ての定義はこちら、ドキュメントはこちらにあります。


ベストプラクティス

ヘルパービルダー(Helper Builders)とチェッカー(Checkers)の作成

特定のチェック(ノードが特定のタイプであるかどうか)を独自のヘルパー関数(Helper Functions)に抽出したり、特定のノードタイプ用のヘルパーを抽出したりするのはとても簡単です。

function isAssignment(node) {
  return node && node.operator === opts.operator + "=";
}

function buildAssignment(left, right) {
  return t.assignmentExpression("=", left, right);
}

極力、ASTの走査(Traversing)を避ける

ASTの走査(Traverse)にはコストがかかりますし、誤って必要以上にASTを走査してしまうこともあります。これは、何万回とは言わないまでも、何千回もの余分な操作になる可能性があります。

Babelはこれを可能な限り最適化し、1回の走査ですべてを行うために、可能であればビジター(Visitors)を結合します。

可能な限りビジター(Visitors)を統合する

ビジター(Visitors)を書いていると、論理的に必要な複数の場所で path.traverseを呼び出したくなることがあります。

path.traverse({
  Identifier(path) {
    // ...
  }
});

path.traverse({
  BinaryExpression(path) {
    // ...
  }
});

しかし、これらは一度だけ実行される単一のビジターとして記述する方がはるかに良いです。そうしないと、同じASTを意味もなく何度も横断することになります。

path.traverse({
  Identifier(path) {
    // ...
  },
  BinaryExpression(path) {
    // ...
  }
});

手動で済む場合は走査(Traverse)しない

特定のノードタイプを探すときに、path.traverseを呼びたくなることもあるでしょう。

const nestedVisitor = {
  Identifier(path) {
    // ...
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    path.get('params').traverse(nestedVisitor);
  }
};

しかし、対象が具体的で浅ければ、コストのかかる走査を行わなくても、必要なノードを手動で探せる可能性があります。

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

入れ子になったビジター(Nesting Visitors)の最適化

ビジターを入れ子(Nesting Visitors)にしているときは、コードの中に入れ子にして書くとよいかもしれません。

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

しかしこれは、FunctionDeclaration()が呼ばれるたびに、新しいビジターオブジェクト(Visitor Object)を作成します。なぜなら、Babelは新しいビジターオブジェクトが渡されるたびに、いくつかの処理を行うからです(複数のタイプを含むキーのエクスプロード(Explod)、検証の実行、オブジェクト構造(Object Structure)の調整など)。Babelは、その処理を既に行ったことを示すフラグをビジターオブジェクトに保存するので、ビジターを変数に保存して、毎回同じオブジェクトを渡す方が良いでしょう。

const nestedVisitor = {
  Identifier(path) {
    // ...
  }
};

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

入れ子になったビジターの中で何らかの状態が必要な場合は、次のようにします。

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

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

これを状態(State)として traverse()メソッドに渡すことで、ビジターのthisからアクセスできるようになります。

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

入れ子構造(Nested Structures)を意識する

与えられた変換(Transform)について考えるとき、与えられた構造がネストできることを忘れてしまうことがあります。

例えば、FooClassDeclarationからconstructorClassMethodを検索したいとします。

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

ここではクラスが入れ子になっているという事実を無視しており、上記の走査を使用すると、入れ子になったconstructorも発見してしまいます。

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

ユニットテスト(Unit Testing)

Babelプラグインのテストには、いくつかの主要な方法があります。スナップショットテスト(Snapshot Tests)、ASTテスト、エクゼクティブテスト(Exec Tests)です。この例では、スナップショットテストをサポートしているjestを使用しています。ここで作成しているサンプルは、このレポジトリでホストされています。

まず、Babelプラグインが必要です。これをsrc/index.jsに入れます。

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

スナップショットテスト(Snapshot Tests)

次に、npm install -save-dev babel-core jestで依存関係をインストールして、最初のテストであるスナップショット(Snapshot Tests)を書き始めます。スナップショットテストでは、Babelプラグインの出力を視覚的に検査することができます。入力を与えてスナップショットを作成するように指示すると、それをファイルに保存します。そのスナップショットをGitにチェックインします。これにより、テストケースの出力に影響を与えたときにそれを確認することができます。また、プルリクエストの際にも差分を使うことができます。もちろん、これはどんなテストフレームワークでもできますが、Jestではスナップショットの更新は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();
});

これで、src/__tests__/__snapshots__/index-test.js.snapに(Snapshot File)ができました。

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

プラグインで'bar'を'baz'に変更して、再度Jestを実行すると、次のようになります。

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

プラグインのコードを変更したことで、プラグインの出力にどのような影響があったかを確認し、出力に問題がなければ、jest -uを実行してスナップショットを更新します。

ASTテスト(AST Tests)

スナップショットテストに加えて、ASTを手動で検査することもできます。これはシンプルで脆い例です。もっと複雑な状況では、babel-traverseを活用するとよいでしょう。babel-traverseでは、プラグインと同じように、visitorキーでオブジェクトを指定することができます。

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)

ここでは、コードを変換し、それが正しく動作するかどうかを評価します。このテストではassertを使用していないことに注意してください。これは、プラグインが誤ってassertの行を削除するなどの変なことをしても、テストが失敗することを保証するためです。

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は、スナップショットとエクゼクティブテストに類似したアプローチを使用しています。

本パッケージはプラグインのテストを容易にします。ESLintのRuleTesterに慣れている方にはお馴染みのパッケージです。どのようなことができるかについてはドキュメントを参照してください。ここでは簡単な例を紹介します。

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

今後のアップデートについては、Twitterで@thejameskyle@babeljsをフォローしてください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment