Skip to content

fix(lockfiles): retain base packages entry for peer-variant injected workspace deps#12819

Closed
Eyalm321 wants to merge 3 commits into
vercel:mainfrom
Eyalm321:fix/pnpm-prune-peer-variant-resolution
Closed

fix(lockfiles): retain base packages entry for peer-variant injected workspace deps#12819
Eyalm321 wants to merge 3 commits into
vercel:mainfrom
Eyalm321:fix/pnpm-prune-peer-variant-resolution

Conversation

@Eyalm321

Copy link
Copy Markdown

Summary

Residual of #12002 / #11059, not covered by #12073.

#12073 made turbo prune retain injected file: workspace-dep entries. But when the injected workspace package has peer dependencies that resolve per-consumer, pnpm v9 writes:

  • the variant under snapshots:@scope/pkg@file:packages/pkg(peer@1.0.0)
  • the resolution on the base packages: entry → @scope/pkg@file:packages/pkgresolution: {type: directory, directory: packages/pkg}

The injected handler computes key = format_key(dep, version) where version is the peer-variant string, then get_packages(&key) — which is None for the variant (the base entry has no suffix). So the base entry carrying the directory resolution is dropped from the pruned lockfile.

pnpm's convertToLockfileObject then does Object.assign(snapshot, packages[removeSuffix(depPath)])undefined → and later crashes:

Cannot use 'in' operator to search for 'directory' in undefined

…on pnpm install --frozen-lockfile against the pruned lockfile (reproducible with injectWorkspacePackages: true + a workspace dep that has per-consumer peer deps).

Fix

After retaining the variant, strip the peer suffix and also retain the base packages: entry — reusing the exact DepPath::parse(...).format_key(dp.name, dp.version) peer-suffix handling the transitive-deps path in this same function already uses a few lines below. It's a no-op when there is no peer suffix (base_key == key), so the non-variant case #12073 added is unaffected.

~13 lines, no new APIs.

Test

Adds test_subgraph_injected_peer_variant_retains_base_resolution (mirrors #12073's own test_subgraph_with_injected_workspace_packages_setting): an injected @repo/shared with a react peer → base packages: entry + peer-variant snapshots: entry. Asserts the pruned lockfile retains the base entry and its directory: resolution. Fails before this change, passes after.

🤖 Generated with Claude Code

…workspace deps

vercel#12073 retains injected `file:` workspace snapshots, but when the dep has
peer dependencies that resolve per-consumer, pnpm writes the variant under
`pkg@file:packages/pkg(peer@1)` (snapshots) while `resolution` lives on the
base `pkg@file:packages/pkg` (packages). vercel#12073 only looked up
get_packages(variant_key) — None for the variant — so the base entry with
the directory resolution was dropped. pnpm's convertToLockfileObject then
inherits an undefined resolution and crashes with
`Cannot use 'in' operator to search for 'directory' in undefined` during
`pnpm install --frozen-lockfile`.

Strip the peer suffix to also retain the base entry, mirroring the
peer-suffix handling the transitive-deps path in this same function
already uses. No-op when there is no peer suffix (base_key == key), so the
non-variant case vercel#12073 added is unaffected.

Residual of vercel#12002 / vercel#11059. Adds a peer-variant regression test.
@Eyalm321 Eyalm321 requested a review from a team as a code owner May 17, 2026 21:13
@Eyalm321 Eyalm321 requested review from tknickman and removed request for a team May 17, 2026 21:13
@vercel

vercel Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@Eyalm321 Eyalm321 marked this pull request as draft May 17, 2026 21:14
Eyalm321 pushed a commit to Eyalm321/pnpm that referenced this pull request May 17, 2026
Per the PR review (zkochan); pairs with the turbo-side root-cause fix
vercel/turborepo#12819.

Bug 1 (peer-variant resolution): normalize once at the read layer in
convertToLockfileObject — when a pruned lockfile dropped the base
packages entry, synthesize the directory resolution from the file:
depPath (reconstruction of exactly what pnpm's writer emits, not a
guess). This makes inheritOrSynthesizeResolution + its 5 reader call
sites inert idempotent guards (resolution is always populated before
they run); the active normalization now lives solely in the converter.
Mechanical removal of the now-dead helper is a trivial follow-up.

Bug 2 (lifecycle re-import .bin ENOENT): drop the pnpm#4299 scanDir-into-
filesMap workaround and pass keepModulesDir: true so importIndexedDir
skips the pnpm#11088 makeEmptyDir fast path and preserves the target's
existing node_modules via its staging/move path. Stays on
storeController.importPackage so source files keep hardlinks — replaces
the copy-loop mirror.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@anthonyshew

Copy link
Copy Markdown
Contributor

Closed for slop.

Eyalm321 pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
vercel/turborepo#12819 was closed; the live root-cause fix (retain the
base packages entry for peer-variant injected deps) is tracked at
vercel/turborepo#12825. Keeps the source comment consistent with the
PR description.
github-actions Bot pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
Per the PR review (zkochan); pairs with the turbo-side root-cause fix
vercel/turborepo#12819.

Bug 1 (peer-variant resolution): normalize once at the read layer in
convertToLockfileObject — when a pruned lockfile dropped the base
packages entry, synthesize the directory resolution from the file:
depPath (reconstruction of exactly what pnpm's writer emits, not a
guess). This makes inheritOrSynthesizeResolution + its 5 reader call
sites inert idempotent guards (resolution is always populated before
they run); the active normalization now lives solely in the converter.
Mechanical removal of the now-dead helper is a trivial follow-up.

Bug 2 (lifecycle re-import .bin ENOENT): drop the pnpm#4299 scanDir-into-
filesMap workaround and pass keepModulesDir: true so importIndexedDir
skips the pnpm#11088 makeEmptyDir fast path and preserves the target's
existing node_modules via its staging/move path. Stays on
storeController.importPackage so source files keep hardlinks — replaces
the copy-loop mirror.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
vercel/turborepo#12819 was closed; the live root-cause fix (retain the
base packages entry for peer-variant injected deps) is tracked at
vercel/turborepo#12825. Keeps the source comment consistent with the
PR description.
zkochan pushed a commit to Eyalm321/pnpm that referenced this pull request May 19, 2026
Per the PR review (zkochan); pairs with the turbo-side root-cause fix
vercel/turborepo#12819.

Bug 1 (peer-variant resolution): normalize once at the read layer in
convertToLockfileObject — when a pruned lockfile dropped the base
packages entry, synthesize the directory resolution from the file:
depPath (reconstruction of exactly what pnpm's writer emits, not a
guess). This makes inheritOrSynthesizeResolution + its 5 reader call
sites inert idempotent guards (resolution is always populated before
they run); the active normalization now lives solely in the converter.
Mechanical removal of the now-dead helper is a trivial follow-up.

Bug 2 (lifecycle re-import .bin ENOENT): drop the pnpm#4299 scanDir-into-
filesMap workaround and pass keepModulesDir: true so importIndexedDir
skips the pnpm#11088 makeEmptyDir fast path and preserves the target's
existing node_modules via its staging/move path. Stays on
storeController.importPackage so source files keep hardlinks — replaces
the copy-loop mirror.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zkochan pushed a commit to Eyalm321/pnpm that referenced this pull request May 19, 2026
vercel/turborepo#12819 was closed; the live root-cause fix (retain the
base packages entry for peer-variant injected deps) is tracked at
vercel/turborepo#12825. Keeps the source comment consistent with the
PR description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants