Problem with Library Version Resolution in Monorepo with Yarn Workspaces
Hello! We have a monorepo set up using Yarn Workspaces:
"workspaces": {
"packages": [
"apps/*",
"packages/*"
]
}
Different packages use different versions of the “Admiral” library:
- Package
apps/pkbuses version 2.0 - Package
apps/opsnextuses version 1.0
The library versions cannot be unified. The problem is that Yarn promotes one dependency to the monorepo level (version 1.0), while the newer version (2.0) remains at the package level.
When running locally, the system should theoretically check local node_modules in the package first, then move up to the monorepo level. That is, when running from the pkb package, version 2.0 should be used. However, in practice, the version from the monorepo level (1.0) is always used, which leads to errors.
I tried configuring Webpack with:
resolve.aliasfor the libraryresolve.modules
But these changes had no effect.
Versions being used:
- Yarn: 1.2.22
- Webpack: 5.88
Could you suggest what to look into or what solutions might be available for this problem?
In a monorepo with Yarn Workspaces, when using different versions of the same library, the problem arises due to the hoisting mechanism, which lifts dependencies to the root project level. To solve your situation with the “Admiral” library in versions 1.0 and 2.0, you need to configure nohoist in the root package.json to prevent lifting conflicting dependencies, and use the correct module resolution strategy in Webpack.
Table of Contents
- How Yarn Workspaces and hoisting work
- Problem with different dependency versions
- Solution through nohoist configuration
- Webpack configuration for version resolution
- Alternative approaches
- Verification and debugging
How Yarn Workspaces and hoisting work
Yarn Workspaces uses the hoisting mechanism to optimize the dependency structure. When you run install, Yarn analyzes all dependencies across all packages and lifts common dependencies to the root node_modules level [1]. This prevents duplication and speeds up installation.
In your case with Yarn 1.2.22, when the same library is used in different versions, Yarn selects the version with the most usage and lifts it to the monorepo level [6]. The “Admiral” library version 1.0 is lifted to the monorepo level, while version 2.0 remains in the local node_modules of the pkb package.
Important: Yarn version 1.2.22 is quite old and has different behavior from modern versions regarding workspaces [10].
Problem with different dependency versions
Your situation is a classic monorepo problem with different dependency versions [5]. When version 1.0 is lifted to the monorepo level but you need to use version 2.0 in the pkb package, a module resolution conflict occurs.
Node.js uses a module resolution algorithm that first looks for dependencies in the local node_modules, then moves up the hierarchy. However, with workspaces, the hoisting mechanism disrupts this logic [8].
Problem symptoms:
- Version 1.0 is used instead of 2.0 when running from the
pkbpackage - Compatibility errors due to API differences
- Unpredictable behavior during development
Solution through nohoist configuration
To prevent lifting conflicting dependencies, use the nohoist setting in the root package.json [2]:
{
"workspaces": {
"packages": [
"apps/*",
"packages/*"
],
"nohoist": [
"apps/pkb/node_modules/admiral",
"apps/pkb/node_modules/admiral/**"
]
}
}
This tells Yarn not to lift the “Admiral” library from the pkb package to the monorepo level [4]. After changing the configuration, you must:
- Delete all
node_modulesfolders - Delete the
yarn.lockfile - Run
yarn installagain
Important: In Yarn 1.x,
nohoistworks differently than in newer versions [7]. For your case with Yarn 1.2.22, the syntax will be the same.
Webpack configuration for version resolution
Since you’re using Webpack 5, you can additionally configure module resolution [9]:
// webpack.config.js
module.exports = {
resolve: {
alias: {
'admiral': path.resolve(__dirname, 'node_modules/admiral')
},
modules: [
path.resolve(__dirname, 'node_modules'),
// Add local modules
path.resolve(__dirname, 'apps/pkb/node_modules')
]
}
};
However, as you mentioned, resolve.alias and resolve.modules didn’t have an effect. This is because the issue lies in the Node.js resolution mechanism, not in Webpack [3].
A more effective approach is to use resolve.fallback in Webpack 5:
module.exports = {
resolve: {
fallback: {
'admiral': require.resolve('admiral')
}
}
};
Alternative approaches
1. Yarn Constraints (for modern versions)
If you plan to migrate to Yarn 3+, you can use the yarn-constraints plugin [5], which allows you to control dependencies between workspaces.
2. Manual dependency management
Add the “Admiral” library version 2.0 to the root package.json with a private: true prefix:
{
"private": true,
"dependencies": {
"admiral": "2.0.0"
}
}
3. Using scoped packages
Rename the library in each package using a scope:
{
"dependencies": {
"@app/admiral-v1": "1.0.0",
"@app/admiral-v2": "2.0.0"
}
}
Verification and debugging
After making changes, ensure that resolution works correctly:
- Check the
node_modulesstructure:
ls -la apps/pkb/node_modules/admiral
ls -la node_modules/admiral
- Run a test script to verify resolution:
// test-resolution.js
console.log(require.resolve('admiral'));
- Use the command to check dependencies:
yarn why admiral
Important: In Yarn 1.2.22, it’s sometimes necessary to completely clear the cache and reinstall dependencies after changing
nohoistsettings [8].
Sources
- nohoist in Workspaces | Yarn Blog
- Using different versions of a dependency in different packages of a Yarn Workspace - Stack Overflow
- Yarn workspace
nohoistoption does not seem to work as documentation states… · Issue #6412 · yarnpkg/yarn - Yarn Workspaces hoisting in nested packages with different versions of a dependency results in importing wrong dependency version · Issue #8068 · yarnpkg/yarn
- Yarn Constraints: have your monorepo under control | DATAMOLE
- Workspaces | Yarn
- nohoist with workspaces still hoisting - Stack Overflow
- Yarn nohoist without using workspaces - Stack Overflow
- A concise guide to configuring React Native with Yarn Workspaces | Medium
- Migrating our Monorepo to Yarn 2 | DoltHub Blog
Conclusion
-
The main problem is related to the hoisting mechanism in Yarn Workspaces, which lifts common dependencies to the monorepo level, ignoring local versions in packages.
-
The key solution is configuring
nohoistin the root package.json to prevent lifting conflicting dependencies, especially for the “Admiral” library in thepkbpackage. -
Essential steps after changing configuration: delete all
node_modules,yarn.lock, and reinstall dependencies. -
For debugging, use the
yarn why admiralcommand and check resolution paths withrequire.resolve('admiral'). -
Upgrade recommendation: consider migrating to a more modern version of Yarn (3+), where the workspace mechanism is improved and has more tools for managing dependency versions.