← 記事一覧に戻る

ESLint v9 flat config移行でハマった5つのポイント:.eslintrc.jsからeslint.config.jsへの実践ガイド

ESLint v9.0がリリースされ、flat config(eslint.config.js)がデフォルト設定形式になった。これまでの.eslintrc.jsはv9でもESLINT_USE_FLAT_CONFIG=false環境変数で使えるが、いずれ廃止される予定だ。

いざ移行しようとすると「プラグインが読み込めない」「ルールが効かない」「TypeScriptのパースエラー」など、じわじわとハマりポイントが出てくる。実際のプロジェクトで踏んだ5つの罠と対処法を解説する。


flat configの基本構造をおさらい

従来の.eslintrc.jsとflat configの最大の違いは、グローバルなextendsがなくなり、すべてを配列で組み合わせる点だ。

// 旧: .eslintrc.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  rules: {
    'no-console': 'warn',
  },
}
// 新: eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      'no-console': 'warn',
    },
  },
]

一見シンプルだが、この構造の変化がいくつかの罠を生む。


罠1: eslint-plugin-xxx がflat configに未対応でエラーになる

症状

TypeError: Key "plugins": Key "react": Object was not normalized.

または

Error: Plugin "react" was already defined.

原因

flat configはプラグインオブジェクトに新しい正規化要件がある。古いプラグインはflat configが想定するオブジェクト構造に対応していない場合がある。特にeslint-plugin-reactはv7.34以前でこの問題が起きやすい。

対策

プラグインのバージョンを確認し、flat config対応バージョンにアップデートする。

npm ls eslint-plugin-react
# eslint-plugin-react@7.33.x → flat config未対応
# eslint-plugin-react@7.34.x以降 → 対応済み

対応バージョンがない場合は@eslint/compatfixupPluginRulesでラップする:

import { fixupPluginRules } from '@eslint/compat'
import reactPlugin from 'eslint-plugin-react'

export default [
  {
    plugins: {
      react: fixupPluginRules(reactPlugin),
    },
    rules: {
      'react/jsx-uses-react': 'error',
    },
  },
]

罠2: ignoresの書き方が変わり、意図しないファイルがLint対象になる

症状

node_modulesdistディレクトリがLint対象になり、大量のエラーが出る。または逆に、Lintしたいファイルが除外される。

原因

旧来の.eslintignoreはflat configでは無視される。また、ignoresをconfigオブジェクト内に書くか、単独オブジェクトとして書くかで挙動が異なる。

// NG: これはこのconfigオブジェクトのfilesにのみ適用される「ローカルignore」
export default [
  {
    files: ['src/**/*.ts'],
    ignores: ['src/**/*.test.ts'], // このconfigブロック内だけで有効
    rules: { /* ... */ },
  },
]
// OK: グローバルignoreにするには、filesを持たない単独オブジェクトにする
export default [
  {
    ignores: ['dist/**', 'node_modules/**', '**/*.min.js'],
  },
  {
    files: ['src/**/*.ts'],
    rules: { /* ... */ },
  },
]

対策

グローバルな除外設定は必ず**filesを持たない独立したオブジェクト**として配列の先頭に置く。

// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'

export default [
  // グローバルignoreは必ず先頭に
  {
    ignores: [
      'dist/**',
      'build/**',
      'coverage/**',
      '**/*.d.ts',
    ],
  },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['src/**/*.{ts,tsx}'],
    rules: {
      'no-console': 'warn',
    },
  },
]

罠3: TypeScriptプロジェクトでparserOptions.projectの設定が別物になった

症状

@typescript-eslintの型情報を使うルール(@typescript-eslint/no-floating-promisesなど)を有効にすると:

Error: You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file.

原因

flat configではparserOptionslanguageOptions.parserOptionsに移動した。旧来の書き方では型情報が渡されない。

// NG: 旧来の書き方(flat configでは無効)
export default [
  {
    parserOptions: {        // ← ここに書いても効かない
      project: './tsconfig.json',
    },
  },
]

対策

languageOptionsの下にparserparserOptionsを移す:

import tseslint from 'typescript-eslint'
import tsParser from '@typescript-eslint/parser'

export default [
  ...tseslint.configs.recommendedTypeChecked,
  {
    files: ['src/**/*.{ts,tsx}'],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: './tsconfig.json',
        tsconfigRootDir: import.meta.dirname, // Node.js 20.11+
      },
    },
  },
]

import.meta.dirnameが使えない環境(Node.js 20.10以前)では:

import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __dirname = dirname(fileURLToPath(import.meta.url))

export default [
  {
    languageOptions: {
      parserOptions: {
        project: './tsconfig.json',
        tsconfigRootDir: __dirname,
      },
    },
  },
]

罠4: eslint-config-prettierの適用順序でルールが効かなくなる

症状

Prettierと競合するESLintルールを無効化するためにeslint-config-prettierを入れたのに、indentquotesなどのルールが依然として発火する。または逆に、意図したルールが無効化される。

原因

flat configではextendsの代わりに配列の順序でルールが上書きされる。eslint-config-prettier途中に挟むと、後続の設定がPrettier無効化より優先されてしまう

対策

eslint-config-prettierは必ず配列の最後に置く:

import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettier from 'eslint-config-prettier'

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    // プロジェクト固有のルール
    rules: {
      'no-console': 'warn',
      '@typescript-eslint/no-explicit-any': 'error',
    },
  },
  prettier, // ← 必ず最後!Prettierと競合するルールをすべて上書き
]

罠5: eslint --initで生成されたconfigがv8形式でv9に通らない

症状

npm init @eslint/config@latestを実行して生成されたeslint.config.jsをそのまま使ったら動いたが、既存の.eslintrc.jsを手動でflat configに変換したら動かない。

Error [ERR_REQUIRE_ESM]: require() of ES Module eslint.config.js not supported.

または

SyntaxError: Unexpected token 'export'

原因

flat configはESモジュール形式で書く必要があるが、プロジェクトが"type": "commonjs"(または未設定)の場合、eslint.config.jsがCJSとして解釈される。

対策

2つの方法がある。

方法A: ファイル拡張子を.mjsにする

mv eslint.config.js eslint.config.mjs

方法B: package.json"type": "module"を追加する

{
  "type": "module"
}

ただし方法Bはプロジェクト全体のモジュール形式が変わるため、他のCJS形式のconfigファイル(jest.config.jsなど)も影響を受ける。CJSで書かれたファイルを.cjsにリネームする必要があるケースもある。

プロジェクト全体の移行推奨手順:

# 1. flat config移行ツールを使う(旧.eslintrcを自動変換)
npx @eslint/migrate-config .eslintrc.js

# 2. 生成された eslint.config.mjs を確認・調整

# 3. 旧設定ファイルを削除
rm .eslintrc.js .eslintignore

# 4. 動作確認
npx eslint src/ --debug 2>&1 | head -30

まとめ

ESLint v9 flat config移行でよくある罠をまとめると:

原因 対策
プラグインがエラー 旧プラグインが未対応 バージョンアップ or fixupPluginRules
ignoresが効かない files付きオブジェクト内に書いた 単独オブジェクトとして先頭に置く
型情報ルールがエラー parserOptionsの場所が違う languageOptions.parserOptionsに移動
Prettierルールが残る 適用順序が間違い eslint-config-prettierを最後に
ESMエラー CJSプロジェクトで.jsを使用 .mjsにリネーム

移行時はnpx @eslint/migrate-configを使って自動変換し、そこからプラグインのバージョンやlanguageOptionsの設定を調整するのが最も確実なアプローチだ。

公式マイグレーションガイドも合わせて参照してほしい。