Git hookマネージャー"lefthook"でコミット前にeslintとprettierを通す

# なぜやろうと思ったか？
[フロントエンドクラブ](https://frontendclub.org/ )のdiscordで
「githook使うならlefthookがいいですよ！」という知見をいただき、自分も使いたくなったため。

# lefthookとは？
2019年から開発されているGit hookのマネージャーツール。
[lefthookのリポジトリ](https://github.com/evilmartians/lefthook)

特徴は、
- goで書かれていて高速
- 柔軟に渡すファイルやパラメータをコントロールできる
- 依存性のない単一バイナリなのでどんな環境でも動く

こと。

# husky + lint-stagedとの違い
よく`husky + lint-staged`構成と比較されることが多いので解説します。

## husky
huskyはGit hookを簡単にするためのツールです。
[husky](https://typicode.github.io/husky/)にも、  
> Modern native git hooks made easy

という記述があります。
より包括的な対応を行える`left hook`よりも対応範囲は狭いイメージですね。

## lint-staged
lint-stagedは、lefthookの`stage_fixed`プロパティを一つのライブラリにした感じです。
lintやformatの内容は`husky`だけだとステージングに追加されないため、これを利用してステージングにあげる部分を対応しています。

## husky + lint-stagedと比べてどこがいいの？
### 個人設定を作れる
`lefthook-local.yml`に個人環境での設定をそれぞれ作ることで、不要なツールの実行をスキップできます。
個人の環境に合わせて、対象ファイルを変更したり、パラメータを変更したりももちろん対応可能です。

huskyでは無理やり対応できないこともないですが、推奨されていないですし、管理が煩雑になります。

### monorepoでの設定に向いてる
いわゆるモノレポ構成ですが、
```
├── /client
│   ├── ...
│   └── ...
└── /server
    ├── ...
    └── ...
```
huskyで対応しようとすると、cdでサブディレクトリに移動してからコマンドを叩くような記述が必要になるのと、書くディレクトリ内にhuskyの設定ファイルが必要になります。

lefthookで対応すると、`lefthook.yml`の変更だけでサクッと対応できます。
rootにmonorepoの各サブディレクトリを設定するだけです。
```yml
pre-commit:
  parallel: true
  commands:
    client:
      root: client/
      glob: '*.{ts}'
      run: // ここに実行したいコマンド
    server:
      root: server/
      glob: '*.{ts}'
      run: // ここに実行したいコマンド
```

### 依存関係
husky + lint-stagedでは二つのライブラリをインストールする必要がありますし、node.jsランタイムに載せる必要があります。
例えばどっちかが使えなくなった時はまた構成を考え直す必要がありますし、node.js以外の環境での利用はできません。

lefthookは単一のバイナリで他に依存するライブラリがありませんし、多数のインストール方法を兼ね備えています。
https://github.com/evilmartians/lefthook/blob/master/docs/install.md

### その他
- husky + lint-stagedより早い
    - goで書かれている
    - 並列実行ができる（parallel）


# 実際に使ってみる
ちょうどフォーマット忘れ、lint忘れに悩んでいたので、
対応中のVue製PJに導入して威力を確認してみます。

## 既存の運用
元々は`npm create vue@latest`したら生成される`package.json`のデフォルトのeslint、prettierを一々叩かなくてはいけませんでした。
```json
"script": {
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
}
```

なので、コミットした後にフォーマット忘れに気づいて`npm run format`しただけのコミットが生まれたり、ideのエラー警告に頼って`lint`を全く使っていなかったりしていました。

## lefthookのインストール、設定

```
npm i -D lefthook
```
してライブラリを追加します。

`lefthook`を導入すると、リポジトリ直下に`lefthook.yml`が自動で追加されます。やさしい。
ファイル内にコメントで基本的な書き方も教えてくれています。

今回は`pre-commit`のタイミングでprettierとeslintを通したいため、下記のような設定とします。
```yml
pre-commit:
# parallelにすると並列実行される。
  parallel: true
  commands:
    prettier:
# 対象のファイル
      glob: '*.{vue,tsx,ts,mts,mcs,mjs,cjs,js,json,md,yml,yaml}'
      run: |
# 実行コマンド。npmだと動かなかったのでnpxにした
        npx prettier --write {staged_files}
# 修正内容をステージングするかどうか？
      stage_fixed: true
# マージ、リベースの際はこの処理をスキップしている。
      skip:
        - merge
        - rebase
    eslint:
      glob: '*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts}'
      run: |
        npx eslint --fix {staged_files} --ignore-path .gitignore
      stage_fixed: true
      skip:
        - merge
        - rebase
```

これで適当なファイル作って確認してみましょう。
まずprettierの確認のために、
- ぐちゃぐちゃなインデント
- 不要なセミコロン

を入れてみます。
```vue
<script setup lang="ts">
 const a = 0;
   const b = 1;
</script>

<template>
  <div>
    <p>
       </p>
</div>
</template>
```

これをコミットしたら自動でこうなりました。
ちゃんと修正されていますね。
```vue
<script setup lang="ts">
const a = 0
const b = 1
</script>

<template>
  <div>
    <p></p>
  </div>
</template>
```

あと、ついでにですがlintから警告も出ていました。
宣言されたのに使われてないことをログでちゃんと教えてくれています。
```
┃  eslint ❯ 
SampleView.vue
  2:7  warning  'a' is assigned a value but never used  @typescript-eslint/no-unused-vars
  3:7  warning  'b' is assigned a value but never used  @typescript-eslint/no-unused-vars
✖ 2 problems (0 errors, 2 warnings)
```

次に問題のあるjsを書いてeslintでエラーを出してみます。
constに再代入してエラーが出るか確認してみます。
```vue
<script setup lang="ts">
const a = 0
a = 1
</script>

<template>
  <div>
    <p></p>
  </div>
</template>
```

こちらをコミットしようとすると、エラーになります。
コミット前にエラー終了するため、コミット自体が成功せず、変更はステージングされたままになってくれます。
```vue
┃  eslint ❯ 
SampleView.vue
  3:1  error    'a' is constant                         no-const-assign
  3:1  warning  'a' is assigned a value but never used  @typescript-eslint/no-unused-vars
✖ 2 problems (1 error, 1 warning)
⠙
exit status 1                                      
```


# 参考文献
各公式ドキュメント
https://github.com/evilmartians/lefthook/tree/master/docs
https://typicode.github.io/husky/
https://github.com/lint-staged/lint-staged


設定はほぼこの方の記述ママで、私はVueやnpm環境に合わせて多少書き方を変えてるだけです。助かりました。
正直これが最適解だと思うので変えるなら`stage_fixed`ぐらいかなという感じです。
https://zenn.dev/kimuson/articles/husky_to_lefthook