fix(npm): share copy-package variants via symlink for class identity (#34691)
## Summary
When `deno install` resolves an npm package through multiple peer-dep
contexts, deno_npm produces `copy_index` variants of the same
name+version (`@pkg@1.0.0`, `@pkg@1.0.0_1`, ...). The local installer
filled each variant's `node_modules/<pkg>/` with `clone_dir_recursive`
(per-file hardlinks at distinct directory paths). `realpath()` then
returned the variant path verbatim, so Deno's CJS `Module._cache` kept
one entry per variant and every variant produced a distinct class
object.
NestJS DI compares the constructor-parameter type captured by
`@Injectable()` decorator metadata to the registered provider class
via `===`. With two `ModuleRef` classes alive the lookup fails:
```
[Nest] ... ERROR [ExceptionHandler] Nest can't resolve dependencies of
the TypeOrmHealthIndicator (?). Please make sure that the argument
ModuleRef at index [0] is available in the TerminusModule context.
```
That is denoland/deno#26427 — `@nestjs/terminus` triggers it because it
pulls `@nestjs/core` through a peer-dep cycle (core ↔ platform-express),
producing five physical copies of `@nestjs/core` in the user's
`node_modules`.
The fix symlinks each variant's package directory to the canonical
(copy_index=0) variant's package directory instead of cloning. The
variant's own `node_modules/` neighbours (the peer-dep symlinks)
remain, but `realpath()` of any file inside the package collapses to a
single canonical path. `Module._cache` then has one entry per package
and the class identity is preserved across require chains, matching
Node + npm and pnpm semantics (first-peer-wins for the inner
peer-resolution).
The global cache install path (`ensure_copy_package` in
`libs/npm_cache/lib.rs`) is unchanged.
## Test plan
- [x] Added regression spec
`tests/specs/npm/peer_dep_copy_class_identity`
with new fixture packages `@denotest/peer-dep-test-class@1.0.0`
and `@denotest/peer-dep-test-class-consumer@{1,2}.0.0`: asserts a
class exported from the (formerly duplicated) variant compares
`===` regardless of which consumer (peer-dep context) reached it.
- [x] Updated existing
`tests/specs/npm/peer_deps_with_copied_folders_and_lockfile`
`--node-modules-dir=auto` outputs: the inner-peer values change
from `1\n2` to `1\n1` because the previously distinct grandchild
copies now share a module instance (first-peer-wins). Global
cache runs are untouched and still produce `1\n2`.
- [x] Manually verified upstream repro
(`lucassusanto/nestjs-deno-terminus-issue`):
`[NestApplication] Nest application successfully started`.
- [x] `cargo test --test specs -- specs::npm::` — 276 passed; the 12
pre-existing failures all baseline-fail without this change
(tarball-integrity drift in the local test registry, unrelated).
- [x] `cargo build --bin deno` clean.
Refs #26427
Closes denoland/divybot#404
---------
Co-authored-by: divybot <divybot@users.noreply.github.com>
Co-authored-by: Divy Srivastava <me@littledivy.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>