新しいReactプロジェクトを始めるたびに、同じことを繰り返していました。直近のプロジェクトを開き、eslint.config ファイルをコピーし、対応するdevDependenciesを package.json からコピーし、インストールを実行し、前回からのずれを手作業で調整する。設定自体は良いものでした。ただ、それをすべてのコピーで同期し続けることが問題でした。

今はこんなふうにプロジェクトを始めます:

import gaiaLint from '@gaia-react/lint';
import {defineConfig} from 'eslint/config';
 
export default defineConfig([
  ...gaiaLint.ignores({gitignore: '.gitignore'}),
  ...gaiaLint.base,
  ...gaiaLint.react,
  ...gaiaLint.testing,
  ...gaiaLint.storybook,
  ...gaiaLint.playwright,
  ...gaiaLint.styleHygiene,
  ...gaiaLint.guardrails,
  ...gaiaLint.betterTailwind({
    entryPoint: './app/styles/tailwind.css',
    ignore: ['plain-link', 'plain-table'],
  }),
  ...gaiaLint.prettier,
]);

これが本番ReactアプリのESLint設定の全部です。依存関係は1つ、プリセットのリストだけ。ルールを改善すれば、次のインストール時にすべてのプロジェクトに反映されます。

この記事では、なぜESLintの設定をパッケージに移したのか、何が含まれているのか、そして自分の意見が違う場合にどうカスタマイズするかを説明します。

コピーした設定が劣化する理由

ReactとTypeScript向けの本格的なESLint設定は、20近くのプラグインを必要とします。React、hooks、アクセシビリティ、imports、テスト、ソーター、命名規則チェッカー、TypeScriptルールなど。それぞれが独自のスケジュールでバージョンアップします。それぞれが時折、ルールのデフォルトや推奨セットを変更します。

その設定を5つのプロジェクトにコピーすると、メンテナンスすべきものが5つになります。実際には5つはメンテナンスしません。一番最近開いた1つだけをメンテナンスし、残りは遅れていきます。最新のプロジェクトには今発見したばかりのルールが入っている。18ヶ月前のプロジェクトには入っていない。これは不注意の問題ではありません。設定の共有がコピー&ペーストで行われるとき、起こることです。

ESLint 8から9への移行で、そのコストが明確になりました。フラット設定は単なるバージョンアップではなく、形そのものの変更でした。コピーした設定を持っていると、この移行がリポジトリごとに別々の作業になります。1つのパッケージにまとめていれば、一度だけ、バージョン番号の裏で済みます。

インストールと組み合わせ

pnpm add -D @gaia-react/lint eslint prettier typescript

eslintprettiertypescript はピア依存関係なので、これらのバージョンはプロジェクト側で管理します。設定が必要とするすべてのプラグインはパッケージの中に含まれているので、追加のdevDependenciesリストを合わせる必要はありません。

設定は1つのオブジェクトではありません。順番に展開するプリセットのセットです:

  • basereactstyleHygieneguardrails がコアです。プレーンなJavaScriptとTypeScriptのルール、ReactとhooksとアクセシビリティのルールUse、importの整理、ソート、そして後述するこだわりのガードレールが含まれます。
  • testingstorybookplaywright はオプションでスコープが限定されています。Playwrightを使っている場合のみ playwright を展開し、そのルールはend-to-endフォルダにのみ適用されます。
  • betterTailwindignores は引数が必要なためファクトリーです。betterTailwind はTailwindのエントリーCSSがどこにあるかを知る必要があり、ignores.gitignore を読んでESLintがGitと同じファイルをスキップするようにします。
  • prettier は最後に来ます。これは意図的です。フラット設定は最後に書かれた設定が優先されるため、このプリセットはすべてのフォーマットルールをオフにし、PrettierとESLintが競合しないようにします。

PrettierとStylelintの設定も同じパッケージから、サブパスエクスポートで取得できます:

// prettier.config.mjs
export {default} from '@gaia-react/lint/prettier';

3つのツール全部で、パッケージ1つ、バージョン番号1つ。MITライセンスで、ソースとルールの全一覧は GitHub にあります。

パッケージに含まれる意見

このパッケージは「意見が強い」と自称しており、本気でそうです。guardrailsbase の一部のルールは万人向けではないかもしれません。説明する価値のあるものを以下に挙げます。

enum 禁止。 TypeScriptのenumはランタイムコードを生成し、ツリーシェイク不可で、値の空間と型の空間の境界を曖昧にします。数値enumと文字列enumの挙動は異なり、逆引きマッピングが予期せぬ驚きをもたらします。const オブジェクトと導出されたユニオン型を使えば、同じことをそれらの問題なしに実現できます:

const Status = {
  Active: 'active',
  Inactive: 'inactive',
} as const;
type Status = (typeof Status)[keyof typeof Status];

switch 禁止。 switch は、ルックアップマップ、早期リターンチェーン、または網羅性チェックが変装したものであり、フォールスルーバグと default 漏れのバグまで一緒についてきます。正直な形はそれぞれ、置き換えるものよりも明確です。

アロー関数のみ。 Reactは関数型です。アロー関数は巻き上げされないため、呼び出し順序が明示的になり、定義が読んでいる行より上にあることが常にわかります。this を持たないため、Reactでは不要なものです。mapfilter に渡す関数式と一貫しています。このルールはアップストリームのギャップも埋めます。元のプラグインが export default function Named() {} を静かに免除しているため、設定にはそれを補足する独自チェックが追加されています。

すべてをアルファベット順に。 imports、JSXのprops、オブジェクトのキー、ユニオンメンバーが自動的にソートされます。ソート順はエンジニアが永遠に議論しても決まらない種類のものなので、設定は問いません。AからZ、保存時に固定、すべてのファイルで同じ。

これらはバグを検出するルールではありません。一貫性のためのルールです。プロジェクトのすべてのファイルが、そしてすべてのプロジェクトが、同じように読めるために存在します。

意見が合わない場合

意見の強いパッケージには明白なコストがあります。私の意見を引き継いだことになります。switch を使いたい場合、今やそれは追加しなかったルールではなく、オフにしなければならないルールです。

答えは、prettier を最後に置くのと同じ「最後に書かれた設定が優先」の挙動です。gaia-lintの展開の後にオーバーライドブロックを追加します:

export default defineConfig([
  ...gaiaLint.base,
  ...gaiaLint.react,
  // プロジェクトの判断で
  {rules: {'no-switch/no-switch': 'off'}},
]);

特定の場所だけ例外を適用したい場合はglobでスコープを限定できます:

{
  files: ['app/legacy/**'],
  rules: {'sonarjs/cognitive-complexity': 'off'},
}

これがパッケージの提供する取引です。厳格な状態から始めて、意図的に緩める。その例外はconfigの中に書き留められ、次の人が見ることができます。代替案、つまり空の設定から始めて後で厳格にするという計画は、締め切りに直面してほとんど生き残れません。

AIが加わるとこれがより重要になる理由

先日、Claudeにコードを書かせる前に設定するクオリティバーについて書きました。すべてのルールをエラーにし、warningをゼロにし、最初のプロンプトの前にゲートを設けること。

パッケージ化された設定は、そのバーを複数のプロジェクトで維持するためのものです。AIはセッションのたびに新鮮にコードベースに参加します。長年在籍したエンジニアのようにプロジェクト間のハウススタイルを持ち越しません。AIが持つのは、設定とその設定が出すエラーです。その設定が、バラバラにずれていくコピーの集まりではなく、バージョン管理された1つのパッケージであれば、「良いコードとはどういうものか」はすべてのリポジトリで同一になります。その基準は私の頭の中にもwikiにもありません。インストールされています。

これがコピペをやめた本当の理由です。プロジェクトごとに節約される時間のためではありません。私の設定の最良バージョンが唯一のバージョンになり、すべてのプロジェクトが、そしてその中でコードを書くものすべてが、まさにそれを得るためです。