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:
- Setting up the proper commands in
package.json
- Setting up
lint-staged
(more on that later) - 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:
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 runeslint
on them. - We select all the files that have a
.js
,.ts
,.html
,.css
or a.scss
extension, and we runprettier --write
on them. - We select all the files that end with
.spec.ts
, and we runjest --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:
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:
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:
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 withlint-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 thebutton.component.ts
. This means that you might have affected the tests inbutton.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
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