Before jumping into what is monorepos and yarn workspaces let's try to understand the problem that we face.
Consider we have an application that has Frontend, backend, and a chrome extension.
The usual structure for it would be something like this
All the 3 separate repos have the same contracts, helper functions and have a common piece of code.
If we need to change a contract, then we'll have to make that change in all the repos.
The same holds good for any logic modification in any of the helper functions.
The problem that we are aiming to solve here is this mandatory change in all the repos for any kind of code modification.
Monorepo is a single repository that stores all the code for your application. Frontend, backend, extensions, common code, and contracts, etc.
Yarn workspaces let you organize your codebase and allows you to abstract out the major logic that might be used in various places or maintain correct contract throughout the app.
If this same application was built in a workspace, then it would be something like this:
TL'DR
We are gonna create "local" packages for any code that we want to abstract out and share and then use it like regular packages.
Let's get started with a simple setup:
Make a new folder
mkdir monorepo
cd monorepo
# create another folder inside the monorepo
mkdir packages
We'll be having all our code inside the packages folder and in a package.json
file we tell yarn about our workspace
touch package.json
package.json
{
"private": "true",
"workspaces": ["packages/*"]
}
By specifying packages/*
we tell where are our packages located.
Now, let's create a React app inside our packages
yarn create react-app web
And for our backend create another folder called server
mkdir server
cd server
yarn init # to create the package.json file
touch index.ts # create an index.ts file
Now let's create another folder utils
. This will serve as a common code/contracts that will be shared by our frontend and our backend application.
mkdir utils
cd utils
yarn init
mkdir src
cd src
touch index.ts
utils/src/index.ts
export const add = (a: number, b:number ) => {
return a + b;
}
The folder structure should be looking something like
monorepo
-- package.json
-- packages
-- utils
-- src
-- index.ts
-- package.json
-- server
-- index.ts
-- package.json
-- web
-- public
-- src
-- package.json
Go to the web
directory and add utils
as a dependency. The name should be the same as it is in the package.json of utils.
Now, install the dependencies again for web.
Go to your App.js
and import add()
that we defined in the utils
// ...
import { add } from "utils";
function App() {
return (
<div className="App">
<header className="App-header">
<p>
{add(2,3)}
</p>
</header>
</div>
);
}
export default App;
Now, run the start the dev environment for the frontend with
yarn workspace web start
The syntax is yarn workspace [project-name] start
The project name is to be picked up from the package.json of the project.
Go to localhost:3000, you'll be able to see the result 5
Awesome !!! This part is working all good.
Let's head to the backend now.
Go to the server directory and add utils as a dependency in package.json, similar to how we did in web.
Now go to the index.ts file and add this code.
import { add } from "utils";
console.log("This is the server file, ", add(1,2))
Lets run it
node index.ts
You'll be able to see your console from the commonFunction as well as the console from index.
And just like that you have shared a piece of code among two sub-projects.
Improvements
You can name all the sub-projects in a better way in their respective package.json
utils
→@monorepo/utils
web
→@monorepo/web
server
→@monorepo/server
For starting the dev environments, in the package.json in monorepo directory add these scripts
{
"private": true,
"name": "monorepo",
"workspaces": ["packages/*"],
"scripts": {
"client": "yarn workspace @monorepo/web start",
"server": "yarn workspace @monorepo/server start",
}
}
Another advantage of this codebase structure is that there is only 1 node_modules created at the top level.
If you open the node_modules then you'll see a package called @monorepo
which would contain all the packages that are inside your packages folder.
Another tool that would be helpful here is Lerna.
In monorepo dir's package.json, add Lerna as a dependency and add another script
"build": "lerna run build"
Now, add a lerna.json
file
{
"lerna": "2.11.0",
"packages": ["packages/*"],
"version": "0.0.0",
"npmClient": "yarn",
"useWorkspaces": true
}
To run with lerna, before using the packages run the script yarn build
and lerna will register the available packages.
To set up the initial structure you can also use lerna init
And just like that, now you have a better codebase for your project !!!
You can play around with the code in this github repo