修正したファイルの一部だけをstageして、残りは次のコミットへ回したい。そこへ自動整形を入れると、どの差分がコミットに入るのか不安になることがあります。
huskyでコミット前にlint-stagedを呼び、JavaScriptはESLintのあとにPrettierを順番に実行します。 ただしlint-stagedが選ぶのは「stageされたファイル」です。整形が変更行だけに限定されるわけではないので、導入時にはGitのindexと作業ツリーの両方を確認します。
この記事では、JavaScriptのES Modulesを使う小さなリポジトリで設定を組み、部分的なstageとlint失敗も試します。npmの操作自体を確認したい場合は、npmコマンドとpackage.jsonの基本から読めます。
4つの道具を、コミットされるファイルの順路に置く
| 道具 | この設定での仕事 | 結果を見る場所 |
|---|---|---|
| husky | git commitの前にコマンドを起動する | .husky/pre-commit |
| lint-staged | stageされた対象ファイルを選び、処理へ渡す | lint-staged.config.js |
| ESLint | 未定義の変数などを検出し、可能なものを修正する | eslint.config.js |
| Prettier | 空白、改行、引用符などの表記をそろえる | .prettierrc.json |
ESLintで直せないエラーがあれば、コミットを止めます。整形で直せるものはPrettierへ任せ、その結果もindexへ反映されます。hookにgit add .を足して、関係ないファイルまで追加する必要はありません。
以下はNode.js 24.13.0、ESLint 10.10.0、Prettier 3.9.6、husky 9.1.7、lint-staged 17.5.1で確認した構成です。lint-staged 17.5.1のNode要件は22.22.1以上で、ここではNode 24系を使います。
リポジトリ直下へ設定をそろえる
新しい検証用ディレクトリをGitリポジトリにし、以下のファイルを配置します。package.jsonと.gitが同じ階層にある前提です。既存プロジェクトへ入れる場合は、今のscriptsやhookを読んでから必要な設定を統合し、ファイル全体を置き換えないようにします。
mkdir precommit-example
cd precommit-example
git init
package.jsonは次の形です。ツールは開発依存に固定し、実際に解決された依存関係はnpm installで作るpackage-lock.jsonに記録します。
{
"name": "next50-precommit-example",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=24"
},
"scripts": {
"prepare": "husky",
"lint": "eslint . --max-warnings 0",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint:staged": "lint-staged",
"check": "npm run lint && npm run format:check"
},
"devDependencies": {
"@eslint/js": "10.0.1",
"eslint": "10.10.0",
"eslint-config-prettier": "10.1.8",
"husky": "9.1.7",
"lint-staged": "17.5.1",
"prettier": "3.9.6"
}
}
ESLintとPrettierの担当を重ねない
eslint.config.jsでは、JavaScriptの推奨ルールのあとにPrettierと競合する表記ルールを無効化する設定を置きます。この例のコードは計算用のES Modulesです。ブラウザのdocumentやNode固有のprocessを使うプロジェクトでは、その実行環境のglobal設定も必要になります。
import js from '@eslint/js';
import prettier from 'eslint-config-prettier/flat';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{ ignores: ['dist/**', 'coverage/**'] },
{
files: ['**/*.{js,mjs}'],
extends: [js.configs.recommended, prettier],
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
},
]);
eslint-config-prettierはPrettierを実行するものではありません。Prettier公式の連携説明に沿って、linterのルールと整形の競合を減らすために使っています。整形コマンドは後で別に実行します。
.prettierrc.jsonでは、このプロジェクトでそろえる表記を決めます。
{
"singleQuote": true,
"semi": true,
"trailingComma": "all"
}
.editorconfigには、対応エディターでの文字コード・改行・インデントを指定します。Markdownの行末空白には意味があるため、その自動削除は分けています。
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
.prettierignoreでは、依存関係や生成物、ツールが生成するlockfileなどを、この例の手動整形から外します。最後のverified-results.jsonは、本記事の検証結果ファイルです。
node_modules/
dist/
coverage/
.husky/_/
package-lock.json
verified-results.json
.gitignoreも置き、node_modulesやhuskyの内部生成ファイルをコミットへ入れないようにします。
node_modules/
dist/
coverage/
.husky/_/
同じJavaScriptファイルは、配列の順に処理する
lint-staged.config.jsは次の設定です。
export default {
'*.{js,mjs}': ['eslint --fix --max-warnings 0', 'prettier --write'],
'*.{json,css,md}': 'prettier --write',
};
JavaScriptは「eslint –fix → prettier –write」の順です。JSON・CSS・MarkdownはPrettierだけへ渡します。二つのglobが同じファイルを書き換えないように分けています。
lint-stagedのタスク実行の説明では、globごとの処理は既定で並列になります。*.jsでESLintを書き、別の*でPrettierを走らせると、同じJSを書き換える処理が重なり得ます。同じファイルへの複数コマンドは、この例のように配列にまとめます。
最後に、.husky/pre-commitへ次の一行を保存します。旧版の設定を追加で貼り付けず、今回のhusky 9の構成として使います。
npm run lint:staged
動作確認用にsrc/total.jsも作ります。
export function total(unitPrice, quantity) {
return unitPrice * quantity;
}
ファイルを配置したら、依存関係を導入し、全体のチェックを実行します。npm install時にはprepareも実行されます。すでに依存関係を入れたあとでprepareを追加した場合は、npm run prepareを一度実行します。
npm install
npm run check
git config --get core.hooksPath
今回の構成ではhooksPathは.husky/_になりました。huskyの手動設定と同じく、prepareでhookの実行に必要なファイルを用意しています。共有するのは.husky/pre-commitで、生成された.husky/_の中身を手編集する運用にはしません。
実際にコミットして、成功・部分stage・失敗を比べる
設定しただけでは、hookが呼ばれることも、未完成の変更が混ざらないことも確認できません。最初のコミットがある検証用リポジトリで、次のケースを試しました。
| 試した状態 | 確認した結果 |
|---|---|
| 整形されていないJSの変更をstage | 整形後の内容でコミット成功。別の未追跡ファイルは追加されない |
| 同じファイルの一部だけstage | stageした値だけコミットされ、別の未stage変更は作業ツリーに残る |
| 未定義変数を含むJSをstage | no-undefで停止。HEAD、index、未stage差分は実行前の状態に戻る |
| 整形するとHEADと同じになる変更だけをstage | 空コミットになるため、既定の動作では停止 |
| 未stageのJSに未定義変数がある | stage対象のhookには入らず、全体のnpm run lintでは検出される |
部分stageの検証では、同じファイルにfirstとsecondという値を置きました。firstを1から2へ変えた状態をstageしたあと、作業ツリーでsecondを1から3へ変更しました。コミットに入ったのはfirst=2・second=1で、作業ツリーにはfirst=2・second=3が残りました。
これは、今回の独立した変更で確かめた結果です。lint-stagedは未stageの部分を一時的に隠し、処理後に復元しますが、formatterは対象ファイル全体を整形します。近い行を大きく書き換えると復元で競合することもあるため、「変更した行だけが必ず整形される」とは考えないようにします。
処理が止まったときは、まずログと現在の差分を見ます。
git status --short
git diff --cached
git diff
git stash list
自動復元ができなかった場合は、残されたバックアップやpatchをログで確認し、現在の作業を退避してから復旧を判断します。理由を確認せずgit reset –hardで一掃したり、stashを削除したりすると、救える差分を失うおそれがあります。--no-stashや--no-hide-partially-stagedを付ければ同じ動作になる、というわけでもありません。
手元で試す場合も、コミット前に対象を限定してstageします。次はsrc/total.jsだけを対象にする例です。
git add src/total.js
git diff --cached
git commit -m "Update total calculation"
コミット後は内容を開き、整形と意図した修正が入っているか確認します。未定義変数などのエラーで止まったら、コードを直してstageし直します。hookを飛ばすことを通常の解決方法にはしません。
導入時の一括整形と、CIでの全体チェックを分ける
既存コードの表記がばらばらな状態では、最初に触ったファイルへ大きな整形差分が出ます。まず設定だけの変更を確認し、一括整形が必要なら、機能変更を混ぜないコミットとして扱います。
そのときも、最初からリポジトリ全体へnpm run formatをかけるのではなく、生成物や管理対象を確認して対象を絞ります。例えば、合意したsrc配下だけを整えるなら、次のように指定できます。
npm exec -- prettier --write src
npm run check
git diff --stat
git diff
差分の内容が表記の変更に収まっていることと、既存のテスト・buildが通ることを確認してからコミットします。命名や処理の分割まで同時に行うと、整形の影響だけを見分けにくくなります。読みやすさそのものを改善する観点は、読みやすいJavaScriptの考え方で整理しています。
ローカルのhookは無効にすることもでき、変更していないファイルまでは検査しません。CIでは依存関係を固定して入れ、npm run checkで全体を検査します。この例のcheckはlintと整形確認なので、アプリのテストやbuildは別途既存の手順を続けます。
HUSKY=0 npm ci
npm run check
上はCIなどでhookをインストールせずに依存関係を入れるUnix系shellの例です。開発依存もインストールする前提であり、--omit=devへ変えるとhuskyやESLint自体がなくなる点は別に考えます。CIの環境変数欄でHUSKYを0へ設定する方法もあります。
GitのGUIからだけ失敗するなら、ターミナルでの成功をそのまま当てはめず、GUIが使うNodeとPATHを確認します。複数packageがあるリポジトリも、今回の「Gitとpackage.jsonが同じ階層」という条件から外れるため、hookの実行場所を調整します。
整形の議論を減らす仕組みを入れたあとも、確認する中心はコミットの差分です。どのファイルが処理され、何が自動修正され、何が理由で止まったかを読めれば、AIが書いたコードにも自分で書いたコードにも、同じ確認を適用できます。