Bugs
pnpm install --frozen-lockfile and pnpm deploy on a workspace with injectWorkspacePackages: true hit three separate crashes when the workspace has packages with peer deps that resolve differently per consumer and at least one of those packages has a prepare/postinstall script + a dep with a bin entry. The first two are the same underlying problem (pkgSnapshot.resolution deref without null-check); the third surfaces only once Bug 1 is unblocked, in the lifecycle re-import path.
All three reproduce on stock pnpm 11.1.2.
Bug 1 — graph-builder: Cannot use 'in' operator to search for 'directory' in undefined
[ERROR] Cannot use 'in' operator to search for 'directory' in undefined
pnpm: Cannot use 'in' operator to search for 'directory' in undefined
at file:///.../pnpm.mjs:150423:42
at buildGraphFromPackages (file:///.../pnpm.mjs:150503:7)
at lockfileToDepGraph2 (file:///.../pnpm.mjs:150356:73)
at headlessInstall (file:///.../pnpm.mjs:156439:288)
at async tryFrozenInstall (file:///.../pnpm.mjs:159321:56)
deps/graph-builder/src/lockfileToDepGraph.ts:217 accesses pkgSnapshot.resolution with the in operator without first checking that resolution is defined.
Bug 2 — deploy: Cannot use 'in' operator to search for 'integrity' in undefined
Same root cause, different site. Triggered by pnpm deploy:
pnpm: Cannot use 'in' operator to search for 'integrity' in undefined
at convertPackageSnapshot (file:///.../pnpm.mjs:217243:19)
at createDeployFiles (file:///.../pnpm.mjs:217152:45)
at deployFromSharedLockfile (file:///.../pnpm.mjs:217555:23)
releasing/commands/src/deploy/createDeployFiles.ts iterates lockfile.packages and dereferences inputResolution.integrity directly.
Bug 3 — lifecycle re-import: ERR_PNPM_ENOENT on .bin/<tool>
Once Bug 1 is unblocked, the next crash hits during the post-script re-import of an injected workspace package:
ERR_PNPM_ENOENT copyfile '<targetDir>/node_modules/.bin/<tool>'
at tryImportIndexedDir (indexed-pkg-importer/src/importIndexedDir.ts)
at runLifecycleHooksConcurrently (exec/lifecycle/src/runLifecycleHooksConcurrently.ts)
runLifecycleHooksConcurrently runs prepare/postinstall scripts for workspace packages, then re-imports each into its injected target dirs so the post-script state propagates. The existing node_modules/ inside each target was set up during the initial install (transitive deps + bin symlinks); the re-import only ships the workspace source, so it has to preserve the existing tree.
The historical approach (added in #4299, 2022) was a manual scanDir over <targetDir>/node_modules that fed absolute paths into the re-import's filesMap. That worked when importIndexedDir always staged-and-renamed. But #11088 made the fast path (write directly into the final dir) the default, and the fast path's makeEmptyDirSync(targetDir) wipes the very files the scanned entries point at, so tryImportIndexedDir's first copyFileSync(<source-inside-newDir>, <stage>) throws ENOENT.
Lockfile shape that triggers Bug 1/2
'@scope/foo@file:packages/foo':
resolution: {directory: packages/foo, type: directory}
'@scope/foo@file:packages/foo(peer@2.0.0)(esbuild@0.28.0)':
dependencies:
peer: 2.0.0
...
# No \`resolution:\` field — pnpm's own writer produces this for
# workspace deps with peer deps that resolve differently per consumer
# when \`injectWorkspacePackages: true\` is set. The variant inherits
# \`resolution\` from the base entry above.
Repro
Any workspace where:
pnpm-workspace.yaml has injectWorkspacePackages: true
- A workspace package declares peer deps that consumers resolve to different versions → triggers Bug 1 (install) and Bug 2 (deploy)
- An injected workspace package has a
prepare/postinstall script AND a dep with a bin entry → triggers Bug 3
PR #11662 ships minimal fixtures: installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/ for Bug 1, and a regression test in installing/deps-installer/test/install/injectLocalPackages.ts (using @pnpm.e2e/hello-world-js-bin) for Bug 3.
Workaround
pnpm install --no-frozen-lockfile bypasses headlessInstall and avoids Bug 1, but doesn't help with Bug 2 (pnpm deploy) or Bug 3 (re-import).
Fix
PR #11662.
Bugs
pnpm install --frozen-lockfileandpnpm deployon a workspace withinjectWorkspacePackages: truehit three separate crashes when the workspace has packages with peer deps that resolve differently per consumer and at least one of those packages has aprepare/postinstallscript + a dep with a bin entry. The first two are the same underlying problem (pkgSnapshot.resolutionderef without null-check); the third surfaces only once Bug 1 is unblocked, in the lifecycle re-import path.All three reproduce on stock pnpm 11.1.2.
Bug 1 — graph-builder:
Cannot use 'in' operator to search for 'directory' in undefineddeps/graph-builder/src/lockfileToDepGraph.ts:217accessespkgSnapshot.resolutionwith theinoperator without first checking thatresolutionis defined.Bug 2 — deploy:
Cannot use 'in' operator to search for 'integrity' in undefinedSame root cause, different site. Triggered by
pnpm deploy:releasing/commands/src/deploy/createDeployFiles.tsiterateslockfile.packagesand dereferencesinputResolution.integritydirectly.Bug 3 — lifecycle re-import:
ERR_PNPM_ENOENTon.bin/<tool>Once Bug 1 is unblocked, the next crash hits during the post-script re-import of an injected workspace package:
runLifecycleHooksConcurrentlyruns prepare/postinstall scripts for workspace packages, then re-imports each into its injected target dirs so the post-script state propagates. The existingnode_modules/inside each target was set up during the initial install (transitive deps + bin symlinks); the re-import only ships the workspace source, so it has to preserve the existing tree.The historical approach (added in #4299, 2022) was a manual
scanDirover<targetDir>/node_modulesthat fed absolute paths into the re-import'sfilesMap. That worked whenimportIndexedDiralways staged-and-renamed. But #11088 made the fast path (write directly into the final dir) the default, and the fast path'smakeEmptyDirSync(targetDir)wipes the very files the scanned entries point at, sotryImportIndexedDir's firstcopyFileSync(<source-inside-newDir>, <stage>)throws ENOENT.Lockfile shape that triggers Bug 1/2
Repro
Any workspace where:
pnpm-workspace.yamlhasinjectWorkspacePackages: trueprepare/postinstallscript AND a dep with a bin entry → triggers Bug 3PR #11662 ships minimal fixtures:
installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/for Bug 1, and a regression test ininstalling/deps-installer/test/install/injectLocalPackages.ts(using@pnpm.e2e/hello-world-js-bin) for Bug 3.Workaround
pnpm install --no-frozen-lockfilebypassesheadlessInstalland avoids Bug 1, but doesn't help with Bug 2 (pnpm deploy) or Bug 3 (re-import).Fix
PR #11662.