新しい React プロジェクトを始めるたびに、決まって同じ手順を踏んでいました。直近のプロジェクトを開いて eslint.config をコピーし、対応する devDependencies を package.json から拾い、インストールを走らせ、前回からずれた分を手で直す。設定そのものは悪くなかったのですが、コピーした分すべてを同期し続けることが問題でした。
いまはこう始めます。
import gaiaLint from '@gaia-react/lint';
import {defineConfig} from 'eslint/config';
const lint = gaiaLint();
export default defineConfig([
...lint.ignores,
...lint.base,
...lint.react,
...lint.testing,
...lint.storybook,
...lint.playwright,
...lint.styleHygiene,
...lint.guardrails,
...lint.betterTailwind({entryPoint: './app/styles/tailwind.css'}),
...lint.prettier,
]);これが本番 React アプリの ESLint 設定のすべて。依存関係は一つ、あとはプリセットを並べるだけで、ルールを改善すれば次の install のタイミングですべてのプロジェクトへ届きます。
本稿では、なぜ ESLint 設定をパッケージへ移したのか、中身に何が入っているのか、そして特定のルールを自分のプロジェクトに合わせて調整する方法、という順に書いていきます。
コピーした設定が劣化する理由
React と TypeScript 向けの本格的な ESLint 設定では、多数のプラグインとプリセットが欠かせません。React、hooks、アクセシビリティ、imports、テスト、ソート、命名規則チェッカー、TypeScript ルール、と並べていけばきりがなく、それぞれが独自のスケジュールでバージョンを上げては、ときにルールの既定値や推奨セットまで変えてきます。
これを 5 つのプロジェクトにコピーすれば、メンテナンス対象もそのまま 5 つに膨れあがる。とはいえ実際に 5 つともメンテし続けるはずもなく、直近で開いた一つだけ世話をして、残りは置き去りになっていきます。最新のプロジェクトには昨日見つけた便利なルールが入っているのに、18 ヶ月前のプロジェクトには入っていない。不注意の問題ではなく、共有の手段がコピー & ペーストである限り必然的に起こることです。
ESLint 8 から 9 への移行で、このコストははっきりと表に出ました。フラット設定はただのバージョンアップではなく、形そのものが変わる変更だったため、コピーした設定を抱えているとリポジトリごとに同じ移行作業を繰り返す羽目になります。一つのパッケージにまとめてあれば、バージョン番号の裏で一度きりで片づきます。
インストールと組み合わせ
npm add -D @gaia-react/lint eslint prettier typescripteslint、prettier、typescript はピア依存。バージョンはプロジェクト側の責任で握ります。設定が必要とするプラグインはすべてパッケージの中に同梱されているため、二重に devDependencies の一覧を揃え直す手間はありません。
設定は一つのオブジェクトではなく、順に spread するプリセットの集まりです。
base、react、styleHygiene、guardrailsがコア。素の JavaScript と TypeScript のルール、React と hooks とアクセシビリティのルール、import の整理、ソート、そして後述するこだわりのガードレールが含まれています。testing、storybook、playwrightはオプションかつスコープ付き。Playwright を使っているプロジェクトでのみplaywrightを spread し、そのルールも end-to-end フォルダにしか適用されません。betterTailwindとignoresは引数を取るためファクトリー形式。betterTailwindには Tailwind のエントリー CSS の位置を渡す必要があり、ignoresは.gitignoreを読んで ESLint が Git と同じファイルをスキップしてくれます。prettierは意図的に最後へ。フラット設定は最後に書かれたものが勝つ仕様なので、このプリセットでフォーマット系のルールをすべて落とし、Prettier と ESLint が衝突しないようにしています。
Prettier と Stylelint の設定も、同じパッケージのサブパスエクスポートから取り出せます。
// prettier.config.mjs
export {default} from '@gaia-react/lint/prettier';3 つのツールすべてに対してパッケージ一つ、バージョン番号一つ。MIT ライセンスで、ソースとルールの全一覧は GitHub で公開しています。
パッケージに込めた意見
このパッケージは自身を「opinionated」だと名乗っており、それは誇張ではありません。guardrails と base に入っているルールの中には、万人受けしないものもあります。説明する価値のあるものを以下に挙げておきます。
enum を禁止。 TypeScript の enum はランタイムコードを吐き、ツリーシェイクが効かず、値空間と型空間の境界を曖昧にしたうえに、数値 enum と文字列 enum で挙動が違い、逆引きマッピングが人を驚かせるという厄介まで連れてきます。const オブジェクトと、そこから導出したユニオン型を使えば、同じことをこれらの問題抜きで実現できます。
switch を禁止。 switch の正体は、ルックアップマップ、早期リターンの連鎖、あるいは網羅性チェックの変装であり、おまけにフォールスルーや default 抜けのバグまで連れてきます。正直な形に分解したほうが、置き換えられる対象よりつねに明快です。
JSX 内の IIFE を禁止。 JSX 式コンテナの中で {(() => { ... })()} のようにロジックブロックを包むと、意図がぼやけるうえ、レンダリングのたびに新しい関数が確保されてしまうため、値は return の上で変数に算出しておくか、インラインの && 式に書き換えるのが筋です。なお、このルールのスコープは .tsx と .jsx に絞ってあります。
アロー関数のみ。 React は関数型で、アロー関数はホイスティングされないため呼び出し順序が明示的になり、定義が読んでいる行より上にあると常に確信できます。this を持たないところも React では好都合であり、map や filter に渡す関数式とも自然に揃うのが嬉しいところ。さらに、このルールはアップストリームの穴まで塞いでくれます。元のプラグインは export default function Named() {} を黙って見逃してしまうため、設定側に独自チェックを足してそこを拾えるようにしてあります。
何もかもアルファベット順。 imports、JSX の props、オブジェクトのキー、ユニオンメンバーが自動でソートされます。ソート順はエンジニアが延々と議論しても決着がつかない類いの話なので、設定は最初から問いません。A から Z、保存時に固定、すべてのファイルで同じ。
これらはバグを捕まえるためのルールではなく、一貫性のためのルールです。プロジェクト内のどのファイルも、そしてどのプロジェクトも、同じ顔つきで読めるようにする。それがこの一群を入れている理由です。
プロジェクトに合わせてルールを調整する
意見の強いパッケージは、すべてのルールに既定値を当ててきます。これを自分のプロジェクトに合わせる手は三つ。ルールをオフにするか、特定のフォルダにスコープを絞るか、設定値を書き換えるか。たとえば switch を使いたいときには、「足さなかったルール」ではなく「明示的にオフにするルール」を書くという扱いになるわけです。
仕組みとしては、prettier を最後に置くのと同じ「最後に書かれた設定が優先される」挙動を応用しているだけ。gaia-lint の spread の後ろにオーバーライドブロックを足してあげれば済みます。
export default defineConfig([
...lint.base,
...lint.react,
// プロジェクトの判断で
{rules: {'no-switch/no-switch': 'off'}},
]);例外を特定の場所だけに留めたければ、glob でスコープを切ります。
{
files: ['app/legacy/**'],
rules: {'sonarjs/cognitive-complexity': 'off'},
}これがこのパッケージの差し出す取引です。最初は厳しく入り、意図を持って緩める。その例外は config に書き留められ、次の担当者の目にもきちんと触れます。逆に「空の設定から始めて、あとで厳しくしていく」という計画は、締め切りの前ではまず生き残れません。
AI が入ってくると、これがさらに重要になる
Claude にコードを書かせる前に立てておくべきクオリティバーについては、別の記事で改めてまとめます。すべてのルールをエラー扱いにし、warning をゼロにし、最初のプロンプトの前にゲートを上げきっておく、という話。
パッケージ化された設定は、このバーを複数のプロジェクトにわたって保ち続けるための仕掛けです。AI はセッションごとにまっさらな状態でコードベースに参加してくるので、在籍の長いエンジニアのようにプロジェクトをまたいでハウススタイルを引き継いだりはしません。AI の手元にあるのは、設定と、その設定が吐き出すエラーだけ。その設定がばらばらにずれていくコピーの集合ではなく、バージョン管理された一つのパッケージになっていれば、「良いコードとはどういうものか」はどのリポジトリでもまったく同じ姿で立ち上がってきます。基準は私の頭の中にも wiki の中にもなく、インストール済みの状態でそこに置かれているわけです。
コピペをやめた本当の理由はここにあります。プロジェクトごとの時短のためではなく、私の設定の最良バージョンが唯一のバージョンになり、すべてのプロジェクトと、そこでコードを書くものすべてが、まさにそれを手にできるようになるためです。