かなで技術日誌

プログラミングやエンジニアリング周りについて

主なアウトプットはScrapboxObsidianにまとめてます。

monorepoでのhusky+lint-stagedとlefthookと比較

monorepo環境でのgit hooksの管理でどう設定するのがいいのか、まとまった資料はなさそうなので書き記します。

以降はhuskyとlint-staged、lefthookについては知っている前提です。 知らない場合はざっくり説明すると

husky → git hooksを管理するライブラリ

lint-staged → gitのstagedなファイルに対して任意のコマンドを実行できるライブラリ

lefthook → go製のgit hooks管理ライブラリ

github.com

github.com

github.com

個人的には後述する理由からlefthook推奨です。

前提

バージョンは以下のとおりです。

husky: 8.0.1
lint-staged: 13.0.3
lefthook: 1.1.1

また、今回はgoとnext.jsのアプリケーションが横並びである状態を想定しています。

$ tree -L 1
.
├── go
└── next

目標は、commit時に各アプリケーションのファイルごとに特定のコマンドを実行するようにします。 jsやts、tsxファイルなどに対してはeslintやprettier、goファイルに対してはgo fmtやgolangcil-intを実行します。

husky+lint-stagedでの設定

まずroot directoryで以下コマンドを実行します。

# package.jsonを作成
yarn init
# 依存ライブラリをinstall
yarn add -D husky lint-staged
# package.jsonにhuskyの初期化コマンド追加
npm set-script prepare "husky install"
# huskyの初期化
yarn prepare
# git hooksにpre-commit時のコマンド
yarn husky add .husky/pre-commit "yarn lint-staged"

これでcommit時にlint-stagedが実行されます。

ではアプリケーションごとに実行アプリケーションごとに実行させるには各アプリケーションのディレクトリごとに.lintstagedrcを追加します。

# root/next/.lintstagedrc.js
const path = require('path')

module.exports = {
  '*.{js,ts,tsx}': (absolutePaths) => {
    const cwd = process.cwd()
    const relativePaths = absolutePaths.map((file) => path.relative(`${cwd}/next`, file)).join(' ')
    return [`eslint ${relativePaths}`, `prettier --write ${relativePaths}`]
  },
}
# root/go/.lintstagedrc.js
module.exports = {
    "*.go": "go fmt"
}

これで各アプリケーションごとにcommit時に指定したコマンドが実行されます。 pushとコマンドを分けたい場合は別名でconfigファイルを作成して-cオプションで指定すれば良いです。

lefthookでの設定

lefthookはnpmのpackageはdeprecatedになっているため、macであればhomebrew経由やgoバイナリをdownloadするのが良いです。

www.npmjs.com

lefthookをdownloadしたら以下コマンドを実行してlefthook.ymlを作成します。

lefthook install

そしてpre-commitに実行したいコマンドを追加します。

rootにアプリケーションのディレクトリを指定することで、そのディレクトリからコマンド実行とファイルパスの解決をしてくれます。 {staged_files}でstagedなファイルの全件を半角スペース区切りで渡してくれます。

pre-commit:
  parallel: true
  commands:
    eslint:
      root: 'next/'
      glob: '**/*.{js,ts,jsx,tsx}'
      run: yarn eslint {staged_files}
    gofmt:
      root: 'go/'
      glob: '**/*.go'
      run: go fmt {staged_files}

そしてpre-commit設定を追加します。 lefthook add pre-commit

これでcommit時に各ファイルに対して任意のコマンドを実行してくれます。

どちらが良いか

lefthookのwikiにも記載がありますが、以下の理由から自分はlefthookを推奨します。

  • huskyはNodejsランタイムが必要だがlefthookはシングルバイナリのみで動く
  • huskyは並列実行できないがlefthookは並列実行可能
  • lefthookは一つの設定に集約可能
  • lefthookはgit hooks管理ライブラリではなくタスクランナーの側面もあるので適用範囲が広い

github.com

4番目は私見ですが、commitを契機とするタスク実行をしてくれるライブラリなのでhusky+lint-stagedよりも適用範囲は広いと思います。 任意のscriptを実行でき、dockerにも対応しているのでそもそもできることの幅がhusky+lint-stagedとはだいぶ違うという認識です。

終わりに

どちらでも基本的なmonorepoでのpre-commitの設定はできることがわかりました。

husky+lint-stagedがデファクトスタンダードですが、まだ使ったことが無い方はlefthookの導入も検討してみてください。