Turborepoを活用したモノレポ環境のpre-commitフックの効率化

はじめに

こんにちは。イベント・エンタメ事業開発部の kaorun343 です。本記事では、pre-commit フックで実行するタスクを lint-staged から Turborepo に移行した経緯と効果について説明します。

背景

私たち座席指定チームでは、toC 向けのアプリケーション、toB 向けのアプリケーション、そして共通コンポーネントパッケージを開発しています。これらは yarn workspaces を用いたモノレポ環境で開発しています。

workspace-root/
  packages/
    toC 向けアプリケーション
    toB 向けアプリケーション
    共通コンポーネント

このモノレポに対して変更を加えた場合、pre-commit フックにおいて lint-staged を用いて各種静的検査や単体テストを実施していました。

しかしながら lint-staged による pre-commit フック運用では、以下のような課題がありました。

ワークスペース同士の依存関係を考慮しないタスク実行

ステージングファイルのみをチェックする仕組みでは、依存関係の変更による影響を検知しきれない場合がありました。

例えば、共通コンポーネントパッケージのみを変更した場合を考えてみます。この変更が、共通コンポーネントパッケージに依存している toC、toB 向けのアプリケーションで型エラーを起こしたり、単体テストが落ちる原因となったりしても、lint-staged を基盤とした pre-commit フックでは検知することができませんでした。

設定の手間

現状は少ない数のワークスペースで構成されていますが、今後ワークスペースが増加したときに、都度 lint-staged の設定ファイルを更新していく必要があります。ワークスペース追加のコストとなることを懸念していました。

実行速度の問題

lint-staged はタスクを直列実行するため、効率的なタスク実行ができていない点に課題を感じていました。

Turborepo への移行

Turborepo とは

Turborepo は Vercel が開発しているモノレポ向けの高性能なビルドシステムです。複数のパッケージやアプリケーションを含むモノレポにおいて、タスクの並列実行、増分ビルド、およびキャッシュ機構を通じて開発体験を向上させることを目的としています。

主な特徴は以下の通りです。

  • 並列実行: 複数のタスクを同時に実行し、ビルド時間を短縮
  • 増分ビルド: 変更されたファイルに関連するタスクのみを実行
  • 依存関係の認識: パッケージ間の依存関係を理解し、適切な順序でタスクを実行
  • シンプルな設定: JSON 形式の設定ファイルで簡単にセットアップ可能

これらの特徴が、前述の lint-staged を用いた pre-commit フックの課題を解決できると判断し、移行を決めました。

並列実行による効率的なタスク実行

Turborepo は並列実行が可能なため、pre-commit フック内での各種タスクを効率的に実行できます。

ワークスペース同士の依存関係を考慮したタスク実行

Turborepo の増分ビルドの仕組みにより、依存関係を考慮したタスク実行が可能になりました。

これにより、共通コンポーネントの変更が各アプリケーションに与える影響を速やかに検知できるようになりました。

各種ツール側の対応

特に設定をしないとワークスペース内の全てのファイルに対して各種ツールの検査が走ります。これでは pre-commit の実行時間が増えてしまうので、各種ツールの実行方法を調整しました。

Vitest

Vitest では --changed オプションを使用することで、Git の変更差分に基づいてテストファイルを選択的に実行できます。このオプションを使うことで、変更されたファイルに関連するテストのみが実行され、全体のテスト実行時間を大幅に短縮できます。

vitest --changed

このコマンドは以下のような動作をします:

  • Git の変更履歴を参照し、前回のコミットから変更されたファイルを特定
  • 変更されたファイルに関連するテストファイルを自動的に判断
  • 該当するテストのみを実行

ただし、依存関係やテスト設定に影響を与える重要なファイル(package.jsonvite.config.js など)が変更された場合は、全てのテストを実行します。これにより、設定変更による影響を確実に検知できるようになっています。

ESLint

ESLint についても、変更があったファイルのみを検査対象とするように設定しました。 Vitest とは異なり、最新コミットとの差分ではなく、前回実行時から内容に差分があるファイルについて検査するようにしました。ESLint は明示的に差分検知戦略を指定する必要があります。ESLint のデフォルト設定は、保存日時に変更があるかどうかを調べるためです。

キャッシュファイルの場所は https://github.com/sindresorhus/find-cache-directory に倣って node_modules/.cache/eslint としました。

また、パフォーマンスの測定と最適化のため、TIMING=1 環境変数を設定しています。これにより ESLint の各ルールの実行時間が表示され、ボトルネックの特定に役立ちます。今後の改善に生かすために設定しました。

TIMING=1 eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint ./src

Prettier

Prettier についても同様に、前回実行時から内容に差分があるファイルについて検査するようにしました。Prettier はデフォルト設定で内容に差分が無いか確認する戦略で、キャッシュファイルが node_modules/.cache/prettier に保存されるため、特別な設定は不要です(ESLint の設定を Prettier のデフォルト設定に合わせました)。

加えて、フォーマット済みかどうかを確認するために --list-different オプションも使用しています。差分があったファイルを表示するために付与しています。

prettier . --cache --list-different

タスク設計

pre-commit フックで呼び出すための、Turborepo のタスクを設計しました。

npm スクリプトの定義

まず、各ワークスペースの package.json に pre-commit フック用のスクリプトを追加しました。また、lint-staged ではファイルを自動修正する機能を利用できていましたが、Turborepo への移行によりこの機能を実現できなくなったため、フォーマット用のスクリプトも別途追加しました。

{
  "scripts": {
    "format:fixpack": "fixpack",
    "lint:eslint": "TIMING=1 eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint ./src",
    "lint:prettier": "prettier . --cache --list-different",
    "test:pre-commit": "vitest run --changed",
    "ts-compile-check": "tsc -p tsconfig.json --noEmit"
  }
}

fixpack は package.json ファイルのプロパティを決められた順序に並べ替えるツールです。一貫したファイル構造を保つことで、レビュー時の差分を見やすくし、チーム全体でのコード品質向上を目的として導入しています。fixpack には検査のみを実行するオプションがないため、ファイルの書き換えも実行してしまいますが、フォーマットされていない場合は exit 1 を返すため、pre-commit フックでも実行しています。

TypeScript については特に最適化の設定をしていないので、すでにあるスクリプトを pre-commit フックに用います。

pre-commit 用タスクの定義

次に、Turborepo の設定ファイル turbo.json で各タスクの依存関係を定義しました。

Turborepo では、tasks オブジェクト内でタスク名をキーとして、各タスクの設定を記述します。dependsOn プロパティでタスク間の依存関係を定義することで、依存するタスクが完了してから次のタスクが実行されるよう制御できます。また、依存関係のないタスクは自動的に並列実行されます。

今回は pre-commit タスクが以下の検査タスクに依存するよう設定しました。

  • format:fixpack: package.json の構造チェック
  • lint:eslint: ESLint による静的解析
  • lint:prettier: Prettier によるフォーマットチェック
  • test:pre-commit: Vitest による単体テスト
  • ts-compile-check: TypeScript の型チェック

この設定により、yarn turbo run pre-commit を実行すると、これらの検査タスクが各ワークスペースで並列実行されます。

また、それぞれ以下のような設定内容になっています。

  • "lint:eslint": {} のように中身が空のタスクは、各ワークスペースの package.json に定義された同名のスクリプトをそのまま実行します。
  • dependsOn を持つタスクは、 "pre-commit" タスクのように配列で依存関係を定義すると、リストされたタスクがすべて完了してから実行されます
  • inputs プロパティを使って "format:fixpack" のように特定のファイルを入力として指定すると、そのファイルが変更された時のみタスクが実行されます(キャッシュの最適化)
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "format": {
      "dependsOn": ["format:eslint", "format:fixpack", "format:prettier"]
    },
    "format:eslint": {},
    "format:fixpack": {
      "inputs": ["package.json"]
    },
    "format:prettier": {},
    "lint:eslint": {},
    "lint:prettier": {},
    "pre-commit": {
      "dependsOn": [
        "format:fixpack",
        "lint:eslint",
        "lint:prettier",
        "test:pre-commit",
        "ts-compile-check"
      ]
    },
    "test:pre-commit": {},
    "ts-compile-check": {}
  }
}

pre-commit フックの実装

pre-commit フックの中で、以下のように pre-commit タスクを呼び出すことで、各種ツールを実行できるようになりました。

yarn install
yarn turbo run pre-commit

ワークスペースルートの package.json に対する fixpack の適用方法

yarn workspaces を使用したモノレポ環境では、fixpack がそれぞれのワークスペースの package.json のみを対象としてしまい、ルートの package.json が処理されません。そのため --single-package オプションを付与することで、ワークスペースルートに対しても適用できるようにしました。

yarn turbo run lint:fixpack --single-package

結果

Turborepo の導入により、依存パッケージの変更による影響をより確実に検知できるようになり、コード品質の向上を実現しました。依存パッケージによって生じる各種エラーを確実に検出できるため、安定した開発環境を維持できています。

さらに、Turborepo 経由でpre-commitフックの前に各種タスク(ESLint、Prettier、TypeScript など)を実行しておけば、pre-commit フック内ではタスクの実行がスキップされ、フック処理を効率化できます。すべてのタスクが実行済みの場合、pre-commit フックは即座に完了します。

今後の展望として、pre-commit フックに留まらず、CI/CD パイプラインにも Turborepo を活用し、より効率的な開発・デリバリープロセスの構築を進める予定です。

最後に

本記事では、lint-staged から Turborepo への移行について説明しました。

アソビュー株式会社では新しいメンバーを随時募集していますので、ご興味ある方はぜひご連絡ください。