- Managing monorepos with lerna and yarn workspaces
- Getting Started
- Adding packages
- Adding npm packages as dependencies
- Publishing
- Run npm-scripts in multiple packages
- Monorepo Setup — Typescript, Lerna, and Yarn Workspaces
- What are we building?
- Workspaces & Lerna
- Create Packages
- Diceroll
- Watch mode
- UI — for real!
- API
- Closing statements
- So many scripts and boilerplate
- Path mappings
- Articles and references:
Managing monorepos with lerna and yarn workspaces
Reusing code is a good thing, splitting your project into multiple small packages could be helpful, its’ easier to focus when working on teams. It’s always better to split the problem into smaller pieces. But when it comes to managing these packages, it rapidly turns into a nightmare, things are dependent on one another, updating multiple packages and maintaing their separate repos. That’s where monorepos come in. You can have multiple packages in a single repository. They all can share the same node_modules folder. You can easily make them dependent on one another, and publish them all at once. No need of managing versions, lerna will take care of that.
Getting Started
This should create your package.json file. You must have workspaces here set to the folders where you have your packages created. Note the version here, it’s 1.0.0 at start, but as you progess further, lerna is gonna update it according. A good pattern is to use the conventionalCommits config with lerna to manage versions. You can read more about that here Eg. package.json
Next run lerna init , you can combine the —independent flag if you wanna maintain versions of each package separately. Eg. lerna.json
useWorkspaces option enables us to reuse the setting for Yarn Workspaces as Lerna’s workspace setting.
You can switch the npmClient option from yarn to npm , if you want npm to run all your commands. Finally commands option is set to use conventionalCommits for our publish process.
Adding packages
You can create a packages folder and start adding creating your packages there or use this command $ npx lerna create @projectName/packagename .
Adding npm packages as dependencies
With yarn workspaces you don’t need to cd to each packages folder in order to install dependcies, all you gotta do is $ yarn workspace @projectName/yourpackagename add npm-package-name If you want to install shared dependencies, $ yarn add -W —dev typescript prettier eslint . Pretty slick, right?😎
Publishing
Once you have coded your packages in the respective directories, now is the time to publish them. lerna publish is the command used to publish your packges. If you used the Conventional Commits pattern, you can use the —conventional-commits flag with the above command to determine the version of your package. With this option, Lerna generates a CHANGELOG.md for a new version. Here is an example.
$ npx lerna publish --conventional-commits lerna notice cli v3.19.0 lerna info versioning independent lerna info Looking for changed packages since @zoomify/rest-api-client@1.0.0 lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular" Changes: - @zoomify/rest-api-client: 1.0.0 => 1.1.0 - @zoomify/cutomize-uploader: 3.0.1 => 3.0.2 ? Are you sure you want to publish these packages? (ynH) : Successfully published: - @zoomify/rest-api-client@1.1.0 - @zoomify/customize-uploader@3.0.2 lerna success published 2 packages
Run npm-scripts in multiple packages
Even with the handy yarn workspaces commands, it could be frustating to test and build each and every package, if you wanna run a command across all pacakges, just do this, Eg, let’s say i wanna build all my packages before publishing.
$ npx lerna run build —stream , this will call the build command specified in package.json files of each package.
Monorepo Setup — Typescript, Lerna, and Yarn Workspaces
UPDATE: I have since tried Turborepo and Nx. And both of those are probably better than this solution.
The information in this article is still valid, but probably not the best advice I can give.
This article is the first in a three-part series about monorepos.
We’ll be looking at setting up a monorepo and use the following tools: Typescript, Lerna, Yarn Workspaces, Webpack, Nodemon. If my choices are not to your liking I’ve linked some articles in the notes section of this article.
If you don’t want to read and want to skip stright to the code you can find the repository here.
What are we building?
We’re going to be creating a monorepo for a fake dice roll application. So we’ll have the following packages:
Workspaces & Lerna
We will be using Lerna and Yarn Workspaces to help in creating, bootstrapping and running commands across our packages. As usual, it’s a good idea to read the documentation. If a concept isn’t explained here (and it probably isn’t) the documentation should help.
The first thing we’ll do is create a package.json for our monorepo
We want to pass the -W flag to force the installation at the workspace root.
It’s probably best to clone the repo.
>
So far our project looks like this:
monorepo/ ├─ lerna.json ├─ package.json ├─ tsconfig.json
Create Packages
Now it’s time to create our three packages. First we’ll create diceroll and ui packages.
You can also use the lerna create command to scaffold a package. I’m not using it here because it creates more than it should for our use case.
Our goal is to end up with something that looks like:
monorepo/ ├─ packages/ │ ├─ diceroll/ │ │ ├─ src/ │ │ │ ├─ index.ts │ │ ├─ package.json │ │ ├─ tsconfig.json │ ├─ ui/ │ │ ├─ src/ │ │ │ ├─ index.ts │ │ ├─ package.json │ │ ├─ tsconfig.json ├─ lerna.json ├─ package.json ├─ tsconfig.json
Diceroll
The diceroll package contains the core business logic and is going to not have any internal dependencies (actually any dependencies whatsoever). The following files do not contain anything of interest and are here to pad the reading time.
You may want to read about the main property . types should be self
Options like exclude , outDir , and rootDir are repeated throughout the tsconfig.json files. They are relative to their origin file and must be specified.
You may change src to lib or forego it entirely. If you do this, don’t forget to change the rootDir in the tsconfig.json as well!
This package doesn’t need the main , types and files set, but feel free to do so.
, >
This has nothing to do with the import! That’s Lerna’s part.
, ],
Let’s first ensure our dependencies are installed and linked by running:
Now, let’s build everything in one go:
This command runs the build for every package that has it specified and streams the output.
You’ll notice a new file called tsconfig.buildinfo in each package folder. This is where Typescript stores some information about the compile process for each package. You may have seen this before if you used the incremental option.
Great, now let’s add a clean script to all our packages and one in our root package.json.
Watch mode
What’s a good build system if it doesn’t have watch mode? Let’s add it to the packages:
I’ve added a folder called pkg-template to copy when creating a new package, but you can use whatever generator you want.
So let’s take a step back and see what we have achieved thus far:
- We can have an arbitrary number of packages which work together
- Typescript compiles as fast as it can and we have a watch mode too
- We have a cleanup script should we ever need it
But our UI doesn’t work, it’s not a UI!
UI — for real!
Let’s add Webpack. We have two choices. Either we run:
Run this inside the packages/ui folder.
yarn add --dev html-webpack-plugin webpack webpack-cli webpack-dev-server
We need to get used to Lerna and its commands. This installs a package to a specific package.
lerna add --scope=@monorepo/ui -D webpack lerna add --scope=@monorepo/ui -D webpack-cli lerna add --scope=@monorepo/ui -D webpack-dev-server lerna add --scope=@monorepo/ui -D html-webpack-plugin
Now let’s scaffold some files:
Why are we doing this? It’s because our final build artifacts bundled by Webpack not Typescript. We’ll have to update our scripts to reflect this.
This is where I’m going to do the first unorthodox thing: I won’t use Babel or a typescript loader such as awesome-typescript-loader or ts-loader .This is so we have a better understanding of the role each tool has. Feel free to add these if you want them.
API
Ok great, we have a UI, but we also want an API! Let’s get cracking!
For our API we’ll use Nodemon to restart out application. We won’t use ts-node . In my experience, ts-node is slow so I’ve decided to skip it this time.
Let’s start by copying package.json and tsconfig.json from the diceroller folder and applying the following modifications.
Of course, we’ll also change the name of the package in package.json to api . That change is omitted for brevity.
Node 12+ has 100% support for ES2019 features. Find out more
Or install them using Yarn
lerna add --scope=@monorepo/api express lerna add --scope=@monorepo/api -D nodemon lerna add --scope=@monorepo/api -D @types/express
How do we start our api? This is where some people will groan. You have to run two commands: watch and start . You can add a helper like concurrently if you so desire. But for the purposes of this example we will stick to running two commands manually.
Closing statements
We have created a monorepo using the minimum amount of tools. We have a stable, working base that provides a good developer experience. In part two of this series will focus on adding Jest, ESLint and more to our monorepo. Stay tuned!
So many scripts and boilerplate
Yeah, but also it takes one click in my IDE to run and see them at a glance.
Adding packages can be automated using a generator or copy/paste and modify as needed. I don’t think boilerplate is that big of a problem.
Adding, removing and script management isn’t what developers will spend their entire work week doing so it may not even be worth automating.
Path mappings
We didn’t use Typescript’s path mappings, this is because we don’t need them. Lerna ensures we can use our packages in a nice and neat manner. Many tutorials and starters for monorepos use path mappings and I don’t see the point. We have everything set up nicely using each tool to do one thing.
To me, this feature’s purpose is to tell Typescript how to resolve some packages when it can’t otherwise, not to provide you with a way to rewrite your folder structure. You can do that if you want but you’ll run into some problems since the output will have the alias, not the resolved version.