🐙

ESLintのconfigがどのように変わり得るか(flat configとは何か)

2022/08/12に公開

はじめに

https://eslint.org/blog/2022/08/new-config-system-part-2/
ESLint v8.21.0のリリースでこれまでとは異なるconfigシステム(flat config)が持ち込まれた。

以下の通り、新しいconfigシステムへの一歩というように言及があり、ESLintを利用する上で無視できない影響を受ける変更となりそうなので、どのようなものか確認しておきたい。

We took a big step toward ESLint’s new config system! The new FlatESLint class is now merged. Its API is not yet stable, and not all features are implemented yet, but it is accessible via the Node.js API for early testing. See RFC9 for the original design.

https://eslint.org/blog/2022/08/eslint-v8.21.0-released/

flat configとは何かを大雑把にまとめてしまうと

シンプルさを重視した新しいconfigのこと。
これまでのconfigがさまざまな経路で拡張や上書き可能なことで設定コストが高くなっていたのを、1つのファイルの配列に集約することで設定が容易になることを意図していると思う。
また、デフォルト値を今のJavaScriptの状況に合わせたり、これまで整理されていなかったconfigの構成を妥当性のあるように変更したりもしている。

これまでのconfig(eslintrc)とflat configを比較する

表にして比較してみると主な相違点は以下のようものがあると思う。なお便宜上これまでの設定をeslintrcとする。

項目 eslintrc flat config
configファイル .eslintrc.js.eslintrc.yml.eslintrc.jsonpackage.jsoneslintConfigフィールド)などさまざま eslint.config.jsのみ
configの探索 config cascadingによってroot: trueもしくはルートディレクトリまでconfigファイルを探し続ける カレントディレクトリか最も近い上位のディレクトリのeslint.config.js
configのカスケーディング ファイルシステムベースやoverridesなど eslint.config.jsでのconfigオブジェクトの配列
ecmaVersionのデフォルト 5 latest
sourceTypeのデフォルト script module(ESM)かcommonjs(CommonJS)
対象ファイルのデフォルト *.js *.js*.mjs.cjs
JavaScriptの解釈に関するオプション globals, ecmaVersion, sourceType, envなど languageOptionsに集約
プラグインの利用 文字列での指定 モジュールとして明示的にimport

eslint.config.js

flat configではカレントディレクトリかより上位のディレクトリからeslint.config.jsをconfigとして探す。見つかった時点でそれ以上探すことはなく、そのファイルだけを利用する。
eslint.config.jsがこれまでとどのように異なるか実際にイメージするため、適当な内容ではあるけどもeslint.config.jsを以下に記述してみる。

eslint.config.js
import graphqlEslint from "@graphql-eslint/eslint-plugin" // プラグインは明示的にimportするように
import eslintImport from "eslint-plugin-import"

// 配列にconfigオブジェクトを並べる。後方のconfigオブジェクトが優位となって上書き・マージされていく。
export default [
  // ESLintのあらかじめ用意された eslint:recommended、eslint:all のconfigは文字列で配列に含めることができる。
  "eslint:recommended",
  {
    // pluginの指定がfiles指定なしで行われた場合、全てのファイルを対象としてpluginが有効になる
    plugins: {
      eslintImport,
    },
    rules: {
      "eslintImport/default": "error",
    },
  },
  {
    // ignoresのみを指定したconfigオブジェクトで.eslintignoreのようにグローバルに無視するファイルを指定できる(.eslintignoreのパターンの後に追加となる)。
    ignores: ["./lib/**/*"],
  },
  {
    rules: {
      // 後方のconfigオブジェクトが前方のconfigオブジェクトをマージ(競合したら上書き)するので、semiのoffは後方のerrorによって上書きされる
      semi: "off",
    },
  },
  {
    files: ["./**/*.js"],
    ignores: ["./*.config.js"],
    // ESLintのJavaScriptの解釈に関わることはlanguageOptionsのオプションで設定する。
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: "commonjs",
      globals: {
        FOO: "readonly",
      },
    },
    rules: {
      semi: "error",
    },
  },
  {
    files: ["./src/bar/*.js"],
    languageOptions: {
      // 後方のconfigオブジェクトが前方のconfigオブジェクトをマージ(競合したら上書き)するので、globalsにはFOOとBARが含まれた状態になり、FOOは書き込み可能として扱う。
      globals: {
        FOO: "writable",
        BAR: "readonly",
      },
    },
    // Linter特有のオプションをlinterOptionsに。
    linterOptions: {
      // /* eslint semi: error */のようなインラインコメントを無効にする。
      noInlineConfig: true,
      // /*eslint-disable-next-line*/ のようなLint無効化のコメントが不要になっている場合にリポートする。
      reportUnusedDisableDirectives: true,
    },
  },
  {
    files: ["./src/**/*.graphql"],
    languageOptions: {
      // parserに関するconfigもlanguageOptionsで設定する。
      parser: graphqlEslint,
      parserOptions: {
        schema: "./schema.graphql",
      },
    },
    plugins: {
      // pluginsにはプラグインのキー名とプラグイン自体の組み合わせで指定する。
      // このキー名がそのままrulesにおけるルール名やprocessorなどのプレフィックスとなる。
      graphql: graphqlEslint,
    },
    processor: "graphql/processor",
    rules: {
      "graphql/known-type-names": "error",
    },
  },
]

個別の内容についてもう少し見ていく。

configオブジェクト

配列に指定するconfigオブジェクトは、全体としては以下のようなプロパティ構成になる。

files: ["./**/*.js"], // Lint対象のファイルのglobパターン
ignores: ["./*.config.js"],// Lint対象外のファイルのglobパターン。filesにマッチしているファイル全てを対象としてパターンマッチする。
languageOptions: {// JavaScriptをどのように解釈するかのオプション群。
  ecmaVersion: "latest",// サポートするECMAScriptのバージョン。
  sourceType: "module", // JavaScriptのコードの種別。scriptかmoduleかcommonjsか。
  globals: {// グローバルに定義しておくべき値のまとまり。
    Promise: "off",
  },
  parser: "pluginName/parserName",// parser名。
  parserOptions: {...},// parserのオプション群。
},
linterOptions: {// Linter特有のオプション群。
  noInlineConfig: false,// ESLintのインラインコメントを無効にするかどうかの真偽値。
  reportUnusedDisableDirectives: false,// Lint無効化のコメントが不要になっている場合にリポートするかどうかの真偽値。
},
processor: "pluginName/processorName", // processor名。
plugins: {// プラグイン群のオブジェクト。プラグイン名のキーとパッケージのオブジェクトの組み合わせ。
  pluginName: pluginName,
}
rules: {// ルール群のオブジェクト。
  "ruleName": "error",
},
settings: {...}// 全てのルールで共通して参照する情報のオブジェクト。

https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects

論理的なデフォルト値

混乱を減らすようにデフォルト値が見直されている。ECMAScriptのバージョンやESModuleに関連して現在のJavaScriptの状況を反映したものになっている。
具体的な内容としては以下のようなもの。

  • ecmaVersion: "latest"
    • 常に最新のバージョンを選択するように。手動でecmaVersionを指定することは基本的になくなるはず
  • sourceType
    • デフォルトでesmoduleとして扱うように。
    • sourceType: "module"
      • .js.mjs
    • sourceType: "commonjs"が追加に
      • .cjs向け
  • .js,.mjs,.cjsを探すように
    • --extで指定しなければ.jsだけを探していたけどもこれらの拡張子もみるようになった

globベースでの設定をあらゆる場所で可能に

これまでoverridesでは可能だったglobベースでの対象ファイルの設定が、flat configでは全てのconfigオブジェクトで利用可能となる。これによってconfiguration cascadingによる設定の上書きを代替可能になる。

{
  files: ["./src/**/*.js"],
  ignores: ["./*.config.js"],
  ...

extendsプロパティは無くなる

eslint.config.jsでの配列において、先頭のconfigオブジェクトから末尾のconfigオブジェクトに至るまでカスケーディングする。以下のようなルールのコンフリクトがあれば常に後方のconfigオブジェクトが優先される。
Shareable Configsとしてeslint-config-*のようなconfigのパッケージを全体のベースとして使いたいケースがあると思うけど、配列の先頭に含めることでベースとして扱うことになる。

import fooConfig from "eslint-config-foo";

export default {
  fooConfig,
  {
    ...,
  },
}

languageOptionsの追加

globals, ecmaVersion, sourceType, envなどJavaScriptをどう解釈するかの設定が混乱しやすい構成になっているので、それらの設定はlanguageOptionsへ集約するように。
envはもはや必要性がないということで廃止となっていて、環境に関する情報の設定はlanguageOptions.globalsが代替となり得る。
globalsパッケージがESLintでの環境に関する情報を全て持っている。

https://github.com/sindresorhus/globals

plugin

既存のプラグインは変更せずともこれまで通り動作するはず。flat configでは文字列での指定ではなく明示的にimportしたそのものを利用する。
プラグイン名はユーザの自由となり、そのネームスペースもユーザの自由となる。
プロジェクトローカルでのカスタムルールの追加には--rulesdirオプションが必要だったけども、それもただカスタムルールのモジュールをimportすれば良いだけに。

import customRule from "./custom-rule.js";

export default [
  {
    files: ["./src/**/*.js"],
    plugins: {
      custom: {
        rules: {
          customRule,
        },
      },
    },
    rules: {
      "customRule": "error",
    },
  },
];

後方互換

@eslint/eslintrcFlatCompatによってflat configでこれまでの設定の互換を保つことが可能になる。

https://github.com/eslint/eslintrc

flat configに至った背景

flat config導入に至った背景については以下のブログ記事にまとめられていて、現在のconfigがどのように複雑になっていったか過去の経緯を振り返っている。
https://eslint.org/blog/2022/08/new-config-system-part-1

主に以下のような機能追加や修正を加えていった結果、複雑なconfigになってしまったので、その反省からflat configへと至ったということのよう。

  • extends
    • npmパッケージに設定を切り出して利用することが可能になった
    • eslint:recommendedによって推奨されるルールを明示することができた
  • ユーザ固有のconfigファイル
    • 設定ファイルが見つからなければ個人の設定ファイルとして~/.eslintrcを探すように
  • configファイルのフォーマット
    • .eslintrc, .eslintrc.json, .eslintrc.yml, .eslintrc.yaml, .eslintrc.js。フォーマットの多様化によってJSとそれ以外での互換性を保てないところが出てきているらしい。
  • root
    • configuration cascadingがユーザの混乱を生んでいて、configファイルがどの親ディレクトリに存在するか把握しきれない
    • 辿る親ディレクトリを制限できるようにrootキーを追加した(--initでデフォルト有効にして混乱を減らすように)
  • overrides
    • globパターンベースで特定のサブセットに対するルールを上書き可能にした
    • overridesconfiguration cascadingを排除する代替として適切なアプローチであるように考えられたけど、当時はconfiguration cascadingの排除に至らなかったという振り返り
  • overridesに対するextendsの追加
    • 上書きした設定に拡張した設定を加えることを可能に(かなり複雑な印象を受ける)

まとめ

ESLintのconfigが最近複雑になっているのは実感としてあったので、flat configで1箇所の配列で集約されたシンプルな形になると良さそうな印象がある。
今回特に試してないけども、Linterクラスを利用していたりするとflat configを試すことが可能(new Linter({ configType: "flat" }))となっている。
https://eslint.org/blog/2022/08/new-config-system-part-3/

恐らく今後ESLintのエコシステムにおけるflat configが問題なく動作するかなどのフィードバックを受け付けて、どこかで正式なリリースタイミングがくると思う(いつかは不明瞭だけども)ので、それを気にかけておきたい。

参考

GitHubで編集を提案

Discussion