Replacing Lerna with npm builtins
I've used Lerna for years at work and in open source projects. It's a great tool for managing monorepos.
It also does a lot of things, and an npm install --save-dev
installs 614 packages. The last commit in the Lerna GitHub repo (at the time of writing) was June 2021. It's unclear whether it's still maintained.
As part of an effort to apply the same standards that I apply to the source code of PRPL to the tooling in the monorepo, I decided to replace Lerna.
In this note I'll share a bit about what went into this project.
Identifying current behavior
I went through the exact same process I went through to replace Cypress. The first step is to identify what exactly Lerna does for me today:
- PRPL makes use of Lerna's
bootstrap
andexec
commands to install and hoist package node modules.
The npm script looks like:
&&
- PRPL makes use of Lerna's publish command to bump package versions, publish to npm, and generate changelogs automatically.
The npm script looks like:
&&
Replacing bootstrap
See this PR for the feature parity replacement.
Solving for the first problem was straightforward.
In the time since Lerna released its first major version six years ago, the major package managers of the JavaScript ecosystem have introduced a feature called workspaces that solve the same problems that lerna bootstrap
does.
I like using the builtin tools that platforms offer where possible, so naturally I reached for npm workspaces.
This is all I needed to add in the root package.json
:
"workspaces":
After defining the workspaces, running npm install
in the root of the project installs all dependencies of workspace modules and hoists them at the project root. That's it!
Replacing publish
Replacing lerna publish
was less straightforward and can be broken down into three sub-problems: versioning, publishing to npm, and changelog generation.
Versioning
In the case of versioning, npm has a builtin version command that does that, but it's much less smart than what Lerna does.
Lerna will automagically decide for you based on your changes what the next version of a package should be, while npm requires you tell it that explicitly.
Well, unlike some much larger monorepos out there, PRPL has only a dozen or so modules to deal with and probably not many more on the horizon.
I decided that this was an acceptable solution, and wrote a bash script to make it just a little more elegant for cases when I needed to bump all packages at the same time:
# Bump all package versions.
# See https://docs.npmjs.com/cli/v8/commands/npm-version#synopsis
# Example usage:
# `npm run version patch`
bump= # e.g. major, minor, patch
for; do
done
Publishing
The same is true of publishing, npm has a less smart builtin publish command.
Lerna would publish all modules for you in a single command, but I couldn't figure out a way to do that with npm. Back to writing bash:
# Publish packages.
# Example usage:
# - `npm run publish [OTP]`
# - `npm run publish dry-run`
# - `npm run publish [OTP] core server`
# - `npm run publish dry-run core server`
# Does not automatically bump versions or write changelogs, do this prior to running this script.
pkgs=
run_state=""
otp=""
if [; then
fi
if [; then
pkgs=""
run_state="--"
else
pkgs=""
otp="--otp="
fi
if [; then
for; do
done
fi
for; do
done
I didn't bother making the arguments non-positional, and I also didn't solve the potential scenario of the OTP expiring before all modules are published. This is good enough.
Changelog generation
After a detour through the conventional changelog repository, which has many modules for solving the individual problems of generating changelogs automatically from conventional commits, I was tired.
I decided that PRPL is small and stable enough that I can deal with writing them manually. Maybe someday I'll automate this again, but not today.
One interesting find is that Lerna's default changelog generation creates files that say this at the top:
All notable changes to this project will be documented in this file.
Then it goes on to flood the file with messages like this for every new patch:
Note: Version bump only for package @prpl/plugin-sitemap
So, I deleted all those messages. When I write them manually, I'll only include the meaningful refactors, fixes and features.
Bonus: Replacing npm link
A fun side effect of using npm workspaces is that I no longer have to use npm link
to test out packages. Well, only when testing in another workspace in the monorepo at least.
Here's what my dev script looked like before (by now you've realised I like bash):
# Develop a package - e.g. `npm run dev -- core server plugin-rss`
pkgs=
comma_separated_pkgs=
for
do
&& &&
done
It would run npm link
and rollup
in watch mode for the packages you specify, and through the use of a trap
unlink the modules when you killed the process.
After moving to workspaces, I reduced it to:
# Develop packages.
# Example usage:
# - `npm run dev core`
# - `npm run dev core server`
pkgs=
comma_separated_pkgs=
And that's it! Now only the sturdy rollup
module and its watch mode is required to rebuild packages when there are changes.
Bonus x2: Documentation
After all this I decided to create two new documents so that I don't forget everything:
- The release process document
- The development process document
I used to have this info in the main README, but if we're honest that file is mostly for people considering using PRPL, not contributing to it.
Retrospective
And there we have it! Now if you git clone
the PRPL monorepo and npm install
, you'll be downloading 614 fewer modules to your file system.
Thanks for joining me on this esoteric journey about monorepo tooling. Until next time, farewell!
Thanks for reading! Go home for more notes.