Unifying dependency versions across npm workspaces

npm overrides helps enforce specific dependency versions across packages within a codebase.

Given workspaces a and b require different versions of lodash:

/npm-workspaces ❯ npm pkg get dependencies -ws
{
  "a": {
    "lodash": "^3.10.1"
  },
  "b": {
    "lodash": "^4.17.21"
  }
}

Setting overrides in the root package.json will ensure all workspaces use that same version:

{
  "name": "npm-workspaces",
  "workspaces": ["packages/a", "packages/b"],
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "overrides": {
    "lodash": "$lodash"
  }
}

Running npm install will then attempt to replace every non-installed reference of lodash with the version in overrides. lodash is hoisted to the root node_modules and automatically symlinked to both workspaces:

/npm-workspaces ❯ npm ls
├─┬ a@1.0.0 -> ./packages/a
│ └── lodash@4.17.21 deduped invalid: "^3.10.1" from packages/a
├─┬ b@1.0.0 -> ./packages/b
│ └── lodash@4.17.21 deduped invalid: "^3.10.1" from packages/a
└── lodash@4.17.21 invalid: "^3.10.1" from packages/a overridden

Note: npm will flag packages that don’t satisfy the overrides dependency version as invalid, and so take care when replacing dependency versions.

npm overrides only replaces non-installed references

overrides in installed dependencies (including workspaces) will not be considered in dependency tree resolution.

— Dependency resolution overrides RFC

If npm install already ran for workspace a, and so workspace a has it’s own reference to lodash@^3.10.1 in node_modules and package-lock.json, then overrides will not consider replacing this version:

/npm-workspaces ❯ npm i -w=a
/npm-workspaces ❯ npm i
/npm-workspaces ❯ npm ls
├─┬ a@1.0.0 -> ./packages/a
│ └── lodash@3.10.1
├─┬ b@1.0.0 -> ./packages/b
│ └── lodash@4.17.21 deduped
└── lodash@4.17.21 overridden

In this scenario, to enforce the overrides.lodash version on workspace a, then all installed references to lodash in package-lock.json and node_modules must be removed. Precisely removing references to an installed dependency is not trivial as package-lock.json does not update when dependencies are uninstalled. Consequently, nuclear solutions like deleting package-lock.json and node_modules, and then re-running npm install from the root may seem warranted; but removing package-lock.json carries the severe risk of introducing breaking changes and security vulnerabilities as different dependency versions are installed that still satisfy package.json:

/npm-workspaces ❯ rm -rf node_modules
/npm-workspaces ❯ rm package-lock.json
/npm-workspaces ❯ npm i
/npm-workspaces ❯ npm ls
├─┬ a@1.0.0 -> ./packages/a
│ └── lodash@4.17.21 deduped invalid: "^3.10.1" from packages/a
├─┬ b@1.0.0 -> ./packages/b
│ └── lodash@4.17.21 deduped invalid: "^3.10.1" from packages/a
└── lodash@4.17.21 invalid: "^3.10.1" from packages/a overridden

For stability and security, ideally dependencies are updated in place:

/npm-workspaces ❯ npm i lodash@^4.17.21 -w=a
/npm-workspaces ❯ npm ls
├── a@1.0.0 -> ./packages/a
├─┬ b@1.0.0 -> ./packages/b
│ └── lodash@4.17.21 deduped
└── lodash@4.17.21 overridden

But this may not be feasible, especially with third party dependencies.

Read more