summaryrefslogtreecommitdiff
path: root/cli/npm/resolution/mod.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-02-17 09:12:22 -0500
committerGitHub <noreply@github.com>2023-02-17 09:12:22 -0500
commit610b8cc2bf6404d0905cc273b31d85555a6912e9 (patch)
tree8e7bf0a56a90e6bb33462a86e6d886501fb4c621 /cli/npm/resolution/mod.rs
parentf8435d20b0e9408e50bfb24793becc0e476cc285 (diff)
refactor: add `NpmPackageId` back from deno_graph as `NpmPackageNodeId` (#17804)
The `NpmPackageId` struct is being renamed to `NpmPackageNodeId`. In a future PR it will be moved down into only npm dependency resolution and a `NpmPackageId` struct will be introduced in `deno_graph` that only has the name and version of the package (no peer dependency identifier information). So a `NpmPackageReq` will map to an `NpmPackageId`, which will map to an `NpmPackageNodeId` in the npm resolution.
Diffstat (limited to 'cli/npm/resolution/mod.rs')
-rw-r--r--cli/npm/resolution/mod.rs214
1 files changed, 209 insertions, 5 deletions
diff --git a/cli/npm/resolution/mod.rs b/cli/npm/resolution/mod.rs
index 0f6dcb910..ec6ee0dda 100644
--- a/cli/npm/resolution/mod.rs
+++ b/cli/npm/resolution/mod.rs
@@ -6,10 +6,11 @@ use std::collections::HashSet;
use deno_core::error::AnyError;
use deno_core::futures;
use deno_core::parking_lot::RwLock;
-use deno_graph::npm::NpmPackageId;
use deno_graph::npm::NpmPackageReq;
+use deno_graph::semver::Version;
use serde::Deserialize;
use serde::Serialize;
+use thiserror::Error;
use crate::args::Lockfile;
@@ -30,9 +31,165 @@ use graph::Graph;
pub use snapshot::NpmResolutionSnapshot;
pub use specifier::resolve_graph_npm_info;
+#[derive(Debug, Error)]
+#[error("Invalid npm package id '{text}'. {message}")]
+pub struct NpmPackageNodeIdDeserializationError {
+ message: String,
+ text: String,
+}
+
+/// A resolved unique identifier for an npm package. This contains
+/// the resolved name, version, and peer dependency resolution identifiers.
+#[derive(
+ Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize,
+)]
+pub struct NpmPackageNodeId {
+ pub name: String,
+ pub version: Version,
+ pub peer_dependencies: Vec<NpmPackageNodeId>,
+}
+
+impl NpmPackageNodeId {
+ #[allow(unused)]
+ pub fn scope(&self) -> Option<&str> {
+ if self.name.starts_with('@') && self.name.contains('/') {
+ self.name.split('/').next()
+ } else {
+ None
+ }
+ }
+
+ pub fn as_serialized(&self) -> String {
+ self.as_serialized_with_level(0)
+ }
+
+ fn as_serialized_with_level(&self, level: usize) -> String {
+ // WARNING: This should not change because it's used in the lockfile
+ let mut result = format!(
+ "{}@{}",
+ if level == 0 {
+ self.name.to_string()
+ } else {
+ self.name.replace('/', "+")
+ },
+ self.version
+ );
+ for peer in &self.peer_dependencies {
+ // unfortunately we can't do something like `_3` when
+ // this gets deep because npm package names can start
+ // with a number
+ result.push_str(&"_".repeat(level + 1));
+ result.push_str(&peer.as_serialized_with_level(level + 1));
+ }
+ result
+ }
+
+ pub fn from_serialized(
+ id: &str,
+ ) -> Result<Self, NpmPackageNodeIdDeserializationError> {
+ use monch::*;
+
+ fn parse_name(input: &str) -> ParseResult<&str> {
+ if_not_empty(substring(move |input| {
+ for (pos, c) in input.char_indices() {
+ // first character might be a scope, so skip it
+ if pos > 0 && c == '@' {
+ return Ok((&input[pos..], ()));
+ }
+ }
+ ParseError::backtrace()
+ }))(input)
+ }
+
+ fn parse_version(input: &str) -> ParseResult<&str> {
+ if_not_empty(substring(skip_while(|c| c != '_')))(input)
+ }
+
+ 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 Version::parse_from_npm(version) {
+ Ok(version) => Ok((input, (name.to_string(), version))),
+ Err(err) => ParseError::fail(at_version_input, format!("{err:#}")),
+ }
+ }
+
+ fn parse_level_at_level<'a>(
+ level: usize,
+ ) -> impl Fn(&'a str) -> ParseResult<'a, ()> {
+ fn parse_level(input: &str) -> ParseResult<usize> {
+ let level = input.chars().take_while(|c| *c == '_').count();
+ Ok((&input[level..], level))
+ }
+
+ move |input| {
+ let (input, parsed_level) = parse_level(input)?;
+ if parsed_level == level {
+ Ok((input, ()))
+ } else {
+ ParseError::backtrace()
+ }
+ }
+ }
+
+ fn parse_peers_at_level<'a>(
+ level: usize,
+ ) -> impl Fn(&'a str) -> ParseResult<'a, Vec<NpmPackageNodeId>> {
+ move |mut input| {
+ let mut peers = Vec::new();
+ while let Ok((level_input, _)) = parse_level_at_level(level)(input) {
+ input = level_input;
+ let peer_result = parse_id_at_level(level)(input)?;
+ input = peer_result.0;
+ peers.push(peer_result.1);
+ }
+ Ok((input, peers))
+ }
+ }
+
+ fn parse_id_at_level<'a>(
+ level: usize,
+ ) -> impl Fn(&'a str) -> ParseResult<'a, NpmPackageNodeId> {
+ move |input| {
+ let (input, (name, version)) = parse_name_and_version(input)?;
+ let name = if level > 0 {
+ name.replace('+', "/")
+ } else {
+ name
+ };
+ let (input, peer_dependencies) =
+ parse_peers_at_level(level + 1)(input)?;
+ Ok((
+ input,
+ NpmPackageNodeId {
+ name,
+ version,
+ peer_dependencies,
+ },
+ ))
+ }
+ }
+
+ with_failure_handling(parse_id_at_level(0))(id).map_err(|err| {
+ NpmPackageNodeIdDeserializationError {
+ message: format!("{err:#}"),
+ text: id.to_string(),
+ }
+ })
+ }
+
+ pub fn display(&self) -> String {
+ // Don't implement std::fmt::Display because we don't
+ // want this to be used by accident in certain scenarios.
+ format!("{}@{}", self.name, self.version)
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NpmResolutionPackage {
- pub id: NpmPackageId,
+ pub id: NpmPackageNodeId,
/// The peer dependency resolution can differ for the same
/// package (name and version) depending on where it is in
/// the resolution tree. This copy index indicates which
@@ -41,7 +198,7 @@ pub struct NpmResolutionPackage {
pub dist: NpmPackageVersionDistInfo,
/// Key is what the package refers to the other package as,
/// which could be different from the package name.
- pub dependencies: HashMap<String, NpmPackageId>,
+ pub dependencies: HashMap<String, NpmPackageNodeId>,
}
impl NpmResolutionPackage {
@@ -189,14 +346,14 @@ impl NpmResolution {
pub fn resolve_package_from_id(
&self,
- id: &NpmPackageId,
+ id: &NpmPackageNodeId,
) -> Option<NpmResolutionPackage> {
self.snapshot.read().package_from_id(id).cloned()
}
pub fn resolve_package_cache_folder_id_from_id(
&self,
- id: &NpmPackageId,
+ id: &NpmPackageNodeId,
) -> Option<NpmPackageCacheFolderId> {
self
.snapshot
@@ -255,3 +412,50 @@ impl NpmResolution {
Ok(())
}
}
+
+#[cfg(test)]
+mod test {
+ use deno_graph::semver::Version;
+
+ use super::NpmPackageNodeId;
+
+ #[test]
+ fn serialize_npm_package_id() {
+ let id = NpmPackageNodeId {
+ name: "pkg-a".to_string(),
+ version: Version::parse_from_npm("1.2.3").unwrap(),
+ peer_dependencies: vec![
+ NpmPackageNodeId {
+ name: "pkg-b".to_string(),
+ version: Version::parse_from_npm("3.2.1").unwrap(),
+ peer_dependencies: vec![
+ NpmPackageNodeId {
+ name: "pkg-c".to_string(),
+ version: Version::parse_from_npm("1.3.2").unwrap(),
+ peer_dependencies: vec![],
+ },
+ NpmPackageNodeId {
+ name: "pkg-d".to_string(),
+ version: Version::parse_from_npm("2.3.4").unwrap(),
+ peer_dependencies: vec![],
+ },
+ ],
+ },
+ NpmPackageNodeId {
+ name: "pkg-e".to_string(),
+ version: Version::parse_from_npm("2.3.1").unwrap(),
+ peer_dependencies: vec![NpmPackageNodeId {
+ name: "pkg-f".to_string(),
+ version: Version::parse_from_npm("2.3.1").unwrap(),
+ peer_dependencies: vec![],
+ }],
+ },
+ ],
+ };
+
+ // this shouldn't change because it's used in the lockfile
+ let serialized = id.as_serialized();
+ assert_eq!(serialized, "pkg-a@1.2.3_pkg-b@3.2.1__pkg-c@1.3.2__pkg-d@2.3.4_pkg-e@2.3.1__pkg-f@2.3.1");
+ assert_eq!(NpmPackageNodeId::from_serialized(&serialized).unwrap(), id);
+ }
+}