Next.js モノレポで ESLint / Prettier / husky を組み、コミット前に品質を固める

目次
フォーマットや Lint は「気をつける」より「コミット時に自動で直す・止める」方が確実です。この記事では、frontend/ に Next.js アプリを置いたモノレポ構成で、git ルートの husky から frontend の ESLint / Prettier を lint-staged 経由で動かす実際の設定をまとめます。console.log・any・未使用変数/import の禁止、import 整列、Tailwind クラス整列まで、コミット前に自動で効かせます。
前提スタックは Next.js 16(ESLint flat config)/ TypeScript / Tailwind CSS v4 / pnpm です。
構成の全体像
ポイントは git のルートとパッケージの場所がずれていることです。
sample-app/ ← git ルート(.git はここ)
├── package.json ← husky + lint-staged だけを置く
├── .husky/pre-commit
├── lint-staged.config.mjs
└── frontend/ ← Next.js 本体(ESLint / Prettier はここ)
├── eslint.config.mjs
└── .prettierrc.jsonhusky は git フックの仕組みなので git ルート基準で動きます。一方 ESLint / Prettier の設定はアプリのある frontend/ に置きたい。そこで「husky はルート、実際の Lint はルートから frontend/ に委譲する」形にします。
husky をルートに入れる
ルートに最小の package.json を用意し、husky と lint-staged を入れます。
# リポジトリのルートで
pnpm add -D husky lint-staged
pnpm exec husky inithusky init で .husky/ が作られ、ルートの package.json に "prepare": "husky" が追加されます。この prepare により pnpm install のたびにフックが自動セットアップされ、クローン直後の別メンバーの環境でも有効になります。
husky が実際にやっているのは、git の探索先をこう切り替えているだけです。
git config core.hooksPath .husky/_.husky/pre-commit は自分で書き換えて、lint-staged を呼ぶだけにします。
# .husky/pre-commit
pnpm exec lint-stagedlint-staged からサブディレクトリの ESLint を呼ぶ
ここが一番のコツです。ESLint の flat config は カレントディレクトリを基準に設定を解決します。ルートから eslint を叩くと frontend/eslint.config.mjs を見つけられません。そこで pnpm --dir frontend exec で cwd を frontend/ に寄せて実行します。
// lint-staged.config.mjs(リポジトリのルート)
export default {
// JS/TS: ESLint(品質チェック+自動修正)→ Prettier(整形)の順に実行。
// --max-warnings=0: Next の ESLint 設定は多くを warning 扱いにするため、
// warning もブロック対象にしてフックを実効的にする。
"frontend/**/*.{js,jsx,ts,tsx,mjs,cjs}": [
"pnpm --dir frontend exec eslint --fix --max-warnings=0",
"pnpm --dir frontend exec prettier --write",
],
// JS/TS 以外(整形のみ Prettier で実施)。
"frontend/**/*.{json,css,scss,md,mdx}": "pnpm --dir frontend exec prettier --write",
};lint-staged はマッチしたファイルの絶対パスをコマンドへ渡します。cwd が frontend/ でも、絶対パスは cwd 配下を指すので ESLint は設定を正しく解決できます。
--max-warnings=0を付けているのは、Next の ESLint プリセットが多くのルールをwarningにしており、ESLint は warning だけなら exit 0 になるためです。これを付けないと「警告は出るがコミットは通る」状態になります。
ESLint:禁止したいものを error にする
「見た目の統一」は Prettier、「禁止・エラーにしたいルール」は ESLint、と役割を分けます。flat config に独自ルールを足します。
// frontend/eslint.config.mjs
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier/flat";
import simpleImportSort from "eslint-plugin-simple-import-sort";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// プロジェクト独自のコード品質ルール
{
plugins: { "simple-import-sort": simpleImportSort },
rules: {
// import / export の順序を自動整列(--fix で自動修正される)
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
// console.log を禁止(デバッグ用途の console.warn / console.error は許可)
"no-console": ["error", { allow: ["warn", "error"] }],
// any 禁止
"@typescript-eslint/no-explicit-any": "error",
// 未使用の変数・import をエラーに(TS では未使用 import も未使用変数として検出される)。
// _ で始まる引数だけは意図的な未使用として許可。
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
// Prettier と衝突する「整形系」ルールを無効化し、整形は Prettier に一任する。
// 他の設定より後(末尾付近)に置くこと。
prettier,
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
]);
export default eslintConfig;意図はこうです。
no-console:console.logは禁止。ただしデバッグに使うconsole.warn/console.errorは許可。@typescript-eslint/no-explicit-any:anyを禁止し、型を書かせる。@typescript-eslint/no-unused-vars: 未使用の変数・import をエラーに。TypeScript では未使用 import も「未使用の変数束縛」として検出されるので、これ 1 つで import と変数の両方を止められます。_始まりだけは意図的な未使用として許可。simple-import-sort: import / export の順序をルール化。--fixで自動整列されるので、手で並べ替える必要がありません。
追加パッケージはこれだけです。
pnpm --dir frontend add -D eslint-config-prettier eslint-plugin-simple-import-sorteslint-config-prettier は必ず最後
ESLint にも文字幅やクォートなど整形系のルールがあり、そのままだと Prettier と喧嘩します。eslint-config-prettier はそれらを無効化するだけの設定なので、配列の末尾(他ルールより後)に置いて上書きさせます。flat config では eslint-config-prettier/flat を import して 1 要素として並べます。
Prettier:整形ルール+Tailwind クラス整列
Prettier 側は整形ルールと、Tailwind クラスの並べ替えプラグインを設定します。
// frontend/.prettierrc.json
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./app/globals.css"
}pnpm --dir frontend add -D prettier prettier-plugin-tailwindcssprettier-plugin-tailwindcss は className の中身を公式推奨順に並べ替えます。Tailwind v4 は設定が CSS ベース(@import "tailwindcss"; を書いた CSS がエントリ)になったため、tailwindConfig ではなく tailwindStylesheet でそのエントリ CSS を指定します。ここでは app/globals.css です。
実際にこう並び替わります。
// before
<div className="p-4 flex text-white bg-blue-500 items-center">
// after(レイアウト → ボックス → 余白 → 文字 の推奨順)
<div className="flex items-center bg-blue-500 p-4 text-white">整形対象外は .prettierignore に書いておきます。
# frontend/.prettierignore
.next/
out/
build/
node_modules/
pnpm-lock.yaml
next-env.d.tsコミット時の挙動
これで git commit するとステージ済みファイルにだけ処理が走り、次のように分かれます。
| ケース | 結果 |
|---|---|
| 整形の乱れ・import 順・Tailwind 並び | 自動修正 → 自動で再ステージ → コミット成立 |
console.log / any / 未使用変数・import |
コミット中止(自分で直して再コミット) |
--fix と --write で直せるものは lint-staged が直して git add し直してくれるので、手作業は不要です。直せない違反(any など)はコミットが止まります。
lint-staged は処理前にステージしていない変更を一旦 stash で退避し、ステージした差分だけを対象に整形・Lint します。関係ない変更まで巻き込んで add してしまう心配はありません。
ハマりどころまとめ
- flat config の解決はカレント基準。git ルートとパッケージがずれるなら
pnpm --dir <pkg> exec eslintで cwd を寄せる。 eslint-config-prettierは末尾。前に置くと整形系ルールが復活して Prettier と衝突する。--max-warnings=0。Next の ESLint 設定は多くが warning なので、これがないとコミットが素通りする。- Tailwind v4 は
tailwindStylesheet。tailwindConfigではなく CSS エントリを指す。 - どうしてもフックを飛ばしたい時は
git commit --no-verify。ただし常用しない。 tscのようなファイル単位で動かない型チェックは lint-staged と相性が悪い。型チェックは pre-push か CI に置くのが無難。
まとめ
husky(ルート)→ lint-staged →(frontend/ に委譲した)ESLint / Prettier という 3 段で、コミット前に品質を固められます。禁止ルールは ESLint、整形は Prettier に分担させ、直せるものは自動修正・再ステージ、直せない違反はコミットを止める。フォーマット論争と Lint 漏れをコミット時点で片付けて、レビューを本質的な議論に集中させましょう。


