Prettier、ESLint、Stylelint は強力なツールです。組み合わせてしっかり設定しておくと、自分とチーム全員のコーディング速度と効率が大きく上がり、コード全体の品質も向上、バグが減り、コードレビューも速くなるため、チームが本当に重要なことに集中できるようになります。
多くのチームは ESLint をコミット時、あるいは CI アクション内でしか走らせていません。それでも問題は拾えますが、ESLint が提供する最も役立つ自動化、つまり「保存時の修正」を取りこぼしています。
この記事の内容
- プロジェクトで ESLint の保存時の修正を使うべき理由
- 入れるべきルールの種類
- プロジェクトへの追加方法
- 推奨するルールセット一式の入手先
Prettier の哲学を受け入れる
Prettier の哲学 をまだ読んでいなければ、ぜひ一度目を通してみてください。
Prettier は保存時に修正するツールで、ファイルを保存するとフォーマットルールが自動で適用されます。
副次的な利点として、Prettier はコードのどこかに構文エラーがあるかを知る手段にもなります。保存しても期待どおりにフォーマットされない場合は、余分な文字や閉じ括弧・タグの欠落など、構文エラーが潜んでいるということ。逆に予想外のフォーマットになった場合は、閉じ括弧やタグの位置がずれている可能性があります。
ESLint の保存時の修正とは
多くの ESLint ルールは --fix flag を受け付けており、可能な範囲でコードを自動的にリファクタリングしてルールに沿わせてくれます。
Prettier と同じく、コードはリアルタイムで自動修正され、修正できないものは即座に通知されます。
JetBrains IDE (WebStorm など)
- 設定を開く
(⌘+,)、または Windows ではCtrl + Alt + S - 言語とフレームワーク > コード品質ツール > ESLint を選択
- 「eslint --fix を実行」の横のチェックボックスをオンにする
VS Code
VS Code で ESLint 拡張機能 を使っている場合、settings.json ファイルを開きます。
⌘+Shift+P、または Windows ではCtrl + Shift + Pを押す- 「ユーザー設定 (JSON) を開く」と入力
- 次のコードを
settings.jsonに追加
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
}
}Vim
Vim で ALE Vim プラグイン を使っている場合、次のコードを .vimrc に追加します。
let g:ale_fix_on_save = 1Stylelint とは
Stylelint は Prettier や ESLint と同じことを CSS に対してやってくれるツールです。
ESLint の保存時の修正の利点
人生の多くのことと同様、ESLint の保存時の修正の利点は説明されるより使ってみたほうが早く実感できます。とはいえ、特に大きいものをいくつか挙げておきます。
- 問題を即座に防ぐ
- 高品質なコードを自動的に強制する
- コーディング中の不要な意思決定が減り、集中力が上がる
- コードレビューでのコードスタイル議論が減るか、なくなる
- 一貫したコードのおかげで、プロジェクトでの共同作業がやりやすくなる
- フォーマットやルールを気にせずコードを書ける
- ファイルの保存が、書いた内容を「整理」する強力なツールになる
- 時間が経つにつれ、ESLint に直してもらわなくてもルールに沿った高品質なコードを誰もが自然に書くようになる
- 経験の浅い開発者にとっては、より良いコードの書き方を学ぶための教師が組み込みで付いてくる
ルールの選択
Prettier の哲学にあるとおり、
人はコードの書き方の細部にとても感情的になる…… Prettier がすべてのコードを 100% 望みどおりにフォーマットしてくれなかったとしても、その「犠牲」を払う価値がある
Prettier のカスタマイズは意図的に絞り込まれています。一方で ESLint には大量のルールがあり、それに比例して大量の意見も存在します。
プロジェクトのニーズやチームの好みに合わせてルールをカスタマイズできる一方で、強固な土台を作る標準的なルールもいくつかあります。
何より大事なのは、ルールセットがありがちな(そしてあまりない)問題を回避してくれること、そして開発者の負担を減らすために保存時の修正ルールをできる限り多く備えていることです。
土台を整えたあとは、どちらに振っても構わないルールがいくつか出てきます。好みの問題に近いものもあり、どう決めるかは企業文化次第ですが、これらをスキップして各自の判断に委ねるのはやめてください。混乱を招くだけ。決断を下す「犠牲」を払う価値はあり、特にルールが --fix に対応している場合はなおさらです。
推奨事項
もうプロジェクトごとにルールセットを手で組み立てることはしていません。推奨するルール一式は、バージョン管理された npm パッケージ @gaia-react/lint として公開済み。その中身と自分のプロジェクトに合わせて調整する方法は、本稿末尾の「推奨されるルール」セクションで扱います。
"warning" を使わない
ESLint のルールは "error" か "off" のどちらかであるべきです。
強制されない限り、ルールはルールではありません。断固とした決断を下してください。
コミット時に実行
保存時の修正に加えて、コミット時にも ESLint を自動で走らせるべきです。明示的に開いていないファイルにリファクタリングの影響で変更が入った場合など、こうした問題はここで拾えます。
TypeScript を使っているなら、tsc もコミット時に走らせておきましょう。ESLint では拾えない TypeScript の問題を検出できます。
ESLint や TypeScript のエラーがあるとコミットが拒否される、というのは決定的に重要です。これにより不注意なミスやバグが防がれ、無駄が減ります。誰しも人間で、一つのコーディングセッションの中ですら何かを忘れることはあるもの。コミット時に ESLint や TypeScript の違反を直すのは一瞬の作業です。
問題のあるコードがコミットされるのを止めなければ、見つけて直す負担は他のメンバーに回り、最悪の場合は見逃されて本番に流れていきます。
ESLint の --max-warnings=0 フラグがこれを自動で処理してくれます。
コミット時に走らせる最も簡単で一般的な方法は、husky 経由で lint-staged を使うことです。
lint-staged
変更のあったファイルだけに linter を走らせることで、時間を節約できます。
我々が使っている .lintstagedrc ファイルがこちら。
{
"{src,.storybook}/**/*.{ts,tsx}": [
"eslint --fix --max-warnings=0 --cache --cache-location ./node_modules/.cache/eslint .",
"prettier --write"
],
"src/**/*.css": [
"stylelint --fix",
"prettier --write"
],
"src/**/*.json": [
"eslint --cache --fix",
"prettier --write"
]
}
React Router 7 プロジェクトでは、src を app に置き換えてください。
husky
husky は、コードに変更があった場合だけ実行するように設定できます。
我々はテストハーネスもコミット時に走らせています。lint-staged とテストは変更されたファイルのみに絞れる一方で、tsc はプロジェクト全体に対して走るため、関連ファイルが変わっていないコミット(例えばドキュメントの変更など)では時間を節約できます。
husky の pre-commit 設定はこちら。
#!/usr/bin/env sh
declare HAS_SRC_CHANGED=$(git diff --cached --name-only --diff-filter=ACDM | grep 'src/')
declare HAS_TEST_CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep 'test/')
if [[ $HAS_SRC_CHANGED || $HAS_TEST_CHANGED ]]
then
npm run typecheck
npx lint-staged
npm run test:lint-staged
else
echo "\n✅ No changed files -- skipping lint-staged\n"
fi
React Router 7 プロジェクトでは、src/ を app/ に置き換えてください。
package.json
関連する package.json スクリプトの例がこちら。
{
...
"scripts": {
"lint": "eslint --fix --max-warnings=0 --ext js,jsx,ts,tsx src --cache --cache-location ./node_modules/.cache/eslint .",
"lint-all": "npm run typecheck && npm run lint && npm run format && npm run stylelint",
"format": "prettier --ignore-path .prettierignore --write \"**/*.{ts,tsx,html,md,mdx,css,json}\"",
"stylelint": "stylelint --fix src/**/*.css",
"typecheck": "tsc",
"test": "vitest",
"test:ci": "vitest --run --passWithNoTests --coverage --bail 1",
"test:lint-staged": "vitest --run --changed --passWithNoTests --bail 1"
},
...
}React Router 7 プロジェクトでは、src を app に置き換えてください。
必要に応じて手動で npm run できるスクリプトを含めてあります。
lintはすべてのファイルに対して ESLint を実行lint-allはすべてのファイルに対してすべて(tsc、Prettier、ESLint、Stylelint)を実行formatはすべてのファイルに対して Prettier を実行(必要に応じて対象ファイル種を変更してください)stylelintはすべてのファイルに対して Stylelint を実行
既存のプロジェクトに導入する
ルールセットに合意し、すべて設定が済んだ段階で、多くのコード変更が必要になるはずです。
朗報は、変更の大半は自動で行われ、手動で直す必要があるのはごく一部だけということ。
悪い知らせは、こうした変更をコミットするとオープンブランチとのコンフリクトが発生し、解消が面倒になりがちなことです。
これに対処する方法は二つ。
- すべてのオープンブランチをマージするタイミングを決めておき、ESLint の変更を一つの PR にまとめ、できれば一日で片づけ切る。最もシニアな開発者にリードを任せ、他のメンバーは画面共有で見学するか、計画立て、ハッカソン、その他のチームビルディング演習などのコーディング以外のタスクに回ってもらう。
- ファイル単位で段階的に導入していく。
段階的な導入
手順は次のとおり。
- eslint-plugin-eslint-comments がインストール済みで、次のルールが有効になっていることを確認してください:
'eslint-comments/no-unused-disable': 'error'。 - ESLint や lint-staged の保存時の修正は(まだ)有効にしない
- Node スクリプト(下記参照)を使ってプロジェクト内のすべてのファイル先頭に
/* eslint-disable */を挿入し、--no-verifyでコミット npm run lintを実行して、(上記no-unused-disableルールにより)ルールに違反していないファイルからeslint-disableを取り除く- PR をメインブランチにマージ
- 全員が main を自分のブランチに取り込む
eslint-disable を入れておけばコンフリクトはほぼ起きなくなり、以降は各開発者が好きなタイミングでその行を消し、保存時の修正を走らせ、必要に応じてリファクタリングできるようになります。
より広範なリファクタリングが必要なファイルでは、eslint-disable 行を一度外して部分的に修正し、後日リファクタリング専用ブランチで戻ってこられるよう再び eslint-disable を入れ直しておく、という運用も可能です。
時間が経つにつれ、コードベースからは eslint-disable 行がすべて消えていきます。恩恵をできるだけ早く受け取るためにも、この作業を最優先で進めることを強く勧めます。
eslint-disable を注入する node スクリプト
このファイルをプロジェクトのルートに追加してください(使い終わったら削除します)。
import fs from 'node:fs';
import path from 'node:path';
export const getAllFiles = (dirPath, arrayOfFiles = []) => {
fs.readdirSync(dirPath).forEach(function (file) {
if (fs.statSync(`${dirPath}/${file}`).isDirectory()) {
arrayOfFiles = getAllFiles(`${dirPath}/${file}`, arrayOfFiles);
} else {
arrayOfFiles.push(path.join(dirPath, '/', file));
}
});
return arrayOfFiles;
};
const INJECT = '/* eslint-disable */\n';
const injectEslintDisable = () => {
const files = getAllFiles(path.resolve('src')).filter(
(file) => ['.ts', '.js', '.tsx', '.jsx'].includes(path.extname(file))
);
const len = files.length;
for (let i = 0; i < len; i++) {
const readFile = fs.readFileSync(files[i]);
if (readFile.includes(INJECT)) continue;
const withInject = INJECT + readFile.toString();
fs.writeFileSync(files[i], withInject);
}
};
injectEslintDisable();React Router 7 を使っている場合は、src を app に変更してください。
package.json のスクリプトに以下を追加し、npm run inject で呼び出します。
"inject": "node inject-disable.mjs"完了したら inject-disable.mjs ファイルは削除して構いません。
推奨されるルール
以前は推奨するルールセットを、プラグインごとに手で組み上げる設定として管理していました。今はもうやっていません。推奨するルール一式は @gaia-react/lint として公開済みのバージョン管理された npm パッケージにまとまっており、インストールしてフラット設定でプリセットを spread するだけで、プロジェクト間で何かをコピーして回ることなく一式が手に入ります。MIT ライセンスで公開しており、プリセット、カスタムルール、同意できないルールの上書き方法は、すべて GitHub の README にあります。
設定をパッケージ化した理由と中身については、プロジェクト間でコピペしなくなった ESLint 設定 をご覧ください。