Vlado Tesanovic CEO / developer at innovic.io, open source lover, lifelong learner. Writing code on GitHub in my free time.

Setting up a monorepo with Lerna for a TypeScript project

5 min read 1537

Setting up a monorepo with Lerna for a TypeScript project

Editor’s note: This post was updated 31 January 2022 to use updated versions of Lerna and TypeScript, improve and better organize the tutorial, and provide clearer instructions for modern implementations.

It is often quite useful to use a single repository to maintain an entire project with all of the packages in it. Companies like Google and Facebook have used a single repository internally for most of their projects. This solution can be quite handy when a company uses a similar technology and when all projects share a common set of dependencies and files.

Popular JavaScript projects like Angular, React, Meteor, Babel, Nest.js and many others are using a single repository for all of their packages. In fact, some of them use Lerna for it.

What is Lerna?

Lerna is a popular and widely used tool written in JavaScript for setting and managing multi-package repositories for Node.js projects with npm and Git.

Lerna has two modes: fixed and independent. Fixed mode keeps all versions of packages at the same level. This approach is quite popular these days. You may have seen it in Angular.

Example of fixed mode in Angular

Independent mode allows us to have different versions per package.

If you have a large or complex project, using a single repository for all its packages can help in organization and maintenance. Thankfully, it is quite easy with Lerna.

Initial setup

Before getting started, make sure you have npm and GitHub accounts because we’ll be using those to host our project. Also, create an npm organization so that you can publish the project with scoped packages once we’re done.

With this done, install Lerna as a global dependency:

npm install -g lerna

We’ll build a simple project. Ours will consist of multiple packages for our project, which we’ll call hospital-sdk.

A visual of our monorepo setup for this project

Create a folder named hospital and initialize Lerna inside the folder:

lerna init && npm install

This command will create a lerna.json file with a default folder structure in it.

Our Lerna project's folder structure

/packages is a placeholder for our shared packages, and lerna.json is a Lerna configuration file. package.json is our usual manifest file.

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

Lerna doesn’t create a .gitignore file, so we will create one with this content:

node_modules/
lerna-debug.log
npm-debug.log
packages/*/lib
.idea

We will use TypeScript in our project, but as we mentioned before, Lerna doesn’t support TypeScript, so we’ll treat it as a shared dependency. Add it on the top level of your package.json with:

npm install typescript @types/node — save-dev

This is the recommended approach because we want to use the same tooling across our packages.

Preparing our build script

Alongside TypeScript, we will also install type declarations for Node.js. As Lerna is intended to be used with Node.js, not TypeScript, we will need to add some configuration to make it work.

We’ll have one common tsconfig.json file defined in the root of our project. Our tsconfig.json will look like:

{
 "compilerOptions": {
   "module": "commonjs",
   "declaration": true,
   "noImplicitAny": false,
   "removeComments": true,
   "noLib": false,
   "emitDecoratorMetadata": true,
   "experimentalDecorators": true,
   "target": "es6",
   "sourceMap": true,
   "lib": [
     "es6"
   ]
 },
 "exclude": [
   "node_modules",
   "**/*.spec.ts"
 ]
}

Building our packages with lerna create

As this is a demo project, we will assume that we will have a few modules: patient, doctor, and scheduler.

To create the packages, we’ll use the lerna create command from the root of our project.

The lerna create command will guide us through the creation of a new package. It requires your new package’s name to be passed as an argument. In this case, it’s doctor, resulting in the full name being @hospital-sdk/doctor.

Executing the command to name the package

We will repeat the same process for the patient and scheduler packages.



The resulting folder structure should now be:

Our updated folder structure

Configuring TypeScript by file

Each individual package requires its own tsconfig.json file for correct relative paths. Other options can be extended from the tsconfig.json at the root of the project.

{
 "extends": "../../tsconfig.json",
 "compilerOptions": {
   "outDir": "./lib"
 },
 "include": [
   "./src"
 ]
}

After we add a tsconfig.json to each package, we will create a src folder inside each package with a TypeScript file for that package.

We also need to register a tsc script in each individual package.json. The result should be:

{
  "name": "@hospital-sdk/doctor",
  "version": "0.0.0",
  "description": "> TODO: description",
  "author": "Vlado Tesanovic <[email protected]>",
  "homepage": "",
  "license": "ISC",
  "main": "lib/doctor.js",
  "typings": "lib/doctor.d.ts",
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": ["lib"],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "tsc": "tsc",
    "test": "echo \"Error: run tests from root\" && exit 1"
  }
}

We added simple logic in each .ts file. We can test our setup by running:

lerna run tsc

The command above will run the tsc script in all created packages:

Run the tsc script for all packages

If all goes well, we will compile TypeScript files from the src folder into the lib folder in each package.

Take a look at the package.json for any package in our project and you’ll see attributes like directories, files, typings, publishConfig, and main:

{
  "name": "@hospital-sdk/doctor",
  "version": "0.0.0",
  "description": "> TODO: description",
  "author": "Vlado Tesanovic <[email protected]>",
  "homepage": "",
  "license": "ISC",
  "main": "lib/doctor.js",
  "typings": "lib/doctor.d.ts",
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": ["lib"],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "tsc": "tsc",
    "test": "echo \"Error: run tests from root\" && exit 1"
  }
}

The packages, doctor, and package.json folders are very important because they control what will be pushed to npm, and will serve as the entry point for our library (main and typings attributes).

We will create a GitHub repository for this project and push all of the code there.

Linking packages

With TypeScript compiled, let’s create a test integration package to see how Lerna handles linking packages.

cd packages
mkdir integration
cd integration
npm init -y
cd ../..

To install necessary dependencies from npm, or link ones from the monorepo, use the lerna add command from the root of the project. In case of any naming conflicts, local packages will always take precedence over remote ones.

lerna add @hospital-sdk/doctor --scope=integration

The --scope parameter specifies which module to add the package to.


More great articles from LogRocket:


The unfortunate limitation of the lerna add command is that it can only add one package at a time. Thus, if you need to install more dependencies at once, you can just add them to the package.json and then run the lerna bootstrap command.

{
  "name": "integration",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "ts-node src/index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ts-node": "^7.0.1",
    "@hospital-sdk/doctor": "^0.0.4",
    "@hospital-sdk/patient": "^0.0.4",
    "@hospital-sdk/scheduler": "^0.0.4"
  }
}

lerna bootstrap installs and links all dependencies, both local and remote. When using lerna add, the lerna bootstrap command is called automatically when adding any new packages.

To see whether packages were linked successfully, create a integration/src/index.ts file with following content:

import { Doctor } from "@hospital-sdk/doctor";
import { Patient } from "@hospital-sdk/patient";
import { Scheduler } from "@hospital-sdk/scheduler";

console.log(new Doctor());
console.log(new Scheduler());
console.log(new Patient());

Then, add the start script in integration/package.json.

{
  "scripts": {
   "start": "ts-node src/index.ts" 
  }
}

Running the script from the terminal should produce the following output:

Doctor {}
Scheduler {}
Patient {}

Publishing to npm

Our goal is to publish all packages under the same npm scope, or organization. npm organizations can be private as well; in that case, you can control who is going to see and use your packages.

Earlier, we created a free, public organization at npmjs.org:

Our free, public npm organization

Login to that organization from the terminal:

Login to the npm organization via the terminal

At this moment, we have our organization and build scripts ready. Let’s glue it all together under one npm script from the root package.json:

{
 "scripts": {
   "publish": "lerna run tsc && lerna publish"
 }
}

From the terminal, run:

npm run publish

Lerna will guide us through the publishing process. We will need to choose a package version and push tags to GitHub.

If all goes well, we will see this message at the end: lerna success published 3 packages.

 
npm run publish
> publish
> lerna run tsc && lerna publish
lerna notice cli v3.4.0
lerna info Executing command in 3 packages: "npm run tsc"
> @hospital-sdk/[email protected] tsc
> tsc

> @hospital-sdk/[email protected] tsc
> tsc

> @hospital-sdk/[email protected] tsc
> tsc
lerna success run Ran npm script 'tsc' in 3 packages:
lerna success - @hospital-sdk/doctor
lerna success - @hospital-sdk/patient
lerna success - @hospital-sdk/scheduler
lerna notice cli v3.4.0
lerna info Verifying npm credentials
lerna info current version 0.0.3
lerna info Looking for changed packages since v0.0.3
? Select a new version (currently 0.0.3) Patch (0.0.4)
Changes:
 - @hospital-sdk/doctor: 0.0.3 => 0.0.4
 - @hospital-sdk/patient: 0.0.3 => 0.0.4
 - @hospital-sdk/scheduler: 0.0.3 => 0.0.4
? Are you sure you want to publish these packages? Yes
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna notice
...                                   
lerna notice 
lerna info published @hospital-sdk/doctor 0.0.4
lerna info published @hospital-sdk/patient 0.0.4
lerna info published @hospital-sdk/scheduler 0.0.4
Successfully published:
 - @hospital-sdk/[email protected]
 - @hospital-sdk/[email protected]
 - @hospital-sdk/[email protected]
lerna success published 3 packages

Conclusion

Setting up Lerna for TypeScript takes some configuration, but in the long run, will help you increase the maintainability of your packages, both public and private.

You can find the source code for this project at my GitHub, and learn more about Lerna in their docs.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
.
Vlado Tesanovic CEO / developer at innovic.io, open source lover, lifelong learner. Writing code on GitHub in my free time.

2 Replies to “Setting up a monorepo with Lerna for a TypeScript…”

Leave a Reply