Catalin Ciubotaru avatar

How to automate your process with git hooks

How to use lint-staged and husky to automate your linting and testing between commits

The Problem

We all do linting and testing and pretty-ing of our code. This is something that, simply put, makes our lives easier, makes the PRs more readable, and makes all the code in a codebase consistent. Now, these are usually run in some sort of CI pipeline, and occasionally, we forget to run them locally before pushing. As a result, we think we’re done with writing the feature, only to discover that our GitHub Action failed ⛔️. Lo and behold, our linting failed. Or we affected a test that it’s now broken. Or the spacing is inconsistent in one of the files we edited.

Good to know

This article assumes that you’re familiar with eslint, prettier and jest. Furthermore, knowledge of git is required.

The Solution

There are 3 parts to our solution:

  1. Setting up the proper commands in package.json
  2. Setting up lint-staged (more on that later)
  3. Setting up husky

Part 1: Setting up the proper commands

package.json is where all our commands live. Now, as I mentioned in the introduction, frequently we want to run linting, testing and pretty-ing of our code before it gets merged into main. This means we need to make sure we have at least these 3 commands in our file:

  • "lint": "eslint" will run linting for our project and make sure we didn’t break any rules.
  • "prettify": "prettier --write" will run prettier on our project and make sure all the files are formatted accordingly. Consistency, consistency, consistency.
  • "test:changed": "jest --onlyChanged" will try to identify which files have changed, and based on that, which tests are affected (needs the dependency graph). Then, it will run those tests.

Part 2: Setting up lint-staged

lint-staged is a remarkable tool. It allows you to automatically run scripts on a subset of the files you are about to commit. You can find more info here. In this article, we’re only going to scratch the surface of what it can do. So, without further ado, here’s an example of a .lintstagedrc file:

{
"*.+(ts|html)": [
"eslint"
],
"*.{js,ts,html,css,scss}": [
"prettier --write"
],
"*.spec.ts": [
"jest --findRelatedTests"
]
}

Since the commands are pretty simple, I skipped the whole npm run part. Here’s a brief explanation of what’s happening in the file:

  • We select all the files that have a .ts or a .html extension, and we run eslint on them.
  • We select all the files that have a .js, .ts, .html, .css or a .scss extension, and we run prettier --write on them.
  • We select all the files that end with .spec.ts, and we run jest --findRelatedTests on them. This means that we will run all the tests in those files. More on this later.

Cool! Now we have a proper setup for lint-staged. We’re almost there.

Part 3 - Setting up husky

No, not the dog. husky is a tool that allows you to run pre-commit and pre-push commands (among other things). More info here. For our setup, we’ll have 2 new files. First will be the .husky/pre-commit. This will have only 1 line:

npx lint-staged

That’s it. This will make sure that every time you try to commit, this command will be run, which in turn it will kick off the lint-staged process that we set up above 👆. In detail, this means that before the commit command goes through, the eslint, prettier -write, and jest --findRelatedTests commands have to pass.

But that’s not all, just to be super confident, we can set up a pre-push hook. We do this by creating a new file: .husky/pre-push. This one is also a one-liner:

npm run test:changed

This will make sure that when you’re trying to push, all the tests are healthy. To do this efficiently, it will use the dependency graph and try to identify all tests that need to be run based on the changed files.

Part 4 - Clarifications

I know, I know, step 4 was not part of the plan. I just wanted to make sure things are a bit clearer.

First, you will need to install these tools. You will need to run:

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

Just in case that wasn’t clear.

Now, for testing, you will have noticed that we have 2 flavours: --findRelatedTests and --onlyChanged. I didn’t do too much research into these, but here’s my understanding.

  • --findRelatedTests needs a list of files as an argument, it does not do any static analysis, and it runs all the tests in the passed files. This is being used with lint-staged to make sure we run all tests that were changed or added. Again, the only info required for this is: which .spec.ts files are part of the commit.
  • --onlyChanged needs to do a static analysis of the project. It will use that info, with the list of files that changed (all files, not just the .spec.ts files) to figure out which tests need to be re-run. For example, let’s say you changed the button.component.ts. This means that you might have affected the tests in button.component.spec.ts even though that file didn’t change. This situation will be caught by the --onlyChanged flag, but will be missed by the --findRelatedTests flag. I hope this clarifies things a bit.

Congrats 🎉

You made it yet through another article. Thanks for spending the time reading this, and hope it provided some sort of value, since time is a limited resource. Use it wisely. 🙏

Want more?

If you want to know more about this, or something doesn't make any sense, please let me know.

Also, if you have questions, you know where to find me… On the internet!

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

Ready to get serious about your coding process? Automate it with #GitHooks! Learn how to use lint-staged and husky to make sure your linting and testing is not broken between commits! Check out my new article for more info https://catalincodes.com/posts/how-to-automate-your-process-with-git-hooks #Git #Husky #LintStaged #testing

Feb 16, 2023
62 people are talking about this

Wanna read more?

Updates delivered to your inbox!

A periodic update about my life, recent blog posts, how-tos, and discoveries.

No spam - unsubscribe at any time!