Easy, step by step guide to Monorepo Architecture using Lerna, Yarn workspace and React Workspace.
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
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-1
or 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 config
by 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 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_modules
without duplication.
- [Side note: The
package.json
file found inside an app defines what libraries will be installed intonode_modules
when you runnpm 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-banking
respectively.
$ 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 withyarn 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 preparepackage.json
inside ui-components with defaultpackage.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 onwardsui-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 packagename
andversion
in it’spackage.json
must match it’s dependency signature ininternet-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’spackage.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:
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!
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 && yarn
command 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:
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!