cover of story

Create your code style for the team from scratch. Eslint, Prettier, Commitlint and etc.

Transitioning from informal style conventions to an automated system for checking and maintaining consistent code.

Maksim Dolgikh
9 min readDec 7, 2023

--

Introduction

Code review is one of the major steps in software development.

When there are 1–2 people in a team, the process takes little time and there are hardly any comments on each other’s style. But when the team grows and there are more than 4 people or several teams, the personal experience of each developer in terms of style comes into play, and the review takes much longer than before.

Many tools are currently available in the community to optimize this process and make the review more functional than style-based.

The most popular ones are:

  • Eslint — used to analyze JavaScript code and identify potential problems statically
  • Prettier — ensures formatting style consistency and improves readability
  • Commitlint — used to check commit messages against specific patterns

In this article I will neither tell you how to install packages nor prescribe basic settings for them, this can be found in other articles or the package instructions.

I would like to focus on how to properly use a particular tool from the style guide ecosystem which is being created

Prettier

Let’s start with the easy stuff

It doesn’t take much effort to set up and create, just familiarize yourself with the prettier settings

You can also declare parsers for each file extension that the team works with, and possibly set extension-specific settings.

I suggest a scheme with 2 configuration files:

  • base.js — general config file of settings and plugins for prettier, not bound to any specific technology.
  • angular.js — config file for projects with angular, with different settings and plugins, such as prettier-plugin-organize-attributes
Package structure for @my-team/prettier-config
Package structure for @my-team/prettier-config

The rules for prettier will be organized in npm-package.

The scheme of the current package with the possibility to add new configs will look like this

Package scheme for @my-team/prettier-config
Package scheme for @my-team/prettier-config

Eslint

Here I would like to suggest how to create your configs for Eslint

If the standard recommended configs from each plugin for the corresponding tool are enough for your team, you can skip this part.

As an example, I will show you how to create a plugin package for eslint with configs:

  • recommended
  • strict
  • spec

Selecting plugins and filtering rules

This process will be the most time-consuming of the whole process, as it requires agreement for each plugin to be used on practically every rule. As a result, we should have an eslint-plugin from our team. It can be published and installed in other repositories if needed.

Filtering of rules by the development team

At this stage, we need to form 3 types of rules:

  • recommended — The rules that we 100% want to follow.
    They also include rules in “warning”, which we should use when writing code in the future, but they do not block our work at the moment.
  • strict — The rules that govern our work in Core and new projects.
    They are based on the “recommended” rules, but those that were in “warning” are already in “error” here
  • spec — In case your team is used to writing tests, this rule is also based on “recommended” but provides more freedom and allows you to break method and class contracts to cover more cases

The team familiarizes itself with each plugin in detail, examining the motivation for using the rule, possible problems, and drawbacks.

General scheme of decision-making
General scheme of decision-making

The overall picture of the rule’s consideration is as follows

picture of the rule’s consideration is as follows

Register rules in configurations

After the rules are already formed by the team and categorized into recommended, strict and spec categories, we need to implement eslint-plugin with the declaration of external plugins used.

For my example, I have chosen 3 plugins that are popular for Angular applications

  • @angular-eslint/eslint-plugin, @angular-eslint/eslint-plugin-templates — the official packages for Eslint from the Angular Team.
  • @typescript-eslint/eslint-plugin — the package for working with typescript
  • eslint-plugin-rxjs — the package for working with RxJs

This list of plugins can be extended ad infinitum, but it is enough to set a general pattern of filtering rules, grouping them, and connecting them to configs for the command eslint-plugin

Package structure for @my-team/eslint-plugin
Package structure for @my-team/eslint-plugin
  • configs — directory for placing recommended, strict and spec configs
  • helpers — helpers functions for the needs of the package
  • plugins — directory with plugins of our package. 1 plugin = 1 directory in plugins.
  • index.js — The entry point of our package
  • package.json — The file to build and publish the package to the npm-registry, as well as declaring the plugins used to make the package work.

I’ll omit the details of creating the selected rule files for each category of each plugin, and just depict it with a diagram to make the links between them clearer.

Package scheme for @my-team/eslint-plugin
Package scheme for @my-team/eslint-plugin

Synchronizing prettier with eslint

Earlier we created prettier-config, but some eslint and typescript rules contribute their own code formatting rules.

To avoid conflicts between eslint and prettier we need to add the packages eslint-config-prettier and eslint-plugin-prettier.

According to the pre-designated plugin addition patterns, it will be possible to create a “prettier” directory and create an index.js file with all the settings for that plugin.

Since the eslint-plugin-prettier is not specific to a particular technology stack and is a universal config, a base-config (base.js) can be created in the /configs directory. This config will be responsible for basic environment settings, and will also inherit from generic plugin configs.

Generalized package scheme for @my-team/eslint-plugin
Generalized package scheme for @my-team/eslint-plugin

Building the package

In index.js we have to mark the path to the configs

module.exports.configs = require('require index')(`${__dirname}/configs`);

And in package.json we need to specify the entry point, and valid exports and designate the plugins used in peerDependencies

{
"version": "0.0.1",
"description": "team rules for all projects",
"keywords": [
"eslint",
"eslint plugin",
"eslint-plugin"
],
"name": "@my-team/eslint-plugin",
"author": "my-team",
"main": "index.js",
"exports": {
".": "./index.js",
"./plugins/": "./plugins/"
},
"peerDependencies": {
"@angular-eslint/builder": ">=16.2.0",
"@angular-eslint/eslint-plugin": ">=16.2.0",
"@angular-eslint/eslint-plugin-template": ">=16.2.0",
"@angular-eslint/template-parser": ">=16.2.0",
"@typescript-eslint/eslint-plugin": ">=5.48.2",
"@typescript-eslint/parser": ">=5.55.0",
"eslint-config-prettier": ">=8.7.0",
"eslint-plugin-prettier": ">=4.2.1",
"eslint": ">=8.49.0",
"eslint-plugin-rxjs": "^5.0.2"
},
"dependencies": {
"require index": "^1.2.0"
},
}

Commitlint

This tool will help us standardize the format of commits from developers to make the commit history consistent, and potentially provide support for CHANGELOG.md if we need to keep a change log for public packages.

Creating a config for commitlint is as simple as creating a config for prettier. The main task is to make configs both for a repository with 1 application and for repositories with NX or Lerna.

I’ll just create a package with the 2 necessary configs I want to regulate:

  • base.js — basic config, suitable for most simple repositories
  • nx.js — config for NX-managed repositories

Example, base.js configuration

module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [2, 'never', []],
'scope-empty': [1, 'always', 'lower-case'],
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'type-enum': [
2,
'always',
[
'chore', 'docs', 'feat', 'fix',
'refactor', 'revert', 'test', 'ci',
'lint', 'style'
],
],
},
};
Package structure for @my-team/commitlint-config
Package structure for @my-team/commitlint-config

Usage example

To demonstrate how the style guide works, and how to integrate packages I created an example repository

example repo structure
  • example-angular-app — conditional repository to which we will apply the recommended eslint-config from @my-team/eslint-plugin
  • packages — directory with ready npm-packages containing configs for “style guide”

Installation

Since the packages are in 1 repository with the application, I will install the packages via the npm filesystem without using npm-registry.

{
"name": "example-style-guide",
"license": "ISC",
"workspaces": [
"example-angular-app",
"packages/*"
],
"devDependencies": {
"eslint": "8.54.0",
"@my-team/eslint-plugin": "file:./packages/eslint-plugin",
"@my-team/prettier-config": "file:./packages/prettier-config",
"@my-team/commitlint-config": "file:./packages/commit lint-config"
}
}

Using command configs

For convenience, I always use *.js files to create all configs. They provide convenience and the ability to manipulate configs, e.g. when interacting with process.env .

  • .eslintrc.js (You can read how to inherit from plugins and use their configs here)
module.exports = {
root: true,
plugins: ['@my-team'],
extends: ['plugin:@my-team/recommended'],
overrides: [
{
files: ['*.ts'],
parserOptions: {
project: ['tsconfig.json'],
tsconfigRootDir: __dirname,
createDefaultProgram: true,
},
},
],
};
  • .prettierrc.js
module.exports = require('@my-team/prettier-config/angular')
  • .commitlintrc.js
module.exports = {
extends: ['@my-team/commitlint-config/base'],
};

Commitlint does not work with git-hooks by itself

It also requires husky to be installed. And the rules of binding commitlint to husky are described here

Checking EsLint + Prettier

At this point, I would like to make sure that the recommended and strict configs respond to issues correctly depending on their settings

To do this I will add 1 more config .eslintrc.strict.js

module.exports = {
root: true,
plugins: ['@my-team'],
extends: ['plugin:@my-team/strict'],
overrides: [
{
files: ['*.ts'],
parserOptions: {
project: ['tsconfig.json'],
tsconfigRootDir: __dirname,
createDefaultProgram: true,
},
},
],
};

I want to check that the rules (@angular-eslint/no-output-native , @angular-eslint/template/no-call-expression and @angular-eslint/template/no-any) give different alerts

  • Launch eslint with recommended config
Launch eslint with recommended config
  • Launch eslint with strict config
Launch eslint with strict config
Launch eslint with strict config

Indeed, different configurations of our plugin give different error levels. 🚀

Checking commitlint

Here I would like to check 3 rules (type-enum, scope-enum and scope-empty) declared in the base config.

I expect a commit like style: restyled component to be valid, but a commit like featuree(some-scope): added some best feature is not.

launch validation for 2 commit messages
launch validation for 2 commit messages

Great 🔥

lint-staged

Current configs and tools for automatic style checking do not block changes even if the code does not match the designated style.

As a consequence, at CI startup time our build will “crash” and changes cannot be accepted by the main code. The developer needs time to identify problem areas from the CI console and resolve them locally. These actions can be repeated several times, which will undoubtedly affect usability and development time for the worse.

Yes, you can run the necessary checks manually before the push, but not everyone does it.

To solve this problem, there is lint-staged — which is a tool that allows you to run lint-staged only for files that were changed or added while working with a version control system (like Git) before committing.

This helps you avoid checking the whole project and focus only on changes, saving time and resources. This approach simplifies and speeds up the development process.

Since husky is already installed, following the package instructions we should have no trouble installing and applying it

--

--