Skip to content

Commit bf9db59

Browse files
authored
fix: Improve version resolution in Bun lockfiles (#11095)
### Description We previously were not checking semver versions when building up the lockfiles, so we had the potential to violate the semver ranges of the original lockfile. Now, we do check them so that we ensure we're preserving semver intent in the lockfile. ### Testing Instructions I've tested this against the reproduction provided in #11007 and added some more fixtures for CI.
1 parent 23641b7 commit bf9db59

18 files changed

+2728
-229
lines changed

crates/turborepo-lockfiles/src/bun/mod.rs

Lines changed: 129 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ use biome_json_formatter::context::JsonFormatOptions;
9494
use biome_json_parser::JsonParserOptions;
9595
use id::PossibleKeyIter;
9696
use itertools::Itertools as _;
97+
use semver::{Version, VersionReq};
9798
use serde::{Deserialize, Deserializer, Serialize, Serializer};
9899
use serde_json::Value;
99100
use turbopath::RelativeUnixPathBuf;
@@ -389,6 +390,116 @@ impl BunLockfile {
389390
version,
390391
}))
391392
}
393+
394+
/// Check if a package version satisfies a version specification.
395+
///
396+
/// Returns true if the version satisfies the spec, false otherwise.
397+
/// For non-semver specs (tags, catalogs, workspaces), returns true.
398+
fn version_satisfies_spec(&self, version: &str, version_spec: &str) -> bool {
399+
let spec = VersionSpec::parse(version_spec);
400+
401+
match spec {
402+
VersionSpec::Semver(spec_str) => {
403+
// Parse both the requirement and the version
404+
let Ok(req) = VersionReq::parse(&spec_str) else {
405+
// If we can't parse the requirement, be lenient and accept it
406+
return true;
407+
};
408+
409+
let Ok(ver) = Version::parse(version) else {
410+
// If we can't parse the version, be lenient and accept it
411+
return true;
412+
};
413+
414+
req.matches(&ver)
415+
}
416+
// For non-semver specs (tags, catalogs, workspace), accept any version
417+
// since validation happens elsewhere
418+
_ => true,
419+
}
420+
}
421+
422+
/// Find a package version that satisfies the given version spec.
423+
///
424+
/// Searches in order:
425+
/// 1. Workspace-scoped entries
426+
/// 2. Top-level entries
427+
/// 3. Nested/aliased entries (by searching all idents)
428+
fn find_matching_version(
429+
&self,
430+
workspace_name: &str,
431+
name: &str,
432+
version_spec: &str,
433+
override_version: &str,
434+
resolved_version: &str,
435+
) -> Result<Option<crate::Package>, crate::Error> {
436+
// Try workspace-scoped first
437+
if let Some(entry) = self.index.get_workspace_scoped(workspace_name, name)
438+
&& let Some(pkg) =
439+
self.process_package_entry(entry, name, override_version, resolved_version)?
440+
&& self.version_satisfies_spec(&pkg.version, version_spec)
441+
{
442+
return Ok(Some(pkg));
443+
}
444+
445+
// Try hoisted/top-level
446+
if let Some((_key, entry)) = self.index.find_package(Some(workspace_name), name)
447+
&& let Some(pkg) =
448+
self.process_package_entry(entry, name, override_version, resolved_version)?
449+
&& self.version_satisfies_spec(&pkg.version, version_spec)
450+
{
451+
return Ok(Some(pkg));
452+
}
453+
454+
// Search for nested/aliased versions that match
455+
// Only search explicitly nested entries (with '/' in key), not bundled deps
456+
for (lockfile_key, entry) in &self.data.packages {
457+
// Only consider explicitly nested entries (not bundled)
458+
if !lockfile_key.contains('/') {
459+
continue;
460+
}
461+
462+
// Skip bundled dependencies
463+
if let Some(info) = &entry.info
464+
&& info
465+
.other
466+
.get("bundled")
467+
.and_then(|v| v.as_bool())
468+
.unwrap_or(false)
469+
{
470+
continue;
471+
}
472+
473+
let ident = PackageIdent::parse(&entry.ident);
474+
475+
// Skip if the name doesn't match
476+
if ident.name() != name {
477+
continue;
478+
}
479+
480+
// Skip workspace mappings
481+
if ident.is_workspace() {
482+
continue;
483+
}
484+
485+
// Check if this version satisfies the spec
486+
if let Some(pkg) =
487+
self.process_package_entry(entry, name, override_version, resolved_version)?
488+
&& self.version_satisfies_spec(&pkg.version, version_spec)
489+
{
490+
tracing::debug!(
491+
"Found matching version {} for {} (spec: {}) in nested entry {}",
492+
pkg.version,
493+
name,
494+
version_spec,
495+
lockfile_key
496+
);
497+
return Ok(Some(pkg));
498+
}
499+
}
500+
501+
Ok(None)
502+
}
392503
}
393504

394505
impl Lockfile for BunLockfile {
@@ -441,19 +552,15 @@ impl Lockfile for BunLockfile {
441552
}
442553
}
443554

444-
// Try workspace-scoped lookup first
445-
if let Some(entry) = self.index.get_workspace_scoped(workspace_name, name)
446-
&& let Some(pkg) =
447-
self.process_package_entry(entry, name, override_version, resolved_version)?
448-
{
449-
return Ok(Some(pkg));
450-
}
451-
452-
// Try finding via the general find_package method (includes bundled)
453-
if let Some((_key, entry)) = self.index.find_package(Some(workspace_name), name)
454-
&& let Some(pkg) =
455-
self.process_package_entry(entry, name, override_version, resolved_version)?
456-
{
555+
// Find a package version that satisfies the version spec
556+
// This searches workspace-scoped, hoisted, and nested entries
557+
if let Some(pkg) = self.find_matching_version(
558+
workspace_name,
559+
name,
560+
version,
561+
override_version,
562+
resolved_version,
563+
)? {
457564
return Ok(Some(pkg));
458565
}
459566

@@ -1904,41 +2011,6 @@ mod test {
19042011
assert!(lockfile.is_err(), "matching packages have differing shas");
19052012
}
19062013

1907-
#[test]
1908-
fn test_override_functionality() {
1909-
let contents = serde_json::to_string(&json!({
1910-
"lockfileVersion": 0,
1911-
"workspaces": {
1912-
"": {
1913-
"name": "test",
1914-
"dependencies": {
1915-
"foo": "^1.0.0"
1916-
}
1917-
}
1918-
},
1919-
"packages": {
1920-
"foo": ["[email protected]", {}, "sha512-original"],
1921-
"foo-override": ["[email protected]", {}, "sha512-override"]
1922-
},
1923-
"overrides": {
1924-
"foo": "2.0.0"
1925-
}
1926-
}))
1927-
.unwrap();
1928-
1929-
let lockfile = BunLockfile::from_str(&contents).unwrap();
1930-
1931-
// Resolve foo - should get override version instead of original
1932-
let result = lockfile
1933-
.resolve_package("", "foo", "^1.0.0")
1934-
.unwrap()
1935-
.unwrap();
1936-
1937-
// Should resolve to overridden version
1938-
assert_eq!(result.key, "[email protected]");
1939-
assert_eq!(result.version, "2.0.0");
1940-
}
1941-
19422014
#[test]
19432015
fn test_override_functionality_no_override() {
19442016
let contents = serde_json::to_string(&json!({
@@ -3169,13 +3241,21 @@ mod test {
31693241
}
31703242

31713243
#[test]
3172-
fn test_prune_issue_11007_2() {
3244+
fn test_prune_issue_11007_2_api() {
31733245
let lockfile = BunLockfile::from_str(PRUNE_ISSUE_11007_ORIGINAL_2).unwrap();
31743246
let pruned = prune_for_workspace(&lockfile, "apps/api");
31753247
let pruned_str = String::from_utf8(pruned.encode().unwrap()).unwrap();
31763248
insta::assert_snapshot!(pruned_str);
31773249
}
31783250

3251+
#[test]
3252+
fn test_prune_issue_11007_2_web() {
3253+
let lockfile = BunLockfile::from_str(PRUNE_ISSUE_11007_ORIGINAL_2).unwrap();
3254+
let pruned = prune_for_workspace(&lockfile, "apps/web");
3255+
let pruned_str = String::from_utf8(pruned.encode().unwrap()).unwrap();
3256+
insta::assert_snapshot!(pruned_str);
3257+
}
3258+
31793259
#[test]
31803260
fn test_prune_issue_11074() {
31813261
let lockfile = BunLockfile::from_str(PRUNE_ISSUE_11074_ORIGINAL).unwrap();

crates/turborepo-lockfiles/src/bun/snapshots/turborepo_lockfiles__bun__test__prune_basic_docs.snap

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,6 @@ expression: pruned_str
212212

213213
"glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
214214

215-
"globals": ["[email protected]", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
216-
217215
"has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
218216

219217
"ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -284,8 +282,6 @@ expression: pruned_str
284282

285283
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
286284

287-
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
288-
289285
"sharp": ["[email protected]", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
290286

291287
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -334,6 +330,8 @@ expression: pruned_str
334330

335331
"@eslint/eslintrc/globals": ["[email protected]", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
336332

333+
"@typescript-eslint/typescript-estree/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
334+
337335
"sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
338336
}
339337
}

crates/turborepo-lockfiles/src/bun/snapshots/turborepo_lockfiles__bun__test__prune_basic_web.snap

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,6 @@ expression: pruned_str
210210

211211
"glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
212212

213-
"globals": ["[email protected]", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
214-
215213
"has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
216214

217215
"ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -282,8 +280,6 @@ expression: pruned_str
282280

283281
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
284282

285-
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
286-
287283
"sharp": ["[email protected]", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
288284

289285
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -334,6 +330,8 @@ expression: pruned_str
334330

335331
"@eslint/eslintrc/globals": ["[email protected]", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
336332

333+
"@typescript-eslint/typescript-estree/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
334+
337335
"sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
338336
}
339337
}

0 commit comments

Comments
 (0)