BabelPlugin開発を加速する「babel-udf-helpers」を作りました

TL;DR

「babel-udf-helpers」を使う事で

  • BabelPlugin開発で@babel/typesを使って、ASTを書く事を減らす事ができる。
  • @babel/template にはない、依存関係を定義した関数に持たせる機能がある。
  • @babel/types@babel/templateを使っていると困る問題、トラバースしようとしているコードのグローバル変数との名前衝突を自動で解決してくれる機能がある。(リネーム機能)

の恩恵が得られます。何かしらのbabel-pluginを作った事がある人にしかピンと来ないかもしれません。すみません。

...と言う事で以下はかなり専門的な内容です。

@babel/helpers のコードとほぼ同じなので、同じ機能があるわけでです。

公式ドキュメントでは特徴に関しては書いてないですね。おそらくユーザーが定義できないものだから説明する必要ないと感じたのでしょう。

作成のきっかけ

普段お世話になっている、webpackの仕組みが知りたいなっと思ってググってたら以下のブログを見つけたのがきっかけでした。

技術探し/module bundlerの作り方(準備編)

webpackはmodule bundlerの一種で、そのmodule bundlerの説明をしてあるブログでした。 詳しい説明があり、ブログの記事を読んでイメージまではできたが、実際動かすまで意味はわかってなかったです。

このブログで紹介されているサンプルプロジェクトの何が素晴らしかったかというと、ちゃんとテストまで書いてくれていて、node debuggerでデバッグできた事でした。これができなければ、どの様な値が渡っているのか想像できず、当然、理解もできなかったと思います。本当に感謝です。

このブログのコードを隈無くデバッグして、トラバース処理もしっかし理解できたあたりから、自分も「module bundler」(実際はほぼbabel-transform-pluginだったが...)を作り始めました。

「module bundler」の雛形として選んだのは、こちらのリポジトリで紹介されていたminipackというやつでした。

Good Point
  • コードを読んで直ぐに理解できた事
  • コメントが丁寧で何やっているかわかりやすかった事
  • 「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 REPLAST 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を作る時、だいたいこんな感じの構成で始めると思うのですが、

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の最小バージョンを書く様になってますが、
 *  必要性を感じなかったので私のではそうはなってません。
 */

こんな感じで定義していきます。 詳しいヘルパーの定義の仕方に関してはこちらのドキュメントをご覧ください。

helper.js
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になってグローバル変数の名前衝突をうまく回避しています。

ヘルパー関数が依存を持つパターン

helper.js
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/coreaddUDFHelperメソッドが実装されたら使えなくなるので安心である。
  • babel-udf-helpersを使うと以下の問題を解決できる。

    • BabelPlugin開発で@babel/typesを使って、ASTを書く事を減らす事ができる。
    • @babel/template にはない、依存関係を定義した関数に持たせる機能がある。

      • これにより繰り返す処理は関数に切り出して書ける。
    • @babel/types@babel/templateを使っていると困る問題、トラバースしようとしているコードのグローバル変数との名前衝突を自動で解決してくれる機能がある。(リネーム機能)

コードはここにあります。

もし使ってみてこれ便利じゃん!など感じられたら🌟下さい。開発の励みになります。🙇‍♂️

以上です。

yukihirop

この記事を書いた人

フロントエンド兼バックエンドなWEBエンジニアです。 既存の考え方に囚われずに自由にツールを作るのが好きです。

いつかBioに「Creator of ...」と書けるようなプログラマを目指している人のブログ

関連記事