1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-04 08:54:20 -05:00
denoland-deno/cli/npm/resolution/reference.rs
David Sherret 600fff79cd
refactor(semver): generalize semver related structs (#17605)
- Generalizes the npm version code (ex. `NpmVersion` -> `Version`,
`NpmVersionReq` -> `VersionReq`). This is a slow refactor towards
extracting out this code for deno specifiers and better usage in
deno_graph.
- Removes `SpecifierVersionReq`. Consolidates `NpmVersionReq` and
`SpecifierVersionReq` to just `VersionReq`
- Removes `NpmVersionMatcher`. This now just looks at `VersionReq`.
- Paves the way to allow us to create `NpmPackageReference`'s from a
package.json's dependencies/dev dependencies
(`VersionReq::parse_from_npm`).
2023-01-31 21:27:40 -05:00

298 lines
8.3 KiB
Rust

// 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."
);
}
}