Using Eslint and Prettier with Husky and LintStaged
Gain more confidence in your code and commits!
As software developers we spend more than 70% of our time just staring at code to figure out what it's doing or just trying to find stupid bugs that have made their way into our code. Let me show you a bug that I've actually seen in production
if (typeof thing === number) {
// Do something here
}
At first glance, it may not be obvious what the problem is. Have you figured it out? Let me give you a hint, typeof
operator returns a string, so it should be
if (typeof thing === "number") {
// Do something here
}
Debugging can be very time consuming and hard. That's where ESLint comes to our resuce. Using ESLint can help us catch these bugs and save countless hours of debugging. Not only ESLint helps us catch bugs, it can also be used to enforce a consistent code style across the code base. Enforcing a consistent code styles makes sure everyone in the team is one the same page and the code becomes more predictable, which ultimately means more stability.
Where does Prettier come into the picture then? Prettier is a tool that helps us format code automatically. Having a consistent formatting across the code base means less time arguing over tabs
versus spaces
in the commit reviews and more time focussing on the code that actually matters.
Let's start by creating a new project and see how to actually use these tools
mkdir linting-demo && code .
This will create new directory called linting-demo and open it in VS Code. Now run
npm init -y
Let's write a lowly add
function
const add = (num1, num2) => {
return num1 + num2;
};
Let's intall eslint
npm i -D eslint
After ESLint is installed we need to create a config file for ESLint. Create .eslintrc
file in the root of the project. Let's start by putting an empty config. Our .eslintrc
file should look like this
{}
To see the visual feed back right away, install the ESLint extension for VS Code. And put the following lines in VS Code settings.json
"eslint.run": "onType",
"eslint.enable": true,
We want to enable ESLint and want to run it as soon as we type something. Let's see how the feedback looks like!
So far nothing seems to be wrong with our code, so why do we have a red line under const
? The fact is that by default ESLint expects our code to be ES5. Let's fix this. Add the following lines to .eslintrc
{
"parserOptions": {
"ecmaVersion": 2019
},
"env": {
"node": true
}
}
Let's add a few rules and see how our code looks after that. Your .eslintrc
shoud look like this
{
"parserOptions": {
"ecmaVersion": 2019
},
"env": {
"node": true
},
"rules": {
"camelcase": "error",
"semi": "warn",
"no-console": "warn"
}
}
And this is how our code looks like.
You can customize as many rules as you want and make it to your liking but that's a hassle and we don't want to do that. Eslint can be extended with cofigs that other people have already written. Let's add a config that comes with eslint out of the box eslint:recommended
. If you're working with React eslint-config-react-app
is a very good one. For now let's use the recommended config. Edit your .eslintrc
file
{
"parserOptions": {
"ecmaVersion": 2019
},
"env": {
"node": true
},
"extends": ["eslint:recommended"],
"rules": {
"no-console": "warn"
}
}
Not everybody is going to be using VS Code and they may not have their editor set up to have visual feed back which VS Code extension eslint
provides. So Let's add a lint script. In your package.json, add the lint
script. Your package.json
should look like this
{
"name": "linting-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lint": "eslint --ignore-path .gitignore ./src"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^7.0.0"
}
}
The --ignore-path .gitignore
flag tells eslint to ignore everything that's in .gitignore
. Since we are not going to commit them, it makes no sense to lint them anyway. Let's run the lint
script and see the output.
As expected eslint shows us an error that the function add
is defined but never used as this violates the rule no-unused-vars
. We can of course change the error to a warning but that's upto you. I'm going to leave it as it is.
Let's talk about Prettier now. Let's install Prettier.
npm i -D prettier
Also let's setup our VS Code to format our files when we save them. Install the prettier extension for VS Code then open settings.json
of VS Code and add the following lines.
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave":true,
"prettier.requireConfig":true,
We are setting Prettier to be our default formatter. Also prettier.requireConfig:true
tells VS Code to format the code only if there's a .prettierrc
file available in the project. That's just how I like it. You can omit this line and even when the config file isn't present, Prettier will format your code using it's default settings. You can also add prettier rules in VS Code setting.json
as well.
Let's add a .prettierrc
file to the root of our project.
{
"arrowParens": "avoid",
"semi": true,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 80,
"bracketSpacing": false
}
These are a few rules that I like. You can of course tweek them to your liking. Let's add a format
script. Our package.json
should look something like this
{
"name": "linting-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lint": "eslint --ignore-path .gitignore ./src",
"format": "prettier --write --ignore-path .gitignore \"**/*.+(js|json|jsx|md|mdx)\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^7.0.0",
"prettier": "^2.0.5"
}
}
We are telling Prettier to format any files that end with these extensiobjectons .js, .json, .jsx, .md, .mdx
. Again, we are excluding everything that's inside .gitignore
.
The way we want to setup ESLint and Prettier is, that we want to let ESLint check our code for errors and let Prettier handle the formatting. But the problem is that sometimes these two tools can have conflicting rules. To fix this, we are going to install an ESLint Config
npm i -D eslint-config-prettier
After we've installed this, let's tweek the extends
property in .eslintrc
. It should be like
{
"parserOptions": {
"ecmaVersion": 2019
},
"env": {
"node": true
},
"extends": ["eslint:recommended", "eslint-config-prettier"],
"rules": {
"no-console": "warn"
}
}
You can add as many configs as you like, just make sure that eslint-config-prettier
is the last one in the array.
Now that we have both ESLint and Prettier setup, let's add Husky into the mix. What we want is to run our lint
and format
script when we are about to commit. Husky is a tool that let's us use git hooks
in a much easier way. Let's setup a pre-commit
hook with Husky to run our scripts.
npm i -D husky
Let's create a .huskyrc.json
file in the root of our project and add our pre-commit
hook.
{
"hooks": {
"pre-commit": "validate"
}
}
Wait, but we don't have a validate
script yet. Let's create and add it into our package.json
"validate": "npm run format && npm run lint"
Let's add some buggy code into our index.js
and try to commit it.
const add = (num1, num2) => {
return num1 + num2;
};
let x = 3;
// y is not defined
const sum = add(x, y);
console.log(sum);
Let's run the following commands
git add .
git commit -m "Code has some errors"
Here's what the terminal looks like when we try to commit this code
As you can see on the bottom of the above screen shot that our pre-commit
hook failed and the code was not comitted. If we have to commit the code anyways, we can always use the no-verify
flag. Pretty nifty, right?
That's great, but there's one small problem, we are running the lint
and format
script on the entire project. Which can take a lot of time. What we want to do is to only lint the staged files when we are about to commit. That's where LintStaged comes in handy. Let's add that to the project
npm i -D lint-staged
Let's create a .lintstagedrc.json
file in the root of our project.
{
"*.+(js|jsx)": ["eslint"],
"**/*.+(js|json)": ["prettier --write"]
}
We want to run ESLint on all our JavaScript files and also format them. Let's change our .huskyrc.json
file to run LintStaged on the pre-commit
hook. Make sure your .huskyrc.json
looks like this
{
"hooks": {
"pre-commit": "lint-staged"
}
}
That's it. We are pretty much done. Let's fix our code, make a commit and check the output.
There we go, we're all done. Now we can have more confidence in commits and pushes.
