summaryrefslogtreecommitdiff
path: root/cli/npm/resolution/reference.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm/resolution/reference.rs')
-rw-r--r--cli/npm/resolution/reference.rs298
1 files changed, 298 insertions, 0 deletions
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."
+ );
+ }
+}