Enforcing Coding Conventions with Husky Pre-commit Hooks
This post is a part of the Clean Code Tooling series.
You may want to read the previous posts.
1. How to use ESLint with TypeScript
2. How to use Prettier with ESLint and TypeScript in VSCode
Intro
On most projects I've ever worked collaboratively on, someone takes the role of the code cleanliness champion. It's usually the team lead, and often times, their role involves reviewing PRs and making sure love and care is put into the quality of the code.
Quality includes both the chosen coding conventions in addition to the formatting of the code.
Today, it's good practice in JavaScript projects to utilize ESLint to define the project's coding conventions. For example, how does your team feel about using for
loops? What about semicolons- are those required? Etc.
Those are conventions.
The other piece of the puzzle is formatting. That's the visual appearance of the code. When there's more than one developer working on a project, ensuring that code looks consistent is something to be addressed.
Prettier is the correct tool for this.
In the previous article, we learned how to combine both ESLint and Prettier, but we didn't learn how to actually enforce the conventions and formatting on a real life project with multiple developers.
In this article, we'll learn how to use Husky to do so on a Git-based project.
Husky
Husky is an npm package that "makes Git hooks easy".
When you initialize Git (the version control tool that you're probably familar with) on a project, it automatically comes with a feature called hooks.
If you go to the root of a project intialized with Git and type:
ls .git/hooks
You'll see a list of sample hooks like pre-push
, pre-rebase
, pre-commit
, and so on. This is a way for us to write plugin code to execute some logic before we perform the action.
If we wanted to ensure before someone creates a commit using the git commit
command, that their code was properly linted and formatted, we could write a pre-commit
Git hook.
Writing that manually probably wouldn't be fun. It would also be a challenge to distribute and ensure that hooks were installed on other developers' machines.
These are some of the challenges that Husky aims to address.
With Husky, we can ensure that for a new developer working in our codebase (using at least Node version 10):
- Hooks get created locally
- Hooks are run when the Git command is called
- Policy that defines how someone can contribute to a project is enforced.
Let's get it set up.
Installing Husky
To install Husky, run:
npm install husky --save-dev
Configuring Husky
To configure Husky, in the root of our project's package.json
, add the following husky
key:
"husky": {
"hooks": {
"pre-commit": "", // Command goes here
"pre-push": "", // Command goes here
"...": "..."
}
}
When we execute the git commit
or git push
command, the respective hook will run the script we supply in our package.json
.
Example workflow
Following along from the previous articles, if we've configured ESLint and Prettier, I suggest to utilize two scripts:
prettier-format
: Format as much code as possible.lint
: Ensure that the coding conventions are being adhered to. Throw an error if important conventions are broken.
{
"scripts": {
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", "lint": "eslint . --ext .ts", ...
},
"husky": {
"hooks": {
"pre-commit": "npm run prettier-format && npm run lint" }
}
}
Include these scripts in the scripts
object in your package.json
. Then, at the very least, run prettier-format
and then lint
as a pre-commit
hook.
This will ensure that you cannot complete a commit
without formatted code that passes the conventions.
No-loops example
I like to use the no-loops package as an example. This convention doesn't allow developers to use for
loops, and instead suggests that we use Array utility functions like forEach
, map
, and the like.
Adding the plugin and its rule to the .eslintrc
:
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"no-loops", "prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"no-loops/no-loops": 2, // 2 means throw an ERROR "no-console": 1,
"prettier/prettier": 2
}
}
And then placing a for
loop in the source code...
console.log('Hello world!');
for (let i = 0; i < 12; i++) {
console.log(i);
}
And attempting to commit, it should exit with a non-zero exit code, which as we know, means an error occurred.
simple-typescript-starter git:(prettier) ✗ git commit -m "Test commit"
husky > pre-commit (node v10.10.0)
> typescript-starter@1.0.0 prettier-format simple-typescript-starter
> prettier --config .prettierrc 'src/**/*.ts' --write
src/index.ts 191ms
> typescript-starter@1.0.0 lint /simple-typescript-starter
> eslint . --ext .ts
/simple-typescript-starter/src/index.ts
1:1 warning Unexpected console statement no-console
3:1 error loops are not allowed no-loops/no-loops
4:3 warning Unexpected console statement no-console
✖ 3 problems (1 error, 2 warnings)
And there it is!
Other considerations
If you notice that linting is taking a long time, check out this package, lint-staged. It runs the linter, but only against files that are staged (files that you're ready to push). This was suggested to me by @glambertmtl. Thank you!
Stay in touch!
Join 20000+ value-creating Software Essentialists getting actionable advice on how to master what matters each week. 🖖
View more in Tooling