@@ -94,6 +94,7 @@ use biome_json_formatter::context::JsonFormatOptions;
9494use biome_json_parser:: JsonParserOptions ;
9595use id:: PossibleKeyIter ;
9696use itertools:: Itertools as _;
97+ use semver:: { Version , VersionReq } ;
9798use serde:: { Deserialize , Deserializer , Serialize , Serializer } ;
9899use serde_json:: Value ;
99100use 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
394505impl 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 ( ) ;
0 commit comments