Reputation: 9574
I have a monorepo with a very basic setup available for reproducing this issue here:
It is a single nestjs app with 2 packages that it reads from.
@nestjs/core
among other dependencies is needed for both the packages and the main app to work, and it is enforced to be the exact same fixed version not only on their own local package.json's but also with the resolutions {}
config in the main package.json.
I can inspect the lockfile and find out that although the same version is used -- the hashes are different, causing major issues with nestjs, not being able to import injectable dependencies reliably causing it to break on bootstrap.
Is there a way to prevent this? to force linking the exact same hash/dependency?
Upvotes: 6
Views: 3966
Reputation: 46
With pnpm v7.29.0, you no longer have to perform the hack described below, but just leaving it here for educational purposes.
Now the solution is just to set dedupe-peer-dependents=true
(e.g. in your .npmrc
).
From pnpm docs
- foo-parent-1
- [email protected]
- [email protected]
- [email protected]
- foo-parent-2
- [email protected]
- [email protected]
- [email protected]
In the example above, [email protected] is installed for foo-parent-1 and foo-parent-2. Both packages have bar and baz as well, but they depend on different versions of baz. As a result, [email protected] has two different sets of dependencies: one with [email protected] and the other one with [email protected]. To support these use cases, pnpm has to hard link [email protected] as many times as there are different dependency sets.
For your specific case, foo === @nestjs/core, baz === @nestjs/microservices. Although the example used here is for "different versions", the same applies for optional peer dependencies. So to re-illustrate the example, in your context:
- my-nestjs-app
- @nestjs/[email protected]
- @nestjs/[email protected]
- my-other-nestjs-app
- @nestjs/[email protected]
Normally, if a package does not have peer dependencies, it is hard linked to a node_modules folder next to symlinks of its dependencies, like so:
However, if foo [@nestjs/core] has peer dependencies, there may be multiple sets of dependencies for it, so we create different sets for different peer dependency resolutions
^ This is usually ok for most packages out there. However @nestjs/core is special. It's stateful so that it can take care of all the runtime dependency injections. pnpm creating multiple copies of @nestjs/core in a monorepo will result in the confusing behaviour you're seeing, as your app could depend on 1 copy, while other NestJS libs depend on another. This seems like a common problem felt by devs using pnpm + nest, according to the NestJS discord.
Use pnpm hooks to modify nestjs packages' peerDependenciesMeta
at resolution time:
// .pnpmfile.cjs in your monorepo's root
function readPackage(pkg, context) {
if (pkg.name && pkg.name.startsWith('@nestjs/')) {
context.log(`${pkg.name}: make all peer dependencies required`);
pkg.peerDependenciesMeta = {};
}
return pkg;
}
module.exports = {
hooks: {
readPackage,
}
};
This is a hack IMO, and it's really annoying to deal with because Renovate
/ Dependabot
will ignore the .pnpmfile.cjs when it performs dependency updates. I'd suggest going with Nx or some other package manager that Nest / stateful packages work better with.
Upvotes: 3
Reputation: 742
Set use-lockfile-v6=true
in your .npmrc
Then pnpm install
to get the new lockfile
Then analyze the top of the pnpm-lock.yaml
to see which packages (yours) of your monorepo appear with different versions.
It's a bit a manual work but starts from top to bottom and look into the importers:
block. It's all the direct dependencies you listed into your package.json
files.
Stop at each dependency that looks with a suffix, like version: 0.31.1([email protected])
(note that something with no risk of duplication would be version: 0.31.1
).
In my case for example I found for my packageA
:
'@mui/material':
specifier: ^5.10.16
version: 5.10.16(@emotion/[email protected])(@emotion/[email protected])(@types/[email protected])([email protected])([email protected])
Then copy into the clipboard '@mui/material':
and search for all occurrences into the importer:
section. If you find a different pattern of version it means it's deduplicated, for example I had for my packageB
:
'@mui/material':
specifier: ^5.10.16
version: 5.10.16(@types/[email protected])([email protected])([email protected])
In my case my packageA
has @emotion/*
dependencies that are not specified into packageB
, making them a mismatch since @mui/material
lists them as peerDependencies
. Since I was no longer needing @emotion/*
dependencies I just deleted them. Then pnpm install
and there is only now version: 5.10.16(@types/[email protected])([email protected])([email protected])
. Meaning it will uses the exact same package for both packageA
and packageB
.
In case I would have needed @emotion/*
in packageA
I would have been able to add them into packageB
, it would make the same result of merging the dependency. The idea is just to fix the versions of peerDependencies
of my different @mui/material
so they can match and be merged. It's explained in https://pnpm.io/how-peers-are-resolved .
I think it can also help to make sure YOUR direct dependencies use same version everywhere in the monorepo (it's a first step before aligning peerDependencies
. For this I use into my root package.json
:
"pnpm": {
"overrides": {
"@mui/material": "^5.10.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
}
},
(repeat the process for each dependency that you don't want to see "duplicated"...)
(note that it's from my own experience, it's maybe not perfect, but it helped me at least ^^...)
Upvotes: 0
Reputation: 7646
When a dependency has peer dependencies, it might be written to node_modules several times if the peer dependencies are resolved differently in various parts of the dependency graph.
In your case, @nestjs/core
is in the dependencies of the graphql-server
project and the @myapp/entities
project. @nestjs/core
has @nestjs/platform-express
as an optional peer dependency.
@nestjs/platform-express
is in the dependencies of the graphql-server
project, so pnpm links it to @nestjs/platform-express
. You can see it in the lockfile:
/@nestjs/core/8.4.7_fkqgj3xrohk2pflugljc4sz7ea:
resolution: {integrity: sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw==}
requiresBuild: true
peerDependencies:
'@nestjs/common': ^8.0.0
'@nestjs/microservices': ^8.0.0
'@nestjs/platform-express': ^8.0.0
'@nestjs/websockets': ^8.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
'@nestjs/microservices':
optional: true
'@nestjs/platform-express':
optional: true
'@nestjs/websockets':
optional: true
dependencies:
'@nestjs/common': 8.4.7_47vcjb2de6lyibr6g4enoa5lyu
'@nestjs/platform-express': 8.4.7_7tsmhnugyerf5okgqzer2mfqme # <------HERE
'@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
object-hash: 3.0.0
path-to-regexp: 3.2.0
reflect-metadata: 0.1.13
rxjs: 7.5.5
tslib: 2.4.0
uuid: 8.3.2
transitivePeerDependencies:
- encoding
In the other project (@myapp/entities
), @nestjs/platform-express
is not in the dependencies, so when installing @nestjs/core
, pnpm cannot resolve the optional peer dependency. As a result, pnpm needs to create another instance of @nestjs/core
, which doesn't have this optional peer linked in. As you can see in the lockfile, the other entry doesn't have @nestjs/platform-express
:
/@nestjs/core/8.4.7_g7av3gvncewo44y4rurz3mgav4:
resolution: {integrity: sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw==}
requiresBuild: true
peerDependencies:
'@nestjs/common': ^8.0.0
'@nestjs/microservices': ^8.0.0
'@nestjs/platform-express': ^8.0.0
'@nestjs/websockets': ^8.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
'@nestjs/microservices':
optional: true
'@nestjs/platform-express':
optional: true
'@nestjs/websockets':
optional: true
dependencies:
'@nestjs/common': 8.4.7_47vcjb2de6lyibr6g4enoa5lyu
'@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
object-hash: 3.0.0
path-to-regexp: 3.2.0
reflect-metadata: 0.1.13
rxjs: 7.5.5
tslib: 2.4.0
uuid: 8.3.2
transitivePeerDependencies:
- encoding
To solve this, you can add @nestjs/platform-express
to the dependencies of the @myapp/entities
project. It should be the same version as in the other project.
Upvotes: 2