Code consistency with ESLint and Husky

At orangejellyfish we're big on consistency in any codebase. It can be really frustrating when you're learning your way around a new project to be confronted with a mess of different styles and conventions. If nothing is consistent there's a much greater cognitive overhead to understanding, navigating and updating an existing codebase. With a language like JavaScript in which there are so many ways to do the same thing and a lack of safety features such as types, the problem can be even worse.

The JavaScript community has made many efforts to improve the situation. Linters such as JSLint, JSHint and more recently ESLint have gained traction as a method of guiding programmers towards a set of opinionated guidelines. Prettier takes this a step further by automatically rewriting your code to comply with the guidelines. This has the additional benefit of allowing everyone to write in the style that suits them while remaining consistent for others when they pull down your changes from source control.

However, Prettier focuses solely on formatting and does not include any of the code quality rules that linters provide. We've built up a set of ESLint rules over the years that offers a code style that works well for us. By combining this, ESLint's fix command, and Husky we have a system that gets us that consistency without sacrificing any flexibility.

ESLint

Our ESLint rule set is based upon the hugely popular Airbnb config. This provides rules covering JavaScript from ES5 onwards as well as React. It's written in a modular way that makes it really easy to customise.

Combining this with editor or IDE plugins goes a long way towards achieving consistency across a codebase and it helps to build discipline among a team.

Auto-fixing

It can be very difficult to break habits made over years of writing JavaScript code, so while having a standardised ESLint config with an editor plugin that notifies you when you write something that doesn't conform, it often doesn't quite go far enough. Setting up ESLint to automatically rewrite such code takes that burden off the developer. This is done with the --fix option. For example, the following command would run ESLint against the specified file, fix any errors it finds that can be automatically fixed, and then either fail with further errors that require human intervention or succeed:

eslint src/components/app.js --fix

Husky

Now that we have a mechanism for linting and automatically cleaning up our code it makes sense to take another step towards automation and get the whole thing running at sensible times without us having to remember to run the previous command every time we change a file. Husky is a tool, written in JavaScript, that makes it easier to manage git hooks.

A git hook is a script that runs in response to a given event such as a commit, push or checkout. If the script exits with a non-zero status code the intended git action will fail. With Husky you can configure git hooks in your package.json file. For example, to define a pre-commit hook that runs our ESLint command we would do something like this:

{
  "husky": {
    "hooks": {
      "pre-commit": "eslint src --fix"
    }
  }
}

Now, any time we run git commit the hook will be executed and the commit will fail if there are any ESLint errors that cannot be automatically fixed. However, there are a couple of problems with this:

  • Any changes that ESLint makes will not be included in the commit. If you run git status after committing you'll see further unstaged changes caused by ESLint auto-fixing errors.

  • We're linting our entire codebase on every commit which is likely to be unnecessary and will slow us down as the project grows. Fortunately, we can once again turn to the world of open-source for a solution.

lint-staged

Combining Husky with lint-staged allows us to resolve both of the problems mentioned previously. Rather than linting the whole codebase it would be much more efficient to only process files that are going to be updated by the commit we want to make. Those files are known as "staged" files in git terminology, hence "lint-staged".

Like Husky, lint-staged can be configured in package.json. A configuration to solve our remaining issue from before, the fact that any auto-fixes made by ESLint won't end up in the final commit, looks something like this:

{
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "git add"
    ]
  }
}

If you have JavaScript code in files with different extensions (such as .jsx when using React perhaps) you can modify the glob pattern accordingly. All that remains now is to update the Husky config to run lint-staged instead of ESLint itself:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }
}

Next steps

At this point we have a solid JavaScript linting setup that goes a long way towards helping us achieve that original goal of consistent code. There are still ways to take this further though, a common example being to configure a pull request handler in your continuous integration environment. This is useful to catch those cases where engineers have bypassed the git hook mechanism.

Do you have any other linting tips? Tweet them to us and we'll update this post to include the best!

Twitter