この記事は JavaScript Advent Calendar 2019 の 23日目の記事です。
前日の22日目は Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版) でした。
今回は表題通りBabel Plugin
を作りながらAST
とBabelを学ぼうという記事です。
AST とは?
まずは根幹であるAST
について軽く説明します。
AST
はAbstract Syntax Tree
の略で、日本語では抽象構文木などと呼ばれるものです。
AST
はプログラムの構造を示したデータ構造体であり、JavaScriptではJSON
データの形で表現されることが一般的になり、基本的に仕様は、ESTree に準拠されています。
AST
はBabel
以外に ES Lint や webpack などにも使用されています。
実際にAST
がどのようなものなのかをAST explorerというサイトで簡単に確認することができます。
今回はconst a = 1
をAST
の構造体にしてみました。
画面左側が実際の値、右側がAST
の構造体になります。
これは acorn というミニマムな JavaScript の parser により生成されたものです。
こちらがそのデータになります。
{
"type": "Program",
"start": 0,
"end": 11,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
後ほど詳しく説明します。
今はAST
はこのようなJSON
なんだなというくらいの理解で大丈夫です。
Babel とは?
続いてBabel
について説明をします。
Babel
とは JavaScript のコードを変換してくれる Compiler です。
元々Babel
は6 to 5
という名称でした、その名の通り ES6 から ES5 にコードを変換するだけのものでした。
しかし、その後さまざまな要望を得た機能が実装され、現在のBabel
という名称になりました。
Babelの機能
Babel
は主に3つの機能を備えています。
- 構文変換
- Polyfill の提供
- ソースコードの変換
Babel
は上記のような機能をもって、ブラウザでは使用できない最新の機能を書いた JavaScript や TypeScript を、指定したブラウザでも使用できるようにコードを変換します。
例えば const a = () => {}
のようなアロー関数は、@babel/plugin-transform-arrow-functions によって処理され、このようなコードになります。
https://babeljs.io/docs/en/babel-plugin-transform-arrow-functions
Babel はコードをどのように変換するのか?
Babel
の変換にはこのように3つの段階があります。
- Parsing
-
@babel/parser を用いて、ソースコードを
AST
に変換
-
@babel/parser を用いて、ソースコードを
- Transformation
-
@babel/traverseを用いて、
AST
を変換する、
-
@babel/traverseを用いて、
- Code Generat
-
@babel/generator を用いて
AST
をソースコードに変換する
-
@babel/generator を用いて
実際に Babel Plugin を作りながら AST を学ぶ
ここからは実際にBabel Plugin
を作りながらBabel
がどのようにAST
を駆使してコードを変換しているのか見ていきましょう。
今回はconst
とlet
をすべてvar
に置き換えるものを作成します。
前準備
必要なパッケージを事前に落としておきます。
npm i -D @babel/parser @babel/generator @babel/traverse
1. Parse
@babel/parser を使用して、ソースコードをAST
に変換しましよう。
// parser/index.js
const { parse } = require("@babel/parser");
// AST に変換
const ast = parse(`
const a = 1
`);
console.log(JSON.stringify(ast, null, 2));
このコードを実際に.json
ファイルに出力します。
node node parser/index.js
実際に出力されたものはこちらです。
{
"type": "File",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 0
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 0
}
},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 1,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 2,
"column": 11
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 11
}
},
"id": {
"type": "Identifier",
"start": 7,
"end": 8,
"loc": {
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 7
},
"identifierName": "a"
},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 11,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 11
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": []
}
これでAST
に変換することができました!
2. Generate
続いては @babel/generator を用いて先ほどAST
にしたデータをソースコードに変換します。
// generator/index.js
const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
// ソースコードを AST に変換
const ast = parse(`
const a = 1
`);
// generate の第一引数に AST を格納
console.log(generate(ast).code);
このコードを実行してみましょう。
node generator/index.js
実行結果はこのようになります。
const a = 1;
AST
に変更される前と変わりないコードが生成されました、これはAST
になにも変更を加えずにコードに戻したからです。
3. Travers
ここから本題であるconst
とlet
をすべてvar
に置き換える作業を行います。
もう一度1. Parse
で吐き出したAST
を見てましょう。
{
"type": "File",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 0
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 0
}
},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 1,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 2,
"column": 11
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 11
}
},
"id": {
"type": "Identifier",
"start": 7,
"end": 8,
"loc": {
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 7
},
"identifierName": "a"
},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 11,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 11
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": []
}
VariableDeclaration
に"kind": "const"
という値があるのが分かります。
ここがかなり怪しいので、冒頭にも紹介した JavaScript の AST
の仕様に相当するEsTree
でVariableDeclaration
を検索してみましょう。
検索結果はこちら
このような検索結果コードが表示されています。
extend interface VariableDeclaration {
kind: "var" | "let" | "const";
}
どうやらこのkind
の値をvar
にすればよさそうなので、これからASTを使って変換していきます。
今回は @babel/traverse を使用していきます。
// traverse/index.js
const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
// AST を変換
const ast = parse(`
const a = 1
let b = 2
`);
// AST を第一引数に、変更内容を第二引数にする
traverse(ast, {
// 変更したい部分(今回は VariableDeclaration )
VariableDeclaration(path) {
// kind を var に変更
path.node.kind = "var";
},
});
// // generate の第一引数に AST を格納し、コードを生成
console.log(generate(ast).code);
これを実行します。
node traverse/index.js
実行結果はこちらになります。
var a = 1;
var b = 2;
これで、const
とlet
をvar
に変更することができました。
ほかにもこのようにすることで値の変更もできます。
// traverse/index.js
const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const ast = parse(`
const a = 1
let b = 2
`);
traverse(ast, {
VariableDeclaration(path) {
path.node.kind = "var";
},
VariableDeclarator(path) {
if (path.node.id.name === "a") {
path.node.id.name = "c";
path.node.init.value = 3;
}
}
});
console.log(generate(ast).code);
実行。
node traverse/index.js
実行結果。
var c = 3;
var b = 2;
このようにAST
を駆使すれば正規表現では表現できないようなパターンも変更可能になります。
最後に
今回説明した部分は、Babel Plugin
の肝となる部分になります。
AST
とBabel Plugin
の肝を理解すれば、 @babel/plugin-transform-arrow-functions のコード のようなプラグインがやっていることは案外簡単なことのように思えるかもしれません。
AST
を使って便利ツールを作っていきましょう!!
今回使用したソースコードはこちらのレポジトリにあります。
https://github.com/sakit0/babel-plugin-demo