mirror of
https://github.com/denoland/deno.git
synced 2024-11-02 09:34:19 -04:00
600fff79cd
- 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`).
298 lines
8.3 KiB
Rust
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."
|
|
);
|
|
}
|
|
}
|