🪻

2024年9月 俺の eslint.config.js

2024/09/18に公開1

はじめに

「Flat Config が何かはわかったからサンプルをくれ!」という方向けの記事です。

筆者が普段使用している eslint.config.js をご紹介します。

Flat Config については良記事がたくさんあるため ↓ などを参照してください。

https://zenn.dev/babel/articles/eslint-flat-config-for-babel

https://zenn.dev/cybozu_frontend/articles/about-eslint-flat-config

今回ご紹介する設定は、React × TypeScript を使用し、フォーマットは Prettier に任せる前提になっています。

しかし、Vue やサーバー Node.js でも使いまわせる箇所は多いと思います。

eslint.config.js

まずは結論から。

eslint.config.js
// @ts-check

import { fixupConfigRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import eslint from '@eslint/js';
import vitestPlugin from '@vitest/eslint-plugin';
import prettierConfig from 'eslint-config-prettier';
import * as importPlugin from 'eslint-plugin-import';
import jestDomPlugin from 'eslint-plugin-jest-dom';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import perfectionistPlugin from 'eslint-plugin-perfectionist';
import reactPlugin from 'eslint-plugin-react';
import spellcheckPlugin from 'eslint-plugin-spellcheck';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import globals from 'globals';
import tseslint from 'typescript-eslint';

const flatCompat = new FlatCompat();

export default tseslint.config(
  {
    // このオブジェクトは ignores プロパティだけにする必要あり
    ignores: ["dist"], // ESLint のチェック対象外 (node_modules と .git はデフォルトで対象外)
  },
  {
    languageOptions: {
      globals: globals.browser,
    },
  },
  // Shareable Configs を有効化
  eslint.configs.recommended,
  ...tseslint.configs.strict, // strict は recommended よりも厳しめな設定
  reactPlugin.configs.flat.recommended,
  reactPlugin.configs.flat['jsx-runtime'],
  jsxA11yPlugin.flatConfigs.recommended,
  vitestPlugin.configs.recommended,
  jestDomPlugin.configs['flat/recommended'],
  // Flat Config 未対応のプラグインは FlatCompat を使用
  ...flatCompat.extends('plugin:react-hooks/recommended'),
  // ESLint v9 で削除された API "context.getScope" を内部で使用しているプラグインは fixupConfigRules で対応
  ...fixupConfigRules(
    flatCompat.extends(
      // "plugin:import/recommended", // TODO: 現時点だと色々動かないので eslint-plugin-import が Flat Config に対応したら有効化する
      'plugin:testing-library/react',
      'plugin:storybook/recommended'
    )
  ),
  {
    // eslint-plugin-react の設定
    settings: {
      react: {
        version: 'detect',
      },
    },
    // recommended に含まれていない eslint-plugin-react のルールを有効化
    rules: {
      'react/destructuring-assignment': 'error', // Props などの分割代入を強制
      'react/function-component-definition': [
        // コンポーネントの定義方法をアロー関数に統一
        'error',
        {
          namedComponents: 'arrow-function',
          unnamedComponents: 'arrow-function',
        },
      ],
      'react/hook-use-state': 'error', // useState の返り値の命名を [value, setValue] に統一
      'react/jsx-boolean-value': 'error', // boolean 型の Props の渡し方を統一
      'react/jsx-fragments': 'error', // React Fragment の書き方を統一
      'react/jsx-curly-brace-presence': 'error', // Props と children で不要な中括弧を削除
      'react/jsx-no-useless-fragment': 'error', // 不要な React Fragment を削除
      'react/jsx-sort-props': 'error', // Props の並び順をアルファベット順に統一
      'react/self-closing-comp': 'error', // 子要素がない場合は自己終了タグを使う
      'react/jsx-pascal-case': 'error', // コンポーネント名をパスカルケースに統一
      'react/no-danger': 'error', // dangerouslySetInnerHTML を許可しない
      'react/prop-types': 'off', // Props の型チェックは TS で行う & 誤検知があるため無効化
    },
  },
  {
    // eslint-plugin-react-hooks の設定
    rules: {
      'react-hooks/exhaustive-deps': 'error', // recommended では warn のため error に上書き
    },
  },
  {
    // eslint-plugin-import の設定
    plugins: { import: importPlugin },
    rules: {
      'import/order': [
        // import の並び順を設定
        'warn',
        {
          groups: [
            'builtin',
            'external',
            'internal',
            ['parent', 'sibling'],
            'object',
            'type',
            'index',
          ],
          'newlines-between': 'always',
          pathGroupsExcludedImportTypes: ['builtin'],
          alphabetize: { order: 'asc', caseInsensitive: true },
          pathGroups: [
            {
              pattern: 'react',
              group: 'external',
              position: 'before',
            },
          ],
        },
      ],
    },
  },
  {
    // eslint-plugin-unused-imports の設定
    plugins: { 'unused-imports': unusedImportsPlugin },
    rules: {
      '@typescript-eslint/no-unused-vars': 'off', // 重複エラーを防ぐため typescript-eslint の方を無効化
      'unused-imports/no-unused-imports': 'error',
      'unused-imports/no-unused-vars': [
        'error',
        {
          vars: 'all',
          varsIgnorePattern: '^_',
          args: 'after-used',
          argsIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    // @vitest/eslint-plugin の設定
    rules: {
      'vitest/consistent-test-it': ['error', { fn: 'test' }], // it ではなく test に統一
    },
  },
  {
    // eslint-plugin-spellcheck の設定
    plugins: { spellcheck: spellcheckPlugin },
    rules: {
      'spellcheck/spell-checker': [
        'error',
        {
          minLength: 5, // 5 文字以上の単語をチェック
          // チェックをスキップする単語の配列
          skipWords: [
            "noreferrer",
            "compat",
            "vitest",
            "tseslint",
            "globals",
            "fixup",
          ],
        },
      ],
    },
  },
  {
    // eslint-plugin-perfectionist の設定
    plugins: { perfectionist: perfectionistPlugin },
    rules: {
      'perfectionist/sort-interfaces': 'warn', // interface のプロパティの並び順をアルファベット順に統一
      'perfectionist/sort-object-types': 'warn', // Object 型のプロパティの並び順をアルファベット順に統一
    },
  },
  prettierConfig // フォーマット は Prettier で行うため、フォーマット関連のルールを無効化
);

サンプルとして Vite プロジェクトに導入したリポジトリはこちら

https://github.com/kazukixmatsuda/eslint-config-2024-09

使用ライブラリ

eslint

ESLint 本体

@eslint/js

ESLint 公式の Shareable Configs

README によると JavaScript 固有の機能を ESLint から分離する取り組みの始まり らしい。

@eslint/@compat

README より

このパッケージには、ESLint v8.x で使用することを目的とした既存の ESLint ルール、プラグイン、および構成をラップして、ESLint v9.x でそのまま動作できるようにする関数が含まれています。

typescript-eslint

ESLint で TypeScript を解析するためのツール群。

@vitest/eslint-plugin

先日 Vitest の公式ライブラリになり、eslint-plugin-vitest から @vitest/eslint-plugin に変わった。

Jest 使っている人は eslint-plugin-jest を使おう。

eslint-config-prettier

フォーマット関連のルールを無効化する。設定ファイルの一番最後に書く。

eslint-plugin-prettier は非推奨なのでこっちを使おう。

eslint-plugin-import

🚧 2024 年 9 月 18 日時点で Flat Config 未対応。

import 文の並び順を揃えるために入れている。

eslint-plugin-jest-dom

@testing-library/jest-dom の使い方をチェックする。

例えば ↓ のようなルールがある

// prefer-to-have-text-content ルール
expect(button.textContent).toBe('ユーザー名取得'); // NG
expect(button).toHaveTextContent('ユーザー名取得'); // OK

eslint-plugin-jsx-a11y

アクセシビリティチェックしてくれる。Storybook の A11y チェックも合わせて使っている。

eslint-plugin-perfectionist

色々並び替えしてくれる。この記事の設定では interface と type のプロパティをアルファベット順にしている。

eslint-plugin-react

recommended 以外にも有用なルールが結構あるので有効化している。詳しくは ↓

https://zenn.dev/kazukix/articles/create-similar-react-components

eslint-plugin-react-hooks

🚧 2024 年 9 月 18 日時点で Flat Config 未対応。

公式の設定ではなぜか exhaustive-depswarn なのでerror に上書きして放置されないようにしよう。

eslint-plugin-spellcheck

スペルチェックしてくれる。チェックの対象外にする単語を skipWords に追加するのが少し手間だが、レビューで typo を指摘するよりはいいと思っている。

eslint-plugin-storybook

🚧 2024 年 9 月 18 日時点で Flat Config 未対応。

Storybook 入れるならとりあえず入れておく。

eslint-plugin-testing-library

🚧 2024 年 9 月 18 日時点で Flat Config 未対応。

Testing Library ファミリーの使い方をチェックする。

例えば不要な await をエラーにする。

expect(await screen.getByRole('heading')).toHaveTextContent('Hello'); // NG
expect(screen.getByRole('heading')).toHaveTextContent('Hello'); // OK

// 逆に findBy は await がないとエラーになる
expect(screen.findByRole('heading')).toHaveTextContent('Hello'); // NG
expect(await screen.findByRole('heading')).toHaveTextContent('Hello'); // OK

eslint-plugin-unused-imports

未使用 import を自動削除してくれる。

globals

各環境のグローバル変数を定義している。

使い方

  1. ライブラリのインストール
# eslint-plugin-import と eslint-plugin-testing-library の peerDependencies の影響で --force をつける必要あり
npm install -D --force \
  eslint \
  @eslint/js \
  @eslint/compat \
  typescript-eslint \
  @vitest/eslint-plugin \
  eslint-config-prettier \
  eslint-plugin-import \
  eslint-plugin-jest-dom \
  eslint-plugin-jsx-a11y \
  eslint-plugin-perfectionist \
  eslint-plugin-react \
  eslint-plugin-react-hooks \
  eslint-plugin-spellcheck \
  eslint-plugin-storybook \
  eslint-plugin-testing-library \
  eslint-plugin-unused-imports \
  globals
  1. eslint.config.js ファイルを作成して、記事に記載のコードを貼り付ける

    • package.json の typemodule でない場合は、拡張子を .mjs にする
  2. package.json を修正

package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}
  1. 実行
npm run lint
npm run lint:fix

その他

導入しようか検討中のもの

必要なのか検証中のもの

  • eslint-plugin-react-refresh : Vite でプロジェクトを作成するとデフォルトで入っている。ルール的に Fail になるコードを書いても、ステートを維持したまま Fast Refresh が機能するので、どんな場合に役立つか調べたい。(Fast Refresh の仕様を調べる必要がありそう)

まとめ

改めて設定ファイルを見直してみると、ESLint の設定はやはり複雑で手間がかかると感じます。特に現在は Flat Config への移行期ということもあり、各ライブラリのドキュメントや、場合によってはライブラリのコードを読む必要が出てきます。

そこで、Biome を使うことでこの課題をどう解消できるか、また同様のルールを再現できるかを現在検証中です。Biome の検証結果も記事にしたいと思います。

最後までお読みいただきありがとうございました 🙇‍♂️

GitHubで編集を提案

Discussion

Satoshi OnodaSatoshi Onoda

色々と知らないプラグインも多く参考になりました。
実は FlatCompatのあとにもESLintから色々とツールがリリースされてまして、
https://eslint.org/blog/2024/05/eslint-compatibility-utilities/

この中の fixupPluginRules を用いたところ自分の環境では importPlugin も動かせましたのでよろしかったらお試しください。

import { fixupPluginRules } from "@eslint/compat";
import eslint from "@eslint/js";
import prettierConfig from "eslint-config-prettier";
import importPlugin from "eslint-plugin-import";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint";

export default tseslint.config({
  extends: [
    eslint.configs.recommended,
    ...tseslint.configs.recommended,
    reactPlugin.configs.flat.recommended,
    prettierConfig,
    {
      plugins: {
        "react-hooks": fixupPluginRules(reactHooksPlugin),
        import: fixupPluginRules(importPlugin),
      },
    },
  ]});