Guide: Monorepo in practice
A guide on how to use monorepo in practice
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.
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:
packages:
- "packages/*"
Our directory structure looks like this:
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.
{
"private": true
}
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.
{
"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.
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.
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.