create-react-app Lint構築よくばりパック(ESLint + TypeScript + airbnb + Prettier + lint-staged & husky + VSCode拡張)

create-react-app はデフォルトでESLintおよび独自ルールが組み込まれており、何もしなくてもLintの恩恵を受けられる。しかし、そうであってもカスタマイズしたくなるのがエンジニアの悲しき性。多くの人がカスタマイズに手をだすことだろう。

本記事ではcreate-react-app 環境におけるLint構築で話題によく上がる、「ESLint + TypeScript + airbnb + Prettier + lint-staged & husky + VSCode拡張」の全てを一気に導入するための手順を記載する。

2020年5月現在でLint環境としてまず導入したい全てのものを、可能な限りその出典(主に各パッケージの公式ドキュメント)も含めて抑えてあるつもりだ。またチュートリアル形式で全手順を記載しているため初心者にも安心。ぜひ役立てていただきたい。
(といいつつそもそものモチベーションは自分用メモ)

# はじめに
導入として本記事の背景にあたる部分を説明する。実は「create-react-app におけるLint構築のカスタマイズ」は割とカオスで、真面目に考えると結構迷う部分もある。その中で紹介する手順がどういった位置付けになるかを理解いただければ、以降の理解がより深まるはず。

そんな話はいいから具体的な手順をまず教えてくれ！という方は、
[本編： Lint環境構築](https://qiita.com/jonakp/private/7d9f47c613c16cbf95aa#%E6%9C%AC%E7%B7%A8-lint%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89) まで飛ばしてもらってもOKだ。

## 最初に押さえておいて欲しいこと
「create-react-app におけるLint構築のカスタマイズ」を行うにあたり、
最初にどうしても押さえておいて欲しいことがある。

それは「create-react-appは公式にはLintのカスタマイズに対応していない」ということだ。
より正確には「デフォルトのLint機能はカスタマイズも除去もできない[^1]」

詳しい経緯はかなり込み入った内容で本題から大きく外れるので省くが、知りたい方は以下のgithub issue等を参照いただきたい。
https://github.com/facebook/create-react-app/issues/3886

ともかく、基本的に`yarn start`時やブラウザのコンソールで表示されるLintの結果に関しては、こちらで手を加えることはできないと考えた方が良い。

## カスタマイズの方針
その上で取りうる方針は基本的に以下の3つになる。

1. 公式に従う
2. デフォルトのLintとは別ラインでESLint環境を構築する。<br>(デフォルト＆カスタマイズの2つのLint環境が両立している状態)
3. ejectする

"1." を採用するのが一番楽で手堅い。しかしそれで良い人はこの記事を読まないだろう。
"3." は沼なのでオススメしない。twitterあたりで「cra eject」あたりで検索するとそのことが理解できる(craはcreate-react-appの略称)。

本記事では"2."を採用する。
実際「create-react-app におけるLint構築のカスタマイズ」に関して調べると、多くは "2."を採用しているようだ。この場合「コンソールやビルド時のlintログがカスタマイズのLint内容と一致しない」というデメリットがあるが、Prettierやlint-stagedを用いることで、その影響を実運用上ほぼゼロに出来る。

いずれにせよ本記事の内容は、その多くがcreate-react-app公式推奨のやり方ではないという点だけは留意いただきたい。それでも他の情報を見る限り、このやり方はスタンダードに近いだろう。

# 本編： Lint環境構築
以下create-react-appによるアプリの作成から、

- ESLint(TypeScript, airbnb)
- Prettier
- lint-staged & husky
- VSCode拡張

を導入するための手順をチュートリアル形式で説明する。

これら全て行うことで、
「Lintの指摘を全てクリアしない限り、git commitが出来ない」
という素晴らしい環境が実現できる。

Lintを軽視する愚か者も、ここまですれば軽率な行動を取ることはないだろう。
(それってもしかして私のこ・・・ うっ、頭が…！)

以降は手順を端的に記載していく。ぜひ手を動かして試してみて欲しい。

## アプリの作成
まずはじめにcreate-react-appでReactアプリを作成する。--typescriptを忘れないこと。

```bash
$ npx create-react-app react-lint-env --typescript
```

## ESLintの設定
Lint環境のメインであるESLintの設定を行う。
相応に設定内容も多いが、記載の通りにやってもらえれば問題ないはずだ。
なお本手順ではairbnbを採用しているが、それは厳しすぎるという人のためにairbnbを使わない場合の手順も記載してある。必要な人は参照していただきたい。

### eslint --initの実行
はじめに`eslint --init`を実行し、.eslintrc.json(eslintの設定ファイル)を作成する。
ESLint自体はcreate-react-appの段階でインストール済みなので、yarn add等する必要はない。
(`yarn eslint ...`で実行可能)

`eslint --init`では対話式で設定内容を聞かれるが、それも含めて以下を参考にして欲しい。

参考サイト:
https://eslint.org/docs/user-guide/getting-started#installation-and-usage

```bash
$ yarn eslint --init
yarn run v1.22.4
react-lint-env/node_modules/.bin/eslint --init
? How would you like to use ESLint? # -> To check syntax, find problems, and enforce code style
? What type of modules does your project use? # -> JavaScript modules (import/export)
? Which framework does your project use? # -> React
? Does your project use TypeScript? # -> Yes
? Where does your code run? # -> Browser
? How would you like to define a style for your project? # -> Use a popular style guide
? Which style guide do you want to follow? # -> Airbnb: https://github.com/airbnb/javascript
? What format do you want your config file to be in? # -> JSON (または好きなものを選ぶ)
? Would you like to install them now with npm? # -> No (yarnを使う場合はNo必須)
```

なおairbnbを使用しない場合は、最初の質問で以下の選択をする。

```bash
To check syntax and find problems # -> "To check syntax and find problems" または "To check syntax only"
```


### 必要パッケージのインストール
.eslintrc.jsonの設定内容に応じて必要になるパッケージをインストールする。

```bash
yarn add -D eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-import
```

各パッケージの概要は以下の通り。

- eslint-plugin-react... ESLint公式推奨のReactルール
- @typescript-eslint/parser & @typescript-eslint/eslint-plugin... ESLintをTypeScriptに対応させるためのツール
- eslint-config-airbnb... airbnbのESLintルール
- eslint-plugin-jsx-a11y & eslint-plugin-import... eslint-config-airbnbの依存パッケージ

参考:
なし (これらのパッケージは、インストールしていない状態で`yarn eslint .`を実行することで(エラー表示と共に)必要なことが示される)

airbnbを使用しない場合は、以下のコマンドを実行する

```bash
yarn add -D eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

### "plugin:@typescript-eslint/recommended"の追加
前項で追加したパッケージのうち、typescript-eslint関連(@typescript-eslint/parser @typescript-eslint/eslint-plugin)については、.eslintrc.jsonへの設定追加が必要

参考:
https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md#configuration

```diff
diff --git a/.eslintrc.json b/.eslintrc.json
index 04f1ce9..19f04da 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -5,7 +5,8 @@
   },
   "extends": [
     "plugin:react/recommended",
-    "airbnb"
+    "airbnb",
+    "plugin:@typescript-eslint/recommended"
   ],
   "globals": {
     "Atomics": "readonly",
```

### eslintへの設定追加(任意)
現状の設定だと開発時に不都合が生じるため、.eslintrc.jsonに以下の設定追加を推奨。
この点に関しては公式ではなく、参考サイト様の情報頼り。
(ありがとうございます)

参考:
https://ginpen.com/2019/08/06/eslint-for-react-in-typescript/

```diff
diff --git a/.eslintrc.json b/.eslintrc.json
index 4eebfbe..5461c90 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -22,5 +22,21 @@
     "sourceType": "module"
   },
   "plugins": ["react", "@typescript-eslint"],
-  "rules": {}
+  "settings": {
+    "import/resolver": {
+      "node": {
+        "extensions": [".js", ".jsx", ".ts", ".tsx"]
+      }
+    }
+  },
+  "rules": {
+    "@typescript-eslint/indent": ["error", 2],
+    "@typescript-eslint/prefer-interface": "off",
+    "react/jsx-filename-extension": [
+      "error",
+      { "extensions": [".jsx", ".tsx"] }
+    ],
+    "react/prop-types": "off",
+    "spaced-comment": ["error", "always", { "markers": ["/ <reference"] }]
+  }
 }
```

### eslintの動作確認
ここまででeslintの設定は完了になる。そこでESLint単体で正常に動作しているか確認する。

```bash
yarn eslint . --ext .tsx --ext .ts
yarn run v1.22.4
$ /react-lint-env/node_modules/.bin/eslint . --ext .tsx --ext .ts

react-lint-env/src/App.test.tsx
  3:17  error  Unable to resolve path to module './App'        import/no-unresolved
  3:17  error  Missing file extension for "./App"              import/extensions
  5:1   error  'test' is not defined                           no-undef
  6:32  error  JSX not allowed in files with extension '.tsx'  react/jsx-filename-extension
  8:3   error  'expect' is not defined                         no-undef
  # (中略)
  145:14  error    Expected parentheses around arrow function argument          arrow-parens
  146:9   warning  Unexpected console statement                                 no-console

✖ 42 problems (36 errors, 6 warnings)
  22 errors and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```
多数エラーが出てくるが、Eslintの動作確認としてはこの結果でOK。
(create-react-appで生成されるコードはairbnb基準だとかなり指摘が入る)

念の為 yarn start(Reactアプリのビルド)が正常に動作しているかも確認する。

```bash
$ yarn start
Compiled successfully!

You can now view react-lint-env in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.0.51:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

```

## Prettierの設定
次にformatterとしてPrettierを導入する。ルールに合致しないコードを自動で修正してくれる。
(特にVSCodeの拡張と組み合わせることでその真価を発揮する)
Prettierはあまりカスタマイズしない運用を推奨しており、設定内容もそれほど多くない。

### 関連パッケージのインストール
必要なパッケージをインストールする。

参考サイト:
https://prettier.io/docs/en/integrating-with-linters.html#recommended-configuration

```bash
yarn add -D prettier eslint-plugin-prettier eslint-config-prettier
```

各パッケージの概要は以下の通り。

prettier... prettier本体
eslint-plugin-prettier... ESLintでformatterとしてPrettierを使うようにしてくれる
eslint-config-prettier... ESLintとPrettierでバッティングする箇所をよしなにしてくれる


### .eslintrc.jsonへの設定追加
eslint-plugin-prettier, eslint-config-prettier の両方を使用する設定。
この設定を行うことで、`yarn eslint --fix`実行時にPrettierを利用したコードフォーマットが行われるようになる。

参考: https://prettier.io/docs/en/integrating-with-linters.html#recommended-configuration

```diff
diff --git a/.eslintrc.json b/.eslintrc.json
index 19f04da..3bac18e 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,7 +6,8 @@
   "extends": [
     "plugin:react/recommended",
     "airbnb",
-    "plugin:@typescript-eslint/recommended"
+    "plugin:@typescript-eslint/recommended",
+    "plugin:prettier/recommended"
   ],
   "globals": {
     "Atomics": "readonly",
```

### .prettierrc.jsonの作成(任意)
prettierのカスタマイズ。やらなくても問題はない。
以下の設定は私の趣味(一応airbnbに合わせている)

参考:
https://prettier.io/docs/en/configuration.html#basic-configuration

```bash
$ touch .prettierrc.json
```

```diff
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..fd496a8
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+  "semi": false
+}
```

### Prettier動作確認
Prettierの設定は以上。動作確認を行う。eslint コマンドに”—fix”をつける

```bash
$ yarn eslint . --ext .tsx --ext .ts --fix
yarn run v1.22.4
$ react-lint-env/node_modules/.bin/eslint . --ext .tsx --ext .ts

react-lint-env/src/App.test.tsx
  1:26  error  Delete `;`                                      prettier/prettier
  2:48  error  Delete `;`                                      prettier/prettier
  3:17  error  Unable to resolve path to module './App'        import/no-unresolved
# (中略)
@typescript-eslint/explicit-module-boundary-types
  143:9   warning  Unexpected console statement                                 no-console

✖ 28 problems (19 errors, 9 warnings)
  7 errors and 0 warnings potentially fixable with the `--fix` option.

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/App.test.tsx
        modified:   src/App.tsx
        modified:   src/index.tsx
        modified:   src/react-app-env.d.ts
        modified:   src/serviceWorker.ts
        modified:   src/setupTests.ts

no changes added to commit (use "git add" and/or "git commit -a")
```

Prettierによる指摘の確認と、それに伴うコードの修正が実施されればOK。
(create-react-appで生成されたコードであっても、Prettierによってかなり修正を加えられているはず)


## lint-staged & haskyの設定
lint-staged & haskyの設定を行うことで、git commit時にLintチェックし、パスしない場合commitを却下することが可能になる。

### パッケージのインストール

```bash
$ yarn add -D lint-staged husky
```

各パッケージの概要は以下の通り。

lint-staged... gitのstage状態にあるファイルを対象にLintを実行する
husky... Gitフック(特にpre-commit）をハンドリングする


### package.jsonへの設定追加
参考:
https://github.com/okonet/lint-staged#examples

```diff
diff --git a/package.json b/package.json
index 80bb375..4c4ccc2 100644
--- a/package.json
+++ b/package.json
@@ -49,5 +49,13 @@
     "husky": "^4.2.5",
     "lint-staged": "^10.2.7",
     "prettier": "^2.0.5"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "lint-staged": {
+    "*.{ts,tsx}": "eslint --max-warnings=0 --ext .tsx --ext .ts --fix"
   }
 }
```

eslintのコマンドに、`--max-warning=0`というオプションを付けている点に注意。
これが無いとwarningだけ残っている場合にcommitがパスしてしまう。

参考:
https://qiita.com/kondei/items/fbad4b746836383c6481

### lint-staged & husky 動作確認
適当なtsxファイルにlintに引っかかる修正を加えた上で、git commitを行う。
lintが実行され、git commitの失敗を確認できればOK。

```diff
diff --git a/src/App.tsx b/src/App.tsx
index a53698a..cf0d16b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,6 +3,8 @@ import logo from './logo.svg';
 import './App.css';
 
 function App() {
+  const test = 1
+
   return (
     <div className="App">
       <header className="App-header">
```

```bash
$ git commit -m "add variable"
husky > pre-commit (node v12.7.0)
✔ Preparing...
⚠ Running tasks...
  ❯ Running tasks for *.{ts,tsx}
    ✖ eslint --max-warnings=0 --ext .tsx --ext .ts --fix [FAILED]
↓ Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up... 

✖ eslint --max-warnings=0 --ext .tsx --ext .ts --fix:

react-lint-env/src/App.tsx
   5:1   warning  Missing return type on function                          @typescript-eslint/explicit-module-boundary-types
   6:9   warning  'test' is assigned a value but never used                @typescript-eslint/no-unused-vars
  13:16  error    `code` must be placed on a new line                      react/jsx-one-expression-per-line
  13:40  error    Replace `⏎{'·'}⏎` with `·`                               prettier/prettier
  14:1   error    Expected indentation of 10 space characters but found 0  react/jsx-indent
  14:1   error    Expected indentation of 10 spaces but found 0            @typescript-eslint/indent
  14:6   error    Expected indentation of 10 space characters but found 0  react/jsx-indent
  16:1   error    Insert `········`                                        prettier/prettier
  16:1   error    Expected closing tag to match indentation of opening     react/jsx-closing-tag-location
  16:1   error    Expected indentation of 8 space characters but found 0   react/jsx-indent
  16:1   error    Expected indentation of 8 spaces but found 0             @typescript-eslint/indent

✖ 11 problems (9 errors, 2 warnings)
  9 errors and 0 warnings potentially fixable with the `--fix` option.

husky > pre-commit hook failed (add --no-verify to bypass)

```

## VSCode拡張機能
### ESLint
以下をVSCodeにインストール。VSCode上でリアルタイムにLintのエラーを表示してくれる。
https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

### Prettier
以下をVSCodeにインストール。ファイル保存のタイミングでPrettierの実行(= コードフォーマット)が可能となる。
https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode

以下の設定も必要。
Editor: Format On Save を有効化
![スクリーンショット 2020-05-25 23.38.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/905f4a95-8f84-19f1-52af-40f24727b398.png)

# さいごに
この記事書くの大変だった。疲れた。

[^1]: 公式ドキュメントには「Extending the ESLint config」に関する記載はあるが、これはExperimentalなものであり、また利用した記事もあまり見当たらないため、本記事では対象外とする。