Starting with Monorepos using Lerna

Random guy thinking lerna + js = tacos

Organizing code has become to difficult task, projects growths faster and architectures has to evolve to be more maintainable and scalable.

Not just the part of deployment has evolved, the part of organizing code has did too.

Sometimes New ideas are taken from “old” ideas

When I started in development world, organizing code was simple: one project one repository. But with time I worked in projects more complex and finally one day I met the Monorepo.

A monorepo is an approach where we save all our code in a single repository … and that’s all. This technique makes the code management easier, because in big solutions we have a lot of projects/packages related with others.

Before to continue

I suggest follow next steps or you can clone the repository:

  • Create a repository (use the common gitignore for nodejs) and clone it
  • Executes npm init
  • Create a folder called packages
  • Inside packages create two apps using create-react-app
  • Remove yarn.lock and node_modules inside two projects

Lerna

Using monorepos is not an easy task, we can have problems like: dependency management, execution of tasks across projects, build or deployment process changes and deployment. A lot of things can go wrong. To solve some problems we can use a tool called Lerna.

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

Repository using Lerna

In a Lerna Repository we have a package.json and a folder called packages, in this folder we can put our projects/packages

  • repository
  • package.json
  • lerna.json — lerna config
  • packages — folder with projects
Root project

Getting started

To start to use lerna you need install the package in mode global or use npx, when you decide what you need to do.

Before to continue I suggest you create a branch and call it lerna-init, it will be used in the future.

After that we need to init our lerna project, lerna has two modes: fixed/locked and independent.

  • Fixed/Locked (default): All packages have the same version specified in lerna.json. This is useful when you are working in only one product. The command for this is lerna init
  • Independent: Every package has his own version, useful if you have more than one product. The command for this is lerna init — independent

No matters if you are working in a existing project or new, if lerna is not defined you need to user lerna init command. For this example we use:

lerna init  --independent

With this command we can see a new file appears: lerna.json

lerna.json configuration

The property packages is an array with paths of the directories, those directories gonna be managed by lerna. You can check the result here, save your changes and upload your branches, for future references I refer to this point like lerna-init.

Bootstrap

The next command we gonna use is lerna bootstrap, this command install dependencies of all packages. By default this command use npm to install the dependencies. Execute the next command:

lerna bootstrap

When task be completed you will see the package-lock.json

Folder structure

You can see the results here. Also you can change the client to download dependencies. For example yarn, just add a property called npmClient and assign “yarn” in lerna.json

Yarn configuration using yarn npmClient

When task be completed you will see the yarn.lock

You can see the results here.

If you look carefully the applications inside packages, you’ll notice both have the same dependencies, with two projects this is not a big deal, but what happens when you have 10 or more? Exactly, you have a life so, you can use the — hoist option to solve this inconvenient.

Hoist install external dependencies, creating a root node_modules (in the root of project) where install all common dependencies, saving space in the disk and time of your life. To continue execute the next command:

lerna bootstrap --hoist

The result of this command creates a new file package-lock.json and a node_modules in root, creates symlinks in the node_modules inside packages. See the result here.

Of course this hoist can have some inconvenient:

  • Is not supported with yarn, yarn has a feature called workspaces to provide a similar functionality
  • When you have two different versions of same packages hoist install the most used
  • If you use relative paths and you make a mistake and uses root node_modules packages, there is not an error but maybe that dependency is not listed in your package.json

For more details you can check hoist documentation.

Hoist is not supported for yarn instead we have something cooler called workspaces. Workspaces is a feature of yarn is not related with lerna, but we can combine this with lerna to improve the workflow.

Workspaces allow us:

  • Setup all packages dependencies executing yarn install only once.
  • Have different versions of packages in our workspace (hoist doesn’t have this)

I suggest to do a checkout to lerna-init and create a new branch based in lerna-init

In other part, workspaces only works if your root package.json is private.

To start we need to go to root package.json and key workspaces, workspaces is an array with all the relative paths for our projects, add one by one is inconvenient but this paths supports globs pattern so we can use the value packages/*

The next step is configure lerna.json with property npmClient with value yarn and useWorkspaces with value true.

After this you can run lerna bootstrap and you watch something like this

You can check the result here, If you want learn more about workspaces.

For the next samples we gonna use yarn

Adding dependencies

In some moment we need to install dependencies in our projects, to do this we can use npm install or yarn add.

When you use npm or yarn add in a project, 2 things happens:

  1. The dependency is downloaded
  2. Dependency and version is added to your package.json and lock file

If you need install a dependency for all projects you need to run it in every folder. Lerna provide us another option using the command lerna add.

lerna add command install a dependency in all our packages, if for some reason we need to install only in some packages we can use the argument — scope limit the installation.

I strongly recommend use lerna add instead yarn add or npm install to avoid mistakes (just for packages, for root folder is fine use them), one of the limitations of this command is just only install a package for execution.

Run scripts across packages

Lerna provide us the command lerna run to run scripts in the package.json for all our projects, if package.json doesn’t have the script is not executed and error is avoided.

Try to run the command lerna run build and see what happens.

lerna run build

The result was a build folder for every project, but you didn’t see any output during the process, now execute:

lerna run build stream

The option — stream show you the output for process adding a prefix to show you the package.

Now run a script for every package take a time, definitely we don’t wanna invest time running a command in project that we don’t modify. Before to do this modify app1(any file is fine). To limit the scope of the execution, we can use filter options of lerna (almost all commands has support for that), — since this use a reference, you can use a branch name, in my case the branch is lerna-yarn-workspaces

lerna run build --stream --since lerna-yarn-workspaces

Husky

In some projects we need add tools like husky, when we work with multiple repositories everyone has his own husky package, but when we work with a monorepo this package must be only in the root. If we have husky in some projects when the installation starts can override our configuration.

Installing husky

The first step is add husky, we need to go to root and use yarn add with some options

yarn add husky -D -W

The option -D indicates is a development dependency, -W tells to yarn this dependency ignore a checking workspaces, normally yarn add is executed inside projects in packages (our workspaces) and that dependencies are added to yarn.lock in root, but this time we are executing this command in root not in a workspace, so we need set this option or husky not will be installed.

Configuring husky

After add husky we need to configure the hooks, to do this we create a file called .huskyrc

We are setting the hook pre-push, before to run push this hook gonna executes the command yarn pre:push in package.json

We can run directly lerna run in hook, but this force use lerna globally and sometimes that is not possible, for that reason we use scripts because we can use lerna like local dependency.

Configuring script

We need to add script in root package.json, in this case we create the script pre:push to execute lerna run test.

Before to test the changes we need to modify test script in projects inside packages (just to finish the test command)

To tests this changes you just need execute git push (no matters if you don’t have any commit).

You can check the result here. Don’t forget use lerna bootstrap when you use the project.

For the last, I give you a challenge, this scripts run test script for every package don’t matters if we don’t have changes, can you fix this?

You can follow me and give me a like in my facebook page, I hope this help you and if it is, share it :)

Application Developer at IBM, Google Developer Expert in Web Technologies And Google Maps Platform, A Reverse Developer & Fullsnack JS Developer