diff options
Diffstat (limited to 'cli/npm/resolution')
-rw-r--r-- | cli/npm/resolution/graph.rs | 73 | ||||
-rw-r--r-- | cli/npm/resolution/mod.rs | 35 | ||||
-rw-r--r-- | cli/npm/resolution/reference.rs | 298 | ||||
-rw-r--r-- | cli/npm/resolution/snapshot.rs | 14 | ||||
-rw-r--r-- | cli/npm/resolution/specifier.rs | 310 |
5 files changed, 363 insertions, 367 deletions
diff --git a/cli/npm/resolution/graph.rs b/cli/npm/resolution/graph.rs index e21048149..20f192fbf 100644 --- a/cli/npm/resolution/graph.rs +++ b/cli/npm/resolution/graph.rs @@ -14,22 +14,25 @@ use deno_core::futures; use deno_core::parking_lot::Mutex; use deno_core::parking_lot::MutexGuard; use log::debug; +use once_cell::sync::Lazy; use crate::npm::cache::should_sync_download; use crate::npm::registry::NpmDependencyEntry; use crate::npm::registry::NpmDependencyEntryKind; use crate::npm::registry::NpmPackageInfo; use crate::npm::registry::NpmPackageVersionInfo; -use crate::npm::semver::NpmVersion; -use crate::npm::semver::NpmVersionReq; use crate::npm::NpmRegistryApi; +use crate::semver::Version; +use crate::semver::VersionReq; use super::snapshot::NpmResolutionSnapshot; use super::snapshot::SnapshotPackageCopyIndexResolver; use super::NpmPackageId; use super::NpmPackageReq; use super::NpmResolutionPackage; -use super::NpmVersionMatcher; + +pub static LATEST_VERSION_REQ: Lazy<VersionReq> = + Lazy::new(|| VersionReq::parse_from_specifier("latest").unwrap()); /// A memory efficient path of visited name and versions in the graph /// which is used to detect cycles. @@ -419,11 +422,11 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> fn resolve_best_package_version_and_info<'info>( &self, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, package_info: &'info NpmPackageInfo, ) -> Result<VersionAndInfo<'info>, AnyError> { if let Some(version) = - self.resolve_best_package_version(package_info, version_matcher)? + self.resolve_best_package_version(package_info, version_req)? { match package_info.versions.get(&version.to_string()) { Some(version_info) => Ok(VersionAndInfo { @@ -440,20 +443,19 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> } } else { // get the information - get_resolved_package_version_and_info(version_matcher, package_info, None) + get_resolved_package_version_and_info(version_req, package_info, None) } } fn resolve_best_package_version( &self, package_info: &NpmPackageInfo, - version_matcher: &impl NpmVersionMatcher, - ) -> Result<Option<NpmVersion>, AnyError> { - let mut maybe_best_version: Option<&NpmVersion> = None; + version_req: &VersionReq, + ) -> Result<Option<Version>, AnyError> { + let mut maybe_best_version: Option<&Version> = None; if let Some(ids) = self.graph.packages_by_name.get(&package_info.name) { for version in ids.iter().map(|id| &id.version) { - if version_req_satisfies(version_matcher, version, package_info, None)? - { + if version_req_satisfies(version_req, version, package_info, None)? { let is_best_version = maybe_best_version .as_ref() .map(|best_version| (*best_version).cmp(version).is_lt()) @@ -478,7 +480,10 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> ) -> Result<(), AnyError> { let (_, node) = self.resolve_node_from_info( &package_req.name, - package_req, + package_req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), package_info, None, )?; @@ -557,12 +562,12 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> fn resolve_node_from_info( &mut self, pkg_req_name: &str, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, package_info: &NpmPackageInfo, parent_id: Option<&NpmPackageId>, ) -> Result<(NpmPackageId, Arc<Mutex<Node>>), AnyError> { - let version_and_info = self - .resolve_best_package_version_and_info(version_matcher, package_info)?; + let version_and_info = + self.resolve_best_package_version_and_info(version_req, package_info)?; let id = NpmPackageId { name: package_info.name.to_string(), version: version_and_info.version.clone(), @@ -575,7 +580,7 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> None => "<package-req>".to_string(), }, pkg_req_name, - version_matcher.version_text(), + version_req.version_text(), id.as_serialized(), ); let (created, node) = self.graph.get_or_create_for_id(&id); @@ -996,22 +1001,22 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> #[derive(Clone)] struct VersionAndInfo<'a> { - version: NpmVersion, + version: Version, info: &'a NpmPackageVersionInfo, } fn get_resolved_package_version_and_info<'a>( - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, info: &'a NpmPackageInfo, parent: Option<&NpmPackageId>, ) -> Result<VersionAndInfo<'a>, AnyError> { - if let Some(tag) = version_matcher.tag() { + if let Some(tag) = version_req.tag() { tag_to_version_info(info, tag, parent) } else { let mut maybe_best_version: Option<VersionAndInfo> = None; for version_info in info.versions.values() { - let version = NpmVersion::parse(&version_info.version)?; - if version_matcher.matches(&version) { + let version = Version::parse_from_npm(&version_info.version)?; + if version_req.matches(&version) { let is_best_version = maybe_best_version .as_ref() .map(|best_version| best_version.version.cmp(&version).is_lt()) @@ -1042,7 +1047,7 @@ fn get_resolved_package_version_and_info<'a>( "Try retrieving the latest npm package information by running with --reload", ), info.name, - version_matcher.version_text(), + version_req.version_text(), match parent { Some(id) => format!(" as specified in {}", id.display()), None => String::new(), @@ -1053,17 +1058,17 @@ fn get_resolved_package_version_and_info<'a>( } fn version_req_satisfies( - matcher: &impl NpmVersionMatcher, - version: &NpmVersion, + version_req: &VersionReq, + version: &Version, package_info: &NpmPackageInfo, parent: Option<&NpmPackageId>, ) -> Result<bool, AnyError> { - match matcher.tag() { + match version_req.tag() { Some(tag) => { let tag_version = tag_to_version_info(package_info, tag, parent)?.version; Ok(tag_version == *version) } - None => Ok(matcher.matches(version)), + None => Ok(version_req.matches(version)), } } @@ -1081,7 +1086,7 @@ fn tag_to_version_info<'a>( // explicit version. if tag == "latest" && info.name == "@types/node" { return get_resolved_package_version_and_info( - &NpmVersionReq::parse("18.0.0 - 18.11.18").unwrap(), + &VersionReq::parse_from_npm("18.0.0 - 18.11.18").unwrap(), info, parent, ); @@ -1090,7 +1095,7 @@ fn tag_to_version_info<'a>( if let Some(version) = info.dist_tags.get(tag) { match info.versions.get(version) { Some(info) => Ok(VersionAndInfo { - version: NpmVersion::parse(version)?, + version: Version::parse_from_npm(version)?, info, }), None => { @@ -1128,7 +1133,11 @@ mod test { )]), }; let result = get_resolved_package_version_and_info( - &package_ref.req, + package_ref + .req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), &package_info, None, ); @@ -1157,7 +1166,11 @@ mod test { )]), }; let result = get_resolved_package_version_and_info( - &package_ref.req, + package_ref + .req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), &package_info, None, ); diff --git a/cli/npm/resolution/mod.rs b/cli/npm/resolution/mod.rs index 407651ccb..990ad8d06 100644 --- a/cli/npm/resolution/mod.rs +++ b/cli/npm/resolution/mod.rs @@ -11,6 +11,7 @@ use serde::Deserialize; use serde::Serialize; use crate::args::Lockfile; +use crate::semver::Version; use self::graph::GraphDependencyResolver; use self::snapshot::NpmPackagesPartitioned; @@ -19,33 +20,25 @@ use super::cache::should_sync_download; use super::cache::NpmPackageCacheFolderId; use super::registry::NpmPackageVersionDistInfo; use super::registry::RealNpmRegistryApi; -use super::semver::NpmVersion; use super::NpmRegistryApi; mod graph; +mod reference; mod snapshot; mod specifier; use graph::Graph; +pub use reference::NpmPackageReference; +pub use reference::NpmPackageReq; pub use snapshot::NpmResolutionSnapshot; pub use specifier::resolve_graph_npm_info; -pub use specifier::NpmPackageReference; -pub use specifier::NpmPackageReq; - -/// 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( Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, )] pub struct NpmPackageId { pub name: String, - pub version: NpmVersion, + pub version: Version, pub peer_dependencies: Vec<NpmPackageId>, } @@ -103,14 +96,12 @@ impl NpmPackageId { if_not_empty(substring(skip_while(|c| c != '_')))(input) } - fn parse_name_and_version( - input: &str, - ) -> ParseResult<(String, NpmVersion)> { + fn parse_name_and_version(input: &str) -> ParseResult<(String, Version)> { let (input, name) = parse_name(input)?; let (input, _) = ch('@')(input)?; let at_version_input = input; let (input, version) = parse_version(input)?; - match NpmVersion::parse(version) { + match Version::parse_from_npm(version) { Ok(version) => Ok((input, (name.to_string(), version))), Err(err) => ParseError::fail(at_version_input, format!("{err:#}")), } @@ -417,30 +408,30 @@ mod tests { fn serialize_npm_package_id() { let id = NpmPackageId { name: "pkg-a".to_string(), - version: NpmVersion::parse("1.2.3").unwrap(), + version: Version::parse_from_npm("1.2.3").unwrap(), peer_dependencies: vec![ NpmPackageId { name: "pkg-b".to_string(), - version: NpmVersion::parse("3.2.1").unwrap(), + version: Version::parse_from_npm("3.2.1").unwrap(), peer_dependencies: vec![ NpmPackageId { name: "pkg-c".to_string(), - version: NpmVersion::parse("1.3.2").unwrap(), + version: Version::parse_from_npm("1.3.2").unwrap(), peer_dependencies: vec![], }, NpmPackageId { name: "pkg-d".to_string(), - version: NpmVersion::parse("2.3.4").unwrap(), + version: Version::parse_from_npm("2.3.4").unwrap(), peer_dependencies: vec![], }, ], }, NpmPackageId { name: "pkg-e".to_string(), - version: NpmVersion::parse("2.3.1").unwrap(), + version: Version::parse_from_npm("2.3.1").unwrap(), peer_dependencies: vec![NpmPackageId { name: "pkg-f".to_string(), - version: NpmVersion::parse("2.3.1").unwrap(), + version: Version::parse_from_npm("2.3.1").unwrap(), peer_dependencies: vec![], }], }, diff --git a/cli/npm/resolution/reference.rs b/cli/npm/resolution/reference.rs new file mode 100644 index 000000000..2d34bcc34 --- /dev/null +++ b/cli/npm/resolution/reference.rs @@ -0,0 +1,298 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +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 serde::Deserialize; +use serde::Serialize; + +use crate::semver::VersionReq; + +/// A reference to an npm package's name, version constraint, and potential sub path. +/// +/// This contains all the information found in an npm specifier. +#[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 original_text = specifier; + let specifier = match specifier.strip_prefix("npm:") { + Some(s) => { + // Strip leading slash, which might come from import map + s.strip_prefix('/').unwrap_or(s) + } + None => { + // don't allocate a string here and instead use a static string + // because this is hit a lot when a url is not an npm specifier + return Err(generic_error("Not an npm 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 req = match NpmPackageReq::parse_from_parts(name_parts) { + Ok(pkg_req) => pkg_req, + Err(err) => { + return Err(generic_error(format!( + "Invalid npm specifier '{original_text}'. {err:#}" + ))) + } + }; + let sub_path = if parts.len() == name_parts.len() { + None + } else { + let sub_path = parts[name_part_len..].join("/"); + if sub_path.is_empty() { + None + } else { + Some(sub_path) + } + }; + + 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:{req}/{sub_path}'. Did you mean to write 'npm:{req}{version}/{new_sub_path}'?" + ); + return Err(generic_error(msg)); + } + } + + Ok(NpmPackageReference { 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, "npm:{}/{}", self.req, sub_path) + } else { + write!(f, "npm:{}", self.req) + } + } +} + +/// The name and version constraint component of an `NpmPackageReference`. +#[derive( + Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, +)] +pub struct NpmPackageReq { + pub name: String, + pub version_req: Option<VersionReq>, +} + +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 NpmPackageReq { + pub fn from_str(text: &str) -> Result<Self, AnyError> { + let parts = text.split('/').collect::<Vec<_>>(); + match NpmPackageReq::parse_from_parts(&parts) { + Ok(req) => Ok(req), + Err(err) => { + let msg = format!("Invalid npm package requirement '{text}'. {err:#}"); + Err(generic_error(msg)) + } + } + } + + fn parse_from_parts(name_parts: &[&str]) -> Result<Self, AnyError> { + assert!(!name_parts.is_empty()); // this should be provided the result of a string split + let last_name_part = &name_parts[name_parts.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 = VersionReq::parse_from_specifier(version) + .with_context(|| "Invalid version requirement.")?; + let name = if name_parts.len() == 1 { + last_name_part.to_string() + } else { + format!("{}/{}", name_parts[0], last_name_part) + }; + (name, Some(version_req)) + } else { + (name_parts.join("/"), None) + }; + if name.is_empty() { + bail!("Did not contain a package name.") + } + Ok(Self { name, version_req }) + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + 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(VersionReq::parse_from_specifier("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(VersionReq::parse_from_specifier("~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(VersionReq::parse_from_specifier("^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(VersionReq::parse_from_specifier("~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" + ); + + // should parse leading slash + 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/").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: None, + }, + sub_path: None, + } + ); + + // should error for no name + assert_eq!( + NpmPackageReference::from_str("npm:/") + .err() + .unwrap() + .to_string(), + "Invalid npm specifier 'npm:/'. Did not contain a package name." + ); + assert_eq!( + NpmPackageReference::from_str("npm://test") + .err() + .unwrap() + .to_string(), + "Invalid npm specifier 'npm://test'. Did not contain a package name." + ); + } +} diff --git a/cli/npm/resolution/snapshot.rs b/cli/npm/resolution/snapshot.rs index be64ea611..934320a1d 100644 --- a/cli/npm/resolution/snapshot.rs +++ b/cli/npm/resolution/snapshot.rs @@ -19,11 +19,11 @@ use crate::npm::cache::NpmPackageCacheFolderId; use crate::npm::registry::NpmPackageVersionDistInfo; use crate::npm::registry::NpmRegistryApi; use crate::npm::registry::RealNpmRegistryApi; +use crate::semver::VersionReq; use super::NpmPackageId; use super::NpmPackageReq; use super::NpmResolutionPackage; -use super::NpmVersionMatcher; /// Packages partitioned by if they are "copy" packages or not. pub struct NpmPackagesPartitioned { @@ -159,12 +159,8 @@ impl NpmResolutionSnapshot { // 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(id) = self.resolve_best_package_id(name, &req) { + let any_version_req = VersionReq::parse_from_npm("*").unwrap(); + if let Some(id) = self.resolve_best_package_id(name, &any_version_req) { if let Some(pkg) = self.packages.get(&id) { return Ok(pkg); } @@ -201,14 +197,14 @@ impl NpmResolutionSnapshot { pub fn resolve_best_package_id( &self, name: &str, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, ) -> Option<NpmPackageId> { // todo(dsherret): this is not exactly correct because some ids // will be better than others due to peer dependencies let mut maybe_best_id: Option<&NpmPackageId> = None; if let Some(ids) = self.packages_by_name.get(name) { for id in ids { - if version_matcher.matches(&id.version) { + if version_req.matches(&id.version) { let is_best_version = maybe_best_id .as_ref() .map(|best_id| best_id.version.cmp(&id.version).is_lt()) diff --git a/cli/npm/resolution/specifier.rs b/cli/npm/resolution/specifier.rs index 0aa693472..78d313412 100644 --- a/cli/npm/resolution/specifier.rs +++ b/cli/npm/resolution/specifier.rs @@ -6,165 +6,13 @@ use std::collections::HashSet; use std::collections::VecDeque; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::Context; -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_graph::ModuleGraph; use deno_graph::Resolved; -use serde::Deserialize; -use serde::Serialize; -use super::super::semver::NpmVersion; -use super::super::semver::SpecifierVersionReq; -use super::NpmVersionMatcher; +use crate::semver::VersionReq; -#[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 original_text = specifier; - let specifier = match specifier.strip_prefix("npm:") { - Some(s) => { - // Strip leading slash, which might come from import map - s.strip_prefix('/').unwrap_or(s) - } - None => { - // don't allocate a string here and instead use a static string - // because this is hit a lot when a url is not an npm specifier - return Err(generic_error("Not an npm 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 { - let sub_path = parts[name_part_len..].join("/"); - if sub_path.is_empty() { - None - } else { - Some(sub_path) - } - }; - - 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:{name}/{sub_path}'. Did you mean to write 'npm:{name}{version}/{new_sub_path}'?" - ); - return Err(generic_error(msg)); - } - } - - if name.is_empty() { - let msg = format!( - "Invalid npm specifier '{original_text}'. Did not contain a package name." - ); - 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, "npm:{}/{}", self.req, sub_path) - } else { - write!(f, "npm:{}", 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 NpmPackageReq { - pub fn from_str(text: &str) -> Result<Self, AnyError> { - // probably should do something more targeted in the future - let reference = NpmPackageReference::from_str(&format!("npm:{text}"))?; - Ok(reference.req) - } -} - -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()) - } -} +use super::NpmPackageReference; +use super::NpmPackageReq; pub struct GraphNpmInfo { /// The order of these package requirements is the order they @@ -537,10 +385,7 @@ fn cmp_folder_specifiers(a: &ModuleSpecifier, b: &ModuleSpecifier) -> Ordering { // duplicate packages (so sort None last since it's `*`), but // mostly to create some determinism around how these are resolved. fn cmp_package_req(a: &NpmPackageReq, b: &NpmPackageReq) -> Ordering { - fn cmp_specifier_version_req( - a: &SpecifierVersionReq, - b: &SpecifierVersionReq, - ) -> Ordering { + fn cmp_specifier_version_req(a: &VersionReq, b: &VersionReq) -> Ordering { match a.tag() { Some(a_tag) => match b.tag() { Some(b_tag) => b_tag.cmp(a_tag), // sort descending @@ -582,153 +427,6 @@ 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" - ); - - // should parse leading slash - 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/").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - // should error for no name - assert_eq!( - NpmPackageReference::from_str("npm:/") - .err() - .unwrap() - .to_string(), - "Invalid npm specifier 'npm:/'. Did not contain a package name." - ); - assert_eq!( - NpmPackageReference::from_str("npm://test") - .err() - .unwrap() - .to_string(), - "Invalid npm specifier 'npm://test'. Did not contain a package name." - ); - } - - #[test] fn sorting_folder_specifiers() { fn cmp(a: &str, b: &str) -> Ordering { let a = ModuleSpecifier::parse(a).unwrap(); |