Set up ESLint, Prettier, Commitizen, Husky and friends in 2022

Uploaded: 08 Feb 2022

A good developer experience when working with a front end javascript project is really important. We're going to look at integrating ESLint, Prettier, Commitizen and Husky git hooks (plus a few other things!), to ensure your codebase is consistent and easy to work with. So, grab a cuppa and find your favourite chair, because your development experience will shortly change for the better!

ESLint is a javascript linting tool which can be used to find and fix problems and issues with your code. By default, ESLint enforces some stylistic rules as well as code quality rules, for example semi-colon insertion, semi or double quote marks. ESLint is highly configurable and individuals or teams can decide at a very granular level which rules to enable or disable.

Prettier, unlike ESLint, is focused purely on code formatting. It isn't just restricted to javascript, but can also be used to format HTML, CSS, SCSS, markdown and more. Unlike ESLint, Prettier is opinionated about the formatting that it does, which meants there are far less configuration options to choose ... or argue amongst your team about!

Husky is a great tool for configuring git hooks, which can automatically lint code and commit messages, run unit tests (and so much more) before you push or commit to a remote repository. This helps to ensure no bad code gets into the repository because someone forgot to manually run the lint or test commands manually first!

Finally, Commitizen is a tool that is useful in helping to ensure that git commit messages are formatted in a consistent way. An interactive prompt will guide you through the authoring of the commit message. We'll use the cz-conventional-changelog adapter, which follows the conventional-changelog format (although there are others to choose from).

Getting started

In a new worspace, we use npm init to generate a package.json file, accepting the defaults unless we want to change anything.

package.json
{
  "name": "my-soon-to-be-pleasurable-to-work-with-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Set up ESLint and Prettier

Now our package.json is created, we'll install ESLint and Prettier. We also make use of the eslint-config-prettier package, which is a great tool to help de-conflict Prettier and ESLint. It essentially turns off ESLint rules that are unnecessary or might conflict with Prettier. These three tools should all be installed as devDependencies.

npm install eslint prettier eslint-config-prettier  --save-dev

At the time of writing, the versions of these packages installed is:

  • prettier @2.5.1
  • eslint-config-prettier @8.3.0
  • eslint @8.8.0

Create an ESLint configuration file

The ESLint configuration file is used to ... well, configure ESLint! ESLint provides a handy tool to generate our configuration file with the following command:

npm init @eslint/config

We will select 'To check syntax and find problems', and choose a JSON format for the config file. The other options can be set based on the specifics of our project. A config file will then be created, (but may look different to the below, based on the choices for the other options).

.eslintrc.json
{
    "env": {
        "browser": true,
        "commonjs": true,
        "es2021": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": "latest"
    },
    "rules": {
    }
}

Create Prettier configuration and ignore files

We'll create a .prettierrc file in the root of our project. This lets editors and other tools know that we're using Prettier.

Below, we have told Prettier that we want our tab width to be 4 spaces (sorry, 2 space lovers :)). Other Prettier configuration options can be found in the Prettier documentation.

.prettierrc
{
    "tabWidth": 4
}

Let's create a .prettierignore file in the root of our project too, which lets the Prettier CLI tool and extension know which files/directories we don't want to format with Prettier.

.prettierignore
# Ignore artifacts:
package-lock.json
build
coverage

Deconflict ESLint and Prettier rules

As we installed the eslint-config-prettier package, we now need to use it to automatically turn off ESLint rules that Prettier should handle. In the .eslintrc.json file, we should add Prettier to the extends array - making sure that it goes last so that it has the chance to override other configs.

.eslintrc.json
{
    "env": {
        ...
    },
    "extends": ["eslint:recommended", "prettier"],
    "parserOptions": {
        ...
    },
    "rules": {
        ...
    }
}

eslint-config-prettier comes with a great CLI tool which can help to identify ESLint rules in a project which are conflicting with Prettier rules. This is expecially helpful when adding Prettier to an existing ESLint enabled project, which may have a bunch of ESLint rules configured already. To see which rules may conflict, we can run the following command, and manually turn them off in the ESLint rules array if necessary.

npx eslint-config-prettier path/to/main.js

Testing ESLint and Prettier

At this point, we will be able to check if our project conforms to Prettier rules by running:

npx eslint . // run ESLint on all files

npx prettier --check . // Prettier check all files
npx prettier --write . // Prettier check all files and fix automatically

Adding helper scripts to package.json

We can of course add these commands to the scripts in package.json if needed, too.

package.json
{
  ...
  "scripts": {
    "prettier:check": "npx prettier --check .",
    "prettier:fix": "npx prettier --write .",
    "lint": "npx eslint ."
    ...
  },
}

Add extensions to VSCode (optional)

If using VSCode, we can add the ESLint extension and Prettier - Code Formatter extensions. You can then set your 'Default Formatter' to Prettier, and turn on 'Format On Save'. Then, whenever we save a file, Prettier will reformat it automatically.

We could also set these two extensions to recommended extensions by creating a .vscode/extensions.json file. This is useful if multiple people work on the codebase, for better collaboration, as they'll get a popup when opening the project, bugging them to use the recommended extensions.

.vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ]
}

Even recommended extensions do not currently stop the developer from commiting to source control without first ensuring that the code is linted and prettified - or even that their commit messages are useful. The next section will show how to use Husky git-hooks to run these linting and prettifying tasks automatically, upon committing.

Configuring Husky and lint-staged to lint and format code before commit

Setting up Husky

Husky is used to configure git hooks, which can automatically lint code and commit messages before certain git actions take place.

Husky has a one time command which quickly incorporates Husky into a codebase:

npx husky-init && npm install  --save-dev

At the time of writing, the Husky version installed with this command was 7.0.4.

Upon running this command, Husky will add a .hooks directory with a sample pre-commit hook, and modify the package.json with a prepare script (to ensure the Husky hooks are installed whenever a local npm install occurs or before the package is published).

Looking at the new .husky/pre-commit file that is generated, we can see that this sample hook runs the npm test command before commiting (and if any tests fail, it'll abort the commit).

Setting up lint-staged

lint-staged is used to limit linting to staged files only, for performance reasons. If adding ESLint/Prettier into an existing codebase, it is efficient only to lint staged files rather than the whole codebase.

npm install lint-staged --save-dev

At the time of writing, the lint-staged version installed with this command was 12.3.3.

There are many methods to configure lint-staged, such as an .lintstagedrc file or lint-staged.config.js file, but a lint-staged object into the package.json file is also an option:

package.json
{
  ...
  "lint-staged": {
    "*.{css,less,scss,html,json,jsx,js}": [
      "prettier --write ."
    ],
    "*.js": "eslint --fix"
  }
}

The above code tells lint-staged which linting to apply to which staged file types. So, we are running Prettier against a number of staged file types (with the '--write' flag to automatically fix and formatting errors that Prettier finds). We then instruct lint-staged to run ESLint (and to automatically fix any errors that it can) against staged javascript files.

The final piece now is to wire up the Husky pre-commit hook to call lint-staged. This change is done in the .husky/pre-commit file:

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

npx lint-staged

Now, we can run an npm install so that Husky has a chance to pick up the new hooks. After this, when we have any ESLint errors, or Prettier formatting issues in our staged files (and we didn't use IDE extensions to automatically reformat on file save etc), then before the commit task runs, our files will checked and automatically fixed if possible.

Set up Commitizen and friends

We're now going to look at adding Commitizen to our project, and with the help of Husky and commitlint, we can enforce standardised commit messages.

We need to install a number of new packages:

  • Commitizen, to help us to write nice and conventional commit messages wrapped up in a CLI tool
  • cz-conventional-changelog adapter, which is a Commitizen adapter enforcing conventional changelog format commit messages
  • @commitlint/cli and @commitlint/config-conventional which are tools to lint commit messages

Install Commitizen, cz-conventional-changelog lint-staged and @commitlint

To install these packages, run the following command:

npm install commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional --save-dev

At the time of writing, the versions of these packages installed is:

  • commitizen @4.2.4
  • cz-conventional-changelog @3.3.0
  • @commitlint/cli @16.1.0
  • @commitlint/config-conventional @16.0.0

Commitizen configuration

Now, we add a config.commitizen key to the root of our package.json file like follows. This configuration tells Commitizen which adapter to use when attempting to commit to the repository. This configuration could also be stored in a .czrc file in the root of the project if we wanted to keep our package.json file clean.

package.json
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  },
  ...
},

Commitlint configuration

Next, we add a commitlint.config.js file to the root of our project. This works in a similar way to an ESLint config. We set this to extend the @commitlint/config-conventional format, but could also override some rules if necessary.

commitlint.config.js
module.exports = { extends: ["@commitlint/config-conventional"] };

As with other tools, the configuration can be defined in a number of other ways, including but not limited to a commitlint.config.js, .commitlintrc.js, .commitlintrc, .commitlintrc.json, .commitlintrc.yml file or a commitlint field in our package.json file.

Husky hook creation

Now we need Husky to ensure that a consistent commit format is enforced on contributions from those unfamiliar with Commitizen, the project, or who are just lazy!

In the .husky folder, we're going to add a new hook: prepare-commit-msg

.husky/prepare-commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

exec < /dev/tty && git cz --hook || true

The above script means that when we run a 'git commit' command, we'll be presented with an interactive Commitizen session to guide us through writing a meaningful commit message.

Now, we need to add a commit-msg hook to Husky. This hook is called after the user enters a commit message. We will configure this hook so that we ask commitlint to lint our commit message:

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

npx commitlint --edit $1

To test this works in a terminal, we simply change a file, and try to commit it with a non conventional commit message.

git add .
git commit -m "I'm a cowboy"

After the lint-staged task runs, we should then see the interactive Commitizen CLI tool is launched, to assist us in writing a message which conforms to the requirements.

If using the VSCode source control panel, rather than the terminal, inputting an invalid commit message should result in the task failing, and the in built terminal will show us the reasons why the commit message we entered is not valid.

Wrap up and demo

So there you are - with these tools configured we can stop bad code making it's way into the repository and start to enforce some standards, whether that's for consistency between our own projects, or for projects with multiple contributors.

View the commitizen/eslint/prettier/husky starter code.

I hope you've learned something!

Note: I try to make these articles as error free as possible, but if you notice something that's not quite right, please contact me!