🎶

ESLint v9 に色々つまづきながら移行した話

2024/12/12に公開

はじめに

この記事は「コネヒト Advent Calender 2024」の12日目の記事です。
https://adventar.org/calendars/10480

この記事で取り扱うこと

  • ESLint v9 にどのようなステップで移行したかの説明
  • Flat Config に対応した設定ファイルの生成方法・修正について

この記事で取り扱わないこと

  • 各種 ESLint のルールについての説明
  • Flat Config についての詳細な説明

ESLint v9について

今年の4月に ESLint のメジャーバージョンがv9.0にアップデートされ、10月に8系を含む以前のバージョンが EOL となりました。
https://eslint.org/blog/2024/04/eslint-v9.0.0-released/
https://eslint.org/blog/2024/09/eslint-v8-eol-version-support/
この記事を書いている現在(2024/12)の最新バージョンはv9.16.0となっております。
v9.xへのアップデートに伴い、サポートする設定ファイルの形式が変更され、「Flat Config」と呼ばれる形式に完全に移行されたり、v18.18.0以前とv19.xの Node.js のサポートが打ち切られるなど、いくつか Braking Changes がありました。そのため、社内のリポジトリで下記の公式ドキュメントを参考にv9.16.0への移行を実施しました。この記事では移行した手順と、ハマったポイントを実際に出力されたエラーログを交えつつ紹介したいと思います。
https://eslint.org/docs/latest/use/migrate-to-9.0.0

最新バージョンへの移行

※該当リポジトリは TypeScript v5.7 で動作確認を行なっております。

1.パッケージマネージャーでESlintの最新バージョンをインストール

パッケージマネージャーは yarn を使っています。まず ESLint 本体といくつかの関連パッケージの最新バージョンをインストールしました。

yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser --latest     

インストールし終わったら、設定ファイルなどは特に書き換えず無邪気に$ eslint ./を実行してみます。

(node:20287) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files
(Use `node --trace-warnings ...` to show where the warning was created)

Oops! Something went wrong! :(

ESLint: 9.16.0

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

エラーメッセージの内容

前述の「Flat Config」に対応する形で設定ファイルの内容を統合・変更しなさいというメッセージが表示されました。
要約すると,

  • .eslintrc.*ファイルと.eslintignoreはもうサポートしてないよ
  • 新しい設定ファイルの形式は.eslint.config.(js|mjs|cjs)だよ
  • .eslintrc.*.eslintignoreを統合して新しい設定ファイルに書き換えてね
    といったことを警告してくれます。

2.Flat Config形式の設定ファイルの生成

さて、バージョン移行の要となるのが既存の設定ファイルを Flat Config の形式に書き換える作業です。とはいえ、Migration Guide のドキュメントとにらめっこしながら1から書き換える必要はなく、ESLint 公式が用意してくれたファイル以降のコマンドを実行すれば元々の設定ファイルをほぼ引き継ぐ形で Flat Config 形式の設定ファイルを新しく生成してくれます。
https://eslint.org/docs/latest/use/configure/migration-guide

.eslintrcの記述形式(拡張子)にはyml, json, jsなどいくつか選択肢がありますが、該当リポジトリでは.js形式を採用していたため、以下のコマンドを実行して新しい設定ファイルを生成しました。

npx @eslint/migrate-config .eslintrc.js

公式ドキュメント内では「.eslintrc.jsの形式にはまだうまく機能しない可能性がある」との記述がありましたが、当方では特に問題なく移行ファイルを作成することができました。ただし、.eslintrc.js内に何らかの条件式やロジックを埋め込んでいる場合はこの限りではありません。
リポジトリはバンドルツールに webpack を使用している React ベースのアプリケーションのため、ES Modules に対応した.mjs形式の設定ファイル(eslint.config.mjs)が生成されました。長くなるためトグルの中を開いてご覧ください。

生成された設定ファイル
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import _import from "eslint-plugin-import";
import jest from "eslint-plugin-jest";
import react from "eslint-plugin-react";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
    baseDirectory: __dirname,
    recommendedConfig: js.configs.recommended,
    allConfig: js.configs.all
});

export default [{
    ignores: [
        "**/node_modules",
        "**/vendor",
        "**/webroot",
        "**/*.config.js",
    ],
}, ...fixupConfigRules(compat.extends(
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:import/errors",
    "plugin:import/typescript",
    "plugin:react/recommended",
    "prettier",
)), {
    plugins: {
        "@typescript-eslint": fixupPluginRules(typescriptEslint),
        import: fixupPluginRules(_import),
        jest,
        react: fixupPluginRules(react),
    },

    languageOptions: {
        globals: {
            ...globals.browser,
            ...globals.node,
        },

        parser: tsParser,
        ecmaVersion: 2020,
        sourceType: "module",

        parserOptions: {
            ecmaFeatures: {
                jsx: true,
            },
        },
    },

    settings: {
        react: {
            version: "detect",
        },

        "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
    },

    rules: {
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-empty-interface": "off",
        "@typescript-eslint/no-unused-vars": "error",
        "no-extend-native": "error",
        "no-unused-vars": "off",
        "sort-imports": "off",

        "import/order": ["error", {
            alphabetize: {
                order: "asc",
            },
        }],
    },
}, {
    files: ["**/*.tsx"],

    rules: {
        "react/prop-types": "off",
    },
}, ...compat.extends("plugin:jest/recommended", "plugin:jest/style").map(config => ({
    ...config,
    files: ["**/__tests__/*"],
})), {
    files: ["**/__tests__/*"],

    languageOptions: {
        globals: {
            ...jest.environments.globals.globals,
        },
    },
}];

3.生成された設定ファイルの修正

fixupConfigRules, fixupPluginRules で書かれている箇所の修正

fixupConfigRules,fixupPluginRulesはESLint v9.xに対応していないプラグインを Flat Config 上で有効化するために提供されている Wrapper です。旧設定ファイルのextends:内に記載のあるプラグインは自動的にfixupConfigRulesで以下のようにラップされていましたが、調べたところいずれのプラグインも Flat Config に対応していたため、yarn のコマンドで最新版をインストールしました。

yarn add -D eslint-plugin-import eslint-plugin-jest eslint-plugin-prettier eslint-plugin-react --latest

その後、設定ファイルを以下のように書き換えました。

-}, ...fixupConfigRules(compat.extends(
-    "eslint:recommended",
-    "plugin:@typescript-eslint/recommended",
-    "plugin:import/errors",
-    "plugin:import/typescript",
-    "plugin:react/recommended",
-    "prettier",
-)), {
+{
    plugins: {
-        "@typescript-eslint": fixupPluginRules(typescriptEslint),
+        "@typescript-eslint": typescriptEslint,
-        import: fixupPluginRules(_import),
+        import: _import,
+        prettier,
         jest,
-        react: fixupPluginRules(react),
+        react
    },

ここまで修正が終わったところで、.eslintignoreを消去して再度$ eslint ./を実行してみました。(.eslintignoreを消去しないと実行エラーとなります)

$ eslint ./

Oops! Something went wrong! :(

ESLint: 9.16.0

TypeError: Key "languageOptions": Key "globals": Global "AudioWorkletGlobalScope " has leading or trailing whitespace.
    at new Config (/Users/myname/projects/repository/node_modules/eslint/lib/config/config.js:195:19)
    (省略)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
ERROR: "lint:eslint" exited with 2.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

見慣れないエラーに出くわしました。どうやらglobalsというパッケージにエラーがあるようだということはわかったのですが、解決方法の見当がつかない・・・とりあえずnpm ls globalsで依存パッケージを調べて更新したり、該当エラーでググったりしてみたが解決せず。

該当のエラーについての GitHub の Issue
https://github.com/sindresorhus/globals/issues/239

4.不足していたパッケージを追加でインストール

頭を捻らせていましたが、コンソールを遡っていたところ、手順2で設定ファイルを生成するコマンドnpx @eslint/migrate-config .eslintrc.jsを入力した後、下記のメッセージが表示されていたことに気づきました。

Migrating .eslintrc.js

WARNING: This tool does not yet work great for .eslintrc.(js|cjs|mjs) files.
It will convert the evaluated output of our config file, not the source code.
Please review the output carefully to ensure it is correct.

Also importing your .eslintignore file

Wrote new config to ./eslint.config.mjs

You will need to install the following packages to use the new config:
- @eslint/compat
- globals
- @eslint/js
- @eslint/eslintrc

You can install them using the following command:

npm install @eslint/compat globals @eslint/js @eslint/eslintrc -D

「下記のパッケージが必要になるからちゃんとインストールしておいてね」という丁寧な説明があったのにも関わらず、見落としていたようです…とりあえずglobalsを新たにインストールすることと、他の@eslint関連のパッケージをインストールしなければならないことに気づけて幸いでした。
上記のリストのうち、@eslint/compatは設定ファイル内で import する必要性がなくなったため、残りのパッケージをインストールして続行してみます。

yarn add -D globals @eslint/js @eslint/eslintrc -D

おそらくこれで準備は整っただろう、という段階まで辿り着きました。さっそくもう一度$ eslint ./を実行してみました。

/Users/myname/projects/repository/eslint.config.mjs
   6:1  error  `@typescript-eslint/parser` import should occur before import of `eslint-plugin-import`     import/order
   7:1  error  `eslint-plugin-prettier` import should occur before import of `eslint-plugin-react`         import/order
   8:1  error  `node:path` import should occur before import of `@typescript-eslint/eslint-plugin`         import/order
   9:1  error  `node:url` import should occur before import of `@typescript-eslint/eslint-plugin`          import/order
  10:1  error  `@eslint/js` import should occur before import of `@typescript-eslint/eslint-plugin`        import/order
  11:1  error  `@eslint/eslintrc` import should occur before import of `@typescript-eslint/eslint-plugin`  import/order

詳細な出力結果は省きますが、無事に ESLint が動作しました!🎉
見落としていましたが、上記ログのようにeslint.config.mjs自体にもimport/orderでエラーを指摘されてしまったので修正しておきましょう。

長くなりましたが、このアップデート記録が誰かの役に立てば幸いです。
それでは、よき ESLint ライフを!

余談

@typescript-eslint/eslint-plugin@typescript-eslint/parserは手順1でESLint本体と合わせて最新版をインストールしており、どちらも Flat Config で使用できるオプションとして動作することを確認しています。
しかし、環境によって下記の記事にあるようにtypescript-eslintをインストールし、上記2つのプラグインをアンインストールする作業が必要な場合もあるかもしれません。
https://zenn.dev/hsato_workman/articles/728e1551ab8b36#%40typescript-eslint-の移行

Discussion