Code quality controls in our Node.js monorepo

A concrete overview of our setup for code quality tools: This highlights how we lint/format code, with a clear focus configuration management through two primary tools: Eslint and Prettier.

Code quality controls in our Node.js monorepo
This article is a part of the monorepo series.

The first part mentioned the theoretical guidelines regarding the monorepo and its usage at Escape.

The following details will bring a concrete overview of the significant steps of our delivery process: Coding, linting/formatting, and running Escape locally. This one highlights how we lint/format code, with a clear focus configuration management through two primary tools: Eslint and Prettier.

Future articles will discuss the challenges we face in the context of the monorepo and how we solve them: Ensuring decent build times, testing, and scaffolding new workspaces.

Introduction

While stating our objectives for the monorepo, we mentioned this requirement:

It is easy to scaffold a new sub-project to meet all the company's requirements.

The best way to achieve this result is for the company to automatically enforce its requirements across the whole monorepo. For instance, we have standards about how we format and lint our code. We should enforce them:

  • out-of-the-box
  • for every new project
  • with no additional configuration.

That said, we need things to remain flexible, and enhancing these standards is also necessary.

We will focus on quality assessment, mostly performed through the action of linting and formatting our code. In the following, we will assume that we have already set up a basic monorepo using yarn workspaces (or every other workspace tool).

Quality assessment

Configuration

Whether a linter or a formatter, the standard Node.Js tools for code quality assessment provide similar ways of sharing and extending configurations. This section will discuss the pros & cons of using a shared or global configuration for these tools.

Shared configuration

Shared configuration means that one workspace in the monorepo intends to provide and export the base configuration of the tool used in every other workspace.

This workspace exposes a local package, and other workspaces use it as a regular dependency. Eslint and Prettier have relevant documentation on shareable configuration, and Turborepo has documented this kind of setup for eslint.

It is easy to set up a sub-project meeting the company's requirements using shared configurations since we only have to extend a configuration defined in a dedicated package in the repository.

We still have to write that configuration file, install eslint or prettier as a dependency of the sub-project, and trigger one lint/format command per project.

Global configuration

Instead of linting/formatting projects separately, we could have one global configuration for our quality tool, which we use globally in the monorepo.

├── <monorepo>
├── .eslintrc
├── .prettierrc
└── services
    ├── serviceA
    │   └── .eslintrc
    └── serviceB
        └── .eslintrc

In the case of eslint, everything is globally very smooth. Eslint works with a cascading configuration pattern, allowing us to validate our requirements perfectly. When linting our code, eslint recursively walks through every folder and looks for files matching the pattern of files to lint. Every time it enters a folder, it will look for a configuration file and merge the one it eventually founds with the current configuration. The resulting configuration is used to lint the files of this folder.

Thus, we can invoke eslint only once to lint the whole monorepo, following eventual per-project rules.

For Prettier, things get a bit more tedious. We cannot use such a cascading feature. Prettier is configured globally once and for all in the project. However, formatting is relatively simple and prettier is compatible with many languages. It would make sense to have a strict policy on not extending the base prettier configuration.

Our take at Escape

Both solutions presented above match our requirements but have different levels of acceptance.

  • On the one side, shared configurations simplify the setup of linting/formatting tools.
  • Conversely, it is easier to use global configurations since the problem of setting up linting/formatting in a new project of the monorepo disappears with this method.

However, global configurations have a cost on the execution of the command: We loose the fine-grained control over what is linted compared to a per-project configuration (using shared configurations).

Sharing (contrary to make global) would be the way to go if we expect our developers to code in a single sub-project at once or if cross-project modifications are seldom. The setup is longer, but we lint only what needs to be.

In our case, cross-project modifications happen daily, and it makes more sense for us to consider the monorepo as a whole. Our linting task might end up being longer, but we do set up new projects every week, and we cannot afford the cost of re-doing this setup (possibly in the wrong way, introducing technical debt) every time.

Conclusion

We are moving forward with Escape's monorepo. We can now ensure that our code is up to our standards. We can also guarantee it will remain the case for every new project we introduce in the repository.

Stay tuned for our following articles on the monorepo series. We will introduce the challenges we face while running a large and complex monorepo locally and how we tackle them.

Sources

  • Eslint documentation on shareable configurations
  • Prettier documentation on shareable configurations
  • Turborepo's example for a shared eslint configuration
  • Eslint documentation on cascading configurations