From 48a13e982b7cc8a7e20c2019d83b83d19ec2d56d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 20:22:16 +0000 Subject: [PATCH 1/4] Start fix for #1606 Assisted-by: Claude Code:claude-opus-5-5 From 3086a4cb7f62ff1c01e81e1a876c28f7a6e99b87 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 20:43:32 +0000 Subject: [PATCH 2/4] Parse each bun.lock once per scan A Bun scan read the same bun.lock three times (the lock inventory, VEX discovery and the hosted redirect), and on a 3000-package lock each parse cost more than the .bun store walk #599 added. Those readers now share one parse of an unchanged text, so Bun rescans and hosted scans spend less CPU. A changed lock is always parsed again. Refs #1606 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/formats/bun/mod.rs | 128 ++++++++++++++++++ .../src/patch/redirect/mod.rs | 9 +- .../src/vendor/lock_inventory/bun.rs | 12 +- 3 files changed, 140 insertions(+), 9 deletions(-) diff --git a/crates/socket-patch-core/src/formats/bun/mod.rs b/crates/socket-patch-core/src/formats/bun/mod.rs index 540207601..35cbb929c 100644 --- a/crates/socket-patch-core/src/formats/bun/mod.rs +++ b/crates/socket-patch-core/src/formats/bun/mod.rs @@ -8,6 +8,8 @@ //! the splice's line coordinates, parse the `packages` section — is //! [`BunTextLock::parse`]; each caller keeps its own refusal wording. +use std::sync::{Arc, Mutex, PoisonError}; + use crate::vendor::bun_lock_text::{check_lock_version, parse_packages_section, BunEntry}; /// Why [`BunTextLock::parse`] refused a lock. @@ -46,4 +48,130 @@ impl BunTextLock { let entries = parse_packages_section(&lines).map_err(BunTextError::Packages)?; Ok(BunTextLock { lines, entries }) } + + /// [`Self::parse`], shared: a scan reads one `bun.lock` in the lock + /// inventory, in VEX discovery and in the hosted redirect, and parsing + /// a large lock is most of a Bun rescan's CPU (#1606). The last text + /// parsed is kept with its result, and the same text (compared in + /// full, so a changed lock is always parsed again) gets that result + /// back. Readers only; a splice edits a copy of [`Self::lines`]. + pub(crate) fn parse_shared(text: &str) -> Result, BunTextError> { + static LAST: ParseMemo = ParseMemo::new(); + LAST.parse(text) + } +} + +/// The last text [`BunTextLock::parse_shared`] parsed, with its result. +struct ParseMemo(Mutex>); + +/// A lock text and what [`BunTextLock::parse`] made of it. +type ParsedText = (Arc, Result, BunTextError>); + +impl ParseMemo { + const fn new() -> Self { + ParseMemo(Mutex::new(None)) + } + + fn parse(&self, text: &str) -> Result, BunTextError> { + let last = || self.0.lock().unwrap_or_else(PoisonError::into_inner); + if let Some((cached, parsed)) = last().as_ref() { + if **cached == *text { + return parsed.clone(); + } + } + let parsed = BunTextLock::parse(text).map(Arc::new); + *last() = Some((Arc::from(text), parsed.clone())); + parsed + } +} + +/// The `packages` entries of a [`BunTextLock::parse_shared`] lock. +#[derive(Clone)] +pub(crate) struct BunEntries(Arc); + +impl BunEntries { + pub(crate) fn new(lock: Arc) -> Self { + BunEntries(lock) + } +} + +impl std::ops::Deref for BunEntries { + type Target = [BunEntry]; + + fn deref(&self) -> &[BunEntry] { + &self.0.entries + } +} + +impl<'a> IntoIterator for &'a BunEntries { + type Item = &'a BunEntry; + type IntoIter = std::slice::Iter<'a, BunEntry>; + + fn into_iter(self) -> Self::IntoIter { + self.0.entries.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const LOCK: &str = r#"{ + "lockfileVersion": 1, + "workspaces": { + "": { "name": "fixture", "dependencies": { "left-pad": "1.3.0" } }, + }, + "packages": { + "left-pad": ["left-pad@1.3.0", "", {}, "sha512-XI5MPz=="], + } +} +"#; + + #[test] + fn same_text_is_parsed_once() { + let memo = ParseMemo::new(); + let first = memo.parse(LOCK).unwrap(); + // An equal text read again (a fresh String) gets the same parse. + let again = memo.parse(&LOCK.to_string()).unwrap(); + assert!(Arc::ptr_eq(&first, &again)); + assert_eq!(first.entries.len(), 1); + assert_eq!(first.entries[0].key, "left-pad"); + } + + #[test] + fn changed_text_is_parsed_again() { + let memo = ParseMemo::new(); + let first = memo.parse(LOCK).unwrap(); + let bumped = LOCK.replace("left-pad@1.3.0", "left-pad@1.3.1"); + let second = memo.parse(&bumped).unwrap(); + assert!(!Arc::ptr_eq(&first, &second)); + assert!(second.entries[0].elems[0].contains("1.3.1")); + // Back to the first text: parsed again, never a stale result. + let third = memo.parse(LOCK).unwrap(); + assert!(third.entries[0].elems[0].contains("1.3.0")); + assert_eq!( + third.lines, + LOCK.split('\n').map(str::to_string).collect::>() + ); + } + + #[test] + fn refusals_are_shared_too() { + let memo = ParseMemo::new(); + let future = LOCK.replace("\"lockfileVersion\": 1,", "\"lockfileVersion\": 99,"); + let refused = memo.parse(&future).err().unwrap(); + assert!(matches!(refused, BunTextError::Version(_))); + assert_eq!(memo.parse(&future).err(), Some(refused)); + assert!(memo.parse(LOCK).is_ok()); + } + + #[test] + fn shared_entries_read_like_the_parse() { + let shared = BunEntries::new(BunTextLock::parse_shared(LOCK).unwrap()); + let parsed = BunTextLock::parse(LOCK).unwrap(); + let keys: Vec<&str> = (&shared).into_iter().map(|e| e.key.as_str()).collect(); + assert_eq!(keys, ["left-pad"]); + assert_eq!(shared.len(), parsed.entries.len()); + assert_eq!(shared[0].elems, parsed.entries[0].elems); + } } diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 231394835..b3d98a81a 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -5501,15 +5501,18 @@ fn berry_catalog_selectors(yarnrc: Option<&str>, name: &str, ranges: &[&str]) -> // deviations). Byte-for-byte twin of the TS `rewriteBun`. fn parse_bun_hosted_lock( content: &str, -) -> Result<(Vec, Vec), RewriteWarning> { +) -> Result<(Vec, crate::formats::bun::BunEntries), RewriteWarning> { use crate::vendor::bun_lock_text::{has_workspace_packages, lock_version}; // The shared gate's `Err` text IS the detail: hosted and vendored refuse // an unsupported head with one message (and one remedy per arm — a // future version means "update socket-patch", a missing integer means // "re-lock"), so the two modes cannot drift apart. - let (lines, entries) = match crate::formats::bun::BunTextLock::parse(content) { - Ok(lock) => (lock.lines, lock.entries), + let (lines, entries) = match crate::formats::bun::BunTextLock::parse_shared(content) { + Ok(lock) => ( + lock.lines.clone(), + crate::formats::bun::BunEntries::new(lock), + ), Err(crate::formats::bun::BunTextError::Version(detail)) => { return Err(RewriteWarning { code: "redirect_bun_lock_unsupported".into(), diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs b/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs index fa8647d92..9c9fec7a2 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs @@ -3,7 +3,7 @@ use std::path::Path; use crate::constants::npm_family::{BUN_LOCK, BUN_LOCKB}; -use crate::formats::bun::{BunTextError, BunTextLock}; +use crate::formats::bun::{BunEntries, BunTextError, BunTextLock}; use crate::patch::redirect::hosted_url_version; use crate::vendor::bun_lock_text::{self, BunEntry}; use crate::vendor::bun_lockb::BunLockb; @@ -19,9 +19,9 @@ use super::{http_url, LockIntegrity, LockfileEntry, UnsupportedNpmLayout}; /// elements. `Err` is the user-facing refusal detail (it names the file): a /// lock the backends refuse — a hand re-indented one included — is one /// neither the inventory nor lockfile discovery reads. -pub(crate) fn bun_text_entries(text: &str) -> Result, String> { - match BunTextLock::parse(text) { - Ok(lock) => Ok(lock.entries), +pub(crate) fn bun_text_entries(text: &str) -> Result { + match BunTextLock::parse_shared(text) { + Ok(lock) => Ok(BunEntries::new(lock)), Err(BunTextError::Version(detail)) => Err(detail), Err(BunTextError::Packages(e)) => Err(format!("{BUN_LOCK}: {e}")), } @@ -118,8 +118,8 @@ pub(super) async fn inventory_bun_in(view: &ProjectView<'_>) -> Option Date: Fri, 23 Oct 2026 20:59:01 +0000 Subject: [PATCH 3/4] Keep no copy of the lock text in the parse memo The shared bun.lock parse kept a copy of the whole text to recognize it, which added to a scan's peak memory. It now checks a text against the lines it already holds, and does not keep a refused lock. Refs #1606 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/formats/bun/mod.rs | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/crates/socket-patch-core/src/formats/bun/mod.rs b/crates/socket-patch-core/src/formats/bun/mod.rs index 35cbb929c..2b736fcf6 100644 --- a/crates/socket-patch-core/src/formats/bun/mod.rs +++ b/crates/socket-patch-core/src/formats/bun/mod.rs @@ -51,21 +51,27 @@ impl BunTextLock { /// [`Self::parse`], shared: a scan reads one `bun.lock` in the lock /// inventory, in VEX discovery and in the hosted redirect, and parsing - /// a large lock is most of a Bun rescan's CPU (#1606). The last text - /// parsed is kept with its result, and the same text (compared in - /// full, so a changed lock is always parsed again) gets that result - /// back. Readers only; a splice edits a copy of [`Self::lines`]. + /// a large lock is most of a Bun rescan's CPU (#1606). The last lock + /// parsed is kept, and a text whose lines are exactly its + /// [`Self::lines`] gets it back; any other text (a changed lock) is + /// parsed again. A refused lock is not kept. Readers only; a splice + /// edits a copy of [`Self::lines`]. pub(crate) fn parse_shared(text: &str) -> Result, BunTextError> { static LAST: ParseMemo = ParseMemo::new(); LAST.parse(text) } -} -/// The last text [`BunTextLock::parse_shared`] parsed, with its result. -struct ParseMemo(Mutex>); + /// Whether `text` is the text this lock was parsed from. + fn is_parse_of(&self, text: &str) -> bool { + let mut lines = self.lines.iter(); + text.split('\n') + .all(|line| lines.next().is_some_and(|parsed| parsed == line)) + && lines.next().is_none() + } +} -/// A lock text and what [`BunTextLock::parse`] made of it. -type ParsedText = (Arc, Result, BunTextError>); +/// The last lock [`BunTextLock::parse_shared`] parsed. +struct ParseMemo(Mutex>>); impl ParseMemo { const fn new() -> Self { @@ -74,14 +80,12 @@ impl ParseMemo { fn parse(&self, text: &str) -> Result, BunTextError> { let last = || self.0.lock().unwrap_or_else(PoisonError::into_inner); - if let Some((cached, parsed)) = last().as_ref() { - if **cached == *text { - return parsed.clone(); - } + if let Some(lock) = last().as_ref().filter(|lock| lock.is_parse_of(text)) { + return Ok(Arc::clone(lock)); } - let parsed = BunTextLock::parse(text).map(Arc::new); - *last() = Some((Arc::from(text), parsed.clone())); - parsed + let lock = Arc::new(BunTextLock::parse(text)?); + *last() = Some(Arc::clone(&lock)); + Ok(lock) } } @@ -156,13 +160,34 @@ mod tests { } #[test] - fn refusals_are_shared_too() { + fn refusals_are_not_kept() { let memo = ParseMemo::new(); + let first = memo.parse(LOCK).unwrap(); let future = LOCK.replace("\"lockfileVersion\": 1,", "\"lockfileVersion\": 99,"); let refused = memo.parse(&future).err().unwrap(); assert!(matches!(refused, BunTextError::Version(_))); assert_eq!(memo.parse(&future).err(), Some(refused)); - assert!(memo.parse(LOCK).is_ok()); + // The refusal left the last good parse in place. + assert!(Arc::ptr_eq(&first, &memo.parse(LOCK).unwrap())); + } + + #[test] + fn a_prefix_or_extension_is_another_text() { + let memo = ParseMemo::new(); + let first = memo.parse(LOCK).unwrap(); + for other in [ + format!("{LOCK}\n"), + LOCK.trim_end().to_string(), + LOCK.replace('\n', "\r\n"), + ] { + let parsed = memo.parse(&other).unwrap(); + assert!(!Arc::ptr_eq(&first, &parsed), "{other:?}"); + assert_eq!( + parsed.lines, + other.split('\n').map(str::to_string).collect::>() + ); + memo.parse(LOCK).unwrap(); + } } #[test] From cdbe8317928b63884525f754ac8f8e4d07465e27 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 21:29:51 +0000 Subject: [PATCH 4/4] Share the bun.lock parse with the vendored preflight Address review: the readers now get the shared parse itself instead of a wrapper, the hosted redirect copies the lock's lines only when it splices one, and the vendored preflight, which read bun.lock once per patched package, uses the shared parse too. Refs #1606 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/formats/bun/mod.rs | 43 ++++--------------- .../src/patch/redirect/mod.rs | 24 +++++------ .../src/patch/redirect/upstream/npm.rs | 8 ++-- .../socket-patch-core/src/vendor/bun_lock.rs | 14 +++--- .../src/vendor/lock_inventory/bun.rs | 11 ++--- .../socket-patch-core/src/vex/discover/bun.rs | 6 +-- 6 files changed, 40 insertions(+), 66 deletions(-) diff --git a/crates/socket-patch-core/src/formats/bun/mod.rs b/crates/socket-patch-core/src/formats/bun/mod.rs index 2b736fcf6..b67278751 100644 --- a/crates/socket-patch-core/src/formats/bun/mod.rs +++ b/crates/socket-patch-core/src/formats/bun/mod.rs @@ -50,8 +50,9 @@ impl BunTextLock { } /// [`Self::parse`], shared: a scan reads one `bun.lock` in the lock - /// inventory, in VEX discovery and in the hosted redirect, and parsing - /// a large lock is most of a Bun rescan's CPU (#1606). The last lock + /// inventory, in VEX discovery and in the hosted redirect (and the + /// vendored preflight once per package), and parsing a large lock is + /// most of a Bun rescan's CPU (#1606). The last lock /// parsed is kept, and a text whose lines are exactly its /// [`Self::lines`] gets it back; any other text (a changed lock) is /// parsed again. A refused lock is not kept. Readers only; a splice @@ -89,33 +90,6 @@ impl ParseMemo { } } -/// The `packages` entries of a [`BunTextLock::parse_shared`] lock. -#[derive(Clone)] -pub(crate) struct BunEntries(Arc); - -impl BunEntries { - pub(crate) fn new(lock: Arc) -> Self { - BunEntries(lock) - } -} - -impl std::ops::Deref for BunEntries { - type Target = [BunEntry]; - - fn deref(&self) -> &[BunEntry] { - &self.0.entries - } -} - -impl<'a> IntoIterator for &'a BunEntries { - type Item = &'a BunEntry; - type IntoIter = std::slice::Iter<'a, BunEntry>; - - fn into_iter(self) -> Self::IntoIter { - self.0.entries.iter() - } -} - #[cfg(test)] mod tests { use super::*; @@ -191,12 +165,11 @@ mod tests { } #[test] - fn shared_entries_read_like_the_parse() { - let shared = BunEntries::new(BunTextLock::parse_shared(LOCK).unwrap()); + fn shared_parse_reads_like_the_parse() { + let shared = BunTextLock::parse_shared(LOCK).unwrap(); let parsed = BunTextLock::parse(LOCK).unwrap(); - let keys: Vec<&str> = (&shared).into_iter().map(|e| e.key.as_str()).collect(); - assert_eq!(keys, ["left-pad"]); - assert_eq!(shared.len(), parsed.entries.len()); - assert_eq!(shared[0].elems, parsed.entries[0].elems); + assert_eq!(shared.lines, parsed.lines); + assert_eq!(shared.entries.len(), parsed.entries.len()); + assert_eq!(shared.entries[0].elems, parsed.entries[0].elems); } } diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index b3d98a81a..c505e94a5 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -5501,18 +5501,15 @@ fn berry_catalog_selectors(yarnrc: Option<&str>, name: &str, ranges: &[&str]) -> // deviations). Byte-for-byte twin of the TS `rewriteBun`. fn parse_bun_hosted_lock( content: &str, -) -> Result<(Vec, crate::formats::bun::BunEntries), RewriteWarning> { +) -> Result, RewriteWarning> { use crate::vendor::bun_lock_text::{has_workspace_packages, lock_version}; // The shared gate's `Err` text IS the detail: hosted and vendored refuse // an unsupported head with one message (and one remedy per arm — a // future version means "update socket-patch", a missing integer means // "re-lock"), so the two modes cannot drift apart. - let (lines, entries) = match crate::formats::bun::BunTextLock::parse_shared(content) { - Ok(lock) => ( - lock.lines.clone(), - crate::formats::bun::BunEntries::new(lock), - ), + let lock = match crate::formats::bun::BunTextLock::parse_shared(content) { + Ok(lock) => lock, Err(crate::formats::bun::BunTextError::Version(detail)) => { return Err(RewriteWarning { code: "redirect_bun_lock_unsupported".into(), @@ -5546,7 +5543,7 @@ fn parse_bun_hosted_lock( // workspaces is kept at 0 by an in-place install on 1.2.0, 1.2.23 and // 1.3.0 and bumped to 1 by 1.3.9 and every later release; that case is // accepted here either way.) - if lock_version(content) == Some(0) && has_workspace_packages(&entries) { + if lock_version(content) == Some(0) && has_workspace_packages(&lock.entries) { return Err(RewriteWarning { code: "redirect_bun_workspace_unsupported".into(), detail: "Bun version-0 workspace locks cannot preserve hosted tarballs on frozen \ @@ -5559,7 +5556,7 @@ fn parse_bun_hosted_lock( }); } - Ok((lines, entries)) + Ok(lock) } /// The warning for a package the bun rewriters pinned to its hosted URL that @@ -5630,13 +5627,16 @@ fn rewrite_bun_lock( let Some(content) = files.get("bun.lock") else { return; }; - let (mut lines, entries) = match parse_bun_hosted_lock(content) { - Ok(parsed) => parsed, + let lock = match parse_bun_hosted_lock(content) { + Ok(lock) => lock, Err(warning) => { result.warnings.push(warning); return; } }; + let entries = &lock.entries; + // The shared parse is never edited: the first splice copies its lines. + let mut lines = std::borrow::Cow::Borrowed(lock.lines.as_slice()); let user_patched = crate::vendor::bun_lock_text::patched_dependency_keys( files.get("package.json").map(String::as_str), @@ -5791,7 +5791,7 @@ fn rewrite_bun_lock( if rebuilt == original { continue; } - lines[entry.line_idx] = rebuilt.clone(); + lines.to_mut()[entry.line_idx] = rebuilt.clone(); result.edits.push(FileEdit { path: "bun.lock".into(), kind: "redirect_bun_lock_package".into(), @@ -5833,7 +5833,7 @@ fn rewrite_bun_lock( // holds a pin is touched, and the manifests are the engine's advisory // `/package.json` reads. if pinned_any { - let heals = crate::vendor::bun_lock_text::heal_workspace_literals(&mut lines, |dir| { + let heals = crate::vendor::bun_lock_text::heal_workspace_literals(lines.to_mut(), |dir| { let rel = if dir.is_empty() { "package.json".to_string() } else { diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs index c3e8cf31c..7d8b0abfe 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs @@ -2607,16 +2607,18 @@ pub(crate) async fn restore_bun_locks( let Some(text) = read_or_refuse(view, rel, &pins, &mut result).await else { continue; }; - let (mut lines, entries) = match super::super::parse_bun_hosted_lock(&text) { - Ok(parsed) => parsed, + let lock = match super::super::parse_bun_hosted_lock(&text) { + Ok(lock) => lock, Err(w) => { refuse_all_in(&pins, rel, &mut result, w.detail); continue; } }; + let entries = &lock.entries; + let mut lines = lock.lines.clone(); // (line, uuid, name, version, deps) let mut hits: Vec<(usize, String, String, String, String)> = Vec::new(); - for entry in &entries { + for entry in entries { if !matches!(entry.elems.len(), 2 | 3) || !entry.elems[1].starts_with('{') { continue; } diff --git a/crates/socket-patch-core/src/vendor/bun_lock.rs b/crates/socket-patch-core/src/vendor/bun_lock.rs index f864c1e5f..9fc219707 100644 --- a/crates/socket-patch-core/src/vendor/bun_lock.rs +++ b/crates/socket-patch-core/src/vendor/bun_lock.rs @@ -206,10 +206,9 @@ pub async fn preflight_vendor(project_root: &Path) -> Result<(), (&'static str, } Err(error) => return Err(("vendor_lockfile_missing", error.to_string())), }; - let entries = BunTextLock::parse(&text) - .map_err(|e| ("vendor_lockfile_version_unsupported", e.detail()))? - .entries; - check_workspace_compatibility(&text, &entries) + let lock = BunTextLock::parse_shared(&text) + .map_err(|e| ("vendor_lockfile_version_unsupported", e.detail()))?; + check_workspace_compatibility(&text, &lock.entries) } /// Whether `bun.lock` already wires EVERY packages entry resolving the npm @@ -281,13 +280,12 @@ pub async fn wired_instances_all_ours( } Err(error) => return Err(("vendor_lockfile_missing", error.to_string())), }; - let entries = BunTextLock::parse(&text) - .map_err(|e| ("vendor_lockfile_version_unsupported", e.detail()))? - .entries; + let lock = BunTextLock::parse_shared(&text) + .map_err(|e| ("vendor_lockfile_version_unsupported", e.detail()))?; let target_spec = format!("{name}@{version}"); let target_leaf = tgz_rel_leaf(&name, &version); let mut matched = 0usize; - for entry in &entries { + for entry in &lock.entries { match classify_rewritable(entry, &target_spec, &name, &target_leaf) { Some(TupleShape::Ours { .. }) => matched += 1, Some(TupleShape::Registry) => return Ok(false), diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs b/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs index 9c9fec7a2..9aa634919 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/bun.rs @@ -1,9 +1,10 @@ //! `bun.lock` / `bun.lockb`: the registry views. use std::path::Path; +use std::sync::Arc; use crate::constants::npm_family::{BUN_LOCK, BUN_LOCKB}; -use crate::formats::bun::{BunEntries, BunTextError, BunTextLock}; +use crate::formats::bun::{BunTextError, BunTextLock}; use crate::patch::redirect::hosted_url_version; use crate::vendor::bun_lock_text::{self, BunEntry}; use crate::vendor::bun_lockb::BunLockb; @@ -19,9 +20,9 @@ use super::{http_url, LockIntegrity, LockfileEntry, UnsupportedNpmLayout}; /// elements. `Err` is the user-facing refusal detail (it names the file): a /// lock the backends refuse — a hand re-indented one included — is one /// neither the inventory nor lockfile discovery reads. -pub(crate) fn bun_text_entries(text: &str) -> Result { +pub(crate) fn bun_text_entries(text: &str) -> Result, String> { match BunTextLock::parse_shared(text) { - Ok(lock) => Ok(BunEntries::new(lock)), + Ok(lock) => Ok(lock), Err(BunTextError::Version(detail)) => Err(detail), Err(BunTextError::Packages(e)) => Err(format!("{BUN_LOCK}: {e}")), } @@ -115,10 +116,10 @@ pub(super) async fn inventory_bun(root: &Path) -> Option> { pub(super) async fn inventory_bun_in(view: &ProjectView<'_>) -> Option> { let text = view.read_text(BUN_LOCK).await.ok()?; - let entries = bun_text_entries(&text).ok()?; + let lock = bun_text_entries(&text).ok()?; let mut out = Vec::new(); - for entry in &entries { + for entry in &lock.entries { if let Some(hosted) = hosted_pin_entry(entry) { out.push(hosted); continue; diff --git a/crates/socket-patch-core/src/vex/discover/bun.rs b/crates/socket-patch-core/src/vex/discover/bun.rs index 42615ef1d..85b24e95b 100644 --- a/crates/socket-patch-core/src/vex/discover/bun.rs +++ b/crates/socket-patch-core/src/vex/discover/bun.rs @@ -124,8 +124,8 @@ async fn extract_text(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { let Some(text) = ctx.read_text(BUN_LOCK, out).await else { return; }; - let entries = match bun_text_entries(&text) { - Ok(entries) => entries, + let lock = match bun_text_entries(&text) { + Ok(lock) => lock, Err(detail) => { out.diag(DIAG_LOCKFILE_UNPARSEABLE, BUN_LOCK, detail); return; @@ -133,7 +133,7 @@ async fn extract_text(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { }; let mut bundled = Bundled::default(); let mut unwired = Unwired::default(); - for entry in &entries { + for entry in &lock.entries { let elems = &entry.elems; let Some(spec) = elems.first().and_then(|e| decode_json_string(e)) else { continue;