BabelPlugin開発を加速する「babel-udf-helpers」を作りました
TL;DR
「babel-udf-helpers」を使う事で
- BabelPlugin開発で
@babel/types
を使って、ASTを書く事を減らす事ができる。 @babel/template
にはない、依存関係を定義した関数に持たせる機能がある。@babel/types
と@babel/template
を使っていると困る問題、トラバースしようとしているコードのグローバル変数との名前衝突を自動で解決してくれる機能がある。(リネーム機能)
の恩恵が得られます。何かしらのbabel-pluginを作った事がある人にしかピンと来ないかもしれません。すみません。
...と言う事で以下はかなり専門的な内容です。
@babel/helpers のコードとほぼ同じなので、同じ機能があるわけでです。
公式ドキュメントでは特徴に関しては書いてないですね。おそらくユーザーが定義できないものだから説明する必要ないと感じたのでしょう。
作成のきっかけ
普段お世話になっている、webpackの仕組みが知りたいなっと思ってググってたら以下のブログを見つけたのがきっかけでした。
webpackはmodule bundler
の一種で、そのmodule bundlerの説明をしてあるブログでした。
詳しい説明があり、ブログの記事を読んでイメージまではできたが、実際動かすまで意味はわかってなかったです。
このブログで紹介されているサンプルプロジェクトの何が素晴らしかったかというと、ちゃんとテストまで書いてくれていて、node debugger
でデバッグできた事でした。これができなければ、どの様な値が渡っているのか想像できず、当然、理解もできなかったと思います。本当に感謝です。
このブログのコードを隈無くデバッグして、トラバース処理もしっかし理解できたあたりから、自分も「module bundler」(実際はほぼbabel-transform-pluginだったが...)を作り始めました。
「module bundler」の雛形として選んだのは、こちらのリポジトリで紹介されていたminipack
というやつでした。
- コードを読んで直ぐに理解できた事
- コメントが丁寧で何やっているかわかりやすかった事
- 「module bundler」の最小構造を有してそうだった事
が決め手でした。スターの数がものがったっている様に納得のわかりやすさです。
で、このコードを読んでいくと、ECMAScript Module(ESM)をCommonJS(CJS)に変換する処理を @babel/preset-env
に任せている事がわかりました。それを見て、「そうか@babel/preset-envの部分を自分で作る様にしたら勉強になりそうだな」 って思って作り始めました。
@babel/preset-envの紹介
@babel/preset-env
は、ESMをCJSだったり、Asynchronous Module Definition(AMD)だったり、SystemJS(SJS)だったり、Universal Module Definition(UMD)だったりのモジュール形式にトラバースしたり、destructuring(分割代入)構文など新形式の構文をトラバースして、JavaScriptの構文で実現できる様にしたりするための基本babel-transform-plugin
の集合体です。
丁寧に見ていくと、全部で53+5=58個のプラグインで構成されています。
(5個は特定のブラウザでの不具合対応用のプラグインで公式ドキュメントはありません。)
公式ドキュメントと関連ずけて書き出してみると以下の様な感じです。
- @babel/plugin-syntax-async-generators(async generatorsの対応)
- @babel/plugin-syntax-class-properties
- @babel/plugin-syntax-dynamic-import
- @babel/plugin-syntax-json-strings
- @babel/plugin-syntax-nullish-coalescing-operator
- @babel/plugin-syntax-numeric-separator
- @babel/plugin-syntax-object-rest-spread
- @babel/plugin-syntax-optional-catch-binding
- @babel/plugin-syntax-optional-chaining
- @babel/plugin-syntax-top-level-await
- @babel/plugin-proposal-async-generator-functions(余力があったらサポートしてみる)
- @babel/plugin-proposal-class-properties
- @babel/plugin-proposal-dynamic-import
- @babel/plugin-proposal-json-strings(メリットがよくわからん)
- @babel/plugin-proposal-nullish-coalescing-operator(ヌリッシュ合体演算子)
- @babel/plugin-proposal-numeric-separator(ヌメリックセパレーター)
- @babel/plugin-proposal-object-rest-spread(オブジェクト・スプレッド・レスト)
- @babel/plugin-proposal-optional-catch-binding(try...cactch文にfinalyをつけるやつ?)
- @babel/plugin-proposal-optional-chaining(オプショナルチェイン(babel提案)・nodeで受け入れられたのでやる)
- @babel/plugin-proposal-private-methods(#をつけて変数を書いたらプライベートになる)
- @babel/plugin-proposal-unicode-property-regex
- @babel/plugin-transform-async-to-generator(async・awaitの変換)
- @babel/plugin-transform-arrow-functions(arrow functionの変換)
- @babel/plugin-transform-block-scoped-functions(functionを取り除くやつ・メリットがわからん)
- @babel/plugin-transform-block-scoping(ブロックスコーピング)
- @babel/plugin-transform-classes(ネイティブクラスはSuperClass.applyの呼び出しできないからextendsしたクラスを使うようにするプラグイン)
- @babel/plugin-transform-computed-properties(計算されたプロパティー)
- @babel/plugin-transform-destructuring(展開代入)
- @babel/plugin-transform-dotall-regex(dotAll正規表現)
- @babel/plugin-transform-duplicate-keys(オブジェクトのキーの重複を回避するプラグイン)
- @babel/plugin-transform-exponentiation-operator(2乗)
- @babel/plugin-transform-for-of(for-ofの理解になるからやってみるか)
- @babel/plugin-transform-function-name(arrow functionを解決して、変数名を関数名にするプラグイン)
- @babel/plugin-transform-literals(unicodeを文字列に変換するプラグイン)
- @babel/plugin-transform-member-expression-literals(予約語をプロパティーのキーにしている時に文字列に変換するプラグイン)
- @babel/plugin-transform-modules-amd(ESMをAMDに変換するプラグイン)
- @babel/plugin-transform-modules-commonjs(ESMをCJSに変換するプラグイン)
- @babel/plugin-transform-modules-systemjs(ESMをSystemJSに変換するプラグイン)
- @babel/plugin-transform-modules-umd(ES6をUMDに変換するプラグイン)
- @babel/plugin-transform-named-capturing-groups-regex(正規表現のネームキャプチャリング)
- @babel/plugin-transform-new-target(よくわからない)
- @babel/plugin-transform-object-super(メソッドをキー: functionに書き直すプラグイン)
- babel-plugin-transform-es2015-parameters(ES2015(ES6)のパラメーターをES5に変換)
- @babel/plugin-transform-property-literals(プロパティーキーの""をとるプラグイン(default除く))
- @babel/plugin-transform-regenerator(generatorの変換)
- @babel/plugin-transform-reserved-words(ES3の予約語の名前が使われていたら回避するようにするプラグイン,abstractとか)
- @babel/plugin-transform-shorthand-properties(ショートハンドプロパティー, {a,b} => {a: a, b: b}と書き直すやつ)
- @babel/plugin-transform-spread(スプレッドの変換)
- @babel/plugin-transform-sticky-regex(スティッキー正規表現)
- @babel/plugin-transform-template-literals(テンプレートリテラル)
- @babel/plugin-transform-typeof-symbol(typeofのES6で導入されたSymbol対応)
- @babel/plugin-transform-unicode-escapes(unicodeのエスケープ)
- @babel/plugin-transform-unicode-regex(unicodeのエスケープ(正規表現))
ここから、特定のブラウザでの不具合対応用のプラグイン(公式ドキュメントなし)
- @babel/preset-modules/lib/plugins/transform-async-arrows-in-class(Safari10.3でのarrow関数で起きた問題の回避用プラグイン)
- @babel/preset-modules/lib/plugins/transform-edge-default-parameters(デフォルト値をもつ非構造化パラメーターを短縮形以外の構文に変換。Edge16 or Edge17でのバグを回避)
- @babel/preset-modules/lib/plugins/transform-tagged-template-caching(デフォルト値をもつ非構造化パラメーターを短縮形以外の構文に変換。ESMをサポートするブラウザーSafari10 or Safari11のタグ付きテンプレート関係のバグが修正される)
- @babel/preset-modules/lib/plugins/transform-safari-block-shadowing(Safari 10/11のブロックシャドウのlet/constのバインディング修正)
- @babel/preset-modules/lib/plugins/transform-safari-for-shadowing(Safari〜11には、Forステートメントの変数宣言がパラメーターをシャドウする場合にスローされるという問題があります。これは、For *ステートメントのleft / init部分の宣言の名前を変更することで修正され、シャドウされません。)
だいたいこんな感じです。
チェックマークをつけたプラグインを、公式のプラグインで書いてあるテストが通る様に、Babel REPL
とAST Explorer
の力を借りて書いていきました。とりあえず作ったのは上から順番に
- @babel/plugin-transform-arrow-functions
- @babel/plugin-transform-modules-commonjs
- @babel/plugin-transform-typeof-symbol (問題を感じ始める)
- @babel/plugin-transform-destructuring (現在ここ。問題にぶち当たる)
です。
実は難易度も上から順番になっているんです。これは偶然でしたね。持ってるものがあるんですかね。(笑)
ESMをCJSにするプラグイン(modules-commonjs)より、分割代入を実現するプラグイン(destructuring)の方が難しいのです。
なんとなく最初のイメージは逆でしたが、全く見当はずれでした。
コードを見てもわかりますが、コード量が3倍です。今だに、destructuringの方のコードは読んでもわかりません。(笑)
destructuringのコードが難しいのはヘルパーが多く使われているというのもあるのですが、変換が複雑な点だと思います。
var _f = f()
の上に定義されているコードは全てヘルパー関数です。
destructuringの様なプラグインを作りたい場合は、間違いなくヘルパーがないと作るの辛いと思いました。
それで「プラグイン作成者がヘルパーを定義できたらいいなぁ」って思い始めて、@babel/helpers
のコードを読み始めたわけです。
babel-udf-helpersとは
ここまでの流れから、どういうツールかわかったかと思うのですが、Babel REPLの例でいうところの
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
Babelでは this.addHelper("objectWithoutProperties")
のコードで挿入を実現しています。
この部分をプラグイン作成者も簡単に作れたらいいねと言う事です。
なおobjectWithoutProperties
と言うヘルパーはここで定義されていて、@babel/helpers
のここで読み込まれてます。で、肝心のloadHelper
はどこから呼び出されるかと言うと、@babel/core
に定義されている addHelper
メソッドのここから呼び出されます。
addHelper
のUDF版と言う事で、addUDFHelper
というメソッドを@babel/core
に定義しました。
なのでこのツールは@babel/core
のcore extensionになります。
安心してください。仮に @babel/core
がサポートしたら使えなくなる様にエラーハンドリングしてありますので。
babel-udf-helpersの使い方
babel-transform-pluginを作る時、だいたいこんな感じの構成で始めると思うのですが、
export default function ({ types: t}) {
return {
name: "babel-transform-plugin-sample",
visitor: {
Program(path) {
/* something */
}
}
}
}
returnの部分にpreを追加します。
import { useDangerousUDFHelpers } from 'babel-udf-helpers'
import helpers from 'helpers'
export default function ({ types: t}) {
return {
name: "babel-transform-plugin-sample",
pre(){ useDangerousUDFHelpers(this, { helpers }) }, visitor: {
Program(path) {
/* something */
}
}
}
}
最後に helpers.js
を定義します。
import { helper } from 'babel-udf-helpers'
const helpers = Object.create(null)
export default helpers
/** ここから下に@babel/helpersがやっているみたいにhelperを定義していきます。
* @babel/helpersでは@babel/coreの最小バージョンを書く様になってますが、
* 必要性を感じなかったので私のではそうはなってません。
*/
こんな感じで定義していきます。 詳しいヘルパーの定義の仕方に関してはこちらのドキュメントをご覧ください。
helpers.sampleHelper = helper`
export default function _sampleHelper(){
return "sampleHelper"
};
`;
で最後に、visitorのどこからでもいいので呼び出します。
import { useDangerousUDFHelpers } from 'babel-udf-helpers'
import helpers from 'helpers'
export default function ({ types: t}) {
return {
name: "babel-transform-plugin-sample",
pre(){
useDangerousUDFHelpers(this, { helpers })
},
visitor: {
Program(path) {
// 返り値の型は、t.Identifier const identifier = this.addUDFHelpers("sampleHelper") }
}
}
}
基本
トラバースしたいコードが、var a;
でこのプラグインを適用したなら結果はこうなります。
function _sampleHelper(){
return "sampleHelper"
}
var a;
ただ上に挿入されるだけですね。特に意味のないコードです。
リネームされるパターン
トラバースしたいコードが、function _sampleHelper(){}
でこのプラグインを適用したなら結果はこうなります。
function _sampleHelper2(){
return "sampleHelper"
}
function _sampleHelper(){};
ヘルパー関数名が_sampleHelper2
になってグローバル変数の名前衝突をうまく回避しています。
ヘルパー関数が依存を持つパターン
helpers.sampleHelper = helper`
import dependency from 'dependency'
export default function _sampleHelper(){
return dependency();
};
`;
helpers.dependency = helper`
export default function _dependency(){
return "dependency";
};
`;
こんな感じでヘルパー関数は依存を持つ事ができます。これは、@babel/template
にはない機能です。
トラバースしたいコードが、var a;
でこのプラグインを適用したなら結果はこうなります。
function _sampleHelper(){
return _dependency()
}
function _dependency(){
return "dependency";
}
var a;
ヘルパー関数が依存の方にリネームの必要が発生するパターン
トラバースしたいコードが、function _dependency(){};
でこのプラグインを適用したなら結果はこうなります。
function _sampleHelper(){
return _dependency2()
}
function _dependency2(){
return "dependency";
}
function _dependency(){};
依存関数が_dependency2
にリネームされて、ちゃんとsampleHeleprの呼び出し側でもそれを参照する様になってます。
どうしてこう言う事が可能かと言うと、addUDFHelper
を呼び出した時に、定義したヘルパー関数に対してプログラムボディーの
グローバル変数と重ならない様に、ReferencedIdentifierブロックでトラバース処理をしているからです。
詳しくはコードを見てください。
BabelPluginの車輪の再発明をしていて思った事(余談)
よく、車輪の再発明は無駄だからやらないほうがいいと言う意見がありますが、BabelPluginの再発明はやって効果がありました。😁
具体的に書くと
-
巻き上げ(hoisting)について知った。(恥ずかしながら、巻き上げ知りませんでした。)
-
巻き上げとは、グローバルスコープで書かれた変数がファイルのどこに書かれていても、インタプリタが巻き上げてファイルの最初に定義されたかの様に振る舞うにするインタプリタが持つ機能である。これが以下に対して起きます。
- var変数
- 関数宣言(Function Declaration)
-
-
JavaScriptには、thisのbindを解除できるシーケンス式
(0,fn)(args)
と言うものがある事を知りました。- こちらのコードをブラウザで実行してみてください。
var foo = { fullName: "Peter", sayName: function() { console.log("My name is", this.fullName); } }; // もしくは単に、fullName = 'Shiny'; window.fullName = "Shiny"; foo.sayName(); // My name is Peter (foo).sayName(); // My name is Peter (foo.sayName)(); // My name is Peter (0, foo.sayName)(); // My name is Shiny
-
babel-transform-plugin書けそうな気がしてきました。
- Babelにコントリビュートできそうな予感がします。
- まぁ...実際やってみんですけどね...
-
Babelのコントリビュートといえば面白いIssueを発見しました。
- #11527 traverse of Array Literal takes O(n^2) time
-
Array Literalのトラバース処理にO(n^2)の計算量がかかっていると言うIssueです。
- チャレンジしてみましたが、解決できませんでした。(残念)
- 計算量を減らそうと苦悩している時に、
kd-tree
という2次元二分木探索のやり方を知りました。 - 競技プログラマーの人チャレンジしてみませんか?
-
for文の面白い記法を知りました。
- for文をよく理解してないと、普段なかなかこう言う使い方は思いつかないですね。
babel-udf-helpers
というライブラリーを作る事ができました。
まとめ
如何でしょうか。
babel-udf-helpers
を使うと、BabelPlugin開発が楽になる様な予感がしませんか?
テスト(特にエラー処理)を読んで、問題ないかもって感じられたら使ってみたらどうでしょうか?😁
まとめます。
-
babel-udf-helpers
は@babel/core
のcore extensionライブラリーである。- だが、仮に
@babel/core
にaddUDFHelper
メソッドが実装されたら使えなくなるので安心である。
- だが、仮に
-
babel-udf-helpers
を使うと以下の問題を解決できる。- BabelPlugin開発で
@babel/types
を使って、ASTを書く事を減らす事ができる。 -
@babel/template
にはない、依存関係を定義した関数に持たせる機能がある。- これにより繰り返す処理は関数に切り出して書ける。
@babel/types
と@babel/template
を使っていると困る問題、トラバースしようとしているコードのグローバル変数との名前衝突を自動で解決してくれる機能がある。(リネーム機能)
- BabelPlugin開発で
コードはここにあります。
もし使ってみてこれ便利じゃん!
など感じられたら🌟下さい。開発の励みになります。🙇♂️
以上です。