1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 23:34:47 -05:00
denoland-deno/cli/npm/resolution/graph.rs
David Sherret 23e1ba7e2d
fix(npm): improve peer dependency resolution with circular dependencies (#18069)
This improves peer dependency resolution yet again. We did not handle
scenarios like the following:

```
// a -> b -> c -> d -> c -> b (peer)
```

...which would maybe work ok the first time its run in some cases, but
then lead to a lockfile that would error on load.

This now keeps track of circular dependencies and updates nodes
accordingly. That said, there is still a lurking bug in this code
somewhere that I've added a comment for (there is a mitigation on the
tail end that seems to work well). The current state is much better than
before and I can look into it later. I think it's something small that's
incorrect.
2023-03-08 12:22:08 -05:00

3726 lines
120 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_graph::npm::NpmPackageNv;
use deno_graph::npm::NpmPackageReq;
use deno_graph::semver::Version;
use deno_graph::semver::VersionReq;
use log::debug;
use crate::npm::registry::NpmDependencyEntry;
use crate::npm::registry::NpmDependencyEntryKind;
use crate::npm::registry::NpmPackageInfo;
use crate::npm::registry::NpmPackageVersionInfo;
use crate::npm::resolution::common::resolve_best_package_version_and_info;
use crate::npm::resolution::snapshot::SnapshotPackageCopyIndexResolver;
use crate::npm::NpmRegistryApi;
use super::common::version_req_satisfies;
use super::common::LATEST_VERSION_REQ;
use super::snapshot::NpmResolutionSnapshot;
use super::NpmPackageId;
use super::NpmResolutionPackage;
// todo(dsherret): for perf we should use an arena/bump allocator for
// creating the nodes and paths since this is done in a phase
/// A unique identifier to a node in the graph.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
struct NodeId(u32);
/// A resolved package in the resolution graph.
#[derive(Debug)]
struct Node {
/// The specifier to child relationship in the graph. The specifier is
/// the key in an npm package's dependencies map (ex. "express"). We
/// use a BTreeMap for some determinism when creating the snapshot.
///
/// Note: We don't want to store the children as a `NodeRef` because
/// multiple paths might visit through the children and we don't want
/// to share those references with those paths.
pub children: BTreeMap<String, NodeId>,
/// Whether the node has demonstrated to have no peer dependencies in its
/// descendants. If this is true then we can skip analyzing this node
/// again when we encounter it another time in the dependency tree, which
/// is much faster.
pub no_peers: bool,
}
#[derive(Clone)]
enum ResolvedIdPeerDep {
/// This is a reference to the parent instead of the child because we only have a
/// node reference to the parent, since we've traversed it, but the child node may
/// change from under it.
ParentReference {
parent: GraphPathNodeOrRoot,
child_pkg_nv: Arc<NpmPackageNv>,
},
/// A node that was created during snapshotting and is not being used in any path.
SnapshotNodeId(NodeId),
}
impl ResolvedIdPeerDep {
pub fn current_state_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.current_state_hash_with_hasher(&mut hasher);
hasher.finish()
}
pub fn current_state_hash_with_hasher(&self, hasher: &mut DefaultHasher) {
match self {
ResolvedIdPeerDep::ParentReference {
parent,
child_pkg_nv,
} => {
match parent {
GraphPathNodeOrRoot::Root(root) => root.hash(hasher),
GraphPathNodeOrRoot::Node(node) => node.node_id().hash(hasher),
}
child_pkg_nv.hash(hasher);
}
ResolvedIdPeerDep::SnapshotNodeId(node_id) => {
node_id.hash(hasher);
}
}
}
}
/// A pending resolved identifier used in the graph. At the end of resolution, these
/// will become fully resolved to an `NpmPackageId`.
#[derive(Clone)]
struct ResolvedId {
nv: Arc<NpmPackageNv>,
peer_dependencies: Vec<ResolvedIdPeerDep>,
}
impl ResolvedId {
/// Gets a hash of the resolved identifier at this current moment in time.
///
/// WARNING: A resolved identifier references a value that could change in
/// the future, so this should be used with that in mind.
pub fn current_state_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.nv.hash(&mut hasher);
for dep in &self.peer_dependencies {
dep.current_state_hash_with_hasher(&mut hasher);
}
hasher.finish()
}
pub fn push_peer_dep(&mut self, peer_dep: ResolvedIdPeerDep) -> bool {
let new_hash = peer_dep.current_state_hash();
for dep in &self.peer_dependencies {
if new_hash == dep.current_state_hash() {
return false; // peer dep already set
}
}
self.peer_dependencies.push(peer_dep);
true
}
}
/// Mappings of node identifiers to resolved identifiers. Each node has exactly
/// one resolved identifier.
///
/// The mapping from resolved to node_ids is imprecise and will do a best attempt
/// at sharing nodes.
#[derive(Default)]
struct ResolvedNodeIds {
node_to_resolved_id: HashMap<NodeId, (ResolvedId, u64)>,
resolved_to_node_id: HashMap<u64, NodeId>,
}
impl ResolvedNodeIds {
pub fn set(&mut self, node_id: NodeId, resolved_id: ResolvedId) {
let resolved_id_hash = resolved_id.current_state_hash();
if let Some((_, old_resolved_id_key)) = self
.node_to_resolved_id
.insert(node_id, (resolved_id, resolved_id_hash))
{
// ensure the old resolved id key is removed as it might be stale
self.resolved_to_node_id.remove(&old_resolved_id_key);
}
self.resolved_to_node_id.insert(resolved_id_hash, node_id);
}
pub fn get(&self, node_id: NodeId) -> Option<&ResolvedId> {
self.node_to_resolved_id.get(&node_id).map(|(id, _)| id)
}
pub fn get_node_id(&self, resolved_id: &ResolvedId) -> Option<NodeId> {
self
.resolved_to_node_id
.get(&resolved_id.current_state_hash())
.copied()
}
}
// todo(dsherret): for some reason the lsp errors when using an Rc<RefCell<NodeId>> here
// instead of an Arc<Mutex<NodeId>>. We should investigate and fix.
/// A pointer to a specific node in a graph path. The underlying node id
/// may change as peer dependencies are created.
#[derive(Clone, Debug)]
struct NodeIdRef(Arc<Mutex<NodeId>>);
impl NodeIdRef {
pub fn new(node_id: NodeId) -> Self {
NodeIdRef(Arc::new(Mutex::new(node_id)))
}
pub fn change(&self, node_id: NodeId) {
*self.0.lock() = node_id;
}
pub fn get(&self) -> NodeId {
*self.0.lock()
}
}
#[derive(Clone)]
enum GraphPathNodeOrRoot {
Node(Arc<GraphPath>),
Root(Arc<NpmPackageNv>),
}
/// Path through the graph that represents a traversal through the graph doing
/// the dependency resolution. The graph tries to share duplicate package
/// information and we try to avoid traversing parts of the graph that we know
/// are resolved.
struct GraphPath {
previous_node: Option<GraphPathNodeOrRoot>,
node_id_ref: NodeIdRef,
specifier: String,
// we could consider not storing this here and instead reference the resolved
// nodes, but we should performance profile this code first
nv: Arc<NpmPackageNv>,
/// Descendants in the path that circularly link to an ancestor in a child.These
/// descendants should be kept up to date and always point to this node.
linked_circular_descendants: Mutex<Vec<Arc<GraphPath>>>,
}
impl GraphPath {
pub fn for_root(node_id: NodeId, nv: Arc<NpmPackageNv>) -> Arc<Self> {
Arc::new(Self {
previous_node: Some(GraphPathNodeOrRoot::Root(nv.clone())),
node_id_ref: NodeIdRef::new(node_id),
// use an empty specifier
specifier: "".to_string(),
nv,
linked_circular_descendants: Default::default(),
})
}
pub fn node_id(&self) -> NodeId {
self.node_id_ref.get()
}
pub fn specifier(&self) -> &str {
&self.specifier
}
pub fn change_id(&self, node_id: NodeId) {
self.node_id_ref.change(node_id)
}
pub fn with_id(
self: &Arc<GraphPath>,
node_id: NodeId,
specifier: String,
nv: Arc<NpmPackageNv>,
) -> Arc<Self> {
Arc::new(Self {
previous_node: Some(GraphPathNodeOrRoot::Node(self.clone())),
node_id_ref: NodeIdRef::new(node_id),
specifier,
nv,
linked_circular_descendants: Default::default(),
})
}
/// Gets if there is an ancestor with the same name & version along this path.
pub fn find_ancestor(&self, nv: &NpmPackageNv) -> Option<Arc<GraphPath>> {
let mut maybe_next_node = self.previous_node.as_ref();
while let Some(GraphPathNodeOrRoot::Node(next_node)) = maybe_next_node {
// we've visited this before, so stop
if *next_node.nv == *nv {
return Some(next_node.clone());
}
maybe_next_node = next_node.previous_node.as_ref();
}
None
}
/// Gets the bottom-up path to the ancestor not including the current or ancestor node.
pub fn get_path_to_ancestor_exclusive(
&self,
ancestor_node_id: NodeId,
) -> Vec<&Arc<GraphPath>> {
let mut path = Vec::new();
let mut maybe_next_node = self.previous_node.as_ref();
while let Some(GraphPathNodeOrRoot::Node(next_node)) = maybe_next_node {
if next_node.node_id() == ancestor_node_id {
break;
}
path.push(next_node);
maybe_next_node = next_node.previous_node.as_ref();
}
debug_assert!(maybe_next_node.is_some());
path
}
pub fn ancestors(&self) -> GraphPathAncestorIterator {
GraphPathAncestorIterator {
next: self.previous_node.as_ref(),
}
}
}
struct GraphPathAncestorIterator<'a> {
next: Option<&'a GraphPathNodeOrRoot>,
}
impl<'a> Iterator for GraphPathAncestorIterator<'a> {
type Item = &'a GraphPathNodeOrRoot;
fn next(&mut self) -> Option<Self::Item> {
if let Some(next) = self.next.take() {
if let GraphPathNodeOrRoot::Node(node) = next {
self.next = node.previous_node.as_ref();
}
Some(next)
} else {
None
}
}
}
#[derive(Default)]
pub struct Graph {
/// Each requirement is mapped to a specific name and version.
package_reqs: HashMap<NpmPackageReq, Arc<NpmPackageNv>>,
/// Then each name and version is mapped to an exact node id.
/// Note: Uses a BTreeMap in order to create some determinism
/// when creating the snapshot.
root_packages: BTreeMap<Arc<NpmPackageNv>, NodeId>,
package_name_versions: HashMap<String, HashSet<Version>>,
nodes: HashMap<NodeId, Node>,
resolved_node_ids: ResolvedNodeIds,
// This will be set when creating from a snapshot, then
// inform the final snapshot creation.
packages_to_copy_index: HashMap<NpmPackageId, usize>,
/// Packages that the resolver should resolve first.
pending_unresolved_packages: Vec<Arc<NpmPackageNv>>,
}
impl Graph {
pub fn from_snapshot(
snapshot: NpmResolutionSnapshot,
) -> Result<Self, AnyError> {
fn get_or_create_graph_node(
graph: &mut Graph,
pkg_id: &NpmPackageId,
packages: &HashMap<NpmPackageId, NpmResolutionPackage>,
created_package_ids: &mut HashMap<NpmPackageId, NodeId>,
) -> Result<NodeId, AnyError> {
if let Some(id) = created_package_ids.get(pkg_id) {
return Ok(*id);
}
let node_id = graph.create_node(&pkg_id.nv);
created_package_ids.insert(pkg_id.clone(), node_id);
let peer_dep_ids = pkg_id
.peer_dependencies
.iter()
.map(|peer_dep| {
Ok(ResolvedIdPeerDep::SnapshotNodeId(get_or_create_graph_node(
graph,
peer_dep,
packages,
created_package_ids,
)?))
})
.collect::<Result<Vec<_>, AnyError>>()?;
let graph_resolved_id = ResolvedId {
nv: Arc::new(pkg_id.nv.clone()),
peer_dependencies: peer_dep_ids,
};
graph.resolved_node_ids.set(node_id, graph_resolved_id);
let resolution = match packages.get(pkg_id) {
Some(resolved_id) => resolved_id,
// maybe the user messed around with the lockfile
None => bail!("not found package: {}", pkg_id.as_serialized()),
};
for (name, child_id) in &resolution.dependencies {
let child_node_id = get_or_create_graph_node(
graph,
child_id,
packages,
created_package_ids,
)?;
graph.set_child_of_parent_node(node_id, name, child_node_id);
}
Ok(node_id)
}
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(),
package_reqs: snapshot
.package_reqs
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
pending_unresolved_packages: snapshot
.pending_unresolved_packages
.into_iter()
.map(Arc::new)
.collect(),
..Default::default()
};
let mut created_package_ids =
HashMap::with_capacity(snapshot.packages.len());
for (id, resolved_id) in snapshot.root_packages {
let node_id = get_or_create_graph_node(
&mut graph,
&resolved_id,
&snapshot.packages,
&mut created_package_ids,
)?;
graph.root_packages.insert(Arc::new(id), node_id);
}
Ok(graph)
}
pub fn take_pending_unresolved(&mut self) -> Vec<Arc<NpmPackageNv>> {
std::mem::take(&mut self.pending_unresolved_packages)
}
pub fn has_package_req(&self, req: &NpmPackageReq) -> bool {
self.package_reqs.contains_key(req)
}
pub fn has_root_package(&self, id: &NpmPackageNv) -> bool {
self.root_packages.contains_key(id)
}
fn get_npm_pkg_id(&self, node_id: NodeId) -> NpmPackageId {
let resolved_id = self.resolved_node_ids.get(node_id).unwrap();
self.get_npm_pkg_id_from_resolved_id(resolved_id, HashSet::new())
}
fn get_npm_pkg_id_from_resolved_id(
&self,
resolved_id: &ResolvedId,
seen: HashSet<NodeId>,
) -> NpmPackageId {
if resolved_id.peer_dependencies.is_empty() {
NpmPackageId {
nv: (*resolved_id.nv).clone(),
peer_dependencies: Vec::new(),
}
} else {
let mut npm_pkg_id = NpmPackageId {
nv: (*resolved_id.nv).clone(),
peer_dependencies: Vec::with_capacity(
resolved_id.peer_dependencies.len(),
),
};
let mut seen_children_resolved_ids =
HashSet::with_capacity(resolved_id.peer_dependencies.len());
for peer_dep in &resolved_id.peer_dependencies {
let maybe_node_and_resolved_id = match peer_dep {
ResolvedIdPeerDep::SnapshotNodeId(node_id) => self
.resolved_node_ids
.get(*node_id)
.map(|resolved_id| (*node_id, resolved_id)),
ResolvedIdPeerDep::ParentReference {
parent,
child_pkg_nv: child_nv,
} => match &parent {
GraphPathNodeOrRoot::Root(_) => {
self.root_packages.get(child_nv).and_then(|node_id| {
self
.resolved_node_ids
.get(*node_id)
.map(|resolved_id| (*node_id, resolved_id))
})
}
GraphPathNodeOrRoot::Node(parent_path) => {
self.nodes.get(&parent_path.node_id()).and_then(|parent| {
parent
.children
.values()
.filter_map(|child_id| {
let child_id = *child_id;
self
.resolved_node_ids
.get(child_id)
.map(|resolved_id| (child_id, resolved_id))
})
.find(|(_, resolved_id)| resolved_id.nv == *child_nv)
})
}
},
};
// this should always be set
debug_assert!(maybe_node_and_resolved_id.is_some());
if let Some((child_id, child_resolved_id)) = maybe_node_and_resolved_id
{
let mut new_seen = seen.clone();
if new_seen.insert(child_id) {
let child_peer = self.get_npm_pkg_id_from_resolved_id(
child_resolved_id,
new_seen.clone(),
);
if seen_children_resolved_ids.insert(child_peer.clone()) {
npm_pkg_id.peer_dependencies.push(child_peer);
}
}
}
}
npm_pkg_id
}
}
fn get_or_create_for_id(
&mut self,
resolved_id: &ResolvedId,
) -> (bool, NodeId) {
if let Some(node_id) = self.resolved_node_ids.get_node_id(resolved_id) {
return (false, node_id);
}
let node_id = self.create_node(&resolved_id.nv);
self.resolved_node_ids.set(node_id, resolved_id.clone());
(true, node_id)
}
fn create_node(&mut self, pkg_nv: &NpmPackageNv) -> NodeId {
let node_id = NodeId(self.nodes.len() as u32);
let node = Node {
children: Default::default(),
no_peers: false,
};
self
.package_name_versions
.entry(pkg_nv.name.clone())
.or_default()
.insert(pkg_nv.version.clone());
self.nodes.insert(node_id, node);
node_id
}
fn borrow_node_mut(&mut self, node_id: NodeId) -> &mut Node {
self.nodes.get_mut(&node_id).unwrap()
}
fn set_child_of_parent_node(
&mut self,
parent_id: NodeId,
specifier: &str,
child_id: NodeId,
) {
assert_ne!(child_id, parent_id);
let parent = self.borrow_node_mut(parent_id);
parent.children.insert(specifier.to_string(), child_id);
}
pub async fn into_snapshot(
self,
api: &NpmRegistryApi,
) -> Result<NpmResolutionSnapshot, AnyError> {
let packages_to_pkg_ids = self
.nodes
.keys()
.map(|node_id| (*node_id, self.get_npm_pkg_id(*node_id)))
.collect::<HashMap<_, _>>();
let mut copy_index_resolver =
SnapshotPackageCopyIndexResolver::from_map_with_capacity(
self.packages_to_copy_index,
self.nodes.len(),
);
let mut packages = HashMap::with_capacity(self.nodes.len());
let mut packages_by_name: HashMap<String, Vec<_>> =
HashMap::with_capacity(self.nodes.len());
// todo(dsherret): there is a lurking bug within the peer dependencies code.
// You can see it by using `NodeIds` instead of `NpmPackageIds` on this travered_ids
// hashset, which will cause the bottom of the "tree" nodes to be populated in
// the result instead of the top of the "tree". I think there's maybe one small
// thing that's not being updated properly.
let mut traversed_ids = HashSet::with_capacity(self.nodes.len());
let mut pending = VecDeque::new();
for root_id in self.root_packages.values().copied() {
let pkg_id = packages_to_pkg_ids.get(&root_id).unwrap();
if traversed_ids.insert(pkg_id.clone()) {
pending.push_back((root_id, pkg_id));
}
}
while let Some((node_id, pkg_id)) = pending.pop_front() {
let node = self.nodes.get(&node_id).unwrap();
packages_by_name
.entry(pkg_id.nv.name.clone())
.or_default()
.push(pkg_id.clone());
// todo(dsherret): grab this from the dep entry cache, which should have it
let dist = api
.package_version_info(&pkg_id.nv)
.await?
.unwrap_or_else(|| panic!("missing: {:?}", pkg_id.nv))
.dist;
let mut dependencies = HashMap::with_capacity(node.children.len());
for (specifier, child_id) in &node.children {
let child_id = *child_id;
let child_pkg_id = packages_to_pkg_ids.get(&child_id).unwrap();
if traversed_ids.insert(child_pkg_id.clone()) {
pending.push_back((child_id, child_pkg_id));
}
dependencies.insert(specifier.clone(), (*child_pkg_id).clone());
}
packages.insert(
(*pkg_id).clone(),
NpmResolutionPackage {
copy_index: copy_index_resolver.resolve(pkg_id),
pkg_id: (*pkg_id).clone(),
dist,
dependencies,
},
);
}
Ok(NpmResolutionSnapshot {
root_packages: self
.root_packages
.into_iter()
.map(|(nv, node_id)| {
(
(*nv).clone(),
packages_to_pkg_ids.get(&node_id).unwrap().clone(),
)
})
.collect(),
packages_by_name: packages_by_name
.into_iter()
.map(|(name, mut ids)| {
ids.sort();
ids.dedup();
(name, ids)
})
.collect(),
packages,
package_reqs: self
.package_reqs
.into_iter()
.map(|(req, nv)| (req, (*nv).clone()))
.collect(),
pending_unresolved_packages: self
.pending_unresolved_packages
.into_iter()
.map(|nv| (*nv).clone())
.collect(),
})
}
// Debugging methods
#[cfg(debug_assertions)]
#[allow(unused)]
fn output_path(&self, path: &Arc<GraphPath>) {
eprintln!("-----------");
self.output_node(path.node_id(), false);
for path in path.ancestors() {
match path {
GraphPathNodeOrRoot::Node(node) => {
self.output_node(node.node_id(), false)
}
GraphPathNodeOrRoot::Root(pkg_id) => {
let node_id = self.root_packages.get(pkg_id).unwrap();
eprintln!(
"Root: {} ({}: {})",
pkg_id,
node_id.0,
self.get_npm_pkg_id(*node_id).as_serialized()
)
}
}
}
eprintln!("-----------");
}
#[cfg(debug_assertions)]
#[allow(unused)]
fn output_node(&self, node_id: NodeId, show_children: bool) {
eprintln!(
"{:>4}: {}",
node_id.0,
self.get_npm_pkg_id(node_id).as_serialized()
);
if show_children {
let node = self.nodes.get(&node_id).unwrap();
eprintln!(" Children:");
for (specifier, child_id) in &node.children {
eprintln!(" {}: {}", specifier, child_id.0);
}
}
}
#[cfg(debug_assertions)]
#[allow(unused)]
pub fn output_nodes(&self) {
eprintln!("~~~");
let mut node_ids = self
.resolved_node_ids
.node_to_resolved_id
.keys()
.copied()
.collect::<Vec<_>>();
node_ids.sort_by(|a, b| a.0.cmp(&b.0));
for node_id in node_ids {
self.output_node(node_id, true);
}
eprintln!("~~~");
}
}
#[derive(Default)]
struct DepEntryCache(HashMap<Arc<NpmPackageNv>, Arc<Vec<NpmDependencyEntry>>>);
impl DepEntryCache {
pub fn store(
&mut self,
nv: Arc<NpmPackageNv>,
version_info: &NpmPackageVersionInfo,
) -> Result<Arc<Vec<NpmDependencyEntry>>, AnyError> {
debug_assert!(!self.0.contains_key(&nv)); // we should not be re-inserting
let mut deps = version_info
.dependencies_as_entries()
.with_context(|| format!("npm package: {nv}"))?;
// Ensure name alphabetical and then version descending
// so these are resolved in that order
deps.sort();
let deps = Arc::new(deps);
self.0.insert(nv, deps.clone());
Ok(deps)
}
pub fn get(
&self,
id: &NpmPackageNv,
) -> Option<&Arc<Vec<NpmDependencyEntry>>> {
self.0.get(id)
}
}
struct UnresolvedOptionalPeer {
specifier: String,
graph_path: Arc<GraphPath>,
}
pub struct GraphDependencyResolver<'a> {
graph: &'a mut Graph,
api: &'a NpmRegistryApi,
pending_unresolved_nodes: VecDeque<Arc<GraphPath>>,
unresolved_optional_peers:
HashMap<Arc<NpmPackageNv>, Vec<UnresolvedOptionalPeer>>,
dep_entry_cache: DepEntryCache,
}
impl<'a> GraphDependencyResolver<'a> {
pub fn new(graph: &'a mut Graph, api: &'a NpmRegistryApi) -> Self {
Self {
graph,
api,
pending_unresolved_nodes: Default::default(),
unresolved_optional_peers: Default::default(),
dep_entry_cache: Default::default(),
}
}
pub fn add_root_package(
&mut self,
package_nv: &NpmPackageNv,
package_info: &NpmPackageInfo,
) -> Result<(), AnyError> {
if self.graph.root_packages.contains_key(package_nv) {
return Ok(()); // already added
}
// todo(dsherret): using a version requirement here is a temporary hack
// to reuse code in a large refactor. We should resolve the node directly
// from the package name and version
let version_req =
VersionReq::parse_from_specifier(&format!("{}", package_nv.version))
.unwrap();
let (pkg_nv, node_id) = self.resolve_node_from_info(
&package_nv.name,
&version_req,
package_info,
None,
)?;
self.graph.root_packages.insert(pkg_nv.clone(), node_id);
self
.pending_unresolved_nodes
.push_back(GraphPath::for_root(node_id, pkg_nv));
Ok(())
}
pub fn add_package_req(
&mut self,
package_req: &NpmPackageReq,
package_info: &NpmPackageInfo,
) -> Result<(), AnyError> {
if self.graph.package_reqs.contains_key(package_req) {
return Ok(()); // already added
}
let (pkg_id, node_id) = self.resolve_node_from_info(
&package_req.name,
package_req
.version_req
.as_ref()
.unwrap_or(&*LATEST_VERSION_REQ),
package_info,
None,
)?;
self
.graph
.package_reqs
.insert(package_req.clone(), pkg_id.clone());
self.graph.root_packages.insert(pkg_id.clone(), node_id);
self
.pending_unresolved_nodes
.push_back(GraphPath::for_root(node_id, pkg_id));
Ok(())
}
fn analyze_dependency(
&mut self,
entry: &NpmDependencyEntry,
package_info: &NpmPackageInfo,
parent_path: &Arc<GraphPath>,
) -> Result<NodeId, AnyError> {
debug_assert_eq!(entry.kind, NpmDependencyEntryKind::Dep);
let parent_id = parent_path.node_id();
let (child_nv, mut child_id) = self.resolve_node_from_info(
&entry.name,
&entry.version_req,
package_info,
Some(parent_id),
)?;
// Some packages may resolves to themselves as a dependency. If this occurs,
// just ignore adding these as dependencies because this is likely a mistake
// in the package.
if child_id != parent_id {
let maybe_ancestor = parent_path.find_ancestor(&child_nv);
if let Some(ancestor) = &maybe_ancestor {
child_id = ancestor.node_id();
}
let new_path = parent_path.with_id(
child_id,
entry.bare_specifier.to_string(),
child_nv,
);
if let Some(ancestor) = maybe_ancestor {
// this node is circular, so we link it to the ancestor
self.add_linked_circular_descendant(&ancestor, new_path);
} else {
self.graph.set_child_of_parent_node(
parent_id,
&entry.bare_specifier,
child_id,
);
self.pending_unresolved_nodes.push_back(new_path);
}
}
Ok(child_id)
}
fn resolve_node_from_info(
&mut self,
pkg_req_name: &str,
version_req: &VersionReq,
package_info: &NpmPackageInfo,
parent_id: Option<NodeId>,
) -> Result<(Arc<NpmPackageNv>, NodeId), AnyError> {
let version_and_info = resolve_best_package_version_and_info(
version_req,
package_info,
self
.graph
.package_name_versions
.entry(package_info.name.clone())
.or_default()
.iter(),
)?;
let resolved_id = ResolvedId {
nv: Arc::new(NpmPackageNv {
name: package_info.name.to_string(),
version: version_and_info.version.clone(),
}),
peer_dependencies: Vec::new(),
};
let (_, node_id) = self.graph.get_or_create_for_id(&resolved_id);
let pkg_nv = resolved_id.nv;
let has_deps = if let Some(deps) = self.dep_entry_cache.get(&pkg_nv) {
!deps.is_empty()
} else {
let deps = self
.dep_entry_cache
.store(pkg_nv.clone(), version_and_info.info)?;
!deps.is_empty()
};
if !has_deps {
// ensure this is set if not, as it's an optimization
let mut node = self.graph.borrow_node_mut(node_id);
node.no_peers = true;
}
debug!(
"{} - Resolved {}@{} to {}",
match parent_id {
Some(parent_id) => self.graph.get_npm_pkg_id(parent_id).as_serialized(),
None => "<package-req>".to_string(),
},
pkg_req_name,
version_req.version_text(),
pkg_nv.to_string(),
);
Ok((pkg_nv, node_id))
}
pub async fn resolve_pending(&mut self) -> Result<(), AnyError> {
// go down through the dependencies by tree depth
while let Some(parent_path) = self.pending_unresolved_nodes.pop_front() {
let (parent_nv, child_deps) = {
let node_id = parent_path.node_id();
if self.graph.nodes.get(&node_id).unwrap().no_peers {
// We can skip as there's no reason to analyze this graph segment further.
continue;
}
let pkg_nv = self
.graph
.resolved_node_ids
.get(node_id)
.unwrap()
.nv
.clone();
let deps = if let Some(deps) = self.dep_entry_cache.get(&pkg_nv) {
deps.clone()
} else {
// the api should have this in the cache at this point, so no need to parallelize
match self.api.package_version_info(&pkg_nv).await? {
Some(version_info) => {
self.dep_entry_cache.store(pkg_nv.clone(), &version_info)?
}
None => {
bail!("Could not find version information for {}", pkg_nv)
}
}
};
(pkg_nv, deps)
};
// cache all the dependencies' registry infos in parallel if should
self
.api
.cache_in_parallel({
child_deps.iter().map(|dep| dep.name.clone()).collect()
})
.await?;
// resolve the dependencies
let mut found_peer = false;
for dep in child_deps.iter() {
let package_info = self.api.package_info(&dep.name).await?;
match dep.kind {
NpmDependencyEntryKind::Dep => {
let parent_id = parent_path.node_id();
let node = self.graph.nodes.get(&parent_id).unwrap();
let child_id = match node.children.get(&dep.bare_specifier) {
Some(child_id) => {
// this dependency was previously analyzed by another path
// so we don't attempt to resolve the version again
let child_id = *child_id;
let child_nv = self
.graph
.resolved_node_ids
.get(child_id)
.unwrap()
.nv
.clone();
let maybe_ancestor = parent_path.find_ancestor(&child_nv);
let child_path = parent_path.with_id(
child_id,
dep.bare_specifier.clone(),
child_nv,
);
if let Some(ancestor) = maybe_ancestor {
// when the nv appears as an ancestor, use that node
// and mark this as circular
self.add_linked_circular_descendant(&ancestor, child_path);
} else {
// mark the child as pending
self.pending_unresolved_nodes.push_back(child_path);
}
child_id
}
None => {
self.analyze_dependency(dep, &package_info, &parent_path)?
}
};
if !found_peer {
found_peer = !self.graph.borrow_node_mut(child_id).no_peers;
}
}
NpmDependencyEntryKind::Peer
| NpmDependencyEntryKind::OptionalPeer => {
found_peer = true;
// we need to re-evaluate peer dependencies every time and can't
// skip over them because they might be evaluated differently based
// on the current path
let maybe_new_id = self.resolve_peer_dep(
&dep.bare_specifier,
dep,
&package_info,
&parent_path,
)?;
// For optional dependencies, we want to resolve them if any future
// same parent version resolves them. So when not resolved, store them to be
// potentially resolved later.
//
// Note: This is not a good solution, but will probably work ok in most
// scenarios. We can work on improving this in the future. We probably
// want to resolve future optional peers to the same dependency for example.
if dep.kind == NpmDependencyEntryKind::OptionalPeer {
match maybe_new_id {
Some(new_id) => {
if let Some(unresolved_optional_peers) =
self.unresolved_optional_peers.remove(&parent_nv)
{
for optional_peer in unresolved_optional_peers {
let peer_parent = GraphPathNodeOrRoot::Node(
optional_peer.graph_path.clone(),
);
self.set_new_peer_dep(
&[&optional_peer.graph_path],
peer_parent,
&optional_peer.specifier,
new_id,
);
}
}
}
None => {
// store this for later if it's resolved for this version
self
.unresolved_optional_peers
.entry(parent_nv.clone())
.or_default()
.push(UnresolvedOptionalPeer {
specifier: dep.bare_specifier.clone(),
graph_path: parent_path.clone(),
});
}
}
}
}
}
}
if !found_peer {
self.graph.borrow_node_mut(parent_path.node_id()).no_peers = true;
}
}
Ok(())
}
fn resolve_peer_dep(
&mut self,
specifier: &str,
peer_dep: &NpmDependencyEntry,
peer_package_info: &NpmPackageInfo,
ancestor_path: &Arc<GraphPath>,
) -> Result<Option<NodeId>, AnyError> {
debug_assert!(matches!(
peer_dep.kind,
NpmDependencyEntryKind::Peer | NpmDependencyEntryKind::OptionalPeer
));
let mut path = vec![ancestor_path];
// the current dependency might have had the peer dependency
// in another bare specifier slot... if so resolve it to that
{
let maybe_peer_dep = self.find_peer_dep_in_node(
ancestor_path,
peer_dep,
peer_package_info,
)?;
if let Some((peer_parent, peer_dep_id)) = maybe_peer_dep {
// this will always have an ancestor because we're not at the root
self.set_new_peer_dep(&path, peer_parent, specifier, peer_dep_id);
return Ok(Some(peer_dep_id));
}
}
// Peer dependencies are resolved based on its ancestors' siblings.
// If not found, then it resolves based on the version requirement if non-optional.
for ancestor_node in ancestor_path.ancestors() {
match ancestor_node {
GraphPathNodeOrRoot::Node(ancestor_graph_path_node) => {
path.push(ancestor_graph_path_node);
let maybe_peer_dep = self.find_peer_dep_in_node(
ancestor_graph_path_node,
peer_dep,
peer_package_info,
)?;
if let Some((parent, peer_dep_id)) = maybe_peer_dep {
// this will always have an ancestor because we're not at the root
self.set_new_peer_dep(&path, parent, specifier, peer_dep_id);
return Ok(Some(peer_dep_id));
}
}
GraphPathNodeOrRoot::Root(root_pkg_id) => {
// 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,
peer_package_info,
self.graph.root_packages.iter().map(|(nv, id)| (*id, nv)),
)? {
let peer_parent = GraphPathNodeOrRoot::Root(root_pkg_id.clone());
self.set_new_peer_dep(&path, peer_parent, specifier, child_id);
return Ok(Some(child_id));
}
}
}
}
// We didn't find anything by searching the ancestor siblings, so we need
// to resolve based on the package info
if !peer_dep.kind.is_optional() {
let parent_id = ancestor_path.node_id();
let (_, node_id) = self.resolve_node_from_info(
&peer_dep.name,
peer_dep
.peer_dep_version_req
.as_ref()
.unwrap_or(&peer_dep.version_req),
peer_package_info,
Some(parent_id),
)?;
let peer_parent = GraphPathNodeOrRoot::Node(ancestor_path.clone());
self.set_new_peer_dep(&[ancestor_path], peer_parent, specifier, node_id);
Ok(Some(node_id))
} else {
Ok(None)
}
}
fn find_peer_dep_in_node(
&self,
path: &Arc<GraphPath>,
peer_dep: &NpmDependencyEntry,
peer_package_info: &NpmPackageInfo,
) -> Result<Option<(GraphPathNodeOrRoot, NodeId)>, AnyError> {
let node_id = path.node_id();
let resolved_node_id = self.graph.resolved_node_ids.get(node_id).unwrap();
// check if this node itself is a match for
// the peer dependency and if so use that
if resolved_node_id.nv.name == peer_dep.name
&& version_req_satisfies(
&peer_dep.version_req,
&resolved_node_id.nv.version,
peer_package_info,
None,
)?
{
let parent = path.previous_node.as_ref().unwrap().clone();
Ok(Some((parent, node_id)))
} else {
let node = self.graph.nodes.get(&node_id).unwrap();
let children = node.children.values().map(|child_node_id| {
let child_node_id = *child_node_id;
(
child_node_id,
&self.graph.resolved_node_ids.get(child_node_id).unwrap().nv,
)
});
find_matching_child(peer_dep, peer_package_info, children).map(
|maybe_child_id| {
maybe_child_id.map(|child_id| {
let parent = GraphPathNodeOrRoot::Node(path.clone());
(parent, child_id)
})
},
)
}
}
fn add_peer_deps_to_path(
&mut self,
// path from the node above the resolved dep to just above the peer dep
path: &[&Arc<GraphPath>],
peer_deps: &[(&ResolvedIdPeerDep, Arc<NpmPackageNv>)],
) {
debug_assert!(!path.is_empty());
for graph_path_node in path.iter().rev() {
let old_node_id = graph_path_node.node_id();
let old_resolved_id = self
.graph
.resolved_node_ids
.get(old_node_id)
.unwrap()
.clone();
let mut new_resolved_id = old_resolved_id;
let mut has_changed = false;
for (peer_dep, nv) in peer_deps {
if *nv == new_resolved_id.nv {
continue;
}
if new_resolved_id.push_peer_dep((*peer_dep).clone()) {
has_changed = true;
}
}
if !has_changed {
continue; // nothing to change
}
let (created, new_node_id) =
self.graph.get_or_create_for_id(&new_resolved_id);
if created {
let old_children =
self.graph.borrow_node_mut(old_node_id).children.clone();
// copy over the old children to this new one
for (specifier, child_id) in &old_children {
self.graph.set_child_of_parent_node(
new_node_id,
specifier,
*child_id,
);
}
}
graph_path_node.change_id(new_node_id);
let circular_descendants =
graph_path_node.linked_circular_descendants.lock().clone();
for descendant in circular_descendants {
let path = descendant.get_path_to_ancestor_exclusive(new_node_id);
self.add_peer_deps_to_path(&path, peer_deps);
descendant.change_id(new_node_id);
// update the bottom node to point to this new node id
let bottom_node_id = path[0].node_id();
self.graph.set_child_of_parent_node(
bottom_node_id,
descendant.specifier(),
descendant.node_id(),
);
}
// update the previous parent to have this as its child
match graph_path_node.previous_node.as_ref().unwrap() {
GraphPathNodeOrRoot::Root(pkg_id) => {
self.graph.root_packages.insert(pkg_id.clone(), new_node_id);
}
GraphPathNodeOrRoot::Node(parent_node_path) => {
let parent_node_id = parent_node_path.node_id();
let parent_node = self.graph.borrow_node_mut(parent_node_id);
parent_node
.children
.insert(graph_path_node.specifier().to_string(), new_node_id);
}
}
}
}
fn set_new_peer_dep(
&mut self,
// path from the node above the resolved dep to just above the peer dep
path: &[&Arc<GraphPath>],
peer_dep_parent: GraphPathNodeOrRoot,
peer_dep_specifier: &str,
peer_dep_id: NodeId,
) {
debug_assert!(!path.is_empty());
let peer_dep_nv = self
.graph
.resolved_node_ids
.get(peer_dep_id)
.unwrap()
.nv
.clone();
let peer_dep = ResolvedIdPeerDep::ParentReference {
parent: peer_dep_parent,
child_pkg_nv: peer_dep_nv.clone(),
};
let top_node = path.last().unwrap();
let (maybe_circular_ancestor, path) = if top_node.nv == peer_dep_nv {
// it's circular, so exclude the top node
(Some(top_node), &path[0..path.len() - 1])
} else {
(None, path)
};
self.add_peer_deps_to_path(path, &[(&peer_dep, peer_dep_nv.clone())]);
// now set the peer dependency
let bottom_node = path.first().unwrap();
self.graph.set_child_of_parent_node(
bottom_node.node_id(),
peer_dep_specifier,
peer_dep_id,
);
// queue next step
let new_path = bottom_node.with_id(
peer_dep_id,
peer_dep_specifier.to_string(),
peer_dep_nv,
);
if let Some(ancestor_node) = maybe_circular_ancestor {
// it's circular, so link this in step with the ancestor node
ancestor_node
.linked_circular_descendants
.lock()
.push(new_path);
} else {
// mark the peer dep as needing to be analyzed
self.pending_unresolved_nodes.push_back(new_path);
}
debug!(
"Resolved peer dependency for {} in {} to {}",
peer_dep_specifier,
&self
.graph
.get_npm_pkg_id(bottom_node.node_id())
.as_serialized(),
&self.graph.get_npm_pkg_id(peer_dep_id).as_serialized(),
);
}
fn add_linked_circular_descendant(
&mut self,
ancestor: &Arc<GraphPath>,
descendant: Arc<GraphPath>,
) {
let ancestor_node_id = ancestor.node_id();
let path = descendant.get_path_to_ancestor_exclusive(ancestor_node_id);
let ancestor_resolved_id = self
.graph
.resolved_node_ids
.get(ancestor_node_id)
.unwrap()
.clone();
let peer_deps = ancestor_resolved_id
.peer_dependencies
.iter()
.map(|peer_dep| {
(
peer_dep,
match &peer_dep {
ResolvedIdPeerDep::ParentReference { child_pkg_nv, .. } => {
child_pkg_nv.clone()
}
ResolvedIdPeerDep::SnapshotNodeId(node_id) => self
.graph
.resolved_node_ids
.get(*node_id)
.unwrap()
.nv
.clone(),
},
)
})
.collect::<Vec<_>>();
if !peer_deps.is_empty() {
self.add_peer_deps_to_path(&path, &peer_deps);
}
let bottom_node_id = path[0].node_id();
self.graph.set_child_of_parent_node(
bottom_node_id,
descendant.specifier(),
descendant.node_id(),
);
ancestor.linked_circular_descendants.lock().push(descendant);
}
}
fn find_matching_child<'a>(
peer_dep: &NpmDependencyEntry,
peer_package_info: &NpmPackageInfo,
children: impl Iterator<Item = (NodeId, &'a Arc<NpmPackageNv>)>,
) -> Result<Option<NodeId>, AnyError> {
for (child_id, pkg_id) in children {
if pkg_id.name == peer_dep.name
&& version_req_satisfies(
&peer_dep.version_req,
&pkg_id.version,
peer_package_info,
None,
)?
{
return Ok(Some(child_id));
}
}
Ok(None)
}
#[cfg(test)]
mod test {
use deno_graph::npm::NpmPackageReqReference;
use pretty_assertions::assert_eq;
use crate::npm::registry::TestNpmRegistryApiInner;
use super::*;
#[test]
fn resolved_id_tests() {
let mut ids = ResolvedNodeIds::default();
let node_id = NodeId(0);
let resolved_id = ResolvedId {
nv: Arc::new(NpmPackageNv::from_str("package@1.1.1").unwrap()),
peer_dependencies: Vec::new(),
};
ids.set(node_id, resolved_id.clone());
assert!(ids.get(node_id).is_some());
assert!(ids.get(NodeId(1)).is_none());
assert_eq!(ids.get_node_id(&resolved_id), Some(node_id));
let resolved_id_new = ResolvedId {
nv: Arc::new(NpmPackageNv::from_str("package@1.1.2").unwrap()),
peer_dependencies: Vec::new(),
};
ids.set(node_id, resolved_id_new.clone());
assert_eq!(ids.get_node_id(&resolved_id), None); // stale entry should have been removed
assert!(ids.get(node_id).is_some());
assert_eq!(ids.get_node_id(&resolved_id_new), Some(node_id));
}
#[tokio::test]
async fn resolve_deps_no_peer() {
let api = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-b".to_string(), "package-b@2.0.0".to_string(),),
("package-c".to_string(), "package-c@0.1.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-c@0.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-d".to_string(),
"package-d@3.2.1".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-d@3.2.1".to_string(),
copy_index: 0,
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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@2.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn peer_deps_simple_top_tree() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1.0", "npm:package-peer@1.0"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
}
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1.0".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string()
),
(
"package-peer@1.0".to_string(),
"package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn peer_deps_simple_root_pkg_children() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-0", "1.0.0");
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_dependency(("package-0", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-0", "1.0.0"), ("package-peer", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-a".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@1.0.0".to_string(),)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
}
]
);
assert_eq!(
package_reqs,
vec![(
"package-0@1.0".to_string(),
"package-0@1.0.0_package-peer@1.0.0".to_string()
),]
);
}
#[tokio::test]
async fn peer_deps_simple_deeper() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-0", "1.0.0");
api.ensure_package_version("package-1", "1.0.0");
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_dependency(("package-0", "1.0.0"), ("package-1", "1"));
api.add_dependency(("package-1", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-1", "1.0.0"), ("package-peer", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-1".to_string(),
"package-1@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-1@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-a".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@1.0.0".to_string(),)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: 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_with_peer_deps_top_tree() {
let api = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@2.0.0_package-peer@4.0.0".to_string(),
),
(
"package-c".to_string(),
"package-c@3.0.0_package-peer@4.0.0".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-peer@4.0.0".to_string(),
copy_index: 0,
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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-0@1.1.1".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-peer@4.0.0".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@2.0.0_package-peer@4.0.0".to_string(),
),
(
"package-c".to_string(),
"package-c@3.0.0_package-peer@4.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@4.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-peer@4.0.0".to_string(),
copy_index: 0,
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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@2.0.0_package-peer@4.1.0".to_string(),
),
(
"package-c".to_string(),
"package-c@3.0.0_package-peer@4.1.0".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer@4.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.1.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0_package-peer@4.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.1.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-peer@4.1.0".to_string(),
copy_index: 0,
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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-b".to_string(), "package-b@2.0.0".to_string(),),
("package-c".to_string(), "package-c@3.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0".to_string(),
copy_index: 0,
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_found() {
let api = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@2.0.0_package-peer@4.0.0".to_string(),
),
(
"package-c".to_string(),
"package-c@3.0.0_package-peer@4.0.0".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-peer@4.0.0".to_string(),
copy_index: 0,
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_optional_peer_first_not_resolved_second_resolved_scenario1()
{
// When resolving a dependency a second time and it has an optional
// peer dependency that wasn't previously resolved, it should resolve all the
// previous versions to the new one
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "^1"));
api.add_dependency(("package-a", "1.0.0"), ("package-peer", "^1"));
api.add_optional_peer_dependency(
("package-b", "1.0.0"),
("package-peer", "*"),
);
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1", "npm:package-b@1"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@1.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string()
),
(
"package-b@1".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_optional_peer_first_not_resolved_second_resolved_scenario2()
{
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "2.0.0");
api.add_optional_peer_dependency(
("package-a", "1.0.0"),
("package-peer", "*"),
);
api.add_dependency(("package-b", "1.0.0"), ("package-a", "1.0.0"));
api.add_dependency(("package-b", "1.0.0"), ("package-peer", "2.0.0"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1", "npm:package-b@1"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@2.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-a".to_string(),
"package-a@1.0.0_package-peer@2.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@2.0.0".to_string(),)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1".to_string(),
"package-a@1.0.0_package-peer@2.0.0".to_string()
),
(
"package-b@1".to_string(),
"package-b@1.0.0_package-peer@2.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_optional_dep_npm_req_top() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_optional_peer_dependency(
("package-a", "1.0.0"),
("package-peer", "*"),
);
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1", "npm:package-peer@1"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string()
),
(
"package-peer@1".to_string(),
"package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_optional_dep_different_resolution_second_time() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.ensure_package_version("package-peer", "2.0.0");
api.add_optional_peer_dependency(
("package-a", "1.0.0"),
("package-peer", "*"),
);
api.add_dependency(("package-b", "1.0.0"), ("package-a", "1.0.0"));
api.add_dependency(("package-b", "1.0.0"), ("package-peer", "2.0.0"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec![
"npm:package-a@1",
"npm:package-b@1",
"npm:package-peer@1.0.0",
],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(),
copy_index: 1,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@2.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-peer".to_string(), "package-peer@2.0.0".to_string(),),
(
"package-a".to_string(),
"package-a@1.0.0_package-peer@2.0.0".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string()
),
(
"package-b@1".to_string(),
"package-b@1.0.0_package-peer@2.0.0".to_string()
),
(
"package-peer@1.0.0".to_string(),
"package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_peer_dep_other_specifier_slot() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-peer", "2.0.0");
// bit of an edge case... probably nobody has ever done this
api.add_dependency(
("package-a", "1.0.0"),
("package-peer2", "npm:package-peer@2"),
);
api.add_peer_dependency(("package-a", "1.0.0"), ("package-peer", "2"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-peer".to_string(), "package-peer@2.0.0".to_string(),),
(
"package-peer2".to_string(),
"package-peer@2.0.0".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
]
);
assert_eq!(
package_reqs,
vec![(
"package-a@1".to_string(),
"package-a@1.0.0_package-peer@2.0.0".to_string()
),]
);
}
#[tokio::test]
async fn resolve_nested_peer_deps_auto_resolved() {
let api = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0"
.to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer-a".to_string(),
"package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer-b".to_string(),
"package-peer-b@3.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-b@3.0.0".to_string(),
copy_index: 0,
dependencies: 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()
)]
);
}
#[tokio::test]
async fn resolve_nested_peer_deps_ancestor_sibling_deps() {
let api = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0_package-peer-b@3.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-peer-a".to_string(),
"package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(),
),
(
"package-peer-b".to_string(),
"package-peer-b@3.0.0".to_string(),
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer-b".to_string(),
"package-peer-b@3.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-b@3.0.0".to_string(),
copy_index: 0,
dependencies: 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_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 = TestNpmRegistryApiInner::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"), // will be auto-resolved
);
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![
TestNpmResolutionPackage {
pkg_id: "package-0@1.1.1".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@2.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1_package-peer-c@6.2.0".to_string(),
),
(
"package-c".to_string(),
"package-c@3.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(),
),
(
"package-d".to_string(),
"package-d@3.5.0".to_string(),
),
(
"package-peer-a".to_string(),
"package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(),
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1_package-peer-c@6.2.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-peer-a".to_string(),
"package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(),
),
(
"package-peer-c".to_string(),
"package-peer-c@6.2.0".to_string(),
)
])
},
TestNpmResolutionPackage {
pkg_id: "package-c@3.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer-a".to_string(),
"package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-d@3.5.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([]),
},
TestNpmResolutionPackage {
pkg_id: "package-e@3.6.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer-b".to_string(),
"package-peer-b@5.4.1".to_string(),
)])
},
TestNpmResolutionPackage {
pkg_id: "package-peer-b@5.4.1".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-peer-c@6.2.0".to_string(),
copy_index: 0,
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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@2.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0".to_string(), "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 = TestNpmRegistryApiInner::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![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-dep".to_string(),
"package-dep@3.0.0_package-peer@4.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@4.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-peer@5.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-dep".to_string(),
"package-dep@3.0.0_package-peer@5.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@5.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-dep@3.0.0_package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@4.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-dep@3.0.0_package-peer@5.0.0".to_string(),
copy_index: 1,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@5.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@4.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@5.0.0".to_string(),
copy_index: 0,
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-b@2".to_string(),
"package-b@2.0.0_package-peer@5.0.0".to_string()
)
]
);
}
}
#[tokio::test]
async fn resolve_dep_with_peer_deps_dep_then_peer() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-peer", "1"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1.0", "npm:package-b@1.0"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-b@1.0.0__package-peer@1.0.0"
.to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0__package-peer@1.0.0".to_string(),
),
("package-peer".to_string(), "package-peer@1.0.0".to_string(),)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-b@1.0.0__package-peer@1.0.0"
.to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([]),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1.0".to_string(),
"package-a@1.0.0_package-b@1.0.0__package-peer@1.0.0".to_string()
),
(
"package-b@1.0".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_dep_with_peer_deps_then_other_dep_with_different_peer() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-peer", "1.1.0");
api.ensure_package_version("package-peer", "1.2.0");
api.add_peer_dependency(("package-a", "1.0.0"), ("package-peer", "*")); // should select 1.2.0, then 1.1.0
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-peer", "=1.1.0"));
api.add_dependency(("package-c", "1.0.0"), ("package-a", "1"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-a@1.0", "npm:package-b@1.0"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.1.0".to_string(),
copy_index: 1,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.1.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.2.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.2.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-c".to_string(),
"package-c@1.0.0_package-peer@1.1.0".to_string(),
),
("package-peer".to_string(), "package-peer@1.1.0".to_string(),)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-peer@1.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-peer@1.1.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.1.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.2.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([]),
},
]
);
assert_eq!(
package_reqs,
vec![
(
"package-a@1.0".to_string(),
"package-a@1.0.0_package-peer@1.2.0".to_string()
),
(
"package-b@1.0".to_string(),
"package-b@1.0.0_package-peer@1.1.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_dep_and_peer_dist_tag() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "2.0.0");
api.ensure_package_version("package-b", "3.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-d", "1.0.0");
api.ensure_package_version("package-e", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "some-tag"));
api.add_dependency(("package-a", "1.0.0"), ("package-d", "1.0.0"));
api.add_dependency(("package-a", "1.0.0"), ("package-c", "1.0.0"));
api.add_dependency(("package-a", "1.0.0"), ("package-e", "1.0.0"));
api.add_dependency(("package-e", "1.0.0"), ("package-b", "some-tag"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-d", "other-tag"));
api.add_dist_tag("package-b", "some-tag", "2.0.0");
api.add_dist_tag("package-d", "other-tag", "1.0.0");
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-d@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-b".to_string(), "package-b@2.0.0".to_string(),),
(
"package-c".to_string(),
"package-c@1.0.0_package-d@1.0.0".to_string(),
),
("package-d".to_string(), "package-d@1.0.0".to_string(),),
("package-e".to_string(), "package-e@1.0.0".to_string(),),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-d@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-d".to_string(),
"package-d@1.0.0".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-d@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-e@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@2.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![(
"package-a@1.0".to_string(),
"package-a@1.0.0_package-d@1.0.0".to_string()
),]
);
}
#[tokio::test]
async fn package_has_self_as_dependency() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.add_dependency(("package-a", "1.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![TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
// in this case, we just ignore that the package did this
dependencies: Default::default(),
}]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn package_has_self_but_different_version_as_dependency() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-a", "0.5.0");
api.add_dependency(("package-a", "1.0.0"), ("package-a", "^0.5"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@0.5.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@0.5.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn grand_child_package_has_self_as_peer_dependency_root() {
let api = TestNpmRegistryApiInner::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", "2"));
api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "*"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@2.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
}
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn grand_child_package_has_self_as_peer_dependency_under_root() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-0", "1.0.0");
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "2.0.0");
api.add_dependency(("package-0", "1.0.0"), ("package-a", "*"));
api.add_dependency(("package-a", "1.0.0"), ("package-b", "2"));
api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "*"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@2.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
}
]
);
assert_eq!(
package_reqs,
vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string())]
);
}
#[tokio::test]
async fn resolve_peer_deps_in_ancestor_root() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-a", "1"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn resolve_peer_deps_in_ancestor_non_root() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-b@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn nested_deps_same_peer_dep_ancestor() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-0", "1.0.0");
api.ensure_package_version("package-1", "1.0.0");
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-d", "1.0.0");
api.add_dependency(("package-0", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-0", "1.0.0"), ("package-1", "1"));
api.add_dependency(("package-1", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-c", "1.0.0"), ("package-d", "1"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-a", "*"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-a", "*"));
api.add_peer_dependency(("package-d", "1.0.0"), ("package-a", "*"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-0", "*"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-0", "*"));
api.add_peer_dependency(("package-d", "1.0.0"), ("package-0", "*"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-0@1.0.0".to_string(),
), (
"package-1".to_string(),
"package-1@1.0.0_package-0@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-1@1.0.0_package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-0@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-0".to_string(),
"package-0@1.0.0".to_string(),
),
(
"package-a".to_string(),
"package-a@1.0.0_package-0@1.0.0".to_string(),
),
(
"package-c".to_string(),
"package-c@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-0".to_string(),
"package-0@1.0.0".to_string(),
),
(
"package-a".to_string(),
"package-a@1.0.0_package-0@1.0.0".to_string(),
),
(
"package-d".to_string(),
"package-d@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-d@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-0".to_string(),
"package-0@1.0.0".to_string(),
),
(
"package-a".to_string(),
"package-a@1.0.0_package-0@1.0.0".to_string(),
)
]),
}
]
);
assert_eq!(
package_reqs,
vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string())]
);
}
#[tokio::test]
async fn peer_dep_resolved_then_resolved_deeper() {
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-0", "1.0.0");
api.ensure_package_version("package-1", "1.0.0");
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-peer", "1.0.0");
api.add_dependency(("package-0", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-0", "1.0.0"), ("package-1", "1"));
api.add_dependency(("package-1", "1.0.0"), ("package-a", "1"));
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*"));
let (packages, package_reqs) = run_resolver_and_get_output(
api,
vec!["npm:package-0@1.0", "npm:package-peer@1.0"],
)
.await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-0@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-1".to_string(),
"package-1@1.0.0_package-peer@1.0.0".to_string(),
),
(
"package-a".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string(),
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-1@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-peer".to_string(),
"package-peer@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-peer@1.0.0".to_string(),
copy_index: 0,
dependencies: Default::default(),
}
]
);
assert_eq!(
package_reqs,
vec![
(
"package-0@1.0".to_string(),
"package-0@1.0.0_package-peer@1.0.0".to_string()
),
(
"package-peer@1.0".to_string(),
"package-peer@1.0.0".to_string()
)
]
);
}
#[tokio::test]
async fn resolve_dep_with_peer_deps_circular_1() {
// a -> b -> c -> d -> c where c has a peer dependency on b
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-d", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-c", "1.0.0"), ("package-d", "1"));
api.add_dependency(("package-d", "1.0.0"), ("package-c", "1"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-b@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
("package-b".to_string(), "package-b@1.0.0".to_string(),),
(
"package-d".to_string(),
"package-d@1.0.0_package-b@1.0.0".to_string(),
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-d@1.0.0_package-b@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn resolve_dep_with_peer_deps_circular_2() {
// a -> b -> c -> d -> c where c has a peer dependency on b
// -> e -> f -> d -> c where f has a peer dep on a
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-d", "1.0.0");
api.ensure_package_version("package-e", "1.0.0");
api.ensure_package_version("package-f", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-c", "1.0.0"), ("package-d", "1"));
api.add_dependency(("package-c", "1.0.0"), ("package-e", "1"));
api.add_dependency(("package-d", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-e", "1.0.0"), ("package-f", "1"));
api.add_dependency(("package-f", "1.0.0"), ("package-d", "1"));
api.add_peer_dependency(("package-f", "1.0.0"), ("package-a", "1"));
api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-b".to_string(),
"package-b@1.0.0_package-a@1.0.0".to_string(),
),
(
"package-d".to_string(),
"package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
),
(
"package-e".to_string(),
"package-e@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string()
)
]),
},
TestNpmResolutionPackage {
pkg_id: "package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-e@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-f".to_string(),
"package-f@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-f@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string(),
), (
"package-d".to_string(),
"package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(),
)]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[tokio::test]
async fn resolve_dep_with_peer_deps_circular_3() {
// a -> b -> c -> d -> c (peer)
// -> e -> a (peer)
let api = TestNpmRegistryApiInner::default();
api.ensure_package_version("package-a", "1.0.0");
api.ensure_package_version("package-b", "1.0.0");
api.ensure_package_version("package-c", "1.0.0");
api.ensure_package_version("package-d", "1.0.0");
api.ensure_package_version("package-e", "1.0.0");
api.add_dependency(("package-a", "1.0.0"), ("package-b", "1"));
api.add_dependency(("package-b", "1.0.0"), ("package-c", "1"));
api.add_dependency(("package-c", "1.0.0"), ("package-d", "1"));
api.add_dependency(("package-d", "1.0.0"), ("package-e", "1"));
api.add_peer_dependency(("package-d", "1.0.0"), ("package-c", "1"));
api.add_peer_dependency(("package-e", "1.0.0"), ("package-a", "1"));
let (packages, package_reqs) =
run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await;
assert_eq!(
packages,
vec![
TestNpmResolutionPackage {
pkg_id: "package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-b".to_string(),
"package-b@1.0.0_package-a@1.0.0".to_string(),
),]),
},
TestNpmResolutionPackage {
pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-c".to_string(),
"package-c@1.0.0_package-a@1.0.0".to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id: "package-c@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-d".to_string(),
"package-d@1.0.0_package-c@1.0.0__package-a@1.0.0_package-a@1.0.0"
.to_string(),
)]),
},
TestNpmResolutionPackage {
pkg_id:
"package-d@1.0.0_package-c@1.0.0__package-a@1.0.0_package-a@1.0.0"
.to_string(),
copy_index: 0,
dependencies: BTreeMap::from([
(
"package-c".to_string(),
"package-c@1.0.0_package-a@1.0.0".to_string(),
),
(
"package-e".to_string(),
"package-e@1.0.0_package-a@1.0.0".to_string()
),
]),
},
TestNpmResolutionPackage {
pkg_id: "package-e@1.0.0_package-a@1.0.0".to_string(),
copy_index: 0,
dependencies: BTreeMap::from([(
"package-a".to_string(),
"package-a@1.0.0".to_string()
),]),
},
]
);
assert_eq!(
package_reqs,
vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())]
);
}
#[derive(Debug, PartialEq, Eq)]
struct TestNpmResolutionPackage {
pub pkg_id: String,
pub copy_index: usize,
pub dependencies: BTreeMap<String, String>,
}
async fn run_resolver_and_get_output(
api: TestNpmRegistryApiInner,
reqs: Vec<&str>,
) -> (Vec<TestNpmResolutionPackage>, Vec<(String, String)>) {
let mut graph = Graph::default();
let api = NpmRegistryApi::new_for_test(api);
let mut resolver = GraphDependencyResolver::new(&mut graph, &api);
for req in reqs {
let req = NpmPackageReqReference::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 new_snapshot = Graph::from_snapshot(snapshot.clone())
.unwrap()
.into_snapshot(&api)
.await
.unwrap();
assert_eq!(
snapshot, new_snapshot,
"recreated snapshot should be the same"
);
// create one again from the new snapshot
let new_snapshot2 = Graph::from_snapshot(new_snapshot.clone())
.unwrap()
.into_snapshot(&api)
.await
.unwrap();
assert_eq!(
snapshot, new_snapshot2,
"second recreated snapshot should be the same"
);
}
let mut packages = snapshot.all_packages();
packages.sort_by(|a, b| a.pkg_id.cmp(&b.pkg_id));
let mut package_reqs = snapshot
.package_reqs
.into_iter()
.map(|(a, b)| {
(
a.to_string(),
snapshot.root_packages.get(&b).unwrap().as_serialized(),
)
})
.collect::<Vec<_>>();
package_reqs.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string()));
let packages = packages
.into_iter()
.map(|pkg| TestNpmResolutionPackage {
pkg_id: pkg.pkg_id.as_serialized(),
copy_index: pkg.copy_index,
dependencies: pkg
.dependencies
.into_iter()
.map(|(key, value)| (key, value.as_serialized()))
.collect(),
})
.collect();
(packages, package_reqs)
}
}