1. 記事一覧 >
  2. ブログ記事
category logo

Next.jsでhuskyを使ったcommit時lintエラー検出とGitHub Desktop対応

(更新) (公開)

はじめに

Next.js で husky と lint-staged を使って、commit 時に ESLint エラー検出 & GitHub Desktop(Windows 10 x64)でも動作するようにするようにしました。
今回、その手順と仕組みについて、書いていきます。


「husky と lint-staged は知ってる。なんか、GitHub Desktop ってあったから、来た。」という方は、こちらに飛んでください。

GitHub Desktop


【検証環境のバージョン】

Windows 10 x64

GitHub Desktop 3.2.0 (x64)

node 16.16.0

npm 8.11.0

husky 8.0.0

lint-staged 13.2.2


概要説明

まず、目標は、git commit と同時に、ESLint(正確には、next lint)によるコードチェックが動作して、まずいところがある場合、commit できなくすることです。


そして、それがどのように実現されるかを先に整理しますと、以下の通りです。


なんか、my-app っていうリポジトリを clone して開発しろと言われたので、clone して、とりあえず、npm install する。

> cd my-app

> npm install


npm パッケージインストール前に
"prepare": "husky install"
が発動

Git フックが設定される、すなわち、
.git/config に
hooksPath = .husky
が設定される。

npm パッケージがインストールされる

ごにょごにょ開発する

> git add .
> git commit -m "ごにょごにょ開発した"


Git フックにより、
.husky/pre-commit
が起動される

.husky/pre-commit
に書かれている
npx lint-staged
が起動される

lint-staged は、git にステージングされたファイルに対して、.lintstagedrc.js に書かれたルールに従ってコマンドを実行する

.lintstagedrc.js に
next lint --fix --file [ステージングされたファイル] --file [ステージングされたファイル] ...
を実行するように書いてあるため、実行される

next lint の実行結果に lint エラーがある場合、git commit を中止。(エラーが無い場合、普通に commit 完了)

【 ESLint 】

ESLint は、JavaScript, TypeScript の静的コード解析ツールの一つであり、コード品質を向上するために使用されます。

ESLint は、コードの潜在的な問題を検出し、推奨されるプラクティスに従うようにコードを修正することを支援します。

【 husky 】

husky は、Git フックを使用して、Git コマンドを実行する前に特定のスクリプトを実行することができるツールです。

例えば、Git コミットをする前に、コードのフォーマットや Lint チェックを実行することができます。

【 lint-staged 】

lint-staged は、Git のステージングエリアにあるファイルに対して何らかのコマンドを実行するツールです。

lint-staged は、特定のファイル形式に対して、特定のコマンドを実行するように設定することができます。

これにより、例えば、変更された JavaScript ファイルに対して ESLint を実行することができます。


Next.js プロジェクト作成

C:\dev に my-app という Next.js プロジェクトを作成するものとします。
このとき、No / Yes は、全てエンター(デフォルトの選択)で ESLint を有効にします。

> npx create-next-app@latest
Need to install the following packages:
  create-next-app@latest
Ok to proceed? (y) y
√ What is your project named? ... my-app
√ Would you like to use TypeScript with this project? ... Yes
√ Would you like to use ESLint with this project? ... Yes
√ Would you like to use Tailwind CSS with this project? ... Yes
√ Would you like to use `src/` directory with this project? ... No
√ Use App Router (recommended)? ... Yes
√ Would you like to customize the default import alias? ... No
Creating a new Next.js app in c:\dev\my-app.

my-app には、create-next-app により、git リポジトリが作成されて、一回目のコミットが行われた状態になっています。

package.json を見ると、各バージョンは、以下でした。

"next": "13.4.1",

"postcss": "8.4.23",

"react": "18.2.0",

"react-dom": "18.2.0",

"tailwindcss": "3.3.2",

"typescript": "5.0.4"


husky, lint-stated 導入

husky と lint-stated を導入します。

> cd my-app
> npm install --save-dev husky lint-staged

【 --save-dev 】

開発時のみに必要なパッケージですので、--save-dev です。

package.json に

"devDependencies": {

"husky": "^8.0.0",

"lint-staged": "^13.2.2"

}

のように書き込まれます。


husky 初期化

husky-init を実行して、husky 環境を初期化します。

> npx husky-init

これにより、起きたことは、以下です。


●.git/config にGitフックパス設定追加

.git/config
	hooksPath = .husky

git config 確認結果

> git config --local core.hooksPath
.husky

●package.json に追加

package.json
  "scripts": {
    ・・・,
    "prepare": "husky install"

●.husky および、.husky/pre-commit 新規作成

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test

package.json の "prepare": "husky install" の記述により、別の環境にプロジェクトを持ってきても npm install を行って、npm パッケージがインストールされる前に husky がインストールされ、.git/config に

hooksPath = .husky が設定されます。そのため、.git/config をリポジトリに含めなくても問題ありません。

.husky はリポジトリに含める必要があります。


.husky/pre-commit 修正

今回、Git フックの pre-commit(すなわち、git commit 実行直後、コミットされる直前)で、
npm test
ではなく、
npx lint-staged
を実行させたいため、

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

と修正します。

#!/usr/bin/env sh

/usr/bin/env は、環境変数 PATH から指定されたプログラムを探し出し、そのプログラムを実行するためのコマンドです。この場合、sh が指定されているため、sh が探し出されて、実行されます。

. "$(dirname -- "$0")/\_/husky.sh"

. は、source と同じ意味で、同一プロセス内で、husky.sh を実行します。これが無いと、husky.sh で環境変数をセットしたとしても次の行の処理では、失われます。

dirname は、パスからディレクトリを返します。例:dirname a/b/ca/b

-- は、オプションの終わりを示すシェルの特別な構文です。-- の後に続く引数は、オプションとして解釈されなくなります。

"$0" は、自分(pre-commit)のファイル名を含むフルパスです。

husky.sh は、中で .huskyrc を読み込んでいます。

【 .huskyrc 】

.huskyrc は、例えば、

PATH="/usr/local/bin:$PATH"

と書くと、PATH 環境変数を書き換えます。

今回使用しません。


.lintstagedrc.js 作成

Next.js の場合、next lint により、lint を行います。 また、next lint は、
next lint --fix --file [ステージングされたファイル] --file [ステージングされたファイル]...
と実行したいため、そのようなコマンドを指定する設定ファイルを置きます。


プロジェクト直下に以下のファイルを作成します。(今回の場合、my-app/.lintstagedrc.js です。)

.lintstagedrc.js
const path = require('path');

const buildEslintCommand = (filenames) =>
  `next lint --fix --file ${filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(' --file ')}`;

module.exports = {
  '*.{js,jsx,ts,tsx}': [buildEslintCommand],
};

【 .lintstagedrc.js 】

.lintstagedrc.js は、lint-staged の設定ファイルの一種であり、JavaScript で書かれたファイルです。このファイルは、lint-staged が Git のステージングエリアにあるファイルに対して実行するコマンドを定義するために使用されます。


.lintstagedrc.js ファイルは、以下のような構文で実行するコマンドを記述できます。

.lintstagedrc.js
module.exports = {
  "<pattern>": "<command>",
  "<pattern>": "<command>",
  // ...
}

<pattern> は、lint-staged が実行対象と認識するファイルのパターンです。
例えば、src/**/*.ts とすると、src/xxx.tssrc/a/b/c/xxx.ts がマッチします。


今回設定する JavaScript コードの意味は、以下のコメントを参照してください。

.lintstagedrc.js
//パスに関わる処理をしたいため、pathモジュールを読み込み
const path = require('path');

//buildEslintCommandは、lint-stagedに実行させたいコマンド
//を生成する関数(アロー関数式)
//filenamesは、引数で配列で渡される。
const buildEslintCommand = (filenames) =>
//`文字列${式}`のテンプレートリテラル構文
//fは、filenamesの1要素(1ファイルのパス)
//process.cwd()は、カレントディレクトリ
//path.relative(process.cwd(), f)でfの相対パスを取得
//.join(' --file ')は、--file 2番目のファイル --file 3番目のファイル
//のように --file で連結する意味
//--fixでESLintの自動修正機能が有効
  `next lint --fix --file ${filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(' --file ')}`;

//設定したいことの本体
module.exports = {
  //*.{js,jsx,ts,tsx}にマッチするファイルに対して、buildEslintCommand関数を実行
  '*.{js,jsx,ts,tsx}': [buildEslintCommand],
  //右側のコマンドは、[複数コマンド]とできるけど、今回は、buildEslintCommandのみ。
};

commit テスト

エラーになるように対象ファイルをデタラメに書き換えます。
今回の場合、app/pages.tsx をデタラメにします。

pages.tsxをエラーになるように書き換え


Git Bash で、git add git commit すると、エラーになります。

$ git add .
$ git commit -m "test"
[STARTED] Preparing lint-staged...
[SUCCESS] Preparing lint-staged...
[STARTED] Running tasks for staged files...
[STARTED] .lintstagedrc.js — 1 file
[STARTED] *.{js,jsx,ts,tsx} — 1 file
[STARTED] next lint --fix --file app\page.tsx
[FAILED] next lint --fix --file app\page.tsx [FAILED]
[FAILED] next lint --fix --file app\page.tsx [FAILED]
[FAILED] next lint --fix --file app\page.tsx [FAILED]
[STARTED] Applying modifications from tasks...
[SKIPPED] Skipped because of errors from tasks.
[STARTED] Reverting to original state because of errors...
[SUCCESS] Reverting to original state because of errors...
[STARTED] Cleaning up temporary files...
[SUCCESS] Cleaning up temporary files...

× next lint --fix --file app\page.tsx:
- warn Detected next.config.js, no exported configuration found. https://nextjs.org/docs/messages/empty-configuration

./app/page.tsx
Error: Parsing error: '}' expected.

info  - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules
husky - pre-commit hook exited with code 1 (error)

このとき、commit されません。
エラーが無くなったら commit されます。

$ git reset HEAD
$ git add  .
$ git commit -m "test"
[STARTED] Preparing lint-staged...
[SUCCESS] Preparing lint-staged...
[STARTED] Running tasks for staged files...
[STARTED] .lintstagedrc.js — 1 file
[STARTED] *.{js,jsx,ts,tsx} — 1 file
[STARTED] next lint --fix --file app\page.tsx
[SUCCESS] next lint --fix --file app\page.tsx
[SUCCESS] *.{js,jsx,ts,tsx} — 1 file
[SUCCESS] .lintstagedrc.js — 1 file
[SUCCESS] Running tasks for staged files...
[STARTED] Applying modifications from tasks...
[SUCCESS] Applying modifications from tasks...
[STARTED] Cleaning up temporary files...
[SUCCESS] Cleaning up temporary files...
[main fbf1d1c] test
 1 file changed, 1 insertion(+)

OK!


GitHub Desktop

通常は、ここまでで目標達成なのですが、困ったことに、GitHub Desktop では、動きません。
正常にチェックできないどころか、コミットができなくなります。


検証環境では、常に、以下のエラーでした。
husky - pre-commit hook exited with code 1 (error)

husky - pre-commit hook exited with code 1 (error)

コミットできるはずのコードでも、エラーになるコードでも同じエラーになります。


これは、.husky/pre-commit から呼び出される npx/usr/bin/env bash のシェルスクリプトになっていて、GitHub Desktop から呼び出される MINGW64 環境に /usr/bin/bash が存在しないからのようです。

pre-commit に以下のデバッグコードを入れると、分かります。

echo "$(ls /usr/bin/)" > debug.log

GitHub Desktop、Git Bash でコミット 図解


結果、以下の2つの方法のいずれかにて、lint-stated の起動に成功しました。
1.npx lint-staged. npx lint-staged もしくは、source npx lint-staged とする。
2.npx lint-stagednpx.cmd lint-staged とする。

【 npx 】

引数のコマンド(npm パッケージ)の依存関係を都度メモリ上で解決して、実行するツールです。Linux の場合、npx は、シェルスクリプトではなく、node スクリプトになっています。

【 npx.cmd 】

C:\Program Files\nodejs\npx.cmd にあるツールです。Windows のバッチですが、MINGW64 環境から呼び出せるため、問題無く機能します。

ただし、Linux 環境では存在しません。


.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

if [ "$(expr substr "$(uname -s)" 1 5)" = "MINGW" ]; then
. npx lint-staged
else
npx lint-staged
fi

もしくは、

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

if [ "$(expr substr "$(uname -s)" 1 5)" = "MINGW" ]; then
npx.cmd lint-staged
else
npx lint-staged
fi

と修正します。


GitHub Desktop で husky, lint-staged 成功


OK!


(おまけ)React の場合

以下にまとめて React の場合の手順を示します。
React は、next lint が無いため、eslint を直接起動になります。

作成するプロジェクトは、create-react-app で作成した TypeScript のアプリとします。

Next.js の時のように JavaScript でごにょごにょする必要はありませんので、.lintstagedrc.js は、不要で、package.json にコマンドを指定します。(注意:.lintstagedrc.js を使っても良いです。)


プロジェクトを作成します。

> npx create-react-app my-app --template typescript
> cd my-app

ESLint に対応します。

> npx eslint --init

各質問を以下のように回答します。

How would you like to use ESLint?
To check syntax, find problems, and enforce code style


What type of modules does your project use?
JavaScript modules (import/export)


Which framework does your project use?
React


Does your project use TypeScript?
Yes


Where does your code run?
Browser


How would you like to define a style for your project?
Use a popular style guide


What format do you want your config file to be in?
JavaScript


Would you like to install them now?
Yes


Which package manager do you want to use?
npm


ESLint をテストします。

> npx eslint src --ext .tsx --ext .ts
Warning: React version not specified in eslint-plugin-react settings.

Warning になったため、.eslintrc.js に以下を追記します。

.eslintrc.js
    "settings": {
        "react": {
            "version": "detect"
        }
    },
> npx eslint src --ext .tsx --ext .ts

husky と lint-stated を導入します。

> npm install --save-dev husky lint-staged

husky 環境を初期化します。

> npx husky-init

pre-commit を修正します。

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

と修正します。(注意:GitHub Desktop のことは考えないものとします。)


package.json にコマンドを指定します。

package.json
  "lint-staged": {
    "src/**/*.{js,jsx,ts,tsx}": "eslint --cache --fix"
  },

commit テストを実施します。
エラーになるように対象ファイルをデタラメに書き換えます。
今回の場合、src/index.tsx をデタラメにします。

index.tsxをエラーになるように書き換え


Git Bash で、git add git commit すると、エラーになります。

$ git add .
$ git commit -m "test"
[STARTED] Preparing lint-staged...
[SUCCESS] Preparing lint-staged...
[STARTED] Hiding unstaged changes to partially staged files...
[SUCCESS] Hiding unstaged changes to partially staged files...
[STARTED] Running tasks for staged files...
[STARTED] package.json — 5 files
[STARTED] src/**/*.{js,jsx,ts,tsx} — 1 file
[STARTED] eslint --cache --fix
[FAILED] eslint --cache --fix [FAILED]
[FAILED] eslint --cache --fix [FAILED]
[FAILED] eslint --cache --fix [FAILED]
[STARTED] Applying modifications from tasks...
[SKIPPED] Skipped because of errors from tasks.
[STARTED] Restoring unstaged changes to partially staged files...
[SKIPPED] Skipped because of errors from tasks.
[STARTED] Reverting to original state because of errors...
[SUCCESS] Reverting to original state because of errors...
[STARTED] Cleaning up temporary files...
[SUCCESS] Cleaning up temporary files...

× eslint --cache --fix:

C:\dev\my-app\src\index.tsx
  8:0  error  Parsing error: Argument expression expected

✖ 1 problem (1 error, 0 warnings)

husky - pre-commit hook exited with code 1 (error)

ヨシ!


loading...