ESLint Flat Configを「真の武器」に。local-rulesに頼らないカスタムプラグインの極意
テックリード諸君、日々増え続けるレガシーなルールや、チーム固有の「守るべき作法」に疲弊していないか?
これまで、プロジェクト固有の静的解析ルールを作るために `eslint-plugin-local-rules` を導入し、Flat Configへの移行でビルドエラーに頭を抱えた経験はないだろうか。結論から言おう。もう、外部パッケージに頼る必要はない。
ESLint Flat Configの本質は「単なるJSオブジェクトの集積」にある。この特性を活かせば、プロジェクトルートに置いたファイルを直接インポートするだけで、自作ルールを即座にCI/CDのパイプラインに組み込める。今日は、その「最もシンプルで、最も拡張性の高い」設計手法を授ける。
—
1. なぜ「外部プラグイン」を使わないのか?
従来の `eslint-plugin-local-rules` は、ルールを読み込むためのメタデータ管理が煩雑であり、何より「ESLintのライフサイクル」と「Node.jsのモジュール解決」の間に不要な抽象レイヤーを挟んでいた。
Flat Configの世界では、`plugins` フィールドにはオブジェクトを直接渡せる。つまり、プロジェクト内の特定のディレクトリをモジュールとして扱い、ESLintに直接読み込ませることで、以下のメリットが享受できる。
- 型安全性の確保: ルール定義自体をTSで書き、型定義を共有できる。
- テストの爆速化: `RuleTester` をそのままテストファイルにインポートするだけで、JestやVitestでの単体テストが即座に動く。
- ゼロ依存: ツール側のアップデートを待つ必要がない。
—
2. 実践:プロジェクトルートでの自作ルール管理術
ディレクトリ構造を以下のように標準化する。
/my-project
├── eslint.config.js # メインのFlat Config
├── scripts/
│ └── eslint-rules/ # ここに自作ルールを格納
│ ├── index.js # プラグインの定義(後述)
│ └── no-console-in-prod.js # 個別のルール定義
ステップ1:ルール本体の作成 (`scripts/eslint-rules/no-console-in-prod.js`)
まずは、特定の条件でエラーを吐く単純なルールを作る。
// scripts/eslint-rules/no-console-in-prod.js
module.exports = {
meta: {
type: ‘problem’,
docs: { description: ‘本番環境でのconsole出力を禁止する’ },
fixable: ‘code’,
},
create(context) {
return {
CallExpression(node) {
if (node.callee.object?.name === ‘console’) {
context.report({ node, message: ‘本番コードにconsoleを残すな!’ });
}
},
};
},
};
ステップ2:プラグインとして統合 (`scripts/eslint-rules/index.js`)
// scripts/eslint-rules/index.js
const noConsoleInProd = require(‘./no-console-in-prod’);
// ESLintのプラグイン仕様に合わせてエクスポート
module.exports = {
rules: {
‘no-console-in-prod’: noConsoleInProd,
},
};
ステップ3:Flat Configでの読み込み (`eslint.config.js`)
ここがアーキテクチャの肝だ。`require` でモジュールを直読みし、`plugins` プロパティにマッピングする。
// eslint.config.js
const localRules = require(‘./scripts/eslint-rules/index’);
module.exports = [
{
plugins: {
// プレフィックスを ‘local’ と定義
local: localRules,
},
rules: {
// 適用したいルールを記述
‘local/no-console-in-prod’: ‘error’,
},
},
];
—
3. 生産性を極限まで高める「神」テクニック
A. テストを「開発体験」の核にする
`RuleTester` を使用する際、`eslint-plugin-local-rules` を経由するとテストが重くなるが、直読みなら `vitest` 等で高速に回せる。
// scripts/eslint-rules/__tests__/no-console-in-prod.test.js
const { RuleTester } = require(‘eslint’);
const rule = require(‘../no-console-in-prod’);
const tester = new RuleTester({ languageOptions: { ecmaVersion: 2022 } });
tester.run(‘no-console-in-prod’, rule, {
valid: [‘console.log()’], // ここは適切に調整
invalid: [{ code: ‘console.log()’, errors: [{ message: ‘…’ }] }],
});
B. 設定ファイルの「共有ルール」
チーム規模が拡大したら、`eslint.config.js` を関数化し、`baseConfig` として配布せよ。
// eslint.config.base.js
module.exports = (customRules) => [
{
plugins: { local: customRules },
rules: { … }
}
];
これを各マイクロサービスで `require` して拡張すれば、「全リポジトリで同じカスタムルールを維持する」という悪夢から解放される。
—
4. テックリードからの提言:なぜこれが「最強」なのか
多くのエンジニアがツールに依存しすぎることで、ESLint内部の構造をブラックボックス化させている。しかし、今回紹介した手法は、「ESLintとは単なるAST(抽象構文木)のトラバーサルエンジンである」という本質に立ち返るものだ。
- パフォーマンス: 不要なファイル走査が消えるため、CLIの実行速度が向上する。
- デバッグ: 異常が発生しても、`scripts/` 内のコードに `console.log` を仕込むだけで、解析プロセスを完全にトレースできる。
「ツールは使うな、飼い慣らせ。」
この知見が、君たちのチームのコード品質を次の次元へ引き上げる一助となれば幸いだ。さあ、今すぐ `local-rules` の依存を消し去り、自分たちの手で解析エンジンを制御する快感を味わってほしい。