From 5262937285e72419c6576ae1595d43f23e0c7b9d Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 2 Sep 2022 13:06:42 -0400 Subject: [PATCH] fix(npm): respect `latest` dist tag for getting current version (#15746) --- Cargo.lock | 1 + cli/npm/registry.rs | 21 ++++++++++-- cli/npm/resolution.rs | 75 ++++++++++++++++++++++++++++++++++++++++++- cli/npm/semver/mod.rs | 4 +++ test_util/Cargo.toml | 1 + test_util/src/npm.rs | 11 ++++++- 6 files changed, 108 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2ca9d58e8..9e85b8065e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4627,6 +4627,7 @@ dependencies = [ "reqwest", "ring", "rustls-pemfile 1.0.0", + "semver 1.0.13", "serde", "serde_json", "tar", diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index c70741b01d..1fb4b2e0a6 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -31,6 +31,8 @@ use super::semver::NpmVersionReq; pub struct NpmPackageInfo { pub name: String, pub versions: HashMap, + #[serde(rename = "dist-tags")] + pub dist_tags: HashMap, } pub struct NpmDependencyEntry { @@ -39,7 +41,7 @@ pub struct NpmDependencyEntry { pub version_req: NpmVersionReq, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone)] pub struct NpmPackageVersionInfo { pub version: String, pub dist: NpmPackageVersionDistInfo, @@ -89,7 +91,7 @@ impl NpmPackageVersionInfo { } } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone)] pub struct NpmPackageVersionDistInfo { /// URL to the tarball. pub tarball: String, @@ -231,7 +233,20 @@ impl NpmRegistryApi { Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; - Ok(serde_json::from_str(&file_text)?) + match serde_json::from_str(&file_text) { + Ok(package_info) => Ok(Some(package_info)), + Err(err) => { + // This scenario might mean we need to load more data from the + // npm registry than before. So, just debug log while in debug + // rather than panic. + log::debug!( + "error deserializing registry.json for '{}'. Reloading. {:?}", + name, + err + ); + Ok(None) + } + } } fn save_package_info_to_file_cache( diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs index 4f46bbed46..8fac92716e 100644 --- a/cli/npm/resolution.rs +++ b/cli/npm/resolution.rs @@ -19,9 +19,10 @@ use super::semver::NpmVersion; use super::semver::SpecifierVersionReq; /// The version matcher used for npm schemed urls is more strict than -/// the one used by npm packages. +/// the one used by npm packages and so we represent either via a trait. pub trait NpmVersionMatcher { fn matches(&self, version: &NpmVersion) -> bool; + fn is_latest(&self) -> bool; fn version_text(&self) -> String; } @@ -112,6 +113,10 @@ impl NpmVersionMatcher for NpmPackageReq { } } + fn is_latest(&self) -> bool { + self.version_req.is_none() + } + fn version_text(&self) -> String { self .version_req @@ -484,6 +489,25 @@ fn get_resolved_package_version_and_info( parent: Option<&NpmPackageId>, ) -> Result { let mut maybe_best_version: Option = None; + if version_matcher.is_latest() { + if let Some(version) = info.dist_tags.get("latest") { + match info.versions.get(version) { + Some(info) => { + return Ok(VersionAndInfo { + version: NpmVersion::parse(version)?, + info: info.clone(), + }); + } + None => { + bail!( + "Could not find version '{}' referenced in dist-tag 'latest'.", + version, + ) + } + } + } + } + for (_, version_info) in info.versions.into_iter() { let version = NpmVersion::parse(&version_info.version)?; if version_matcher.matches(&version) { @@ -651,4 +675,53 @@ mod tests { assert_eq!(name_without_path("@foo/bar/baz"), "@foo/bar"); assert_eq!(name_without_path("@hello"), "@hello"); } + + #[test] + fn test_get_resolved_package_version_and_info() { + // dist tag where version doesn't exist + let package_ref = NpmPackageReference::from_str("npm:test").unwrap(); + let result = get_resolved_package_version_and_info( + "test", + &package_ref.req, + NpmPackageInfo { + name: "test".to_string(), + versions: HashMap::new(), + dist_tags: HashMap::from([( + "latest".to_string(), + "1.0.0-alpha".to_string(), + )]), + }, + None, + ); + assert_eq!( + result.err().unwrap().to_string(), + "Could not find version '1.0.0-alpha' referenced in dist-tag 'latest'." + ); + + // dist tag where version is a pre-release + let package_ref = NpmPackageReference::from_str("npm:test").unwrap(); + let result = get_resolved_package_version_and_info( + "test", + &package_ref.req, + NpmPackageInfo { + name: "test".to_string(), + versions: HashMap::from([ + ("0.1.0".to_string(), NpmPackageVersionInfo::default()), + ( + "1.0.0-alpha".to_string(), + NpmPackageVersionInfo { + version: "0.1.0-alpha".to_string(), + ..Default::default() + }, + ), + ]), + dist_tags: HashMap::from([( + "latest".to_string(), + "1.0.0-alpha".to_string(), + )]), + }, + None, + ); + assert_eq!(result.unwrap().version.to_string(), "1.0.0-alpha"); + } } diff --git a/cli/npm/semver/mod.rs b/cli/npm/semver/mod.rs index 10a87a13ff..53f0f199fe 100644 --- a/cli/npm/semver/mod.rs +++ b/cli/npm/semver/mod.rs @@ -174,6 +174,10 @@ impl NpmVersionMatcher for NpmVersionReq { self.satisfies(version) } + fn is_latest(&self) -> bool { + false + } + fn version_text(&self) -> String { self.raw_text.clone() } diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml index 8d9da66e20..7fa309dac4 100644 --- a/test_util/Cargo.toml +++ b/test_util/Cargo.toml @@ -28,6 +28,7 @@ regex = "1.6.0" reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks"] } ring = "0.16.20" rustls-pemfile = "1.0.0" +semver = "1.0.13" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" tar = "0.4.38" diff --git a/test_util/src/npm.rs b/test_util/src/npm.rs index fad59e3b8a..6e393d8353 100644 --- a/test_util/src/npm.rs +++ b/test_util/src/npm.rs @@ -71,6 +71,7 @@ fn get_npm_package(package_name: &str) -> Result> { // read all the package's versions let mut tarballs = HashMap::new(); let mut versions = serde_json::Map::new(); + let mut latest_version = semver::Version::parse("0.0.0").unwrap(); for entry in fs::read_dir(&package_folder)? { let entry = entry?; let file_type = entry.file_type()?; @@ -134,13 +135,21 @@ fn get_npm_package(package_name: &str) -> Result> { let mut version_info: serde_json::Map = serde_json::from_str(&package_json_text)?; version_info.insert("dist".to_string(), dist.into()); - versions.insert(version, version_info.into()); + versions.insert(version.clone(), version_info.into()); + let version = semver::Version::parse(&version)?; + if version.cmp(&latest_version).is_gt() { + latest_version = version; + } } + let mut dist_tags = serde_json::Map::new(); + dist_tags.insert("latest".to_string(), latest_version.to_string().into()); + // create the registry file for this package let mut registry_file = serde_json::Map::new(); registry_file.insert("name".to_string(), package_name.to_string().into()); registry_file.insert("versions".to_string(), versions.into()); + registry_file.insert("dist-tags".to_string(), dist_tags.into()); Ok(Some(CustomNpmPackage { registry_file: serde_json::to_string(®istry_file).unwrap(), tarballs,