Improving development productivity: the magic of a unified ESLint configuration

Dmitrii Pashkevich
Quadcode
Published in
10 min readSep 22, 2023

Hello everyone! My name is Dmitry Pashkevich, and I’m a Frontend developer at Quadcode. This article isn’t just a tutorial on creating a unified ESLint configuration that can be reused between projects. It’s a story about solving the pain of discussions about code formatting from project to project during reviews.

The article will be useful for developers who want to standardize their approach to code formatting across different projects; looking for a proven solution for codebase standardization.

[SPOILER ALERT] You can review the code in our repository on GitHub repository as well as find the package in NPM.

Why do we need a unified ESLint plugin/configuration?

Uniform code formatting in a team reduces the mental load during code reviews, reading/writing code, or starting a new project. It allows one to focus on how the code works rather than being distracted by how semicolons are placed.

Imagine you have 5 projects, and each has its own formatting rules. You start the 6th and copy configs from previous projects, adding new rules. And so on in a loop. We end up with inconsistent ESLint configs in all projects and, as follows, inconsistently formatted code between projects. As a result, simple things are discussed over and over during code reviews.

In this article, I will explain how to write a plugin/configuration for ESLint and publish it as a package. This will allow you to correct, add, and change the necessary rules in one place and connect them as a single module in other projects.

In our Quadcode team, we use publication to a private registry, but in this article, you’ll see publication to NPM (in the source code).

Repository Preparation

So, the first thing we’ll start with is creating a template for the ESLint plugin project.

To do this, go to the ESLint documentation, the “Create Plugin” section, and follow the recommendation for creating a new project. Navigate to the installation section and perform the required actions.

Open the command line.

Installing Node.js

If you haven’t installed the Node.js platform yet, you need to do so.

Installing Yeoman

Next, install Yeoman — a tool for generating template projects, if you haven’t installed it yet.

npm i -g yo

Installing the ESLint Plugin Generator

Next, we’ll install a utility to generate an ESLint plugin.

npm i -g generator-eslint

Great! All preliminary work is done, now it’s time to create the base project for our plugin.

Creating the Project Directory

Let’s create a directory for our project.

mkdir eslint-plugin-nimbus-clean

And navigate into it.

cd ./eslint-plugin-nimbus-clean

Next, we’ll set up the project structure.

yo eslint:plugin

This command will start the wizard for creating an ESLint plugin project.

Answering the Setup Wizard’s Questions

Let’s go through a short survey.

? What is your name? dipiash
? What is the plugin ID? nimbus-clean
? Type a short description of this plugin: A comprehensive linting solution that sweeps your code clean
? Does this plugin contain custom ESLint rules? No
? Does this plugin contain one or more processors? No

The last two questions were answered with “No” because at this stage, we won’t be using any custom rules or custom processors. Instead, we will use a certain combination of other plugins.

Wait for the generator to create the starting project and open the resulting project in your IDE.

Setting Up .gitignore File

Next, we will create a “.gitignore” file to exclude unnecessary files from being committed to the repository.

touch .gitignore

To avoid drafting this file’s content from scratch, I always use the service: https://www.toptal.com/developers/gitignore. You can also find plugins for your IDE that allow you to generate this file directly there.

We’re interested in “.gitignore” for Node.js — take the content from the link and add it to the “.gitignore” file we created earlier.

Initializing git

Initialize the git repository.

git init

Project Preparation

Let’s make changes to the created project.

In the future, we might need to write custom rules. Let’s immediately add a plugin for linting ESLint rules and connect it as indicated in the documentation.

npm install eslint-plugin-eslint-plugin - save-dev

In the package.json file, in the “scripts” section, add two commands: “build” and “pack”.

The “build” command will compile our project.

rm -rf ./dist && mkdir ./dist && cp -r ./lib/* ./dist

The “pack” command will be needed to locally check the plugin’s operation.

npm pack - pack-destination=./dist

We should also adjust the sections: “main”, “exports”, and “files”, as the content for npm publication will be located in the “dist” directory.

"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"/dist",
"README.md",
"package.json"
]

Additionally, let’s edit the “lib/index.js” file. We won’t need the require index package, so remove that part of the code.

ESLint Plugin vs. ESLint Config

When setting up ESLint, one can often encounter packages with names starting with: “eslint-plugin-*” and “eslint-config-*”. So, what’s the difference?

Plugins must be named as “eslint-plugin-*”. When adding a plugin to your project, the rules won’t be enabled automatically, and therefore you will need to enable each rule individually.

Configs must be named as “eslint-config-*”. When adding a config to your project, all rules will be enabled automatically, and you won’t need to enable each rule individually.

In practice, plugins are needed if you are creating your own code linting rules and want the plugin users to be able to turn them on or off by themselves. In all other cases, you can use a config since it will most likely simply reuse a set of configurations from other plugins.

However, a plugin can also be used as a config with general rules enabled by default. In this article, we will consider such a plugin version that includes a default configuration (recommended). Often in the documentation for plugins, one can see that such plugins are connected to the “extends” section as “plugin:your-plugin-name/recommended” — more details can be found in the ESLint documentation.

Set of Configs / Plugins

Next, let’s determine the main plugins that we will use in our projects.

ESLint

This is directly the eslint itself, from which we will take the recommended config.

Prettier

So as not to use a separate configuration for Prettier, we’ll add to our ESLint plugin/config the rules from the packages:

  • eslint-config-prettier — disables all rules that are unnecessary or might conflict with Prettier.
  • eslint-plugin-prettier — allows us to set up Prettier as ESLint rules and will display information about problems as ESLint issues.

Imports

Let’s set up rules for working with import/export in our code.

React

Since all our projects are written using React, we will naturally add linting support for code written with React.

Typescript

Since the entire codebase is written in TypeScript, we will add rules for linting TypeScript code — @typescript-eslint/eslint-plugin.

Promises

We will add a plugin for linting code that works with Promises — eslint-plugin-promise.

Code quality

And we will add two final plugins for linting code quality.

We will install all the packages listed above and add them as peerDependencies in package.json.

npm i -D eslint-import-resolver-typescript @typescript-eslint/eslint-plugin 
eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-
plugin-prettier eslint-plugin-promise eslint-plugin-react eslint-plugin-react-
hooks eslint-plugin-simple-import-sort eslint-plugin-sonarjs eslint-
plugin-testing-library eslint-plugin-unicorn

Also, to ensure the user sees the entire list of missing projects when installing our package, we will use the directive in package.json peerDependenciesMeta and mark each dependency as `optional: false`.

Rules for ESLint

In this section, I will describe the inclusion of rules for each section from the previous one — I will describe in the same order.

Let’s create a `rules` directory in `lib` — where files for each part of the config will be located.

mkdir ./rules/lib

ESLint

Let’s create a file to describe the ESLint configuration.

touch lib/rules/common.js

And add the rules there.

/** eslint */
module.exports = {
// https://eslint.org/docs/latest/rules/curly
"curly": ["error", "all"],
// https://eslint.org/docs/latest/rules/padding-line-between-statements
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] },
{ "blankLine": "always", "prev": "*", "next": "return" }
],
// https://eslint.org/docs/latest/rules/no-multiple-empty-lines
"no-multiple-empty-lines": ["error"],
// https://eslint.org/docs/latest/rules/arrow-body-style
"arrow-body-style": ["error", "as-needed"],
// https://eslint.org/docs/latest/rules/prefer-arrow-callback
"prefer-arrow-callback": "off",
// https://eslint.org/docs/latest/rules/no-console
"no-console": ["error", { "allow": ["warn", "info", "error"] }],
// https://eslint.org/docs/latest/rules/no-underscore-dangle
"no-underscore-dangle": [
"error",
{
"allow": ["_id", "__typename", "__schema", "__dirname", "_global"],
"allowAfterThis": true
}
],
}

Prettier

Let’s create a file to describe the Prettier configuration.

touch lib/rules/prettier.js

Add the rules.

/** eslint-plugin-prettier */
module.exports = {
"prettier/prettier": "error",
}

Imports

Let’s create a file to describe the configuration for “eslint-plugin-import” and “eslint-plugin-simple-import-sort”.

touch lib/rules/import.js
touch lib/rules/simple-import-sort.js

Add the rules.

/** eslint-plugin-import */
module.exports = {
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/first.md
"import/first": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/newline-after-import.md
"import/newline-after-import": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-duplicates.md
"import/no-duplicates": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md
"import/prefer-default-export": "off",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-anonymous-default-export.md
"import/no-anonymous-default-export": [
"error",
{
"allowArray": false,
"allowArrowFunction": false,
"allowAnonymousClass": false,
"allowAnonymousFunction": false,
"allowCallExpression": true,
"allowLiteral": false,
"allowObject": true
}
],
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unassigned-import.md
"import/no-unassigned-import": "off",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unused-modules.md
"import/no-unused-modules": "error"
}

React

Let’s create a configuration file for React.

touch lib/rules/react.js

Add the rules.

/** eslint-plugin-react-* */
module.exports = {
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/prop-types.md
"react/prop-types": "off",
// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md
"react-hooks/exhaustive-deps": [2],
}

TypeScript

Let’s create a configuration file for TypeScript.

touch lib/rules/typescript.js

Add the rules.

/** @typescript-eslint-* */
module.exports = {
// https://typescript-eslint.io/rules/no-use-before-define/
"@typescript-eslint/no-use-before-define": ["error"],
// https://typescript-eslint.io/rules/no-unused-vars/
"@typescript-eslint/no-unused-vars": [
"error"
],
// https://typescript-eslint.io/rules/no-explicit-any/
"@typescript-eslint/no-explicit-any": "error",
// https://typescript-eslint.io/rules/naming-convention/
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"custom": {
"regex": "[A-Za-z]Interface$",
"match": true
}
},
{
"selector": "typeAlias",
"format": ["PascalCase"],
"custom": {
"regex": "[A-Za-z]Type$",
"match": true
}
}
],
// https://typescript-eslint.io/rules/ban-types/
"@typescript-eslint/ban-types": [
"error",
{
"types": {
// un-ban a type that's banned by default
"{}": false
},
"extendDefaults": true
}
]
}

Promises

Let’s create a configuration file for Promises.

touch lib/rules/promise.js

Add the rules.

/** eslint-plugin-promise */
module.exports = {
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/prefer-await-to-then.md
"promise/prefer-await-to-then": "off",
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/always-return.md
"promise/always-return": "off",
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/catch-or-return.md
"promise/catch-or-return": [2, { "allowThen": true, "allowFinally": true }],
}

Code Quality

Let’s create a configuration file for ESLint.

touch lib/rules/sonarjs.js
touch lib/rules/unicorn.js

Add the rules.

/** eslint-plugin-sonarjs */
module.exports = {
// https://github.com/SonarSource/eslint-plugin-sonarjs/blob/master/docs/rules/no-identical-functions.md
"sonarjs/no-identical-functions": ["error", 5],
}
/** eslint-plugin-unicorn */
module.exports = {
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
"unicorn/no-array-reduce": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-module.md
"unicorn/prefer-module": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-null.md
"unicorn/no-null": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-undefined.md
"unicorn/no-useless-undefined": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
"unicorn/filename-case": [
"error",
{
"cases": {
"pascalCase": true,
"camelCase": true
},
"ignore": [
"next-env.d.ts",
"vite(st)?.config.ts",
"vite-environment.d.ts",
"\\.spec.ts(x)?",
"\\.types.ts(x)?",
"\\.stories.ts(x)?",
"\\.styled.ts(x)?",
"\\.styles.ts(x)?",
]
}
],
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
"unicorn/prevent-abbreviations": [
"error",
{
"checkFilenames": false
}
],
}

Publishing an NPM package

To publish the package, we will use the following utilities:

These utilities will allow us to version our ESLint plugin according to SemVer and describe commits in such a way that it occurs automatically and a CHANGELOG is generated. You can see the entire setup in the repository.

Integration into a Project

After publishing to NPM, you can install and integrate the package into any of your projects.

npm i eslint-plugin-nimbus-clean

Setting up the ESLint configuration.

{
"extends": [
"plugin:nimbus-clean/recommended"
]
}

Detailed instructions are described in the project’s README.

Conclusion

This is my experience in creating custom configurations and plugins for ESLint and publishing them to NPM.

Using this approach, you can create the desired configuration for your projects once and then reuse it. If you need to make some changes to the ESLint config, it only needs to be done in one place, and in the projects, you just need to update the version if necessary.

What else would you recommend adding to this plugin?

You can see all the code in the Github repository, and the package can be found on NPM. I will be glad if you can star the repository:) Feel free to ask me any questions in the comments below.

--

--