diff --git a/crates/socket-patch-core/src/vendor/bun_lock.rs b/crates/socket-patch-core/src/vendor/bun_lock.rs index 7acb22ac..294377a4 100644 --- a/crates/socket-patch-core/src/vendor/bun_lock.rs +++ b/crates/socket-patch-core/src/vendor/bun_lock.rs @@ -41,7 +41,9 @@ use crate::vendor::bun_lock_text::{ }; use super::common::{already_patched_result, refused}; -use super::npm_common::{done_failure, guard_coordinates, guard_revert_uuid_dir, stage_patch_pack}; +use super::npm_common::{ + done_failure, guard_coordinates, guard_revert_uuid_dir, stage_patch_pack, tgz_rel_leaf, +}; use super::path::parse_vendor_path; use super::state::{ write_marker, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord, @@ -107,9 +109,10 @@ pub(crate) async fn vendor_bun( // ── 3. Pre-flight: at least one rewritable instance ────────────────── let target_spec = format!("{name}@{version}"); + let target_leaf = tgz_rel_leaf(name, version); let has_match = entries .iter() - .any(|e| classify(e, &target_spec, name).is_some()); + .any(|e| classify(e, &target_spec, name, &target_leaf).is_some()); if !has_match { return refused( "vendor_lock_entry_not_found", @@ -166,7 +169,7 @@ pub(crate) async fn vendor_bun( let mut wiring: Vec = Vec::new(); let mut changed = false; for entry in &entries { - let Some(shape) = classify(entry, &target_spec, name) else { + let Some(shape) = classify(entry, &target_spec, name, &target_leaf) else { continue; }; let (deps_verbatim, was_ours) = match shape { @@ -181,8 +184,16 @@ pub(crate) async fn vendor_bun( } }; let original_line = lines[entry.line_idx].clone(); + // Lines come from a bare `split('\n')`, so a CRLF lock's lines carry + // a trailing `\r` (the grammar trims it away when parsing). Re-emit + // it verbatim: the surgery must never mix line endings. + let cr = if original_line.ends_with('\r') { + "\r" + } else { + "" + }; let new_line = format!( - "{indent}{key}: [\"{name}@{rel_tgz}\", {deps}, \"{integrity}\"]{comma}", + "{indent}{key}: [\"{name}@{rel_tgz}\", {deps}, \"{integrity}\"]{comma}{cr}", indent = entry.indent, key = entry.key_raw, deps = deps_verbatim, @@ -422,8 +433,17 @@ enum TupleShape { /// Classify an entry against the target: `Some(Registry)` for the exact /// `name@version` registry tuple, `Some(Ours{..})` for one of our own -/// `.socket/vendor/npm/` tuples for the same package, `None` otherwise. -fn classify(entry: &BunEntry, target_spec: &str, name: &str) -> Option { +/// `.socket/vendor/npm/` tuples for the same `name@version` (any uuid), +/// `None` otherwise. The Ours arm matches on the uuid-independent tarball +/// leaf, NOT the name alone: a vendored tuple for ANOTHER version of the +/// same package is someone else's edit (two patched versions can coexist in +/// one lock — nested instances) and must never be cross-clobbered. +fn classify( + entry: &BunEntry, + target_spec: &str, + name: &str, + target_leaf: &str, +) -> Option { let spec = decode_json_string(entry.elems.first()?)?; match entry.elems.len() { 4 if spec == target_spec @@ -439,7 +459,7 @@ fn classify(entry: &BunEntry, target_spec: &str, name: &str) -> Option