Skip to content

Commit bf11989

Browse files
authored
fix: Improve parity with real Bun lockfile format for prune (#11048)
### Description Updating our Bun lockfile parser to achieve full parity with `--frozen-lockfile`. ### Testing Instructions I've added fixtures from #11007 and hand-tested each fixture with real repositories with this newly updated logic. You'll also note that I've removed some unit testing in favor of snapshot testing. Ultimately, we care that the lockfile gets pruned and created correctly, which is what the snapshots are testing for. Given Bun's relatively fast iteration speed, the internals of how we compute the pruned lockfile don't help as much for now. We can solidify with unit tests in the future if we need to.
1 parent 29c8af8 commit bf11989

25 files changed

+32672
-1926
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/turborepo-lockfiles/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ turbopath = { path = "../turborepo-paths" }
2929
turborepo-errors = { workspace = true }
3030

3131
[dev-dependencies]
32+
insta = { workspace = true }
3233
pretty_assertions = "1.3"
3334
test-case = "3.1.0"
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
//! Package indexing for efficient lockfile lookups.
2+
3+
use std::{collections::HashMap, sync::Arc};
4+
5+
use super::{PackageEntry, types::PackageKey};
6+
7+
type StringRef = Arc<str>;
8+
9+
#[derive(Debug, Clone)]
10+
pub struct PackageIndex {
11+
/// Direct lookup by lockfile key (e.g., "lodash", "parent/dep")
12+
by_key: HashMap<StringRef, PackageEntry>,
13+
14+
/// Lookup by ident (e.g., "lodash@4.17.21")
15+
/// Maps ident -> lockfile key
16+
/// Multiple keys may map to the same ident (nested versions)
17+
by_ident: HashMap<StringRef, Vec<StringRef>>,
18+
19+
/// Workspace-scoped lookup for quick resolution
20+
/// Maps (workspace_name, package_name) -> lockfile key
21+
workspace_scoped: HashMap<(StringRef, StringRef), StringRef>,
22+
23+
/// Bundled dependency lookup
24+
/// Maps (parent_key, dep_name) -> lockfile key
25+
bundled_deps: HashMap<(StringRef, StringRef), StringRef>,
26+
}
27+
28+
impl PackageIndex {
29+
/// Create a new package index from a packages map.
30+
pub fn new(packages: &super::Map<String, PackageEntry>) -> Self {
31+
let mut by_key = HashMap::with_capacity(packages.len());
32+
let mut by_ident: HashMap<StringRef, Vec<StringRef>> = HashMap::new();
33+
let mut workspace_scoped = HashMap::new();
34+
let mut bundled_deps = HashMap::new();
35+
36+
// First pass: populate by_key and by_ident
37+
for (key, entry) in packages {
38+
// Convert key to Arc<str> once
39+
let key_ref: StringRef = Arc::from(key.as_str());
40+
41+
by_key.insert(Arc::clone(&key_ref), entry.clone());
42+
43+
// Index by ident - convert ident to Arc<str>
44+
let ident_ref: StringRef = Arc::from(entry.ident.as_str());
45+
by_ident
46+
.entry(ident_ref)
47+
.or_default()
48+
.push(Arc::clone(&key_ref));
49+
50+
// Index workspace-scoped packages
51+
// Example: "workspace/package" -> ("workspace", "package")
52+
let parsed_key = PackageKey::parse(key);
53+
if let Some(parent) = parsed_key.parent() {
54+
let parent_ref: StringRef = Arc::from(parent);
55+
let name_ref: StringRef = Arc::from(parsed_key.name());
56+
workspace_scoped.insert((parent_ref, name_ref), Arc::clone(&key_ref));
57+
}
58+
59+
// Index bundled dependencies
60+
if key.contains('/') {
61+
let parsed_key = PackageKey::parse(key);
62+
if let Some(parent) = parsed_key.parent()
63+
&& let Some(info) = &entry.info
64+
&& info
65+
.other
66+
.get("bundled")
67+
.and_then(|v| v.as_bool())
68+
.unwrap_or(false)
69+
{
70+
let parent_ref: StringRef = Arc::from(parent);
71+
let name_ref: StringRef = Arc::from(parsed_key.name());
72+
bundled_deps.insert((parent_ref, name_ref), Arc::clone(&key_ref));
73+
}
74+
}
75+
}
76+
77+
// Sort by_ident vectors for deterministic selection (prefer workspace-scoped)
78+
for keys in by_ident.values_mut() {
79+
keys.sort();
80+
}
81+
82+
Self {
83+
by_key,
84+
by_ident,
85+
workspace_scoped,
86+
bundled_deps,
87+
}
88+
}
89+
90+
/// Get a package entry by lockfile key.
91+
#[cfg(test)]
92+
pub fn get_by_key(&self, key: &str) -> Option<&PackageEntry> {
93+
self.by_key.get(key)
94+
}
95+
96+
/// Returns the number of packages in the index.
97+
#[cfg(test)]
98+
pub fn len(&self) -> usize {
99+
self.by_key.len()
100+
}
101+
102+
/// Get a package entry by ident (e.g., "lodash@4.17.21").
103+
///
104+
/// If multiple keys map to the same ident, returns the first one
105+
/// (which is typically the workspace-scoped one due to sorting).
106+
pub fn get_by_ident(&self, ident: &str) -> Option<(&str, &PackageEntry)> {
107+
let keys = self.by_ident.get(ident)?;
108+
let key = keys.first()?;
109+
let entry = self.by_key.get(key)?;
110+
Some((key.as_ref(), entry))
111+
}
112+
113+
/// Get all lockfile keys that map to a given ident.
114+
///
115+
/// This is useful when you need to find all aliases for a package.
116+
#[cfg(test)]
117+
pub fn get_all_keys_for_ident(&self, ident: &str) -> Option<&[StringRef]> {
118+
self.by_ident.get(ident).map(|v| v.as_slice())
119+
}
120+
121+
/// Get a workspace-scoped package entry.
122+
///
123+
/// For example, get_workspace_scoped("web", "lodash") looks up
124+
/// "web/lodash".
125+
pub fn get_workspace_scoped(&self, workspace: &str, package: &str) -> Option<&PackageEntry> {
126+
// Use a temporary Arc for the lookup key
127+
let lookup_key = (Arc::from(workspace), Arc::from(package));
128+
let key = self.workspace_scoped.get(&lookup_key)?;
129+
self.by_key.get(key)
130+
}
131+
132+
/// Get a bundled dependency entry.
133+
///
134+
/// For example, get_bundled("parent", "dep") looks up "parent/dep" if it's
135+
/// bundled.
136+
#[cfg(test)]
137+
pub fn get_bundled(&self, parent: &str, dep: &str) -> Option<&PackageEntry> {
138+
// Use a temporary Arc for the lookup key
139+
let lookup_key = (Arc::from(parent), Arc::from(dep));
140+
let key = self.bundled_deps.get(&lookup_key)?;
141+
self.by_key.get(key)
142+
}
143+
144+
/// Find a package entry by name, searching in order:
145+
/// 1. Workspace-scoped (if workspace provided)
146+
/// 2. Top-level / hoisted
147+
/// 3. Bundled dependencies
148+
pub fn find_package<'a>(
149+
&'a self,
150+
workspace: Option<&str>,
151+
name: &'a str,
152+
) -> Option<(&'a str, &'a PackageEntry)> {
153+
// Try workspace-scoped first
154+
if let Some(ws) = workspace {
155+
let lookup_key = (Arc::from(ws), Arc::from(name));
156+
if let Some(key) = self.workspace_scoped.get(&lookup_key)
157+
&& let Some(entry) = self.by_key.get(key)
158+
{
159+
return Some((key.as_ref(), entry));
160+
}
161+
}
162+
163+
// Try top-level
164+
if let Some(entry) = self.by_key.get(name) {
165+
return Some((name, entry));
166+
}
167+
168+
// Try bundled (search all parents)
169+
for ((_parent, dep_name), key) in &self.bundled_deps {
170+
if dep_name.as_ref() == name
171+
&& let Some(entry) = self.by_key.get(key)
172+
{
173+
return Some((key.as_ref(), entry));
174+
}
175+
}
176+
177+
None
178+
}
179+
}
180+
181+
#[cfg(test)]
182+
mod tests {
183+
use serde_json::json;
184+
185+
use super::*;
186+
use crate::bun::{Map, PackageInfo};
187+
188+
fn create_test_entry(ident: &str) -> PackageEntry {
189+
PackageEntry {
190+
ident: ident.to_string(),
191+
registry: Some("".to_string()),
192+
info: Some(PackageInfo::default()),
193+
checksum: Some("sha512".to_string()),
194+
root: None,
195+
}
196+
}
197+
198+
fn create_bundled_entry(ident: &str) -> PackageEntry {
199+
let mut info = PackageInfo::default();
200+
info.other.insert("bundled".to_string(), json!(true));
201+
PackageEntry {
202+
ident: ident.to_string(),
203+
registry: Some("".to_string()),
204+
info: Some(info),
205+
checksum: Some("sha512".to_string()),
206+
root: None,
207+
}
208+
}
209+
210+
#[test]
211+
fn test_package_index_basic_lookup() {
212+
let mut packages = Map::new();
213+
packages.insert("lodash".to_string(), create_test_entry("lodash@4.17.21"));
214+
packages.insert("react".to_string(), create_test_entry("react@18.0.0"));
215+
216+
let index = PackageIndex::new(&packages);
217+
218+
assert_eq!(index.len(), 2);
219+
assert!(index.get_by_key("lodash").is_some());
220+
assert!(index.get_by_key("react").is_some());
221+
assert!(index.get_by_key("nonexistent").is_none());
222+
}
223+
224+
#[test]
225+
fn test_package_index_by_ident() {
226+
let mut packages = Map::new();
227+
packages.insert("lodash".to_string(), create_test_entry("lodash@4.17.21"));
228+
packages.insert(
229+
"web/lodash".to_string(),
230+
create_test_entry("lodash@4.17.21"),
231+
);
232+
233+
let index = PackageIndex::new(&packages);
234+
235+
// Should find the entry
236+
let (_key, entry) = index.get_by_ident("lodash@4.17.21").unwrap();
237+
assert_eq!(entry.ident, "lodash@4.17.21");
238+
239+
// Should have both keys indexed
240+
let all_keys = index.get_all_keys_for_ident("lodash@4.17.21").unwrap();
241+
assert_eq!(all_keys.len(), 2);
242+
assert!(all_keys.iter().any(|k| k.as_ref() == "lodash"));
243+
assert!(all_keys.iter().any(|k| k.as_ref() == "web/lodash"));
244+
}
245+
246+
#[test]
247+
fn test_package_index_workspace_scoped() {
248+
let mut packages = Map::new();
249+
packages.insert(
250+
"web/lodash".to_string(),
251+
create_test_entry("lodash@4.17.21"),
252+
);
253+
packages.insert(
254+
"@repo/ui/react".to_string(),
255+
create_test_entry("react@18.0.0"),
256+
);
257+
258+
let index = PackageIndex::new(&packages);
259+
260+
// Workspace-scoped lookup
261+
let entry = index.get_workspace_scoped("web", "lodash").unwrap();
262+
assert_eq!(entry.ident, "lodash@4.17.21");
263+
264+
let entry = index.get_workspace_scoped("@repo/ui", "react").unwrap();
265+
assert_eq!(entry.ident, "react@18.0.0");
266+
267+
// Non-existent workspace
268+
assert!(
269+
index
270+
.get_workspace_scoped("nonexistent", "lodash")
271+
.is_none()
272+
);
273+
}
274+
275+
#[test]
276+
fn test_package_index_bundled() {
277+
let mut packages = Map::new();
278+
packages.insert("parent".to_string(), create_test_entry("parent@1.0.0"));
279+
packages.insert(
280+
"parent/bundled-dep".to_string(),
281+
create_bundled_entry("bundled-dep@2.0.0"),
282+
);
283+
284+
let index = PackageIndex::new(&packages);
285+
286+
// Bundled lookup
287+
let entry = index.get_bundled("parent", "bundled-dep").unwrap();
288+
assert_eq!(entry.ident, "bundled-dep@2.0.0");
289+
290+
// Non-existent bundled
291+
assert!(index.get_bundled("parent", "nonexistent").is_none());
292+
}
293+
294+
#[test]
295+
fn test_package_index_find_package() {
296+
let mut packages = Map::new();
297+
packages.insert("lodash".to_string(), create_test_entry("lodash@4.17.21"));
298+
packages.insert(
299+
"web/lodash".to_string(),
300+
create_test_entry("lodash@4.17.20"),
301+
);
302+
packages.insert(
303+
"parent/bundled".to_string(),
304+
create_bundled_entry("bundled@1.0.0"),
305+
);
306+
307+
let index = PackageIndex::new(&packages);
308+
309+
// Workspace-scoped takes priority
310+
let (key, entry) = index.find_package(Some("web"), "lodash").unwrap();
311+
assert_eq!(key, "web/lodash");
312+
assert_eq!(entry.ident, "lodash@4.17.20");
313+
314+
// Falls back to top-level if workspace not found
315+
let (key, entry) = index.find_package(Some("other"), "lodash").unwrap();
316+
assert_eq!(key, "lodash");
317+
assert_eq!(entry.ident, "lodash@4.17.21");
318+
319+
// Finds bundled dependencies
320+
let (key, entry) = index.find_package(None, "bundled").unwrap();
321+
assert_eq!(key, "parent/bundled");
322+
assert_eq!(entry.ident, "bundled@1.0.0");
323+
}
324+
}

0 commit comments

Comments
 (0)