Do you still need Lerna in 2023?

Michał Weskida
Aug 25, 2023
Man thinking

Intro

For years now, lerna has been the tool to go if you need to maintain a monorepo with a list of libraries that you’re publishing to a (somewhat) public registry. Especially if your team had grown and you needed an automated way to manage all the version changing, cross-dependency versions, and the package release process. But is lerna the only way in which you can achieve it? Is it the best tool to do the job?
This 2022 State of JS results show that lerna is not only losing popularity but also isn’t very well received as a tool in general, with more people claiming they wouldn’t use it again compared to the ones that would. In this article, I’m comparing lerna to npm’s, yarn’s, and pnpm’s built-in package management tools to show how one can remove the dependency on lerna altogether, possibly improving the developer experience at the same time.

The backstory

The beginnings

Lerna has been around for quite some time already. The first stable version released to npm goes back to April 2015. You’ve got to give credit to its creators, as it has provided a before-unseen Developer Experience for managing libraries versioning and release processes in monorepo.
Additionally, it came with some useful general working-with-monorepo tools like dependencies hoisting, cross-dependencies symlinking, and process orchestration. But time has passed, and a lot of standalone monorepo-managing libraries have been created, some of them widely adopted, like turborepo, nx, or lage.
The new tooling defied the need for lerna built-in monorepo utilities, especially the ones aimed at task execution. In the meantime, npm, yarn, and pnpm have all been extended with monorepo features themselves, including dependencies hoisting and symlinking. Discussions on whether lerna is needed anymore at all have started popping up here and there on the web.
Then, in April 2022, the new README addition stated that lerna is no longer being maintained, which seemed like a near end for the so-far-popular package.
Then, surprising news was announced - lerna got overtaken by nrwl, a company behind the aforementioned monorepo tool nx. It sparked some hope in the community; a new era for the package was about to start.

The 2023 era

So, how did it go for the last year? Well, nrwl has made some great improvements. They started by replacing or removing old and vulnerable dependencies, then stripping lerna of all the functionalities it wasn’t the best at anymore. The nx utilities have become complementary to lerna commands, then the defaults in v6.
Soon after, in version 7, lerna built-in dependency hoisting and symlinking commands were removed from the main package altogether. So now, in mid-2023, what are we left with? There are pretty much three operations: changed libraries detection, library release strategy assignment, and the publishing process itself. As you can imagine, there are already some alternatives that solve those problems too, like changesets or rush.

The future

Lerna’s future is unsure, especially given the community's reception that hasn’t changed in its favor even after all the changes introduced by nrwl. Additionally, all of lerna’s functionalities can now be replicated with the use of other tools. In case you are thinking about replacing lerna, this article can help you find alternatives.

Tools

For the most part, I’ll be comparing these four tools:

Prerequisites

In order to be able to run workspaces-related commands, you’ll need to do the workspaces setup for a tool of choice. The setup process for all of these tools is quite straightforward.
Here are some basic requirements:
lerna
  • packages entry in lerna.json
npm
  • version 7.0.0 or later
  • workspaces entry in package. json
yarn
  • version 1.0 or later
  • workspaces entry in package. json
pnpm
  • version 1.35.0 or later
  • packages entry in pnpm-workspace.yaml
For other getting started steps, take a look at their respective documentation for the most up-to-date information:

Follow along

For the purpose of this article, I have prepared three exemplary repositories:
Head over there to find all of the commands mentioned in this article and see them in action, including GitHub Actions implementation of prerelease and release CI workflows for all of these approaches. There is no exemplary repository for pnpm, as there is not much that would be shown there. You’ll see what I mean later on.

Lerna core functionalities

In order to find out if lerna can be replaced, we should identify its most important features and find alternative solutions - that’s exactly how this section is divided. In case you’d like to check the whole list of lerna features, it’s available here.
In general, the biggest hassle in managing monorepo that contains publishable libraries is the version management (especially cross-dependencies) and the process of publishing them itself. This section talks about three of the main functionalities that lerna brings to help you address those issues:
  • Automatic detection of changed libraries
  • Assigning release strategies to libraries
  • Automated publishing (e.g., on CI)
Additionally, you’ll see examples of how the same functionalities can be achieved without using lerna at all.

Automatic detection of changed libraries

lerna
lerna changed
As you might already know, this is one of the most often praised features of lerna. It retrieves information about which workspaces have changed, including the ones that depend on others that have changed. Usually, you don’t even need to run it, as this is built into the lerna version, which is a part of lerna publish. More on that a bit later.
npm
// not supported!
As of version 9.8.0, npm does not offer such a feature out of the box. For this, you’d need to use external tools like changesets or rush.
yarn
yarn version check
In the case of yarn, this operation not only checks which workspaces have changed but also throws an error if some of them do not have a release strategy attached (generally, in yarn, that’s an important step of the release process, described in detail later on).
That makes it especially useful when used within the CI workflow that runs on a PR created to the release branch or to a common develop branch - to make sure no one triggers a release workflow without specifying what and how it should be released. You can also run it within the husky pre-commit hook to make sure that all commits include versioning.
pnpm
// not supported!
As of version 8.6.12, pnpm does not offer such a feature out of the box. For this, you’d need to use external tools like changesets or rush.
Note: The setups for changests and rush are not part of this article, as it focuses on features built into package managers. But they are mentioned here for you to take a shot if that’s what you need.

Assigning release strategy to libraries

In order to automatically release libraries (e.g., on CI), you need to assign a release strategy to each library at some point (mark under which version they should be released). There are a couple of ways to achieve that.

Automated release strategy assignment based on commit tags

One of the ways to semi-automate strategy assignment is to follow the Conventional Commits Specification, which describes how to tag commits so that they are truly informative, also for the benefit of automated tools. A good way to force it for all commits is to use commitlint combined with husky.
The good thing about it is that you no longer need to care about managing versions in package.jsons of your libraries, as those can be automatically deducted based on your commit tags. The bad thing about it is that you can’t merge PRs with squash commits, as lerna needs to know the full history of commits with their original messages to deduct which package should receive which release strategy.
In theory, Conventional Commits can be used in combination with any tool, but activating it for lerna is especially straightforward, as you just need to set the conventionalCommits flag to true. In case you don’t (want to) use lerna while still benefiting from automated strategy assignment based on Conventional Commits tags, you’d need to integrate with a tool like semantic-release yourself.
Keep in mind that semantic-release is intended to be used in a project being the npm package, not in a monorepo of libraries and applications. There’s an open issue to support it. Currently, in order to use it in monorepo, you’d need to look into 3rd party solutions like the unmaintained semantic-release-monorepo or multi-semantic-release.
I’ve tried using the latter one, but after spending a couple of hours trying to configure it properly, I have abandoned it and left it on a separate branch. Feel free to build upon it, but beware, as even the author of multi-semantic-release mentions that its implementation is quite flaky. So, all in all, if you’d like to ditch lerna, it’s probably best to use other methods of release strategy assignment mentioned below.

Manual release strategy/versions assignment

Instead of tagging commits, you can assign release strategies (or new versions) to libraries yourself. At this point, you should ask yourself if your libraries need to be independently versioned or if all of them should receive a new version at once, even if not all of them have actually changed.

Independent versioning

In the case of independent versioning, some tools offer a user-friendly CLI that guides you through the process of assigning release strategies to all libraries one by one.
lerna
lerna version
npm
// not supported!
Again, npm runs short and, as of version 9.8.0, does not support such a feature for workspaces. You’ll need some extra dependencies (like the ones mentioned before), custom scripting, or some manual steps to achieve that.
yarn
yarn version check --interactive
yarn version apply --all
What’s specific to yarn is that using the check --interactive command makes the version changes deferred, meaning saved in an internal .yarn/versions directory, like here. They are not being applied to package.jsons automatically. That’s why it needs to be followed up by the apply method before running the publish command. Usually, the release flow is as follows:
  • while working locally, you attach release strategies to workspaces with the use of yarn version check --interactive
  • then, you verify it either with husky pre-commit and/or on CI pre-merge workflow with yarn version check
  • then, within the post-merge CI workflow, you run 'yarn version apply --all' to apply those release strategies to package.jsons, followed by the publish command (described later on) and git commit with push, to apply package.json changes to your repository.
pnpm
// not supported!
As of version 8.6.12, pnpm does not offer such a feature out of the box. Additional work is required, the same as it is for npm. There’s an open issue for it here.

Non-independent versioning

In this case, you’ll be bumping all libraries using the same strategy.
lerna
lerna version <strategy>
npm
npm version <strategy> --workspaces
This command bumps all workspaces with the same release strategy. But beware, this is not updating your inner (cross) dependencies, you’ll still need to do it by hand before triggering the release workflow (like here) to avoid libraries depending on old versions of each other, which leads to bugs quite fast. This is a big difference when compared to lerna and yarn, which can detect how packages depend on each other and bump their respective dependencies for you.
yarn
yarn workspaces foreach version <strategy> [--deferred]
yarn version apply --all // Only if deferred used
In this case, yarn’s implementation gives you the ability to choose whether the version changes should be deferred or not. In the latter case, you need to follow it up with the apply command.
pnpm
// not supported!
As of version 8.6.12, pnpm does not offer such a feature out of the box. As stated in the documentation: “Versioning packages inside a workspace is a complex task and pnpm currently does not provide a built-in solution for it.”

Automated publishing of libraries on CI

Depending on the method of strategy assignment you have chosen, this process might be a bit different.
lerna
lerna publish [from-git] [from-package] --yes
In case you have been using Conventional Commits to tag your commits, you don’t need to use those optional flags. Lerna will deduct which libraries need what upgrades, and it will apply them for you.
Note: lerna needs to have permission to commit directly to the branch on which the workflow runs. Otherwise, the operation will fail. This might require additional steps if that branch is protected from direct pushes. One of the workarounds is to make the branch unprotected in GH but use a bit of custom scripting to prevent people from pushing to it, like here:
In case you have already bumped the versions using lerna version you should be using the from-git keyword on CI. That makes lerna look into git tags pushed and deduct libraries to release from there.
In case you have bumped the versions by hand or without pushing the tags, you should be using the from-package keyword on CI. That makes lerna look into individual package.jsons and publish those that are of a newer version compared to the registry.
The yes flag is needed to confirm the release automatically.
npm
npm publish --workspaces
Note: this assumes you have already bumped package.json versions and dependencies versions.
yarn
yarn workspaces foreach --no-private npm publish --tolerate-republish
Note: this assumes you have already bumped package.json versions and dependencies versions.
pnpm
pnpm -r publish
Note: this assumes you have already bumped package.json versions and dependencies versions.

Features overview

Below is a summary for support of the most important features within the tools mentioned.
In the case of npm and pnpm you’d need to use additional tools like changesets or rush to achieve what’s built-in into yarn already. Read more here:
*Note: In the case of npm, there is no built-in way to assign independent versioning to multiple workspaces at once. You either need to use non-independent versioning or bump workspaces within separate commands.

Summary

As years have passed, alternative solutions have started providing better implementation and user experience for most of lerna functionalities. Monorepo task execution orchestration and caching are now handled by nx by default either way and can be replaced with other solutions mentioned. The same goes for dependency hoisting and symlinking - those are now built-in into npm, yarn, and pnpm and require very little configuration.
What’s significant of what’s left in lerna is package change detection, versioning, and release management, all of which can be replaced right now with the use of built-in yarn commands or with npm/pnpm with additional tools. In the future, another option would be to go with semantic-release, but for now, the lack of support for multiple publishable libraries is a deal-breaker.

My recommendation

I wouldn’t recommend going the npm-only path. It lacks many useful features and is basically a restrain in many cases. It might be viable when used with a combination of additional tools mentioned.
The standard lerna monorepo is fine. It has been around for years, it’s battle-tested, and has good support from nx. On the other hand, its future is unsure, and it puts some restrictions on the developers (like conventional commits, no squash commits, etc.), and you can achieve what it does with different tools.
The yarn implementation seems to be a full-fledged replacement. As can be seen from the features overview table, it has everything you need to maintain a monorepo containing multiple publishable and cross-dependent packages out of the box.
As for the pnpm-only solution, it’s quite similar to npm - you won’t get what lerna provides out of the box. If you’d like to use pnpm and keep all of those features, you need extra tooling, but it doesn’t necessarily need to be lerna. Both changesets and rush are good candidates for a replacement.
If you’re currently using lerna, or starting a new project, consider using other tools mentioned above. You might like yarn’s deferred versioning more (and save some bundle size, not needing to depend on lerna). The ability to merge PRs with squash commits, and no need for strict commit naming (if you’re not a fan) are a nice bonus.
Or maybe, changeset’s or rush’s implementation would fit you better. If you’re currently using nx, you can keep it, no matter if you stay with lerna or not. Alternatively, you can try other monorepo management tools, like turborepo, or go full-rush way, making use of its built-in lerna-like features.
What’s important is that there are a lot of options now, and you should consider them all before locking your team into a tool you probably won’t change for a long time. Hopefully, this article will help you choose what’s best for your product and your team.
category

Michał Weskida

Software Architect at Vazco
A long-time JS enthusiast. Open to all emerging technologies. Focused on aligning business needs with development teams. Open-minded but always challenging the status quo.