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