😇

ESLint の FlatConfig が難しかったけど、とりあえず動いた話

2023/12/02に公開

はじめに

※ この記事で紹介する設定が正しい保証はありません。
※ ただし、自分の手元では意図した通りに動きました。

自分はフロントエンドエンジニアです。自分ではそう思っています。

webpack や babel を利用してビルドしたりしながら、複雑になっていくエコシステムとずっと付き合ってきました。扱っているプロダクトの技術スタックが陳腐化していくのと同時にキャッチアップが弱まっている気はしていますが、それでもそれなりにやれるレベルだと思います。

しかし、そんな自分でも .eslintrc の設定だけは常に苦手でした。

eslint-config-***esling-plugin-*** の違いは未だにわからないし、plugins: [***] とか extends: [***] に何を書けばいいのかもわかっていません。
plugin:*** と書いてみたり eslint-plugin-*** と書いてみたり、自分には難しすぎる。

そんな折に ESLint に FlatConfig という新しい仕組みが導入されたと聞きました。

ひと目見て「めっちゃわかりやすくなってる〜」と嬉しくなったのですが、既存の設定をどう移植したらいいのか全くわかりません。

今回は色々と格闘してみた結果を共有します。

どういう構成を FlatConfig 化したのか?

素振りとして下記のような構成のプロダクトの設定を作り込んでいます。

  • Next.js
  • TypeScript
  • Storybook

ふーんって感じですが、これをどうやってまともにリントすればいいのかという話です。
「いや、FlatConfig 使わなきゃいいじゃん」という声が聞こえてきますが、素振りなのでいいんです。

まずは最終的な構成

最終的にこうなりました。

const globals = require("globals");
const tsPlugin = require("@typescript-eslint/eslint-plugin");
const jsPlugin = require("@eslint/js");
const nextPlugin = require("@next/eslint-plugin-next");
const reactPlugin = require("eslint-plugin-react");
const reactHooksPlugin = require("eslint-plugin-react-hooks");
const jsxA11yPlugin = require("eslint-plugin-jsx-a11y");
const importPlugin = require("eslint-plugin-import");
const storybookPlugin = require("eslint-plugin-storybook");

/**
 * @type {Partial<import('eslint').Linter.FlatConfig>}
 */
const common = {
  ignores: ['node_modules/**/*', '.*/**/*'],
  languageOptions: {
    parser: require("@typescript-eslint/parser"),
    globals: {
      ...globals.browser,
      ...globals.node,
      ...globals.es2021,
    },
  },
  settings: {
    react: {
      version: "detect",
    },
    "import/parsers": {
      "@typescript-eslint/parser": [".js", "jsx", ".ts", ".tsx"],
    },
    "import/resolver": {
      typescript: {
        alwaysTryTypes: true,
        project: __dirname,
      },
    },
  },
  plugins: {
    "@typescript-eslint": tsPlugin,
    react: reactPlugin,
    "react-hooks": reactHooksPlugin,
    "jsx-a11y": jsxA11yPlugin,
    import: importPlugin,
    "@next/next": nextPlugin,
  },
  rules: {
    ...jsPlugin.configs.recommended.rules,
    ...tsPlugin.configs.recommended.rules,
    ...reactPlugin.configs.recommended.rules,
    ...reactHooksPlugin.configs.recommended.rules,
    ...jsxA11yPlugin.configs.recommended.rules,
    ...importPlugin.configs.recommended.rules,
    ...nextPlugin.configs["core-web-vitals"].rules,
  }
};

/**
 * @type {import('eslint').Linter.FlatConfig}
 */
module.exports = [
  {
    files: ["**/*.{ts,tsx}"],
    ...common,
    rules: {
      ...common.rules,
      "no-undef": "off",
      "react/display-name": "off",
      "react/react-in-jsx-scope": "off",
    },
  },
  {
    files: ["**/*.{js,jsx}"],
    ...common,
    rules: {
      ...common.rules,
      "@typescript-eslint/no-var-requires": "off",
      "react/display-name": "off",
      "react/react-in-jsx-scope": "off",
    },
  },
  {
    files: ["**/*.stories.tsx"],
    ...common,
    plugins: {
      storybook: storybookPlugin,
    },
    rules: {
      ...storybookPlugin.configs.recommended.rules,
    },
  },
];

最初は FlatCompat を使おうとしましたが、全く意図したリントが行われなくて心が折れました。
結果として「すべて明示的に書く」というストロングスタイルに落ち着きました。

それぞれの解説

globals ってなに?

旧スタイルだと eslint:recommended という設定をすることで、幅広くダメダメコードを検出するということが可能だったので、今回は const jsPlugin = require('eslint/js') を設定しています。
しかし、それだけだと __dirname is not defined と怒られてしまいます。

旧スタイルでは env: { browser: true, node: true } などと記載して回避します。
FlatConfig では こちら に設定方法が乗っていたので globals を設定したという流れです。

jsPlugin*.ts に適用すると no-undef が頻発する

これは FlatConfig は関係ない話ですね。自分は ESLint が苦手なので躓いてしまいました。

結論としては こちら に記載してあるとおり、'no-undef': 'off' を設定することで回避しました。

FlatCompat の正しい使い方がわからなかった

最近 FlatConfig について調べると ...(new FlatCompat().extends('next/core-web-vitals')) のようなコードが出てきますが試したところ、eslint-next-plugin 由来のルールのみが適用されているようでした。

eslint-plugin-reacteslint-plugin-react-hooks なども欲しいので分離して書かざるを得なかったという感じです。

おわりに

もっともっと罠を踏んだ気がしますが試行錯誤の末にいろいろ忘れてしまっています。

個人的には、断然 FlatConfig のほうが好きです。
ぱっと見でめちゃくちゃわかりやすい。
(まあ、この設定があっているかわからないのだが...)

どんどん FlatConfig になっていけばいいのになあと思いました。

Discussion