Why and How to Lint like a PRO
ESlint + Stylelint + Prettier + Husky + Lint-Staged === 💅🏻
One day while fixing some bugs, I discovered that we had a different indentation on scss
and ts
files. So I got up and tried to long-term fix it. Now I am going to share what approach we use at TBC and explain step by step how we got here.
First I tried looking through the web on how to the fix issues like this and found many outdated solutions, so I had to spend some time figuring out the best one. We will be using up-to-date tools and explain in-depth how to integrate each one of them into our project.
Let’s imagine we have Jimmy’s pull request that’s adding the following code into the codebase:
class Person {
constructor(private name,private height) { }
getInfo(){
return `${this.name} ${this.calculate(this.height)}`
}
calculate(height)
{
var tmp = 5;
return this.height*100
} private test() {}
}
What if this pull request was 500 lines of code. What do we do? Do we comment on each formatting issue? Do we suggest removing all unused functions? Or do we leave it for a later refactoring task?
In the formatting part, the popular opinionated code formatter Prettier comes to the rescue. So let's start with it.
Prettier’s simple definition is that it makes your team not worry about if you need a space between the brackets of an empty object {}
🤔
Space, or not space, that is the question…
Simply install it via npm i -D prettier
. We need -D
to mark that we’re going to use this package only for development purposes.
Now if we want to, we can create a config file .prettierrc.js
at the root of our application to specify custom configurations how we want the code to look like.
Let's specify printWidth
(Prettier will try it’s best to keep the length of lines close to this number) to be 100
so It doesn’t break everything in half with new lines:
module.exports = {
printWidth: 100,
singleQuote: true,
};
For more configuration options see this.
(Note that specifying your options is not as good as it seems. In fact, it’s not recommended. The main purpose of opinionated code formatter is to give you guidelines and developers across the world to have 1 and only standard)
Now running prettier . --write
will format your entire repository 🥳
For simpler usage, we can integrate Prettier into our IDEs. For VS Code simply install esbenp.prettier-vscode
. In settings set default formatter to Prettier and use a shortcut alt shift f
or enable format on save.
(We will talk about Webstorm plugins at the end)
Let’s see what Jimmy’s pull requests looks like:
class Person {
constructor(private name,private height) { }
getInfo(){
return `${this.name} ${this.calculate(this.height)}`
}
calculate(height)
{
var tmp = 5;
return this.height*100
}private test() {}
}
What? 🤯
Looks like he forgot to manually format code and also he did not enable format on save or whole another reason might be that his favorite IDE doesn’t support Prettier.
There has to be another way!
Command prettier . --write
works everywhere, so we have to somehow call it before commits.
Husky’s main goal is to simplify a complex topic, git hooks, and enable us to run specific commands on certain events. 🕗
Run npm i -D husky
and followed with npx husky install
for setting up git hooks. After someone checks out to the branch with Husky installed, everyone needs to run npx husky install
. To automate this, we can use npm set-script prepare “husky install”
. The latter command ensures that git hooks are automatically set up after any npm i
.
(Note that npm set-script
only works with npm@^7, you can do this manually by setting “prepare”
the script in package.json to “husky install”
)
Now it’s actually time to enforce formatted code on each commit. Run this to add our pre-commit hook:
npx husky add .husky/pre-commit “npx prettier . --write”
That’s it, but… 😕
First of all, if we try to commit unformatted code next things happen:
- Every file is formatted (Jimmy has a quite weak computer so he doesn't like Prettier crawling over the entire project again and again)
- Staged unformatted code gets actually committed and new formatted code by Prettier appears as new changes, which we have to commit again…
So how we gonna fix those issues...
Lint-Staged exactly does what we wanted, it calls commands on staged files only and adds formatted code to the commit. 👀
You might have guessed → npm i -D lint-staged
. Then we add lint-staged
field in package.json, which specifies what to call on which staged files, in our case: “Call prettier --write
on every staged file”
"lint-staged": {
"*": "prettier --write"
}
Now we edit our old script in .husky/pre-commit and set it to:
npx lint-staged
That’s fine and dandy, let’s try committing unformatted code….
It really works…
class Person {
constructor(private name, private height) {}
getInfo() {
return `${this.name} ${this.calculate(this.height)}`;
}
calculate(height) {
var tmp = 5;
return height * 100;
} private test() {}
}
Newer Jimmy’s pull request seems pretty good, but we have a long way to go. Who’s gonna add a new line between calculate
and getInfo
or who’s gonna fix some logic-based issues in our code, like that unused function and v̷a̸r̴?
ESLint is a static code analysis tool, that helps us write much better and readable code. 👾
Yes yes, you have heard that right. We’re gonna actually use ESlint for typescript. For this quite a few packages are needed…
npm i -D eslint
And now for ts
support
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
We also need to create and configure .eslintrc.js
It will look something like this. We need to use override
to only lint ts
files
Now after installing dbaeumer.vscode-eslint
VSCode extension you will get little red squiggles on ESlint errors.
You can also try Requiring Type Checking for catching more advanced issues that require type information. Without installing any packages just add:
...
extends: {
...
'plugin:@typescript-eslint/recommended-requiring-type-checking'
}
(Note that the rule above might affect your IDE performance)
For stricter rules let's take a look at Airbnb Javascript Rules. Install rules via npm i -D eslint-config-airbnb-typescript eslint-plugin-import
and add it in extends array.
...
extends: [
'airbnb-typescript/base',
...
]
(Note that this config is not final, there can be some rules which may not apply to us, for example, noplusplus or prefer default import in angular project. You may also look into lines between class members and see if it fits your needs)
...
rules: {
'import/prefer-default-export': 'off',
'no-plusplus': 'off',
}
Don’t be scared if you get peerDependency
warnings after installing this package. This is a known problem.
Unfortunately, there can be formatting conflicts between Prettier and ESLint.
So, to fix this we can extend plugin:prettier/recommended
and install eslint-config-prettier
and eslint-plugin-prettier
. The latter one enables prettier and the first one disables the conflicting rules.
(Note that you need to have plugin:prettier/recommended
as the last element of the extends array, so It overrides any rule it needs to override)
extends: [
...
'plugin:prettier/recommended'
],
Our final and more advanced ESlint may look like this:
(Note that we have used plugin:prettier/recommended
at 2 places, the first one is for js
files. Also, note that the default severity of Prettier is error
, I have changed it to warn
so we don't get distracted with red squiggles while writing code)
Now that we integrated Prettier into ESlint, we can change the lint-staged script to:
"lint-staged": {
"*.{js,ts}": "eslint --fix"
}
(Note that we are now linting only js
and ts
files, we will get back to this later)
One might wonder what Jimmy’s pull request looks like right now…
class Person {
constructor(private name: string, private height: number) {} getInfo(): string {
return `${this.name} ${Person.calculate(this.height)}`;
} static calculate(height: number): number {
return height * 100;
}
}
(Note he had to make calculate
static, because the method was not using the class’s state. Rules like this make us write semantically correct code. If you don’t like any of the rules you can turn them off simply by using rules
object in the config file)
Angular
You can also install framework-specific ESLint plugins like Angular-ESLint. For Angular ready ESLint config file see this gist
Stylelint is the same as ESLint, but for style files. Do you want to see less confusing selectors and no more nested classes with 2 or more depth? Do you also want to have ordered properties and want to standardize between #FFF
, #fff
andwhite
? 😎
.first[disabled]:first-child { // <-- Confusing selector
color: red;
... :hover:nth-child(2 - 3n)#shiish { // <-- More Confusing selector
...
} color: white; // <-- Duplicate styles
}
You are in the right hands 🤠👍
Start with npm i -D stylelint stylelint-config-standard
For SCSS stylelint-config-sass-guidelines
and for Prettier npm i -D stylelint-prettier stylelint-config-prettier
Your .stylelintrc.js
should look this likes
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-sass-guidelines',
'stylelint-prettier/recommended',
],
};
(Note that we have stylelint-config-sass-guidelines
second in place, so It can override standard-config)
After installing stylelint.vscode-stylelint
VSCode extension you should get this kind of errors:
If you think this is a little bit strict for style files, you can change Stylelint’s severity to warning
module.exports = {
defaultSeverity: 'warning',
...
If you are using a framework that heavily depends on pseudo-classes, like :host
in Angular, you might also like looking into increasing max-nesting-depth, ignoring pseudo-classes, or bumping this Github issue ignorePseudoClasses: []
...
rules: {
'max-nesting-depth': [
1,
{
ignore: ['pseudo-classes']
},
],
},
All we have to do is to add Stylelint into lint-staged:
"lint-staged": {
"*.{js,ts}": "eslint --fix",
"*.{css,scss}": "stylelint --fix"
}
Since we no longer use Prettier in lint-staged, we don't get formatted html
and json
files, so we have to add this line:
"*.{html,json}": "prettier --write"
If you are trying to integrate these tools into an already existing code base, these changes might be handy:
Use comments to ignore specific files or code sections for ESLint and Stylelint.
You can also configure ESlint to lint specific directories only, but use Prettier for everything (You can slowly add more directories to files
array) or change the severity of common problems to warn
like we did on the entire Stylelint.
Webstrom already has these IDE plugins built-in and you just need to enable them.
Prettier:
Languages & Frameworks → Javascript → Prettier
Stylelint:
Languages & Frameworks → Style Sheets → Stylelint
ESLint:
Languages & Frameworks → Javascript → Code Quality Tools → ESLint
That’s it…
"husky": "^6.0.0",
"prettier": "^2.3.0",
"lint-staged": "^11.0.0","eslint": "^7.27.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^3.4.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-airbnb-typescript": "^12.3.1","@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0","stylelint": "^13.13.1",
"stylelint-prettier": "^1.2.0"
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-config-sass-guidelines": "^8.0.0"
All these packages just for linting? 🤯
Yep, having a good codebase comes with its price.
If you have any questions or spot any mistakes don’t hesitate to comment below.