【ESLint】Legacy ConfigからFlat Configへ移行するための情報収集をしたい

モチベーション
過去にESLint周りの記事を書いていたのでこちらをFlat Configにする。その後に本業のFlat Config移行に着手する
完了。今回のFlat Config 移行でaribnbのルールセットの利用をやめた。以下のissueを見るとわかるが結構つらそう。20250101時点でもFlat Configへの対応ができていない?
本業の方の移行も完了🎉

Flat Config へ移行する背景
- ESLint v8 は2024/10/05でEOLを迎えた(ref)
- ESLint v9 でFlat Configがデフォルトになり、eslintrc は非推奨になる
- ESLint v10 (2024年末〜2025 年初頭にリリース予定)で eslintrc が削除される(ref)
-> eslintrcからFlat Configへの移行しない場合、ESLint v10以降のバージョンアップができなくなってしまう。
想定している取り組み
8系でもv8.21.0(2022/08/01リリース)以降であればFlat Configが使用可能なので、まずはFlat Configへの移行させることをゴールにする。余裕があれば v9 へのバージョンアップまで着手する。
-> 結局 ESLint Config Inspectorを使って作業をしたかったのでv9へ上げてからFlat Configへの移行作業に着手した。
想定している作業は以下。
- 使用しているextend, plugin, ruleがFlat Config対応しているかの調査
- マイグレーションガイド(ref)に従ったFlat Configへの移行
- カスタムルールをFlat Configに対応させる(ref)
- Flat Configを有効にするために.vscode/settings.jsonを修正する(ref)
- ルールが新旧で一致しているかの確認 etc
「具体的にどうやってやるか?」は以下の記事が参考になりそう。

Flat Config にすると何が嬉しいのか?
Flat Configに至るまでの背景が書いてある。
その中でも触れられているが、Flat Configへ移行することによるメリットは大きく2つあるので以下の引用記事も交えながら見ていくと良いかも。
プラグインを自分で解決しなくなる!
-
https://www.wantedly.com/companies/wantedly/post_articles/939693
- 記述スタイルもこれまでと大きく異なり、ESLint が内部で持っていたモジュール読み込みシステムを使わず、JS のモジュール読み込みシステムを使用することができるため、
外部の config や plugin の使用がより分かりやすくなりました。
- 記述スタイルもこれまでと大きく異なり、ESLint が内部で持っていたモジュール読み込みシステムを使わず、JS のモジュール読み込みシステムを使用することができるため、
-
https://zenn.dev/babel/articles/eslint-flat-config-for-babel
- ESLint のエコシステムでは、plugin や preset という形で再利用可能な設定の開発が行われてきました。従来の
.eslintrc
では、プラグイン等の解決を ESLint ランタイムが担っていました。
- ESLint のエコシステムでは、plugin や preset という形で再利用可能な設定の開発が行われてきました。従来の
-
https://zenn.dev/cybozu_frontend/articles/about-eslint-flat-config
- rule や config を独自で解決せず、JavaScript のモジュール解決の仕組みに乗った
-
https://tech.smarthr.jp/entry/eslint_v9
- 文字列ベースの設定から、モジュールとして明示的にインポートする形式に変わり、可読性やメンテナンス性が向上します
override や extends という概念がなくなる!
-
https://zenn.dev/cybozu_frontend/articles/about-eslint-flat-config
- FlatConfig は
override
やextends
という概念をなくし、代わりにconfiguration object
と呼ばれる各設定情報を要素とする配列で表現するようになりました。
- FlatConfig は
-
https://zenn.dev/babel/articles/eslint-flat-config-for-babel
- このカスケーディングの機構は、従来の
.eslintrc
が持っていた 2 つの機構を統合するものです。つまり、extends
で再利用可能な設定を読み込む機構と、overrides
により一部のファイルのみ設定を変えられる機構です。両者はネスト可能(extends
をたどるとoverrides
があったり、overrides
の中でextends
できたり)であったため、従来は設定ファイルのカスケーディングが木構造の上で行われていたことになります。
- このカスケーディングの機構は、従来の
-
https://tech.smarthr.jp/entry/eslint_v9
- カスケード構造が廃止され、設定がフラット化されることで、適用範囲やルールの構造が直感的に理解できるようになります

Introducing ESLint Compatibility Utilities
Flat Configへの変更に伴って「Rule APIの変更」と「Flat Configへの変更」の2点が必要になる
@eslint/eslintrc (ex: flatCompat.extends)
Rule APIの変更
The primary class in this package is FlatCompat, which is a utility to translate ESLintRC-style configs into flat configs
カスタムルールを作るときにcontext.getSourceCode()でアクセスするとdeprecatedの警告が表示されcontext.sourceCodeを使うように促されるので、新しくルールを作るとき、または変更する時は見つけやすいはず。
@eslint/compat(ex: fixupConfigRules)
Flat Configスタイルへの変更
you may not be able to access each of the plugins that are referenced inside of an eslintrc-style configuration. In that case, you can use the fixupConfigRules() function to wrap all plugins
なのでラップすることでeslintrc-styleからflat-config-styleに変えてくれる
カスタムルールをFlat Configに対応させる
対応自体は思ったより簡単だった。公式docsをちゃんと読めばそこまで大変ではなさそう。
Rule APIの破壊的変更の対応
RuleTesterも直す

@eslint/js
recommendedの中身は純粋なルールの定義がされている
以下のようにnameを追加すると
const jsConfig = [
{
name: 'js recommended',
...eslint.configs.recommended,
},
{
name: 'js override',
rules: {
'no-undef': 'off',
},
},
]
こんな感じになるのでデバッグしやすくなる

typescript-eslint
いくつか記事を見たけれどtseslint.config
は使わない想定で進めたい。
もちろんtseslint.config
でラップするメリットはあるが以下理由から使わなくても良いかなと。
-
typescript-eslint
の独自プロパティ(extendsプロパティ)が生え、コード記法を統一できない可能性がある(PRで弾くしかなさそう) - そもそも
tseslint.config
の影響が大きすぎる。責務として抱え込みすぎな気がする。 - ある程度作ってきたけど、
tseslint.config
を使わないことで致命的な後戻りはなさそう。
tseslint.config
を使わずにtypescript-eslint
みたいな配列でconfig object(正式にはConfiguration Objectと呼ぶ)を受け取るかつ、例えばfilesの対象を上書きした場合は以下のような実装をする必要がある。
同僚と話してやはりtseslint.config
は使わない方針になった。理由は以下。
-
tseslint.config
がただのeslintに対する便利関数である点 - 「...config or config」「extends 使う使わない」など記法が増えてしまう点
- 今後eslint側からdefineConfigなる
tseslint.config
に似たものが出るよう点(ref)
eslint-recommended-raw の役割
「TypeScriptがカバーしているため、不要なESLintの推奨ルールを無効にします」とのこと。recommended, recommended-type-checkedなどを指定するときに一緒に読み込まれている。以下のdocsにも明記されている
ちなみに「TypeScriptがカバーしているため、不要なESLintの推奨ルールを無効にします」に関してはeslint-plugin-import
でも対応が必要になる(以下参照)
base
baseも呼ばれていたので一応メモする。baseは以下を参照すれば良い。docsにも記載されている。
recommendedとrecommended-type-checkedの違い
recommendedとrecommended-type-checkedってどのくらい差分あるんだろうか?
-> recommended + recommended-type-checked-only = recommended-type-checked
例えば以下のルールはリンクを見てみるとrecommendedって書いたあるけどrecommended-type-checkedでも呼ばれているので念頭に置いたおきたい。呼びたくない時はrecommended-type-checked-onlyを使えば良い。
'@typescript-eslint/no-require-imports': 'off', // https://typescript-eslint.io/rules/no-require-imports/
'@typescript-eslint/no-unused-expressions': 'off', // https://typescript-eslint.io/rules/no-unused-expressions/
'@typescript-eslint/no-unused-vars': 'off', // https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/no-empty-object-type': 'off', // https://typescript-eslint.io/rules/no-empty-object-type/

Combine Configs
sharerable configがObject形式かArray形式かで微妙に適用方法が異なる。すでに設定されているfilesを上書きする方法(部分的に上書きする方法)も記載されている。通常のJavaScriptと同じような感じなのでFEを触っているなら違和感はないはず。
sharerable configでArray形式なのは例えばtypescript-eslint/recommended
やstorybook/recomennded
が該当する。Object形式だと@eslint/js
、eslint-plugin-react
やeslint-plugin-import
が該当する。ここら辺はREADMEを見るか直接コードを見てどういう形式でexportされているか確認する必要がありそう。
なのでよく記事で見る@eslint/js
とtypescript-eslint
の適用でtypescript-eslint
だけ展開されているのはArray形式だったから。という前提を知っておく必要がある。
[MEMO]いい感じのexがあれば添付する
また、以下の記事でも注意書きされているがObject形式の場合はマージ方法に注意しないとrulesを上書きしたつもりがshareable configで設定しているルールを全て上書きする可能性があるので気をつけること。

トラブルシュート
外部モジュールとして切り出したファイル変更を ESLint Config Inspector で検知できない
表題の通り。eslint.config.jsの変更自体は検知できるので、中で差分があれば即時反映してくれる(ex: rulesの変更とかnameの変更など)しかし外部に切り出したファイルの変更は再度npx eslint --inspect-configを叩かないと反映されない。
SyntaxError: Cannot use import statement outside a module
以下の記事で取り上げられていた。package.jsonのtypeをmoduleに変更して解決。全部.mjs
(外部モジュールも含めて)で進めるか.js
でpackage.jsonのtypeをmoduleに変更するかになりそう。
ちなみにFlat Configの拡張子.mjs
サポートはされていて、むしろ前は.js
のみのサポートだったのでプロジェクトのpackage.jsonのtypeに依存して、中身がCommonJSかESMなのかが変わってしまう負の側面があったそう。
TypeScriptで設定ファイルを書けるeslint.config.ts
のサポートが可能になった。カスタムルールをいちいちトランスパイルする必要なくなった?

ぼやき
初めは以下のようにsharable config(+最低限のoverride)とoverrideのconfig objectを分けるように書いていたが、ESLint Config Inspectorでみると結局何がoverrideされているか見えにくい。
export const importConfig = [
{
...pluginImport.flatConfigs.recommended,
name: 'import recommended',
files: ['**/*.{ts,tsx}'],
},
{
name: 'import recommended override',
files: ['**/*.{ts,tsx}'],
rules: {
'import/named': 'off',
'import/namespace': 'off',
'import/default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': 'off',
},
},
]
なので一つのconfig objectにまとめると見やすくなる。1プラグインで1config objectにした方が良さそう。冗長的に書いていたfilesなども不要になるので良さそう。
export const importConfig = [
{
...pluginImport.flatConfigs.recommended,
rules: {
...pluginImport.flatConfigs.recommended.rules,
'import/named': 'off',
'import/namespace': 'off',
'import/default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': 'off',
},
name: 'import recommended',
files: ['**/*.{ts,tsx}'],
},
]

デバッグ
v9になっているなら基本はESLint Config Inspectorを使ったデバッグでokだと思う。これの登場でかなり触りやすくなった気がする。
--print-config オプション
現在適用されているルールを確認することができる。ただルールの量が多いとみるのが大変かもしれない。工夫をすればスナップショットテストができるのでESLintの知見がない人でも安心して改修に着手できるので試してみたい。
npx eslint --print-config <filename> > output.json
ESLint Config Inspector
スクラップの中で何回かすでに登場していたが改めて。添付の記事が画像付きで説明しているので雰囲気掴みたい人はどうぞ。

Tipsや作業ログをまとめています。
プラグインの Flat Config 対応状況を確認したい
以下をウォッチしておくと良さそう。
Flat Configに型定義を当てたい
以下の通り。
npm i -D @types/eslint
/** @type {import("eslint").Linter.FlatConfig[]} */
export default [ ]
Parsing error: Unexpected token エラーが発生する
おそらくparserの設定がおかしいのでそちらを確認する
(ex: @typescript-eslint/parser
を設定していない等)
.vscode/settings.jsonの設定を変える
筆者はv9まで上げたので対応不要。
.eslintignoreを削除する
--ignore-path
で指定している、または.eslintignoreでしている場合はFlat Configのignoresに移動させる
export default [
// ...other config
{
// Note: there should be no other properties in this object
ignores: ["**/temp.js", "config/*"]
}
];
npm scriptの修正
// "lint:js": "eslint --ext .tsx,.ts app/",
"lint:js": "eslint 'app/**/*.{tsx,ts}'",
--ext
が廃止になった。--ext .ts,.tsx
を使っている場合は以下のようにする必要がある
export default [
{
files: ["**/*.ts", "**/*.tsx"]
// any additional configuration for these file types here
}
];

graphql-eslint
v3系のままFlatConfigに対応させるようと思ったが、最新の記事が散見し期待した記事が見つけにくい状態だったので直接該当のバージョンのコードを見にいった(以下参照)
既にv3のタイミングからFlat Config対応はされていた模様。
docsはこちら。
parser: pluginGraphql.parser
(v4のparserの読み込み方法)にしたら10分以上解析に時間がかかった。一応解析は進むけどすごく遅くなる。parser: pluginGraphql
(v3のparserの読み込み方法)にすると期待している速さになった。
これに気づくの遅れてtypescript-eslintの型チェックの解析が遅いのか?config objectが多すぎたのか?と少し遠回りしてしまった。
次にCannot destructure property 'schema' of 'context.parserServices' as it is undefined.
のエラーが出た。fixupPluginRulesを当てることで解決。Flat Configに対応していないRule APIを使っていたのが原因。
対応としては以下と同じ。
context.getScope
-> context.sourceCode
に破壊的変更があったのをカスタムルールを作るときに知っていたので、おそらくRule APIが原因だろうなと目安が立っていたので速攻で直せた。
類似の記事は以下になる。
Flat Configではnext lintがrun commandで使えないためnpx eslint filename --cache
をしていたがgraphql-eslintのv3はどうやら対応されていないようなのでv4に結局あげた。
詳細は次の記事になる。
documentsの指定をしておらずエラーが発生した。指定したらいくつかエラーが発生した。おそらくv3だとdocumentsの指定は強制されておらず、その分解析から漏れていたのかなと。
Fragmentをファイルを跨いで使用している場合、単一ファイルだけでは情報が足りず、ルールが正しく適用されない可能性があります。
その他参考になりそうな記事

eslint-config-next(14.2.22)
@next/next/no-duplicate-head
と@next/next/no-page-custom-font
で以下のエラーが発生する。
で、これも今までと同じ原理でRules APIの対応ができていないのが原因なためfixupPluginRulesでラップする必要がある。
詳細までは追っていないが公式のアナウンス通りならv15に上げると直るかも。
@next/next/no-page-custom-font context.getAncestors is not a function
next lint でエラーが発生する
以下のissueと同じ事象が発生している。
v15に上げると直るぽい。
しかしまだv15に上げる見込みはないのでv14でnext lint相当の動きができるようにセットアップする必要がある。このときに--cacheオプションを使う必要がある(next lintではデフォルトでキャッシュが効いている)が--cacheオプションはgraphql-eslint v3では対応されていないためv4に上げる必要がある(以下参照)

eslint-plugin-import
recommended
は問題ないけどimport/typescript
(plugin:import/typescript
)はFlat Config対応されていないようなのでFlat Compatを使った対応をする必要がある。
あとは、できたらくらいの温度感だがtypescript-eslint
を使っているので不要なルールはOFFにしておきたい。
[追記]20250218
import/typescript
のFlat Config対応されていた。ドキュメントが更新されていなかったぽい。
以下のPRで更新されていた

eslint-plugin-jsx-expressions
以下のエラーが発生する。
Error: Error while loading rule 'jsx-expressions/strict-logical-expressions': You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.
こちらのissueと全く同じ状態。
で、こっちで修正PRが立てられているのでマージされたら直りそう。ということで一時的にfixupPluginRulesで対応する。

eslint-plugin-react-hooks
React Hook "useDisclosure" is called in function "〇〇: FC<Props>" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
関数コンポーネントの命名を日本語にしていた場合、最新のrules-of-hooks
を当てるとエラーになる。ヒストリからどういった変更が加えられたか確認できていないので手隙で見ておきたい。
おそらくの以下の部分で日本語が弾かれてしまう
-> 確認した結果、それっぽいルール変更があったのでバージョンを下げる必要がある。
以下でインストールできるバージョンを探す。
pnpm view eslint-plugin-react-hooks versions
eslint-config-next
のv14.2.2ではeslint-plugin-react-hooks
はv4.5を使っていたこともあったので、v4.5まで下げて解決。

どこで適用されているかわからないが、こちらが有効になったのでautosaveのタイミングで削除される。どこで適用されているか探す。
"linterOptions": {
"reportUnusedDisableDirectives": 1
},
eslint/defaults/languages
で設定されている。

typescript-eslint v8 の破壊的変更について
Flat Config移行に際しtypescript-eslintをv8まで上げたがいくつか変更があったらしく、見逃していたのでメモしておく。
Announcing typescript-eslint v8
基本は以下の通りだが実際に修正した箇所について少し触れていく
- @typescript-eslint/prefer-ts-expect-error
- 利用していなかったためスキップ
- @typescript-eslint/no-var-requires
- @typescript-eslint/no-require-imports にリプレイス
- 修正に当たって参考になった記事 -> https://github.com/scottrippey/next-router-mock/pull/90
- @typescript-eslint/no-throw-literal
- 利用していなかったためスキップ
- @typescript-eslint/no-useless-template-literals
- 利用していなかったためスキップ
- @typescript-eslint/no-loss-of-precision
- no-loss-of-precisionを有効にする(既に有効になっていたため対応不要)
-
eslint.style
- 利用していなかったためスキップ
- @typescript-eslint/prefer-nullish-coalescing
- ignoreConditionalTestsのデフォルトがfalse -> trueになった。静的解析で引っかかってなさそうなで大丈夫なそう
- @typescript-eslint/no-unused-vars
- ルールに変更があったため修正
- 修正に当たって参考になった記事 -> https://github.com/typescript-eslint/typescript-eslint/issues/10266#issuecomment-2581080684
- @typescript-eslint/ban-types
- @typescript-eslint/no-empty-object-type にリプレイス
メモ
Flat Configへのリプレイスが終わった後に判明したのでLegacyでどういったルールが適用されているか知っておかないと上記の破壊的変更の修正ができない。筆者の場合は作業ログを取っていてprint configを使ってLegacyのルールのバックアップを取っていたので助かった。
もしFlat Config移行を検討されている方がいらっしゃったらバックアップをとっておくと良いかもしれない。

仕上げ
移行が完了した後に見つけた
移行で気を付けるポイントは以下。筆者も同様のことを考えていて、特に2個目に関してはprint-debugで照らし合わせて人力でやった。
- lint を適用するファイルの範囲が同じか?
- すべてのファイルで適用されている rule・および設定、option が変わっていないか?

まだ続く
no-restricted-syntaxが複数箇所使われていてルールとしては有効になっているものの上書きされてしまっていて、一部無効化させていた。結構気を遣ってno-restricted-syntaxしないと思わぬ穴になりそうかもしれない
例えばsharable configでno-restricted-syntaxが使われている場合は以下のようにoverrideする必要があったり。
Flat Configが終わった後の後片付け
- 移行に伴ってOFFにしたreportUnusedDisableDirectivesを有効にする
- 移行に伴ってOFFにしたtypescript-eslintを有効にする
- 移行に伴ってOFFにしたgraphql-eslintを有効にする
- 無効化されていたno-restricted-syntaxを有効にする + ASTのリファクタ
- eslint-plugin-jsx-a11yを導入する
- 骨子となるconfig objectを外部モジュールに切り分けてeslint.config.mjsは上書きする設定のみ配置する
プラグインのバージョンを上げる時に気をつけること
単純にBreaking Changeがないか確認しておく
例えば今回の移行で言うならtypescript-eslintとかgraphql-eslintが該当する

TODO
- rules配下のcjsファイルを削除する
- rules配下のmjsをconfigsに移動させる
- カスタムルールをTypeScriptで書けるようにする
- import/typescriptでcompat.extendsしている箇所を削除する
- eslint-plugin-next, eslint-plugin-react-hooksのfixupPluginRulesを削除する
- eslint-plugin-jsx-expressionsは2年放置されているので代替ルール用意した方が良さそう
- カスタムルールは実装PR、適用PRで分けた方が良さそう
- カスタムルール作成のTips esaを書く

[追記]defineConfig
ざっくりとみてみると
- 型安全にする
- 配列またはオブジェクトで提供されるプラグインを平す
- JS初心者にもmodificationを楽にする
extends(defineConfig)を再導入した
Ultimately, we realized that the best way to solve this set of problems was to reintroduce extends. The defineConfig() function allows you to specify an extends array in any object, and that array can contain objects, arrays, or strings (for plugin configs that follow the recommended approach). This allows you to rewrite your configuration file in a more consistent way:
で、以下のスクラップで触れていたtseslint.configと同じじゃんという感想。上記のdocsでは代替としては名言されていなかった。