diff options
Diffstat (limited to 'cli/npm/resolution.rs')
-rw-r--r-- | cli/npm/resolution.rs | 1051 |
1 files changed, 0 insertions, 1051 deletions
diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs deleted file mode 100644 index 3df2e4ce5..000000000 --- a/cli/npm/resolution.rs +++ /dev/null @@ -1,1051 +0,0 @@ -// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -use std::cmp::Ordering; -use std::collections::HashMap; -use std::collections::HashSet; -use std::collections::VecDeque; - -use deno_ast::ModuleSpecifier; -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::parking_lot::Mutex; -use deno_core::parking_lot::RwLock; -use serde::Deserialize; -use serde::Serialize; -use std::sync::Arc; - -use crate::lockfile::Lockfile; - -use super::cache::should_sync_download; -use super::registry::NpmPackageInfo; -use super::registry::NpmPackageVersionDistInfo; -use super::registry::NpmPackageVersionInfo; -use super::registry::NpmRegistryApi; -use super::semver::NpmVersion; -use super::semver::NpmVersionReq; -use super::semver::SpecifierVersionReq; - -/// The version matcher used for npm schemed urls is more strict than -/// the one used by npm packages and so we represent either via a trait. -pub trait NpmVersionMatcher { - fn tag(&self) -> Option<&str>; - fn matches(&self, version: &NpmVersion) -> bool; - fn version_text(&self) -> String; -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct NpmPackageReference { - pub req: NpmPackageReq, - pub sub_path: Option<String>, -} - -impl NpmPackageReference { - pub fn from_specifier( - specifier: &ModuleSpecifier, - ) -> Result<NpmPackageReference, AnyError> { - Self::from_str(specifier.as_str()) - } - - pub fn from_str(specifier: &str) -> Result<NpmPackageReference, AnyError> { - let specifier = match specifier.strip_prefix("npm:") { - Some(s) => s, - None => { - bail!("Not an npm specifier: {}", specifier); - } - }; - let parts = specifier.split('/').collect::<Vec<_>>(); - let name_part_len = if specifier.starts_with('@') { 2 } else { 1 }; - if parts.len() < name_part_len { - return Err(generic_error(format!("Not a valid package: {}", specifier))); - } - let name_parts = &parts[0..name_part_len]; - let last_name_part = &name_parts[name_part_len - 1]; - let (name, version_req) = if let Some(at_index) = last_name_part.rfind('@') - { - let version = &last_name_part[at_index + 1..]; - let last_name_part = &last_name_part[..at_index]; - let version_req = SpecifierVersionReq::parse(version) - .with_context(|| "Invalid version requirement.")?; - let name = if name_part_len == 1 { - last_name_part.to_string() - } else { - format!("{}/{}", name_parts[0], last_name_part) - }; - (name, Some(version_req)) - } else { - (name_parts.join("/"), None) - }; - let sub_path = if parts.len() == name_parts.len() { - None - } else { - Some(parts[name_part_len..].join("/")) - }; - - if let Some(sub_path) = &sub_path { - if let Some(at_index) = sub_path.rfind('@') { - let (new_sub_path, version) = sub_path.split_at(at_index); - let msg = format!( - "Invalid package specifier 'npm:{}/{}'. Did you mean to write 'npm:{}{}/{}'?", - name, sub_path, name, version, new_sub_path - ); - return Err(generic_error(msg)); - } - } - - Ok(NpmPackageReference { - req: NpmPackageReq { name, version_req }, - sub_path, - }) - } -} - -impl std::fmt::Display for NpmPackageReference { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(sub_path) = &self.sub_path { - write!(f, "{}/{}", self.req, sub_path) - } else { - write!(f, "{}", self.req) - } - } -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, -)] -pub struct NpmPackageReq { - pub name: String, - pub version_req: Option<SpecifierVersionReq>, -} - -impl std::fmt::Display for NpmPackageReq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.version_req { - Some(req) => write!(f, "{}@{}", self.name, req), - None => write!(f, "{}", self.name), - } - } -} - -impl NpmVersionMatcher for NpmPackageReq { - fn tag(&self) -> Option<&str> { - match &self.version_req { - Some(version_req) => version_req.tag(), - None => Some("latest"), - } - } - - fn matches(&self, version: &NpmVersion) -> bool { - match self.version_req.as_ref() { - Some(req) => { - assert_eq!(self.tag(), None); - match req.range() { - Some(range) => range.satisfies(version), - None => false, - } - } - None => version.pre.is_empty(), - } - } - - fn version_text(&self) -> String { - self - .version_req - .as_ref() - .map(|v| format!("{}", v)) - .unwrap_or_else(|| "non-prerelease".to_string()) - } -} - -#[derive( - Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, -)] -pub struct NpmPackageId { - pub name: String, - pub version: NpmVersion, -} - -impl NpmPackageId { - #[allow(unused)] - pub fn scope(&self) -> Option<&str> { - if self.name.starts_with('@') && self.name.contains('/') { - self.name.split('/').next() - } else { - None - } - } - - pub fn serialize_for_lock_file(&self) -> String { - format!("{}@{}", self.name, self.version) - } - - pub fn deserialize_from_lock_file(id: &str) -> Result<Self, AnyError> { - let reference = NpmPackageReference::from_str(&format!("npm:{}", id)) - .with_context(|| { - format!("Unable to deserialize npm package reference: {}", id) - })?; - let version = - NpmVersion::parse(&reference.req.version_req.unwrap().to_string()) - .unwrap(); - Ok(Self { - name: reference.req.name, - version, - }) - } -} - -impl std::fmt::Display for NpmPackageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}@{}", self.name, self.version) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NpmResolutionPackage { - pub id: NpmPackageId, - pub dist: NpmPackageVersionDistInfo, - /// Key is what the package refers to the other package as, - /// which could be different from the package name. - pub dependencies: HashMap<String, NpmPackageId>, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct NpmResolutionSnapshot { - #[serde(with = "map_to_vec")] - package_reqs: HashMap<NpmPackageReq, NpmVersion>, - packages_by_name: HashMap<String, Vec<NpmVersion>>, - #[serde(with = "map_to_vec")] - packages: HashMap<NpmPackageId, NpmResolutionPackage>, -} - -// This is done so the maps with non-string keys get serialized and deserialized as vectors. -// Adapted from: https://github.com/serde-rs/serde/issues/936#issuecomment-302281792 -mod map_to_vec { - use std::collections::HashMap; - - use serde::de::Deserialize; - use serde::de::Deserializer; - use serde::ser::Serializer; - use serde::Serialize; - - pub fn serialize<S, K: Serialize, V: Serialize>( - map: &HashMap<K, V>, - serializer: S, - ) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.collect_seq(map.iter()) - } - - pub fn deserialize< - 'de, - D, - K: Deserialize<'de> + Eq + std::hash::Hash, - V: Deserialize<'de>, - >( - deserializer: D, - ) -> Result<HashMap<K, V>, D::Error> - where - D: Deserializer<'de>, - { - let mut map = HashMap::new(); - for (key, value) in Vec::<(K, V)>::deserialize(deserializer)? { - map.insert(key, value); - } - Ok(map) - } -} - -impl NpmResolutionSnapshot { - /// Resolve a node package from a deno module. - pub fn resolve_package_from_deno_module( - &self, - req: &NpmPackageReq, - ) -> Result<&NpmResolutionPackage, AnyError> { - match self.package_reqs.get(req) { - Some(version) => Ok( - self - .packages - .get(&NpmPackageId { - name: req.name.clone(), - version: version.clone(), - }) - .unwrap(), - ), - None => bail!("could not find npm package directory for '{}'", req), - } - } - - pub fn top_level_packages(&self) -> Vec<NpmPackageId> { - self - .package_reqs - .iter() - .map(|(req, version)| NpmPackageId { - name: req.name.clone(), - version: version.clone(), - }) - .collect::<HashSet<_>>() - .into_iter() - .collect::<Vec<_>>() - } - - pub fn package_from_id( - &self, - id: &NpmPackageId, - ) -> Option<&NpmResolutionPackage> { - self.packages.get(id) - } - - pub fn resolve_package_from_package( - &self, - name: &str, - referrer: &NpmPackageId, - ) -> Result<&NpmResolutionPackage, AnyError> { - match self.packages.get(referrer) { - Some(referrer_package) => { - let name_ = name_without_path(name); - if let Some(id) = referrer_package.dependencies.get(name_) { - return Ok(self.packages.get(id).unwrap()); - } - - if referrer_package.id.name == name_ { - return Ok(referrer_package); - } - - // TODO(bartlomieju): this should use a reverse lookup table in the - // snapshot instead of resolving best version again. - let req = NpmPackageReq { - name: name_.to_string(), - version_req: None, - }; - - if let Some(version) = self.resolve_best_package_version(name_, &req) { - let id = NpmPackageId { - name: name_.to_string(), - version, - }; - if let Some(pkg) = self.packages.get(&id) { - return Ok(pkg); - } - } - - bail!( - "could not find npm package '{}' referenced by '{}'", - name, - referrer - ) - } - None => bail!("could not find referrer npm package '{}'", referrer), - } - } - - pub fn all_packages(&self) -> Vec<NpmResolutionPackage> { - self.packages.values().cloned().collect() - } - - pub fn resolve_best_package_version( - &self, - name: &str, - version_matcher: &impl NpmVersionMatcher, - ) -> Option<NpmVersion> { - let mut maybe_best_version: Option<&NpmVersion> = None; - if let Some(versions) = self.packages_by_name.get(name) { - for version in versions { - if version_matcher.matches(version) { - let is_best_version = maybe_best_version - .as_ref() - .map(|best_version| (*best_version).cmp(version).is_lt()) - .unwrap_or(true); - if is_best_version { - maybe_best_version = Some(version); - } - } - } - } - maybe_best_version.cloned() - } - - pub async fn from_lockfile( - lockfile: Arc<Mutex<Lockfile>>, - api: &NpmRegistryApi, - ) -> Result<Self, AnyError> { - let mut package_reqs: HashMap<NpmPackageReq, NpmVersion>; - let mut packages_by_name: HashMap<String, Vec<NpmVersion>>; - let mut packages: HashMap<NpmPackageId, NpmResolutionPackage>; - - { - let lockfile = lockfile.lock(); - - // pre-allocate collections - package_reqs = - HashMap::with_capacity(lockfile.content.npm.specifiers.len()); - packages = HashMap::with_capacity(lockfile.content.npm.packages.len()); - packages_by_name = - HashMap::with_capacity(lockfile.content.npm.packages.len()); // close enough - let mut verify_ids = - HashSet::with_capacity(lockfile.content.npm.packages.len()); - - // collect the specifiers to version mappings - for (key, value) in &lockfile.content.npm.specifiers { - let reference = NpmPackageReference::from_str(&format!("npm:{}", key)) - .with_context(|| format!("Unable to parse npm specifier: {}", key))?; - let package_id = NpmPackageId::deserialize_from_lock_file(value)?; - package_reqs.insert(reference.req, package_id.version.clone()); - verify_ids.insert(package_id.clone()); - } - - // then the packages - for (key, value) in &lockfile.content.npm.packages { - let package_id = NpmPackageId::deserialize_from_lock_file(key)?; - let mut dependencies = HashMap::default(); - - packages_by_name - .entry(package_id.name.to_string()) - .or_default() - .push(package_id.version.clone()); - - for (name, specifier) in &value.dependencies { - let dep_id = NpmPackageId::deserialize_from_lock_file(specifier)?; - dependencies.insert(name.to_string(), dep_id.clone()); - verify_ids.insert(dep_id); - } - - let package = NpmResolutionPackage { - id: package_id.clone(), - // temporary dummy value - dist: NpmPackageVersionDistInfo { - tarball: "foobar".to_string(), - shasum: "foobar".to_string(), - integrity: Some("foobar".to_string()), - }, - dependencies, - }; - - packages.insert(package_id, package); - } - - // verify that all these ids exist in packages - for id in &verify_ids { - if !packages.contains_key(id) { - bail!( - "the lockfile ({}) is corrupt. You can recreate it with --lock-write", - lockfile.filename.display(), - ); - } - } - } - - let mut unresolved_tasks = Vec::with_capacity(packages_by_name.len()); - - // cache the package names in parallel in the registry api - for package_name in packages_by_name.keys() { - let package_name = package_name.clone(); - let api = api.clone(); - unresolved_tasks.push(tokio::task::spawn(async move { - api.package_info(&package_name).await?; - Result::<_, AnyError>::Ok(()) - })); - } - for result in futures::future::join_all(unresolved_tasks).await { - result??; - } - - // ensure the dist is set for each package - for package in packages.values_mut() { - // this will read from the memory cache now - let package_info = api.package_info(&package.id.name).await?; - let version_info = match package_info - .versions - .get(&package.id.version.to_string()) - { - Some(version_info) => version_info, - None => { - bail!("could not find '{}' specified in the lockfile. Maybe try again with --reload", package.id); - } - }; - package.dist = version_info.dist.clone(); - } - - Ok(Self { - package_reqs, - packages_by_name, - packages, - }) - } -} - -pub struct NpmResolution { - api: NpmRegistryApi, - snapshot: RwLock<NpmResolutionSnapshot>, - update_sempahore: tokio::sync::Semaphore, -} - -impl std::fmt::Debug for NpmResolution { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let snapshot = self.snapshot.read(); - f.debug_struct("NpmResolution") - .field("snapshot", &snapshot) - .finish() - } -} - -impl NpmResolution { - pub fn new( - api: NpmRegistryApi, - initial_snapshot: Option<NpmResolutionSnapshot>, - ) -> Self { - Self { - api, - snapshot: RwLock::new(initial_snapshot.unwrap_or_default()), - update_sempahore: tokio::sync::Semaphore::new(1), - } - } - - pub async fn add_package_reqs( - &self, - package_reqs: Vec<NpmPackageReq>, - ) -> Result<(), AnyError> { - // only allow one thread in here at a time - let _permit = self.update_sempahore.acquire().await.unwrap(); - let snapshot = self.snapshot.read().clone(); - - let snapshot = self - .add_package_reqs_to_snapshot(package_reqs, snapshot) - .await?; - - *self.snapshot.write() = snapshot; - Ok(()) - } - - pub async fn set_package_reqs( - &self, - package_reqs: HashSet<NpmPackageReq>, - ) -> Result<(), AnyError> { - // only allow one thread in here at a time - let _permit = self.update_sempahore.acquire().await.unwrap(); - let snapshot = self.snapshot.read().clone(); - - let has_removed_package = !snapshot - .package_reqs - .keys() - .all(|req| package_reqs.contains(req)); - // if any packages were removed, we need to completely recreate the npm resolution snapshot - let snapshot = if has_removed_package { - NpmResolutionSnapshot::default() - } else { - snapshot - }; - let snapshot = self - .add_package_reqs_to_snapshot( - package_reqs.into_iter().collect(), - snapshot, - ) - .await?; - - *self.snapshot.write() = snapshot; - - Ok(()) - } - - async fn add_package_reqs_to_snapshot( - &self, - mut package_reqs: Vec<NpmPackageReq>, - mut snapshot: NpmResolutionSnapshot, - ) -> Result<NpmResolutionSnapshot, AnyError> { - // multiple packages are resolved in alphabetical order - package_reqs.sort_by(|a, b| a.name.cmp(&b.name)); - - // go over the top level packages first, then down the - // tree one level at a time through all the branches - let mut unresolved_tasks = Vec::with_capacity(package_reqs.len()); - for package_req in package_reqs { - if snapshot.package_reqs.contains_key(&package_req) { - // skip analyzing this package, as there's already a matching top level package - continue; - } - // inspect the list of current packages - if let Some(version) = - snapshot.resolve_best_package_version(&package_req.name, &package_req) - { - snapshot.package_reqs.insert(package_req, version); - continue; // done, no need to continue - } - - // no existing best version, so resolve the current packages - let api = self.api.clone(); - let maybe_info = if should_sync_download() { - // for deterministic test output - Some(api.package_info(&package_req.name).await) - } else { - None - }; - unresolved_tasks.push(tokio::task::spawn(async move { - let info = match maybe_info { - Some(info) => info?, - None => api.package_info(&package_req.name).await?, - }; - Result::<_, AnyError>::Ok((package_req, info)) - })); - } - - let mut pending_dependencies = VecDeque::new(); - for result in futures::future::join_all(unresolved_tasks).await { - let (package_req, info) = result??; - let version_and_info = get_resolved_package_version_and_info( - &package_req.name, - &package_req, - info, - None, - )?; - let id = NpmPackageId { - name: package_req.name.clone(), - version: version_and_info.version.clone(), - }; - let dependencies = version_and_info - .info - .dependencies_as_entries() - .with_context(|| format!("npm package: {}", id))?; - - pending_dependencies.push_back((id.clone(), dependencies)); - snapshot.packages.insert( - id.clone(), - NpmResolutionPackage { - id, - dist: version_and_info.info.dist, - dependencies: Default::default(), - }, - ); - snapshot - .packages_by_name - .entry(package_req.name.clone()) - .or_default() - .push(version_and_info.version.clone()); - snapshot - .package_reqs - .insert(package_req, version_and_info.version); - } - - // now go down through the dependencies by tree depth - while let Some((parent_package_id, mut deps)) = - pending_dependencies.pop_front() - { - // sort the dependencies alphabetically by name then by version descending - deps.sort_by(|a, b| match a.name.cmp(&b.name) { - // sort by newest to oldest - Ordering::Equal => b - .version_req - .version_text() - .cmp(&a.version_req.version_text()), - ordering => ordering, - }); - - // cache all the dependencies' registry infos in parallel if should - if !should_sync_download() { - let handles = deps - .iter() - .map(|dep| { - let name = dep.name.clone(); - let api = self.api.clone(); - tokio::task::spawn(async move { - // it's ok to call this without storing the result, because - // NpmRegistryApi will cache the package info in memory - api.package_info(&name).await - }) - }) - .collect::<Vec<_>>(); - let results = futures::future::join_all(handles).await; - for result in results { - result??; // surface the first error - } - } - - // now resolve them - for dep in deps { - // check if an existing dependency matches this - let id = if let Some(version) = - snapshot.resolve_best_package_version(&dep.name, &dep.version_req) - { - NpmPackageId { - name: dep.name.clone(), - version, - } - } else { - // get the information - let info = self.api.package_info(&dep.name).await?; - let version_and_info = get_resolved_package_version_and_info( - &dep.name, - &dep.version_req, - info, - None, - )?; - let dependencies = version_and_info - .info - .dependencies_as_entries() - .with_context(|| { - format!("npm package: {}@{}", dep.name, version_and_info.version) - })?; - - let id = NpmPackageId { - name: dep.name.clone(), - version: version_and_info.version.clone(), - }; - pending_dependencies.push_back((id.clone(), dependencies)); - snapshot.packages.insert( - id.clone(), - NpmResolutionPackage { - id: id.clone(), - dist: version_and_info.info.dist, - dependencies: Default::default(), - }, - ); - snapshot - .packages_by_name - .entry(dep.name.clone()) - .or_default() - .push(id.version.clone()); - - id - }; - - // add this version as a dependency of the package - snapshot - .packages - .get_mut(&parent_package_id) - .unwrap() - .dependencies - .insert(dep.bare_specifier.clone(), id); - } - } - - Ok(snapshot) - } - - pub fn resolve_package_from_id( - &self, - id: &NpmPackageId, - ) -> Option<NpmResolutionPackage> { - self.snapshot.read().package_from_id(id).cloned() - } - - pub fn resolve_package_from_package( - &self, - name: &str, - referrer: &NpmPackageId, - ) -> Result<NpmResolutionPackage, AnyError> { - self - .snapshot - .read() - .resolve_package_from_package(name, referrer) - .cloned() - } - - /// Resolve a node package from a deno module. - pub fn resolve_package_from_deno_module( - &self, - package: &NpmPackageReq, - ) -> Result<NpmResolutionPackage, AnyError> { - self - .snapshot - .read() - .resolve_package_from_deno_module(package) - .cloned() - } - - pub fn all_packages(&self) -> Vec<NpmResolutionPackage> { - self.snapshot.read().all_packages() - } - - pub fn has_packages(&self) -> bool { - !self.snapshot.read().packages.is_empty() - } - - pub fn snapshot(&self) -> NpmResolutionSnapshot { - self.snapshot.read().clone() - } - - pub fn lock( - &self, - lockfile: &mut Lockfile, - snapshot: &NpmResolutionSnapshot, - ) -> Result<(), AnyError> { - for (package_req, version) in snapshot.package_reqs.iter() { - lockfile.insert_npm_specifier(package_req, version.to_string()); - } - for package in self.all_packages() { - lockfile.check_or_insert_npm_package(&package)?; - } - Ok(()) - } -} - -#[derive(Clone)] -struct VersionAndInfo { - version: NpmVersion, - info: NpmPackageVersionInfo, -} - -fn get_resolved_package_version_and_info( - pkg_name: &str, - version_matcher: &impl NpmVersionMatcher, - info: NpmPackageInfo, - parent: Option<&NpmPackageId>, -) -> Result<VersionAndInfo, AnyError> { - let mut maybe_best_version: Option<VersionAndInfo> = None; - if let Some(tag) = version_matcher.tag() { - // For when someone just specifies @types/node, we want to pull in a - // "known good" version of @types/node that works well with Deno and - // not necessarily the latest version. For example, we might only be - // compatible with Node vX, but then Node vY is published so we wouldn't - // want to pull that in. - // Note: If the user doesn't want this behavior, then they can specify an - // explicit version. - if tag == "latest" && pkg_name == "@types/node" { - return get_resolved_package_version_and_info( - pkg_name, - &NpmVersionReq::parse("18.0.0 - 18.8.2").unwrap(), - info, - parent, - ); - } - - if let Some(version) = info.dist_tags.get(tag) { - 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 '{}'.", - version, - tag, - ) - } - } - } else { - bail!("Could not find dist-tag '{}'.", tag,) - } - } else { - for (_, version_info) in info.versions.into_iter() { - let version = NpmVersion::parse(&version_info.version)?; - if version_matcher.matches(&version) { - let is_best_version = maybe_best_version - .as_ref() - .map(|best_version| best_version.version.cmp(&version).is_lt()) - .unwrap_or(true); - if is_best_version { - maybe_best_version = Some(VersionAndInfo { - version, - info: version_info, - }); - } - } - } - } - - match maybe_best_version { - Some(v) => Ok(v), - // If the package isn't found, it likely means that the user needs to use - // `--reload` to get the latest npm package information. Although it seems - // like we could make this smart by fetching the latest information for - // this package here, we really need a full restart. There could be very - // interesting bugs that occur if this package's version was resolved by - // something previous using the old information, then now being smart here - // causes a new fetch of the package information, meaning this time the - // previous resolution of this package's version resolved to an older - // version, but next time to a different version because it has new information. - None => bail!( - concat!( - "Could not find npm package '{}' matching {}{}. ", - "Try retrieving the latest npm package information by running with --reload", - ), - pkg_name, - version_matcher.version_text(), - match parent { - Some(id) => format!(" as specified in {}", id), - None => String::new(), - } - ), - } -} - -fn name_without_path(name: &str) -> &str { - let mut search_start_index = 0; - if name.starts_with('@') { - if let Some(slash_index) = name.find('/') { - search_start_index = slash_index + 1; - } - } - if let Some(slash_index) = &name[search_start_index..].find('/') { - // get the name up until the path slash - &name[0..search_start_index + slash_index] - } else { - name - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_npm_package_ref() { - assert_eq!( - NpmPackageReference::from_str("npm:@package/test").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test@1").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: Some(SpecifierVersionReq::parse("1").unwrap()), - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test@~1.1/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: Some(SpecifierVersionReq::parse("~1.1").unwrap()), - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test@^1.2").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: Some(SpecifierVersionReq::parse("^1.2").unwrap()), - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test@~1.1/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: Some(SpecifierVersionReq::parse("~1.1").unwrap()), - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package") - .err() - .unwrap() - .to_string(), - "Not a valid package: @package" - ); - } - - #[test] - fn test_name_without_path() { - assert_eq!(name_without_path("foo"), "foo"); - assert_eq!(name_without_path("@foo/bar"), "@foo/bar"); - 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"); - } -} |