diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs
index 05d27b75d..c370a14b8 100644
--- a/crates/socket-patch-core/src/patch/redirect/mod.rs
+++ b/crates/socket-patch-core/src/patch/redirect/mod.rs
@@ -4837,6 +4837,17 @@ fn rewrite_bun_lock(
Some(content),
);
+ // Decode each entry's spec once per lock, not once per (patch, entry)
+ // pair. The bundled flag needs a full JSON parse of the entry's meta
+ // object, so it is computed lazily, only for entries whose spec matches
+ // some patch, and cached (#580: the eager per-patch parse made bun scans
+ // O(entries x patches) JSON parses).
+ let specs: Vec> = entries
+ .iter()
+ .map(|e| e.elems.first().and_then(|s| decode_json_string(s)))
+ .collect();
+ let mut bundled: Vec > = vec![None; entries.len()];
+
let mut changed = false;
let mut pinned_any = false;
for dep in &npm {
@@ -4854,18 +4865,20 @@ fn rewrite_bun_lock(
let target_spec = format!("{fname}@{}", dep.version);
let url_spec = format!("{fname}@{}", dep.artifact_url);
let mut matched_any = false;
- for entry in &entries {
- let Some(spec) = entry.elems.first().and_then(|e| decode_json_string(e)) else {
+ for (i, entry) in entries.iter().enumerate() {
+ let Some(spec) = specs[i].as_deref() else {
continue;
};
// Bun unpacks a bundled copy from its PARENT's tarball and never
// reads the entry's spec (#469), so a rewrite here would count
// as redirected (and VEX-attest the patch) while the unpatched
// bundled bytes keep installing. Mirrors npm's `inBundle` guard.
- if is_bundled_entry(entry)
- && (spec == target_spec
- || spec == url_spec
- || is_prior_hosted_bun_spec(&spec, &fname, &dep.artifact_url))
+ // The cheap spec match runs first; the bundled parse only for
+ // a match (#580).
+ if (spec == target_spec
+ || spec == url_spec
+ || is_prior_hosted_bun_spec(spec, &fname, &dep.artifact_url))
+ && *bundled[i].get_or_insert_with(|| is_bundled_entry(entry))
{
matched_any = true;
result.bundled_skipped_uuids.insert(dep.patch_uuid.clone());
@@ -4909,7 +4922,7 @@ fn rewrite_bun_lock(
deps_verbatim = entry.elems[1].clone();
} else if matches!(entry.elems.len(), 2 | 3)
&& entry.elems[1].starts_with('{')
- && is_prior_hosted_bun_spec(&spec, &fname, &dep.artifact_url)
+ && is_prior_hosted_bun_spec(spec, &fname, &dep.artifact_url)
{
// A URL tuple written by an EARLIER redirect whose artifact
// URL has since changed (a patch republish rotates the uuid
@@ -11857,6 +11870,48 @@ mod tests {
);
}
+ /// #580: the bundled check runs only for entries whose spec matches the
+ /// patch. A bundled copy of ANOTHER package (or another version) beside
+ /// the target never warns or keeps the patch out of VEX, while a bundled
+ /// copy of the target itself still does, across several patches.
+ #[test]
+ fn bun_lock_bundled_check_only_applies_to_matching_entries() {
+ let sha512 = format!("sha512-{}==", "A".repeat(86));
+ let ovr = npm_override("is-number", "7.0.0", "http://p.test/isn.tgz", &sha512);
+ let regular = "\"is-number\": [\"is-number@7.0.0\", \"\", {}, \"sha512-UP==\"],";
+ let other_bundled = "\"@bh/bund/kind-of\": [\"kind-of@6.0.3\", \"\", { \"bundled\": true }, \"sha512-KO==\"],";
+ let other_version_bundled = "\"@bh/bund/is-number\": [\"is-number@6.0.0\", \"\", { \"bundled\": true }, \"sha512-OV==\"],";
+
+ let lock = format!("{regular}\n {other_bundled}\n {other_version_bundled}");
+ let mut files = BTreeMap::new();
+ files.insert("bun.lock".to_string(), bun_lock_file(&lock, 1));
+ let mut r = RewriteResult::default();
+ rewrite_bun_lock(&files, std::slice::from_ref(&ovr), &mut r);
+ assert_eq!(r.edits.len(), 1, "{:?}", r.edits);
+ assert_eq!(r.edits[0].key.as_deref(), Some("is-number"));
+ assert!(r.warnings.is_empty(), "{:?}", r.warnings);
+ assert!(r.bundled_skipped_uuids.is_empty());
+ let out = r.files.get("bun.lock").expect("lock rewritten");
+ assert!(out.contains(other_bundled) && out.contains(other_version_bundled));
+
+ // A second patch whose target IS bundled still warns, and only for
+ // its own uuid.
+ let mut kind_of = npm_override("kind-of", "6.0.3", "http://p.test/ko.tgz", &sha512);
+ kind_of.patch_uuid = "33333333-3333-4333-8333-333333333333".into();
+ let mut r = RewriteResult::default();
+ rewrite_bun_lock(&files, &[ovr.clone(), kind_of.clone()], &mut r);
+ assert_eq!(r.edits.len(), 1, "{:?}", r.edits);
+ assert_eq!(
+ warning_codes(&r),
+ vec!["redirect_bun_bundled_instance_skipped"],
+ "{:?}",
+ r.warnings
+ );
+ assert!(r.warnings[0].detail.contains("@bh/bund/kind-of"));
+ assert!(r.bundled_skipped_uuids.contains(&kind_of.patch_uuid));
+ assert!(!r.bundled_skipped_uuids.contains(&ovr.patch_uuid));
+ }
+
/// REGRESSION (#367): `bun patch --commit` keys the project's own patch
/// on the registry `name@version` in package.json (and bun.lock's
/// mirror). Rewiring that package to a hosted URL makes Bun drop the
diff --git a/crates/socket-patch-core/src/vendor/bun_lock.rs b/crates/socket-patch-core/src/vendor/bun_lock.rs
index 4e20d5d52..713ee8142 100644
--- a/crates/socket-patch-core/src/vendor/bun_lock.rs
+++ b/crates/socket-patch-core/src/vendor/bun_lock.rs
@@ -1092,10 +1092,10 @@ fn classify_rewritable(
name: &str,
target_leaf: &str,
) -> Option {
- if is_bundled_entry(entry) {
- return None;
- }
- classify(entry, target_spec, name, target_leaf)
+ // The cheap spec match runs first; the bundled parse only for a match
+ // (#580).
+ let shape = classify(entry, target_spec, name, target_leaf)?;
+ (!is_bundled_entry(entry)).then_some(shape)
}
/// Keys of the bundled entries that resolve the target `name@version`.
@@ -1107,7 +1107,7 @@ fn bundled_matches(
) -> Vec {
entries
.iter()
- .filter(|e| is_bundled_entry(e) && classify(e, target_spec, name, target_leaf).is_some())
+ .filter(|e| classify(e, target_spec, name, target_leaf).is_some() && is_bundled_entry(e))
.map(|e| e.key.clone())
.collect()
}
diff --git a/crates/socket-patch-core/src/vendor/bun_lock_text.rs b/crates/socket-patch-core/src/vendor/bun_lock_text.rs
index 52b9241b6..5513ccd4a 100644
--- a/crates/socket-patch-core/src/vendor/bun_lock_text.rs
+++ b/crates/socket-patch-core/src/vendor/bun_lock_text.rs
@@ -229,6 +229,12 @@ pub(crate) fn is_bundled_entry(entry: &BunEntry) -> bool {
let Some(meta) = entry.elems.iter().skip(1).find(|e| e.starts_with('{')) else {
return false;
};
+ // Fast path (#580): a meta that never spells `bundled` cannot carry the
+ // key. A backslash could hide it behind a JSON escape, so only a meta
+ // with neither skips the parse.
+ if !meta.contains("bundled") && !meta.contains('\\') {
+ return false;
+ }
match serde_json::from_str::(meta) {
Ok(value) => value.get("bundled").and_then(serde_json::Value::as_bool) == Some(true),
Err(_) => meta.contains("\"bundled\""),
@@ -717,6 +723,26 @@ mod tests {
));
}
+ /// The substring fast path (#580) never changes the answer: a meta that
+ /// never spells `bundled` is not bundled, a malformed meta that does
+ /// still fails closed, and a JSON-escaped key still takes the parse.
+ #[test]
+ fn bundled_fast_path_matches_the_full_parse() {
+ let bundled = |line: &str| is_bundled_entry(&parse_entry_line(line).unwrap());
+ assert!(!bundled(
+ r#" "q": ["q@1.0.0", "", { "dependencies": { "a": "1" } }, "sha512-X=="],"#
+ ));
+ assert!(!bundled(
+ r#" "q": ["q@1.0.0", "", { "x": , }, "sha512-X=="],"#
+ ));
+ assert!(bundled(
+ r#" "q": ["q@1.0.0", "", { "bundled": true, "x": , }, "sha512-X=="],"#
+ ));
+ assert!(bundled(
+ r#" "q": ["q@1.0.0", "", { "bundl\u0065d": true }, "sha512-X=="],"#
+ ));
+ }
+
#[test]
fn line_grammar_parses_the_fixture_shapes() {
// Registry 4-tuple with deps and trailing comma.