I spent the last week working on ESLint configuration and ensuring that syntax checks were built into the developer workflow. In the process, I read a bunch of different docs, which is usually my signal that an "all in one" blog post needs to be written! So here we go.

What is Eslint?

For starters, ESLint is a tool that statically analyzes your code. Typically, it's used to ensure consistent syntax across a project with multiple collaborators. You've likely used ESLint without realizing it because it was already configured in your project. Ever seen those red squiggly lines in VS code? Those are often courtesy of Eslint.

One thing to keep in mind is that ESLint is incredibly powerful. It has the ability to not only analyze code, but transform it. We'll get to that later.

Configuration

ESLint allows you to set project-level rules using an .eslintrc file. Since every team and project are slightly different, the control you have over your ruleset is quite extensive.

Rules

For every rule, let's say you're setting the no-console rule, you can decide whether the rule should be off, or set to warn or error. Like this:

module.exports = {
rules: {
'no-console': 'warn',
},
}

In the above example, the no-console rule determines whether console log statements should exist in the codebase. If the rule is set to off then console.log can be littered through your code and the linter won't care. If it's set to warn, the linter will let you know the there are console.log statements in the code, but it won't be a showstopper. But if the rule is set to error, linting will fail if a console.log statement shows up in the code.

While this is helpful, some rules need to get more specific. For example, ESLint has a rule called import/no-extraneous-dependencies. The goal of this rule is to catch situations in which you've imported a dependency into your file that is not included in your project's package.json.

While you could use off, warn, or error, it's not as helpful as it could be. That's because there are different types of dependencies, like devDependencies and peerDependencies. A more nuanced configuration of the rule would look like this:

module.exports = {
rules: {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: false,
optionalDependencies: false,
peerDependencies: false,
},
],
},
}

The above code will only show a linting error when core dependencies are imported but not included. Any other dependency type can be safely ignored.

Extends

You may be thinking that this seems a bit tedious. Do you really want to go through and determine your preferences for all of these individual rules? You may, but probably not. In fact, in most cases, you'll only need to configure a handful of individual rules; the rules that differ from the ruleset you're extending.

Many projects use the core ESLint rules, as shown here.

module.exports = {
extends: 'eslint:recommended',
rules: {
'no-console': 'warn',
},
}

However, ESLint also allows you to extend rulesets that are exported by other projects. So you may opt to use the React recommendations, for example.

Root

Another interesting thing about ESLint is that it follows a cascade model. Suppose you're using a monorepo structure with multiple packages that each have their own .eslintrc file. You can include a configuration file in the root of your repo. In that case, ESLint will check the configuration file closest to a given line of code first and move up the tree, merging as it goes.

Typically, the top-level directory will include root: true so ESLint knows it can stop looking for additional config files.

module.exports = {
root: true,
extends: 'eslint:recommended',
rules: {
'no-console': 'warn',
},
}

However, this rule can exist in any .eslintrc. So, if you wanted to include a standalone package in your monorepo that should not comply with the top-level .eslintrc, you can do that. This is a great trick so that you don't need to supersede all of the rules at the top level.

Overrides

Alternatively, you may want to supersede individual files that wouldn't have their own .eslintrc. In that case, you can use overrides, like this:

module.exports = {
root: true,
rules: {
'no-console': 'warn',
},
overrides: [
{
files: ['example/*.js'],
rules: {
'no-console': 'error',
},
},
}

CLI

Now that you have ESLint configured, what can it actually do?

If you run an ESLint command it will go through the files in your project and spit out all the warnings and errors to the command line.

eslint .

You may remember that I mentioned up top that ESLint can perform transforms. Running ESLint with the --fix flag means it will attempt to change any syntax that errors out! It's worth noting that it can't fix every error it finds, but it can handle some of them.

You can also use the --debug flag which will show you what rules ESLint is using. This is helpful if you're attempting to determine why something is failing/passing that shouldn't be.

Scripts

While running ESLint locally is helpful, the point of ESLint is repeatability and consistency in your project. To get that you likely want to add ESLint commands to your package.json scripts.

{
"scripts": {
"lint": "eslint 'packages/**/*.{js,jsx,ts,tsx}'"
}
}

When you do that you can make use of things like husky! We'll talk about that next time.

Wow

There is a lot in this post but there is, even more, I didn't cover. In the scripts example, I used a glob, there are flags like --quiet, you can even ignore certain files throughout your project. But this is a good start towards helping you understand the setup of an existing project or how to start setting up your own.

And who knows, an ESLinterror may lead to finding and solving a bug! It did for me 😃.