XO から Oxlint に移行しました

  • URLをコピーしました!

こんにちは、フロントエンドエンジニアのやなぎ(@apple_yagi)です。

PR TIMESでは2023年9月から XO をリンター・フォーマッターとして使ってきましたが、先日 Oxlint に移行しました。本エントリーでは Oxlint に移行した経緯や進め方、そして導入した結果についてご紹介します。

XO は、sindresorhus 氏が開発した Opinionated なリントルールを備えたESLintベースのラッパーツールです。フォーマッターの機能も兼ね備えており、 xo というワンコマンドでリントとフォーマットを実行することができます。

目次

XO を導入した経緯

Oxlint に移行した経緯の前に、XO を導入していた経緯について以下のエントリーから引用しながら軽く触れておきます。

あわせて読みたい
フロントエンドのLintツールをXOに統一した話 こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。 PR TIMESではこれまでLintツールとしてESLintを使用していましたが、2023年9月からXO...

XO を導入する前は PR TIMES のフロントエンド環境のリントルールは全く整備されておらず、なぜこのルールが有効になっているのかや、このルールは必要なのかなどの疑問がありました。これはそもそもリントルールに興味関心がある人が少なかったというのが問題でもありました。そのような状況であったため、各々が ESLint の設定を自由に変え、本来であればエラーになってほしいルールがオフにされていたりしました。

これらの問題を解決するために Opinionated で設定不要な XO を導入しました。XO には500近いルールが入っており、PR TIMES のフロントエンドコードの統一性・安全性を向上してくれました。また、リント結果を通じて JavaScript/TypeScript の記述方法のベストプラクティスを知ることができたり、リントの利便性について再認識する良い機会になりました。最近では社内で自作のリントルールを記述することも増えており、リントルールに興味関心があるエンジニアが増えたことを実感しています。

実行速度の遅さ

XOを導入してから2年半が経ち、PR TIMESのフロントエンドコードは導入当初と比べてかなり大きくなりました。その結果、当初あまり気にならなかったリントの実行速度の遅さが目立ってくるようになりました。

実行速度の遅さをカバーするために CI 上では変更のあったファイルのみをリントするなどの対応を行なってきましたが、no-floating-promiseseslint-plugin-import などのルールでは変更のあったファイルに依存しているファイルもリントを通す必要があるため、そのようなファイルがリントをすり抜けてしまうことがありました。また、リファクタリングデーのような規模の大きい変更をした際はすべてのファイルに対してリントを実行する必要があり、CI が遅くなることがありました。

PR TIMES では Claude Code や Codex などのコーディングエージェントによる開発も盛んに行なっているため、フィードバックサイクルの高速化は生産性に直結します。ESLint では2025年8月マルチスレッドリンティングが実装されましたが、XO では2026年5月現在マルチスレッドリンティングは実装されておらず、XO 自体を高速化するのは難しいと考えています。

そこで ESLint よりも高速に動作し、互換性のある Oxlint への移行を考えました。

Oxlint とは

Rust 製の JavaScript/TypeScript のリンターツールです。ESLint との高い互換性を持ちながらも、ESLint の50~100倍高速に動作するとされています。

GitHub
GitHub - oxc-project/bench-linter: Oxlint is 50 - 100 times faster than ESLint Oxlint is 50 - 100 times faster than ESLint. Contribute to oxc-project/bench-linter development by creating an account on GitHub.

Oxlint への移行方法

前提として XO のルールをすべて Oxlint に移行することにしました。結果としていくつかのルールは移行することができませんでしたが、それについては後ほどご紹介します。

@oxlint/migrate を使用して Oxlint の設定ファイルを自動生成する

Oxlint への移行では @oxlint/migrate を使用しました。@oxlint/migrate を使用することで ESLint の設定ファイルから Oxlint の設定ファイルを自動生成することができます。しかし、XO は設定ファイルが XO 専用の(xo.config.tsのような)ファイルになっているため、まずは --print-config オプションを使用して ESLint の設定ファイルを出力しました。

$ xo --print-config=src/components/button.tsx > .eslintrc.json

--print-config で出力した設定ファイルは JSON 形式になっていますが、@oxlint/migrate は Flat Config の形式しかサポートしていないため、手動で Flat Config の形式に変更してから @oxlint/migrate を実行する必要がありました。

// --print-configで出力した .eslintrc.json
{
  "settings": {
    ...
  },
  "rules": {
    ...
  }
}

// .eslintrc.jsonをeslint.config.js(Flat Config)に変更
import {defineConfig} from 'eslint/config';

// defineConfigの引数に出力されたjsonをそのまま渡せばFlat Config化できる
export default defineConfig({
  settings: {
    ...
  },
  rules: {
    ...
  },
};
// 上記で作成したeslint.config.jsを指定してOxlintの設定ファイルを自動生成する
$ npx @oxlint/migrate eslint.config.js --with-nursery --type-aware

@oxlint/migrate を使用することで Oxlint の設定ファイルを自動生成することができますが、Oxlint で未対応のルールなどはスキップされるため、スキップされたものに関しては手動で設定を追加する必要があります。スキップされたルール数などの情報はログに出力されます。以下は実際に @oxlint/migrate を実行した際のログです。

デフォルトではスキップされたルールがサマリーになっているため、1つずつルールを確認することはできません。そのため、--details オプションを追加して @oxlint/migrate を再実行し、確認しました。

今回は424ルールを自動で移行でき、移行できなかったものは61ルールでした。この61ルールは JS Plugins を使用して1つ1つ有効にしていきました。

JS Plugins を使用して未実装のルールを有効化する

JS Plugins は JavaScript で記述されている既存の ESLint プラグインや、カスタムルールを Oxlint で実行することができる機能です。

Oxc
JS Plugins | Oxlint A collection of high-performance JavaScript tools written in Rust

元々 @oxlint/migrate を実行した際に JS Plugins を使用して eslint-plugin-avaeslint-plugin-no-use-extend-native などのプラグインが有効になっているので、そこに手動でプラグインを追加していきました。

{
  ...,
  "jsPlugins": [
    "eslint-plugin-ava",
    "eslint-plugin-no-use-extend-native",
    "@eslint-community/eslint-plugin-eslint-comments",
    "@stylistic/eslint-plugin",
    "eslint-plugin-prettier",
    "@tanstack/eslint-plugin-query",
    "eslint-plugin-no-barrel-files",
    "eslint-plugin-import-access",
+   {name: 'import-js', specifier: 'eslint-plugin-import'},
+   {name: 'node-js', specifier: 'eslint-plugin-n'},
+   {name: 'promise-js', specifier: 'eslint-plugin-promise'},
+   {name: 'react-js', specifier: 'eslint-plugin-react'},
+   {name: 'unicorn-js', specifier: 'eslint-plugin-unicorn'},
+   {name: 'vitest-js', specifier: '@vitest/eslint-plugin'},
+   // ESLintのコアルールはOxlint-plugin-eslintを使用して有効にする必要がある
+   // eslint-js/ というプレフィックスでルールを指定する
+   'oxlint-plugin-eslint',
  ],
  "rules": {
    ...,
+   'unicorn-js/better-regex': [
+     'error',
+     {
+       sortCharacterClasses: false,
+     },
+   ],
+   'eslint-js/no-unreachable-loop': ['error'],
+   ...,
  }
}

Prettier を Oxlint 上で実行する

XO 内部では eslint-plugin-prettier を使用してフォーマットとリントを同時に実行しています。今回の移行でも XO の設定と同様に eslint-plugin-prettier を JS Plugins 経由で実行するようにしました。

{
  ...,
  "jsPlugins": [
    ...,
    "eslint-plugin-prettier"
  ],
  "rules": {
    ...,
    'prettier/prettier': [
      'error',
      {
        singleQuote: true,
        bracketSpacing: false,
        bracketSameLine: false,
        trailingComma: 'all',
        tabWidth: 2,
        useTabs: false,
        semi: true,
        jsxSingleQuote: true,
        plugins: ['prettier-plugin-packagejson'],
      },
    ]
  }
}

これにより、 Oxlint でもワンコマンドでリントとフォーマットを同時に実行することができます。しかし、この方法ではリントの実行速度が低下するというデメリットがあるため、今後は eslint-plugin-prettier を Oxfmt に移行することを検討しています。

native 実装ではなく JS Plugins を選択したルール

Oxlint で実装されているルールでもあえて JS Plugins 経由で有効にしたルールがいくつかありました。その1つの例として react/jsx-no-target-blank があります。

PR TIMES のフロントエンドコードには a タグに target='_blank' を指定していても、あえて rel=noreferrer を指定していない箇所がいくつかあります。その際に以下のように eslint-disable-line でエラーを抑制していましたが、Oxlint の native のルールでは disable-line を指定する箇所が違い、コードの変更が少々必要だったため、eslint-plugin-react のルールを使用しました。

// eslint-plugin-reactではaタグの横にdisable-lineを記述する
export function Link() {
  return (
    <a // eslint-disable-line react/jsx-no-target-blank
      className={styles.link}
      href='https://prtimes.jp/'
      target='_blank'
    >
      PR TIMES
    </a>
  );
}

// Oxlintのnativeルールでは target='_blank' の上にdisable-lineを記述する
export function Link() {
  return (
    <a
      className={styles.link}
      href='https://prtimes.jp/'
      // oxlint-disable-next-line react/jsx-no-target-blank
      target='_blank'
    >
      PR TIMES
    </a>
  );
}

また eslint-plugin-react のオリジナルのルールでは出なかった箇所でエラーが発生するようになったのも native のルールを使用しなかった一因です。

type Props = PropsWithChildren<'form'>;

export function CompanyRegistrationFormRoot(props: Props) {
  return (
    // {...props} でエラーが発生
    // all spread attributes are treated as if they contain an unsafe combination of props, 
    // unless specifically overridden by props after the last spread attribute prop.
    // help: add rel=`noreferrer` to the element oxc(eslint-plugin-react(jsx-no-target-blank))
    <form {...props}>
      <Input />
      <Button type='submit'>送信</Button>
    </form>
  );
}

そのほかにも react/jsx-no-constructed-context-valuesunicorn/explicit-length-check などでオリジナルのルールでは出なかったエラーが発生したため、JS Plugins 経由で ESLint プラグインのルールを実行しています。

JS Plugins 経由でリントを実行することでパフォーマンスの劣化が懸念されますが、AST へのパースやマルチスレッドリンティングなどは Oxlint側(Rust)でされるため、その点でメリットがあります。

JS Plugins でも有効にできなかった・しなかったルール

JS Pluginsでも有効にできなかった、元々有効だったがあえて有効にしなかったルールは以下の10個です。

unicorn/expiring-todo-comments は内部で

context.sourceCode.getDisableDirectives() という ESLint の API を使用しており、Oxlint への移行を開始した当初のバージョン(v1.61.0)では TypeError: context.sourceCode.getDisableDirectives is not a function というエラーが発生し動作しませんでした。そのため、有効にすることができませんでした。

Oxlint v1.63.0で context.sourceCode.getDisableDirectives 互換のAPIが実装されたため、unicorn/expiring-todo-comments を有効にすることができました 🎉

@typescript-eslint/member-ordering、@typescript-eslint/naming-convention は Oxlint の native で実装されていないため、有効にすることができませんでした。JS Plugins では TypeScript の型情報を使用することはできないため、native で実装されていない typescript-eslint 系のルールは native 実装を待つしかありません。

@typescript-eslint/no-unnecessary-type-arguments、@typescript-eslint/prefer-optional-chain は native で実装されているルールが typescript-eslint のオリジナルのルールと異なる点があり、現環境でリントエラーが出たため、無効にしました。

no-dupe-args、no-octal、no-octal-escape は TypeScript のコンパイルエラーで検知できること、no-return-await、no-buffer-constructor は ESLint 側で非推奨になっていること、またこれらのルールは Oxlint の native でも実装されないことが決まっていたため、有効にしませんでした。

Oxlint 移行当初の前提では XO のルールを全て移行することが条件でしたが、社内で話し合った結果、これらのルールを有効にできるまで待つよりも無効のまま移行する方がメリットが高いと判断し、移行に踏み切りました。

Oxlint に移行した結果

GitHub Actions 上で Oxlint を実行した結果、以下の通りになりました。GitHub Actions の Runner には blacksmithblacksmith-8vcpu-ubuntu-2404 を使用しています。

LinterDurationSpeedup vs. XO
XO1分55秒
Oxlint32秒x3.6

Oxlint が公開しているベンチマーク結果(ESLint の 50〜100 倍)ほどは速くなりませんでしたが、3.6 倍ほど高速化できました。

今回は JS Plugins 経由で ESLint プラグインを多数利用していることや、eslint-plugin-prettier を併用しているため、Oxlint 単体のベンチマークほど大きな差は出なかったと考えています。eslint-plugin-prettier を無効にした場合、Oxlint の実行速度は10秒近く短縮することがわかっているため、今後 eslint-plugin-prettier を Oxfmt に移行することでさらに実行時間を短縮できると考えています。

まとめ

PR TIMES のフロントエンドで使用するリンターを XO から Oxlint に移行しました。XO を使用した期間があったことで様々な ESLint プラグイン、リントルールを知ることができました。フロントエンドチームとしてもリントルールに興味関心を持つことができるようになりました。XO から Oxlint に移行したことにより、自分たちでリントルールをメンテナンスしていく必要がありますが、現在の状況であればそれも問題ないと考えています。今回行った移行は単なるツールの移行ではなく、フロントエンドチームが、より自分たちでリントルールを運用・改善できる段階に進めると考えた結果でもあります。これからもチームの習熟度や開発におけるボトルネックを考えて、取捨選択をしていきたいと思います。

We are hiring!

PR TIMES では、フロントエンドエンジニアを含む各種ポジションでの採用を進めています!興味があればぜひご応募ください。

エンジニア採用情報|株式会社PR T...
エンジニア採用情報|株式会社PR TIMES 開発本部 株式会社PR TIMES のエンジニア採用サイトです。PRTIMESの開発本部は、「行動者発の情報が、人の心を揺さぶる時代へ」の実現に向けて、共に未来をつくっていくエンジニアを...
株式会社PR TIMES
採用情報|株式会社PR TIMES 株式会社PR TIMESの採用情報(新卒採用・キャリア採用)をご紹介します。
  • URLをコピーしました!

この記事を書いた人

株式会社PR TIMES 開発本部 フロントエンドエンジニア

目次