Claude Media
Claude CodeでESLintのflat config移行を進める — migrate-configの後始末

Claude CodeでESLintのflat config移行を進める — migrate-configの後始末

ESLint 9のflat configへの移行は、@eslint/migrate-configで終わりません。残る書き換えをlint実行の反復でClaude Codeに詰めさせる手順と、症状別の直し方をまとめます。

ESLint 9のflat configへの移行は、@eslint/migrate-configを1回走らせても終わりません。ESLint側が「生成物はそのままでは動くと保証できない出発点」と書いているためです。残る作業は、pluginsやextendsの書き換え、envの置き換え、除外パターンの移し替えなど、どれも「lintを実行してエラーを読み、1種類ずつ直す」反復に向いています。

この反復をClaude Codeに任せるときの要点は3つです。移行前の結果を保存しておくこと、検証コマンドをCLAUDE.mdに固定すること、エラーの種類ごとに直し方の辞書を持たせること。本記事ではこの順に、コマンドと設定の断片つきで進めます。

着手前に固定しておく前提

ESLint 9.0.0は、Node.jsのv18.18.0以上、v20.9.0以上、v21以上だけを対応対象にしています。v19と18.18未満は対象外です。エディタが内部で使うNode.jsのバージョンも見ておく必要があります。上げられない場合の逃げ道として、ESLint v8.56.0にとどまる案が示されています。

もう一つ、見落としやすい点があります。ESLint 9.0.0からeslint.config.jsが既定の設定形式になり、旧形式のeslintrcは自動探索されません。環境変数ESLINT_USE_FLAT_CONFIGをfalseにすれば旧形式を使い続けられます。この環境変数は、移行前後を見比べる場面で役に立ちます。

逆方向の手もあります。ESLint v8でも、プロジェクト直下にeslint.config.jsを置くかESLINT_USE_FLAT_CONFIG=trueを設定すれば、flat configを使えます。

手順

移行の進め方(2段階にする場合)

  1. 1

    v8のまま設定だけ移す

    eslint.config.jsを置いて、lintの結果が移行前と揃うところまで詰めます。

  2. 2

    v9へ上げる

    Node.jsの要件を満たしたうえでeslint本体を上げます。設定の問題と本体更新の問題を切り分けられます。

移行前の結果を保存する

反復の「正解」になるのは、移行前のlint結果です。最初に保存します。v8のまま作業する場合は、そのまま実行して構いません。

npx eslint . -f json -o before.json
jq -r '.[].messages[].ruleId' before.json \
  | sort | uniq -c | sort -rn > before-rules.txt

-f jsonは出力形式、-oは出力先ファイルの指定です。before-rules.txtはルールごとの指摘件数で、移行後に同じ集計を取って差分を見ます。jqが無い環境では、件数の集計だけClaudeにスクリプトで書かせれば足ります。

すでにv9へ上げてしまった場合は、ESLINT_USE_FLAT_CONFIG=falseを前置して同じコマンドを実行します。

なお、設定ファイルが.eslintrc.jsのときは別の注意が要ります。移行ツールは.eslintrc.jsをあまり得意としておらず、結果は「評価後の設定値をそのまま書き出したもの」になります。関数や条件分岐は残らないため、環境ごとに設定を切り替えていた場合は、その分岐を人かClaudeが書き戻す必要があります。

CLAUDE.mdに検証コマンドと禁止事項を書く

ClaudeにはこのリポジトリのlintコマンドとNGな近道を先に伝えます。Claude Codeのベストプラクティスでも、リンターは「Claudeが会話の中で読める信号」の例に挙がっています。検証手段を渡すかどうかで、目を離せるかどうかが変わります。

## ESLint flat config 移行(作業中のみ)
- 設定は eslint.config.js に集約する。.eslintrc.* と .eslintignore は、
  移行完了を伝えるまで削除しない
- 検証は `npx eslint . --max-warnings 0`。出力の先頭20行を読んでから直す
- 1回の修正で扱う原因は1種類。直したら必ず再実行し、件数の増減を報告する
- ルールを off にして通さない。off にしたいときは理由を添えて確認する
- 移行前の結果は before.json と before-rules.txt に保存済み。上書きしない

「ルールをoffにして通さない」の1行が肝です。lintが赤いままだと、最短で緑にする手はルールを無効化することだからです。この一文がないと、通ったように見えて検査が空洞化した設定ができあがります。

実行権限も先に開けておくと、反復が止まりません。permissions.allowに検証コマンドを足します。

{
  "permissions": {
    "allow": [
      "Bash(npx eslint *)",
      "Bash(npx @eslint/migrate-config *)",
      "Bash(jq *)"
    ]
  }
}

Bash(git *)のようなパターンで許可を絞る書き方は、hooksのリファレンスにある権限ルール構文と同じです。npm run lintを許可リストに入れる例も、ベストプラクティスに載っています。インストールを伴うコマンド(npm installなど)は、この段階では許可に入れず、都度確認を挟むほうが安全です。

migrate-configを実行して出力を読む

移行ツールは、旧設定ファイルを渡して実行します。

npx @eslint/migrate-config .eslintrc.json

yarnならyarn dlx、pnpmならpnpm dlx、bunならbunxで同じものを実行できます。生成直後に、いきなりlintを回さないでください。まずClaudeに、生成されたeslint.config.jsと旧設定を並べて読ませます。

.eslintrc.json と生成された eslint.config.js を読んで、
旧設定の項目ごとに、新設定のどこへ移ったかの対応表を作ってください。
対応先が見つからない項目は「未移行」として挙げてください。まだ編集はしないでください。

編集を禁じて対応表だけ作らせるのが狙いです。「未移行」に挙がった項目は、後で原因不明のエラーに化けやすいものです。先に洗い出しておけば、反復の回数が減ります。

残作業をlint実行の反復で詰める

ここからは、次の形のループです。

手順

1周の流れ

  1. 1

    実行する

    npx eslint . --max-warnings 0を実行し、終了コードと先頭のエラーを読みます。

  2. 2

    原因を1種類に絞る

    件数の多いエラーより、同じ原因から出ているエラーをまとめて扱います。

  3. 3

    直して再実行する

    件数の増減を報告させ、増えていたら直前の修正を疑います。

最初に見るのは終了コードです。ESLintは、指摘がなければ0、エラーがあるか--max-warningsの上限を超えれば1、設定の問題や内部エラーなら2で終わります。2が返ったら設定ファイルそのものが壊れているので、ルールの指摘を直す前に設定を直します。1が返ったら、設定は読めていて、指摘の中身を見る段階です。

症状別の直し方

エラーの文面ごとに、原因と直し方を引ける表にしておきます。Claudeには「該当する行があればこの直し方を第一候補にする」と伝えます。

症状原因直し方
context.getScope is not a function原因プラグインがESLint v9のルールAPIに未対応直し方プラグインを更新する。更新が無ければ互換ユーティリティで手当てする
/* eslint-env */コメントがエラーになる原因flat configではこのコメントが認識されず、エラーとして報告される直し方コメントを消し、設定側にglobalsを書く。/* global describe, it */への置き換えも可
no-undefが大量に出る原因envが消え、実行環境ごとのグローバル変数が未定義になる直し方globalsパッケージを入れ、languageOptions.globalsに展開する
除外したはずのファイルがlintされる原因.eslintignoreは読み込まれない。temp.jsは設定ファイルと同じ階層の1ファイルしか指さない直し方ignoresだけを持つオブジェクトに移し、**/temp.jsの形に直す
.dotfile.jsなどが新たに対象になる原因flat configではドットファイルが既定で除外されない直し方除外したければ**/.*をignoresに足す
eslint:recommendedが解決できない原因文字列でのextendsが無くなった直し方@eslint/jsを入れ、js.configs.recommendedを配列に並べる
shareable configが読み込めない原因そのconfigがflat config未対応直し方@eslint/eslintrcのFlatCompatでcompat.extends(...)に通す
TypeScriptなど.tsが対象にならない原因filesを持たない設定は**/*.{js,mjs,cjs}にしか効かない直し方該当設定のfilesに拡張子を足す

globalsパッケージは、ESLintが自動で入れてくれません。no-undefの対処では、npm install --save-dev globalsが先に必要になります。この手の追加インストールは、許可を開けていないので、Claudeが確認を求めてきます。そこで止まるのは正常です。

迷いやすい書き換えの実体

extendsの置き換えは、ここだけ例を出しておきます。ESLint公式のガイドにある形に沿った書き方は、次のとおりです。

// eslint.config.js
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import globals from "globals";
 
export default defineConfig([
  js.configs.recommended,
  {
    files: ["**/*.js"],
    languageOptions: {
      globals: { ...globals.browser, ...globals.node },
    },
    rules: { semi: ["warn", "always"] },
  },
  { ignores: ["**/temp.js", "config/*"] },
]);

ignoresは、ほかのプロパティを持たない独立したオブジェクトに置きます。ガイドのコード例にも「このオブジェクトに他のプロパティを置かない」という注記があります。キーを同居させてしまうのが、除外が効かないときの第一の疑いどころです。

pluginsも同じで、文字列の配列からオブジェクトのマップに変わります。plugins: ["jsdoc"]は、import jsdoc from "eslint-plugin-jsdoc"で読み込んだうえでplugins: { jsdoc }と書きます。ルール名のjsdoc/require-descriptionは、そのまま使えます。

移行前と後を同じ尺度で見比べる

lintが緑になっても、「検査の中身が移行前と同じか」は別問題です。ここで、最初に保存した結果を使います。

npx eslint . -f json -o after.json
jq -r '.[].messages[].ruleId' after.json \
  | sort | uniq -c | sort -rn > after-rules.txt
diff before-rules.txt after-rules.txt

差分がなければ、少なくとも指摘の出方は揃っています。差分が出たら、増えたルールと減ったルールをClaudeに挙げさせます。減ったルールは「検査が落ちた」疑いです。増えたルールは、ドットファイルや拡張子の扱いが変わって、対象ファイルが増えた可能性を最初に疑います。

指摘がもともと0件のリポジトリでは、この比較が空振りします。その場合は代表的なファイルに絞って、適用される設定そのものを比べます。ESLintの--print-configは、指定したファイルに適用される設定を出力するオプションで、実行中はlintを行いません。

ESLINT_USE_FLAT_CONFIG=false npx eslint --print-config src/index.js > before-config.json
npx eslint --print-config src/index.js > after-config.json

2つのJSONは書式や項目の並びが同じとは限らないので、diffの行数ではなく、rulesの中身をClaudeに読み比べさせます。見るのは、ルールが落ちていないか、重大度が変わっていないかの2点です。環境変数を前置した1行目は、ESLint 9の環境で旧形式を読ませるための指定です。v8のままなら外します。

最後の後片付けと、つまずきやすい点

緑になって比較も済んだら、最後に次を消します。

  • .eslintrc.*と.eslintignore
  • package.jsonのeslintConfigキー(flat configではここに設定を置けません)
  • スクリプトに残った--env、--ignore-path、--no-eslintrc、--resolve-plugins-relative-to、--rulesdir

最後の項目が見落とされがちです。これらのフラグはflat configでは使えなくなりました。--no-eslintrcは--no-config-lookupに置き換わりました。--ignore-path .gitignoreのように.gitignoreの内容を除外に使っていた場合は、設定側でincludeIgnoreFile()を使います。ガイドには、@eslint/compatの同名関数が非推奨になり、@eslint/config-helpers(eslint/configから再エクスポート)版に移ったという更新注記があります。どちらを使うかは、インストール済みのバージョンで選びます。

rootオプションは廃止されました。flat configはroot: trueが設定されているものとして動きます。filesに単一の文字列を書くこともできなくなり、配列にします。

エディタ側も確認します。VS CodeのESLint拡張は、v3.0.10以降でESLint 9に対応しています。それより古い拡張では、.vscode/settings.jsonに"eslint.experimental.useFlatConfig": trueを足します。CLIが緑でエディタだけ赤い場合は、先にこの拡張のバージョンを見てください。

最後に、移行後のLintをhookで自動修正する構成にしたいときは、PostToolUse hookのLint自動修正が扱っています。テストランナー側の規約は、Claude CodeでVitestを回すとpytestのCLAUDE.md規約に同じ型で書かれています。モノレポで複数パッケージを抱える場合は、Turborepoのモノレポ設定も併せて読むと、タスクの切り方を決めやすくなります。

まとめ

migrate-configは、設定を新形式に書き写す出発点にとどまります。残りは、移行前の結果を保存し、検証コマンドと「ルールをoffにしない」をCLAUDE.mdに固定し、症状別の辞書で1種類ずつ直す反復です。終了コードの2は設定の破損、1は指摘の中身という切り分けを先にClaudeへ渡しておくと、反復の無駄が減ります。

旧設定が.eslintrc.jsで条件分岐を含むときは、生成物を信用しすぎないほうが無難です。評価済みの値に潰れているため、書き戻した分岐ごとに、前後の結果を比べる確認が欠かせません。

この記事を共有:XはてブLinkedIn