When a team works on a project, keeping the codebase up to coding standards might be challenging. Perhaps a team member submits code that deviates from the established coding style, or you make a commit fixing a component but unintentionally breaking another. What can we do to mitigate these risks?
We can leverage several command-line tools to safeguard the quality of the codebase even before changes are merged into the repository:
These tools are fantastic, but there's a catch: you and your teammates must remember to run them before pushing code to the repository. And, being humans, sometimes you might forget to do so.
That's why today we'll talk about Husky, a tool that automatically runs any number of commands whenever you commit or push. You'll never have to worry about forgetting to format, lint, or test before uploading code to the repo — Husky does it for you every time you run git commit
or git push
. Let's get started.
Suppose we are working on a Laravel project containing PHP and JavaScript code. Don't worry, these instructions will work even if you're not in this exact tech stack.
For PHP, let's use:
./vendor/bin/duster fix
php artisan test
And for JavaScript:
npm run format
npm run lint
npm run test
Husky is a modern native solution for managing your Git hooks, custom scripts you can define to be fired when certain Git events take place. Created by typicode (the developer behind json-server and jsonplaceholder) and counting over 10 million weekly downloads on npm, this tool allows you to automate command execution upon Git events like committing or pushing.
To start, install Husky as a development dependency:
npm install --save-dev husky
After installation, initialize Husky by running the following command:
npx husky init
Please note that, to run this command successfully, you'll need to first initialize your Git repository.
This simple command does two essential things:
prepare
script to your package.json
..husky
folder in your project's root directory containing a pre-commit
file.This pre-commit
file is a bash script that will run before every commit; by default, it contains just one line:
npm test
We can edit this file to include additional commands. Let's adapt it to run the commands we need for our specific project. Modify the pre-commit
file as follows:
npm run formatnpm run lintnpm run test./vendor/bin/duster fixphp artisan test
Now, when you execute a commit using:
git add -A && git commit -m "My commit message"
Husky will automatically run the specified commands before allowing the commit to proceed.
For example, imagine we have an undefined method in a JavaScript file. Husky will execute the first command, npm run format
, without issues. It will then proceed to the second command, npm run lint
, where it will encounter the error:
/dev/laravel-app/resources/js/bootstrap.js 4:1 error 'translate' is not defined no-undef ✖ 1 problem (1 error, 0 warnings) husky - pre-commit script failed (code 1)
Husky will halt the execution of subsequent commands and cancel the commit, allowing us to rectify the issue before attempting to commit again. There's nothing like catching a bug before it flies!
Once you've completed your Husky setup, add the .husky
folder to your repository to ensure that the pre-commit
hook we created will run before anyone commits to the codebase.
You can create another hook by creating more files in the .husky
folder. The filename must be a valid Git hook name; for example, pre-push
.
This strategy is excellent, but you might have noted a downside: running the formatter and linter across the entire codebase may seem excessive when modifying only a few files. Let's address that.
With Duster, we can utilize the --dirty
flag, which instructs the tool to run linters or fixers only on files that have been staged but not yet committed.
In our example, the command would be:
./vendor/bin/duster fix --dirty
We can proceed to edit the .husky/pre-commit
file:
npm run formatnpm run lintnpm run test-./vendor/bin/duster fix+./vendor/bin/duster fix --dirtyphp artisan test
And we are done! Now, let's move to JavaScript.
lint-staged is a tool that enables us to perform checks selectively on just the files we've edited and staged for commit, resulting in a significantly quicker check run.
To use it, let's install it as a development dependency:
npm install --save-dev lint-staged
Add this script to package.json
:
{ "scripts": { // other scripts "lint-staged": "lint-staged" }}
Now, let's update the Husky pre-commit hook. Open .husky/pre-commit
and replace the multiple npm
commands with the one we just added:
-npm run format-npm run lint-npm run test+npm run lint-staged./vendor/bin/duster fix --dirtyphp artisan test
Finally, let's create a configuration file named .lintstagedrc.json
in the root of the project. This file specifies which commands to run based on file extensions. The precise set of commands may vary depending on your setup, but in our example, it will be:
{ "*.css": [ "prettier --write" ], "*.{js,vue}": [ "prettier --write", "eslint --ignore-path .gitignore --fix", "vitest related --run --environment=jsdom" ]}
We are instructing lint-staged
to perform the following tasks:
When CSS files are staged, then:
When JavaScript files are staged, then:
So, if we only have three files staged, lint-staged
will check just those three, skipping the rest of the codebase. This way, we save time by focusing only on what's new or changed.
$ git add -A && git commit -am "Add a cool feature" > demo-husky@0.0.0 lint-staged> lint-staged ✔ Preparing lint-staged...✔ Running tasks for staged files...✔ Applying modifications from tasks...✔ Cleaning up temporary files...[main b57de91] Test lint-staged 2 files changed, 4 insertions(+), 7 deletions(-)
We run the whole test suite whenever we stage JavaScript code, because our changes might have unexpected side effects on other parts of the app. However, if we only edited CSS files, we can safely assume it's okay to skip JavaScript testing.
And if we did not stage JavaScript or CSS files, none of these checks will run. For example, if we stage app/Models/User.php
, Husky will run Duster and Pest, but none of the commands included in lint-staged (Prettier, ESLint, and Vitest).
That's it! We have covered how to format, lint, and test our front-end and back-end code.
This setup will allow you and your team to comply with the project's code styling and quality standards and catch bugs before they get merged into the repo's main branches.
Yes, initially, it might be frustrating to have a commit halted because of an error. But the payoff greatly exceeds that initial discomfort. And here's a little secret: if you want to bypass Husky, add --no-verify
at the end of your commit command. But don't tell anyone I told you!
You can access the complete codebase for this article on this public GitHub repo.
I hope you can implement one or two tips from this guide. If you want to see more content like this, let us know. Until next time!
We appreciate your interest.
We will get right back to you.