monorepoにESLintのFlat Configを導入した

2024/12/02に公開

先日、業務で開発しているmonorepoのESLintをv9にアップデートして設定ファイルをFlat Configに変更しました。

自分自身はmonorepoにFlat Configを導入するのが初めてでした。色々調べた結果、学びがあったりきれいにまとまった設定ファイルが作れたと感じたので共有します。

Flat Config自体は目新しいものではなくなってきているので、細かい使い方は他の記事や公式ドキュメントを御覧ください。

設定ファイル完成形

my-monorepoは各自のプロジェクト名に、some-workspaceは個別のワークスペース名に置き換えてください。

eslint.config.mjs
import globals from "globals";
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginReact from "eslint-plugin-react";
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import * as eslintPluginImport from "eslint-plugin-import";
import eslintPluginUnusedImports from "eslint-plugin-unused-imports";
import eslintPluginOnlyWarn from "eslint-plugin-only-warn";
import eslintPluginNext from "@next/eslint-plugin-next";
import eslintPluginTailwindCSS from "eslint-plugin-tailwindcss";

export default tseslint.config(
  {
    name: "my-monorepo/ignore-globally",
    ignores: [
      "**/node_modules",
      "**/.turbo",
      "**/dist",
      "**/.next",
      "**/temp",
      "server/src/__tests__/__generated__",
      "server/src/openapi",
      "some-workspace/src/lib/openapi/genereated-client",
    ],
  },
  {
    name: "my-monorepo/load-plugins",
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: "module",
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
    plugins: {
      import: eslintPluginImport,
      "unused-imports": eslintPluginUnusedImports,
      react: eslintPluginReact,
      "react-hooks": eslintPluginReactHooks,
      "@next/next": eslintPluginNext,
      "only-warn": eslintPluginOnlyWarn,
      tailwindcss: eslintPluginTailwindCSS,
    },
    settings: {
      react: {
        version: "detect",
      },
      next: {
        rootDir: "./*",
      },
    },
  },

  // 以下、ルールのon/off設定
  {
    name: "my-monorepo/global-tuning",
    extends: [eslint.configs.recommended],
    rules: {
      "import/order": "error",
      "unused-imports/no-unused-imports": "error",
      "unused-imports/no-unused-vars": [
        "error",
        {
          vars: "all",
          varsIgnorePattern: "^_",
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
    },
  },
  {
    name: "my-monorepo/for-typescript",
    files: ["**/*.ts", "**/*.tsx"],
    extends: [
      tseslint.configs.strict,
      eslintPluginReact.configs.flat.recommended,
      eslintPluginReact.configs.flat["jsx-runtime"],
    ],
    rules: {
      "@typescript-eslint/no-unused-vars": "off",
      "@typescript-eslint/no-namespace": "off",
      "react/prop-types": "off",
      ...eslintPluginReactHooks.configs.recommended.rules,
    },
  },
  {
    name: "my-monorepo/for-nextjs",
    files: [
      "some-workspace/**/*.{ts,tsx,js,jsx}",
      "some-workspace-2/**/*.{ts,tsx,js,jsx}",
      "some-workspace-3/**/*.{ts,tsx,js,jsx}",
    ],
    rules: {
      ...eslintPluginNext.configs.recommended.rules,
      ...eslintPluginNext.configs["core-web-vitals"].rules,
    },
  },
  {
    name: "my-monorepo/for-some-workspace",
    files: ["some-workspace/**/*.{ts,tsx,js,jsx}"],
    extends: [...eslintPluginTailwindCSS.configs["flat/recommended"]],
    settings: {
      tailwindcss: {
        callees: ["cn"],
        config: "some-workspace/tailwind.config.ts",
      },
    },
    rules: {
      "no-restricted-imports": [
        "error",
        {
          paths: [
            {
              name: "next/link",
              importNames: ["default"],
              message:
                "Please use NextLink from '@/components/next-link' instead.",
            },
          ],
        },
      ],
      "@next/next/no-img-element": "off",
    },
  },
  {
    name: "eslint-config-prettier",
    ...eslintConfigPrettier,
  },
);

説明

tseslint.configの引数に渡されるオブジェクトひとつひとつを指してConfiguration Objectと呼びます。

全体的な方針

monorepoにおけるESLint設定ファイルの配置方法には2通りあるかと思います。

  • 各ワークスペース毎にeslint.config.jsを配置する
  • monorepoのルートに1つだけeslint.config.jsを配置する

このmonorepoではTurborepoを利用しているため、各ワークスペース毎に配置してnpm run lintをワークスペース単位で実行することでTurborepoのキャッシュを活用する選択肢もありました。しかし今回はルートに1つだけ配置することにしました。理由としては以下が挙げられます。

  • ESLint自体にもキャッシュ機能がある
  • Flat Configでは明示的にディレクトリ(ワークスペース)を指定することが可能
  • ルートに配置することでESLint設定を一元管理できる

Configuration Objectの記述順序については以下のような方針を取りました。

  • ignoreの指定やpluginの読み込みは上にまとめる
  • rulesの設定は下にまとめる
    • より広範囲に適用されるrulesは上の方に書く
    • ワークスペース固有のrulesは下の方に書く

外部パッケージのConfiguration Objectを読み込むことで、それに必要なプラグインが自動で読み込まれることもあります。しかし個人的にはどのプラグインを利用しているかを明示したいため、eslint.config.jsの上の方でプラグインをまとめて読み込むようにしました。

適用するルールについては基本的に各種プラグインのrecommended

Configuration Objectにはnameを指定します。ESLint config inspectorが名前を表示してくれるため、独自のConfiguration Objectなのか外部パッケージ由来のものなのかが判別できます。

config inspectorはFlat Configでどのファイルにどのルールが適用されているかなどが確認できるツールです。以下のコマンドを実行するとローカルサーバーが起動してブラウザが立ち上がります。

npx eslint --inspect-config

tseslint.configについて

tseslint.configtypescript-eslintが提供してくれているFlat Config向けのヘルパー関数です。

https://typescript-eslint.io/packages/typescript-eslint#config

これを通すことで// @ts-checkコメントによる型チェックが有効になったり、VSCodeの入力補完が効くようになります。

ほとんど可変長引数に渡した要素そのままのFlat Configを生成してくれるのですが、extendsプロパティだけはtypescript-eslintの独自プロパティです。これを使うことで、他のルールセットを利用しつつ一部のルールを追加・削除できます(旧来の設定ファイルにおけるextendsとは違い、モジュール名の文字列ではなくConfiguration Objectの変数を受け取ります)。

{
  name: "my-monorepo/global-tuning",
  extends: [eslint.configs.recommended], // ESLintのrecommendedを継承
  rules: {
    "no-unused-vars": "off", // 特定のルールを削除
    "unused-imports/no-unused-imports": "error", // 特定のルールを追加
  },
}

eslint-plugin-nextについて

Next.jsプロジェクトでESLintを使用する場合、eslint-config-nextを使うことが一般的です。eslint-config-nextを読み込めばeslint-plugin-nextが勝手に読み込まれるようになっているため、eslint-plugin-nextを直接インストールすることは多くありません。しかし、eslint-config-nextはFlat Configに対応した形式になっていないため、eslint-plugin-nextを直接使用することにしました。

eslint-config-nexteslint-plugin-nextが提供するルールの適用だけでなく、Reactプロジェクトとして必要な多くのルールを適用してくれます。eslint-config-nextを使わないため、eslint-plugin-reacteslint-plugin-react-hooksもまた自前で読み込む必要があることに注意が必要です。

eslint-config-prettierについて

eslint-config-prettierはPrettierと競合する可能性があるESLintのルールをオフにしてくれるパッケージです。Flat Configになっても最後に読み込ませることに変わりはありません。

eslint-config-prettierをスプレッド構文で展開しているのには理由があります。

{
  name: "eslint-config-prettier",
  ...eslintConfigPrettier,
}

eslint-config-prettierは内部でnameプロパティが指定されていませんでした。config inspectorで一覧する際、すべてのConfiguration Objectがnameプロパティを持っていてほしいと感じたため、自分でnameを明示的に指定しました。

eslint-config-prettiernameを追加するプルリクエストはすでに提出されているので、いずれこのような書き方は不要になると思います。

https://github.com/prettier/eslint-config-prettier/pull/294

eslint-plugin-tailwindcssについて

eslint-plugin-tailwindcssはTailwindCSSのクラス名をチェックするプラグインです。

どんなtailwind.config.jsを使うかやどんなクラス名連結関数(clsx, classNames, cn,...)を使っているかはワークスペース毎に異なります。そのため、ワークスペース毎のConfiguration Objectでsettingsプロパティとともに読み込むことにしています。

その他好みのプラグイン

eslint-plugin-import

import文周りのルールを提供してくれるプラグインです。僕はimport/orderだけほしいので導入しています。

eslint-plugin-unused-imports

未使用のimport文を自動で削除してくれます。ESLint本体のno-unused-varsはautofixを提供していないので、こちらを追加で導入しています。

eslint-plugin-only-warn

ESLintが検出したすべてのerrorをwarnレベルに変更してくれるプラグインです。これを導入することで「どのルールをerrorにしてどのルールをwarnにするか」という議論を避けることができます。

これを導入しているのはエラーを許容したいからではありません。ESLintはwarnであってもコードにひとつも残すべきではなく、warnが出るようなコードは修正するかeslint-ignoreを明示的に残すべきと考えます。

このプラグインを利用しつつeslint --max-warnings=0で実行することで、ひとつのwarnすらも許さない開発環境を構築できます。

感想

Flat ConfigによってESLintの設定ファイルの読みやすさが格段に向上したと感じています。

monorepoでもただ一つのeslint.config.jsに全体のルールを集約することで、Flat Configのカスケードの仕組みを活かしつつもワークスペース固有のルールを設定しやすくなりました。

まとめ

業務で開発しているmonorepoにESLintのFlat Configを導入しました。

  • monorepoのルートにひとつだけeslint.config.jsを配置
  • Configuration Objectの記述順序に方針を持つ
  • tseslint.configを利用してTypeScriptの型チェックを有効にする
  • 適用するルールは各種プラグインのrecommendedをベースに好みで調整

Flat ConfigによってESLintの設定ファイルの読みやすさが向上し、ワークスペース毎のルールを設定しやすくなりました。

Flat Configの登場によって各種サードパーティパッケージがFlat Configに対応していることが多いため、設定ファイルの記述量が減りました。

それでは良いESLintライフを!

GitHubで編集を提案
chot Inc. tech blog

Discussion