こんにちは。ソフトウェアエンジニアの坂井 (@manabusakai) です。
カミナシは Web ブラウザ向けに React、ネイティブアプリ向けに React Native を採用しています。 2023/02/10 時点で ts, tsx のファイルは 1,468 個ありました。
一定規模を超えたプロジェクトということもあり、Prettier や Organize imports が適用されているファイルとされていないファイルが混在していました(Organize imports は未使用の import を削除したり、既存の import をファイルパスでソートする機能のことで、TypeScript 2.8 から使えるようになりました)。
Pull request を作るときはコードフォーマッターの差分だけコミットを分けたり、少しの変更であれば他のコミットに混ぜてしまっているケースも見受けられました。
レビュアーは本質的な変更と見分ける必要があり、レビューコストを無駄に高めている状態でした。エンジニアリング組織が大きくなるに連れてじわじわと開発生産性を落としていくのは目に見えていたので、そうなる前にカッとなって全ファイルに Prettier と Organize imports を適用することにしました。
先にお伝えしておくと、いろいろ試行錯誤しましたが、最後に書いてある Prettier のプラグインを入れればすべて解決します🙃
Organize imports を一括で適用する
Prettier を再帰的に実行するのは簡単です。カミナシでは yarn format
で実行できるようにしています。
prettier --config .prettierrc.yaml --write '**/*.{ts,tsx}'
問題は Organize imports の方でした。
最初は VS Code の “Folder source actions” という拡張機能を試してみましたが、ファイル数が多いと途中で止まってしまいすべてに適用されませんでした(リポジトリを見ると同じような Issue が報告されていました)。
コマンドラインで実行する方法はないかと思って調べてみると、“organize-imports-cli” というドンピシャの npm パッケージを発見しました。この npm を使えばコマンドラインから Organize imports を適用できたのですが、引数にファイルしか渡せないため一括で適用したい場合にはミスマッチでした。
そこで organize-imports-cli のソースコードを読んでみると、ts-morph という npm パッケージの機能で実現していることがわかりました。
TypeScript Compiler API wrapper. Provides an easier way to programmatically navigate and manipulate TypeScript and JavaScript code.
ts-morph は TypeScript の AST (Abstract Syntax Tree) を簡単に触れるようにしたラッパーで、Organize imports に関しては organizeImports()
というズバリのメソッドが用意されています。
余談ですが、Organize imports は TypeScript の言語仕様として実装されているんですね *1。
スクリプトを書く
今回は ts-morph を使って一括で Organize imports を適用するスクリプトを書いてみました。
まず適用したいファイルのリストを find コマンドで作成します。
$ find packages/web/src -name "*.ts" -or -name "*.tsx" -type f > ts_files.txt
次に以下のコードを organize-imports.mjs
として保存します。
import fs from "fs"; import readline from "readline"; import tsm from "ts-morph"; (async () => { const project = new tsm.Project(); const fileList = process.argv[2]; const stream = fs.createReadStream(fileList); const reader = readline.createInterface({ input: stream }); const cwd = process.cwd(); for await (const path of reader) { project.addSourceFilesAtPaths(cwd + "/" + path); } const sourceFiles = project.getSourceFiles(); sourceFiles.forEach(sourceFile => { sourceFile.organizeImports(); }); project.saveSync(); })();
あとは、コマンドライン引数にさっきのリストを渡して実行するだけです。
$ node organize_imports.mjs ts_files.txt
ファイル数が多いとそこそこメモリを使いますが、1 分弱ですべてのファイルに Organize imports を適用することができました 🙌
ここまで差分の大きい PR は初めて作った🙃 pic.twitter.com/nNwsSKcsG3
— Manabu Sakai (@manabusakai) 2023年2月3日
Prettier のプラグインがある…!
ここまでやったあとに気づいたのですが、”prettier-plugin-organize-imports” という Prettier のプラグインの存在を知りました…!
このプラグインをインストールすると Prettier によって自動的に読み込まれます。設定も不要なので、最初に書いた yarn format
を実行するだけで一緒に適用できるようになりました。
最初からプラグインを使っていればあっという間に終わったのですが、回り道したことでいろいろな知見を得られたので結果オーライです。
このあと、チームメイトの方がチェックするステップを CI に組み込んでくれたので、今後は治安の良い状態を維持できそうです。
最後に宣伝です 📣 カミナシでは絶賛採用中です! 一緒に強いチームを作っていく仲間を募集しています!