「asa1984 Advent Calendar 2024」7日目の記事です。
今日は Turborepo の機能とユースケースについて説明していきます。
Turborepo は JavaScript/TypeScript のモノレポをいい感じに管理するツールです。モノレポとは、普通は別々のリポジトリに分離するようなプロジェクトを1つのリポジトリにまとめて管理する手法で、 JS/TS の場合だと1リポジトリに package.json
で管理された複数のパッケージが配置されている感じです。
モノレポ内のパッケージは相互に依存することができ、パッケージ単位で開発スコープを分離できる上、1リポジトリにまとまっているのでコロケーション的な意味でスピーディーに開発を進められます。npm, yarn, pnpm, Bun には標準でモノレポを実現する機能が搭載されており、子パッケージを「ワークスペース」と呼称して管理できます。
しかし、モノレポで開発を行おうとすると次のような面倒なことが発生します。
- Lint や型検査といった共通のタスクをワークスペースごとにいちいち手動で実行しないといけない
- ビルドの際、パッケージ間の依存関係を考慮して手続き的に実行しないといけない
- ある変更がどのワークスペースに影響を与えるか判断するのが難しく、無駄な静的検査やビルドが発生する
これらをいい感じに管理しようというのがモノレポ管理ツールです。Tureborepo は比較的新しいツールで、Vercel が開発しています。Rust で実装されており、自動で依存関係を追跡するタスク管理機能とキャッシュ機能、そして複数のタスクをいい感じに表示してくれる TUI が特徴です。
Turborepo は 各パッケージマネージャのワークスペース機能に対応しており、それらをラップする形でタスクを実行してくれます。
turbo.json
というファイルでタスクを管理します。次のような turbo.json
があったとします。
{
"tasks": {
"gen": {},
"typecheck": {
"dependsOn": ["gen"]
},
"build": {
"dependsOn": ["gen", "^build"]
},
"dev": {
"dependsOn": ["gen"],
"cache": false,
"persistent": true
}
}
}
コード生成、型検査、ビルド、開発サーバーの起動という典型的なタスクです。
turbo.json
の tasks
はちょうど package.json
の scripts
に対応します。この状態で turbo run gen
を実行すると、package.json
に gen
を持つワークスペース全てで 並列に スクリプトが実行されます。
typecheck タスクは gen タスクでコード生成を行った後に実行しなければいけません。そういう時は dependsOn
でタスクを他のタスクに依存させます。今回の例の場合は、typecheck タスクは gen タスク終了後に実行されます。
build タスクの dependsOn
には ^build
という先頭に記号のついたものが指定されています。これは、各ワークスペースのスクリプトを 直列 に実行するというモノです。ワークスペース間の依存関係に従って、ワークスペースごとに build
スクリプトを逐次実行します。パッケージ A のビルド成果物に パッケージ B が依存している、というようなケースで使用します。Turborepo は package.json
の dependencies
/ devDependencies
を解析して自動で依存関係グラフを作ってくれるので、開発者が実行順序を指定する必要はありません。
dev タスクは、自分では終了しない他とは性質の異なるタスクです。まず、実行結果をキャッシュする必要がない(できない)ので、cache
を false にして後述のキャッシュ機能をオフにします。また、persistant
を true にしてタスクを終了せずにずっと実行状態のままでいることを許可します。
Turborepo はデフォルトでタスクの実行結果をキャッシュします。前述の例の gen タスクを2回実行すると、2回目にはキャッシュされた前回の実行結果(標準出力の内容)を出力して終了します。ソースコードを変更すると Turborepo は自動的にキャッシュを破棄し、それに依存しているワークスペースのキャッシュも同様に破棄されます。
デフォルトのキャッシュは最低限のキャッシュしか行わないので、開発者が厳密な条件を指定してより強いキャッシュを実現する、というのが Turborepo の基本的な使い方になります。
詳細は公式ドキュメントに任せますが、ここでは一番つまづきやすい環境変数の扱い方について解説します。
Tureborepo を初めて使う人や Turborepo 1.x を使っていた人が特に詰まりやすいのが環境変数の扱いです。Turborepo 2.x は、デフォルト設定だと環境変数を strict mode で扱います。
strict mode は、タスクの env
に明示的に指定された環境変数以外をスクリプトの実行環境に渡さないというモードです。env
で指定された環境変数はキャッシュを識別するハッシュの算出に利用されるので、Turborepo の自動キャッシュをより安全に利用することができます。
当然ながら env
に環境変数を指定してやらないとタスクがこけるので注意してください。
globalEnv
に環境変数を指定すると全タスクでその環境変数を利用することができますが、環境変数が変わると問答無用で全タスクのキャッシュが無効になるので、できる限り使用は避けましょう。
タスクの実行結果に影響しない環境変数に関しては、タスクの passThroughEnv
に渡すか、globalPassThroughEnv
に指定して全タスクから利用できるようにしましょう。ここで指定された環境変数の中身が変わってもキャッシュは有効なまま残ります。
筆者は wrangler の認証に用いる ID とトークンを globalPassThrough
に指定して運用しています。
"ui": "tui"
を指定するとタスクのログを TUI で眺めることができます。並列実行しているタスクの確認や複数の開発サーバーのログの確認に非常に便利です。