summaryrefslogtreecommitdiff
path: root/cli/npm/resolution/graph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm/resolution/graph.rs')
-rw-r--r--cli/npm/resolution/graph.rs2033
1 files changed, 2033 insertions, 0 deletions
diff --git a/cli/npm/resolution/graph.rs b/cli/npm/resolution/graph.rs
new file mode 100644
index 000000000..497067925
--- /dev/null
+++ b/cli/npm/resolution/graph.rs
@@ -0,0 +1,2033 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::collections::HashMap;
+use std::collections::VecDeque;
+use std::sync::Arc;
+
+use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
+use deno_core::error::AnyError;
+use deno_core::futures;
+use deno_core::parking_lot::Mutex;
+use deno_core::parking_lot::MutexGuard;
+use log::debug;
+
+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 super::snapshot::NpmResolutionSnapshot;
+use super::snapshot::SnapshotPackageCopyIndexResolver;
+use super::NpmPackageId;
+use super::NpmPackageReq;
+use super::NpmResolutionPackage;
+use super::NpmVersionMatcher;
+
+/// A memory efficient path of visited name and versions in the graph
+/// which is used to detect cycles.
+///
+/// note(dsherret): although this is definitely more memory efficient
+/// than a HashSet, I haven't done any tests about whether this is
+/// faster in practice.
+#[derive(Default, Clone)]
+struct VisitedVersionsPath {
+ previous_node: Option<Arc<VisitedVersionsPath>>,
+ visited_version_key: String,
+}
+
+impl VisitedVersionsPath {
+ pub fn new(id: &NpmPackageId) -> Arc<Self> {
+ Arc::new(Self {
+ previous_node: None,
+ visited_version_key: Self::id_to_key(id),
+ })
+ }
+
+ pub fn with_parent(
+ self: &Arc<VisitedVersionsPath>,
+ parent: &NodeParent,
+ ) -> Option<Arc<Self>> {
+ match parent {
+ NodeParent::Node(id) => self.with_id(id),
+ NodeParent::Req => Some(self.clone()),
+ }
+ }
+
+ pub fn with_id(
+ self: &Arc<VisitedVersionsPath>,
+ id: &NpmPackageId,
+ ) -> Option<Arc<Self>> {
+ if self.has_visited(id) {
+ None
+ } else {
+ Some(Arc::new(Self {
+ previous_node: Some(self.clone()),
+ visited_version_key: Self::id_to_key(id),
+ }))
+ }
+ }
+
+ pub fn has_visited(self: &Arc<Self>, id: &NpmPackageId) -> bool {
+ let mut maybe_next_node = Some(self);
+ let key = Self::id_to_key(id);
+ while let Some(next_node) = maybe_next_node {
+ if next_node.visited_version_key == key {
+ return true;
+ }
+ maybe_next_node = next_node.previous_node.as_ref();
+ }
+ false
+ }
+
+ fn id_to_key(id: &NpmPackageId) -> String {
+ format!("{}@{}", id.name, id.version)
+ }
+}
+
+/// A memory efficient path of the visited specifiers in the tree.
+#[derive(Default, Clone)]
+struct GraphSpecifierPath {
+ previous_node: Option<Arc<GraphSpecifierPath>>,
+ specifier: String,
+}
+
+impl GraphSpecifierPath {
+ pub fn new(specifier: String) -> Arc<Self> {
+ Arc::new(Self {
+ previous_node: None,
+ specifier,
+ })
+ }
+
+ pub fn with_specifier(self: &Arc<Self>, specifier: String) -> Arc<Self> {
+ Arc::new(Self {
+ previous_node: Some(self.clone()),
+ specifier,
+ })
+ }
+
+ pub fn pop(&self) -> Option<&Arc<Self>> {
+ self.previous_node.as_ref()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
+enum NodeParent {
+ /// These are top of the graph npm package requirements
+ /// as specified in Deno code.
+ Req,
+ /// A reference to another node, which is a resolved package.
+ Node(NpmPackageId),
+}
+
+/// A resolved package in the resolution graph.
+#[derive(Debug)]
+struct Node {
+ pub id: NpmPackageId,
+ /// If the node was forgotten due to having no parents.
+ pub forgotten: bool,
+ // Use BTreeMap and BTreeSet in order to create determinism
+ // when going up and down the tree
+ pub parents: BTreeMap<String, BTreeSet<NodeParent>>,
+ pub children: BTreeMap<String, NpmPackageId>,
+ pub deps: Arc<Vec<NpmDependencyEntry>>,
+}
+
+impl Node {
+ pub fn add_parent(&mut self, specifier: String, parent: NodeParent) {
+ self.parents.entry(specifier).or_default().insert(parent);
+ }
+
+ pub fn remove_parent(&mut self, specifier: &str, parent: &NodeParent) {
+ if let Some(parents) = self.parents.get_mut(specifier) {
+ parents.remove(parent);
+ if parents.is_empty() {
+ self.parents.remove(specifier);
+ }
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct Graph {
+ package_reqs: HashMap<String, NpmPackageId>,
+ packages_by_name: HashMap<String, Vec<NpmPackageId>>,
+ // Ideally this value would be Rc<RefCell<Node>>, but we need to use a Mutex
+ // because the lsp requires Send and this code is executed in the lsp.
+ // Would be nice if the lsp wasn't Send.
+ packages: HashMap<NpmPackageId, Arc<Mutex<Node>>>,
+ // This will be set when creating from a snapshot, then
+ // inform the final snapshot creation.
+ packages_to_copy_index: HashMap<NpmPackageId, usize>,
+}
+
+impl Graph {
+ pub fn from_snapshot(snapshot: NpmResolutionSnapshot) -> Self {
+ fn fill_for_id(
+ graph: &mut Graph,
+ id: &NpmPackageId,
+ packages: &HashMap<NpmPackageId, NpmResolutionPackage>,
+ ) -> Arc<Mutex<Node>> {
+ let resolution = packages.get(id).unwrap();
+ let (created, node) = graph.get_or_create_for_id(id);
+ if created {
+ for (name, child_id) in &resolution.dependencies {
+ let child_node = fill_for_id(graph, child_id, packages);
+ graph.set_child_parent_node(name, &child_node, id);
+ }
+ }
+ node
+ }
+
+ let mut graph = Self {
+ // Note: It might be more correct to store the copy index
+ // from past resolutions with the node somehow, but maybe not.
+ packages_to_copy_index: snapshot
+ .packages
+ .iter()
+ .map(|(id, p)| (id.clone(), p.copy_index))
+ .collect(),
+ ..Default::default()
+ };
+ for (package_req, id) in &snapshot.package_reqs {
+ let node = fill_for_id(&mut graph, id, &snapshot.packages);
+ let package_req_text = package_req.to_string();
+ (*node)
+ .lock()
+ .add_parent(package_req_text.clone(), NodeParent::Req);
+ graph.package_reqs.insert(package_req_text, id.clone());
+ }
+ graph
+ }
+
+ pub fn has_package_req(&self, req: &NpmPackageReq) -> bool {
+ self.package_reqs.contains_key(&req.to_string())
+ }
+
+ fn get_or_create_for_id(
+ &mut self,
+ id: &NpmPackageId,
+ ) -> (bool, Arc<Mutex<Node>>) {
+ if let Some(node) = self.packages.get(id) {
+ (false, node.clone())
+ } else {
+ let node = Arc::new(Mutex::new(Node {
+ id: id.clone(),
+ forgotten: false,
+ parents: Default::default(),
+ children: Default::default(),
+ deps: Default::default(),
+ }));
+ self
+ .packages_by_name
+ .entry(id.name.clone())
+ .or_default()
+ .push(id.clone());
+ self.packages.insert(id.clone(), node.clone());
+ (true, node)
+ }
+ }
+
+ fn borrow_node(&self, id: &NpmPackageId) -> MutexGuard<Node> {
+ (**self.packages.get(id).unwrap_or_else(|| {
+ panic!("could not find id {} in the tree", id.as_serialized())
+ }))
+ .lock()
+ }
+
+ fn forget_orphan(&mut self, node_id: &NpmPackageId) {
+ if let Some(node) = self.packages.remove(node_id) {
+ let mut node = (*node).lock();
+ node.forgotten = true;
+ assert_eq!(node.parents.len(), 0);
+
+ // Remove the id from the list of packages by name.
+ let packages_with_name =
+ self.packages_by_name.get_mut(&node.id.name).unwrap();
+ let remove_index = packages_with_name
+ .iter()
+ .position(|id| id == &node.id)
+ .unwrap();
+ packages_with_name.remove(remove_index);
+
+ let parent = NodeParent::Node(node.id.clone());
+ for (specifier, child_id) in &node.children {
+ let mut child = self.borrow_node(child_id);
+ child.remove_parent(specifier, &parent);
+ if child.parents.is_empty() {
+ drop(child); // stop borrowing from self
+ self.forget_orphan(child_id);
+ }
+ }
+ }
+ }
+
+ fn set_child_parent(
+ &mut self,
+ specifier: &str,
+ child: &Mutex<Node>,
+ parent: &NodeParent,
+ ) {
+ match parent {
+ NodeParent::Node(parent_id) => {
+ self.set_child_parent_node(specifier, child, parent_id);
+ }
+ NodeParent::Req => {
+ let mut node = (*child).lock();
+ node.add_parent(specifier.to_string(), parent.clone());
+ self
+ .package_reqs
+ .insert(specifier.to_string(), node.id.clone());
+ }
+ }
+ }
+
+ fn set_child_parent_node(
+ &mut self,
+ specifier: &str,
+ child: &Mutex<Node>,
+ parent_id: &NpmPackageId,
+ ) {
+ let mut child = (*child).lock();
+ let mut parent = (**self.packages.get(parent_id).unwrap_or_else(|| {
+ panic!(
+ "could not find {} in list of packages when setting child {}",
+ parent_id.as_serialized(),
+ child.id.as_serialized()
+ )
+ }))
+ .lock();
+ assert_ne!(parent.id, child.id);
+ parent
+ .children
+ .insert(specifier.to_string(), child.id.clone());
+ child
+ .add_parent(specifier.to_string(), NodeParent::Node(parent.id.clone()));
+ }
+
+ fn remove_child_parent(
+ &mut self,
+ specifier: &str,
+ child_id: &NpmPackageId,
+ parent: &NodeParent,
+ ) {
+ match parent {
+ NodeParent::Node(parent_id) => {
+ let mut node = self.borrow_node(parent_id);
+ if let Some(removed_child_id) = node.children.remove(specifier) {
+ assert_eq!(removed_child_id, *child_id);
+ }
+ }
+ NodeParent::Req => {
+ if let Some(removed_child_id) = self.package_reqs.remove(specifier) {
+ assert_eq!(removed_child_id, *child_id);
+ }
+ }
+ }
+ self.borrow_node(child_id).remove_parent(specifier, parent);
+ }
+
+ pub async fn into_snapshot(
+ self,
+ api: &impl NpmRegistryApi,
+ ) -> Result<NpmResolutionSnapshot, AnyError> {
+ let mut copy_index_resolver =
+ SnapshotPackageCopyIndexResolver::from_map_with_capacity(
+ self.packages_to_copy_index,
+ self.packages.len(),
+ );
+
+ // Iterate through the packages vector in each packages_by_name in order
+ // to set the copy index as this will be deterministic rather than
+ // iterating over the hashmap below.
+ for packages in self.packages_by_name.values() {
+ if packages.len() > 1 {
+ for id in packages {
+ copy_index_resolver.resolve(id);
+ }
+ }
+ }
+
+ let mut packages = HashMap::with_capacity(self.packages.len());
+ for (id, node) in self.packages {
+ let dist = api
+ .package_version_info(&id.name, &id.version)
+ .await?
+ .unwrap()
+ .dist;
+ let node = node.lock();
+ packages.insert(
+ id.clone(),
+ NpmResolutionPackage {
+ copy_index: copy_index_resolver.resolve(&id),
+ id,
+ dist,
+ dependencies: node
+ .children
+ .iter()
+ .map(|(key, value)| (key.clone(), value.clone()))
+ .collect(),
+ },
+ );
+ }
+
+ Ok(NpmResolutionSnapshot {
+ package_reqs: self
+ .package_reqs
+ .into_iter()
+ .map(|(specifier, id)| {
+ (NpmPackageReq::from_str(&specifier).unwrap(), id)
+ })
+ .collect(),
+ packages_by_name: self.packages_by_name,
+ packages,
+ })
+ }
+}
+
+pub struct GraphDependencyResolver<'a, TNpmRegistryApi: NpmRegistryApi> {
+ graph: &'a mut Graph,
+ api: &'a TNpmRegistryApi,
+ pending_unresolved_nodes:
+ VecDeque<(Arc<VisitedVersionsPath>, Arc<Mutex<Node>>)>,
+}
+
+impl<'a, TNpmRegistryApi: NpmRegistryApi>
+ GraphDependencyResolver<'a, TNpmRegistryApi>
+{
+ pub fn new(graph: &'a mut Graph, api: &'a TNpmRegistryApi) -> Self {
+ Self {
+ graph,
+ api,
+ pending_unresolved_nodes: Default::default(),
+ }
+ }
+
+ fn resolve_best_package_version_and_info(
+ &self,
+ name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ package_info: NpmPackageInfo,
+ ) -> Result<VersionAndInfo, AnyError> {
+ if let Some(version) =
+ self.resolve_best_package_version(name, version_matcher)
+ {
+ match package_info.versions.get(&version.to_string()) {
+ Some(version_info) => Ok(VersionAndInfo {
+ version,
+ info: version_info.clone(),
+ }),
+ None => {
+ bail!("could not find version '{}' for '{}'", version, name)
+ }
+ }
+ } else {
+ // get the information
+ get_resolved_package_version_and_info(
+ name,
+ version_matcher,
+ package_info,
+ None,
+ )
+ }
+ }
+
+ fn resolve_best_package_version(
+ &self,
+ name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ ) -> Option<NpmVersion> {
+ let mut maybe_best_version: Option<&NpmVersion> = None;
+ if let Some(ids) = self.graph.packages_by_name.get(name) {
+ for version in ids.iter().map(|id| &id.version) {
+ if version_matcher.matches(version) {
+ let is_best_version = maybe_best_version
+ .as_ref()
+ .map(|best_version| (*best_version).cmp(version).is_lt())
+ .unwrap_or(true);
+ if is_best_version {
+ maybe_best_version = Some(version);
+ }
+ }
+ }
+ }
+ maybe_best_version.cloned()
+ }
+
+ pub fn add_package_req(
+ &mut self,
+ package_req: &NpmPackageReq,
+ package_info: NpmPackageInfo,
+ ) -> Result<(), AnyError> {
+ let node = self.resolve_node_from_info(
+ &package_req.name,
+ package_req,
+ package_info,
+ )?;
+ self.graph.set_child_parent(
+ &package_req.to_string(),
+ &node,
+ &NodeParent::Req,
+ );
+ self.try_add_pending_unresolved_node(None, &node);
+ Ok(())
+ }
+
+ fn analyze_dependency(
+ &mut self,
+ entry: &NpmDependencyEntry,
+ package_info: NpmPackageInfo,
+ parent_id: &NpmPackageId,
+ visited_versions: &Arc<VisitedVersionsPath>,
+ ) -> Result<(), AnyError> {
+ let node = self.resolve_node_from_info(
+ &entry.name,
+ match entry.kind {
+ NpmDependencyEntryKind::Dep => &entry.version_req,
+ // when resolving a peer dependency as a dependency, it should
+ // use the "dependencies" entry version requirement if it exists
+ NpmDependencyEntryKind::Peer | NpmDependencyEntryKind::OptionalPeer => {
+ entry
+ .peer_dep_version_req
+ .as_ref()
+ .unwrap_or(&entry.version_req)
+ }
+ },
+ package_info,
+ )?;
+ self.graph.set_child_parent(
+ &entry.bare_specifier,
+ &node,
+ &NodeParent::Node(parent_id.clone()),
+ );
+ self.try_add_pending_unresolved_node(Some(visited_versions), &node);
+ Ok(())
+ }
+
+ fn try_add_pending_unresolved_node(
+ &mut self,
+ maybe_previous_visited_versions: Option<&Arc<VisitedVersionsPath>>,
+ node: &Arc<Mutex<Node>>,
+ ) {
+ let node_id = node.lock().id.clone();
+ let visited_versions = match maybe_previous_visited_versions {
+ Some(previous_visited_versions) => {
+ match previous_visited_versions.with_id(&node_id) {
+ Some(visited_versions) => visited_versions,
+ None => return, // circular, don't visit this node
+ }
+ }
+ None => VisitedVersionsPath::new(&node_id),
+ };
+ self
+ .pending_unresolved_nodes
+ .push_back((visited_versions, node.clone()));
+ }
+
+ fn resolve_node_from_info(
+ &mut self,
+ name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ package_info: NpmPackageInfo,
+ ) -> Result<Arc<Mutex<Node>>, AnyError> {
+ let version_and_info = self.resolve_best_package_version_and_info(
+ name,
+ version_matcher,
+ package_info,
+ )?;
+ let id = NpmPackageId {
+ name: name.to_string(),
+ version: version_and_info.version.clone(),
+ peer_dependencies: Vec::new(),
+ };
+ debug!(
+ "Resolved {}@{} to {}",
+ name,
+ version_matcher.version_text(),
+ id.as_serialized()
+ );
+ let (created, node) = self.graph.get_or_create_for_id(&id);
+ if created {
+ let mut node = (*node).lock();
+ let mut deps = version_and_info
+ .info
+ .dependencies_as_entries()
+ .with_context(|| format!("npm package: {}", id.display()))?;
+ // Ensure name alphabetical and then version descending
+ // so these are resolved in that order
+ deps.sort();
+ node.deps = Arc::new(deps);
+ }
+
+ Ok(node)
+ }
+
+ pub async fn resolve_pending(&mut self) -> Result<(), AnyError> {
+ while !self.pending_unresolved_nodes.is_empty() {
+ // now go down through the dependencies by tree depth
+ while let Some((visited_versions, parent_node)) =
+ self.pending_unresolved_nodes.pop_front()
+ {
+ let (mut parent_id, deps) = {
+ let parent_node = parent_node.lock();
+ if parent_node.forgotten {
+ // todo(dsherret): we should try to reproduce this scenario and write a test
+ continue;
+ }
+ (parent_node.id.clone(), parent_node.deps.clone())
+ };
+
+ // cache all the dependencies' registry infos in parallel if should
+ if !should_sync_download() {
+ let handles = deps
+ .iter()
+ .map(|dep| {
+ let name = dep.name.clone();
+ let api = self.api.clone();
+ tokio::task::spawn(async move {
+ // it's ok to call this without storing the result, because
+ // NpmRegistryApi will cache the package info in memory
+ api.package_info(&name).await
+ })
+ })
+ .collect::<Vec<_>>();
+ let results = futures::future::join_all(handles).await;
+ for result in results {
+ result??; // surface the first error
+ }
+ }
+
+ // resolve the dependencies
+ for dep in deps.iter() {
+ let package_info = self.api.package_info(&dep.name).await?;
+
+ match dep.kind {
+ NpmDependencyEntryKind::Dep => {
+ self.analyze_dependency(
+ dep,
+ package_info,
+ &parent_id,
+ &visited_versions,
+ )?;
+ }
+ NpmDependencyEntryKind::Peer
+ | NpmDependencyEntryKind::OptionalPeer => {
+ let maybe_new_parent_id = self.resolve_peer_dep(
+ &dep.bare_specifier,
+ &parent_id,
+ dep,
+ package_info,
+ &visited_versions,
+ )?;
+ if let Some(new_parent_id) = maybe_new_parent_id {
+ assert_eq!(
+ (&new_parent_id.name, &new_parent_id.version),
+ (&parent_id.name, &parent_id.version)
+ );
+ parent_id = new_parent_id;
+ }
+ }
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn resolve_peer_dep(
+ &mut self,
+ specifier: &str,
+ parent_id: &NpmPackageId,
+ peer_dep: &NpmDependencyEntry,
+ peer_package_info: NpmPackageInfo,
+ visited_ancestor_versions: &Arc<VisitedVersionsPath>,
+ ) -> Result<Option<NpmPackageId>, AnyError> {
+ fn find_matching_child<'a>(
+ peer_dep: &NpmDependencyEntry,
+ children: impl Iterator<Item = &'a NpmPackageId>,
+ ) -> Option<NpmPackageId> {
+ for child_id in children {
+ if child_id.name == peer_dep.name
+ && peer_dep.version_req.satisfies(&child_id.version)
+ {
+ return Some(child_id.clone());
+ }
+ }
+ None
+ }
+
+ // Peer dependencies are resolved based on its ancestors' siblings.
+ // If not found, then it resolves based on the version requirement if non-optional.
+ let mut pending_ancestors = VecDeque::new(); // go up the tree by depth
+ let path = GraphSpecifierPath::new(specifier.to_string());
+ let visited_versions = VisitedVersionsPath::new(parent_id);
+
+ // skip over the current node
+ for (specifier, grand_parents) in
+ self.graph.borrow_node(parent_id).parents.clone()
+ {
+ let path = path.with_specifier(specifier);
+ for grand_parent in grand_parents {
+ if let Some(visited_versions) =
+ visited_versions.with_parent(&grand_parent)
+ {
+ pending_ancestors.push_back((
+ grand_parent,
+ path.clone(),
+ visited_versions,
+ ));
+ }
+ }
+ }
+
+ while let Some((ancestor, path, visited_versions)) =
+ pending_ancestors.pop_front()
+ {
+ match &ancestor {
+ NodeParent::Node(ancestor_node_id) => {
+ let maybe_peer_dep_id = if ancestor_node_id.name == peer_dep.name
+ && peer_dep.version_req.satisfies(&ancestor_node_id.version)
+ {
+ Some(ancestor_node_id.clone())
+ } else {
+ let ancestor = self.graph.borrow_node(ancestor_node_id);
+ for (specifier, parents) in &ancestor.parents {
+ let new_path = path.with_specifier(specifier.clone());
+ for parent in parents {
+ if let Some(visited_versions) =
+ visited_versions.with_parent(parent)
+ {
+ pending_ancestors.push_back((
+ parent.clone(),
+ new_path.clone(),
+ visited_versions,
+ ));
+ }
+ }
+ }
+ find_matching_child(peer_dep, ancestor.children.values())
+ };
+ if let Some(peer_dep_id) = maybe_peer_dep_id {
+ let parents =
+ self.graph.borrow_node(ancestor_node_id).parents.clone();
+ return Ok(Some(self.set_new_peer_dep(
+ parents,
+ ancestor_node_id,
+ &peer_dep_id,
+ &path,
+ visited_ancestor_versions,
+ )));
+ }
+ }
+ NodeParent::Req => {
+ // in this case, the parent is the root so the children are all the package requirements
+ if let Some(child_id) =
+ find_matching_child(peer_dep, self.graph.package_reqs.values())
+ {
+ let specifier = path.specifier.to_string();
+ let path = path.pop().unwrap(); // go back down one level from the package requirement
+ let old_id =
+ self.graph.package_reqs.get(&specifier).unwrap().clone();
+ return Ok(Some(self.set_new_peer_dep(
+ BTreeMap::from([(specifier, BTreeSet::from([NodeParent::Req]))]),
+ &old_id,
+ &child_id,
+ path,
+ visited_ancestor_versions,
+ )));
+ }
+ }
+ }
+ }
+
+ // We didn't find anything by searching the ancestor siblings, so we need
+ // to resolve based on the package info and will treat this just like any
+ // other dependency when not optional
+ if !peer_dep.kind.is_optional() {
+ self.analyze_dependency(
+ peer_dep,
+ peer_package_info,
+ parent_id,
+ visited_ancestor_versions,
+ )?;
+ }
+
+ Ok(None)
+ }
+
+ fn set_new_peer_dep(
+ &mut self,
+ previous_parents: BTreeMap<String, BTreeSet<NodeParent>>,
+ node_id: &NpmPackageId,
+ peer_dep_id: &NpmPackageId,
+ path: &Arc<GraphSpecifierPath>,
+ visited_ancestor_versions: &Arc<VisitedVersionsPath>,
+ ) -> NpmPackageId {
+ let mut peer_dep_id = Cow::Borrowed(peer_dep_id);
+ let old_id = node_id;
+ let (new_id, old_node_children) =
+ if old_id.peer_dependencies.contains(&peer_dep_id) {
+ // the parent has already resolved to using this peer dependency
+ // via some other path, so we don't need to update its ids,
+ // but instead only make a link to it
+ (
+ old_id.clone(),
+ self.graph.borrow_node(old_id).children.clone(),
+ )
+ } else {
+ let mut new_id = old_id.clone();
+ new_id.peer_dependencies.push(peer_dep_id.as_ref().clone());
+
+ // this will happen for circular dependencies
+ if *old_id == *peer_dep_id {
+ peer_dep_id = Cow::Owned(new_id.clone());
+ }
+
+ // remove the previous parents from the old node
+ let old_node_children = {
+ for (specifier, parents) in &previous_parents {
+ for parent in parents {
+ self.graph.remove_child_parent(specifier, old_id, parent);
+ }
+ }
+ let old_node = self.graph.borrow_node(old_id);
+ old_node.children.clone()
+ };
+
+ let (_, new_node) = self.graph.get_or_create_for_id(&new_id);
+
+ // update the previous parent to point to the new node
+ // and this node to point at those parents
+ for (specifier, parents) in previous_parents {
+ for parent in parents {
+ self.graph.set_child_parent(&specifier, &new_node, &parent);
+ }
+ }
+
+ // now add the previous children to this node
+ let new_id_as_parent = NodeParent::Node(new_id.clone());
+ for (specifier, child_id) in &old_node_children {
+ let child = self.graph.packages.get(child_id).unwrap().clone();
+ self
+ .graph
+ .set_child_parent(specifier, &child, &new_id_as_parent);
+ }
+ (new_id, old_node_children)
+ };
+
+ // this is the parent id found at the bottom of the path
+ let mut bottom_parent_id = new_id.clone();
+
+ // continue going down the path
+ let next_specifier = &path.specifier;
+ if let Some(path) = path.pop() {
+ let next_node_id = old_node_children.get(next_specifier).unwrap();
+ bottom_parent_id = self.set_new_peer_dep(
+ BTreeMap::from([(
+ next_specifier.to_string(),
+ BTreeSet::from([NodeParent::Node(new_id.clone())]),
+ )]),
+ next_node_id,
+ &peer_dep_id,
+ path,
+ visited_ancestor_versions,
+ );
+ } else {
+ // this means we're at the peer dependency now
+ debug!(
+ "Resolved peer dependency for {} in {} to {}",
+ next_specifier,
+ &new_id.as_serialized(),
+ &peer_dep_id.as_serialized(),
+ );
+ assert!(!old_node_children.contains_key(next_specifier));
+ let node = self.graph.get_or_create_for_id(&peer_dep_id).1;
+ self.try_add_pending_unresolved_node(
+ Some(visited_ancestor_versions),
+ &node,
+ );
+ self
+ .graph
+ .set_child_parent_node(next_specifier, &node, &new_id);
+ }
+
+ // forget the old node at this point if it has no parents
+ if new_id != *old_id {
+ let old_node = self.graph.borrow_node(old_id);
+ if old_node.parents.is_empty() {
+ drop(old_node); // stop borrowing
+ self.graph.forget_orphan(old_id);
+ }
+ }
+
+ bottom_parent_id
+ }
+}
+
+#[derive(Clone)]
+struct VersionAndInfo {
+ version: NpmVersion,
+ info: NpmPackageVersionInfo,
+}
+
+fn get_resolved_package_version_and_info(
+ pkg_name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ info: NpmPackageInfo,
+ parent: Option<&NpmPackageId>,
+) -> Result<VersionAndInfo, AnyError> {
+ let mut maybe_best_version: Option<VersionAndInfo> = None;
+ if let Some(tag) = version_matcher.tag() {
+ // For when someone just specifies @types/node, we want to pull in a
+ // "known good" version of @types/node that works well with Deno and
+ // not necessarily the latest version. For example, we might only be
+ // compatible with Node vX, but then Node vY is published so we wouldn't
+ // want to pull that in.
+ // Note: If the user doesn't want this behavior, then they can specify an
+ // explicit version.
+ if tag == "latest" && pkg_name == "@types/node" {
+ return get_resolved_package_version_and_info(
+ pkg_name,
+ &NpmVersionReq::parse("18.0.0 - 18.8.2").unwrap(),
+ info,
+ parent,
+ );
+ }
+
+ if let Some(version) = info.dist_tags.get(tag) {
+ match info.versions.get(version) {
+ Some(info) => {
+ return Ok(VersionAndInfo {
+ version: NpmVersion::parse(version)?,
+ info: info.clone(),
+ });
+ }
+ None => {
+ bail!(
+ "Could not find version '{}' referenced in dist-tag '{}'.",
+ version,
+ tag,
+ )
+ }
+ }
+ } else {
+ bail!("Could not find dist-tag '{}'.", tag)
+ }
+ } else {
+ for (_, version_info) in info.versions.into_iter() {
+ let version = NpmVersion::parse(&version_info.version)?;
+ if version_matcher.matches(&version) {
+ let is_best_version = maybe_best_version
+ .as_ref()
+ .map(|best_version| best_version.version.cmp(&version).is_lt())
+ .unwrap_or(true);
+ if is_best_version {
+ maybe_best_version = Some(VersionAndInfo {
+ version,
+ info: version_info,
+ });
+ }
+ }
+ }
+ }
+
+ match maybe_best_version {
+ Some(v) => Ok(v),
+ // If the package isn't found, it likely means that the user needs to use
+ // `--reload` to get the latest npm package information. Although it seems
+ // like we could make this smart by fetching the latest information for
+ // this package here, we really need a full restart. There could be very
+ // interesting bugs that occur if this package's version was resolved by
+ // something previous using the old information, then now being smart here
+ // causes a new fetch of the package information, meaning this time the
+ // previous resolution of this package's version resolved to an older
+ // version, but next time to a different version because it has new information.
+ None => bail!(
+ concat!(
+ "Could not find npm package '{}' matching {}{}. ",
+ "Try retrieving the latest npm package information by running with --reload",
+ ),
+ pkg_name,
+ version_matcher.version_text(),
+ match parent {
+ Some(id) => format!(" as specified in {}", id.display()),
+ None => String::new(),
+ }
+ ),
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use pretty_assertions::assert_eq;
+
+ use crate::npm::registry::TestNpmRegistryApi;
+ use crate::npm::NpmPackageReference;
+
+ use super::*;
+
+ #[test]
+ fn test_get_resolved_package_version_and_info() {
+ // dist tag where version doesn't exist
+ let package_ref = NpmPackageReference::from_str("npm:test").unwrap();
+ let result = get_resolved_package_version_and_info(
+ "test",
+ &package_ref.req,
+ NpmPackageInfo {
+ name: "test".to_string(),
+ versions: HashMap::new(),
+ dist_tags: HashMap::from([(
+ "latest".to_string(),
+ "1.0.0-alpha".to_string(),
+ )]),
+ },
+ None,
+ );
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "Could not find version '1.0.0-alpha' referenced in dist-tag 'latest'."
+ );
+
+ // dist tag where version is a pre-release
+ let package_ref = NpmPackageReference::from_str("npm:test").unwrap();
+ let result = get_resolved_package_version_and_info(
+ "test",
+ &package_ref.req,
+ NpmPackageInfo {
+ name: "test".to_string(),
+ versions: HashMap::from([
+ ("0.1.0".to_string(), NpmPackageVersionInfo::default()),
+ (
+ "1.0.0-alpha".to_string(),
+ NpmPackageVersionInfo {
+ version: "0.1.0-alpha".to_string(),
+ ..Default::default()
+ },
+ ),
+ ]),
+ dist_tags: HashMap::from([(
+ "latest".to_string(),
+ "1.0.0-alpha".to_string(),
+ )]),
+ },
+ None,
+ );
+ assert_eq!(result.unwrap().version.to_string(), "1.0.0-alpha");
+ }
+
+ #[tokio::test]
+ async fn resolve_deps_no_peer() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "0.1.0");
+ api.ensure_package_version("package-c", "0.0.10");
+ api.ensure_package_version("package-d", "3.2.1");
+ api.ensure_package_version("package-d", "3.2.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^0.1"));
+ api.add_dependency(("package-c", "0.1.0"), ("package-d", "*"));
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-a@1.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized("package-c@0.1.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-c@0.1.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-d".to_string(),
+ NpmPackageId::from_serialized("package-d@3.2.1").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-d@3.2.1").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_deps_circular() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "*"));
+ api.add_dependency(("package-b", "2.0.0"), ("package-a", "1"));
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-a@1.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-b".to_string(),
+ NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-a".to_string(),
+ NpmPackageId::from_serialized("package-a@1.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_peer_deps_top_tree() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "4.1.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4"));
+ api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*"));
+
+ let (packages, package_reqs) = run_resolver_and_get_output(
+ api,
+ // the peer dependency is specified here at the top of the tree
+ // so it should resolve to 4.0.0 instead of 4.1.0
+ vec!["npm:package-a@1", "npm:package-peer@4.0.0"],
+ )
+ .await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![
+ (
+ "package-a@1".to_string(),
+ "package-a@1.0.0_package-peer@4.0.0".to_string()
+ ),
+ (
+ "package-peer@4.0.0".to_string(),
+ "package-peer@4.0.0".to_string()
+ )
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_peer_deps_ancestor_sibling_not_top_tree() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-0", "1.1.1");
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "4.1.0");
+ api.add_dependency(("package-0", "1.1.1"), ("package-a", "1"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ // the peer dependency is specified here as a sibling of "a" and "b"
+ // so it should resolve to 4.0.0 instead of 4.1.0
+ api.add_dependency(("package-a", "1.0.0"), ("package-peer", "4.0.0"));
+ api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4"));
+ api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*"));
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-0@1.1.1"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-0@1.1.1").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-a".to_string(),
+ NpmPackageId::from_serialized("package-a@1.0.0_package-peer@4.0.0")
+ .unwrap(),
+ ),]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-0@1.1.1".to_string(), "package-0@1.1.1".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_peer_deps_auto_resolved() {
+ // in this case, the peer dependency is not found in the tree
+ // so it's auto-resolved based on the registry
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "4.1.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4"));
+ api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*"));
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-a@1.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized("package-c@3.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.1.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-c@3.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.1.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@4.1.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_optional_peer_dep_not_resolved() {
+ // in this case, the peer dependency is not found in the tree
+ // so it's auto-resolved based on the registry
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "4.1.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ api.add_optional_peer_dependency(
+ ("package-b", "2.0.0"),
+ ("package-peer", "4"),
+ );
+ api.add_optional_peer_dependency(
+ ("package-c", "3.0.0"),
+ ("package-peer", "*"),
+ );
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-a@1.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized("package-c@3.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-b@2.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::new(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-c@3.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::new(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_optional_peer_found() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "4.1.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ api.add_optional_peer_dependency(
+ ("package-b", "2.0.0"),
+ ("package-peer", "4"),
+ );
+ api.add_optional_peer_dependency(
+ ("package-c", "3.0.0"),
+ ("package-peer", "*"),
+ );
+
+ let (packages, package_reqs) = run_resolver_and_get_output(
+ api,
+ vec!["npm:package-a@1", "npm:package-peer@4.0.0"],
+ )
+ .await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![
+ (
+ "package-a@1".to_string(),
+ "package-a@1.0.0_package-peer@4.0.0".to_string()
+ ),
+ (
+ "package-peer@4.0.0".to_string(),
+ "package-peer@4.0.0".to_string()
+ )
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_nested_peer_deps_auto_resolved() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-0", "1.0.0");
+ api.ensure_package_version("package-peer-a", "2.0.0");
+ api.ensure_package_version("package-peer-b", "3.0.0");
+ api.add_peer_dependency(("package-0", "1.0.0"), ("package-peer-a", "2"));
+ api.add_peer_dependency(
+ ("package-peer-a", "2.0.0"),
+ ("package-peer-b", "3"),
+ );
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-0@1.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-peer-a".to_string(),
+ NpmPackageId::from_serialized("package-peer-a@2.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-a@2.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-peer-b".to_string(),
+ NpmPackageId::from_serialized("package-peer-b@3.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-b@3.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::new(),
+ dist: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string())]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_nested_peer_deps_ancestor_sibling_deps() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-0", "1.0.0");
+ api.ensure_package_version("package-peer-a", "2.0.0");
+ api.ensure_package_version("package-peer-b", "3.0.0");
+ api.add_dependency(("package-0", "1.0.0"), ("package-peer-b", "*"));
+ api.add_peer_dependency(("package-0", "1.0.0"), ("package-peer-a", "2"));
+ api.add_peer_dependency(
+ ("package-peer-a", "2.0.0"),
+ ("package-peer-b", "3"),
+ );
+
+ let (packages, package_reqs) = run_resolver_and_get_output(
+ api,
+ vec![
+ "npm:package-0@1.0",
+ "npm:package-peer-a@2",
+ "npm:package-peer-b@3",
+ ],
+ )
+ .await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-0@1.0.0_package-peer-a@2.0.0_package-peer-b@3.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-peer-a".to_string(),
+ NpmPackageId::from_serialized(
+ "package-peer-a@2.0.0_package-peer-b@3.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-peer-b".to_string(),
+ NpmPackageId::from_serialized("package-peer-b@3.0.0").unwrap(),
+ )
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-peer-a@2.0.0_package-peer-b@3.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-peer-b".to_string(),
+ NpmPackageId::from_serialized("package-peer-b@3.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-b@3.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::new(),
+ dist: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![
+ (
+ "package-0@1.0".to_string(),
+ "package-0@1.0.0_package-peer-a@2.0.0_package-peer-b@3.0.0"
+ .to_string()
+ ),
+ (
+ "package-peer-a@2".to_string(),
+ "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string()
+ ),
+ (
+ "package-peer-b@3".to_string(),
+ "package-peer-b@3.0.0".to_string()
+ )
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_with_peer_deps_multiple() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-0", "1.1.1");
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-c", "3.0.0");
+ api.ensure_package_version("package-d", "3.5.0");
+ api.ensure_package_version("package-e", "3.6.0");
+ api.ensure_package_version("package-peer-a", "4.0.0");
+ api.ensure_package_version("package-peer-a", "4.1.0");
+ api.ensure_package_version("package-peer-b", "5.3.0");
+ api.ensure_package_version("package-peer-b", "5.4.1");
+ api.ensure_package_version("package-peer-c", "6.2.0");
+ api.add_dependency(("package-0", "1.1.1"), ("package-a", "1"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-d", "^3"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-peer-a", "4.0.0"));
+ api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer-a", "4"));
+ api.add_peer_dependency(
+ ("package-b", "2.0.0"),
+ ("package-peer-c", "=6.2.0"),
+ );
+ api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer-a", "*"));
+ api.add_peer_dependency(
+ ("package-peer-a", "4.0.0"),
+ ("package-peer-b", "^5.4"), // will be auto-resolved
+ );
+
+ let (packages, package_reqs) = run_resolver_and_get_output(
+ api,
+ vec!["npm:package-0@1.1.1", "npm:package-e@3"],
+ )
+ .await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-0@1.1.1").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-a".to_string(),
+ NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ ),]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-b".to_string(),
+ NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-c".to_string(),
+ NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-d".to_string(),
+ NpmPackageId::from_serialized("package-d@3.5.0").unwrap(),
+ ),
+ (
+ "package-peer-a".to_string(),
+ NpmPackageId::from_serialized("package-peer-a@4.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([
+ (
+ "package-peer-a".to_string(),
+ NpmPackageId::from_serialized("package-peer-a@4.0.0").unwrap(),
+ ),
+ (
+ "package-peer-c".to_string(),
+ NpmPackageId::from_serialized("package-peer-c@6.2.0").unwrap(),
+ )
+ ])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-c@3.0.0_package-peer-a@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer-a".to_string(),
+ NpmPackageId::from_serialized("package-peer-a@4.0.0").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-d@3.5.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-e@3.6.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-a@4.0.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: HashMap::from([(
+ "package-peer-b".to_string(),
+ NpmPackageId::from_serialized("package-peer-b@5.4.1").unwrap(),
+ )])
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-b@5.4.1").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer-c@6.2.0").unwrap(),
+ copy_index: 0,
+ dist: Default::default(),
+ dependencies: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![
+ ("package-0@1.1.1".to_string(), "package-0@1.1.1".to_string()),
+ ("package-e@3".to_string(), "package-e@3.6.0".to_string()),
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_peer_deps_circular() {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-b", "*"));
+ api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "1"));
+
+ let (packages, package_reqs) =
+ run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-a@1.0.0_package-a@1.0.0")
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-b".to_string(),
+ NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-a@1.0.0__package-a@1.0.0"
+ )
+ .unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-a@1.0.0__package-a@1.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-a".to_string(),
+ NpmPackageId::from_serialized("package-a@1.0.0_package-a@1.0.0")
+ .unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![(
+ "package-a@1.0".to_string(),
+ "package-a@1.0.0_package-a@1.0.0".to_string()
+ )]
+ );
+ }
+
+ #[tokio::test]
+ async fn resolve_peer_deps_multiple_copies() {
+ // repeat this a few times to have a higher probability of surfacing indeterminism
+ for _ in 0..3 {
+ let api = TestNpmRegistryApi::default();
+ api.ensure_package_version("package-a", "1.0.0");
+ api.ensure_package_version("package-b", "2.0.0");
+ api.ensure_package_version("package-dep", "3.0.0");
+ api.ensure_package_version("package-peer", "4.0.0");
+ api.ensure_package_version("package-peer", "5.0.0");
+ api.add_dependency(("package-a", "1.0.0"), ("package-dep", "*"));
+ api.add_dependency(("package-a", "1.0.0"), ("package-peer", "4"));
+ api.add_dependency(("package-b", "2.0.0"), ("package-dep", "*"));
+ api.add_dependency(("package-b", "2.0.0"), ("package-peer", "5"));
+ api.add_peer_dependency(("package-dep", "3.0.0"), ("package-peer", "*"));
+
+ let (packages, package_reqs) = run_resolver_and_get_output(
+ api,
+ vec!["npm:package-a@1", "npm:package-b@2"],
+ )
+ .await;
+ assert_eq!(
+ packages,
+ vec![
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-a@1.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-dep".to_string(),
+ NpmPackageId::from_serialized(
+ "package-dep@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-b@2.0.0_package-peer@5.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([
+ (
+ "package-dep".to_string(),
+ NpmPackageId::from_serialized(
+ "package-dep@3.0.0_package-peer@5.0.0"
+ )
+ .unwrap(),
+ ),
+ (
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@5.0.0").unwrap(),
+ ),
+ ]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-dep@3.0.0_package-peer@4.0.0"
+ )
+ .unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized(
+ "package-dep@3.0.0_package-peer@5.0.0"
+ )
+ .unwrap(),
+ copy_index: 1,
+ dependencies: HashMap::from([(
+ "package-peer".to_string(),
+ NpmPackageId::from_serialized("package-peer@5.0.0").unwrap(),
+ )]),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@4.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::new(),
+ dist: Default::default(),
+ },
+ NpmResolutionPackage {
+ id: NpmPackageId::from_serialized("package-peer@5.0.0").unwrap(),
+ copy_index: 0,
+ dependencies: HashMap::new(),
+ dist: Default::default(),
+ },
+ ]
+ );
+ assert_eq!(
+ package_reqs,
+ vec![
+ (
+ "package-a@1".to_string(),
+ "package-a@1.0.0_package-peer@4.0.0".to_string()
+ ),
+ (
+ "package-b@2".to_string(),
+ "package-b@2.0.0_package-peer@5.0.0".to_string()
+ )
+ ]
+ );
+ }
+ }
+
+ async fn run_resolver_and_get_output(
+ api: TestNpmRegistryApi,
+ reqs: Vec<&str>,
+ ) -> (Vec<NpmResolutionPackage>, Vec<(String, String)>) {
+ let mut graph = Graph::default();
+ let mut resolver = GraphDependencyResolver::new(&mut graph, &api);
+
+ for req in reqs {
+ let req = NpmPackageReference::from_str(req).unwrap().req;
+ resolver
+ .add_package_req(&req, api.package_info(&req.name).await.unwrap())
+ .unwrap();
+ }
+
+ resolver.resolve_pending().await.unwrap();
+ let snapshot = graph.into_snapshot(&api).await.unwrap();
+ let mut packages = snapshot.all_packages();
+ packages.sort_by(|a, b| a.id.cmp(&b.id));
+ let mut package_reqs = snapshot
+ .package_reqs
+ .into_iter()
+ .map(|(a, b)| (a.to_string(), b.as_serialized()))
+ .collect::<Vec<_>>();
+ package_reqs.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string()));
+ (packages, package_reqs)
+ }
+}