Husky & lint-staged

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?



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:

  1. Adds a prepare npm life-cycle script (opens in a new tab) to our package.json to install Husky automatically whenever our project is installed
  2. 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/_/
  3. Updates Git's core.hooksPath (opens in a new tab) config to point there.
  4. Creates a pre-commit hook that runs Husky and lint-staged
  5. 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:

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:
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.