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?
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
- bar@1.0.0
- baz@1.0.0
- foo@1.0.0
- foo-parent-2
- bar@1.0.0
- baz@1.1.0
- foo@1.0.0
In the example above, foo@1.0.0 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, foo@1.0.0 has two different sets of dependencies: one with baz@1.0.0 and the other one with baz@1.1.0. To support these use cases, pnpm has to hard link foo@1.0.0 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/microservices@9.1.4
- @nestjs/core@9.1.4
- my-other-nestjs-app
- @nestjs/core@9.1.4
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.