Babelマクロを書いてみる
babel-plugin-macros
というものの存在を知って、実際にマクロを書いてみました。
babel-plugin-macros
とは
すでにQiitaへ記事を書いている方がいるので軽く触れる程度にしますが、BabelでJavaScriptを変換する際に、import
した名前を使った特定の場所だけ特別な変換を入れられるようにする、という仕組みです。
ふつうにBabelのプラグインにするのと比べて、以下のようなメリット・デメリットがあります。
- メリット
-
import
した名前と紐づくので、不意にあちこちで変換が適用されてしまうことがない -
babel.config.js
にbabel-plugin-macros
を入れておけば、あとはimport
で適用できるので、特殊な変換を自作する場合にも、そのたびごとにBabelの設定を変更する必要がない
-
- デメリット
- ASTレベルでの変換なので、JavaScriptの文法から外れた入出力を行うことはできない
-
import
して使う必要があるので、全コードへ一律に適用したい変換には向かない - 依存関係の制御ができない(他のファイルや時間など、外部データに依存した変換を行う場合、外部データが変化してもキャッシュが残り続けてしまう)
マクロはどのように作るのか
実際にマクロを書き始める前に、マクロ内部での処理はどのように進むのかをまとめておきます。
まず、Babel内部では、JavaScriptコードはASTという形式で管理されていて、ちょうどHTMLをDOMにしたのと同じように、各コード片(ノード)がJavaScriptのオブジェクトとなっていて、ツリーをたどったり、操作したりといったことができます。AST Explorerのような、JavaScriptコードがどのようなASTになるかを調べられるツールもあります(ツールによって微妙にノードの種類などが違いますので、Babelの参考にする場合は@babel/parser
を選んでおきましょう)。
import { foo } from 'some.macro'
のようにマクロを読み込んだソースコードでfoo
を書くと、マクロコードにはfoo
のノードが渡されます。そこからASTをたどって改変を行う、という流れです(ソースコード全体のASTにアクセスできますので、その必要があればfoo
と離れた箇所に手を入れることも可能です)。
簡単なものを書いてみる
例として、import test from 'test.macro'
のように読み込んで、test('a')
とすれば、コンパイル後のコードではa
の文字コードである97
になっている、というようなマクロを書くことにします。
基本的なルール
マクロとして動かすファイルは、以下のようなルールで作る必要があります。
-
.macro
あるいは/macro
で終わる名前で参照できるようにする -
require
とmodule.exports
を使うCommon.jsで書く - マクロ処理は同期的に実行する
ということで、基本の枠は以下のようになります。
const { createMacro } = require('babel-plugin-macros');
module.exports = createMacro((params) => {});
マクロ関数の引数
createMacro
の引数となった関数には、以下のようなキーを含むオブジェクト1つが引数として渡されます。
-
babel
…Babelの各種ライブラリ。babel.type.***
が適切な型のASTノードを作るのに必要となります。 -
references
…これ自体もオブジェクトとなっていて、キーはこのマクロをimport
した名前(デフォルトインポートならdefault
)、値はソースに出現したノードの配列です。 -
state
…Babelの処理状態を反映したオブジェクト。細かい位置まで正確なエラーを出したい場合は、state
を利用することになります(詳細略)。 -
config
…設定オブジェクト(詳細略)
references
に来る配列をforEach
で回しつつ、適切なノードに置き換えていく、というのが基本的な流れとなります。
実装例
今回はdefault import
なので、references.default.forEach
でループを回していきます。そして、関数名からparentPath
で1階層上がれば、それはCallExpression
のはずなのでそれを確認してから、引数を回収して処理を加え、書き出しておきます。
const { createMacro, MacroError } = require('babel-plugin-macros');
module.exports = createMacro((params) => {
const {babel, references} = params;
references.default.forEach(node => {
const call = node.parentPath;
if(call.type !== 'CallExpression') throw new MacroError('エラーメッセージ');
const rawValue = call.node.arguments[0].value;
call.replaceWith(babel.type.numericLiteral(rawValue.charCodeAt(0)));
});
});
エラー処理
上の例でも関数呼び出し以外で来たときはエラーを出していますが、積極的に詳細なエラーを出すことをおすすめします。具体的には、
-
import
した名前が適当でなかった場合 - 想定した形以外のノードとしてASTに現れた場合
- 関数形で、引数が想定したものでなかった場合
などが考えられます。特に、関数の引数がリテラルでなかった場合には、「あくまでコンパイル時専用の関数として、リテラル以外を投げた場合はエラーにする」方法や、逆に「リテラルのときはコンパイル時に置き換えるけど、そうでない式であれば、実行時処理のコードを書き出しておく」など、いくつかの方策が考えられます。
簡便にはMacroError
を投げる方法がありますが、state
をたどって詳細な位置まで含めたエラーを出すことも可能です。
外部リンク
Author And Source
この問題について(Babelマクロを書いてみる), 我々は、より多くの情報をここで見つけました https://qiita.com/jkr_2255/items/f98541686f01e011c33b著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .