A Comprehensive Guide to npm Workspaces and Monorepos
A little about current workspace managers
Yarn Classic x Yarn Berry Workspaces
Dependency Resolution: Yarn classic workspaces use a “pluggable” resolution algorithm, providing more flexibility in how dependencies are resolved. Like npm, Yarn uses hoisting to share common dependencies at the root level, unlike Yarn Berry, which has a new architecture with a “plug’n’play” resolution strategy, aiming for a zero-installation approach. It takes advantage of the concept of “PnP” files to represent dependencies without the need for a traditional node_modules directory. This is a complex choice if you want to migrate managers later because Yarn Berry eliminates the need for a central node_modules
leading to faster and more efficient installations however brings significant changes in how dependencies are resolved.
Npm Workspaces
Dependency Resolution: npm workspaces utilize a “fixed” strategy for dependency resolution. Each workspace has its own node_modules
, and dependencies are installed per workspace. However, common dependencies are hoisted to the root node_modules
to optimize disk space.
Established Ecosystem Integration: Being the default package manager for Node.js, npm seamlessly integrates with the broader Node.js ecosystem.
Share Dependencies: npm Workspaces enable the sharing of dependencies between packages. If multiple packages require the same dependency, it can be installed once at the workspace level, optimizing disk space and simplifying dependency management.
Hands-on with npm workspace
Setup Project
mkdir your-project && cd your-project && npm init -y
Parent Workspace Configuration (root package.json)
{
"name": "my-monorepo",
"workspaces": [
"packages/*"
],
// Example of shared scripts in workspaces
{
"scripts": {
"build": "npm run build:package-a && npm run build:package-b",
"build:package-a": "cd packages/package-a && npm run build",
"build:package-b": "cd packages/package-b && npm run build"
}
}
}
Directory Structure
/my-monorepo
|-- /packages
| |-- /package-a
| |-- /package-b
|-- package.json
Package A
// /packages/package-a/package.json
{
"name": "package-a",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
// /packages/package-a/index.js
module.exports = () => {
console.log('Hello from Package A!');
};
Package B
// /packages/package-b/package.json
{
"name": "package-b",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"package-a": "1.0.0"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
// /packages/package-b/index.js
const packageA = require('package-a');
module.exports = () => {
packageA();
console.log('Hello from Package B!');
};
npm link in Workspaces:
Local Development and Testing:
npm link
is a powerful tool in the npm ecosystem, allowing packages to be symlinked for local development. In the context of npm workspaces, this tool can be used to connect packages within the workspace for seamless local development and testing.
Mechanism:
npm ensures that when using npm link
, the linked package is correctly hoisted to the top-level node_modules
directory, maintaining the integrity of the workspace structure. After running the above command, package-a
will be symlinked and hoisted to the top-level node_modules
, allowing seamless development.
# Example npm link in workspaces
cd packages/package-a
npm link
cd ../package-b
npm link package-a
After setting up the structure, running npm install
in the root directory installs all dependencies for all packages, linking them where necessary.
Helpful Commands for Reference
Installing Workspace Dependencies
Install dependencies for all packages defined in the workspace configuration.
npm install
Scopes and Private Packages in Workspaces
- Scopes (
@scope/package
) can also be used in workspaces, and the logic of private scopes still applies.
# Example of installation in workspaces with private scope
npm install @scope/package-a --workspace
Listing Dependencies Across Workspaces
List dependencies across all workspaces.
npm ls
Hoisting in npm Workspaces
In npm workspaces, hoisting plays a crucial role in optimizing the installation and management of dependencies across multiple packages within the same workspace.
Common Dependencies
- When multiple packages within a workspace depend on the same package and version, npm hoists that dependency to the top-level
node_modules
directory. This means that instead of having duplicate copies of the same dependency in each package'snode_modules
, there's a single shared instance at the root.
// Example workspace package.json
{
"workspaces": ["packages/*"],
"dependencies": {
"lodash": "^4.17.21"
}
}
If two packages within the workspace use lodash, npm ensures that only one copy of lodash is present at the top-level node_modules
directory.
Scoped Packages
- For scoped packages, hoisting also occurs within the scoped directory, further optimizing the organization of dependencies.
// Example workspace package.json with scoped package
{
"workspaces": ["packages/*"],
"dependencies": {
"@my-monorepo/package-a": "^1.0.0"
}
}
The scoped package, in this case, will be hoisted to the top-level node_modules/@my-scope/package-a
directory.
Benefits of Hoisting in npm Workspaces:
Disk Space Efficiency
By hoisting common dependencies, npm workspaces optimize disk space usage, as there’s only one copy of a dependency shared among multiple packages.
Faster Installations
Hoisting reduces the time it takes to install dependencies, as npm can avoid redundant installations and copy operations.
Consistent Dependency Versions:
Hoisting ensures that all packages within the workspace use the same version of a dependency, avoiding version conflicts.
Conclusion
While it is feasible to manage tasks at the individual workspace level, centralizing shared responsibilities at the root level fosters a more coherent and straightforward approach. Embracing npm Workspaces can significantly improve monorepo management, fostering enhanced collaboration, modularity, and overall development efficiency. However, the choice between npm Workspaces, Yarn Workspaces, and Yarn Berry hinges on considerations such as ecosystem familiarity, project requirements, and personal preferences. Each of these tools possesses unique strengths, and the decision should be carefully aligned with the specific needs and objectives of the project.