Skip to content

fix(vendor): write pnpm overrides to pnpm-workspace.yaml for pnpm >= 11 - #174

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
fix/pnpm11-overrides-location
Aug 15, 2026
Merged

fix(vendor): write pnpm overrides to pnpm-workspace.yaml for pnpm >= 11#174
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
fix/pnpm11-overrides-location

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

In vendored mode for the npm/pnpm ecosystem, the CLI pinned the vendored tarball by writing a pnpm.overrides entry into package.json. pnpm >= 11 no longer reads overrides from the package.json pnpm field — it moved to pnpm-workspace.yaml (https://pnpm.io/settings). So pnpm 11 ignored the override, resolved no overrides from config, and a frozen install of the committable artifact refused:

[WARN] The "pnpm" field in package.json is no longer read by pnpm. ... "pnpm.overrides".
[ERR_PNPM_LOCKFILE_CONFIG_MISMATCH] Cannot proceed with the frozen installation.
  The current "overrides" configuration doesn't match the value found in the lockfile

The vendored tarball + lock were correct (they even pass pnpm 11's tarball-URL supply-chain policy) — only the override location was wrong. Surfaced by PR #169's real-production vendored e2e (e2e_vendored_production.rs, pnpm leg) and documented in docs/testing/vendored-production-e2e.md.

Reproduced on this host (pnpm 11.21.0): pnpm add minimist@1.2.2scan --mode vendored → copy only the committable files → cold pnpm install --frozen-lockfile --offlineERR_PNPM_LOCKFILE_CONFIG_MISMATCH.

Fix

The pnpm vendor backend now mirrors the same versioned <name>@<version>file:<rel-tgz> selector into pnpm-workspace.yaml, alongside the existing package.json override, so whichever surface the installed pnpm reads matches the lock's overrides: section (what pnpm compares for the mismatch):

  • no workspace file → create one with a root-only packages: ['.'] list (pnpm 9 refuses a workspace file whose packages field is missing/empty; . is already the sole importer and, unlike packages/*, cannot glob a stray packages/ subtree into the workspace) plus the overrides: block;
  • existing file, no overrides: → append the section, packages: untouched;
  • existing overrides: section → insert our key (or take over a user's exact-version pin), fail-closed on a conflicting same-name override or an inline/flow mapping.

package.json pnpm.overrides is kept for pnpm 9/10 back-compat. Edits are line splices (byte-stable), tracked with wiring records + PnpmMeta flags. The three surfaces are committed override-first / lock-last so a lock-write failure never strands a desynced override. vendor --revert deletes a file it created (when still the bare scaffold) or splices its override back out of one it edited (byte-for-byte), restoring a taken-over value and dropping an emptied section it created. Scoped to the pnpm vendored path; no other ecosystem touched.

Test

Verified on real pnpm 9.15.9 / 10.34.5 / 11.21.0: a cold pnpm install --frozen-lockfile --offline from only the committable files installs the vendored (patched) minimist@1.2.2 bytes — no ERR_PNPM_LOCKFILE_CONFIG_MISMATCH — and vendor --revert removes the created pnpm-workspace.yaml and restores package.json/pnpm-lock.yaml.

  • New hermetic unit tests (vendor::pnpm_lock): create-scaffold + revert-deletes; append-section + byte-restore; insert-beside-existing + remove-only-ours; inline-mapping refused; conflicting-override refused; commit unwind deletes a created workspace file. state.rs round-trip covers the new PnpmMeta fields. All 602 vendor::* lib tests pass.
  • Capstone e2e_vendor_pnpm_build.rs (real corepack pnpm@10/9): now asserts the workspace file is created with the override + root-only packages:, is copied into the fresh-checkout proof, stays byte-stable on re-vendor, and is deleted on revert. Passes.
  • Production e2e e2e_vendored_production.rs (real Socket production + real npm registry, host pnpm 11): the pnpm leg's xfail workaround is removed — the frozen install now succeeds directly and byte-checks the patched bytes. pnpm_vendored_install_proof passes.
  • cargo build + cargo clippy clean (the only clippy warnings are pre-existing doc_lazy_continuation notes in an unrelated module-doc header, present on main).

🤖 Generated with Claude Code


Note

Medium Risk
Changes pnpm vendoring commit/revert behavior and creates or edits pnpm-workspace.yaml, which affects install correctness across pnpm 9–11; scope is limited to the pnpm vendor path with extensive tests.

Overview
Fixes pnpm >= 11 frozen installs failing with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH because overrides were only written to package.json pnpm.overrides, while pnpm 11 reads them from pnpm-workspace.yaml.

The pnpm vendor backend now mirrors the same versioned <name>@<version>file: override into pnpm-workspace.yaml (still updating package.json for pnpm 9/10). If no workspace file exists, it creates one with a root-only packages: ['.'] list plus overrides: so pnpm 9 accepts the file. Edits use line splices for byte-stable revert; commit_surfaces unwinds override files if the lock write fails.

vendor --revert deletes a scaffold file it created or removes only its override from an edited file. PnpmMeta gains createdWorkspaceFile / createdWorkspaceOverrides. E2E and production tests no longer use a manual workspace workaround; docs mark the pnpm 11 gap as fixed.

Reviewed by Cursor Bugbot for commit a23abb1. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Migration wipe of revert wiring
    • Fixed by returning AlreadyPatched when package.json and lock are in sync, preventing replacement of ledger entry that would lose revert wiring records.

Create PR

Or push these changes by commenting:

@cursor push d2439191b3
Preview (d2439191b3)
diff --git a/crates/socket-patch-core/src/vendor/pnpm_lock.rs b/crates/socket-patch-core/src/vendor/pnpm_lock.rs
--- a/crates/socket-patch-core/src/vendor/pnpm_lock.rs
+++ b/crates/socket-patch-core/src/vendor/pnpm_lock.rs
@@ -271,11 +271,14 @@
         Err(e) => return done_failure(purl, format!("{PNPM_WORKSPACE} surgery failed: {e}")),
     };
 
-    if !pkg_changed && !lock_changed && ws_edit.new_text.is_none() {
-        // Everything already carries this uuid + the packed integrity: the
-        // project is in sync. The tarball re-pack above was byte-identical
-        // by determinism; synthesize AlreadyPatched and record nothing (the
-        // existing ledger entry stays authoritative).
+    if !pkg_changed && !lock_changed {
+        // package.json and the lock already carry this uuid + the packed
+        // integrity: the core wiring is in sync. The tarball re-pack above was
+        // byte-identical by determinism. Even if the workspace file (pnpm >= 11
+        // surface) needs first-time creation during migration, that must not
+        // replace the ledger entry and wipe package.json/lock revert wiring.
+        // Synthesize AlreadyPatched and record nothing (the existing ledger
+        // entry stays authoritative).
         return done(
             already_patched_result(purl, &project_root.join(&rel_tgz), &record.files),
             None,

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a23abb1. Configure here.

Comment thread crates/socket-patch-core/src/vendor/pnpm_lock.rs
…pm >= 11

pnpm 11 stopped reading `overrides` from package.json's `pnpm` field — it
moved to `pnpm-workspace.yaml` (https://pnpm.io/settings). The pnpm vendor
backend wrote only `package.json` `pnpm.overrides`, so pnpm 11 ignored it and
a frozen install of the committable artifact refused with
`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` — the lock carried an `overrides:` section
but pnpm resolved none from config. The vendored tarball and lock were correct
(they even pass pnpm 11's tarball-URL supply-chain policy); only the override
LOCATION was wrong.

Vendoring now mirrors the same versioned `<name>@<version>` → `file:` selector
into `pnpm-workspace.yaml` alongside the existing package.json override, so
whichever surface the installed pnpm reads matches the lock's `overrides:`
section:

- no workspace file → create one with a root-only `packages: ['.']` list
  (pnpm 9 refuses a workspace file whose `packages` field is missing/empty; `.`
  is the sole importer already and cannot glob a stray `packages/` subtree into
  the workspace the way `packages/*` would) plus the `overrides:` block;
- existing file without `overrides:` → append the section (packages untouched);
- existing `overrides:` section → insert our key (or take over a user's exact
  pin), fail-closed on a conflicting same-name override or an inline mapping.

package.json `pnpm.overrides` is kept for pnpm 9/10. Verified across real pnpm
9.15.9 / 10.34.5 / 11.21.0: a cold `pnpm install --frozen-lockfile --offline`
from only the committable files installs the vendored (patched) bytes with no
config mismatch. `vendor --revert` deletes a file it created (when still the
bare scaffold) or splices its override back out of one it edited; the three
surfaces are committed override-first / lock-last so a lock-write failure never
leaves a desynced override behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… surface

The pnpm >= 11 support (#174) mirrors the tarball override into
`pnpm-workspace.yaml` in addition to `package.json` + `pnpm-lock.yaml`. On the
UPGRADE path — a project vendored by the pre-workspace code, then re-vendored
under the new code — `package.json` and `pnpm-lock.yaml` already carry the
override, so `vendor_pnpm` rewrites neither and its fresh `VendorEntry.wiring`
names ONLY the newly created `pnpm-workspace.yaml` surface.

The CLI vendor flow re-invokes the backend on every installed package (there is
no ledger short-circuit; the `already_vendored` classification only happens
AFTER the backend runs and returns `AlreadyPatched`). So `persist_vendor_entry`
would replace the prior ledger entry wholesale with the workspace-only entry,
dropping the `package.json` + `pnpm-lock.yaml` pre-vendor originals the FIRST
vendoring recorded. `vendor --revert` could then restore only the workspace
file, leaving the override wired into the other two surfaces forever.

Fix: extract the re-vendor reconciliation into `vendor::carry_forward_wiring`
and extend it to (1) union the prior entry's wiring records for surfaces THIS
run left in sync, and (2) OR-merge the pnpm "created this table/file/section"
bookkeeping — both scoped to a same-uuid re-vendor so a new-uuid re-vendor
still rewires every surface fresh. The existing original-fill and go-takeover
carry-forward are unchanged. Revert now byte-restores all three surfaces on the
upgrade path.

Regression test `revendor_upgrade_adds_workspace_and_revert_restores_all_three_surfaces`
stages a pre-workspace vendored project, re-vendors (adding only the workspace
mirror), reconciles, then reverts and asserts package.json + pnpm-lock.yaml +
pnpm-workspace.yaml are all byte-restored to their pre-vendor originals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit f5ee819 into main Aug 15, 2026
42 of 44 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/pnpm11-overrides-location branch August 15, 2026 00:03
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Aug 15, 2026
#174 extracted the re-vendor reconciliation out of the CLI's
persist_vendor_entry into vendor::carry_forward_wiring and extended it
(wiring union for untouched surfaces, pnpm created-surface merge). This
branch had added the cargo lock originals carry-forward to the same
inline block, so both sides rewrote it.

Resolved by keeping main's extraction and moving this branch's cargo
carry-forward into carry_forward_wiring beside the other unconditional,
identity-matched reconciliations: a re-vendor over already-detached
wiring records lock: None, and the crates.io source/checksum pair it
replaced is unrecoverable offline, so the prior entry is its only home.
Placed ahead of the same-uuid early return because that loss happens on
a new-uuid re-vendor too.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Aug 15, 2026
Conflict: crates/socket-patch-core/src/vendor/mod.rs — both sides
widened adjacent re-export lines. Resolved as a union:

  * main (#174) exports state::carry_forward_wiring, the extracted
    re-vendor ledger reconciliation persist_vendor_entry now calls;
  * this branch exports verify::artifact_is_file_shaped and
    verify::compute_dir_inventory, which the CLI needs because
    verify is pub(crate).

Both hunks are kept verbatim; neither side's symbol set changes.

The gem sibling (#177) was already in this branch's base, so main
brings only pnpm/redirect work. carry_forward_wiring reconciles
wiring, pnpm meta and the go-takeover flag and never touches
entry.artifact, so the new fileInventory on a re-vendor keeps the
freshly computed inventory — no interaction. Main added no
VendorArtifact literal, so the mechanical file_inventory field
addition stays complete.

Verified: core lib 2215/0, cli lib 376/0, repair_vendor_e2e 24/0,
in_process_vendor 30/0, e2e_vex_vendor 8/0, setup_contract_gaps
10/0, e2e_gem 8/0, in_process_gem_apply 7/0, e2e_vendor_gem_build
5/0, e2e_vendor_pnpm_build 6/0. vendor/mod.rs is rustfmt-clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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