Open24

eslint-plugin-react-refresh周りを調べてあわよくばbiomeに突っ込めたら嬉しいなをゆるりと考える

GunseiKPaseriGunseiKPaseri

react-refresh

react-refreshはreactの公式パッケージで、実行中のReactアプリをリアルタイムで更新できるFast Refresh機能をバンドラに提供する機能。
react-nativeの他、@vitejs/plugin-react@parcel/runtime-react-refresh等開発環境の裏側でよく用いられている。
所謂ホットリロードだが、Stateを保持したまま、編集したコンポーネントのみについて分的に更新することができることが特徴。
元々React native向けの機能だったようだが、技術的詳細は誰かがまとめてくれたところを参照する。
https://zenn.dev/uttk/scraps/f25adbba87e1ba

この機能が有効になるにはReactコンポーネントのみをエクスポートするという条件がある様子。コンポーネント以外のエクスポートを含むファイルを編集すると、全部リロードされてしまうとのこと。
https://reactnative.dev/docs/fast-refresh

eslint-plugin-react-refresh

eslint-plugin-react-refresh@vitejs/plugin-reactからもリンクされているプラグインで、react-refreshを正常に機能させるためのルールを定義している。

GunseiKPaseriGunseiKPaseri

BiomeのLinterルールの追加に関して

最近はプラグイン機能のリリースの動きなどもあるようだが、基本的に本体に追加することになっている。
本体に追加するルールはなるべく実用的で普遍的なものを、ということで議論したうえで追加したりなんだったりが進むらしい
ここにおいて、eslint-plugin-react-refreshは、現時点ではほとんど進んでいない。issueを立てること自体は問題無さそうではある。
https://github.com/biomejs/biome/discussions/3#discussioncomment-8367509

GunseiKPaseriGunseiKPaseri

eslint-plugin-react-refreshの中身

lintルールの本体はonly-export-components.tsというファイルにある。

https://zenn.dev/meijin/scraps/04340d20070f71
等も参考にしつつ読んでいく。
createの引数の関数内でTSESTree(構文木)を調べていって、問題があればcontext.report関数を実行する、といった具合である。

  1. Program ノードのハンドラで、いくつかの初期化処理を行っています。

https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L77-L81

  • hasExports: ファイルにエクスポートがあるかどうかを追跡する変数
  • mayHaveReactExport: 可能性のあるReactコンポーネントのエクスポートがあるかどうかを追跡する変数
  • reactIsInScope: react がインポートされているかどうかを追跡する変数
  • localComponents: ファイル内で定義されたコンポーネントの識別子を格納する配列
  • nonComponentExports: コンポーネント以外のエクスポートの識別子を格納する配列

2.handleLocalIdentifier 関数は、ファイル内で定義された識別子をチェックし、可能性のあるReactコンポーネントの識別子を localComponents 配列に追加します。
https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L4-L9
https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L83-L90

  1. handleExportIdentifier 関数は、エクスポートされた識別子をチェックし、コンポーネントかどうかを判断します。コンポーネントでない場合は nonComponentExports 配列に追加します。
    https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L92-L136

  2. handleExportDeclaration 関数は、エクスポート宣言ノードを処理します。ノードの種類に応じて、handleExportIdentifier 関数を呼び出してエクスポートされた識別子を処理します。
    https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L138-L167

  3. プログラムの本体ノードを探索し、エクスポート宣言ノードを見つけたら handleExportDeclaration 関数を呼び出します。また、変数宣言や関数宣言のノードでは handleLocalIdentifier 関数を呼び出して、ファイル内で定義された識別子を処理します。
    https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L169-L209

  4. プログラムの探索が完了したら、hasExportsmayHaveReactExportlocalComponentsnonComponentExports の値に基づいて、適切な警告メッセージを出力します。
    https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/b7e61e73e368665a44fbc332356e8e5144e4c721/src/only-export-components.ts#L211-L226

GunseiKPaseriGunseiKPaseri

fast-refreshのためにやっていること

  • exportAll: export *は非対応なのでエラー
  • namedExport: コンポーネントでないものが同時にエクスポートされていると機能しないのでエラー
    • Reactエクスポートがあるとき、Reactでないエクスポートに対して
      • React要素の判定は識別子の名前で行う
  • anonymousExport: 匿名関数のエクスポート export default function() では機能しないのでエラー
  • localComponents: エクスポートされないコンポーネントでは機能しないのでエラー
  • noExport: エクスポートされないコンポーネントでは機能しないのでエラー(判定は上記と同一だが、エクスポートの有無でメッセージが変わる)
GunseiKPaseriGunseiKPaseri

他の命名lintルールと合わせれば

  • Jsxファイルでは(指定する例外を除き)パスカルケース要素のみエクスポートを許可する
  • Jsxファイル内ではエクスポートされないパスカルケース要素を許容しない
    みたいな
GunseiKPaseriGunseiKPaseri
export function Component() {};
export const Aa = 'a'

これエラー出さないようにできるけど出せるようにしないとまずいのかな

GunseiKPaseriGunseiKPaseri

ルール作るコマンド

$ just new-js-lintrule noHoge

フォーマット・リント・テスト・コード生成

$ just f
$ just l
$ just t
$ just test-lintrule noHoge
$ just gen-lint