なぜやろうと思ったか?
フロントエンドクラブのdiscordで
「githook使うならlefthookがいいですよ!」という知見をいただき、自分も使いたくなったため。
lefthookとは?
2019年から開発されているGit hookのマネージャーツール。
lefthookのリポジトリ
特徴は、
- goで書かれていて高速
- 柔軟に渡すファイルやパラメータをコントロールできる
- 依存性のない単一バイナリなのでどんな環境でも動く
こと。
husky + lint-stagedとの違い
よくhusky + lint-staged
構成と比較されることが多いので解説します。
husky
huskyはGit hookを簡単にするためのツールです。
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の各サブディレクトリを設定するだけです。
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を一々叩かなくてはいけませんでした。
"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を通したいため、下記のような設定とします。
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の確認のために、
- ぐちゃぐちゃなインデント
- 不要なセミコロン
を入れてみます。
<script setup lang="ts">
const a = 0;
const b = 1;
</script>
<template>
<div>
<p>
</p>
</div>
</template>
これをコミットしたら自動でこうなりました。
ちゃんと修正されていますね。
<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に再代入してエラーが出るか確認してみます。
<script setup lang="ts">
const a = 0
a = 1
</script>
<template>
<div>
<p></p>
</div>
</template>
こちらをコミットしようとすると、エラーになります。
コミット前にエラー終了するため、コミット自体が成功せず、変更はステージングされたままになってくれます。
┃ 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