Easy, step by step guide to Monorepo Architecture using Lerna, Yarn workspace and React Workspace.

Bijay Shrestha
10 min readAug 13, 2019

--

Just so that no frontend developers ever have to wander and suffer and act fool and pretend smart while researching any frontend architecture, this article is an ode to my frustrations, confusions and treacherous journey to finally being able to figure out a stable, scalable, un-opinionated and flexible frontend architecture.

TL;DR

A tool for managing JavaScript projects with multiple packages.

So, when it comes to managing large scale codebases, developed around a heavy-weight application ecosystem; splitting the application into a separate, independently versioned packages or projects becomes an imminent and reasonable engineering decision.

That’s where Lerna thrives.

Simply put, and at the very core, lerna is a monorepo orchestrating tool that will help us bootstrap, build, clean, test and release all our multi-purpose packages/ repositories under a beautiful abstraction of single -repository (that’s why the term mono).

Meaning : with bare-minimum hassle and quite interestingly with ONE SINGLE COMMAND, you can iterate through all or any particular package or repository for regular development operations such as building, linting, testing, publishing, et cetera.

A small snippet of a mono-repo architecture (under lerna) down below should give you a bird-eye-view understanding of what I meant when I stated ‘Lerna thrives’ in a multi-package environment under an abstraction of single-repository.

lerna-repo/                                     #mono or single-repo
lerna.json
package.json
packages/
package-1/
package.json
package-2/
package.json

...

shared-ui-components/
package.json
component-x/

package.json
component-xx/
package.json
...

On the same note, besides optimizing the workflow around managing and publishing multi-package repositories with Git and npm; lerna, also enables code sharing right out-of-the-box, where packages can be reused in the form of an independently maintained npm packages and that also without ever having to publish them.

For instance: in the same snippet above — with the help of lerna, all our ui-components created underneathshared-ui-components package can now be easily shared across other main packages such aspackage-1or package-2 inside packages folder.

lerna-repo/                                     
lerna.json
package.json
packages/
package-1/
package.json
package-2/
package.json

...

shared-ui-components #reusable ui-components package
component-x
package.json
component-xx
package.json
package.json
  • [Side note: Projects like React, Babel, Jest, Ember, Meteor and Angular also use a monorepo architecture to leverage its out-of-the-box multi-package management utility function.]

Prerequisites for Lerna

Install Lerna CLI:

# Using yarn
yarn global add lerna
# or npm
npm i -g lerna #installing lerna globally

You can initialize Lerna configby running the command below from your root folder, which in our case it islerna-repo:

$ mkdir lerna-repo && cd lerna-repo
$ lerna init //initializing lerna

You’ll get lerna.json file and packages/ folder in your project folder.

lerna-repo/
package.json
lerna.json
packages/

Open yourlerna.json and it should look something like:

{
"packages": [
"packages/*"
],
"version": "1.0.0"
} //lerna.json

and, your root’spackage.json as:

{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.15.0"
}
} //root's package.json
Yarn workspaces

Yarn is a package manager in the likes of Node Package manager(npm). and Yarn Workspace, well:

Standard Definition says,

Workspaces are a new way to setup your package architecture that’s available by default starting from Yarn 1.0. It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass. (src: Yarn Workspace)

Meaning: Yarn Workspace of all other goodies it brings along, this tool helps us efficiently take care of dependency management process in all our node projects. Technically dissecting — using Yarn with Workspaces enabled allows us to install dependencies from multiple package.json files across all sub-folders or multi-repositories under a single root folder’s node_moduleswithout duplication.

  • [Side note: The package.json file found inside an app defines what libraries will be installed into node_modules when you run npm install or yarn install or yarn. Thus, node_modules folder contains libraries downloaded from npm or yarn.]

Now, after this brief enlightenment, let’s get our hands dirty for yarn workspaces implementation.

Prerequisites for Yarn Workspace

First make sure yarn is installed into your system, otherwise follow the Yarn installation documentation to install this package manager depending on your system’s OS. Then, you need to add yarn as your npmClient and set useWorkspaces to true in lerna.json located inside your root folder.

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

To enable Yarn Workspace, add the following in your root'spackage.json file. Starting from now on, we’ll call this directory the “root workspace”:

{                                                //root package.json
...
"private": true,
"workspaces": ["package-1", "package-2"]
}
or{
"private": true,
"workspaces": [
"packages/**"
]
}
// Wherein packages/** refer to all the prospective repositories or components we'll be creating in our mono-repo's packages folder
  • [Sidenote: Notice "private": true, because, workspaces generally speaking aren’t meant to be published.]

Moving on, let’s create two child apps (i.e., Create React App projects) inside the packages folder, we’ll name them internet-banking and mobile-bankingrespectively.

$ cd packages 
$ npx create-react-app internet-banking
$ npx create-react-app mobile-banking

Add the following scripts in your root workspace’s package.json . I’ll explain their use cases later.

...
"scripts": {
"internet": "cd packages/internet-banking && yarn start",
"mobile": "cd packages/mobile-banking && yarn start",
"build": "lerna run build",
"clean": "yarn clean:artifacts && yarn clean:packages && yarn clean:root",
"clean:artifacts": "lerna run clean --parallel",
"clean:packages": "lerna clean --yes",
"clean:root": "rm -rf node_modules"
}

Additionally, from your root workspace direcotry, i.e., lerna-repo, add react and react-dom packages too as a dev dependency.

$ yarn add --dev -W react react-dom

As of now, your root workspace’s package.json should look something like:

{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.15.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"workspaces": [
"packages/**"
],
"scripts": {
"internet": "cd packages/internet-banking && yarn start",
"mobile": "cd packages/mobile-banking && yarn start",
"build": "lerna run build",
"clean": "yarn clean:artifacts && yarn clean:packages && yarn clean:root",
"clean:artifacts": "lerna run clean --parallel",
"clean:packages": "lerna clean --yes",
"clean:root": "rm -rf node_modules"
}
}

If you are curious already, execute the command below from your workspace root directory to check if everything is working fine:

$yarn internet         // should boot up a CRA short for Create React App project of internet-banking app you had previously created.$ yarn mobile// should boot up CRA project of mobile-banking app you had created.$yarn build//should build the two CRA projects.$yarn clean// should execute three commands yarn clean:artifacts, yarn clean:packages and yarn clean:root respectively
  • [Side note: After executing yarn clean , you must bootstrap the app with yarn install or yarn command before executing any other scripts. For more scripts you can click here.]

Now, let’s create a ui-components folder inside packages,and executeyarn init command right away:

$ cd packages && mkdir ui-components && cd ui-components && yarn init -y
  • [Sidenote: yarn init -y command will prepare package.jsoninside ui-components with default package.json attributes]
{
"name": "ui-components",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
} //package.json inside ui-components

For the fun, let’s modify package.json a tiny bit as

{
"name": "@batman/ui-components",
"main": "index.js",
"version": "1.0.0",
"license": "MIT"
}
  • [Sidenote: @batman/ui-components will be a package of reusable UI components. And, also, from now onwards ui-components directory is identified as @batman/ui-components]

If everything is going right, this will take us to our next step. That is, inside ui-components folder create new files and subfolders in the following format:

ui-components
package.json
index.js
src/
Button/ //Button subfolder
Button.js
package.json

InputText/ //InputText subfolder
InputText.js
package.json

For our small sample demonstration let’s create Button.js inside Button/ subfolder (which should be again inside src folder as shown above):

Button.js

import React from 'react';


export const Button = () =>{
return (
<>
<button>Reusable Button</button>
</>);
};

and its respective,package.json as

{
"name": "@batman/button",
"main": "Button.js",
"version": "1.0.0",
"license": "MIT",
"private": true
}

Similarly, createInputText.js inside InputText/ subfolder (which is also insde src folder) as:

InputText.js

import React from 'react';

export const InputText = ()=>{
return (
<>
<input type="text" placeholder="Enter anything"/>
</>
);
};

and, its correspondingpackage.json as:

{
"name": "@batman/input-text",
"main": "InputText.js",
"version": "1.0.0",
"license": "MIT",
"private": true
}

Finally, referring to the previous ui-components folder format, which was:

ui-components
package.json
index.js
src/
Button/ //Button subfolder
Button.js
package.json
InputText/ //InputText subfolder
InputText.js
package.json

Createindex.js under ui-components folder as:

index.js

import {Button} from './src/Button';
import {InputText} from './src/InputText';

export {
Button,
InputText
}

Having done all this, let’s add dependency of ui-components in internet-banking package, so that you can reuse all the components that you’re likely to create inside ui-components like the few we just created (for example: Button.js, InputText.js)

To add the dependency, open package.json of internet-banking project and add

"@batman/ui-components": "1.0.0"

as it’s dependency.

  • [Sidenote: Remember, ui-components ‘s package name and version in it’s package.json must match it’s dependency signature in internet-banking's package.]

Your internet-banking’s dependencies JSON should look something like:

..."dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"@batman/ui-components": "1.0.0"

}
// "<name>":"<version>" of ui-component's package.json

Now, from your root workspace folder, run:

$ lerna bootstrap or $ yarn

This will automatically create symlinks of your linked components.

Let’s see if we’ve successfully made the ui-components ‘s elements like InputText and Button reusable inside internet-banking package.

To test this, navigate to App.js file of internet-banking package and replace the entire file’s content with this short, fun code-snippet:

import React from 'react';
import {Button, InputText} from "@batman/ui-components";
import logo from './logo.svg';
import './App.css';

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo"/>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<Button/>
<InputText/>

</header>

</div>
);
}

export default App;

And, from your root workspace directory, i.e., lerna-repo, run $ yarn internet to boot up internet-banking package.

  • [Sidenote: Remember, I assured to explain about scripts we had previously written in our root workspace’s package.json.]
"scripts": {
"internet": "cd packages/internet-banking && yarn start",
"mobile": "cd packages/mobile-banking && yarn start",
"build": "lerna run build",
"clean": "yarn clean:artifacts && yarn clean:packages && yarn clean:root",
"clean:artifacts": "lerna run clean --parallel",
"clean:packages": "lerna clean --yes",
"clean:root": "rm -rf node_modules"
}
  • [Sidenote: yarn internet command from root workspace boots up internet-banking CRA project.]

Good chances are you’ll receive a Failed to compile error on your browser:

Babel Transpiration Error

Don’t you worry! Basically, what’s happening is, the internet-banking package is not capable enough yet, to transpile external JSX element-components (like Button and InputText) we had imported from ui-components into our App.js of internet-banking CRA project.

Well, this is where React Workspace delivers!

React Workspaces

Prerequisites of React Workspaces

  • Yarn 1.13.0
  • Node 11.14.0

Now, at the time of writing this blog post, Official React, yet, does not support monorepo architecture. However, with a well-supported and customizedreact-scripts provided by React Workspaces, we can achieve this fundamental shortcoming of official React in no time.

Thus, without further ado, to fire up React workspaces on all smokes and cylinder, go to your root workspace folder and install React Workspaces as dev dependency:

$ yarn add --dev -W @react-workspaces/react-scripts

And then, remove react-scripts dependencies from bothinternet-banking and mobile-banking projectspackage.json as:

                                    #internet-banking's package.json
{
"name": "internet-banking",
"module": "src/index.js",
"version": "0.1.0",
"private": true,
"dependencies": {
"̵r̵e̵a̵c̵t̵"̵:̵ ̵"̵^̵1̵6̵.̵8̵.̵6̵"̵,̵
"̵r̵e̵a̵c̵t̵-̵d̵o̵m̵"̵:̵ ̵"̵^̵1̵6̵.̵8̵.̵6̵"̵,̵
"̵r̵e̵a̵c̵t̵-̵s̵c̵r̵i̵p̵t̵s̵"̵:̵ ̵"̵3̵.̵0̵.̵1̵"̵,̵

"@batman/ui-components": "1.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

and,

                                         #mobile-banking's package.json
{
"name": "mobile-banking",
"version": "0.1.0",
"private": true,
"dependencies": {
"̵r̵e̵a̵c̵t̵"̵:̵ ̵"̵^̵1̵6̵.̵8̵.̵6̵"̵,̵
"̵r̵e̵a̵c̵t̵-̵d̵o̵m̵"̵:̵ ̵"̵^̵1̵6̵.̵8̵.̵6̵"̵,̵
"̵r̵e̵a̵c̵t̵-̵s̵c̵r̵i̵p̵t̵s̵"̵:̵ ̵"̵3̵.̵0̵.̵1̵"̵,̵

},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

After that create a .env file in internet-banking as well as mobile-banking's projects and set SKIP_PREFLIGHT_CHECK to true as:

SKIP_PREFLIGHT_CHECK=true

And lastly change package.json configuration of ui-components as:

{
"name": "@batman/ui-components",
"version": "1.0.0",
"main": "index.js",
"main:src": "index.js",
"license": "MIT",
"private": true
}
#i.e., add main:src attribute that'd point to the main file of ui-components.

Finally, finally, from your root workspace fire up yarn clean && yarncommand to clean and later install plus link dependencies:

$ yarn clean && yarn

And, now, the time has eventually arrived —

Fire up:

$yarn internet

and you’ll have your reusable ui-components displayed on your browser with no errors:

Reusable components displayed with no error.

Voila!

We’ve finally created a full-fleged monorepo architecture for React Projects. Follow the link to access the Github repository of our small lerna-repo.

Lastly, thank you for your patience!

--

--

Bijay Shrestha
Bijay Shrestha

No responses yet