Next.js + TypeScriptの開発環境をVSCodeで整える(ESLint + Prettier + Husky + lint-staged + commitlint + EditorConfig)

# ESLintとかPrettierとか設定が難しい

1年ぶりにフロントエンドに戻って来ると色々と変わっていました。具体的には...

- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)が非推奨になっていた
- [husky](https://github.com/typicode/husky)がv6になって動かなくなっていた

このあたり本当によく変わりますよね。今回は以下のツールを設定して一流のNext.js + TypeScript開発環境の構築方法をまとめます。ESLintのところ以外はNext.jsでなくてもTypeScriptでなくても使えるはずです。

- [Prettier](#prettier)
- [ESLint](#eslint)
- [Husky + lint-staged](#husky--lint-staged)
- [commitlint](#commitlint)
- [EditorConfig](#editorconfig)

今後できれば更新していきたいです。

# Changelog
- Next.jsに寄せました(2021/06/26)。
- Next.js 11で追加された[eslint-config-next](https://nextjs.org/docs/basic-features/eslint)に対応しました(2021/07/10)。
- [ESLintの設定をNext.jsの公式](https://nextjs.org/docs/basic-features/eslint)に従った形に修正しました(2021/09/17)

# Prettier

## Prettierとは
[Prettier](https://prettier.io)はFormatter(コードを自動整形してくれるツール)です。

- `""`か`''`
- `()`をつけるか
- `,`をつけるか
- ...etc

といったスタイルを統一してくれます。

## Prettierのインストールと設定
```sh
yarn add -D prettier
```

[デフォルトの設定](https://prettier.io/docs/en/options.html)を上書きしたいところだけを設定ファイルに記載します。

```jsonc:.prettierrc
{
  "jsxSingleQuote": true,
  "singleQuote": true,
  "trailingComma": "all"
}
```

CIなどで使えるように`package.json`にscirptを用意しておきます。

```jsonc:.package.json
"scripts": {
  "lint": "prettier --check .",
  "format": "prettier --write .",
  ...
},
```

## VSCodeでファイル保存時にPrettierで自動整形する
さらにファイルを保存したタイミングで自動で整形するようにするために、[Prettierのプラグイン](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)をインストールします。他の開発者のために推奨プラグインに記載しておきます。

```jsonc:.vscode/extensions.json
{
  "recommendations": [
    "esbenp.prettier-vscode",
  ]
}
```

VSCodeの設定ファイルを作成して、ファイルを保存したタイミングでPrettierで整形するように設定します。

```jsonc:.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
}
```

ここで`yarn format`を実行して何が整形されるか見ておくとESLintとの違いがわかりやすいかもしれません。


# ESLint

## ESLintとは
[ESLint](https://eslint.org)はLinter(コードの品質チェックをするツール)です。

- 使われていない変数は無いか
- 意図せずにglobal変数になっている変数は無いか
- `===`の代わりに`==`が使われていないか
- ...etc

といった品質チェックをしてくれます。チェックした後に自動修正もできるのでPrettierとの役割分担が曖昧になりがちです。

これまでは[eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)を使って、ESLintからPrettierを実行する手法が一般的でしたが、[この方法は非推奨になりました](https://prettier.io/docs/en/integrating-with-linters.html)。

これからはPrettierでFormatをして、ESLintで品質チェックをするように役割を分担して、競合する部分はPrettierにやらせるようにするようです。

## ESLintのインストールと設定

```sh
yarn add -D eslint
```

ESLintの初期設定はCLIを使うと簡単です。

```sh
yarn eslint --init

✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · react
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser, node
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
```

次に`extends`に推奨設定を必要なものを入れていきます。

- `plugin:@typescript-eslint/eslint-recommended`と`plugin:@typescript-eslint/recommended`の両方を設定しているのも見かけますが、後者は前者を`extends`しているので後者だけでよいでしょう([src](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/src/configs/recommended.ts#L6))。
- `plugin:@typescript-eslint/recommended-requiring-type-checking`は型を必要とするルールを有効化します。`tsc`を実行しているようなものなのでチェックが遅くなりますが、入れておくべきでしょう。
- [eslint-config-airbnb-typescript](https://github.com/iamturns/eslint-config-airbnb-typescript)を採用するのもありだと思いますが、今回は見送っています。
- `plugin:@next/next/recommended`はNext.jsの`<Link />`や`<Image />`の使い方のチェックをしてくれるので入れておいて損はないでしょう。
- `plugin:@next/next/core-web-vitals`は[Core Web Vitals](https://web.dev/vitals/)の関連のチェックをしてくれます。[`plugin:@next/next/recommended`を継承している](https://github.com/vercel/next.js/blob/2351e5b70198279c2bfcb59891ee0e0b1e587ff5/packages/eslint-plugin-next/lib/index.js#L52)ので、これを採用する場合は`plugin:@next/next/recommended`は必要ありません。

必要なものを追加インストールします。

```sh
yarn add -D eslint-config-prettier @next/eslint-plugin-next 
```

ESLintの`extends`は後ろのものほど優先されるので、`prettier`を最後尾に、その前に`next.js`関連を持ってきます。結果、このような`.eslintrc.js`になります。

```diff_javascript:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
-   'plugin:react/recommended', // reactの設定はairbnbに任せます
    'airbnb',
+   'airbnb/hooks', // reactのhooksの設定は別れているので別途入れます
+   'plugin:@typescript-eslint/recommended', // typescriptの推奨設定を入れます
+   'plugin:@typescript-eslint/recommended-requiring-type-checking', // typescriptの型チェックをします
+   'plugin:@next/next/core-web-vitals', // Next.jsとCore Web Vitalsのチェック
+   'prettier', // prettierと競合するESLintのルールを無効化します
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
+   project: './tsconfig.json', // plugin:@typescript-eslint/recommended-requiring-type-checkingの参照するtsconfig
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {},
};
```

これでは`.eslintrc.js`がに対してこのようなエラーが出てしまうので、`.eslintignore`を作成して`.eslintrc.js`自体をESLintのチェックから除外します。

```shell
0:0  error  Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: .eslintrc.js.
The file must be included in at least one of the projects provided
```

```javascript:.eslintignore
/.eslintrc.js
```

さらにTypeScriptの`compilerOptions.paths`のAliasをサポートするために一手間加えます。

```sh
yarn add -D eslint-import-resolver-typescript
```

```diff_javascript:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:@next/next/core-web-vitals',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    project: './tsconfig.json',
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {},
+ settings: { // typescriptのaliasがエラーにならないように
+   'import/resolver': {
+     typescript: {
+       project: './tsconfig.json',
+     },
+   },
+ },
};
```

後はエラーを消すために独断と偏見で`rules`を設定してください(これが少し面倒)。

<details>
<summary>余計なものも入ってますが、参考までに`rules`設定済みのものを置いておきます。</summary>
<div>

```js:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:@next/next/core-web-vitals',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    project: './tsconfig.json',
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {
    // hush no-use-before-define error in "import React from 'react';"
    // see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
    'no-use-before-define': 'off',
    '@typescript-eslint/no-use-before-define': ['error'],

    // omit .ts .tsx in import statement
    // see https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions
    'import/extensions': [
      'error',
      'ignorePackages',
      {
        js: 'never',
        jsx: 'never',
        ts: 'never',
        tsx: 'never',
      },
    ],

    // allow named exports without default export
    'import/prefer-default-export': 'off',

    // allow importing storybooks from devDependencies
    // see https://github.com/storybookjs/storybook/issues/1992#issuecomment-335001056
    'import/no-extraneous-dependencies': [
      'error',
      {
        devDependencies: ['.storybook/**', '**/*.stories.tsx'],
      },
    ],

    // allow jsx in .tsx
    'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }],

    // nextjs does not need React in every file
    'react/react-in-jsx-scope': 'off',

    // use this pattern for default props
    //
    // interface Props {
    //   prop1?: boolean;
    // }
    // const Component1: FC<Props> = ({ prop1 = false }: Props) => (
    //   <Component1 prop1={prop1} />
    // );
    'react/require-default-props': 'off',

    // allow <App {...props} />
    'react/jsx-props-no-spreading': 'off',
  },
  settings: {
    'import/resolver': {
      typescript: {
        project: './tsconfig.json',
      },
    },
  },
};
```
</div>
</details>

CIなどで使えるように`package.json`にscirptを用意しておきます。ESLintで`;`が追加された後にPrettierの`printWidth`を超えた場合などに備えてPrettierを後にしています。

```diff_json:.package.json
"scripts": {
- "lint": "prettier --check .",
- "format": "prettier --write .",
+ "lint": "eslint . && prettier --check .",
+ "format": "eslint --fix . && prettier --write .",
  ...
},
```

## VSCodeでファイル保存時にESLintで自動整形する
さらにファイルを保存したタイミングで自動で整形するようにするために、[ESLintのプラグイン](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)をインストールします。他の開発者のために推奨プラグインに記載しておきます。

```diff_json:.vscode/extensions.json
{
  "recommendations": [
+   "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
  ]
}
```

VSCodeの設定ファイルにファイルを保存したタイミングでESLintで整形するように設定します。これでファイルを保存するのタイミングでPrettierとESLintの両方で整形されるようになります。

```diff_json:.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.codeActionsOnSave": {
+   "source.fixAll.eslint": true
+ },
}
```

これでPrettierとESLintの設定が終わったので、`yarn format`を実行しておくと良いです。


# Husky + lint-staged

## Huskyとは
[Husky](https://typicode.github.io/husky/#/)は[Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)を設定するツールです。

- commitの前に~する
- pushの前に~する
- commit messageに対して~する
- ...etc

ということが設定できます。

## lint-stagedとは
[lint-staged](https://github.com/okonet/lint-staged)はstageされたファイルだけを操作するためのツールです。Huskyと併用することで、commitされるファイルに対してだけPrettierやESLintのチェックをするということができるようになります。

## lint-stagedのインストールと設定
```sh
yarn add -D lint-staged
```

先に`lint-staged`の設定をします。stageされているファイルに対してESLintでチェックした後にPrettierでチェックするようにします。

```json:.lintstagedrc
{
  "**/*.{js,jsx,ts,tsx}": [
    "yarn eslint",
    "yarn prettier --check"
  ]
}
```

正しく設定されているか確認するには`js`, `jsx`, `ts`, `tsx`のファイルをstageして以下のコマンドを実行します。

```sh
yarn lint-staged
```

## Huskyのインストールと設定
次にHuskyの設定をします。

```sh
yarn add -D husky
```

`hasky@4`までは`.huskyrc`を作成すれば良かったのですが、`hasky@6`ではかなり変わっているので注意が必要です。

`hasky@6`ではyarnとyarn v2ではインストール方法が違いますが、v2の方法でやってもv1のままで動くので、今後の事を考えてv2の手動の方法でやっておけばよいでしょう。

```sh
yarn husky install
```

```diff_json:package.json
{
+ "private": true,
+ "scripts": {
+   "postinstall": "husky install"
+ },
  ...
}
```

pre-commit hookで`yarn lint-staged`を実行するようにします。

```sh
yarn husky add .husky/pre-commit "yarn lint-staged"
```


# commitlint

## commitlintとは
Gitのcommit messageにもルールを設定できるのが、[commitlint](https://github.com/conventional-changelog/commitlint)です。ライブラリーのcommit messageによくあるこういうやつです。

```
chore: upgrade xxxx
docs: update README.md
```

どういうルールにするかは自分で設定できますが、よく使うルールは用意されています。

- [@commitlint/config-angular](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-angular)
- [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional)
- [@commitlint/config-lerna-scopes](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-lerna-scopes)
- ...etc

[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional)でだいたいの場合は事足りるので今回はこれで行きます。


## commitlintのインストールと設定
```sh
yarn add -D @commitlint/config-conventional @commitlint/cli
```

commitlintの設定ファイルを作成します。

```js:commitlintrc.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
};
```

Huskyでcommit-msg hookで`commitlint`を実行するように設定します。

```sh
yarn husky add .husky/commit-msg 'yarn commitlint --edit "$1"'
```

これで変なcommit messageでcommitしようとすると弾かれることが確認できます。

# EditorConfig

## EditorConfigとは
世の中には[JetBrains](https://www.jetbrains.com/webstorm/)の熱烈なファンがいたり、[Vim](https://www.vim.org)マスターがいたりと、すべての開発者が[VSCode](https://code.visualstudio.com)を使っているわけではありません。環境が違えば色々と違いがあり...

- ファイルの最終行に空行を入れるか
- インデントはタブなのかスペース2個なのか
- 改行コードはLFなのかCRLFなのか

などの個人のIDEの設定によってバラバラだったりします。そんなときに登場するのが[EditorConfig](https://editorconfig.org)です。


## EditorConfigのインストールと設定
導入は簡単で、ますIDEでEditorConfigのプラグインをインストールします。

  - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
  - [EditorConfig for JetBrains](https://plugins.jetbrains.com/plugin/7294-editorconfig)
  - [EditorConfig for Vim](https://github.com/editorconfig/editorconfig-vim)
  - ...etc

他の開発者のために推奨プラグインに記載しておきます。

```diff_json:.vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
+   "editorconfig.editorconfig",
    "esbenp.prettier-vscode"
  ]
}
```

`.editorconfig`に設定を書きます。参考までに[Angularの.editorconfig](https://github.com/angular/angular/blob/master/.editorconfig)を置いておきます。大体はこれで十分です。

```toml:.editorconfig
# https://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
```


# まとめ
かなりガチガチなReact + TypeScript開発環境ができました。これで余計なところに気を取られずに開発やレビューに集中できますね。

開発環境ツールに関しては他の言語に比べてJavaScriptが突出していますね。このくらい凄い開発環境を実現するツールがが他の言語でもあるといいなと思います。