Yet another monorepo setup tutorial

Photo by rawpixel on Unsplash

In this tutorial, we will create a monorepo (using yarn workspaces and lerna) with three packages:

  • monorepo/core: a package that contains only typescript functions and has only typescript and jest as dependencies.
  • monorepo/components: a package with React components that will be consumed by other packages.
  • monorepo/webapp: a web application that will consume the core and the components packages and will generate an optimised bundle with webpack.

In order to not make this tutorial too verbose, I will not describe every necessary step to create this monorepo. I will describe here only the steps that I had doubt when setting a monorepo like that for the first time.

I also created a Github repository for this tutorial, you can check it here.

Setting the yarn workspaces

Yarn docs has a great (and not too extensive) documentation about why you would want to use Yarn workspaces, how to use it, etc. So, based on that:

Create a directory and add the following package.json to it:

{
"name": "monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}

The "workspaces" field is an array that contains the path of each workspace. Since it also accepts glob patterns, you can simply assign packages/* to it and create every workspace inside the packages directory.

Creating the monorepo-example/core package:

Create the directory: mkdir packages/core and add a package.json inside it with the following content:

{
"name": "@monorepo-example/core",
"version": "1.0.0",
"main": "lib/index.js", // generated in the build step,
"typings": "lib/index.d.ts" // generated by typescript compiler
}

Setting Typescript:

Since this package will not be consumed directly by a client, it does not need to be transformed in a super optimised bundle. So, no need to use webpack or babel here. I will use only the typescript compiler.

The tsconfig.json file is used to configure the typescript compiler. You can generate one by simply running a tsc --init . This command generates a tsconfig.json file for you with the main options and a description of what each one of them do (so it is easier to see what you would like to set).

# Install it
core$ yarn add --dev typescript
# Create a tsconfig.json file
core$ ./node_modules/.bin/tsc --init

In order to compile the typescript files and generate a javascript file as output, it is necessary to specify the input (.ts files) and the output (.js) directories. So, edit the tsconfig.json file and set the rootDir and the outDir options.

Also, set the declaration option to enable the generation of *.d.ts files in the lib directory. This will make the type definitions available when using this package in another Typescript package.

With that, you should be able to compile your Typescript files using tsc. When running this command, the compiled javascript files will be generated in the lib directory.

Setting jest:

Add the following packages:

yarn add --dev jest ts-jest @types/jest

Although Jest has a great Typescript support, it does not recognise it out-of-the-box. So you need to add a jest.config.js file to specify where jest should search for files, what the regex of a test file is, what kind of transformation it must apply when it finds a .ts file, etc. So…:

// monorepo/packages/core/jest.config.js
module.exports = {
"roots": [
"<rootDir>/src",
],
"transform": {
"^.+\\.ts$": "ts-jest",
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.ts$",
"moduleFileExtensions": [ "ts", "js" ],
};

Using Jest with Typescript is a great tutorial about this.

Creating the monorepo-example/components package:

Create the components directory inside packages and create the followingpackage.json inside it:

// monorepo/packages/components/package.json{
"name": "@monorepo-example/components",
"version": "1.0.0",
"main": "lib/index.js",
"typings": "lib/index.d.ts" // generated by typescript compiler
}

Very similar to what we did when creating the monorepo-example/core , add the typescript package, create a tsconfig.json file, set its input dir to src and its output dir to lib and make it generate .d.ts files.

Since we will use React in this package, you will need to:

  • Set the option "jsx": "react" in your tsconfig.json file.
  • Install react and react-dom
  • Install the required types: yarn add --dev @types/react @types/react-dom. (A great place to look for types of popular packages is this website.)

Now you can create your React components in Typescript:

Important: You must use .tsx extension if you use .jsx notation inside a file. (I usually do not use .jsx extension when I work in js-only projects. So I thought I could do the same with Typescript and I spent some time trying to find why my project was not compiling…).

Creating the monorepo-example/webapp package:

The webpack package will consume both the core and the components packages. Also, since it is a web app, I will use webpack to generate an optimised bundle.

To make the babel+Typescript setup, I used this tutorial. That's a great tutorial and the author also explains the pros and cons of doing the typescript setup using @babel/preset-typescript instead of webpack loaders ( ts-loader or awesome-typescript-loader).

Create the webapp directory inside the packages directory and add the following package.json file:

// monorepo/packages/webapp/package.json
{
"name": "webapp",
"version": "1.0.0"
}

Add the following dependencies:

react
react-dom

Add the following devDependencies:

@babel/core
@babel/preset-typescrip
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-object-rest-spread
babel-loader
webpack
webpack-cli
typescript
@types/react
@types/react-dom

To configure babel, create the following babel.config.js :

Setting Webpack:

Different from the core and components package, in this package we will use webpack. Why? Because it makes easier to create an optimised bundle (something required since this bundle is what we will send to the clients). The other packages are not served directly to the client, so there is no need to optimise their outputs.

Here is the minimal webpack.config.js that allowed me to set up Typescript+webpack. The only point of attention is that even if your project is Typescript-only, you will probably need to add .js and .jsx to the resolve.extensions array. If you do not set this, webpack will probably fail to resolve some imports from your node_modules that are in js.

Consuming local packages:

In order to consume local packages, simply add them as dependencies in your package.json file. Pay attention to the version, otherwise the dependency will be installed from Github rather than linked from your local filesystem.

// monorepo/packages/webapp/package.json
{
...,
"dependencies": {
...,
"@monorepo/core": "1.0.0",
"@monorepo/components": "1.0.0"
},
...,
}

With this, you will be able to import stuff (and their types) from your local packages with a simply:

import { AwesomeComponent } from '@monorepo/components';

Setting up Lerna

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

From the yarn workspaces docs: Yarn’s workspaces are the low-level primitives that tools like Lerna can (and do!) use.

So, Lerna is a layer of functionalities that works above yarn workspaces.

To setup Lerna, you can install lerna globally using npm install -g lerna and run a lerna init on the root of your monorepo. This will create a lerna.json file with the lerna config.

Modify your lerna.json file in order to instruct lerna to use Yarn workspaces:

// monorepo/lerna.json
{
"packages": [
"packages/*"
],
"version": "0.0.0",
"npmClient": "yarn",
"useWorkspaces": true
}

What can I do with lerna?

There is a lot of stuff that you can do with lerna. I described here only the functionalities that I use. I recommend checking out the full command list in the docs.

  1. You can run commands that are common between your packages with a single command:
# Run `yarn run test` in every package that has this script
$ lerna run test --stream
# Build all packages
$ lerna run build

2. You can remove all node_modules/ using lerna clean .

3. You can use lerna exec ... to run an arbitrary command in each package.

Can I run a command across more than one workspace without Lerna?

Yes. Yarn workspaces have a command yarn workspaces run <cmd> that runs the chosen yarn command in each workspace. Docs here.

The problem with it is that it tries to run the command in all packages without checking if the package is able to run it. Lerna does not have this problem since it runs the command only in the packages that have it. So, Lerna probably scales better if you have a lot of workspaces.

Want to see the full code?

Hope you liked! Don't hesitate to contact me if you have any question!

Find me on Twitter: @miltontakamura
Find me on Github: miltoneiji

I like to learn and build stuff