実際にプラグインを作ってみましょう。
まずは単純なものから。
なお、この章で作るプラグインは、リポジトリの ./try-babel
で実際に試すことができます。
ECMAScriptでは比較演算子に ==
と ===
の2種類がありますが、アプリケーションでは厳密な比較をする ===
しかほぼ使いません。
これは console.log(1 == '1')
を試してみたら一瞬で理解できるはずです。
ただし、nullと比較するときだけ、 ほとんどの場合 null
と undefined
を区別しない ==
を使います。
こんな演算子2つも要らないのでどちらも ==
で良きに計らって欲しいですね。
Babelを使って演算子をひとつにまとめてみましょう1。
変換したいのは比較演算子です。
これは値を返すので、何かの式であることが分かります。
仮に比較式とでも名づけておきましょう。
この比較式のどちらかの辺が null
なら ==
、 そうでなければ ===
を使うように演算子を書き換えます。
Babylonで"比較式"を見つけましょう。
リポジトリにAST仕様書があるのでこの中から調べます。
まず ===
で検索すると BinaryOperator の一種であることが分かります。
ではBinaryOperatorを使っている式を探すと、すぐその上に BinaryExpression が見つかりました。
変換したいのは比較式ではなくBinaryExpressionであり、
BinaryOperatorには ==
以外にもたくさんの演算子を含むことが分かりました。
まず、お約束のような雛形です。
export default ({types: t}) => ({
visitor: {
BinaryExpression: (path) => {
}
}
});
この関数の中身を書いていけばよいわけですが、その前にテストを書きましょう。
test/fixtures/equal/equal
ディレクトリを作り、その中に actual.js
と expected.js
を書いていきます2。
actual.js
には変換前のコードを書きます。
変換パターンを網羅しましょう。
1 == 2;1 == null;1 == undefined;null == undefined;
1 === 2;1 === null;1 === undefined;null === undefined;
1 != 2;1 != null;1 != undefined;null != undefined;
1 !== 2;1 !== null;1 !== undefined;null !== undefined;
expected.js
には変換後のコードを書きます。
1 === 2;1 == null;1 === undefined;null == undefined;
1 === 2;1 == null;1 === undefined;null == undefined;
1 !== 2;1 != null;1 !== undefined;null != undefined;
1 !== 2;1 != null;1 !== undefined;null != undefined;
テストを実行して失敗する状態を確認しましょう。 スペースの都合で1行にまとめましたが、本来はきちんと改行したり、テストパターンごとにディレクトリを分けたほうがよいでしょう。 これ以外の演算子に影響がないことや、左右辺を入れ替えたり、 実際にはもっとパターンを増やす必要があります。
まず console.log(path.node);
を追加し、テストを動かしてみましょう。
すると left
, operator
, right
のキーがあることが分かるので、
後はこれで場合分けすれば良さそうです。
関数の中身はこのようになります。
const { left, right, operator} = path.node;
const matched = operator.match(/([!|=])==?/);
if (matched) {
if ((t.isNullLiteral(left) || t.isNullLiteral(right))) {
path.node.operator = `${matched[1]}=`;
} else {
path.node.operator = `${matched[1]}==`;
}
}
正規表現で operator
をマッチさせ、左右どちらかが null
かどうかで書き換える演算子を変えています。
ノードの判定は、 left === null
のような単純な方法ではなく、 t.isNullLiteral
を使わなければならないことに注意です。
テストを動かすと綺麗な緑色の文字が見えることでしょう。
前章で書き換えは Reactive に動くと書きましたが、それを実証するプラグインを書いてみましょう。
if(true) {
function a() {
return 1;
}
}
このようなコードを入力したとき、 return
文の親をたどり function a
を置き換え、
if (true) {
if (false) {
}
}
のように変換するプラグインを作ります。
IfStatement
にログ出力を追加しておき、return
文の書き換え後に IfStatement
のログが出力されるのを確認します。
export default ({types: t}) => ({
visitor: {
IfStatement: (path) => {
console.log('If Statement');
},
ReturnStatement: (path) => {
console.log('Return Statement');
path.parentPath.parentPath.replaceWith(
t.ifStatement(
t.booleanLiteral(false),
t.blockStatement([])
)
);
}
}
});
ReturnStatement
の親の親が function
文です。
このコードを実行すると、
If Statement
Return Statement
If Statement
と出力され、親を書き換えた if
文に対してもVisitorが作用していることが分かります。
最後に、以前の約束を果たしましょう。
plugin
と preset
がたくさん読み込まれた場合、実行される順序を把握しておかないと思わぬ事故にあう可能性があります。
他のプラグインにより先に変換されてしまい、自分のプラグインが想定した通り動かないことがありそうです。
大量のプラグインが必要なときは、 require-self
のコードを参考にします。
node_modules/babel-plugin-a1.js
に
module.exports = require('../plugin-a1');
と言うファイルを、 plugin-a1
, plugin-a2
, plugin-b1
, plugin-b2
, plugin-c
, plugin-d
, preset-a
, preset-b
のぶん作ります。
さらに、それぞれのプラグインでは単純に
{
visitor: {
Program: (path) => {
console.log('a1');
}
}
}
のようなログ出力をすれば、出力順でプラグインが呼ばれる順番を確認できます。
.babelrc
は、
{
"presets": ["a", "b"],
"plugins": ["c", "d"]
}
です。
これを実行すると、
c
d
b1
b2
a1
a2
の順でプラグインが実行されることが分かりました。preset-a
と preset-b
の呼ばれる順は意外でしたね。