Using Husky, Git-hooks and Linting to protect everyone from everyone else and yourself

Most modern software teams use some form of Continuous Integration system to at the very least ensure that changes to a project's code base from one team member still allow the codebase to build and often automated tests will also run, all with the intention of ensuring that with each new feature added by a team member to the project, the existing codebase still works in the manner required.

Of course, this only serves to highlight when something goes wrong, at which point the build-breaker has to fix the code. Whilst this is certainly better than issues like this going undetected it's certainly not the cleanest way of working. Wouldn't it be better if there was a way of catching these issues before they were committed by a developer to our code base?

Well, there is, in fact, a very easy way of doing this. Git Hooks provide the functionality to do this and the Husky npm package provides a simple friction-free way of setting this up.

NB: Given the title of this article I am assuming that both Git and Node.js are available to you. There are of course other ways of achieving the same workflow, but these are beyond the scope of this article.

What are Git Hooks?

Git hooks are a way of running custom scripts on events relating to the usage of Git. In this article, we are looking at its usage by developers on their local machines, and specifically the pre-commit hook, after all, we are pursuing the idea that prevention is always better than cure.

The pre-commit hook is the ideal opportunity to run many of the checks that our CI server would run. Preventing code from even being committed to source control should it fail to meet our project specific requirements.

Introducing Husky

Husky is a really cool npm package that lets you define npm scripts that correlate to local Git events such as a commit or push.

This really reduces the friction of using this feature of Git. So for example, if you install Husky using the command

npm install husky --save-dev

and then add the following to the package.json file

"scripts": {
    "test": "npm run build && nyc --reporter=html --reporter=text mocha lib/index.test.js",
    "precommit": "npm test",
    "prepush": "npm test"

}

an attempt to commit or push to a remote repository will result in the test script being run which would, in turn, build our application and run tests.

This makes the usage of Git Hooks in any project relatively low-hanging fruit. (As long as you're comfortable using a small amount of Node).

A more practical example

So to enable anyone to have a play around with this feature I set up a really basic Angular app that is essentially what you get if you install the CLI with Husky added and a few (deliberate) issues that prevent committing or pushing once Husky is installed.

The use of Husky is not linked to using Angular. I have simply chosen to use the Angular CLI as the default project has a couple of features that make it really easy to contrive examples that I wished to demonstrate.

You can create a fork of the code from this GitHub repo.

I have added the following entries to the package.json

"precommit": "ng lint && npm test",
"prepush": "ng lint && ng build --aot true && npm test"

This sets up means that on a commit we:

  1. lint the code
  2. then run tests

and before pushing to a remote repository we:

  1. perform an optimized build
  2. then run unit tests

Helping to enforce bundle optimizations

Rx.js is a popular JavaScript library that is great for dealing with anything that behaves in an asynchronous manner. It's a pretty large library and therefore most applications will only want to bundle what they need in terms of types and operators.

So whilst you could use its observable type through the following:

    import { Observable } from 'rxjs';

To allow the use of:

    Observable.of('some text')
    .subscribe(s => {
      this.title = s;
    });

This would result in the entirety of the library to be included in any bundle. This is because this library adds functions as properties of other functions (i.e static members) or adds its operators to the Observable prototype (instance members). This means 2 things:

  1. You end up with a massive class if you are not explicit in your imports
  2. As all of the code is in one class the current approach to tree shaking isn't going to protect us from this.

Fortunately, the tslint has an "import-blacklist" option which is set by default by the Angular CLI to prevent the direct import of 'rxjs'.

This will force us to have

import '/add/observable/of';  

and

import { Observable } from 'rxjs/Observable';  

This will prevent our project accidentally including far more code than it actually needs which will, in turn, give users of our app a better experience in terms of page load times.

Whilst our build server can be set to fail builds if the code doesn't pass linting we can use Husky to ensure that linting runs before allowing any code to be committed.

Making sure that the code works as expected

Unit tests are only useful if they are actually run. Whilst it is possible to configure our CI server to fail builds and it is also possible to run tests locally, in the real world this sometimes doesn't happen for a variety of reasons. Again, Husky lets us protect ourselves from this.

If you fancy having a play around with this you can create a fork of the code from this GitHub repo. Just pull the code down and try committing a change without fixing the tests.

Making sure building with production optimizations works

Sometimes code that works on your machine may not work elsewhere. This is particularly true for front-end web applications where optimizations for production may not be run as part of the development process.

Angular has a very attractive AOT compilation features that offers some compelling bundle size optimizations but has some fairly strict requirements. The sample code has 2 common scenarios that can cause issues with this even though our code will build and pass unit tests.

We have a component that renders a "title" property and a form that calls a submit function:

<div style="text-align:center">  
  <h1>
    Welcome to {{title}}!
  </h1>
  <img width="300" src="">
</div>  
<!-- the call to the submit function must match the signature of the method in the class -->  
<form class="contact-form" (ngSubmit)="submit(message)" #contactForm="ngForm">  
  <div>
    <textarea placeholder="Message" rows="6" name="message" [(ngModel)]="message" #messageBody="ngModel"></textarea>
  </div>

  <div>
    <button type="submit" md-button class="contact-full-width">Submit</button>
  </div>
</form>  
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {  
  private title = '';
  message = '';

  ngOnInit(): void {
    Observable.of('the wrong text')
    .subscribe(s => {
      this.title = s;
    });
  }
  submit() {
   console.log(this.message);
  }

This code would work locally but it has 2 issues:

  1. The title property has an access modifier of private even though the title property is used in the template
  2. The submit function is called in the template by passing the message property and yet the function doesn't actually have this argument in its declaration.

These discrepancies do not cause invalid JavaScript so will run on a developer's local machine and yet will not allow Angulars AOT compilation process, which is much stricter, to succeed.

We can catch many of these issues with linting. For example, the default tslint configuration that comes with the Angular CLI comes with the following:

"templates-use-public": true

And the more subtle function argument issue can be detected by performing an AOT build.

The npm script that we looked at earlier:

"prepush": "ng lint && ng build --aot true && npm test"

will guarantee that we lint, build for AOT and run tests before a member of the team can push to a remote branch. Again this is a great way of adding an automated a step that can prevent issues before they affect the wider team.

Final thoughts

I definitely don't think that Git Hooks should be at the center of a development teams attempts to automate quality control measures over a code base.

However, Husky, with its ability to point Git Hooks at npm scripts really makes configuring this feature of Git REALLY easy. Simply by implementing checks we can automate tasks on the pre-commit and pre-push hooks and prevent a lot of embarrassment and frustration for us all.

Further reading

  1. More information on available Git Hooks can be found in the documentation.
  2. The full list of aliases for the Git Hooks supported can be found in the documentation.
  3. And of course, you can check out the package itself which has a list of projects that use it.

Andrew de Rozario

Self / community educated developer who loves all things web, JavaScript and .Net related. Passionate about sharing knowledge, teaching and eating though maybe not in that order.

Subscribe to Just In Time Coder

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!