ムキムキマッチョマン

心も体もムキムキマッチョマン

ESLint職人の朝は早いーー。ESLint v9.0.0への移行とflat config対応

こんにちは。ムキムキマッチョマンになりたい人です。心も体もムキムキマッチョマン。 Webフロントエンド領域でのムキムキマッチョマン目指して頑張っています。 この記事は技術記事の皮を被った、IT知識トレーニングの記録です。

導入: ESLintとは何だったのか?

ESLintといえば、Node.js環境下においてスタンダードなルール=規則ベースのリンター(静的検査ツール)です。

特徴としてESLintの登場した2013年の他リンターになかった、その拡張性の高さが挙げられます。

pluginやごとのextendsといったプロパティに有志が作成したカスタムルールやルールの設定を組み合わせることで複雑なリンティングやが実現可能になり、よりコード全体を安全な状態に保つことができます。 また、rulesにリンティング規則を記述することで既存の設定を上書きできることなどその機能性の高さと柔軟さで多くのJSerに受け入れられてきました。 特にVS CodeのESLint拡張機能にある自動修正機能を利用することで、ファイル保存時にリンターに怒られが発生した箇所を削除させ余計な行や使わない変数、不要なインポートの削除などができることもその強みの1つとして挙げられます。

ですが、ESLintの問題の1つとしてその柔軟性・拡張性ゆえの設定ファイルの書きづらさ、書くことのめんどくささが挙げられます。従来は.eslintrc.jsといった名称の設定ファイルを作成しそこに様々なルールを書く必要がありました一度作られ、フレームワークやライブラリが変わったのに更新されなかったり、コーディング規約が変わったのにrulesが変更されていないなど放置された.eslintrc*はNode.jsプロジェクトのある種スタンダードとなってしまっています。

この記事ではムキムキマッチョマンへの道として、最新のESLint設定方式であるflat configへの理解と一歩進んだESLintの知識をキャッチアップすることを目的としています。

flat configとESLint v9.0.0の登場

2019年にESLintの作者であるNicholas C. Zakas氏より1つのRFCが提案されました。 それがConfig File Simplification(無理やり訳すると設定ファイルの単純化とか?)です。 従来の.eslintrcでは不可能な拡張への対応に向けて、設定ファイルの構造自体を変更し単純化するとともに柔軟性を高めることを目的とした提案でした。

例えば2017年に挙げられたissue、Extending rule options in derived configs? #994では、ユーザーカスタムの共有コンフィグ*1ですでに設定されたルールの値を上書きすることはできても、そのオブジェクトや配列の一部のみを変更または追加はできないことがissueとして挙げられています。

これら問題への対応として、既存の設定ファイル構造を変えるのではなく(影響範囲が大きいため)新しいファイル構造と設定システムを導入することにしたのがflat configでした。

flat configについて詳しいことは後述しますが、従来のextendsとpluginsの組み合わせによる1つのファイルに全て書き込む方式ではなく、extendsやrulesのグループを1つの設定=configとして捉え、それらを横並びで記述していくこと=flatflat configの基本になります。ここのflatJavaScriptのflatMapにおけるflatと同じ意味を持ち、設定を次元数1の配列に記述して行く方式とも捉えられます。(ここも後述しますが配列内の優先順位は後勝ちのため、詳細度の高いルールを後ろに記述する形になります)

2022年8月1日にリリースされたv8.21.0にて、APIが初期実装され、2023年10月10日にNicholas氏によるflat configへの移行プランが公開されました。この時点ではflat configは環境変数によるopt-in方式(利用を明示しないと使えない)機能として提供されていました。 段階的な移行としてv9.0.0でのデフォルト化、2024年末〜2025年にかけてリリースされるv10.0.0での.eslintrcの完全廃止が掲げられました。

そしてこの記事を書いている4月6日、現地時間だと4月5日にv9.0.0がリリースされました!偉業!おめでとうございます... というわけで7時おき現在、記事を執筆しています!頑張るぞ!

もちろんそれ以外にもBreaking ChangesがいっぱいあるのでよしなにMigration Guide読んでください。

eslint.org

.eslinrc時代に立ち返る

flat config への対応をして行く前段階としてESLintとはそもそも何なのか、その拡張性をどう保ってきたのか?を探ります。ここでは前述のsharable config(共有コンフィグ)が鍵を握ります。

ESLintの概観は以下の一言でまとめられると考えています。「膨大なルールベースによる静的検査と、ルールそのものの追加と設定ファイルの共有による拡張性の高さ」これを実現するための機能がsharable config(共有コンフィグ)です。

blog.ojisan.ioこと統合開発環境さんの以下の記事で詳しくまとめられていますので、この記事をしっかり読んでもらえるのがベストです。

blog.ojisan.io

かいつまんで本当にざっくりまとめると以下の通りで

  • ESLintは独立したルール1つ1つの組み合わせで静的検査を行っている
  • extendsで読み込んでいるのは、ESLintの設定オブジェクトそのもの(なのでプラグインとかも読み込んでいる)
  • pluginsはルールそのものを追加する実装

上記2行目がsharable configを指しています。

このsharable config1つ1つを一次元の配列に記述していくこと=flat configであるという認識で良いと思います。

ちなみにeslint-config-hogehogeという名前でもextendsではhogehogeと書くだけで設定が読み込めます。これはESLint側の命名規則としてeslint-config-hogehoge命名されていればhogehogeを参照して設定を読み込む機能があるからです。

ここでは触れないのですがflat config以降は開発者向けに、依存しているプラグイン等のパッケージ情報をpeerDependenciesではなく、dependenciesに記述してねみたいなことが書いていたりします。詳しい話は現段階だと理解不足なのでパス!すません!

flat config への対応

今回は最新のv9.0.0を利用する前提です。v8.xでは環境変数からopt-inする必要があったのですが面倒なのでパス! 変更点をまとめる形で対応を考えます。

ファイル名の変更とESModulesの採用

まずファイル名が変わります。.eslintrcではなく、eslint.config.(js|mjs|cjs)です。お好みのモジュール解決システムに合わせたファイル名を記述してください。v9.0.0以降は.estlinrcは読み込まれず、eslint.config.(js|mjs|cjs)が読み込まれます。そのため既存の.eslintrcを残したまま移行することが可能です。

従来 .eslintrc では ESModulesが使えませんでした。ですが、flat configからは対応しています。export defaultで記述できます。個人的にメチャクチャ嬉しいですね。*2

FlatConfigとFlatConfigArray

.eslintrc でexportしていたのはObjectでしたが、今後はFlatConfigArrayになります。 FlatConfig1つ1つがObjectで、そこにrulesfilesを記述していく形です。

/// オブジェクト
module.exports = {
}
export default [
  {
  /// 1つ1つが設定オブジェクト
 }
]

FlatConfigについて

FlatConfigオブジェクトの基本は以下の通りです。

{
  name: "eslint-config-hogehoge", // Errorメッセージなどに表示される名称、なくても良い
  files: [""], // 静的検査にかけるファイルを指定
  ignores: [""], // 静的検査をかけないファイルを指定
  languageOptions: {}, // JavaScript解釈のための設定を行う。ecmaVersionの指定や, tsconfigの設定などはこちら
  linterOptions: {}, // ESLintの細かい設定が可能。inline configをOKにするかどうかなど
  processor: "", // pluginなどで提供されるプロセッサを使いたい場合は名前を記述
  plugins: { // pluginをname-valueで記述する。従来の配列は使えない
    pluginName: pluginObject 
  },
  rules: {}, // 従来通りにrulesを指定可能
  settings: {} // name-valueで記述できるconfig向け設定。あんまり使わない
 }

詳細:Configuration Files - ESLint - Pluggable JavaScript Linter

ここで重要になってくるのがrulespluginsが、filesignoresにマッチしたファイルのみにしか適用されないということです。

つまり、jsxには適用させたいがtsxには適用させたくないルールや、*.test.tsには適用させたいが*.spec.tsには適用させたくないルールといった形で設定を分離して表現することが可能になります。

export default {
  jsCommonConfig,
  tsCommonConfig,
  {
    files: ["**/*.js","**/*.mjs"],
    ignores: ["**/*.test.*, **/*spec.*"],
    rules: {} // testファイル以外のjsファイルに適用させたいルールを記述
  }
}

新しいsharable config

flat configに対応しているconfigでは、ドキュメントに記載されている方法に従って、そのオブジェクトをflat configとして読み込むことが可能です。 従来一般的だったeslint:recommendedは公式パッケージ@eslintrc/jsよりrecommendedオブジェクトとして配布されます。*3

import js from "@eslint/js";

export default [
    js.configs.recommended, // これだけでrecommendedの読み込みが可能です
    {
        files: ["**/*.js"],
        rules: js.configs.recommended.rules // この記述自体意味を持ちませんが、こういった形でrulesだけを呼び出すことも可能です
    },

]

pluginsについて

"hogehoge"というプラグインがあった場合、従来は配列に格納していましたが、flat configに対応していればname-valueのオブジェクトに書き写すだけで問題なく動きそうです。

import hogehoge from "eslint-plugin-hogehoge";

...
   plugins: {
     "hogehoge":  hogehoge
   },
...

移行にあたって利用しているパッケージの公式ページもしくはリポジトリを「hogehoge flat config」といった形で検索し、移行状況を確認し対応方法が明記されている場合はそれに従って記述してください

@eslint/eslinrcFlatCompatによる後方互換

では対応していないconfig、pluginはどうすれば良いのでしょうか? 公式が後方互換のためのパッケージとFlatCompatを利用した方式を提供しています。

@eslint/eslinrcパッケージから提供されるFlatCompatクラスのインスタンスを利用して既存のextendsを利用する方法が推奨されています。*4

以下公式ドキュメントより抜粋。

import { FlatCompat } from "@eslint/eslintrc";
import path from "path";
import { fileURLToPath } from "url";

// CommonJSで提供されている__dirnameの偽装。Node v20 移行であれば import.meta.dirnameも利用可能
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ディレクトリ指定等
const compat = new FlatCompat({
    baseDirectory: __dirname
});

export default [
    // 以下のように従来と同様のextendsが可能。以下でFlatConfigと等価
    ...compat.extends("eslint-config-my-config"),
];

2023年時点での振る舞いについて検証してくださっている方がいました。大感謝

t28.dev

.eslintignoreの廃止

ignoresに書きましょう。

今後のESLint設計について

v9.0.0の登場により、詳細度ごとに設定を記述できるようになりました。rootオプションは全てにおいてtrueとなっており、モノレポへの対応なども変わってきます。 そこで現時点で筆者が把握している範囲でのベストプラクティスっぽい設計の提案です。これは公式ドキュメントに書かれているものではなく筆者個人の見解のため、誤りを含む場合があります。設計法の起点にできればとは思いますが、これがスタンダードとは考えていないためこれをもとに個々人が独自にベストな方式を生み出していただければ幸いです。

① ただ一つのeslint.config.js

以下のissueではflat configは1つのファイルで管理するのが良いのかどうかを議論しています。 これについて2023年3月9日時点でNicholas氏は開発メンバーの意見を引用する形で、公式では複数ファイルをサポートしておらず単一のファイルで書くことを推奨しています。 github.com

そのため、ディレクトリルートに1つファイルを置く形です。

② プロジェクト全体の設定と個々のフォルダの設定、テスト向けの設定などを適切に書き分ける

filesignoresを活用した設定が重要になると考えます。 特定のフォルダ内に適用したいものはfilesを詳細に記述、その中でもignoreしたいものはignoresに記述してく形が現時点ではベターな方式だと考えます。

また、今後の拡張を考えたときに汎用性の高い一般的な規則はjsCommonConfigsといった形でまとめ、各パッケージのルールはpackageAConfigのように管理し、記述して行くのが良いかもしれません。

となってくるとFlatConfigArray内の前半は汎用的な共通記述、後半は排他的な固有の記述といった形です。 先に汎用的なものを記述しつつ、後半はできる限り影響範囲が小さくなるようfilesignoresを追加するのが良意でしょう。

typescript-eslintが提供するtseslint.configを利用した型情報付の記述

typescript-eslintflat configに対応した際にtseslint.config関数を利用することでFlatConfigの型情報が適切に付与された状態で記述できるようになりました。以下の利用がベストでしょう。

以下公式ドキュメント抜粋です。

// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
);

github.com

最後に

ESLintは長く続くリンターであり、私たちJSerの開発において切っても切れない存在となってきています。 新興のリンター登場によりその立場が危ぶまれつつも、その拡張性による表現力の高さは他に類を見ません。 ぜひ本記事が今後のESLint活用の役に立つと幸いです。

間違いがあればコメントいただけるとありがたいです。

*1:eslint-config-** で表現されるnpmパッケージとして配布される設定群のことを指します。英語ではsharable configとして表記され、ESLintコミュニティの中では一般的な表現として使われているため日本語訳では共有コンフィグと表現します。

*2:デフォルトはCommonJSのため、.mjsファイルに記述するかpackage.jsonのtypeにmoduleを指定するかどちらかが必要です。

*3:npm install @eslint/js -D にてインストールが必要です

*4:npm install -D @eslint/eslinrc でインストール必須