

Monorepos with Yarn Workspaces
source link: https://wanago.io/2024/03/11/yarn-workspaces-monorepo/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


March 11, 2024
With monorepos, we keep the code for different projects together in one big repository. This makes sharing and reusing the code across multiple projects easier and can simplify dependency management.
Each project has its own directory, scripts, and dependencies. We can use Yarn workspaces designed with monorepos in mind to handle all that. In this article, we explore the principles behind Yarn workspaces and learn how to use them.
If you want to see out the full code from this article, check out this repository.
Introducing Yarn Workspaces
A workspace is a package within a larger multi-package monorepo. Each workspace is a standalone unit that can define its dependencies and scripts.
Yarn is an alternative to NPM. While NPM supports workspaces as well, it was Yarn that introduced them.
First, we need to install Yarn.
npm install --global yarn |
Now, we must create an appropriate directory structure.
├── apps │ └── articles-manager-api ├── libraries │ └── logger ├── package.json └── tsconfig.json |
In our case, we are creating a very straightforward API to manage articles. We also want to create a simple logger library to log information to the terminal.
We start by creating a package.json file in the workspace root directory.
package.json
"name": "workspaces-typescript", "version": "1.0.0", "license": "MIT", "private": true, "workspaces": [ "apps/*", "libraries/*" |
A few important things are going on above. First, we must mark the package.json in the workspace root as private. Thanks to that, NPM will refuse if we try to publish it by accident.
We also have to point to all our workspaces. We could list them all separately like this:
"workspaces": [ "apps/articles-manager-api", "libraries/logger" |
However, we would have to modify this list whenever we add another workspace to the apps or libraries directories. Thanks to using apps/* and libraries/*, Yarn immediately recognizes all new projects.
Creating a reusable library
We have to create separate package.json files for all workspaces in our monorepo.
├── libraries │ └── logger │ ├── index.ts │ └── package.json |
While we could also mark the libraries as private, Yarn doesn’t force us to. We could publish individual libraries to NPM if we want to.
libraries/logger/package.json
"name": "@wanago.io/logger", "version": "1.0.0", "main": "index.ts", "license": "MIT", "dependencies": { "cli-color": "^2.0.4" "devDependencies": { "@types/cli-color": "^2.0.6" |
What’s important is that it makes sense to define a prefix for all our libraries, such as @wanago.io/. Thanks to that, the names of our libraries won’t collide with packages published to NPM.
We also should define the primary entry point of our library through the main property in our package.json.
Our logger library is straightforward and focuses on logging messages into the console. The color of the notification depends on the severity of the log.
libraries/logger/index.ts
import { bgBlue, bgYellow, bgRed } from 'cli-color'; export function logInfo(message: string) { console.log('INFO:', bgBlue(message)); export function logWarning(message: string) { console.warn('WARNING:', bgYellow(message)); export function logError(message: string) { console.error('ERROR:', bgRed(message)); |
Using the library
Let’s create a simple app that uses our library.
├── apps │ └── articles-manager-api │ ├── Article.ts │ ├── index.ts │ ├── isNewArticleValid.ts │ └── package.json |
Since our articles-manager-api application is a workspace, it needs a separate package.json file.
apps/articles-manager-api/package.json
"name": "articles-manager-api", "version": "1.0.0", "main": "index.ts", "license": "MIT", "dependencies": { "ts-node": "^10.9.2", "typescript": "^5.4.2", "express": "^4.18.3", "@wanago.io/logger": "*" "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.11.25" "scripts": { "start": "ts-node ./index.ts" |
What’s important is that above, we use "@wanago.io/logger": "*". This means that we want articles-manager-api to always use the latest version of the @wanago.io/logger library.
apps/articles-manager-api/index.ts
import { logInfo, logWarning } from '@wanago.io/logger'; import express from 'express'; import { Article } from './Article'; import { isNewArticleValid } from './isNewArticleValid'; let currentArticleId = 0; const articles: Article[] = []; const app = express(); app.use(express.json()); app.get('/articles', (request, response) => { logInfo('GET /articles'); response.send(articles); app.post('/articles', (request, response) => { logInfo('POST /articles'); if (!isNewArticleValid(request.body)) { logWarning(`Invalid article ${JSON.stringify(request.body)}`); return response.sendStatus(400); const newArticle: Article = { id: ++currentArticleId, title: request.body.title, content: request.body.content articles.push(newArticle); response.send(newArticle); app.listen(3000); |
Above, we create a very simplistic API. If you want to know how to create a fully-fledged API with Node.js, check out the API with NestJS articles series.
Thanks to using it, our articles-manager-api can import the @wanago.io/logger library like any other package.
Installing the dependencies
What’s crucial is that we don’t have to install the dependencies of each workspace directly. Instead, we must go to the root of our monorepo and run yarn install only once. When we do that, Yarn installs the dependencies of all our workspaces at once.
What’s more, Yarn hoists all the dependencies to the top of the monorepo. This means that all of them are located in a single node_modules at the top of our monorepo. It makes the process of installing dependencies more efficient.
├── apps │ └── articles-manager-api │ ├── Article.ts │ ├── index.ts │ ├── isNewArticleValid.ts │ ├── node_modules │ │ └── .bin │ │ ├── ts-node -> ../../../../node_modules/ts-node/dist/bin.js │ │ ├── ... │ └── package.json ├── libraries │ └── logger │ ├── index.ts │ └── package.json ├── node_modules │ ├── @wanago.io │ │ └── logger -> ../../libraries/logger │ ├── express │ ├── cli-color │ ├── ... ├── package.json ├── tsconfig.json └── yarn.lock |
It is important to note that Yarn did not create a copy of the @wanago.io/logger library but created a link and put it into the node_modules at the root of our project. Thanks to that, we don’t have to run yarn install every time we make a change in the library.
It is also worth noticing that Yarn created a small node_modules directory in the articles-manager-api project. It contains the .bin directory with links to executable scripts such as ts-node. Thanks to that, when we run the yarn start command in the articles-manager-api, it can access the dependencies even though the actual packages are hoisted to the root node_modules.
apps/articles-manager-api/package.json
"scripts": { "start": "ts-node ./index.ts" |
Tools like Lerna
Yarn Workspaces efficiently handle dependencies and link packages. Tools such as Lerna that are designed to work with monorepos support Yarn Workspaces and add more features. For example, Lerna can generate changelog information, publish packages, and automatically increment versions of packages.
Lerna was created before Yarn introduced the Workspaces feature and used a linking mechanism to symlink packages together. However, it’s been a long time since Lerna started supporting Yarn Workspaces instead of doing the heavy lifting itself.
Summary
Monorepos can simplify code sharing and dependency management. In this article, we went through how Yarn Workspaces work and how they use the node_modules directory efficiently. Even if we want to move on to tools like Lerna, transitioning to other solutions is smoother if we have a foundational understanding of Yarn Workspaces. All of the above makes Yarn Workspaces a solid choice that allows us to handle multiple projects within a single repository.
Recommend
-
37
Monorepos are a hot topic for a discussion. There have been a lot of articles recently about why you should and shouldn’t use this type of architecture for your project, but most of them are biased in one way or another....
-
20
Workspace script runner Run npm scripts or custom commands in a yarn workspace Usage: wsrun [options] -c <command> [<arg1> <arg2> ...] Mode (choose one): --parallel, -a F...
-
20
Package Sorter A function used for monorepos production build. When you have projects depend on each other. You have to build core first, then the project depends on it and so on. You probably want...
-
14
Manage multiple package.json files, such as in Lerna Monorepos and Yarn/Pnpm Workspaces - JamieMason/syncpack
-
6
Monorepos and AWS Codebuild Feb 3, 2019 • Matt TylerTags: aws
-
6
Ionic Angular monorepos with NX This is part two of a
-
4
Monorepo Javascript Projects with Yarn Workspaces and Lerna Monorepo is a software development strategy in which a single repository contains code for multiple projects with shared dependencies. It has a...
-
3
-
5
Scalable Nuxt 3 Monorepos with PNPM Workspaces These days, many large companies are using a monorepo to store all their codebase. A monorepo is a single repository containing multiple distinct projects, with well-defined relation...
-
5
A Solution to Organizing a Monorepo
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK