Last week I talked about ESLint and its usefulness for keeping projects consistent amongst multiple contributors. If you haven't read that post I recommend doing so before diving into this one.

Today, we're going to focus on running ESLint automatically to ensure that the main branch of your project always follows your specific ruleset.

Lint-staged

The first tool to talk about is lint-staged. Lint-staged is configured in your package.json file.

{
"lint-staged": {
"*.js": "eslint --fix"
}
}

As seen in the above example, you can use a glob pattern to tell lint-staged which files to run against. Additionally, you can give lint-staged a command to execute against those files. In many cases, you'll want more than one command, which lint-staged supports. In this case, you'll run ESLint and prettier.

{
"lint-staged": {
"*.js": ["eslint", "prettier --write"]
}
}

So how does lint-staged work? It's specifically designed to work on "staged" files, thus the name. This means files you've changed or created but haven't yet committed to your project. Working on staged files limits the number of files you need to lint at any given time and makes the workflow faster. The commands you configure will run "pre-commit". As you're attempting to commit files to your project you'll see ESLint run in your terminal. Once it's done you may have successfully committed or find yourself with linting errors you need to fix before you're able to commit the code.

However, what you may not realize, is that lint-staged is not the only tool working under the hood. Lint-staged is designed to work with another tool called husky.

Husky

You may have come across husky before without noticing. For many years it was configured via a few lines of code in your package.json file. Something like this.

"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},

However, the latest version of husky, v6, has changed this approach. Now, husky uses distinct bash files with filenames that match the workflow step they correspond to, e.g. "pre-commit". Luckily you don't have to set this up yourself and husky has a nice CLI command to do it for you.

npx husky-init && npm install
npx husky add .husky/pre-commit "npm test"

The first line of the command is a one-time initialization script that ensures all your coworkers will have husky installed on their machine before they try to commit files.

The second line creates the pre-commit file inside the .husky directory. If you look at the file you'll notice it's running a husky.sh script prior to whatever commands you initialized it with. This can technically be removed, but I'd recommend keeping it. The script allows for a few things, including the use of a --no-verify flag that bypasses the checks.

Once you've initialized the directory and associated file you can add whatever commands you want to it. In my case, I replaced npm test with npx lint-staged.

Pre-push

The pre-commit workflow is more or less the husky happy path. But what if your project doesn't want to lint on commit and would prefer to lint when a developer attempts to push their changes to a branch?

While it's tempting to create a .husky/pre-push file and run lint-staged, it won't work. The pre-push husky workflow is correct, but running lint-staged at that point will turn up 0 matching files. This makes sense, though it certainly messed me up for a bit, because committed files are no longer staged. Instead, you have a couple of options.

  1. Run ESLint against all the files: eslint '*.js'
  2. Diff against main: eslint --no-error-on-unmatched-pattern $(git diff main... --name-only --- '*.js')

Note that this is one example of a diff command and there are numerous considerations depending on your project.

Next steps and CI

Running ESLint, or prettier, or even tests as part of your git workflow is important because it helps you fail fast. However, it's not a replacement for CI checks. Typically, you'll want to run these commands in both environments to ensure nothing slips through.

But altogether these tools help ensure a cleaner, more consistent production codebase. Long term, that's a big win for any project.