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 overriddenNote: 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
overridesin 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 overriddenIn 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 overriddenFor 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 overriddenBut this may not be feasible, especially with third party dependencies.