@@ -5,7 +5,7 @@ use chrono::{DateTime, Utc};
55use futures:: { AsyncReadExt , FutureExt as _, channel:: oneshot, future:: Shared } ;
66use http_client:: { Host , HttpClient , Url } ;
77use log:: Level ;
8- use semver:: Version ;
8+ use semver:: { Version , VersionReq } ;
99use serde:: Deserialize ;
1010use smol:: io:: BufReader ;
1111use smol:: { fs, lock:: Mutex } ;
@@ -253,6 +253,15 @@ impl NodeRuntime {
253253 }
254254
255255 pub async fn npm_package_latest_version ( & self , name : & str ) -> Result < Version > {
256+ self . npm_package_latest_version_with_requirement ( name, None )
257+ . await
258+ }
259+
260+ pub async fn npm_package_latest_version_with_requirement (
261+ & self ,
262+ name : & str ,
263+ version_requirement : Option < & VersionReq > ,
264+ ) -> Result < Version > {
256265 let http = self . 0 . lock ( ) . await . http . clone ( ) ;
257266 let instance = self . instance ( ) . await ;
258267 let output = instance
@@ -285,9 +294,10 @@ impl NodeRuntime {
285294 . log_err ( )
286295 . flatten ( ) ;
287296 let latest_dist_tag = info. dist_tags . latest . clone ( ) ;
288- let selected_version = select_npm_package_version ( name, info, before. as_deref ( ) ) ?;
297+ let selected_version =
298+ select_npm_package_version ( name, info, before. as_deref ( ) , version_requirement) ?;
289299 log:: debug!(
290- "selected latest npm package version package={name:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}"
300+ "selected latest npm package version package={name:?} version_requirement={version_requirement:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}"
291301 ) ;
292302 Ok ( selected_version)
293303 }
@@ -481,7 +491,19 @@ fn select_npm_package_version(
481491 package_name : & str ,
482492 mut info : NpmInfo ,
483493 before : Option < & str > ,
494+ version_requirement : Option < & VersionReq > ,
484495) -> Result < Version > {
496+ if let Some ( version_requirement) = version_requirement {
497+ info. versions
498+ . retain ( |version| version_requirement. matches ( version) ) ;
499+ info. versions . sort ( ) ;
500+ info. dist_tags . latest = info
501+ . dist_tags
502+ . latest
503+ . take ( )
504+ . filter ( |version| version_requirement. matches ( version) ) ;
505+ }
506+
485507 if let Some ( before) = before
486508 && !info. time . is_empty ( )
487509 {
@@ -502,6 +524,7 @@ fn select_npm_package_version(
502524 latest_version,
503525 & info. time ,
504526 & before_timestamp,
527+ version_requirement. is_some ( ) ,
505528 ) ? {
506529 return Ok ( version. clone ( ) ) ;
507530 }
@@ -521,8 +544,9 @@ fn is_allowed_npm_version_before(
521544 latest_version : Option < & Version > ,
522545 published_at_by_version : & HashMap < String , String > ,
523546 before : & DateTime < Utc > ,
547+ allow_prereleases : bool ,
524548) -> Result < bool > {
525- if ! version. pre . is_empty ( )
549+ if ( !allow_prereleases && ! version. pre . is_empty ( ) )
526550 || latest_version. is_some_and ( |latest_version| version > latest_version)
527551 {
528552 return Ok ( false ) ;
@@ -1121,7 +1145,7 @@ mod tests {
11211145
11221146 use anyhow:: { Result , bail} ;
11231147 use http_client:: Url ;
1124- use semver:: Version ;
1148+ use semver:: { Version , VersionReq } ;
11251149
11261150 use super :: {
11271151 NpmInfo , VersionStrategy , build_npm_command_args, deserialize_npm_info_from_response,
@@ -1245,7 +1269,7 @@ mod tests {
12451269 ) ?;
12461270
12471271 assert_eq ! (
1248- select_npm_package_version( "test-package" , info, None ) ?,
1272+ select_npm_package_version( "test-package" , info, None , None ) ?,
12491273 Version :: parse( "3.0.0" ) ?
12501274 ) ;
12511275 Ok ( ( ) )
@@ -1270,7 +1294,12 @@ mod tests {
12701294 ) ?;
12711295
12721296 assert_eq ! (
1273- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1297+ select_npm_package_version(
1298+ "test-package" ,
1299+ info,
1300+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1301+ None
1302+ ) ?,
12741303 Version :: parse( "2.0.0" ) ?
12751304 ) ;
12761305 Ok ( ( ) )
@@ -1291,7 +1320,12 @@ mod tests {
12911320 ) ?;
12921321
12931322 assert_eq ! (
1294- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1323+ select_npm_package_version(
1324+ "test-package" ,
1325+ info,
1326+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1327+ None
1328+ ) ?,
12951329 Version :: parse( "2.0.0" ) ?
12961330 ) ;
12971331 Ok ( ( ) )
@@ -1312,7 +1346,12 @@ mod tests {
13121346 ) ?;
13131347
13141348 assert_eq ! (
1315- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1349+ select_npm_package_version(
1350+ "test-package" ,
1351+ info,
1352+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1353+ None
1354+ ) ?,
13161355 Version :: parse( "2.0.0" ) ?
13171356 ) ;
13181357 Ok ( ( ) )
@@ -1332,7 +1371,12 @@ mod tests {
13321371 ) ?;
13331372
13341373 assert_eq ! (
1335- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1374+ select_npm_package_version(
1375+ "test-package" ,
1376+ info,
1377+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1378+ None
1379+ ) ?,
13361380 Version :: parse( "2.0.0-beta.1" ) ?
13371381 ) ;
13381382 Ok ( ( ) )
@@ -1353,7 +1397,12 @@ mod tests {
13531397 ) ?;
13541398
13551399 assert_eq ! (
1356- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1400+ select_npm_package_version(
1401+ "test-package" ,
1402+ info,
1403+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1404+ None
1405+ ) ?,
13571406 Version :: parse( "1.0.0" ) ?
13581407 ) ;
13591408 Ok ( ( ) )
@@ -1374,7 +1423,12 @@ mod tests {
13741423 ) ?;
13751424
13761425 assert_eq ! (
1377- select_npm_package_version( "test-package" , info, Some ( "2024-02-15T00:00:00.000Z" ) ) ?,
1426+ select_npm_package_version(
1427+ "test-package" ,
1428+ info,
1429+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1430+ None
1431+ ) ?,
13781432 Version :: parse( "1.0.0" ) ?
13791433 ) ;
13801434 Ok ( ( ) )
@@ -1393,9 +1447,12 @@ mod tests {
13931447 }"# ,
13941448 ) ?;
13951449
1396- let Err ( error) =
1397- select_npm_package_version ( "test-package" , info, Some ( "2023-12-01T00:00:00.000Z" ) )
1398- else {
1450+ let Err ( error) = select_npm_package_version (
1451+ "test-package" ,
1452+ info,
1453+ Some ( "2023-12-01T00:00:00.000Z" ) ,
1454+ None ,
1455+ ) else {
13991456 bail ! ( "expected cutoff to reject all package versions" ) ;
14001457 } ;
14011458 assert_eq ! (
@@ -1405,6 +1462,112 @@ mod tests {
14051462 Ok ( ( ) )
14061463 }
14071464
1465+ #[ test]
1466+ fn test_select_npm_package_version_selects_latest_matching_requirement ( ) -> Result < ( ) > {
1467+ let info: NpmInfo = serde_json:: from_str (
1468+ r#"{
1469+ "dist-tags": { "latest": "7.0.0" },
1470+ "versions": ["6.0.3", "7.0.0", "5.9.3", "6.0.2"]
1471+ }"# ,
1472+ ) ?;
1473+ let version_requirement = VersionReq :: parse ( "^6" ) ?;
1474+
1475+ assert_eq ! (
1476+ select_npm_package_version( "test-package" , info, None , Some ( & version_requirement) ) ?,
1477+ Version :: parse( "6.0.3" ) ?
1478+ ) ;
1479+ Ok ( ( ) )
1480+ }
1481+
1482+ #[ test]
1483+ fn test_select_npm_package_version_applies_before_to_matching_versions ( ) -> Result < ( ) > {
1484+ let info: NpmInfo = serde_json:: from_str (
1485+ r#"{
1486+ "dist-tags": { "latest": "7.0.0" },
1487+ "versions": ["6.0.3", "7.0.0", "6.0.2"],
1488+ "time": {
1489+ "6.0.2": "2024-02-01T00:00:00.000Z",
1490+ "6.0.3": "2024-03-01T00:00:00.000Z",
1491+ "7.0.0": "2024-04-01T00:00:00.000Z"
1492+ }
1493+ }"# ,
1494+ ) ?;
1495+ let version_requirement = VersionReq :: parse ( "^6" ) ?;
1496+
1497+ assert_eq ! (
1498+ select_npm_package_version(
1499+ "test-package" ,
1500+ info,
1501+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1502+ Some ( & version_requirement) ,
1503+ ) ?,
1504+ Version :: parse( "6.0.2" ) ?
1505+ ) ;
1506+ Ok ( ( ) )
1507+ }
1508+
1509+ #[ test]
1510+ fn test_select_npm_package_version_allows_requested_prerelease_before_cutoff ( ) -> Result < ( ) > {
1511+ let info: NpmInfo = serde_json:: from_str (
1512+ r#"{
1513+ "dist-tags": { "latest": "7.0.0" },
1514+ "versions": ["7.1.0-beta.1", "7.1.0-beta.2", "7.0.0"],
1515+ "time": {
1516+ "7.0.0": "2024-01-01T00:00:00.000Z",
1517+ "7.1.0-beta.1": "2024-02-01T00:00:00.000Z",
1518+ "7.1.0-beta.2": "2024-03-01T00:00:00.000Z"
1519+ }
1520+ }"# ,
1521+ ) ?;
1522+ let version_requirement = VersionReq :: parse ( ">=7.1.0-beta.1, <7.1.0" ) ?;
1523+
1524+ assert_eq ! (
1525+ select_npm_package_version(
1526+ "test-package" ,
1527+ info,
1528+ Some ( "2024-02-15T00:00:00.000Z" ) ,
1529+ Some ( & version_requirement) ,
1530+ ) ?,
1531+ Version :: parse( "7.1.0-beta.1" ) ?
1532+ ) ;
1533+ Ok ( ( ) )
1534+ }
1535+
1536+ #[ test]
1537+ fn test_select_npm_package_version_errors_without_matching_version ( ) -> Result < ( ) > {
1538+ let info: NpmInfo = serde_json:: from_str (
1539+ r#"{
1540+ "dist-tags": { "latest": "7.0.0" },
1541+ "versions": ["5.9.3", "7.0.0"]
1542+ }"# ,
1543+ ) ?;
1544+ let version_requirement = VersionReq :: parse ( "^6" ) ?;
1545+
1546+ let error =
1547+ select_npm_package_version ( "test-package" , info, None , Some ( & version_requirement) )
1548+ . expect_err ( "expected version requirement to reject all package versions" ) ;
1549+ assert_eq ! (
1550+ error. to_string( ) ,
1551+ "no version found for npm package test-package"
1552+ ) ;
1553+ Ok ( ( ) )
1554+ }
1555+
1556+ #[ test]
1557+ fn test_pinned_version_strategy_replaces_different_installed_version ( ) -> Result < ( ) > {
1558+ let pinned_version = Version :: parse ( "6.0.3" ) ?;
1559+
1560+ assert ! ( !should_install_npm_package_version(
1561+ & pinned_version,
1562+ VersionStrategy :: Pin ( & pinned_version)
1563+ ) ) ;
1564+ assert ! ( should_install_npm_package_version(
1565+ & Version :: parse( "7.0.0" ) ?,
1566+ VersionStrategy :: Pin ( & pinned_version)
1567+ ) ) ;
1568+ Ok ( ( ) )
1569+ }
1570+
14081571 #[ test]
14091572 fn test_deserialize_npm_info_npm11_format ( ) -> Result < ( ) > {
14101573 let json = r#"{
0 commit comments