summaryrefslogtreecommitdiff
path: root/cli/npm/resolution
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm/resolution')
-rw-r--r--cli/npm/resolution/graph.rs73
-rw-r--r--cli/npm/resolution/mod.rs35
-rw-r--r--cli/npm/resolution/reference.rs298
-rw-r--r--cli/npm/resolution/snapshot.rs14
-rw-r--r--cli/npm/resolution/specifier.rs310
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();