React+TSプロジェクトで便利だったLint/Format設定紹介

2021/09/30に公開
3

こんにちは、よしこです。

この記事は 2020年に立ち上げたWebフロントエンド構成の振り返り の「linter/formatter」項の詳細記事です。単体でも読めますが、よければ元記事もあわせてどうぞ!


この記事では、今わたしが 株式会社ナレッジワーク というスタートアップで開発・運用しているプロジェクトにおいて便利だったLint/Format関連の設定についてご紹介していきます。

使っているのは、TSのlintのためにESLint, CSSのlintのためにStylelint, 主なファイルのformatのためにPrettierです。

ESLint

pluginsとextendsだけどんなもの入れてるか載せておきます。

"plugins": [ 
  "strict-dependencies", // 後述
  "unused-imports", // 後述
],
"extends": [
  "airbnb",
  "plugin:@typescript-eslint/recommended",
  "next/core-web-vitals", // 後述
  "prettier", // 後述
],

便利に感じている設定

依存関係のチェック(strict-dependencies)

「このパスのモジュールは、あの階層のファイル郡からしか参照してはいけない」みたいなルールを決めたいことってないですか。

  • src/components/page/* はNextのルーティング用の src/pages/* からしか読み込まない
  • next/router は直接使わずに、ラップした src/libs/router.ts を必ず使いたい

みたいな。あとはアーキテクチャのレイヤー間での依存関係の方向縛りとか。
それを自動でlintできたらいいなと思ったので、社内メンバーにそういうことしたいんだよねーって話したら、しゅっとeslintルールを書いてくれました。(最高か?)

そして元の 構成まとめ記事 への反応のうち、特にこのルールの記述への反応が多かったので、この度そのルールをnpm packageとして公開しました!ナレッジワーク初のOSS🎉
さくっとファイル移動してきただけなのでOSSとしてはあまり体裁整ってないかもですが、公開することで他の方にも役立てばと思いハードル低く公開してみました。

https://www.npmjs.com/package/eslint-plugin-strict-dependencies

READMEを読んでもらえるとわかるかと思うのですが、「module」のパスと、そのmoduleへの依存が許可される「allowReferenceFrom」をセットで設定していく形です。
設定を書いたmoduleはallowReferenceFromに指定されているパスからしか読み込めなくなります。exampleに組み込めなかったのですが、パスにはglobも使えます! (hoge/*/fuga とか)

よければGitHub repoにstarください🌟

importの自動整列(import/order)

またまたimport関連。
コードを見てて、importの順序や位置が気になってしまうことないですか?外部モジュールは最上部に固めたいなぁとか。
そういうのはimport/order ルールで定義できます! ルールのdocsはこちら

"import/order": [
  "error",
  {
    "groups": ["builtin", "external", "internal", ["parent", "sibling"], "object", "type", "index"],
    "newlines-between": "always",
    "pathGroupsExcludedImportTypes": ["builtin"],
    "alphabetize": { "order": "asc", "caseInsensitive": true },
    "pathGroups": [
      // ここに書いた順序で間に1行空行をあけつつ整頓される
      { "pattern": "@/libs/**", "group": "internal", "position": "before" },
      { "pattern": "@/generated/**", "group": "internal", "position": "before" },
        // ... 省略
      { "pattern": "@/components/ui/**", "group": "internal", "position": "before" },

      // styles
      // 最後尾にしたいのでgroupをindex扱いにする
      { "pattern": "./**.module.css", "group": "index", "position": "before" },
    ]
  }
],

これで外部モジュール→内部モジュール(さらにその中で順番を定義)→CSS Modules、とimport順を自動で並び替えられます。すっきり!

ついでにもうひとつimportまわりでおすすめなのが、未使用のimportの自動削除。
eslint-plugin-unused-imports を入れておくと、 eslint --fix で実行時に未使用のimportを自動で消してくれます!
コードを変えたことによって参照されなくなったimportをわざわざ消しにいかなくてよくなり、これもとても便利。

overrides

これは普通にeslintの標準機能なのですが、途中から知って便利!となったので紹介。
ベースのルールはrulesに書いていくと思いますが、「この種類のファイルにはこのルール当てなくていいんだよなー」っていうのをいちいちファイルごとにeslint-disabledコメントでオフってませんか?私はやってました。
例外的なものではなく規則的なものであれば、overridesでまとめてルールを上書きしたほうがスマートでした。たとえば以下。

"overrides": [
  {
    "files": ["*.stories.tsx", "src/pages/**/*"],
    "rules": {
      "import/no-default-export": "off",
    }
  },
  {
    "files": ["*.test.ts", "*.mock.ts"],
    "rules": {
      "max-lines": "off",
    }
  }
],

便利ですね!

この記事を書くにあたって見直したこと

この記事を書くにあたって.eslintrcを見直している中で、初めて気付いて改善できたことがいくつかあったので紹介。

parserOptions.project要らなかった

parserが @typescript-eslint/parser のときは parserOptions.project にtsconfig.jsonのパスを指定しないといけないものかと思っていたのですが、型情報が必要なルールを使わないのであれば不要みたいです。外しても問題なく動いた上、ちょっと速くなりました。

prettierとの協調のさせかた(eslint-config-prettier)

plugin:prettier/recommended をextendsするのはもう古かったみたいです。pluginではなくてconfigを使ってねという案内になっていました。
stylelintのconfigがあるのも知らなかったので追加。

Next.jsの推奨config(eslint-config-next)

Next.js 11でeslintの推奨configが出てたんですね。 extendsに next/core-web-vitals を入れて、 react react-hooks はnext経由で読めるので削除しました。

今後やりたいこと

また依存関係の制御なのですが、以下の記事めっちゃわかる〜〜!!ってなったので eslint-plugin-import-access を取り入れたいです。アプリケーションとしては使わないからexportしたくないけど、テストなどのためにexportせざるを得ないモジュールってあるんですよね。。

https://zenn.dev/uhyo/articles/eslint-plugin-import-access

上記といい前述の自社開発ルールといい、たかがimportをそんなにガチガチに縛ってしんどくない?って思われるかもですが、依存関係の制約はルールとして文書だけで存在していてもチームへの浸透に限界があるので、間違えたときにlintなどで自動的に教えてもらえる環境を作っておくのが一番楽だと思うんですよね。
楽するために苦労するのが好きです。

Stylelint

こちらもextendsとpluginsを。

"extends": [
  "stylelint-config-standard",
  "stylelint-config-recess-order", // 後述
  "stylelint-config-prettier"
],
"plugins": [
  "stylelint-order", // 後述
],

便利に感じている設定

プロパティの自動整列

一番やりたいのがこれ。(自動整列好きだねぇ…)

stylelint-orderstylelint-config-recess-order を入れておくと、stylelint-config-recess-orderに定義されているグループ・順番でプロパティを並び替えてくれます。
とりあえず意味合い的に近いところがグループになりつつ誰が書いても一意な並び順に定まればいいので、これでじゅうぶんです。

加えて、これだけだとCustom Propertiesの定義位置はまとめてもらえないので、rulesのところにも以下を追加しています。

"rules": {
  "order/order": [
    "custom-properties",
    "declarations"
  ],
}

Prettier

特筆事項はないです!ここの設定は好みですよね。

実行タイミング

npm scriptsを以下のように用意しています。

"scripts": {
  "lint:js": "eslint '**/*.@(js|ts|tsx)'",
  "lint:css": "stylelint '**/*.css'",
  "check": "tsc && prettier --list-different . && npm run lint:js && npm run lint:css && npm run test",
  "format": "prettier --write . && npm run lint:js -- --fix && npm run lint:css -- --fix",
}

ちなみにlint:jsでjsファイルを指定しているのは、srcの内側にはないものの、外側にはconfigファイルやnodeのscriptファイルなどで存在するからです。

そして、どのタイミングでどのscriptを実行するか?について。

ファイル保存時(VSCode)

frontendディレクトリ直下に .vscode をgit管理しており、以下の設定を入れています。

"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
  "source.fixAll.eslint": true,
  "source.fixAll.stylelint": true,
  "source.addMissingImports": true,
},

これにより、保存時にprettierでのformatとeslint/stylelintのautofixを効かせられます。

ちなみに、source.addMissingImports はアプリケーション内で使っているモジュールがまだimportされていないときに自動で探してimport文を足してくれる設定で、とても便利です。

  • import自動追加: vscodeの source.addMissingImports
  • import自動削除: eslintの eslint-plugin-unused-imports
  • import自動整列: eslintの eslint-plugin-import/order

この3つの設定に辿り着いたあとは、ファイル上部のimport文を手でいじることが一切なくなりました。最高。超推し設定です。

コミット時(husky)

husky v7とlint-stagedで、pre-commitで実行しています。
pre-commitで色々やるのは好み分かれると思うのでチームの意見を尊重したほうがよいですが、うちはまだチーム人数少ないのと、PR出してからCIで落ちるよりはpre-commitで気づけたほうが楽かなってことでこうしてます。
雑にコミットしたいときは --no-verify でスキップできます。

"lint-staged": {
  "*.@(ts|tsx)": "bash -c tsc",
  "*.@(js|ts|tsx)": "eslint --fix",
  "*.test.@(js|ts|tsx)": "jest",
  "*.css": "stylelint --fix",
},

最初ts/tsxのlint-stagedでtscする方法がわからず、単に tsc とするとファイル一覧が引数に渡ってしまうことでエラーになって困ってたのですが、 bash -c tsc とすれば引数渡さない形で実行できるというのを知ったときにはアハ体験しました。

PR時(CI)

CIで yarn run check を実行しています。エディタ保存時とpre-commitでやっているので、ここで引っかかることはほぼないです。

おまけ:ignoreファイル

.prettierignore .eslintignore .stylelintignore がほぼ同じ内容で存在してたので、 .lintignore というファイルにまとめてからそれぞれシンボリックリンクにしてみたらうまく動きました。


以上!
lint/formatまわりでも色々話せますね。

わたしはimportやプロパティの並び順みたいに結構細かいところが気になっちゃうタイプなのですが、同時にそういうのはコードレビューで指摘する類のものではないとも思っているので、誰がどう書いても機械が勝手に整えてくれるような仕組みを作るようにしています。そうするとみんなすっきりできるのでおすすめです✨

元記事では他にも様々な項目の構成紹介をしています。よければあわせて読んでみてください!

https://zenn.dev/yoshiko/articles/32371c83e68cbe

Discussion

Katsuma NarisawaKatsuma Narisawa

ご存知かもしれませんが、モジュールの依存関係を整理したい場合は、Project Referencesが便利かなと思います!
(自分で記事書いておきながら、あまり使われている事例を耳にしないので、デメリットなどあればむしろぜひ教えて欲しいです)
https://zenn.dev/katsumanarisawa/articles/58103deb4f12b4

よしこよしこ

Project References単純に知りませんでした!
記事拝見しましたが、たとえばテストがまるごとsrcとは別のディレクトリに分かれてるみたいな、大きな単位のパッケージ分割には合いそうですね。

うちの構成&需要だと結構細かい設定がほしくて、Project Referencesでやろうとするとたとえばcomponentsディレクトリだけでも components/page components/model components/ui 以下にそれぞれpackage.jsonを置くみたいな感じになって分散してしまい、設定の一覧性がなくなってしまいそうかなと><
あと hoge/*/fuga.ts から foo/*/bar.ts への依存はどうこう、みたいなglobを使った設定の需要もあったりするので、eslintrcにまとめて書いてlintで軽くチェックできる感じが合ってるかなと思いました!

Katsuma NarisawaKatsuma Narisawa

おーなるほど!
一覧性・細かい依存関係の指定という観点だと確かに向いていないですね。
うちだとレイヤーごと、モジュールごとにtsconfig.jsonを設置してますが、新しくモジュールとして切り出そうと思った時にtsconfig.jsonを新規作成したりファイルを更新するのは、多少面倒なところはあります。
参考になりました!丁寧なお返事ありがとうございます。