Yet another monorepo setup tutorial
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 thecomponents
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 yourtsconfig.json
file. - Install
react
andreact-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.
- 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