Versioning multiple libraries using a monorepo approach in JavaScript

Versioning multiple libraries under the monorepo approach can be challenging, let's see how to face this problems.
14th Mar 2025
Context
When it comes to create a new project, many decisions needs to be taken even before start typing some code. Regarding the project organization, more and more projects today choose to rely on a monorepo approach.
A monorepo is a software development strategy in which multiple projects are stored under the same repository. This approach delivers multiple benefits, some of them are:
- code sharing and reusability: it becomes easier to share code under the same repository, especially when using shared libraries;
- simplify dependency management: if multiple projects rely on the same library, this can reduce compatibility issues and it can be download just once instead of multiple times;
- enhanced collaboration: all team member shares the same codebase, improving the understanding and knowledge sharing.
Challenges
With great power comes great challenges, one of them is related to the library versioning.
When you face with a single library it's easy to choose: you can rely on semver rules and just pick one library available in the plethora of release libraries, like Semantic Release or Commit and Tag Version.
With monorepos is not as easy as it seems, as you may decide between having multiple independent version or a single one common between everyone.
Available options
Let's pretend we have a monorepo with two libraries, @monorepo/core and @monorepo/client. The two main options available when deciding how to version multiple libraries are:
- version multiple libraries with individual version: each library version is independent from the other, this can lead to version discrepancy and difficulties
in choosing the right version for the user. In this scenario our libraries could have versions like:
- @monorepo/core: 1.0.3
- @monorepo/client: 3.8.18
- version multiple libraries using a common version: adopting this approach, each library in the monorepo holds the same version, solving the
challenges encountered in the previous point. At the same time, however, every library update always triggers an update for all the libraries. With this option, our libraries versions would always be like:
- @monorepo/core: 3.8.18
- @monorepo/client: 3.8.18
Make it work with Lerna
During my daily work, with my team we initially adopted the first strategy, but it was causing us a lot of troubles because it was really hard to understand time by time which version of what library should have been installed in some project. After some research, we shifted to the second strategy.
We also needed a smart tool that could help us in realizing this new approach, especially in an already existing project, so we ended up adopting Lerna, which is one of the most famous and adopted tools in the monorepo world.
The following hands-on will help understand how to solve this approach leveraging a library like Lerna without really either publishing or releasing anything. A demo project is available here and will provide an existing repository for starting using Lerna.
Before to start
- Clone the project
git clone https://github.com/federicoibba/lerna-demo-project.git
- Install the dependencies
npm i
Setup Lerna
To install Lerna in the project, you just need to run:
npx lerna init
This command will:
- install the needed dependencies
- create a lerna.json file used as configuration file.
Edit the lerna.json
in order to have the following structure:
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.0.0",
"command": {
"version": {
"conventionalCommits": true
}
}
}
Then, commit the changes with, for instance:
git add . && git commit -m "build: setup lerna"
Let's pretend that we need a do an urgent fix in @monorepo/core
, because the epoch was needed, not a string date.
Go to /core/index.js
and change row 20, returning instead new Date().getTime()
, then commit using a fix commit message:
git add core/index.js && git commit -m "fix: current date"
Use case 1: common version
At this point, you may want to release the new library version. We will use two options that, for this demo project, prevent some errors and will help us focus on the versioning. Run hence:
npx lerna version --no-git-tag-version --no-push
Lerna will ask eventually if you are sure that the changes are correct, printing the future versions:
Changes:
- @monorepo/client: 3.8.18 => 3.8.19
- @monorepo/core: 1.0.3 => 3.8.19
In this case, which is the default behavior called Fixed/Locked, Lerna will see that the versions are highly different, conciliating the versions.
Use case 2: independent version
Let's say that the requirement is to keep the versioning different, then it's enough to just edit the lerna.json
file and change the version row with independent
instead of 0.0.0
.
Also this time, run:
npx lerna version --no-git-tag-version --no-push
Lerna will propose a different bump strategy based on the independent key previously set, showing the following prompt:
Changes:
- @monorepo/client: 3.8.18 => 3.8.19
- @monorepo/core: 1.0.3 => 1.0.4
Conclusions
In this post you have briefly learned:
- what are the monorepos;
- which are the different versioning strategies available;
- how Lerna could help you solving the versioning challenge you can encounter.
If you interested in the topic, I recommend you to keep reading other resources and articles that you can find in the bibliography.