Skip to content

Three install/deploy crashes with injectWorkspacePackages: true (peer-variant resolution + lifecycle re-import) #11663

@Eyalm321

Description

@Eyalm321

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions