Improving development productivity: the magic of a unified ESLint configuration
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.
- eslint-plugin-import — will allow us to avoid various issues when importing/exporting modules in the code.
- eslint-import-resolver-typescript — will add TypeScript support for the previous plugin.
- eslint-plugin-simple-import-sort — will allow us to configure module sorting in the desired order according to certain rules.
React
Since all our projects are written using React, we will naturally add linting support for code written with React.
- eslint-plugin-react — rules for linting React code.
- eslint-plugin-react-hooks — will help us adhere to the rules for writing React Hooks.
- eslint-plugin-testing-library — will check the code of our tests for the Testing Library.
- eslint-plugin-jsx-a11y — will check if we have added accessibility rules to our JSX elements or not.
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.
- eslint-plugin-sonarjs — will help identify potential bugs and the use of suspicious patterns in the code.
- eslint-plugin-unicorn — more than 100 useful rules for ESLint.
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:
- release-it + @release-it/bumper + @release-it/conventional-changelog
- @commitlint/cli + @commitlint/config-conventional
- commitizen + cz-git + cz-conventional-changelog
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.