Let’s dig into Lerna!
Lerna is a handy command line utility that you can use to manage JavaScript projects with multiple packages. It can be handy for both open source and private projects.
This post is intended to be more of a primer, and not a deep dive. That being said, if you have any suggestions to make this post better, or just have a question, please leave a comment below!
What’s a package?
Projects often contain many components; perhaps a front-end, server, maybe some micro services, you get the point. They can be individually worked on, versioned, and released, all while being housed under the same repository, which in turn makes development access and project tracking easier. Think of these standalone components as your packages.
How does Lerna help?
Lerna uses Git and NPM to create an optimal workflow for your many project packages, via the lerna
CLI. This could include versioning, distribution, testing, and so much more.
This tool is exciting to me because it removes the hassle of having to manually go through each package of a project to perform mundane operations. On the surface it’s a fairly simple tool, but it can be configured in some pretty neat ways, and used effectively can make working with a monorepo a huge time saver.
Fun fact:
The seven-headed monster you’ll see at the top of the project’s website and repo depict the Greek mythological creature known as the Lernaean Hydra, or Hydra of Lerna. From the Wikipedia article:
The Hydra possessed many heads, the exact number of which varies according to the source. Later versions of the Hydra story add a regeneration feature to the monster: for every head chopped off, the Hydra would regrow two heads.
I find this to be a pretty suitable mascot for the tool. 🐉
Getting set up
As I mentioned above this tool has a few ways in which in can be configured to suit your needs, but in this post we’re just going to get up and running without too much trouble.
Before you go any further, install the command line utility globally:
# via npm
npm install -g lerna
# via yarn
yarn global add lerna
Now you’ll need to decide how and where you’d like to set it up. Lerna can be initialized in a bare directory, or it can be added to an existing project. Either way, run lerna init
in your project folder and you’ll get the following additions:
lerna.json
package.json
/packages
If you ran it on a bare directory it’ll run git init
for you. Make sure you add a remote so you can push versions up.
If you already had a package.json
it won’t overwrite it, but it will add lerna
as a dev dependency. Be sure to npm install
afterward.
The newly added packages/
dir will serve as the place for all your packages to live. “packages” is just the default. Speaking of which…
Your new lerna.json
is the configuration that will be used when performing commands. You can see a full list of options here, but let’s run through a few:
-
packages
This tells Lerna where to find the packages it can operate on. By default this is["packages/*"]
, which globs any directory underpackages/
, hence why it was auto-generated. -
version
This value tells Lerna which version of the project we’re at. By default this is in “fixed” mode which uses a semver value and auto-increments when you run version commands, but you can optionally change this value toindependent
if you’d like to manage package versions independent from one another. You can set this automatically by runninglerna init
with--independent
. -
npmClient
The client to run commands with. Defaults tonpm
, but you can change it toyarn
if you’d like. -
command.publish.ignoreChanges
This can be an array of globs that Lerna will ignore when looking for changes to generate a new version. Examples could be your README, CHANGELOG, any tests or benchmarks, etc.
And that’s mostly it! From here you can add new packages under packages/
. If you initialized Lerna in an existing root-level project you may need to do some reconfiguring. Just make sure that each package has a valid package.json
that Lerna can invoke with its commands. For example:
/packages
/cli
index.js
package.json
Note that Lerna does not need to exist as a dependency in each package.
Quick tip:
You may feel more comfortable manually setting up your packages, but if you’re setting up new ones a great command to use is lerna create [name]
. This will handle it all for you by creating the appropriate structure and assisting you in creating a package.json
(same as npm init
).
Commonly used commands
I went over lerna init
and lerna create
above to help you get started, so now let’s go over some of the commands you might use in a regular development workflow.
Note that commands support filter options, such as --scope
to only execute on certain packages, --no-private
to exclude private packages, and --since [ref]
to only run on packages that have seen changes since a specified Git ref.
lerna run
This command will run, in each package, the script name affixed to it with the configured npmClient
.
Let’s look at an example from the Firefox Accounts monorepo:
// package.json
"scripts": {
"format": "lerna run test",
}
// packages/fxa-content-server/package.json
"scripts": {
"test": "node tests/intern.js --unit=true",
}
// packages/fxa-payments-server/package.json
"scripts": {
"test": "npm-run-all test:*",
}
// packages/fxa-payments-server/package.json
"scripts": {
"test": "scripts/test-local.sh",
}
These are just a handful of the packages listed in that repo, but you get the point. Now, when you run npm run test
at the root, Lerna will iterate across each package and effectively run npm run test
in each directory.
There are handful of ways to customize how this runs:
# Pass args to the scripts
lerna run <script> -- [..args]
# Run scripts simultaneously, good for long-running processes
lerna run <script> --parallel
# Ignore non-zero exit codes when running
lerna run --no-bail <script>
lerna exec
Use this command to run an arbitrary command inside each package. This might seem familiar to the run
command, and if you wanted to you could probably set it to perform the same operation, but this command will run anything, and not just an NPM script.
Take a look at how Antony Budianto’s Create React App Universal CLI uses it:
// package.json
"scripts": {
"clean:build": "lerna exec -- rimraf lib",
"demo:start:client": "lerna exec --scope cra-universal-demo -- npm start",
}
You can see that for all packages running npm run clean:build
would execute the command rimraf lib
in each package directory, and for subsequent commands like demo:start:client
the execution of npm start
is using the --scope
filter option to run only in the package named cra-universal-demo
.
lerna version
This command performs a version bump on packages that have changed since the last release. It won’t run on any packages that have not seen changes, and if you’ve configured your ignoreChanges
it will ignore those files as well.
By default just running lerna version
will provide you with prompts as to how you’d like to perform each version bump. You can also provide a semver keyword (e.g. prerelease
) or an explicit value (e.g. 1.4.8
).
Once that’s done Lerna will run lifecycle scripts, commit and tag the changes, and then push everything to your Git remote.
You can configure this command in a variety of ways. For example:
# Create a release on GitHub or GitLab
lerna version --create-release github
# Perform the changes on the current commit and don't push
lerna version --amend
# Use Convention Commits Specification to determine the version bump and generate CHANGELOG.md files
# https://www.conventionalcommits.org/en/v1.0.0/
lerna version --conventional-commits
# Provide your own version bump message, where %v is replaced with the new version
lerna version -m "chore(release): publish %v"
lerna publish
You might be able to guess what this one does. As the name suggests, this command will publish a new version of each package to the NPM registry. By default it will call the version
command behind the scenes and publish updates since the last release, but there are a couple other options for this:
# Publish packages that have changed since the last release
lerna publish
# Explicitly publish packages tagged in the current commit
lerna publish from-git
# Handy for when you’d like to manually increment package versions but want to be able to automate publishing
# Explicitly publish packages where the latest version is not present in the registry
# Handy for when you need to retry a publish (e.g. it previously failed to publish)
lerna publish from-package
This too comes with a handful of options to suit your needs. Some examples include:
# Publish to NPM with a given dist-tag (instead of `latest`)
# https://docs.npmjs.com/cli/dist-tag
lerna publish --dist-tag next
# If you have two-factor authentication set up to publish
lerna publish --otp 818391
# Automatically accept prompts that come up during the publish process
lerna publish --yes
Wrapping up
I hope I was able to show you just how cool Lerna is in this post. It’s something I’m looking forward to using in all my projects with multiple components.
If you're in need of some examples of how others are using Lerna, the Lerna website has a nice big list of repos, including Jest, Chrome Workbox, WordPress Gutenberg, Vue CLI, ESLint Typescript, and more.
If this post was interesting and you’re eager to Lerna more (sorry) from the DEV community, check out these fine posts from others:
- What is a mono-repository and why you should try Lerna
- Why Lerna and Yarn Workspaces is a Perfect Match for Building Mono-Repos: A Close Look at Features and Performance
- The highs and lows of using Lerna to manage your JavaScript projects
- pnpm vs Lerna: filtering in a multi-package repository
Also, if you’ve got 13 minutes you should definitely watch this great video from Ben Awad where he demonstrates using Lerna. It helped me get a better grasp on how things work, and I’m sure I’ve used some of that info in this post.
I’d love to see how you’re using Lerna! If you’ve got any comments or questions, hit me up in the comments 😎.
Top comments (2)
Nice compilation/resume of lerna tips ;)
Wasn't aware of some.
Thanks!
Thanks from EGYPT