Guide: Monorepo in practice

A guide on how to use monorepo in practice

Overview

Monorepo is a code repository management approach that integrates multiple projects into a single repository. Unlike monolithic repositories that store all code without separation or multirepos that place each project in a separate repository, monorepo simplifies cross-project collaboration, reduces dependency redundancy, unifies development standards, and is an efficient solution for multi-project management.

Configure Workspace

Packages in a workspace can be stored in multiple locations, as long as the workspace's Glob matching pattern can include all packages.

At the root of the workspace, update pnpm-workspace.yaml or package.json to add matching rules for sub-packages.

For example, if all sub-packages are placed in the packages directory, packages/* will match all packages in that directory:

pnpm-workspace.yaml
packages:
  - "packages/*"

Our directory structure looks like this:

package.json

The workspace root should not be published to the registry, so set the private field to true in the package.json of the workspace root and private package directories to avoid accidental publishing.

package.json
{
  "private": true
}
Manage Dependencies

Move existing packages to the workspace or create new packages in sub-package locations.

If these packages have dependencies, change these dependency versions to the workspace protocol.

packages/pkg-a/package.json
{
  "dependencies": {
    "@scope/pkg-b": "workspace:^",
    "@scope/pkg-c": "workspace:*"
  }
}

It is recommended to hoist development dependencies shared by multiple packages to the workspace root to reduce duplicate installations and avoid version inconsistency issues, while production dependencies should remain in each sub-package's dependency configuration.

Configuration files like tsconfig, eslint, etc., are usually made into separate packages for easy reference by other packages or publishing to the registry.

Incremental Build and Task Scheduling

As the number of sub-packages in a monorepo increases, the time taken for building, testing, and other processes will increase significantly. We don't need to repeatedly process unchanged packages. Therefore, we often use incremental build systems to solve this problem.

Incremental build systems like turbo, nx, and lerna analyze package dependencies, automatically skip cached content, and only process modified parts.

For tasks that only involve source code, such as formatting and code checking, incremental execution is more flexible. Common strategies include:

  • Managed by the above incremental build systems.
  • Caching checked files at the root of the workspace instead of caching sub-packages, such as dprint and biome.
  • Checking only files in the staging area, such as lint-staged.
Version Management and Publishing

Some package managers like npm do not automatically hoist the workspace root's license. Therefore, you may need to copy the license before publishing or maintain it independently in each sub-package.

To recursively run publish commands in the workspace and publish all packages, you might try tools like changesets and lerna. These tools are used for automatically managing versions, generating change logs, publishing to registries, etc.

Edit on GitHub