**Babel 5 から 6 の間に API が大きく変わっています。Babel 6 のプラグインには Babel plugin handbook をおすすめします。**thejameskyle さんは Babel のメンテナの一人です。
ES2015 や JSX を ES5 にトランスパイルする Babel。日常的に使っている方も多いかと思います。
そのままでも便利な Babel ですが、プラグインを使うことで独自のソースコード変換ルールを追加することができます。私もちょっと前から Angular 2 アプリを Babel で作るためのプラグインを書いたりしていました。結構簡単に作れるので、Babel 5.x でプラグインを作る方法を紹介してみたいと思います。
Babel の仕組み
ざっくり言うと Babel は以下のような仕組みで動いています。
- ソースコードをパースして AST を生成(Babylon)
- AST -> AST の変換(transformers)
- AST から JS のコードを生成(generators)
1 で言う AST というのは Abstract Syntax Tree(抽象構文木)で、ソースコードの各要素をツリーとして表現したものです。Babel の AST は ESTree という仕様に基づいており、以下で参照することができます。Babel では、これらにさらに ES7 や JSX などの非標準のノードも加わります。
2 には本体内の transformer(class を ES5 で動く AST に変換したり)と(もし使っていれば)サードパーティプラグインの transformer が含まれます。Babel 6.x からは標準の transformer も全てプラグインとして提供されるようになる予定だそうです。
プラグインを作るのは AST -> AST を変換する transformer を書くだけ。自分で JS をパースしたり AST から JS を出力する必要はないので簡単です。さらに JS のパース、出力が一回で済むでの、Babel と別に JS をパースするツールを導入するよりも多分に効率的です。また Babel の API を使うことができるので、esprima とかを使ってやるよりも簡単に書けるようです(やったことないので比較できないですが)。
できること、できないこと
Babylon がパースできる文法の範囲内(ES2015, ES7+, JSX, flowtype など)であれば AST の変換でできることは何でもできます。ただし 5.x でプラグインでパーサを拡張する仕組みはないので、Babylon がパースできない文法を導入することはできません。(Babylon にモンキーパッチすればできますが。)
プロジェクトの作成
何故かドキュメントには書かれていないのですが npm install -g babel
で入る babel-plugin
コマンドでプラグインプロジェクトのひな形を出力することができます。babel-plugin-*
という名前のディレクトリを自分で作る必要があることに注意。*
の部分がプラグインの名前になります。
# プロジェクトのディレクトリを用意
mkdir babel-plugin-foo-bar
cd babel-plugin-foo-bar
# プロジェクトのひな形を生成
npm install -g babel
babel-plugin init
# 依存モジュールをインストール
npm install
以下のような構成ができあがります。
.
├── .gitignore
├── .npmigonore
├── LICENSE
├── README.md
├── node_modules
├── package.json
└── src
└── index.js
以下の npm run scripts が用意されています。
-
npm run build
:src
ディレクトリ以下を Babel でコンパイルした結果をlib
に出力してくれます。 -
npm run push
: リリース用のコマンド。git と npm を両方面倒見てくれます。 -
npm test
:babel-plugin test
と書かれていますが、そういうタスクはないのでエラーになります。建設予定地の模様。テストは mocha など自分で好きなものを用意しましょう。
src
で Babel を使ったコードを書いて、lib
にコンパイルした結果を出力。git には src
を、npm には lib
を登録するという構成です。これによりプラグインそのものも ES2015+ の文法で書くことができます。
書き方
babel-plugin init
で出力されるひな形は以下のようになっています。
/* eslint no-unused-vars:0 */
export default function ({ Plugin, types: t }) {
return new Plugin("foo", {
visitor: {
// your visitor methods go here
}
});
}
プラグインのファイルでは、Plugin
のインスタンスを返す関数を export
します。Plugin
インスタンスにはプラグインの名前と設定オブジェクトを渡します。
AST の変換処理を記述するのが visitor
です。Babel transformer は変換対象の AST を上から下まで移動していきます。visitor
ではその名前の AST ノードを訪れた時に実行する処理を定義することができます。例えば以下の例ではクラス定義と関数定義のノードに対して処理を行えます。
export default function ({ Plugin, types: t }) {
return new Plugin('plugin-name', {
visitor: {
ClassDeclaration(node, parent) {
// クラス定義に何かする。
},
FunctionDeclaration(node, parent) {
// 関数定義に何かする。
}
}
});
};
似たノードのタイプをグループ化した alias というものを使うと複数のノードにマッチする visitor を書くことができます。例えば Function
は FunctionDeclaration
, FunctionExpression
をまとめたものです。
types
は AST 操作のためのユーティリティ関数群です。
- AST ノードの作成(
identifier()
,memberExpression()
,assignmentExpression
などノード名の先頭を小文字にしたもの。引数の順番はdefinitions のbuilder
プロパティで確認できます。) - AST ノードの判定(
isIdentifier()
,isDecorator()
など。第二引数でプロパティもチェックできます。)
これらのメソッドは definitions の定義から生成されています。詳細は定義を見てみるといいでしょう。
types を使った AST 変換の方法は、組み込みの transformer のソースコード を見れば大体のパターンがあるので、一覧から自分の行いたい変換に近い transformer を探して、ソースを見てみるのが一番の近道だと思います。あまり詳しく書かかれていませんが公式ドキュメントにも目を通しておくといいでしょう。
例(クラスのコンストラクタの引数をインスタンスのプロパティとして代入する)
例として babel-plugin-auto-assign という「クラスのコンストラクタの引数をインスタンスのプロパティとして代入する」プラグインを作ってみます。TypeScript の parameter properties みたいなやつですね。Angular で class ベースの DI をする時などに便利です。
全部の class に対して実行するのも良くないで @autoAssign
という decorator がついた class に対してだけ実行するようにします。また @autoAssign
は後で使わないので消してしまいます。最終的なコードには出てこない ambient decorator というやつですね。
@autoAssign
class Hello {
constructor(foo, bar, baz) {
}
}
class Hello {
constructor(foo, bar, baz) {
this.foo = foo;
this.bar = bar;
this.baz = baz;
}
}
デフォルトでは Babel プラグインは class サポートを含む標準の transformer よりも前に実行されます。このため class は class のままにしておいても後に適用される transformer が ES5 に変換してくれます。(標準の transformer の後に適用したい場合は、利用時に --plugins auto-assign:after
のように名前に :after
を付けます。)
変換前後の AST
AST の変換をするには、変換前後の AST の形を知らなければなりません。ソースコードを入力すると AST を表示してくれる JS AST Explorer という便利なツールがあります。これで変換前後の AST を調べましょう。パーサーは Babylon を選びます。
console.log()
でノードを表示しながら開発するのも捗ります。
実際のコード
目標の構造がわかれば、あとは書くだけです。types を使って必要な AST ノードを作って挿入しています。
テスト方法などは実際のレポジトリを見てみてください。
import AutoAssign from './auto-assign';
export default function ({ Plugin, types: t }) {
return new Plugin('autoAssign', {
visitor: {
ClassDeclaration: function (node, parent) {
new AutoAssign(t).run(node);
}
}
});
}
export default class AutoAssign {
constructor(types) {
this.types = types;
}
run(klass) {
// @autoAssign という decorator がついている時だけ実行。
const decorators = this.findautoAssignDecorators(klass);
if (decorators.length > 0) {
// コンストラクタとその引数を取得。
const ctor = this.findConstructor(klass);
const args = this.getArguments(ctor);
// コンストラクタの最初に代入文を挿入。
this.prependAssignments(ctor, args);
// 用済みの @autoAssign を削除。
this.deleteDecorators(klass, decorators);
}
}
findautoAssignDecorators(klass) {
return (klass.decorators || []).filter((decorator) => {
return decorator.expression.name === 'autoAssign';
});
}
deleteDecorators(klass, decorators) {
decorators.forEach((decorator) => {
const index = klass.decorators.indexOf(decorator);
if (index >= 0) {
klass.decorators.splice(index, 1);
}
});
}
findConstructor(klass) {
return klass.body.body.filter((body) => {
return body.kind === 'constructor';
})[0];
}
getArguments(ctor) {
return ctor.value.params;
}
prependAssignments(ctor, args) {
const body = ctor.value.body.body;
args.slice().reverse().forEach((arg) => {
const assignment = this.buildAssignment(arg);
body.unshift(assignment);
});
}
buildAssignment(arg) {
const self = this.types.identifier('this');
const prop = this.types.memberExpression(self, arg);
const assignment = this.types.assignmentExpression('=', prop, arg);
return this.types.expressionStatement(assignment);
}
}
実行
このプラグインは decorator をパースしたいので --optional es7.decorators
をつけて実行します。plugins は npm module 名だけではなくファイル名でも OK です。
npm run build
echo '@autoAssign class Hello { constructor(foo, bar, baz) {} }' | babel --optional es7.decorators --plugins ./lib/index.js
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Hello = (function () {
function Hello(foo, bar, baz) {
_classCallCheck(this, _Hello);
this.foo = foo;
this.bar = bar;
this.baz = baz;
}
var _Hello = Hello;
return Hello;
})();
うまく行きました。
公開
あとは README.md を書いたら npm run push
で npm モジュールとして全世界に公開できます。
便利な Babel プラグインを作りましょう!
参考
ドキュメント
実例
- shuhei/babel-plugin-auto-assign この記事の例として作ったもの。
- shuhei/babel-plugin-angular2-annotations Angular 2 アプリを Babel で作るためのプラグイン。Babylon にモンキーパッチして、TypeScript のようにメソッドの引数へ decorator を付けられるようにしています。