Husky v5 and NPM prepare

Note: This is a summary I wrote for my coworkers this week, on our internal blog. I am hoping that it helps others who have run into similar issues with upgrading from Husky v4 to v5.

I also filed a ticket, suggesting changes to the Husky documentation.


What is Husky?

First, a bit of background. Husky is "Git hooks made easy." Though the easy part may be a bit of a stretch lately. Husky version 4 was indeed relatively straightforward and easy to use.

Husky version 5 is a bit more tricky to set up. Thankfully, it is not necessary to revisit the configuration once it is done. We essentially use it as a proxy to running lint-staged commands.

The following are steps I took to abstract that process, so that usage of v5 is automatic for repos which have it installed. This involved a slight bit of NPM and bash trickery, but not much.


What is different?

Let's not mince words here. Husky v5 is weird.

At first, I did not understand the "why," because v4 syntax seems more intuitive.

I felt like this…

"Get off my lawn, Husky five!"

However, I did some reading and research. As it turns out, Husky is now more natively tapping into the concept of Git hooks than in previous versions. Okay, cool. I guess I can give it a pass on the odd hoops we have to jump through.

That said, here is a comparison of v4 and v5.


Before: v4

Previously, all hooks lived within package.json under the "husky" object.

{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{css,html,js,json,md,scss}": "prettier --write"
}
}

We could simply point a pre-commit hook at lint-staged and move on with our lives.


After: v5

The installation of Git hooks is no longer automatic.

One could accomplish this with postinstall, but that brings its own set of headaches. It runs after others have installed your NPM package as one of their dependencies.

At the time of this writing, the Husky documentation encourages an additional pinst dependency to disable postinstall when distributing an NPM package. That is overkill.

Prepare (script)

Instead, we can use the NPM prepare script. Read more in the NPM docs.

Our prepare.sh file runs after npm install, when pulling down dependencies.

{
"scripts": {
"prepare": "./scripts/prepare.sh"
},
"lint-staged": {
"*.{css,html,js,json,md,scss}": "prettier --write"
}
}

Unlike postinstall, the prepare command does not run when an NPM package is installed as a dependency in another project.

Meaning, it will not run within a package when someone types this…

npm install your-npm-package

…to install it for their own project.

But it will run when working on your-npm-package and installing other dependencies locally. This replicates the behavior we had before with v4.

Prepare (file)

Our prepare.sh file looks like this.

if [ ! -f './.husky/_/husky.sh' ]; then
npx husky install
fi

if [ ! -f './.husky/pre-commit' ]; then
npx husky add './.husky/pre-commit' 'npx lint-staged'
fi

Code within the conditional logic will create .husky/pre-commit if it does not yet exist.

That adds a snippet (instructions for Husky) at the top of the file, and then appends the actual text that we passed in.

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

Conclusion

Is it worth the effort to upgrade? Perhaps not. Essentially, we are now back to feature parity with v4. Except that v5 purportedly runs faster.

Being on the latest version does make me feel better, because that is what will come down the wire if we were to type npm install husky anyway. In so doing, we are keeping current with the latest changes and can help clients stay up to date.

I am hopeful that a simpler alternative to Husky will come along eventually, or that v6 will be more similar to v4. Disappointingly, what was once effortless has become needlessly convoluted.