はじめに
コミット前に eslint . や prettier --write . をプロジェクト全体に走らせて、
「自分が触っていないファイルまで大量に警告が出て収拾がつかない」という経験はないでしょうか。
チームの人数が増えるほど、この「無関係な差分」はレビューの邪魔になり、コミット前チェックそのものが
敬遠されがちになります。
Lint-Stagedは、この問題を「Gitにステージされたファイルだけ」にリンターやフォーマッターを 限定して実行することで解決するツールです。プロジェクト全体ではなく、いま変更しようとしている ファイルだけを検査するので、実行時間も指摘の件数も現実的な範囲に収まります。
Lint-Stagedとは
Lint-Stagedは、Gitのステージングエリアにあるファイル(git add されたファイル)に対して、
指定したコマンドを実行するNode.js製のツールです。Huskyなどのgitフックツールと組み合わせて
pre-commit フックに仕込むことで、「品質を満たさないコードはそもそもコミットできない」
というワークフローを構築できます。
主な特徴
- ステージ済みファイルのみが対象 - プロジェクト全体を走査しないため実行が高速で、無関係な指摘が出ない
- globパターンによる柔軟な対象指定 -
*.tsや*.{js,jsx,ts,tsx}のように拡張子・ディレクトリ単位で振り分けられる - 複数コマンドの直列・並列実行 - ESLintで検査してからPrettierで整形する、といった順序制御ができる
- 失敗時の自動バックアップ - タスクが失敗した場合は元の変更内容を自動で復元し、作業を失わない
- Huskyなどのgitフックツールと素直に連携 -
pre-commitフックからワンライナーで呼び出せる
インストール
npm、yarn、pnpmのいずれでも導入できます。
npm install --save-dev lint-staged
# または
yarn add --dev lint-staged
# または
pnpm add --save-dev lint-staged
Git フックと組み合わせる場合は、あわせて Husky も導入します。
npm install --save-dev husky
npx husky init
基本的な使い方
設定は package.json の lint-staged フィールド、または .lintstagedrc.json などの
専用ファイルに書きます。キーにglobパターン、値に実行したいコマンドを指定します。
{
"lint-staged": {
"*.js": "eslint --fix"
}
}
これで *.js にマッチするステージ済みファイルに対してのみ eslint --fix が実行されます。
拡張子ごとに複数のパターンを並べることも可能です。
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,scss}": "stylelint --fix",
"*.{js,jsx,ts,tsx,json,md,css}": "prettier --write"
}
}
これを Husky の pre-commit フックから呼び出せば、コミットのたびに自動でチェックが走ります。
# .husky/pre-commit
npx lint-staged
実践的なユースケース
Lint-Stagedは1つのパターンに対して複数のコマンドを指定でき、その並べ方によって 「順番に実行する」「並行して実行する」を使い分けられます。用途に応じたパターンを見ていきます。
複数コマンドを順番に実行する
「まずESLintで修正し、そのあとPrettierで整形する」のように、実行順序が結果に影響する場合は 配列で複数コマンドを並べます。配列内のコマンドは記述した順に直列で実行されます。
{
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write"]
}
}
先にESLintがコードの問題を修正し、その結果に対してPrettierが整形をかけるため、 両者が競合してファイルを壊す心配がありません。
複数コマンドを並列で実行する
互いに干渉しないタスク(型チェックとユニットテストなど)は、順序を気にする必要がないので 並列実行にして待ち時間を減らせます。配列をさらに配列で囲むと並列実行になります。
{
"lint-staged": {
"*.ts": [["tsc --noEmit", "eslint --fix"]]
}
}
tsc と eslint はお互いの出力に依存しないため、同時に走らせても問題は起きません。
順序に意味があるかどうかで、直列([...])と並列([[...]])を使い分けるのがポイントです。
JavaScript設定ファイルで動的にコマンドを組み立てる
対象ファイルのパスを使って動的にコマンドを組み立てたい場合は、.lintstagedrc.js のような
JavaScript設定ファイルを使います。関数を渡すと、マッチしたファイルの配列を受け取れます。
// .lintstagedrc.js
export default {
'*.{js,ts}': (files) => `eslint --max-warnings=0 ${files.join(' ')}`,
'*.md': (files) => files.map((file) => `prettier --write "${file}"`),
}
JSONでは表現しづらい「ファイル数に応じてオプションを変える」「特定ディレクトリだけ除外する」 といったロジックを、関数の中に自由に書けるのがLint-Stagedの設定ファイルの強みです。
CIでステージ外の差分をまとめてチェックする
Lint-Stagedはローカルのpre-commitフック専用というイメージがありますが、--diff オプションを
使えばステージングエリアに関係なく、任意のブランチ間の差分ファイルに対して実行できます。
プルリクエストのCIで「変更されたファイルだけ」を検査したいときに使えます。
npx lint-staged --diff="origin/main...HEAD"
--diff を指定すると通常の「ステージ済みファイル」判定の代わりに、指定した2点間のdiffに
含まれるファイルが対象になります。CI環境でも同じ設定ファイルをそのまま流用できるのが利点です。
まとめ
Lint-Stagedは「変更したファイルだけを検査する」というシンプルな発想で、コミット前チェックの 実行時間と指摘件数を現実的な範囲に抑えてくれるツールです。globパターンによる振り分け、 直列・並列の実行制御、JavaScript設定ファイルによる動的な組み立てまで対応しており、 小さなプロジェクトから大規模なモノレポまで無理なく組み込めます。まずは1つのglobパターンと 1つのコマンドから始めて、チームの運用に合わせて少しずつルールを育てていくとよいでしょう。
