Let’s dive into a key aspect of frontend engineering: Monorepo and Yarn Workspaces. These tools are game-changers for managing large frontend projects, allowing you to house multiple related projects in a single repository, share code, unify dependencies, and maintain development efficiency. Monorepo tackles the pain points of multi-project collaboration, while Yarn Workspaces simplifies dependency management and builds.
What is a Monorepo?
A Monorepo (Monolithic Repository) is a single repository containing multiple projects, as opposed to separate repositories for each project. For example, if your team has a web app, a component library, and a utility package, the traditional approach requires separate repositories, making version synchronization, code sharing, and CI/CD cumbersome. A Monorepo consolidates these projects for unified management. Key benefits include:
- Code Sharing: Multiple projects reuse utility functions, components, and configurations.
- Unified Dependencies: All projects use consistent dependency versions, avoiding conflicts.
- Simplified Builds: One repository runs a single CI/CD pipeline, improving efficiency.
- Easier Collaboration: Teams work in one repository, reducing communication overhead.
Yarn Workspaces, a feature of Yarn, is designed for Monorepos, managing dependencies, scripts, and builds across multiple packages. It treats each sub-project as an independent NPM package, sharing a root-level node_modules and enabling cross-project commands.
Environment Setup
To use Monorepo and Yarn Workspaces, you’ll need Node.js (18.x recommended) and Yarn (1.22.x at the time of writing). We’ll create a Monorepo with a React app, a Vue component library, and a shared utility package.
Install Yarn:
npm install -g yarn
yarn --version
Create a project:
mkdir monorepo-demo
cd monorepo-demo
yarn init -y
Initializing the Monorepo Structure
The core of a Monorepo is housing multiple projects in one repository. We’ll create three sub-projects: a React app, a Vue component library, and a shared utility package.
Configuring Yarn Workspaces
Update package.json:
{
"name": "monorepo-demo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
]
}
The workspaces field tells Yarn that sub-projects are in the packages/ directory, each treated as an independent NPM package.
Create the directory structure:
mkdir -p packages/webapp packages/component-lib packages/utils
Directory structure:
monorepo-demo/
├── packages/
│ ├── webapp/ # React app
│ ├── component-lib/ # Vue component library
│ ├── utils/ # Shared utility package
├── package.json
Initializing Sub-Projects
Create package.json for each sub-project.
packages/webapp/package.json:
{
"name": "@monorepo/webapp",
"version": "1.0.0",
"private": true
}
packages/component-lib/package.json:
{
"name": "@monorepo/component-lib",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js"
}
packages/utils/package.json:
{
"name": "@monorepo/utils",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js"
}
@monorepo/ is a namespace to distinguish packages. main and module specify build outputs.
Run yarn install to create a shared node_modules in the root, with sub-projects referencing each other via @monorepo/*.
Shared Utility Package: utils
Create a utility package for use in React and Vue projects. Add packages/utils/src/index.js:
export function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
export function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
Use Rollup to build the utils package:
yarn global add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs
Create packages/utils/rollup.config.js:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.js',
format: 'cjs'
},
{
file: 'dist/index.esm.js',
format: 'esm'
}
],
plugins: [resolve(), commonjs()]
};
Update packages/utils/package.json with scripts:
{
"scripts": {
"build": "rollup -c"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0"
}
}
Run yarn workspace @monorepo/utils build to generate dist/index.js and dist/index.esm.js.
React App: webapp
Initialize a React project with Create React App:
cd packages/webapp
npx create-react-app .
Update packages/webapp/src/App.js to use the utils package:
import { formatCurrency, truncateText } from '@monorepo/utils';
function App() {
const price = 1234.56;
const text = 'This is a very long product description that needs truncation';
return (
<div style={{ padding: 20 }}>
<h1>React Webapp</h1>
<p>Price: {formatCurrency(price)}</p>
<p>Description: {truncateText(text, 20)}</p>
</div>
);
}
export default App;
Update packages/webapp/package.json to add dependencies:
{
"dependencies": {
"@monorepo/utils": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Run yarn install, and Yarn links @monorepo/utils automatically via Workspaces. Run yarn workspace @monorepo/webapp start to see:
- Price: $1,234.56
- Description: This is a very long...
Dynamic Component Loading
Use React lazy loading with utils. Create packages/webapp/src/ProductCard.js:
import { formatCurrency } from '@monorepo/utils';
function ProductCard({ product }) {
return (
<div style={{ border: '1px solid', padding: 10, margin: 10 }}>
<h3>{product.name}</h3>
<p>{formatCurrency(product.price)}</p>
</div>
);
}
export default ProductCard;
Update App.js:
import { Suspense, lazy } from 'react';
import { formatCurrency, truncateText } from '@monorepo/utils';
const ProductCard = lazy(() => import('./ProductCard'));
function App() {
const price = 1234.56;
const text = 'This is a very long product description that needs truncation';
const product = { name: 'Laptop', price: 999.99 };
return (
<div style={{ padding: 20 }}>
<h1>React Webapp</h1>
<p>Price: {formatCurrency(price)}</p>
<p>Description: {truncateText(text, 20)}</p>
<Suspense fallback={<p>Loading...</p>}>
<ProductCard product={product} />
</Suspense>
</div>
);
}
export default App;
Run the app. ProductCard loads lazily, using formatCurrency from utils to display product information.
Vue Component Library: component-lib
Initialize a Vue project with Vue CLI:
cd packages/component-lib
vue create .
Select Vue 3 with default settings. Create packages/component-lib/src/components/Button.vue:
<template>
<button class="btn" @click="$emit('click')">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'AppButton'
};
</script>
<style scoped>
.btn {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
</style>
Create packages/component-lib/src/index.js:
import Button from './components/Button.vue';
import { formatCurrency } from '@monorepo/utils';
export { Button, formatCurrency };
Use Rollup to build:
Create packages/component-lib/rollup.config.js:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import vue from 'rollup-plugin-vue';
export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.js',
format: 'cjs'
},
{
file: 'dist/index.esm.js',
format: 'esm'
}
],
plugins: [resolve(), commonjs(), vue()],
external: ['vue', '@monorepo/utils']
};
Update packages/component-lib/package.json:
{
"scripts": {
"build": "rollup -c"
},
"dependencies": {
"@monorepo/utils": "^1.0.0",
"vue": "^3.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"rollup-plugin-vue": "^6.0.0"
}
}
Run yarn workspace @monorepo/component-lib build to generate the Vue component library.
Using Vue Components in React
Using Vue components directly in React requires additional setup (not typically done), but we can reuse utils. Update packages/webapp/src/App.js:
import { formatCurrency, truncateText } from '@monorepo/utils';
function App() {
const price = 1234.56;
const text = 'This is a very long product description that needs truncation';
return (
<div style={{ padding: 20 }}>
<h1>React with Shared Utils</h1>
<p>Price: {formatCurrency(price)}</p>
<p>Description: {truncateText(text, 20)}</p>
</div>
);
}
export default App;
This demonstrates utils reuse; direct use of Vue’s Button would require further configuration.
Cross-Project Dependency Management
Yarn Workspaces automatically handles sub-project dependencies. Update packages/webapp/package.json:
{
"dependencies": {
"@monorepo/utils": "^1.0.0",
"@monorepo/component-lib": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Run yarn install, and Yarn links @monorepo/utils and @monorepo/component-lib to webapp’s node_modules without manual publishing.
Unified Dependency Versions
Add resolutions to the root package.json:
{
"name": "monorepo-demo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"resolutions": {
"react": "^18.2.0",
"vue": "^3.2.0"
}
}
This ensures all sub-projects use the same versions of React and Vue, preventing conflicts.
Unified Scripts
Yarn Workspaces supports cross-project scripts. Update the root package.json:
{
"scripts": {
"build": "yarn workspaces foreach -p run build",
"start:webapp": "yarn workspace @monorepo/webapp start",
"build:utils": "yarn workspace @monorepo/utils build"
}
}
-
yarn build: Builds all sub-projects in parallel. -
yarn start:webapp: Starts the React app. -
yarn build:utils: Builds the utils package.
Run yarn build to build all sub-projects efficiently.
Real-World Scenario: E-commerce Monorepo
Extend the project to simulate an e-commerce scenario with a frontend app, component library, and admin dashboard.
Admin Dashboard
Create packages/admin:
cd packages
npx create-react-app admin
Update packages/admin/src/App.js:
import { formatCurrency } from '@monorepo/utils';
function App() {
const product = { name: 'Laptop', price: 999.99 };
return (
<div style={{ padding: 20 }}>
<h1>Admin Dashboard</h1>
<p>Product: {product.name}</p>
<p>Price: {formatCurrency(product.price)}</p>
</div>
);
}
export default App;
Update packages/admin/package.json:
{
"name": "@monorepo/admin",
"version": "1.0.0",
"dependencies": {
"@monorepo/utils": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Run yarn workspace @monorepo/admin start to display the admin dashboard, reusing utils.
Product List Component
Add ProductList.vue to component-lib:
packages/component-lib/src/components/ProductList.vue:
<template>
<div>
<h3>Product List</h3>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} - {{ formatCurrency(product.price) }}
</li>
</ul>
</div>
</template>
<script>
import { formatCurrency } from '@monorepo/utils';
export default {
name: 'ProductList',
props: {
products: Array
},
methods: {
formatCurrency
}
};
</script>
Update packages/component-lib/src/index.js:
import Button from './components/Button.vue';
import ProductList from './components/ProductList.vue';
import { formatCurrency } from '@monorepo/utils';
export { Button, ProductList, formatCurrency };
Rebuild component-lib:
yarn workspace @monorepo/component-lib build
Using Component Library in Vue App
Create packages/vue-app:
cd packages
vue create vue-app
Update packages/vue-app/src/App.vue:
<template>
<div style="padding: 20px;">
<h1>Vue App</h1>
<ProductList :products="products" />
<AppButton @click="addProduct">Add Product</AppButton>
</div>
</template>
<script>
import { ProductList, Button as AppButton } from '@monorepo/component-lib';
export default {
components: { ProductList, AppButton },
data() {
return {
products: [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 499.99 }
]
};
},
methods: {
addProduct() {
this.products.push({ id: this.products.length + 1, name: 'New Item', price: 99.99 });
}
}
};
</script>
Update packages/vue-app/package.json:
{
"name": "@monorepo/vue-app",
"version": "1.0.0",
"dependencies": {
"@monorepo/component-lib": "^1.0.0",
"vue": "^3.2.0"
}
}
Run yarn workspace @monorepo/vue-app serve to display the product list. Clicking the button adds products, with formatCurrency formatting prices.
Performance Testing
Test Monorepo performance with Lighthouse. Build webapp:
yarn workspace @monorepo/webapp build
Deploy webapp/dist:
npx serve packages/webapp/dist
Lighthouse results:
- Single Repository: All dependencies bundled, ~1MB bundle.
- Monorepo: Utils and component-lib bundled separately, main webapp bundle ~500KB, dynamic loading of component-lib, FCP ~0.6s.
Workspaces reduce duplicate dependencies, significantly improving load times.
Conclusion (Technical Details)
Monorepo and Yarn Workspaces streamline multi-project management. The examples demonstrated:
- Initializing a Monorepo with Yarn Workspaces.
- Sharing a utils package across React and Vue.
- Building a Vue component library for cross-project use.
- Unified scripts and dependency management.
- An e-commerce scenario with frontend, admin dashboard, and component library.
Run these examples, experiment with cross-project sharing and dynamic loading, and experience the power of Monorepos!
Top comments (0)