loading...
Cover image for Using Lerna to manage your JavaScript monorepo

Using Lerna to manage your JavaScript monorepo

jody profile image Jody Heavener ・8 min read

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 under packages/, 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 to independent if you’d like to manage package versions independent from one another. You can set this automatically by running lerna init with --independent.
  • npmClient The client to run commands with. Defaults to npm, but you can change it to yarn 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:

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 😎.

Discussion

pic
Editor guide
Collapse
antoniorodrigues profile image
antonio-rodrigues

Nice compilation/resume of lerna tips ;)
Wasn't aware of some.
Thanks!

Collapse
moatazabdalmageed profile image
Moataz Mohammady

Thanks from EGYPT