以前から huskylint-staged を使ってコミット時にリンターを実行していたのですが、huskyのバージョンがv7に上がってから、設定方法が少し変わり、pakcage.jsonだけで完結しなくなったので、改めて設定方法を紹介します。

インストールと初期設定

自力でゼロから設定するのは結構難しいですが、huskylint-stagedをインストールして、サンプル初期設定を作成してくれる便利なコマンドが提供されていますので、これを実行します。

npx mrm@2 lint-staged

実行結果は以下の通りです。

  • package.jsonに以下が追加される
    • huskyの初期化スクリプトを実行する設定が
      "scripts": {
        "prepare": "husky install"
      },

      => npm installなどを実行する際に自動的にhusky installが実行されるようになる

    • huskylint-stagedの依存関係
      "devDependencies": {
        "husky": ">=6",
        "lint-staged": ">=10",
      },
    • lint-stagedのサンプル設定
      "lint-staged": {
        "*.{js,ts}": "eslint --cache --fix",
        "*.{css,scss}": "stylelint --fix",
        "*.{js,ts,css,md}": "prettier --write"
      }

      => GIT でステージング済みのファイルとマッチする場合に、右側のコマンドが実行される設定になっています
      => このままだとリントだけでなく、フォーマットまで実行されるので、個人的にはやりすぎな気はします
      => yarn lint-stagedで直接lint-stagedを実行することもできます

  • huskyの各種ファイルが生成される

    • フォルダ構成
      .husky
      ├── _
      │   ├── .gitignore
      │   └── husky.sh # git管理外
      └── pre-commit # pre-commit hook の定義
    • pre-commitの中身はただのシェル

      #!/bin/sh
      . "$(dirname "$0")/_/husky.sh"
      
      npx lint-staged
  • .git/configの設定が変更される
    [core]
    hooksPath = .husky

    => Git Hook のシェルが格納されるフォルダが先ほど作成された.huskyフォルダに変更されています

上記設定により、コミット時にlint-stagedの定義に従って、リント+フォーマットが自動実行されるようになりました。

lint-stagedを直接実行

huskyを使わず、lint-stagedを直接実行してみます。

README.mdをステージングして試してみました。

git add README.md
yarn lint-staged
yarn run v1.22.17
$ /Users/xxx/xxx/xxx/node_modules/.bin/lint-staged

# 実行中
✔ Preparing...
❯ Running tasks...
  ↓ No staged files match *.{js,ts} [SKIPPED]
  ↓ No staged files match *.{css,scss} [SKIPPED]
  ❯ Running tasks for *.{js,ts,scss,md}
    ⠙ prettier --write
◼ Applying modifications...
◼ Cleaning up...

# 実行後
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
✨  Done in 1.99s.

lint-stagedの三つの定義のうち、*.{js,ts}*.{css,scss}にはマッチしなかったのでスキップされていますが、
*.{js,ts,scss,md}にはマッチしているので、prettier --writeが実行されていることがわかります。

husky経由でlint-stagedを実行

次に、README.mdをそのままコミットして、lint-stagedが実行されるかを試してみました。
全くlint-stagedを実行した時と全く同じログが表示されて、lint-stagedが実行され、今回は問題がひとつもなかったのでそのままコミット完了しました。

git add README.md
git commit -m "change readme"

# 実行中
✔ Preparing...
❯ Running tasks...
  ↓ No staged files match *.{js,ts} [SKIPPED]
  ↓ No staged files match *.{css,scss} [SKIPPED]
  ❯ Running tasks for *.{js,ts,scss,md}
    ⠙ prettier --write
◼ Applying modifications...
◼ Cleaning up...

# 実行後
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
[master 43032fd] change readme
 1 file changed, 9 insertions(+), 2 deletions(-)

次に、リントエラーが発生するケースで試してみます。
src/jest/foo.tsに以下のように、array-callback-return エラーが発生するケースを実装しました。

['a', 'b', 'c'].reduce((memo, item, index) => {
  memo[item] = index;
}, {});

これをステージングしてコミットすると、以下のようにリントエラーが発生して、コミットが失敗しました。

git add src/jest/foo.ts
git commit -m "add array-callback-return case"
✔ Preparing...
⚠ Running tasks...
  ❯ Running tasks for *.{js,ts}
    ✖ yarn eslint --cache --fix [FAILED]
  ↓ No staged files match *.{css,scss} [SKIPPED]
  ✔ Running tasks for *.{js,ts,scss,md}
↓ Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up...

✖ eslint --cache --fix:
error Command failed with exit code 1.
yarn run v1.22.17
$ eslint --cache --fix /Users/xxx/xxx/xxx/src/jest/foo.ts

/Users/xxx/xxx/xxx/src/jest/foo.ts
  3:44  error  Array.prototype.reduce() expects a return value from arrow function  array-callback-return

✖ 1 problem (1 error, 0 warnings)

info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky - pre-commit hook exited with code 1 (error)

ここまでの検証で、設定としてはうまく動いていることが確認できました。
あとは、実運用に即したかたちに設定を調整していきたいと思います。

最終的な設定

リントやフォーマットは、CI で実行したいケースもあると思いますので、まずは yarn スクリプトとして定義したいと思います。
自分の場合は、package.jsonに以下のような形で定義しました。

  "scripts": {
    // リント
    "lint": "yarn run lint:prettier && yarn run lint:es && yarn run lint:style ",
    "lint:prettier": "prettier --check --loglevel=warn '**/*.{js,ts,scss,md}'",
    "lint:es": "eslint --max-warnings=0 '**/*.{js,ts}'",
    "lint:style": "stylelint '**/*.{css,scss}'",
    // フォーマット
    "format": "yarn run format:prettier && yarn run format:es && yarn run format:style",
    "format:prettier": "prettier --check --write --loglevel=warn '**/*.{js,ts,scss,md}'",
    "format:es": "eslint --max-warnings=0 --fix '**/*.{js,ts}'",
    "format:style": "stylelint --fix '**/*.{css,scss}'"
  },

=> prettier --checkおよびeslint --max-warnings=0オプションにより、ワーニングがあればexit code0以外になるように設定してます。

その上で、lint-stagedからは、yarn スクリプトを呼び出す形で設定しました。

  "lint-staged": {
    "*.{js,ts}": "yarn lint:es",
    "*.{css,scss}": "yarn lint:style",
    "*.{js,ts,scss,md}": "yarn lint:prettier"
  }

その他

SourceTreeでコミットする際の注意点

SourceTree などでコミットする場合、以下のようなエラーが発生することがあります。

.husky/pre-commit: line 4: npx: command not found

こちらは、ISSUE が報告されており、対応方法が公式ページに紹介されています。

原因はSourceTree から実行される場合、ログインシェルではないのでnpxにパスが通ってないためです。
~/.huskyrcを作成し、この中でnpxにパスを通してあげればOKです。自分の場合はnvmnodeをインストールしているので、公式に紹介されている通りに設定すれば大丈夫でした。

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

list-stagedの条件マッチング

lint-stagedのマッチングにはmicromatchが使われているので、書き方がわからない時はここを参照しましょう。

例えば、src__tests__フォルダの.js.tsファイルだけリントを実行したい場合は、以下のような設定になります。

"lint-staged": {
  "(src|__tests__)/**/*.(js|ts)": "yarn lint:es"
}

なぜ v7から設定方法が変わったか

huskyの作者がこちらで解説してくれてます。
https://blog.typicode.com/husky-git-hooks-javascript-config/

要約すると、以下のような感じだと思います。(パーっと読んで意訳してるので間違ってるかも)

  • 元々は全てのgit-hook.huskyrc.jsを呼び出して処理していたが、これだと不要なケースでもnodeが起動されていた。(昔の Git だと必要な場合だけ起動することはできなかった)
  • Git のバージョンが2.9core.hooksPathが導入され、.git/hooks/以外のフォルダを指定できるようになった
  • /.husky配下に必要なgit-hook用のシェル(例えばpre-commit)を作成することで、必要なケースだけシェルを実行できるようになった

ただし、内部的には効率的になったかもしれませんが、専用フォルダを作ってそこにgit-hook用のファイルを配置することには反対意見もあったようです。やはり、package.jsonだけで設定が完成する方が嬉しいですしね。