[Monoepo] Dependency Hoisting of Yarn Workspaces

2024-04-13 hit count image

Let's see how Hoisting works when adding dependencies to each project when configuring a Monorepo using Yarn's Workspaces.

Outline

In this blog post, I will introduce how Hoisting that occurs when adding dependencies (libraries) to each project when configuring a Monorepo using Yarn’s Workspaces.

Blog Series

This blog is a series. Please check other blog posts through the following links.

NodeJS Module Resolution

When importing modules using import in NodeJS, it first checks the node_modules of the root folder, and if the module is not found, it checks the parent’s node_modules, and repeats this process until it checks the node_modules of the top folder.

In this blog post, I will introduce the Hoisting that uses the module loading process of NodeJS.

Hoisting of Dependencies

In configuring a Monorepo using Yarn’s Workspaces, when adding dependencies to each project, the dependencies are hoisted and installed in the top folder to efficiently manage dependencies.

The Hoisting of dependencies in Yarn’s Workspaces has the following advantages.

  • Dependencies that are commonly used in each project are installed only once to save memory.
  • By separating dependencies of the same version and different versions, it prevents dependency version conflicts.
  • When installing the same dependency, it is installed only once, so it can be installed quickly, and if the already installed dependency is used, it is not installed additionally, so caching effect can be obtained.

Example

Let’s create an example to check the Hoisting of dependencies. First, create the folder and file structure as follows.

.
├── package.json
└── src
    ├── module-a
    │   ├── index.js
    │   └── package.json
    └── module-b
        ├── index.js
        └── package.json

The package.json of module-a is as follows.

// src/module-a/package.json
{
  "name": "module-a",
  "version": "1.0.0",
  "main": "index.js"
}

The package.json of module-b is as follows.

// src/module-b/package.json
{
  "name": "module-b",
  "version": "1.0.0",
  "main": "index.js"
}

And the index.js of module-b is as follows.

// src/module-b/index.js
console.log('module-b');

The index.js of module-a is as follows.

// src/module-a/index.js
console.log('module-a');

require('module-b');

Lastly, modify the package.json file in the root folder as follows.

{
  "name": "monorepo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "workspaces": {
    "packages": ["src/*"]
  }
}

You can see that the packages is set to workspaces in the package.json to use Yarn’s Workspaces. For more information on Yarn’s Workspaces, please refer to the following blog post

Dependency Installation

Now, let’s install the dependencies.

yarn install

And then, you can see the Symlink of module-a and module-b in the node_modules as follows.

.
├── node_modules
│   ├── module-a -> ../src/module-a
│   └── module-b -> ../src/module-b
├── package.json
├── src
│   ├── module-a
│   │   ├── index.js
│   │   └── package.json
│   └── module-b
│       ├── index.js
│       └── package.json
└── yarn.lock

Adding Dependencies

Now, let’s add the dependency to module-a to check Hoisting. Open the package.json file of module-a in src/module-a/package.json and modify it as follows.

{
  "name": "module-a",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "lodash": "^3"
  }
}

Then, run the following command to install the dependencies.

yarn install

Then, you can see that the lodash is installed and Hoisted in the node_modules of the root folder.

.
├── node_modules
│   ├── lodash
│   ├── module-a -> ../src/module-a
│   └── module-b -> ../src/module-b
├── package.json
├── src
│   ├── module-a
│   └── module-b
└── yarn.lock

Using Hoisted Dependencies

To see if you can use the Hoisted dependencies, open the src/module-a/index.js file and modify it as follows.

const _ = require('lodash');

console.log('module-a');

require('module-b');

console.log(_.flatten([1, [2, 3, 4]]));

And then, run the following command to run module-a.

node ./src/module-a

Then, you can see that lodash is well imported as follows.

module-a
module-b
[ 1, 2, 3, 4 ]

By this, you see the Hoisted dependencies can be used.

Dependency Version Conflict

Let’s see how it works when different versions of the same dependency are installed. Open the package.json file of module-b in src/module-b/package.json and modify it as follows.

{
  "name": "module-b",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "lodash": "^4"
  }
}

And then, run the following command to install the dependencies.

yarn install

Then, you can see that conflicting version of the loadash is installed in the node_modules of the module folder.

.
├── node_modules
│   ├── lodash
│   ├── module-a -> ../src/module-a
│   └── module-b -> ../src/module-b
├── package.json
├── src
│   ├── module-a
│   │   ├── index.js
│   │   └── package.json
│   └── module-b
│       ├── index.js
│       ├── node_modules
│       │   └── lodash
│       └── package.json
└── yarn.lock

nohoist option

If you want to install dependencies in each project’s node_modules without using the Hoisted dependencies, you can use the nohoist option. Open the package.json file in the root folder and add the nohoist option as follows.

{
  "name": "monorepo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "workspaces": {
    "packages": ["src/*"],
    "nohoist": ["**/lodash"]
  }
}

And then, delete the yarn.lock file and run the following command to install the dependencies.

# rm yarn.lock
yarn install

Then, you can see that the different versions of the dependencies are installed in the node_modules of each module folder.

.
├── node_modules
│   ├── module-a -> ../src/module-a
│   └── module-b -> ../src/module-b
├── package.json
├── src
│   ├── module-a
│   │   ├── index.js
│   │   ├── node_modules
│   │   │   └── lodash
│   │   └── package.json
│   └── module-b
│       ├── index.js
│       ├── node_modules
│       │   └── lodash
│       └── package.json
└── yarn.lock

Complete

Done! We’ve seen how Hoisting works when adding dependencies to each project in configuring a Monorepo using Yarn’s Workspaces. We’ve also seen the nohoist option to prevent dependencies from being Hoisted.

I hope this post will help you understand how Hoisting works when adding dependencies to each project in configuring a Monorepo using Yarn’s Workspaces. Also, I hope you can efficiently manage dependencies using Hoisting and nohoist in Yarn’s Workspaces.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts