NeuroAgent

Library Version Resolution in Yarn Workspaces

Solving dependency version conflicts in Yarn Workspaces monorepo. Configuring nohoist and optimizing module resolution.

Question

Problem with Library Version Resolution in Monorepo with Yarn Workspaces

Hello! We have a monorepo set up using Yarn Workspaces:

json
"workspaces": {
  "packages": [
    "apps/*",
    "packages/*"
  ]
}

Different packages use different versions of the “Admiral” library:

  • Package apps/pkb uses version 2.0
  • Package apps/opsnext uses 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.alias for the library
  • resolve.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?

NeuroAgent

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

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 pkb package
  • 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]:

json
{
  "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:

  1. Delete all node_modules folders
  2. Delete the yarn.lock file
  3. Run yarn install again

Important: In Yarn 1.x, nohoist works 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]:

javascript
// 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:

javascript
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:

json
{
  "private": true,
  "dependencies": {
    "admiral": "2.0.0"
  }
}

3. Using scoped packages

Rename the library in each package using a scope:

json
{
  "dependencies": {
    "@app/admiral-v1": "1.0.0",
    "@app/admiral-v2": "2.0.0"
  }
}

Verification and debugging

After making changes, ensure that resolution works correctly:

  1. Check the node_modules structure:
bash
ls -la apps/pkb/node_modules/admiral
ls -la node_modules/admiral
  1. Run a test script to verify resolution:
javascript
// test-resolution.js
console.log(require.resolve('admiral'));
  1. Use the command to check dependencies:
bash
yarn why admiral

Important: In Yarn 1.2.22, it’s sometimes necessary to completely clear the cache and reinstall dependencies after changing nohoist settings [8].


Sources

  1. nohoist in Workspaces | Yarn Blog
  2. Using different versions of a dependency in different packages of a Yarn Workspace - Stack Overflow
  3. Yarn workspace nohoist option does not seem to work as documentation states… · Issue #6412 · yarnpkg/yarn
  4. Yarn Workspaces hoisting in nested packages with different versions of a dependency results in importing wrong dependency version · Issue #8068 · yarnpkg/yarn
  5. Yarn Constraints: have your monorepo under control | DATAMOLE
  6. Workspaces | Yarn
  7. nohoist with workspaces still hoisting - Stack Overflow
  8. Yarn nohoist without using workspaces - Stack Overflow
  9. A concise guide to configuring React Native with Yarn Workspaces | Medium
  10. Migrating our Monorepo to Yarn 2 | DoltHub Blog

Conclusion

  1. 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.

  2. The key solution is configuring nohoist in the root package.json to prevent lifting conflicting dependencies, especially for the “Admiral” library in the pkb package.

  3. Essential steps after changing configuration: delete all node_modules, yarn.lock, and reinstall dependencies.

  4. For debugging, use the yarn why admiral command and check resolution paths with require.resolve('admiral').

  5. 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.