ESLint v9でNext.js 15製プロジェクトへのeslint実行時のエラー対処メモ

## この記事について

当記事は、ESLint v9とNext.js 15の組み合わせに由来すると思われるエラーへのworkaround(暫定対応)を説明します。

:::note warn
注意
当記事の内容は2025/06/22時点での暫定対応であり、Next.js公式やESLint公式が発表する対処方法ではありません。今後のアップデートで解消される可能性が高いです。
:::

## 概要

Next.js 15導入済みプロジェクトに対して、ESLint v9でlintを実行すると、以下のエラーが発生。

`Error: Failed to patch ESLint because the calling module was not recognized.If you are using a newer ESLint version that may be unsupported, please create a GitHub issue:`

```bash
PS D:\05_experiment\payload-template-eslint-error\xpicks> pnpx eslint --fix-dry-run

Oops! Something went wrong! :(

ESLint: 9.29.0

Error: Cannot read config file: D:\05_experiment\payload-template-eslint-error\xpicks\node_modules\.pnpm\eslint-config-next@15.3.0_e_c6e9941d6a1be25bc046eca583a699f1\node_modules\eslint-config-next\index.js
Error: Failed to patch ESLint because the calling module was not recognized.
If you are using a newer ESLint version that may be unsupported, please create a GitHub issue:
https://github.com/microsoft/rushstack/issues
Referenced from: D:\05_experiment\payload-template-eslint-error\xpicks\node_modules\.pnpm\eslint-config-next@15.3.0_e_c6e9941d6a1be25bc046eca583a699f1\node_modules\eslint-config-next\core-web-vitals.js
    at Object.<anonymous> (D:\05_experiment\payload-template-eslint-error\xpicks\node_modules\.pnpm\@rushstack+eslint-patch@1.11.0\node_modules\@rushstack\eslint-patch\lib\_patch-base.js:167:19)
    at Module._compile (node:internal/modules/cjs/loader:1730:14)
    at Object..js (node:internal/modules/cjs/loader:1895:10)
    at Module.load (node:internal/modules/cjs/loader:1465:32)
    at Function._load (node:internal/modules/cjs/loader:1282:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Module.require (node:internal/modules/cjs/loader:1487:12)
    at require (node:internal/modules/helpers:135:16)
    at Object.<anonymous> (D:\05_experiment\payload-template-eslint-error\xpicks\node_modules\.pnpm\@rushstack+eslint-patch@1.11.0\node_modules\@rushstack\eslint-patch\lib\modern-module-resolution.js:11:23)
```

## 環境

| 項目 | 名前 |
|:--|:--|
| OS  | Windows 11  |
| Node.jsバージョン  | v22.15.1  |
|  パッケージ管理ツール | pnpm(10.11.1)  |
|  ESLintバージョン | v9.29.0  |
| Next.jsバージョン | 15.3.0  |

## 背景(再現手順)

Payload CMSの`create-payload-app`でプロジェクトを作成後、lintをかけたら再現しました。

```bash
$ pnpx create-payload-app
$ cd ./project_name
$ pnpx eslint --fix-dry-run
```

## 想定原因

どうも`next/core-web-vitals` がESLint 9に対応しきれていないっぽい？
下のIssueで、`eslint-config-next`との関連性が議論されています。

https://github.com/microsoft/rushstack/issues/4965

## 対策

`next/core-web-vitals`と`next/typescript`の読み込み方法を変更します。

### 手順１：ESLintプラグインをインストール

```bash
$ pnpm install @next/eslint-plugin-next --save-dev
$ pnpm install @typescript-eslint/eslint-plugin@latest --save-dev
```

### 手順２：eslint.config.mjs　修正

#### 修正箇所

```diff_javascript
 import { dirname } from 'path'
 import { fileURLToPath } from 'url'
 import { FlatCompat } from '@eslint/eslintrc'
+import nextPlugin from '@next/eslint-plugin-next'

 const eslintConfig = [
-  ...compat.extends('next/core-web-vitals', 'next/typescript'),
+  {
+    name: 'next/core-web-vitals',
+    plugins: {
+      '@next/next': nextPlugin,
+    },
+    rules: {
+      ...nextPlugin.configs.recommended.rules,
+      ...nextPlugin.configs['core-web-vitals'].rules,
+    },
+  },
+  ...compat.extends('next/typescript'),
```

#### 修正後のeslint.config.mjs

```javascript
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
import nextPlugin from '@next/eslint-plugin-next'

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

const compat = new FlatCompat({
  baseDirectory: __dirname,
})

const eslintConfig = [
  {
    name: 'next/core-web-vitals',
    plugins: {
      '@next/next': nextPlugin,
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },
  ...compat.extends('next/typescript'),
  {
    rules: {
      '@typescript-eslint/ban-ts-comment': 'warn',
      '@typescript-eslint/no-empty-object-type': 'warn',
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          vars: 'all',
          args: 'after-used',
          ignoreRestSiblings: false,
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          destructuredArrayIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^(_|ignore)',
        },
      ],
    },
  },
  {
    ignores: ['.next/'],
  },
]

export default eslintConfig

```

### 手順３：修正確認

lintの実行結果が表示されれば成功！

```bash
$ pnpx eslint --fix-dry-run   

D:\05_experiment\nextjs15-eslint9-bug\test\src\Header\Component.client.tsx
  24:5  error  Definition for rule 'react-hooks/exhaustive-deps' was not found  react-hooks/exhaustive-deps
  29:5  error  Definition for rule 'react-hooks/exhaustive-deps' was not found  react-hooks/exhaustive-deps

D:\05_experiment\nextjs15-eslint9-bug\test\src\collections\Posts\hooks\populateAuthors.ts
  8:71  warning  'req' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars

D:\05_experiment\nextjs15-eslint9-bug\test\src\utilities\useClickableCard.ts
  52:5  error  Definition for rule 'react-hooks/exhaustive-deps' was not found  react-hooks/exhaustive-deps
  74:5  error  Definition for rule 'react-hooks/exhaustive-deps' was not found  react-hooks/exhaustive-deps
  95:5  error  Definition for rule 'react-hooks/exhaustive-deps' was not found  react-hooks/exhaustive-deps

✖ 6 problems (5 errors, 1 warning)
```

## おわりに

たかだかlintかけるだけなのに、結構な労力を使ってしまいました…
TypeScriptのライブラリバージョン管理の難しさは時に理不尽さを感じます。
これが個人開発ならESLintのエラーに1日費やしても笑って済ませられますが、実務でそれやったら「え、環境構築も出来ないの？」って思われかねないなと思いました。

その意味では、実務では迂闊にバージョンを上げられませんね…

また、これを機にBiomeに切り替えようかとも考えたのですが、まだNext.jsへの対応が終わっていないようなので一旦諦めました。

それと、この問題の解消をCursorにお願いしたら、「万策尽きました。」とか言って匙を投げて笑いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610123/ce11e55e-5cc8-4281-bfa6-3599473b85fc.png)