本記事は、SmartHR Advent Calendar 2024 シリーズ 1 の 19 日目の記事です。
こんにちは。SmartHR プロダクトエンジニアの sasaki (@s_sasaki_0529) です。
この記事では、SmartHR が共通で利用する ESLint の設定パッケージを、ESLint v9 と Flat Config に対応させた取り組みを紹介します。
ESLint とは
ESLint は、JavaScript や TypeScript を中心に幅広く使われている静的解析ツール(Linter)です。コード品質やスタイルの不整合を自動検出し、大規模なプロジェクトや、多様なフレームワークにも柔軟に対応できる豊富なプラグイン・ルールセットを備えています。
近年は Oxc や Biome (旧 Rome) など新しい選択肢も増えていますが、ESLint は高いカスタマイズ性と成熟したエコシステムにより、依然としてデファクトスタンダードと言える Linter です。
ESLint v9 と Flat Config
2024/04/06 に本リリースされた ESLint v9 は、これまでのバージョンから大きな変化をもたらしたメジャーバージョンです。このバージョンは従来の設定手法を刷新し、将来を見据えた新しい方向性を打ち出しています。
その中心にあるのが、新たな設定ファイルモデル「Flat Config」のデフォルト化です。
Flat Config は、これまで ESLint の内部実装に頼っていた記法から、JavaScriptのモジュールとして import して明示する仕組みに変わりました。これによって、プラグインとの依存関係が明確になる上、エディタ補完や型チェックの恩恵を受けることが可能になります。
import examplePlugin from "eslint-plugin-example"; import exampleConfigRecommended from "eslint-plugin-x"; export default [ exampleConfigRecommended, // configをextendsするのに相当 { plugins: { example: examplePlugin, // pluginsに指定するのに相当 }, rules: { "example/rule1": "warn", }, }, ];
(仕組みと嬉しさから理解するeslint FlatConfig対応より引用)
また、これまではサブディレクトリごとに複数の設定ファイルを配置することで、内部的に自動でマージされるカスケードの仕組みがありましたが、Flat Config ではその名の通り、eslint.config.js*1 という単一の設定ファイルにすべてフラットに記述するように変わり、より見通し良くプロジェクト全体の設定を記述できるようになりました。
SmartHR における ESLint の利用状況
SmartHR では、記事執筆時点でほぼすべてのプロダクトが ESLint v8 を使用しています。各プロダクトは独立したリポジトリを持ち、それぞれ個別の設定を管理していますが、会社全体で一貫したコーディングルールを適用するために以下の共通パッケージを運用しています。
共通設定: eslint-config-smarthr
社内共通のルールセットとして eslint-config-smarthr を提供しています。このパッケージでは、typescript-eslint や eslint-plugin-react など、SmartHR のフロントエンド開発で基本となる設定をまとめて提供しています。これにより、すべてのプロダクトが統一された共通ルールを簡単に適用できます。
カスタムルール: eslint-plugin-smarthr
一般的なルールだけでは対応できない社内特有の要件を満たすため、カスタムルールを eslint-plugin-smarthr で提供しています。特に、SmartHR の共通 React コンポーネントライブラリである SmartHR UI を適切に活用するためのルールが多く含まれています。
これが Oxc や Biome といった、より高速でコンフィグレスな Linter ではなく ESLint を使い続ける大きな理由にもなっています。
詳細は以下の記事を御覧ください。
パッケージ間の関係性
eslint-config-smarthr、eslint-plugin-smarthr、そして各プロダクトの関係性を表した図が以下になります。
各プロダクトは eslint-config-smarthr を利用することで、eslint-plugin-smarthr 及び Plugin A, Plugin B といった、社内共通の構成を適用することができます。一方で、プロダクトごとに Plugin C, Plugin D といった個別の追加設定も行っています。
なお、当然ながらこの時点では各パッケージ及びプロダクトで利用している ESLint のバージョンは v8 であり、設定ファイルも従来の形式を使用していました。
なぜマイグレーションをしたいのか
ESLint v9 および Flat Config へのマイグレーションは、ややコストの高い作業です。しかし、以下の理由から今のタイミングで実施することを決断しました。
サポート終了に速やかに対応したい
公式ブログにて、新たなバージョンポリシーが発表 され、最新メジャーバージョンのリリースから6ヶ月後に旧バージョンが EOL(End of Life)を迎えることが明示されました。
その通りに、ESLint v8 は v9 のリリースした 2024/04/06 から6ヶ月後の 2024/10/05 に EOL を迎えています。EOL 後も使用を続けることは可能ですが、新機能やバグ修正が提供されなくなるリスクがあります。これを回避し、引き続き最適な環境を維持するため、速やかなバージョンアップが必要と判断しました。
特に ESLint は豊富なエコシステムによって様々な恩恵を受けられる仕組みである以上、それぞれのエコシステムもバージョンアップできなくなるというのは大きな痛手となりそうです。
Flat Config を使いたい
Flat Config は ESLint v9 でデフォルト設定モデルとなりました。従来の .eslintrc.* を用いた設定も利用可能ですが、公式には次期バージョン(v10)で廃止される可能性が示唆されています。そのため、今の段階で移行を進めることが将来的なコスト削減につながります。
Flat Config を利用することで以下のメリットを受けられます。
- 設定の読み書きが容易になる
- カスケード構造が廃止され、設定がフラット化されることで、適用範囲やルールの構造が直感的に理解できるようになります
- プラグインの扱いが明確になる
- 文字列ベースの設定から、モジュールとして明示的にインポートする形式に変わり、可読性やメンテナンス性が向上します
Config Inspector を活用したい
Flat Config を採用することで、ESLint v9 で新たに提供された Config Inspector を利用できるようになります。
このツールは、最終的に適用された ESLint 設定を可視化するもので、以下のような場面で非常に有用です。
- 設定のデバッグ
- 実際にどのルールが適用されているかを確認し、意図した動作をしているかを検証できます
- 設定間の競合解消
- カスタム設定やプラグイン間でルールが競合した場合に、その原因を特定できます
- 新規ルールの追加検証
- 追加したルールがどのように適用されるかを、設定全体の中で視覚的に確認できます
特に eslint-config-smarthr のように、複数のプラグインを取り込んで、SmartHR 共通の設定を必要十分に提供できているかを検証するために有用と考えられます。
Next.js でも ESLint v9 が使えるようになった
SmartHR の一部プロダクトでは Next.js を採用していますが、Next.js 14 までは ESLint v8 までしか対応していませんでした。
2024年にリリースされた Next.js 15 で ESLint v9 が正式にサポートされ、これにより、Next.js を使用しているプロダクトでも v9 への移行が可能になりました。現在、社内でも Next.js 15 へのマイグレーションが進みつつあるため、Flat Config を含めた ESLint のアップグレードを行う絶好のタイミングだと判断しています。
マイグレーションの手順
SmartHR における、ESLint v9 および Flat Config へのマイグレーションのプロセスを紹介します。
以下は、eslint-config-smarthr、eslint-plugin-smarthr、および各プロダクトの依存関係を表した図の再掲です。この構造をもとに、マイグレーションの具体的な手順を説明します。
依存しているプラグインをバージョンアップする
まず、eslint-config-smarthr が依存する Plugin A、Plugin B にあたる各種プラグインが、ESLint v9 および Flat Config に対応しているかを確認します。
各プラグインの対応状況は、ESLint リポジトリの Issue にリスト化されており、これを定期的にウォッチして進捗を確認しました。
ありがたいことに、どのプラグインも破壊的変更はなく、ESLint v8/v9を同時にサポートするバージョンがリリースされました。
それらのバージョンアップだけをした eslint-config-smarthr の新バージョンをリリースし、各プロダクトにも適用することで、ESLint v9 マイグレーション時の影響を小さくしました。
プロダクト側が直接利用している Plugin C、D についても、通常はどのプロダクトでも renovate を用いたパッケージの自動更新のプロセスがあるため、自然と ESLint v9 を利用する準備は出来上がっていました。
対応の順番を確認する
Flat Config は、通常 Flat Config で記述された設定しか参照できません。
よって、依存関係の末端から手前に向かって、以下の順番で対応を進めました。
- eslint-plugin-smarthr
- eslint-config-smarthr
- 各プロダクト
eslint-plugin-smarthr の対応
まずは、SmartHR 独自のカスタムルールを提供する eslint-plugin-smarthr のマイグレーションからです。
このプラグインにはカスタムルールの実装が含まれているため、ESLint v9 の内部 API による破壊的変更の影響を受ける箇所がいくつかありました。
しかし、公式の codemod である eslint-transforms を利用し、v9-rule-migration コマンドを適用することで、これらの問題を自動的に解消することができました。
対応したPull Requestが以下になります。
eslint-config-smarthr の対応
続けて、SmartHR の全社共通設定を提供する eslint-config-smarthr のマイグレーションを行います。
こちらは共通設定の提供を目的としたパッケージなので、Flat Config への書き換えが主な作業となります。
設定内容を従来の .eslintrc.* フォーマットから、eslint.config.js に移行し、プラグインの明示的なインポート形式に変更するなどの作業の繰り返しです。
対応したPull Requestは以下になります。
プロダクト側の対応
SmartHR では、多くのプロダクトエンジニアが、いずれかのプロダクトの開発を担当するチームに所属しています。
ここまでの ESLint のマイグレーション作業を実施した私にも担当プロダクトがあるため、まずは自分の担当プロダクトでの ESLint v9 マイグレーションを実施し、問題がないことを確認することにしました。
以下は私の担当プロダクトの、Flat Config による設定の一部抜粋です。
import smarthr from 'eslint-config-smarthr' import importPlugin from 'eslint-plugin-import' import playwright from 'eslint-plugin-playwright' import storybook from 'eslint-plugin-storybook' import unusedImportPlugin from 'eslint-plugin-unused-imports' /** * @type {import('eslint').Linter.Config[]} */ export default [ ...smarthr, ...storybook.configs['flat/recommended'], { name: 'myProject', files: ['client/**/*.{ts,tsx}'], plugins: { 'unused-imports': unusedImportPlugin, }, rules: { // ここにプロダクト固有のルールの上書きなどを記述する }, }, { name: 'myProject/playwright', files: ['client/test/e2e/**/*.ts'], plugins: { playwright }, rules: { // ここにプロダクト固有のE2Eテストに対するルールを設定する }, }, ]
はじめに eslint-config-smarthr と eslint-plugin-storybook の設定を継承し、あとはプロダクト固有設定を続けて記述するだけで、シンプルに書けるようになりました。
検証方法
前述のとおり、 eslint-plugin-smarthr、eslint-config-smarthr、プロダクトの順でPull Requestを作成しましたが、設定や動作の検証をまとめて行うために、手元ではすべて同時並行で対応しました。
これらのパッケージはそれぞれ異なるリポジトリで管理されており、通常は npm を通じて依存関係を解決します。しかし、開発中のバージョンを検証する場合、リポジトリごとに最新の変更を反映させる必要があるため、今回はローカル環境でパッケージを公開・管理できるツールである yalc を利用しました。
yalc は以下のようなコマンドを利用することで、ローカル環境に閉じた npm パッケージの公開・管理が可能です。
npx yalc publish
- パッケージをローカルに公開します
npx yalc add [package]
- ローカルに公開されたパッケージを依存として追加します。
今回の検証では、以下の手順でパッケージ間の依存を解決し、プロダクトリポジトリでの動作確認を行いました。
# eslint-plugin-smarthr
$ npx yalc publish
# eslint-config-smarthr
$ npx yalc add eslint-plugin-smarthr
$ npx yalc publish
# プロダクトリポジトリ
$ npx yalc add eslint-config-smarthr
すべての依存が解決した状態で、プロダクトリポジトリ側で Config Inspector を利用して適用ルールを可視化し、Flat Config が正しく設定されているかを確認しました。
# プロダクトリポジトリ $ pnpm eslint --inspect-config
以上のように、yalc を用いたローカル公開と Config Inspector による設定の可視化を活用することで、検証を効率的に完了させました。その後、依存関係に沿って順次Pull Requestを作成し、マイグレーションを完了しました。
まとめ
本記事では、SmartHR における ESLint v9 および Flat Config へのマイグレーションについて、その背景、手順、検証方法を詳しく紹介しました。
振り返り
ESLint v9 への移行では、新たな設定モデルである Flat Config を採用することで、設定のフラット化やプラグイン管理の明確化など、多くのメリットを享受できるようになりました。さらに、Config Inspector を活用した設定の可視化は、適用範囲や競合解消の検証を容易にし、開発者体験の向上に寄与しました。
反省点
現在 SmartHR では、eslint-plugin-smarthr や eslint-config-smarthr を含む社内共通パッケージのモノレポ化を進めています。今回は、密結合なパッケージ群をまとめてバージョンアップするために yalc を用いるなど工夫をしましたが、モノレポ化を先に完了させていれば、ワークスペース機能を活用し効率的にマイグレーションを進められたのではないかと感じました。と同時に、モノレポ化に対する期待も高まりました。
We Are Hiring!
SmartHR では、本記事で紹介したような、チームやプロダクトを横断したチャレンジが豊富にあります。私たちと一緒に働いてみたいと思った方は、ぜひ以下のページをご覧ください!
*1:正確には .mjs などの拡張子が使用でき、実際はESMを使用した設定ファイルであることを明示するために eslint.config.mjs を使用しました