こんにちは、フロントエンドエンジニアの桐澤(@kiririLee)です。
PR TIMESではリファクタリングデーを月に一回開催しています。今回そのリファクタリングデーを活用してXOのバージョンを v0.60.0 から v1.2.2 に上げたためやったことを紹介します。
XO は2016年から開発されており歴史の長いライブラリですが、メジャーバージョンが上がるのは初めての事で ESLint の Flat Config 対応が主な修正として含まれていました。
PR TIMESフロントエンドのディレクトリ構成
PR TIMES のフロントエンドはモノレポで構成されています(以下 prtimes-frontend)が、レポジトリ全体を通して適用したい設定は ESLint の Shareable Config として定義し、各プロジェクトで extends する運用をしています。主に XO のルールを一部無効にしていたり、ライブラリ固有のルールをプラグインで適用しています。
コーディングルールに関しての議論をなくすことが XO を導入した背景の一つとしてあるため、XO のデフォルト設定を変更しているルールは少ないです。しかし、ある程度開発が進んだ段階で XO を導入した背景があり、コードを修正するのが難しいルールなどはデフォルのト設定を変えているものがいくつかあります。詳しくは以下の記事をご覧ください。

以下は prtimes-frontend のイメージを示したディレクトリ構成です。packages/eslit-config/.eslintrc.cjsに Shareable Config が配置されています。
monorepo-xo/
├── apps/
│ ├── app1/
│ │ ├── node_modules/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── app.tsx
│ │ ├── tsconfig.json
│ │ └── .xo-config.cjs
│ │
│ └── app2/
│ ├── node_modules/
│ ├── package.json
│ ├── src/
│ │ └── app.tsx
│ └── tsconfig.json
│ └── .xo-config.cjs
│
├── packages/
│ ├── eslint-config-prtimes-frontend/
│ │ ├── .eslintrc.cjs
│ │ ├── node_modules/
│ │ └── package.json
│ │
│ └── shared/
│ ├── node_modules/
│ ├── package.json
│ ├── src/
│ │ └── component.tsx
│ └── tsconfig.json
│ └── .xo-config.cjs
│
├── node_modules/
├── package.json
├── tsconfig.json
└── .xo-config.cjs
各プロジェクトの.xo-config.cjsでは以下のように extends して、各々プロジェクトに合わせて追加の設定を行っています。
module.exports = {
extends: ['prtimes-frontend'],
rules: {
...
}
...
prettier: true,
space: 2
};Shareable Config の .eslintrc.cjs を Flat Config にする
公式で提供されているツールを活用してeslintrc.cjsから Flat Config のeslint.config.js(以下、eslint-config-prtimes-frontend)へと変換を行いました。
npx @eslint/migrate-config .eslintrc.cjs@eslint/migrate-configは、JavaScript で定義された.eslintrc.{js, cjs, mjs}などでは一部サポートしていない構文があります。今回は元々の設定ファイルが if 文や関数などを使用しておらず、シンプルなオブジェクトをexport しているだけであったため出力されたeslint.config.cjsをほぼそのまま使うことができました。
出力されたファイルに対して行った修正を以下に書きます。
CommonJS を ES Modules にする
元々のファイルが.eslintrc.cjsでCommonJSであったため、出力されたファイルもCommonJSで出力(eslint.config.cjs)されました。
prtimes-frontendはESMを中心にプロジェクトが構成されているため、リンターの設定ファイルも今回のXOバージョンアップを機にESMにしました(eslint.config.js)。
@eslint/eslintrc の FlatCompat を剥がす
前述したようにライブラリ固有のルールをプラグインで設定していたため、FlatCompat を使った形式で出力されました。
import {
defineConfig,
globalIgnores,
} from 'eslint/config';
import noBarrelFiles from 'eslint-plugin-no-barrel-files';
import js from '@eslint/js';
import {
FlatCompat,
} from '@eslint/eslintrc';
const __dirname = import.meta.dirname;
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([{
extends: compat.extends(
'plugin:@tanstack/eslint-plugin-query/recommended',
'plugin:jest/recommended',
'plugin:storybook/recommended',
),
...
])しかし、運用を見据えて pckage.json で管理するパッケージを削減するために FlatCompat を使わないように書き換えました。
import { defineConfig } from 'eslint/config';
import noBarrelFiles from 'eslint-plugin-no-barrel-files';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import jest from 'eslint-plugin-jest';
import storybook from 'eslint-plugin-storybook';
export default defineConfig([
...storybook.configs['flat/recommended'],
...tanstackQuery.configs['flat/recommended'],
{
files: [
"**/*.test.ts",
"**/*.test.tsx",
],
...jest.configs['flat/recommended'],
},
...
])利用していたプラグインが全て Flat Config に対応していたのとXOでルールを設定している関係上@eslint/jsのルールが不要であったため FlatCompat への依存を剥がせました。
各プロジェクトで xo.config.js を設定する
React のプロジェクトでは以下のようにxo.config.jsを設定しました。
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import prtimesFrontend from 'eslint-config-prtimes-frontend';
const __dirname = import.meta.dirname;
/** @type {import('xo').FlatXoConfig} */
const xoConfig = [
{
prettier: true,
react: true,
space: 2,
semicolon: true,
},
...prtimesFrontend,
{
rules: {
...
},
},
{
ignores: [
...
],
},
];
export default xoConfig;Flat Config による設定のポイント
xo.config.jsは Flat Config で設定を記述します。Shareable Config である eslint-config-prtimes-frontend を各プロジェクトに合わせてカスタマイズする必要がある場合、eslint-config-prtimes-frontend を展開した後に rules を設定して上書きする必要があります。
また、プロジェクト全体を通してリンター処理を除外したいファイルがある場合、filesなどを設定せずにignoresのみを指定したオブジェクトに除外したいファイルを指定する必要があります。
XO の react オプションを true にする
今回バージョンを上げた v1.2.2 から XO の react オプションを true にして react のルールを有効化できるようになりました。これまでは自前で eslint-config-xo-react をインストールして extends に設定していましたが、eslint-config-xo-react の依存とextendsの設定を削除しました。
eslint-plugin-import から eslint-plugin-import-x に移行する
XO v1.2.2 から XO 内部で依存していたeslint-plugin-importがeslint-plugin-import-xへと依存するようになったため、rules の設定項目名を書き換えました。また、ソースコードの行単位でこのルールを設定していた場合もルールの名前を書き換えました。
rules の書き換え
// Before
{
rules: {
'import/no-extraneous-dependencies': ['error'],
},
},
// After
{
rules: {
'import-x/no-extraneous-dependencies': ['error'],
},
},行単位のルール設定の書き換え
// Before
// eslint-disable-next-line import/no-cycle
import {useHooks} from '../use-hooks';
// After
// eslint-disable-next-line import-x/no-cycle
import {useHooks} from '../use-hooks';Next.jsプロジェクトの xo.config.js の設定
prtimes-frontend は React のプロジェクトと Next.js のプロジェクトが混在しています。
モノレポ全体を XO のルールで統一するために Next.js のプロジェクトでも組み込みのnext lintは使わずにXOを使うようにしています。そのため、Next.js のプロジェクトにおいてもxo.config.jsでリンターの設定を行いました。React プロジェクトと大体同じですが以下のように設定をしました。
@next/eslint-plugin-nextを使った Next.js 用の configuration objects を定義して配列に含めています。
import path from 'node:path';
import prtimesFrontend from 'eslint-config-prtimes-frontend';
import nextPlugin from '@next/eslint-plugin-next';
const __dirname = import.meta.dirname;
/** @type {import('xo').FlatXoConfig} */
const xoConfig = [
{
prettier: true,
react: true,
space: 2,
semicolon: true,
},
...prtimesFrontend,
{
plugins: {
'@next/next': nextPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
'@next/next/no-img-element': 'off',
'@next/next/no-html-link-for-pages': 'off',
},
},
{
rules: {
...
},
},
{
ignores: [
...
],
},
];
export default xoConfig;リリースノートに明示されていない変更
XO のリリースノートには明示されていませんが、XO が内部で依存しているパッケージのアップデートにより修正が必要な箇所がありました。
@typescript-eslint/ban-types の廃止
typescript-eslint の v8 から @typescript-eslint/ban-types が廃止されたため、@typescript-eslint/no-restricted-types を使うように変更しました。fixableのためファイル保存時にnullがundifinedに自動変換されてしまう点に注意が必要でした。
エラーレベルが変更されたルール
arrow-body-style ルールがoffからas-neededへと変更されました。 return のみ行っているアロー関数が省略した形にする強制されるようになりました。
また、以下のルールはoffからerrorへとエラーレベルが上がりました。
- unicorn/prefer-string-raw
- promise/prefer-await-to-then
- オプションに
strictが付与され、await thing().then()のようにチェインされていてもエラーがでるようになりました
- オプションに
- unicorn/prefer-spread
- prefer-destructuring
新規追加されたルール
- react/no-object-type-as-default-prop
- n/prefer-global/buffer
- react/jsx-no-leaked-render
fixableで変更自体は小さい変更です。オプションのvalidStrategiesにternaryとcoerceが指定されているため、!!による修正ではなく必ずnullを return する形に修正されます
–print-config オプションによりルールを確認する
Flat Config への移行において、移行前と移行後で同じルールが適切に設定されているかどうかの確認方法が、一つの重要なチェック項目になると思います。
今回の XO バージョンアップでは、Flat Config への移行に加え、XO が内部で依存しているプラグインの変更や、新たに追加されたルールも含まれていたため、この確認が難しくなっていました。
XO の CLI には--print-configオプションがあり、実行時に適用されるルールを確認できます。今回はこのオプションを活用してルールの内容を確認しました。手順は以下のとおりです。
- 旧設定の状態で
--print-configを実行し、結果をファイルに保存 - Flat Config 移行後の新設定でも同様に実行
- 1 と 2 の出力ファイルを ChatGPT に読み込ませ、2 の方で消えているルールを抽出
3 の結果から消えているルールを把握し、@typescript-eslint/ban-typesのように deprecated になっているルールや、importのように別パッケージのルールへ移行しているものであることを確認しました。
--print-configの出力はルール数が多く、約 3,000 行近い結果になるため、目視での比較は困難でした。そのため ChatGPT に読み込ませて差分を抽出する方法を取りました。
このように大まかではありますが、移行後もルールが適切に設定されているかを確認できました。エラーレベルが変更されたルールや新規に追加されたルールもこのオプションによって把握することができました。
また、Next.js の設定は@eslint/eslintrcの FlatCompat に依存せずにプレーンなJavaScriptオプジェクトで設定可能であったため、公式ドキュメントで紹介されているFlatCompatを使った方法では設定を行いませんでした。そのため--print-configでルールが適用されているかどうかを確認できたことは安心感がありました。
最終的にファイル保存時の自動整形など VSCode との統合が動いていることとプロジェクトの全てのファイルに対する XO の実行結果を見て期待通りルールが適用されていることを確認しました。
まとめ
ESLint が Flat Config に完全移行した v9 をリリースしたのは去年の4月でした。今年の1月に弊社の小張(@kobari41257)がXO の v1 リリースを進めるためにXOにコミットするなど、社内でも ESLint Flat Config 移行に伴う XO のバージョンアップはここ一年間度々議題に上がっていたテーマでした。
プラグインの明示的な import や configuration object の配列によるマージなど JavaScript の標準機能を利用した Flat Config による設定は直感的に分かりやすくなった印象があります。
XO の Flat Config 対応を待つ必要があったため XO に依存することの制約は若干感じつつも、v1 がリリースされてからは比較的素早く prtimes-frontend の Flat Config 対応ができたのは良かったです。
We are hiring!
フロントエンドエンジニアを含む各種ポジションでの採用を進めています!興味があればぜひご応募ください。

