Husky & lint-staged
While we've already set up ESLint and Prettier to help us maintain code quality,
we currently have to remember to run them manually in order to benefit from them. To address this, we'll
use Husky (opens in a new tab) to set up a
pre-commit hook (opens in a new tab) to automatically
ensure certain quality checks are run before any code changes are committed. In addition, we'll use
lint-staged
(opens in a new tab) to run these checks
only for the files that are being committed, not the entire project.
Why not just run it on the entire project?
TODO
Installation
Following the integration instructions on the Prettier site (opens in a new tab),
we'll install both Husky and lint-staged
as follows:
npx mrm@2 lint-staged
In addition to adding the husky
and lint-staged
packages as development dependencies in
our package.json
, this command also:
- Adds a
prepare
npm life-cycle script (opens in a new tab) to ourpackage.json
to install Husky automatically whenever our project is installed - Creates a new
.husky
directory in the root of our project that will contain our hooks (as well as Husky's script to execute them at.husky/_/husky.sh
) - Updates Git's
core.hooksPath
(opens in a new tab) config to point there. - Creates a
pre-commit
hook that runs Husky and lint-staged - Sets up an initial lint-staged configuration in
package.json
. This initial configuration sets things up such that our pre-commit hook automatically runs Prettier on all files staged for commit that have a.gitignore
extension. Of course, this isn't what we want, so we'll next update the configuration.
Configuring to run ESLint
Unfortunately, it is not possible to keep our configuration centralized in package.json
because
integrating lint-staged with Next.js (opens in a new tab)
requires configuration that executes JavaScript code.
We'll delete the lint-staged
configuration in package.json
that was added by the mrm
package and add
create a .lintstagedrc.js
file in the root of the project with the following:
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],
};
Now, ESLint will get run automatically when we commit changes. To see this, update src/app/layout.tsx
to have an unused variable:
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
const x = 0;
// rest of layout.tsx
Then stage the change and attempt to commit it:
git add src/app/layout.tsx
git commit -m "Updating layout to have have an unused variable"
You'll see that lint-staged runs ESLint, catches the error, and aborts the commit:
✔ Preparing lint-staged...
⚠ Running tasks for staged files...
❯ .lintstagedrc.js — 1 file
❯ *.{js,jsx,ts,tsx} — 1 file
✖ next lint --fix --file src/app/layout.tsx [FAILED]
↓ Skipped because of errors from tasks.
✔ Reverting to original state because of errors...
✔ Cleaning up temporary files...
✖ next lint --fix --file src/app/layout.tsx:
./src/app/layout.tsx
7:7 Error: 'x' is assigned a value but never used. @typescript-eslint/no-unused-vars
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)
Configuring to run Prettier
Finally, we'll update the lint-staged configuration to also run Prettier. Update module.exports
in
the .lintstagedrc.js
file as follows:
module.exports = {
"*.{js,jsx,ts,tsx}": [buildEslintCommand],
"*": "prettier --write --ignore-unknown --ignore-path .gitignore",
};
The --ignore-unknown
flag (opens in a new tab) is used to ignore file types
that are not supported by Prettier, which is useful if we later have other file types like .sql or .md in the repository.