mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 04:02:32 -04:00
fix(graph-builder): guard against missing resolution on peer-variant snapshots
Peer-dep variant snapshots in the lockfile (e.g. `pkg@file:packages/pkg(peer@2.0.0)`) inherit their resolution from the base entry and may legally omit `resolution` themselves. The previous code in `buildGraphFromPackages` accessed `pkgSnapshot.resolution` directly with the `in` operator, which crashes with `TypeError: Cannot use 'in' operator to search for 'directory' in undefined` when `resolution` is unset. Guarding the access mirrors the pattern in `@pnpm/deps.graph-hasher`, which already handles missing resolution defensively. A snapshot without `resolution` is treated as "not a directory dep" — the base entry still drives directory-dep detection through its own snapshot.
This commit is contained in:
6
.changeset/guard-missing-resolution-graph-builder.md
Normal file
6
.changeset/guard-missing-resolution-graph-builder.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/deps.graph-builder": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix `TypeError: Cannot use 'in' operator to search for 'directory' in undefined` during `pnpm install --frozen-lockfile` when a peer-dep variant snapshot omits its `resolution` field (the variant inherits resolution from the base entry, so this shape is valid in the lockfile but the graph builder didn't guard the access).
|
||||
6
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
6
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
@@ -214,7 +214,11 @@ async function buildGraphFromPackages (
|
||||
return
|
||||
}
|
||||
|
||||
const isDirectoryDep = 'directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null
|
||||
// Peer-dep variant snapshots (e.g. `pkg@1.0.0(peer@2.0.0)`) inherit
|
||||
// their resolution from the base entry and may omit `resolution`
|
||||
// themselves. Guard so the `'directory' in …` check doesn't crash
|
||||
// with `Cannot use 'in' operator to search for 'directory' in undefined`.
|
||||
const isDirectoryDep = pkgSnapshot.resolution != null && 'directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null
|
||||
if (isDirectoryDep && opts.ignoreLocalPackages) {
|
||||
logger.info({
|
||||
message: `Skipping local dependency ${pkgName}@${pkgVersion} (file: protocol)`,
|
||||
|
||||
8
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/package.json
vendored
Normal file
8
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "peer-variant-missing-resolution",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"pkg-a": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "peer",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "pkg-a",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"peerDependencies": {
|
||||
"peer": "1.0.0"
|
||||
}
|
||||
}
|
||||
45
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-lock.yaml
generated
vendored
Normal file
45
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
injectWorkspacePackages: true
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
pkg-a:
|
||||
specifier: workspace:*
|
||||
version: 'file:packages/pkg-a(peer@1.0.0)'
|
||||
|
||||
packages/peer: {}
|
||||
|
||||
packages/pkg-a:
|
||||
peerDependencies:
|
||||
peer:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
|
||||
packages:
|
||||
|
||||
# Base entry — has `resolution` describing the workspace directory.
|
||||
'pkg-a@file:packages/pkg-a':
|
||||
resolution: { directory: packages/pkg-a, type: directory }
|
||||
|
||||
# Peer-variant entry — inherits resolution from the base, intentionally
|
||||
# omits its own `resolution`. This shape is what pnpm itself writes for a
|
||||
# workspace dep with peer dependencies that resolve differently per
|
||||
# consumer. Before the fix it crashed `lockfileToDepGraph` with
|
||||
# `TypeError: Cannot use 'in' operator to search for 'directory' in undefined`.
|
||||
'pkg-a@file:packages/pkg-a(peer@1.0.0)':
|
||||
dependencies:
|
||||
peer: 1.0.0
|
||||
|
||||
snapshots:
|
||||
|
||||
'pkg-a@file:packages/pkg-a': {}
|
||||
|
||||
'pkg-a@file:packages/pkg-a(peer@1.0.0)':
|
||||
dependencies:
|
||||
peer: 1.0.0
|
||||
3
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-workspace.yaml
vendored
Normal file
3
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
injectWorkspacePackages: true
|
||||
@@ -919,3 +919,25 @@ test('installing a package deeply installs all required dependencies', async ()
|
||||
expect(projectAssertion.requireModule('is-positive')).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
// Regression test: `lockfileToDepGraph` crashed with
|
||||
// `TypeError: Cannot use 'in' operator to search for 'directory' in undefined`
|
||||
// when a peer-dep variant snapshot in the lockfile omitted its `resolution`
|
||||
// field. The variant intentionally inherits resolution from the base entry,
|
||||
// so this shape is valid pnpm output but the graph builder accessed
|
||||
// `pkgSnapshot.resolution` without guarding for undefined.
|
||||
test('headlessInstall: peer-variant snapshot without `resolution` does not crash', async () => {
|
||||
const workspaceFixture = f.prepare('peer-variant-missing-resolution')
|
||||
const projects = [
|
||||
workspaceFixture,
|
||||
path.join(workspaceFixture, 'packages', 'pkg-a'),
|
||||
path.join(workspaceFixture, 'packages', 'peer'),
|
||||
]
|
||||
|
||||
await expect(
|
||||
headlessInstall(await testDefaults({
|
||||
lockfileDir: workspaceFixture,
|
||||
projects,
|
||||
}))
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user