use crate::{DirectDep, LockedPackage};
use std::collections::{BTreeMap, VecDeque};
use super::raw::InstallPathInfo;
pub(super) fn resolve_nested(
pkg_install_path: &str,
dep_name: &str,
install_paths: &BTreeMap<String, InstallPathInfo>,
) -> Option<String> {
let mut base = pkg_install_path.to_string();
loop {
let candidate = if base.is_empty() {
format!("node_modules/{dep_name}")
} else {
format!("{base}/node_modules/{dep_name}")
};
if install_paths.contains_key(&candidate) {
return Some(candidate);
}
if base.is_empty() {
return None;
}
if let Some(idx) = base.rfind("/node_modules/") {
base.truncate(idx);
} else {
base.clear();
}
}
}
pub(super) fn package_name_from_install_path(install_path: &str) -> Option<String> {
let nm_idx = install_path.rfind("node_modules/")?;
let tail = &install_path[nm_idx + "node_modules/".len()..];
if tail.is_empty() {
return None;
}
if let Some(rest) = tail.strip_prefix('@') {
let slash = rest.find('/')?;
let scoped_end = slash + 1;
let name_end = rest[scoped_end..]
.find('/')
.map(|i| scoped_end + i)
.unwrap_or(rest.len());
return Some(format!("@{}", &rest[..name_end]));
}
let end = tail.find('/').unwrap_or(tail.len());
Some(tail[..end].to_string())
}
pub(crate) fn dep_path_tail<'a>(name: &str, dep_path: &'a str) -> &'a str {
dep_path
.strip_prefix(name)
.and_then(|rest| rest.strip_prefix('@'))
.unwrap_or_else(|| {
debug_assert!(
false,
"dep_path '{dep_path}' does not start with name '{name}'"
);
dep_path
})
}
pub(crate) fn segments_to_install_path(segs: &[String]) -> String {
if segs.is_empty() {
return String::new();
}
let mut out = String::from("node_modules/");
for (i, s) in segs.iter().enumerate() {
if i > 0 {
out.push_str("/node_modules/");
}
out.push_str(s);
}
out
}
pub(crate) fn build_hoist_tree(
canonical: &BTreeMap<String, &LockedPackage>,
roots: &[DirectDep],
) -> BTreeMap<Vec<String>, String> {
let mut placed: BTreeMap<Vec<String>, String> = BTreeMap::new();
let mut queue: VecDeque<(Vec<String>, String)> = VecDeque::new();
for dep in roots {
let key = canonical_key_from_dep_path(&dep.dep_path);
if !canonical.contains_key(&key) {
continue;
}
let segs = vec![dep.name.clone()];
if placed.insert(segs.clone(), key.clone()).is_none() {
queue.push_back((segs, key));
}
}
while let Some((parent_segs, parent_key)) = queue.pop_front() {
let Some(pkg) = canonical.get(&parent_key).copied() else {
continue;
};
let mut child_entries: Vec<(String, String)> = Vec::new();
for (child_name, child_value) in &pkg.dependencies {
let child_key = child_canonical_key(child_name, child_value);
if !canonical.contains_key(&child_key) {
continue;
}
child_entries.push((child_name.clone(), child_key));
}
for (child_name, child_key) in child_entries {
match ancestor_resolution(&parent_segs, &child_name, &child_key, &placed) {
AncestorHit::Match => continue,
AncestorHit::Shadowed => {
let mut nested = parent_segs.clone();
nested.push(child_name.clone());
if placed.insert(nested.clone(), child_key.clone()).is_none() {
queue.push_back((nested, child_key));
}
}
AncestorHit::Miss => {
let root_slot = vec![child_name.clone()];
if placed
.insert(root_slot.clone(), child_key.clone())
.is_none()
{
queue.push_back((root_slot, child_key));
}
}
}
}
}
placed
}
enum AncestorHit {
Match,
Shadowed,
Miss,
}
fn ancestor_resolution(
parent_segs: &[String],
child_name: &str,
child_key: &str,
placed: &BTreeMap<Vec<String>, String>,
) -> AncestorHit {
for i in (0..=parent_segs.len()).rev() {
let mut candidate: Vec<String> = parent_segs[..i].to_vec();
candidate.push(child_name.to_string());
if let Some(existing) = placed.get(&candidate) {
return if existing == child_key {
AncestorHit::Match
} else {
AncestorHit::Shadowed
};
}
}
AncestorHit::Miss
}
fn version_from_tail(tail: &str) -> &str {
tail.split_once('(').map(|(v, _)| v).unwrap_or(tail)
}
pub(crate) fn child_canonical_key(child_name: &str, value: &str) -> String {
let no_peer = version_from_tail(value);
let prefix = format!("{child_name}@");
if no_peer.starts_with(&prefix) {
no_peer.to_string()
} else {
format!("{prefix}{no_peer}")
}
}
pub(crate) fn dep_value_as_version<'a>(child_name: &str, value: &'a str) -> &'a str {
let no_peer = version_from_tail(value);
let prefix = format!("{child_name}@");
if let Some(rest) = no_peer.strip_prefix(&prefix) {
rest
} else {
no_peer
}
}
pub(crate) fn canonical_key_from_dep_path(dep_path: &str) -> String {
let trimmed = version_from_tail(dep_path);
let (name, version) = match trimmed.rfind('@') {
Some(0) | None => return trimmed.to_string(),
Some(idx) => (&trimmed[..idx], &trimmed[idx + 1..]),
};
format!("{name}@{version}")
}