Next.jsでcommit時にaddされているファイルにのみESLintとPrettierを実行する

# 何をするのか
タイトル通りで、Next.jsで`commit`時にステージング（`git add`）されているファイルにのみ`next lint --fix`と`prettier --write`を実行します。
ググれば日本語の記事でもやり方は沢山出てくるのですが、意外と落とし穴があったので今回はその詳細を書いていこうと思います。

**※大前提として初めからeslintが導入されている11以降のバージョンを使用しましょう。**

# よくあるやり方
`husky`と`lint-staged`を入れて`.husky/_/pre-commit`と`package.json`で以下のように設定して、`commit`する。

```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged
```
```json
{
 "scripts": {
   "dev": "next dev",
   "build": "next build",
   "start": "next start",
   "lint": "next lint --dir src",
   "lint:fix": "next lint --fix --dir src",
   "format": "prettier --write ./src/**/*",
   "prepare": "husky install"
 },
 "dependencies": {
 }
 "devDependencies": {
 },
 "lint-staged": {
   "*.(js|ts)?(x)": [
     "yarn format",
     "yarn lint:fix"
   ]
 }
}
```

これの何が問題かというと、これだと該当する全てのファイルにリントとフォーマットが効いてしまいます。

今回はステージングされているファイルにのみ反映したいのです。
`lint-staged`という名前ですが、そこら辺をよしなにやってくれたりはしないようです。

# やり方
「よくあるやり方」の問題点ですが、`.lintstagedrc.js`を使ってコマンドを動的に生成する事で解決します。

ただその前にそこまでの一通りの手順を記載いたします。
また今回は`JavaScript Standard Style`を適用したいと思います。（ここはお好みで何でも良いです）

### パッケージの導入
**※全部一気にやってもいいですが、今回はわかりやすく分けてインストールしています。**

まずNext.jsのプロジェクトが作成し終わったら、そのディレクトリで、

```
yarn add --dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node
```
を実行して、`JavaScript Standard Style`に必要なパッケージをインストールします。

次にコードフォーマッターの`prettier`と、`eslint`と`prettier`のルールの競合を回避してくれる`eslint-config-prettier`をインストールします。

```
yarn add --dev prettier eslint-config-prettier
```

そして最後に`commit`や`push`の前に特定の処理を挟んでくれる`husky`と、ステージングされているファイルの処理を行うための`lint-staged`をインストールします。

```
yarn add --dev husky lint-staged
```

### パッケージの設定
次に`eslint`と`husky`の設定を行います。

まず`eslintrc.json`を以下のように編集します。

```json
{
  "extends": ["next/core-web-vitals", "standard", "prettier"]
}
```
この`standard`と`prettier`はそれぞれ`JavaScript Standard Style`と`eslint-config-prettier`の設定です。
またこの時の注意点ですがフォーマッターのルールは`next`より後に書きます。

次に`husky`の設定を行います。
以下のコマンドを実行して`husky`を初期化します。

```
npx husky-init && yarn
```
↑が実行されると、自動でルートディレクトリに`.husky/_/pre-commit`が作成されるので、そのファイルを以下のように書き換えます。

```
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged
```

最後に`lint-staged`の設定を行います。
ルートディレクトリに`.lintstagedrc.js`を作成して、以下の内容を書き込みます。

```javascript
module.exports = {
  '*.(js|ts)?(x)': filenames => {
    const rawFilePaths = filenames.map(file => {
      const cwd = process.cwd()
      const isMatchingPathFormat = file.includes(process.cwd())
      const correctCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/')
      const rawFilePath = file.replace(correctCwd, '.')
      return rawFilePath
    })

    return [
      `prettier --write ${rawFilePaths.join(' ')}`,
      `next lint --fix --file ${rawFilePaths.join(' --file ')}`
    ]
  }
}
```

以上でインストール＆設定は完了です。
これで`git commit`すると、自動でステージングされているファイルにリントとフォーマットが効くようになりました。

# .lintstagedrc.jsについて
まず`.lintstagedrc.js`が何をやっているのかですが、`return`で返される配列のコマンドを`commit`の前に実行してくれます。
つまり単純に

```javascript
module.exports = {
  '*.(js|ts)?(x)': filenames => [
    'yarn format',
    'yarn lint:fix'
  ]
}
```
のように書くと、ステージングされたファイルに`js`/`jsx`/`ts`/`tsx`が存在すると、`yarn format`と`yarn lint`が実行されます。

ただしこれでは「よくあるやり方」で書いたように全てのファイルにリントとフォーマットが反映されてしまいます。
そのためステージングされているファイルにのみ反映するコマンドを引数`filenames`を使用して生成する必要があります。

### コード解説
この`filenames`ですが、こいつはステージングされているファイルの絶対パスを列挙した配列であり、中身は以下のようになっています。

```javascript
[
  'C:/Users/username/Desktop/my_project/.lintstagedrc.js',
  'C:/Users/username/Desktop/my_project/next.config.js',
  'C:/Users/username/Desktop/my_project/src/pages/members/index.tsx'
]
```
この配列を`process.cwd`（ここでは`C:/Users/username/Desktop/my_project`が出力される）を使ってプロジェクトルートまでを`.`に置換します。

```javascript
const rawFilePaths = filenames.map(file => {
  const cwd = process.cwd()
  const isMatchingPathFormat = file.includes(process.cwd())
  const correctCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/')
  const rawFilePath = file.replace(correctCwd, '.')
  return rawFilePath
})
```
```javascript
[
  './.lintstagedrc.js',
  './next.config.js',
  './src/pages/members/index.tsx'
]
```
あとはこの配列を使ってお好みのコマンドが生成できます。
例えば上記の`prettier --write ${rawFilePaths.join(' ')}`であれば、`prettier --write ./.lintstagedrc.js ./next.config.js ./src/pages/members/index.tsx`となります。

### 注意点
※`isMatchingPathFormat`と`correctCwd`ですが、windowsの場合`process.cwd()`の出力が`C:\Users\username\Desktop\my_project`のようにスラッシュがバックスラッシュになってしまう事があるのでそれの調整をしています。