モノレポのESLint地獄を「Flat Config」で制圧する:スケーラブルな設定継承アーキテクチャ
モノレポにおいてESLintの設定が「各パッケージに散らばり、微妙にバージョンやルールが乖離していく」という現象は、技術的負債の典型的な初期症状です。特にTurborepoやNxで数百のプロジェクトを管理する場合、旧来の `.eslintrc` による階層的継承は、ファイルシステムの複雑さとキャッシュの非効率性により、CI時間を肥大化させる原因となります。
今回は、ESLint v9以降で標準となった Flat Config (`eslint.config.js`) を駆使し、モノレポの静的解析を「設定の集約」から「動的なルール注入」へと昇華させるアーキテクチャを解説します。
—
1. なぜ「Flat Config」がモノレポの救世主なのか
従来の `.eslintrc` は、ディレクトリ階層を遡りながら設定をマージする「再帰的探索」を行っていました。これは大規模モノレポにおいて、意図しないルールの競合や、深いネストでのパフォーマンス劣化を招きます。
一方、Flat Configは「単一のJS配列」として解決されます。これにより以下のメリットが生まれます。
- 推論の完全性: どのファイルにどのルールが適用されているかが、単一の配列操作で確定するため、デバッグが容易。
- 動的生成: JS関数として記述できるため、環境変数やディレクトリ構造に基づいた動的なプラグイン注入が可能。
- キャッシュ効率: 物理ファイルシステムへの依存を排除し、メモリ上でルールセットを構築できるため、CI環境でのオーバーヘッドが最小限。
—
2. 究極の「共有設定ファクトリ」の実装
モノレポのルートに、全てのパッケージの基盤となる「設定生成器」を配置します。ここで重要なのは、「パッケージの性質(React, Node, CLI等)」を型安全に分離することです。
// eslint.config.factory.js
import js from “@eslint/js”;
import tsParser from “@typescript-eslint/parser”;
// 共通のベース設定を関数として定義(オーバーライドを容易にする)
export const createBaseConfig = (overrides = []) => [
{
// グローバル無視設定(CIのパフォーマンス向上)
ignores: [“/dist/“, “/node_modules/“, “/.turbo/“],
},
{
// JSの基本ルール
…js.configs.recommended,
rules: { “no-console”: “warn” }
},
…overrides // 動的に渡されたルールを後方で適用(最後に書いたものが勝つ)
];
このように「関数」でラップすることで、各パッケージの `eslint.config.js` は非常にシンプルになります。
—
3. パッケージ別の動的オーバーライド戦略
各パッケージでは、ファクトリをインポートし、自らの役割に応じたルールを注入します。これにより、モノレポ全体の統一感とパッケージ固有の自由度を両立させます。
// packages/api-service/eslint.config.js
import { createBaseConfig } from “../../eslint.config.factory.js”;
export default createBaseConfig([
{
files: [“src//.ts”],
languageOptions: {
parser: tsParser, // パッケージ固有のパーサー指定
},
rules: {
“@typescript-eslint/explicit-function-return-type”: “error” // API層では戻り値型を強制
}
}
]);
—
4. CI/CDパイプラインへの高度な統合:パフォーマンスの極限化
CI環境(GitHub Actions等)では、単に `eslint .` を叩くのは愚策です。変更されたパッケージのみを解析し、かつメモリ消費を抑える必要があります。
CLIによる最適化ハック
Turborepoを使用している場合、`turbo` のキャッシュ機能と組み合わせて、実行自体をスキップさせるのが鉄則です。
.github/workflows/ci.yml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Lint via Turbo
# –filterで変更差分のみを抽出し、キャッシュが効いたものは即座に終了
run: npx turbo run lint –filter=[origin/main…] –concurrency=10
コンテナ環境でのメモリ最適化
大規模なプロジェクトでは、Node.jsのデフォルトのメモリ制限(ヒープサイズ)がボトルネックになります。大規模な静的解析を行う際は、CI側で以下を強制指定してください。
環境変数でNodeのGCを最適化
export NODE_OPTIONS=”–max-old-space-size=4096 –expose-gc”
npx eslint . –cache –cache-strategy content
- `–cache-strategy content`: ファイルのタイムスタンプではなく、ハッシュ値で判定することで、CIのクローン直後でもキャッシュが有効活用されます。
—
5. アーキテクトの視点:なぜこれが「最強」なのか
このアプローチの真髄は、「設定のインフラストラクチャ化」にあります。
1. 疎結合: ルールセットと適用ロジックが分離されており、特定のプラグイン(例: `eslint-plugin-react`)を更新する際、影響範囲を関数単位で特定できます。
2. 型安全性: `eslint.config.js` をTypeScriptで記述することも可能です(`eslint.config.ts`)。これにより、設定値のタイポをCIフェーズではなくIDEの静的解析段階で排除できます。
3. DXの向上: 開発者が新しいパッケージを作る際、`eslint.config.js` をコピペする必要はありません。ファクトリを呼び出す一行を書くだけで、組織の標準に準拠した最高水準のLint環境が即座に立ち上がります。
結論
モノレポのLint管理は、もはや「設定ファイルを置く」作業ではありません。「ルールをコードとして配信するパイプラインを構築する」作業です。Flat Configをフル活用し、設定を「静的なJSON」から「動的な関数」に昇華させることで、あなたのモノレポはどれほど巨大化しても、常にクリーンで、かつ極めて高速なCI体験を維持し続けるでしょう。
この構造を導入した瞬間から、チームは「Lintエラーの修正」という不毛な作業から解放され、本来注力すべき「ビジネス価値の創出」という戦場へ戻れるのです。