この記事は はてなエンジニア Advent Calendar 2021 の 5 日目の記事です。
4 日目は id:anatofuz さんの 「入社してから書いていた分報の行数を眺めてみる」 でした。日報に書き込んだ行数を可視化するというアイデアが面白い! 僕も日報書いているので今度可視化してみようと思います。
本題
さて今回はタイトルにもある通り、Babel の話をします。Babel というのは JavaScript のトランスパイラです。 JavaScript のソースコードを入力として受け取り、適切な変換を施し、JavaScript のソースコードを出力する (トランスパイルする) ツールです。主に新しい構文で書かれた JavaScript を、古いブラウザなどでも動くよう、古い構文で書かれた JavaScript に変換するために使われています。
実はあまり知られていないのですが、 Babel は旧構文への変換以外にも、ソースコードを機械的に書き換えるためのリファクタリングツールとしても利用できます。通常 Babel を利用する際は、src/
配下にある JavaScript ファイルを、Babel で変換してdist/
に出力しますが、Babel をリファクタリングツールとして利用する場合は、src/
配下にある JavaScript ファイルを、Babel で変換してsrc/
に出力することになります。
JavaScript におけるリファクタリングツールとなると、codemods や JSCodeshift が有名で、Babel の名前は聞き慣れないと思います。しかし、公式ドキュメントにも Babel がリファクタリングツールとして利用できることが明記されており、きちんとユースケースの一部としてカバーされていることが分かります。
Here are the main things Babel can do for you:
- Transform syntax
- Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
- Source code transformations (codemods)
- And more! (check out these videos for inspiration)
リファクタリングツールとして利用する方法
Babel をリファクタリングツールとして利用する方法については、Babel のメンテナーである Nicolò Ribaudo 氏が発表されているので、その資料を参考にするのが良いです。
- Babel: A refactoring tool | HolyJS 2020 Moscow. Conference for JavaScript developers, November 25-28, online.
- 発表スライド
発表では React の Class Component を Function Component を書き換える例が紹介されています。合わせてリファクタリングのデモに使ったコード郡も以下のリポジトリで公開されており、ここから簡単に Babel を使ったリファクタリングを試せます。
デモを動かしてみる
まずはデモを動かしてみましょう。README に起動方法が書いてあるので、その手順通りにやれば良いです。
$ # リポジトリを clone $ git clone https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020 $ cd conf-holyjs-moscow-2020 $ # ディレクトリ構成を確認 $ ls -1F Babel_ A refactoring tool.pdf README.md codemod/ ... Babel を使ったリファクタリングツールのコード todomvc/ ... Class Component を含むリファクタリング対象のアプリケーションのコード $ # codemod/ 配下に package.json があるので、`npm ci` で依存パッケージをインストールしておく $ cd codemod $ npm ci $ # プロジェクトルートに戻る $ cd .. $ # リファクタリングツールを起動し、アプリケーションコードをリファクタリング $ node codemod/run.js todomvc/js/*.{ts,tsx}
リファクタリングツールの実行が終わると、以下のように Class Component が Function Component に書き変わっているはずです。
$ git diff diff --git a/todomvc/js/app.tsx b/todomvc/js/app.tsx index 6fc2842..ded2b9c 100644 --- a/todomvc/js/app.tsx +++ b/todomvc/js/app.tsx @@ -7,167 +7,172 @@ /// <reference path="./interfaces.d.ts"/> declare var Router; -import React from "react"; +import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; import { TodoModel } from "./todoModel"; import { TodoFooter } from "./footer"; import { TodoItem } from "./todoItem"; import { ALL_TODOS, ACTIVE_TODOS, COMPLETED_TODOS, ENTER_KEY } from "./constants"; -class TodoApp extends React.Component<IAppProps, IAppState> { +const TodoApp = props => { + const [nowShowing, setNowShowing] = useState(ALL_TODOS); + const [editing, setEditing] = useState(null); ...
リファクタリングツールの仕組み
codemod/
配下のコードを見てみると分かるのですが、リファクタリングツールの実装はcodemod/codemod.js
とcodemod/run.js
のたった 2 つの JS ファイルから構成されています。codemod/codemod.js
は実行したい変換処理を表す Babel Plugin です。ここに Class Component を Function Component に変換する処理が書かれています。一方codemod/run.js
はコマンドライン引数で渡されたファイルを1ファイルずつ AST に parse し (ステップ1)、前述のBabel Plugin を使って AST を変換して (ステップ2)、AST を文字列化して元のファイルに上書きする (ステップ3) コードになっています。
// https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020/blob/7c2b597f284f0fbafe3f823aba0e6bc2f0e46154/codemod/run.js#L20 より一部引用 function transform(source, filename, codemod) { // ステップ1 const ast = recast.parse(source, { parser: { parse(source) { return parser.parse(source, { filename, tokens: true, sourceType: "module", // typescript や jsx が扱えるように plugins: ["typescript", "classProperties", "jsx"], }); }, }, }); // ステップ2 babel.transformFromAstSync(ast, source, { code: false, cloneInputAst: false, configFile: false, // 注: ここで plugin を差し込んでいる plugins: [codemod], }); // ステップ3 return recast.print(ast).code; }
任意のリファクタリングができるように処理を差し替える
何となく気づいていると思いますが、codemod/codemod.js
の実装を変更すれば、任意のリファクタリングを実行できるようになります。先程も触れましたが、codemod/codemod.js
の実態はただの Babel Plugin なので、Babel Plugin のルールに沿ってコードを書いていけば良いです。
// @ts-check // ts-check を使って書くと、型検査が効いてお得 module.exports = function plugin(babel) { /** @type {import('@babel/core').types} */ const t = babel.types; /** @type {import('@babel/core').template} */ const template = babel.template; return { visitor: { StringLiteral(path) { // シングルクオートで囲んでいるstring literal をダブルクオートで囲むように path.node.extra.raw = path.node.extra.raw.replaceAll("'", '"'); }, }, }; };
使い所
正直なところ、単にリファクタリングしたいだけであれば JSCodeshift を使えば事足りるので、Babel をリファクタリングツールとして使う場面はそうないと思います。JSCodeshift はリファクタリングに特化したツール故に、リファクタリングに特化した utility が用意されていて、公式 example やも豊富で、使い方を解説した資料も沢山あります。一方で、Babel を使った手法では、Babel Plugin 向けに提供されている API をそのままリファクタリングに利用できたり、Babel Plugin のノウハウをそのまま利用できるという利点があります。これは Nicolò Ribaudo 氏の発表スライドでも触れられています。
特に Babel Plugin のノウハウをそのまま活かせる、というのは大きなメリットだと考えています。ちょっと Babel Plugin を書いたことのある人なら、簡単に扱えますし、codemod.js
を実装するときも既存の Babel Plugin のコードを参考にしたり、そのまま流用できます。
実際に id:mizdra は社内でとあるリファクタリングを行う際に、既存の Babel Plugin の一部コードをそのまま流用してリファクタリングをする、ということをやっていました。Babel Plugin 向けに書かれたコードを一切変更せずそのまま使えるので、(JSCodeshift 向けの codemod を新規に書くよりは) 簡単にリファクタリングを実施できました。
まとめ
今回は Babel を使ったリファクタリング手法について紹介しました。JSCodeshift という良い代替ツールがあるので、出番はそうないと思いますが、覚えておくとどこかで役に立つかもしれません。
明日は id:yutailang0119 さんです!