Creating the PERFECT ESLint config!

Ever since I've become interested in topics such as code maintainability, style, and architecture, I haven't settled on improving my code to "perfection". But, as perfect things don't exist, here I am - still demanding more and more from the code I write.

Architecture and maintainability are very interconnected - you keep one intact and the other should follow. If you think the code isn't good enough, the so-called "refactorization" might help, but only for a limited number of cases. And repeating such process many times can be... inefficient to say the least. It's better to keep your code's quality consistently high, instead of having to fix it all at one time, not even mentioning rewriting it! But, in order to do so and to make this process easier, you also have to have a consistent and good code style.

Now, code style can often be related to indentation, line's length and stuff like that. With that said, I'd like to go a bit further here and include things like functions and variables declaration rules, how your iterations and loops should look like and etc. Sounds familiar? Yeah, that probably because it's the standard set of functionalities demanded from today's linters. I bet you've heard of them before - you most likely already use one! You know - tools that validate your code against a set of rules related to different things - aka lint it?

So, today we'll make an overview of how to create and use a configuration for one of the most popular linters - ESLint! Oh, and I'll tell you how I've gone extreme when creating mine! Anyway, enjoy!

Rant first!

Before we dive in, I'd like to first present you with a couple of reasons that drove me (and can influence you're choice) to create my own ESLint config. I'll pretend that you know what that tool is and that you've used it before in one way or another. If not, you can check out the official ESLint website - where everything is explained quite clearly - or read one of my previous post, where I explain this topic a bit more in-depth, while relying on another linter called TSLint.

So, why did I take my precious time to create an ESLint config of my own? It's a very popular tool, and NPM is filled with its "shareable configs". The most popular ones usually embrace some kind of a style guide, like the one from Airbnb or Google.

Airbnb's config

Airbnb's style guide and its ESLint config(s) is arguably the most popular option (based on GitHub stars and NPM's downloads). And that's for a good reason - Airbnb's JavaScript style guide is indeed a very well-defined set of rules. The problem is - not each of these rules has a corresponding ESLint rule. Thus, even if you read it all, following it is a bit harder than just having your ESLint-enabled editor show you where the errors are. This leads to me thinking of the config as "not strict enough" or just "not good enough for my use-cases".

Now, because I wanted to keep things simple, I usually just installed the Airbnb's config and... that was it. Unsurprisingly this was far from the best way. Such a configuration left me with quite a few places in my code where I was "free" to do anything I wanted. This could let to an inconsistency in the code style, formatting errors and etc. - all the bad stuff. I could just tweak this config a bit, but somehow such a solution didn't feel very compelling to me. Depending on a 3rd party config with some personal additions just... didn't cut it.

TSLint past

This whole experience left me searching for an alternative, strict config. Sadly, I didn't find much - even when looking through the less popular packages. Most of them didn't suit my needs. The most possible reason for that was the fact that I transitioned to ESLint form my previous linter - TSLint. That's because most of my projects (which usually use the same linter config) were written in TypeScript. And now, that TSLint officially announced the transition to ESLint, it's best to use the "superior" product and future-proof my code-base.

So, what I ended up doing was something unexpected... even by me! I created my own ESLint config from scratch!

How to do it?

Creating an ESLint shareable config isn't anything hard... from a technical standpoint. While we'll get to that in a moment, I'd like to give you a hint about how to best choose the rules for your config.

ESLint provides a great number of available rules built-in. And, while this set is very impressive, you still can extend it with additional plugins! You can either create such a thing on your own (let me know if you want to see such a guide on this blog), or you can use one of the ones available on NPM.

But, how I chose my rules? Well... by going one by one! Yeah, I can't really give you any better advice than just going through the list of available rules, and selecting the ones you want. I can think of a better solution, to be honest!

So, I dedicated quite a lot of my time and go through the very long list of ESLint built-in rules, and created my own, strict-as-hell configuration! Then, I checked one of (possibly multiple) ESLint "awesome lists", and chose a few interesting and well-maintained plugins. Then, after setting them up correctly, I repeated the process of going through the entire list, but this time for each of the installed plugins. The results? - I'll tell you later...

Creating the config

Even though the official ESLint page provides a fair bit of information about creating shareable configs, there might be some gotchas that they might have missed. So, to save you a little googling, here's basically everything I needed to know when creating my personal config!

Basics

If you've ever created a standard ESLint config, creating a shareable one looks pretty much the same! The main difference is the fact that you should use an index.js file instead of any other. There, you place your config and export it in CommonJS-compliant way.

// index.js
module.exports = {
    rules: {
        "semi": [2, "always"],
        "prefer-const": "error"
    }
};

Then you have to provide your index.js for the main field in package.json. There, you also must choose a name for your package/config. Usually (to later use it with a shortcut), your name should be something like eslint-config-myconfig (shortcut - myconfig) or @myscope/eslint-config (shortcut - @myscope).

ESLint config has many settings, but the one you'll spend the most time on is the rules object. Here, you'll set up and configure your rules with values like 0 ("off"), 1 ("warn") or 2 ("error") for the level of warning the rule should emit. Any additional configuration requires you to provide an array, with the first value being one of above, and the following - as written in rule's documentation. It's very important to read the docs properly, as sometimes rules require configuration in the form of an object, multiple properties, or even those two combined!

Anyway, it's not "the ESLint introduction" type of article. I'm just trying to give you some useful tips. So, let's move on!

Plugins

Dealing with plugins for your shareable configs may be... problematic. You see, as ESLint maintainers agree, shareable configs should be just that - configs. Even though they come in the form of NPM packages, they shouldn't contain any logic, including plugins. Sure, they have to be installed and indicated in your package.json devDependencies, but it's not all...

Notice that your plugins aren't imported within the configuration, but rather referenced by name. That's the "config-only" part. To have your plugins and their rules working correctly, you have to have them installed right where they're used - at the end-user side. For that, you'll need your the peerDependencies field. For example:

{
  "name": "eslint-config-myconfig",
  "main": "index.js",
  "peerDependencies": {
    "eslint": ">= 6",
    "eslint-plugin-import": "^2.18.0",
  },
  "devDependencies": {
    "eslint": "^6.0.1",
    "eslint-plugin-import": "^2.18.0",
  }
}

The same applies for ESLint version and all sub-configs that your config will be using. Remember that the config's end-user (probably you) will have to have his devDependencies bloated with all the plugins you use! For me, it's not a very nice solution.

Modules

It's very likely that you'll be configuring a lot of rules. For that, you might consider dividing them into smaller chunks. You can naturally just export usual objects and destruct them in the main config later, but what if you want your "modules" to built upon one another? Or what if you want them to work independently whenever needed? Or make each one of them extend a different config? Well, in all of these cases, you'll have to create multiple smaller configs with a separate .js file for each.

That's all quite fine and seemingly easy, but what would happen when you'd like to bring them all together? Well, the official docs make it clear that you should "extend" your configs by supplying an "absolute" (relative to config's root) path for each of them:

module.exports = {
    rules: {
        'no-console': 1
    },
    extends: 'myconfig/lib/ci/common'
};

But, if you have a simple file structure (one index.js at the root and all the modules in one folder), you can use the same solution as I did (as inspired by eslint-config-airbnb-base). From my actual config:

module.exports = {
  extends: [
    "./configs/errors",
    "./configs/variables",
    "./configs/es6",
    "./configs/modules",
    "./configs/jsdoc",
    "./configs/style",
    "./configs/practices",
    "./configs/typescript"
  ].map(require.resolve)
};

Usage

When you want to use your config without publishing it, you should always remember about npm/yarn link command. And, if you've used any "additional resources" (plugins and etc.) you should either install them directly or use the same trick that Airbnb recommends:

npx install-peerdeps --dev eslint-config-myconfig

Sadly, this won't work for linked or Git-originated packages, so, if you don't want to publish your config on NPM - you're out of luck!

My results

So, with this little overview out of the way, what do my results look like? Well, I can say that they're quite impressive! I used a few plugins (namely import, prettier, jsdoc, sonarjs, and unicorn), turned on quite a few rules, modularize my config, and... it's working! It's very strict and pretty much the best setup I can get for my current TypeScript + ESLint workflow. The only small issue is that sometimes different rules collide with each other, throwing unfixable errors. That's why it still may need some more tweaks and real-world testing! Once that done, I may publish it later. For now, let me know if you'd even want to use such a thing!

Thoughts?

What do you think of all this linting stuff? Do you consider strict ESLint config a good or bad idea? Let me know in the comments below! If you like this post, consider sharing it, following me on Twitter, on my Facebook page, or through my weekly newsletter. Thanks for reading this one, and I wish you a great day!