pub mod override_rule;
pub mod platform;
pub use platform::{SupportedArchitectures, is_supported};
use aube_lockfile::{DepType, DirectDep, LocalSource, LockedPackage, LockfileGraph};
use aube_manifest::PackageJson;
use aube_registry::Packument;
use aube_registry::client::RegistryClient;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
pub trait ReadPackageHook: Send {
fn read_package<'a>(
&'a mut self,
pkg: aube_registry::VersionMetadata,
) -> Pin<Box<dyn Future<Output = Result<aube_registry::VersionMetadata, String>> + Send + 'a>>;
}
#[derive(Debug, Clone, Default)]
pub struct MinimumReleaseAge {
pub minutes: u64,
pub exclude: HashSet<String>,
pub strict: bool,
}
#[derive(Debug, Clone)]
pub struct DependencyPolicy {
pub package_extensions: Vec<PackageExtension>,
pub allowed_deprecated_versions: BTreeMap<String, String>,
pub trust_policy: TrustPolicy,
pub trust_policy_exclude: BTreeSet<String>,
pub trust_policy_ignore_after: Option<u64>,
pub block_exotic_subdeps: bool,
}
impl Default for DependencyPolicy {
fn default() -> Self {
Self {
package_extensions: Vec::new(),
allowed_deprecated_versions: BTreeMap::new(),
trust_policy: TrustPolicy::default(),
trust_policy_exclude: BTreeSet::new(),
trust_policy_ignore_after: None,
block_exotic_subdeps: true,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PackageExtension {
pub selector: String,
pub dependencies: BTreeMap<String, String>,
pub optional_dependencies: BTreeMap<String, String>,
pub peer_dependencies: BTreeMap<String, String>,
pub peer_dependencies_meta: BTreeMap<String, aube_registry::PeerDepMeta>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TrustPolicy {
NoDowngrade,
#[default]
Off,
}
impl MinimumReleaseAge {
pub fn cutoff(&self) -> Option<String> {
if self.minutes == 0 {
return None;
}
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
let cutoff_secs = now.saturating_sub(self.minutes * 60);
Some(format_iso8601_utc(cutoff_secs))
}
}
fn format_iso8601_utc(epoch_secs: u64) -> String {
let days = (epoch_secs / 86_400) as i64;
let secs_of_day = epoch_secs % 86_400;
let h = secs_of_day / 3600;
let m = (secs_of_day % 3600) / 60;
let s = secs_of_day % 60;
let (y, mo, d) = civil_from_days(days);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}.000Z")
}
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[derive(Debug, Clone)]
pub struct ResolvedPackage {
pub dep_path: String,
pub name: String,
pub version: String,
pub integrity: Option<String>,
pub local_source: Option<LocalSource>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ResolutionMode {
#[default]
Highest,
TimeBased,
}
pub struct Resolver {
client: Arc<RegistryClient>,
cache: HashMap<String, Packument>,
resolved_tx: Option<mpsc::UnboundedSender<ResolvedPackage>>,
packument_cache_dir: Option<std::path::PathBuf>,
packument_full_cache_dir: Option<std::path::PathBuf>,
auto_install_peers: bool,
exclude_links_from_lockfile: bool,
supported_architectures: SupportedArchitectures,
overrides: BTreeMap<String, String>,
override_rules: Vec<override_rule::OverrideRule>,
ignored_optional_dependencies: BTreeSet<String>,
resolution_mode: ResolutionMode,
project_root: PathBuf,
minimum_release_age: Option<MinimumReleaseAge>,
catalogs: BTreeMap<String, BTreeMap<String, String>>,
read_package_hook: Option<Box<dyn ReadPackageHook>>,
dependency_policy: DependencyPolicy,
git_shallow_hosts: Vec<String>,
peers_suffix_max_length: usize,
dedupe_peer_dependents: bool,
dedupe_peers: bool,
resolve_peers_from_workspace_root: bool,
registry_supports_time_field: bool,
}
struct ResolveTask {
name: String,
range: String,
dep_type: DepType,
is_root: bool,
parent: Option<String>,
importer: String,
original_specifier: Option<String>,
ancestors: Vec<(String, String)>,
}
impl Resolver {
pub fn new(client: Arc<RegistryClient>) -> Self {
Self {
client,
cache: HashMap::new(),
resolved_tx: None,
packument_cache_dir: None,
packument_full_cache_dir: None,
auto_install_peers: true,
exclude_links_from_lockfile: false,
supported_architectures: SupportedArchitectures::default(),
overrides: BTreeMap::new(),
override_rules: Vec::new(),
ignored_optional_dependencies: BTreeSet::new(),
resolution_mode: ResolutionMode::Highest,
project_root: PathBuf::from("."),
minimum_release_age: None,
catalogs: BTreeMap::new(),
read_package_hook: None,
dependency_policy: DependencyPolicy::default(),
git_shallow_hosts: Vec::new(),
peers_suffix_max_length: 1000,
dedupe_peer_dependents: true,
dedupe_peers: false,
resolve_peers_from_workspace_root: true,
registry_supports_time_field: false,
}
}
pub fn with_stream(
client: Arc<RegistryClient>,
) -> (Self, mpsc::UnboundedReceiver<ResolvedPackage>) {
let (tx, rx) = mpsc::unbounded_channel();
(
Self {
client,
cache: HashMap::new(),
resolved_tx: Some(tx),
packument_cache_dir: None,
packument_full_cache_dir: None,
auto_install_peers: true,
exclude_links_from_lockfile: false,
supported_architectures: SupportedArchitectures::default(),
overrides: BTreeMap::new(),
override_rules: Vec::new(),
ignored_optional_dependencies: BTreeSet::new(),
resolution_mode: ResolutionMode::Highest,
project_root: PathBuf::from("."),
minimum_release_age: None,
catalogs: BTreeMap::new(),
read_package_hook: None,
dependency_policy: DependencyPolicy::default(),
git_shallow_hosts: Vec::new(),
peers_suffix_max_length: 1000,
dedupe_peer_dependents: true,
dedupe_peers: false,
resolve_peers_from_workspace_root: true,
registry_supports_time_field: false,
},
rx,
)
}
pub fn with_packument_cache(mut self, cache_dir: std::path::PathBuf) -> Self {
self.packument_cache_dir = Some(cache_dir);
self
}
pub fn with_packument_full_cache(mut self, cache_dir: std::path::PathBuf) -> Self {
self.packument_full_cache_dir = Some(cache_dir);
self
}
pub fn with_resolution_mode(mut self, mode: ResolutionMode) -> Self {
self.resolution_mode = mode;
self
}
pub fn with_minimum_release_age(mut self, mra: Option<MinimumReleaseAge>) -> Self {
self.minimum_release_age = mra.filter(|m| m.minutes > 0);
self
}
pub fn with_auto_install_peers(mut self, auto_install_peers: bool) -> Self {
self.auto_install_peers = auto_install_peers;
self
}
pub fn with_peers_suffix_max_length(mut self, max_length: usize) -> Self {
self.peers_suffix_max_length = max_length;
self
}
pub fn with_dedupe_peer_dependents(mut self, value: bool) -> Self {
self.dedupe_peer_dependents = value;
self
}
pub fn with_dedupe_peers(mut self, value: bool) -> Self {
self.dedupe_peers = value;
self
}
pub fn with_resolve_peers_from_workspace_root(mut self, value: bool) -> Self {
self.resolve_peers_from_workspace_root = value;
self
}
pub fn with_registry_supports_time_field(mut self, value: bool) -> Self {
self.registry_supports_time_field = value;
self
}
pub fn with_exclude_links_from_lockfile(mut self, value: bool) -> Self {
self.exclude_links_from_lockfile = value;
self
}
pub fn with_supported_architectures(mut self, value: SupportedArchitectures) -> Self {
self.supported_architectures = value;
self
}
pub fn with_overrides(mut self, overrides: BTreeMap<String, String>) -> Self {
self.override_rules = override_rule::compile(&overrides);
self.overrides = overrides;
self
}
pub fn with_catalogs(mut self, catalogs: BTreeMap<String, BTreeMap<String, String>>) -> Self {
self.catalogs = catalogs;
self
}
pub fn with_project_root(mut self, project_root: PathBuf) -> Self {
self.project_root = project_root;
self
}
pub fn with_ignored_optional_dependencies(mut self, ignored: BTreeSet<String>) -> Self {
self.ignored_optional_dependencies = ignored;
self
}
pub fn with_read_package_hook(mut self, hook: Box<dyn ReadPackageHook>) -> Self {
self.read_package_hook = Some(hook);
self
}
pub fn with_dependency_policy(mut self, policy: DependencyPolicy) -> Self {
self.dependency_policy = policy;
self
}
pub fn with_git_shallow_hosts(mut self, hosts: Vec<String>) -> Self {
self.git_shallow_hosts = hosts;
self
}
pub async fn resolve(
&mut self,
manifest: &PackageJson,
existing: Option<&LockfileGraph>,
) -> Result<LockfileGraph, Error> {
self.resolve_workspace(
&[(".".to_string(), manifest.clone())],
existing,
&HashMap::new(),
)
.await
}
pub async fn resolve_workspace(
&mut self,
manifests: &[(String, PackageJson)],
existing: Option<&LockfileGraph>,
workspace_packages: &HashMap<String, String>,
) -> Result<LockfileGraph, Error> {
let resolve_start = std::time::Instant::now();
let mut packument_fetch_count = 0u32;
let mut packument_fetch_time = std::time::Duration::ZERO;
let mut lockfile_reuse_count = 0u32;
let mut resolved: BTreeMap<String, LockedPackage> = BTreeMap::new();
let mut resolved_versions: HashMap<String, Vec<String>> = HashMap::new();
let mut importers: BTreeMap<String, Vec<DirectDep>> = BTreeMap::new();
let mut queue: VecDeque<ResolveTask> = VecDeque::new();
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut resolved_times: BTreeMap<String, String> = BTreeMap::new();
let mut skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>> =
BTreeMap::new();
let mut catalog_picks: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
let importer_declared_dep_names: BTreeMap<String, BTreeSet<String>> = manifests
.iter()
.map(|(importer_path, manifest)| {
let names = manifest
.dependencies
.keys()
.chain(manifest.dev_dependencies.keys())
.chain(manifest.optional_dependencies.keys())
.cloned()
.collect();
(importer_path.clone(), names)
})
.collect();
let mut published_by: Option<String> =
self.minimum_release_age.as_ref().and_then(|m| m.cutoff());
if let Some(c) = published_by.as_deref() {
tracing::debug!("minimumReleaseAge cutoff: {}", c);
}
for (importer_path, manifest) in manifests {
importers.insert(importer_path.clone(), Vec::new());
for (name, range) in &manifest.dependencies {
queue.push_back(ResolveTask {
name: name.clone(),
range: range.clone(),
dep_type: DepType::Production,
is_root: true,
parent: None,
importer: importer_path.clone(),
original_specifier: Some(range.clone()),
ancestors: Vec::new(),
});
}
for (name, range) in &manifest.dev_dependencies {
queue.push_back(ResolveTask {
name: name.clone(),
range: range.clone(),
dep_type: DepType::Dev,
is_root: true,
parent: None,
importer: importer_path.clone(),
original_specifier: Some(range.clone()),
ancestors: Vec::new(),
});
}
for (name, range) in &manifest.optional_dependencies {
if self.ignored_optional_dependencies.contains(name) {
tracing::debug!(
"ignoring optional dependency {name} (pnpm.ignoredOptionalDependencies)"
);
continue;
}
queue.push_back(ResolveTask {
name: name.clone(),
range: range.clone(),
dep_type: DepType::Optional,
is_root: true,
parent: None,
importer: importer_path.clone(),
original_specifier: Some(range.clone()),
ancestors: Vec::new(),
});
}
}
let shared_semaphore = Arc::new(tokio::sync::Semaphore::new(64));
let needs_time = (self.resolution_mode == ResolutionMode::TimeBased
|| self.minimum_release_age.is_some())
&& !self.registry_supports_time_field;
#[allow(clippy::type_complexity)]
let mut in_flight: tokio::task::JoinSet<Result<(String, Packument), Error>> =
tokio::task::JoinSet::new();
let mut in_flight_names: HashSet<String> = HashSet::new();
let mut direct_deps_pending: usize = queue.len();
let mut cutoff_pending = self.resolution_mode == ResolutionMode::TimeBased;
let mut deferred_transitives: Vec<ResolveTask> = Vec::new();
let existing_names: std::collections::HashSet<String> = existing
.map(|g| g.packages.values().map(|p| p.name.clone()).collect())
.unwrap_or_default();
macro_rules! ensure_fetch {
($name:expr) => {{
let name: &str = $name;
if !in_flight_names.contains(name) && !self.cache.contains_key(name) {
in_flight_names.insert(name.to_string());
let name_owned = name.to_string();
let client = self.client.clone();
let cache_dir = self.packument_cache_dir.clone();
let full_cache_dir = self.packument_full_cache_dir.clone();
let sem = shared_semaphore.clone();
in_flight.spawn(async move {
let _permit = sem
.acquire_owned()
.await
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let packument = if needs_time {
match full_cache_dir.as_ref() {
Some(dir) => {
client
.fetch_packument_with_time_cached(&name_owned, dir)
.await
}
None => client.fetch_packument(&name_owned).await,
}
} else if let Some(ref dir) = cache_dir {
client.fetch_packument_cached(&name_owned, dir).await
} else {
client.fetch_packument(&name_owned).await
}
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
Ok::<_, Error>((name_owned, packument))
});
}
}};
}
macro_rules! note_root_done {
() => {
if direct_deps_pending > 0 {
direct_deps_pending -= 1;
}
};
}
macro_rules! prefetchable {
($name:expr, $range:expr) => {{
let r: &str = $range;
let n: &str = $name;
!r.starts_with("workspace:")
&& !r.starts_with("catalog:")
&& !r.starts_with("npm:")
&& !is_non_registry_specifier(r)
&& !self.overrides.contains_key(n)
}};
}
for task in queue.iter() {
if !prefetchable!(task.name.as_str(), task.range.as_str()) {
continue;
}
if existing_names.contains(task.name.as_str()) {
continue;
}
ensure_fetch!(&task.name);
}
'outer: loop {
if cutoff_pending && direct_deps_pending == 0 {
let direct_dep_paths: std::collections::HashSet<&String> = importers
.values()
.flat_map(|deps| deps.iter().map(|d| &d.dep_path))
.collect();
let mut max_time: Option<&String> = None;
for (dep_path, t) in resolved_times.iter() {
if !direct_dep_paths.contains(dep_path) {
continue;
}
if max_time.map(|m| t > m).unwrap_or(true) {
max_time = Some(t);
}
}
if let Some(existing_graph) = existing {
for (dep_path, t) in &existing_graph.times {
if !direct_dep_paths.contains(dep_path) {
continue;
}
if max_time.map(|m| t > m).unwrap_or(true) {
max_time = Some(t);
}
}
}
if let Some(m) = max_time {
tracing::debug!("time-based resolution cutoff: {}", m);
published_by = Some(match published_by.take() {
Some(existing) if existing.as_str() < m.as_str() => existing,
_ => m.clone(),
});
}
cutoff_pending = false;
queue.extend(deferred_transitives.drain(..));
}
let Some(mut task) = queue.pop_front() else {
if !deferred_transitives.is_empty() {
return Err(Error::Registry(
"(resolver)".to_string(),
format!(
"{} transitives still deferred when resolve completed",
deferred_transitives.len()
),
));
}
break 'outer;
};
{
if let Some(catalog_name) = task.range.strip_prefix("catalog:").map(|n| {
if n.is_empty() {
"default".to_string()
} else {
n.to_string()
}
}) {
match self
.catalogs
.get(&catalog_name)
.and_then(|c| c.get(&task.name))
{
Some(real_range) => {
tracing::trace!(
"catalog: {} {} -> {}",
task.name,
task.range,
real_range
);
catalog_picks
.entry(catalog_name.clone())
.or_default()
.insert(task.name.clone(), real_range.clone());
task.range = real_range.clone();
}
None => {
return Err(Error::UnknownCatalogRef {
name: task.name.clone(),
spec: task.range.clone(),
catalog: catalog_name,
});
}
}
}
for _ in 0..2 {
let mut changed = false;
if let Some(override_spec) = pick_override_spec(
&self.override_rules,
&task.name,
&task.range,
&task.ancestors,
) && task.range != override_spec
{
tracing::trace!(
"override: {}@{} -> {}",
task.name,
task.range,
override_spec
);
task.range = override_spec;
changed = true;
}
if let Some(rest) = task.range.strip_prefix("npm:")
&& let Some(at_idx) = rest.rfind('@')
{
let real_name = rest[..at_idx].to_string();
let real_range = rest[at_idx + 1..].to_string();
if real_name != task.name || real_range != task.range {
tracing::trace!(
"npm alias: {} -> {}@{}",
task.name,
real_name,
real_range
);
task.name = real_name;
task.range = real_range;
changed = true;
}
}
if !changed {
break;
}
}
if is_non_registry_specifier(&task.range) {
if !task.is_root && self.dependency_policy.block_exotic_subdeps {
return Err(Error::BlockedExoticSubdep {
name: task.name.clone(),
spec: task.range.clone(),
parent: task
.parent
.clone()
.unwrap_or_else(|| "<unknown>".to_string()),
});
}
let importer_root = if task.importer == "." {
self.project_root.clone()
} else {
self.project_root.join(&task.importer)
};
let Some(raw_local) = LocalSource::parse(&task.range, &importer_root) else {
return Err(Error::Registry(
task.name.clone(),
format!("unparseable local specifier: {}", task.range),
));
};
if !task.is_root
&& !matches!(
raw_local,
LocalSource::Git(_) | LocalSource::RemoteTarball(_)
)
{
return Err(Error::Registry(
task.name.clone(),
format!(
"transitive local specifier {} cannot be resolved without the parent package source root",
task.range
),
));
}
let (local, real_version, target_deps) = if let LocalSource::Git(ref g) =
raw_local
{
let shallow = aube_store::git_host_in_list(&g.url, &self.git_shallow_hosts);
let (resolved_local, version, deps) =
resolve_git_source(&task.name, g, shallow)
.await
.map_err(|e| {
Error::Registry(
task.name.clone(),
format!("git resolve {}: {e}", task.range),
)
})?;
(resolved_local, version, deps)
} else if let LocalSource::RemoteTarball(ref t) = raw_local {
let (resolved_local, version, deps) =
resolve_remote_tarball(&task.name, t, self.client.as_ref())
.await
.map_err(|e| {
Error::Registry(
task.name.clone(),
format!("remote tarball {}: {e}", task.range),
)
})?;
(resolved_local, version, deps)
} else {
let local = rebase_local(&raw_local, &importer_root, &self.project_root);
let (_target_name, version, deps) =
read_local_manifest(&raw_local, &importer_root).unwrap_or_else(|_| {
(task.name.clone(), "0.0.0".to_string(), BTreeMap::new())
});
(local, version, deps)
};
let dep_path = local.dep_path(&task.name);
let linked_name = task.name.clone();
if task.is_root
&& let Some(deps) = importers.get_mut(&task.importer)
{
deps.push(DirectDep {
name: task.name.clone(),
dep_path: dep_path.clone(),
dep_type: task.dep_type,
specifier: task.original_specifier.clone(),
});
}
if !visited.contains(&dep_path) {
visited.insert(dep_path.clone());
resolved.insert(
dep_path.clone(),
LockedPackage {
name: linked_name.clone(),
version: real_version.clone(),
dep_path: dep_path.clone(),
local_source: Some(local.clone()),
..Default::default()
},
);
if let Some(ref tx) = self.resolved_tx {
let _ = tx.send(ResolvedPackage {
dep_path: dep_path.clone(),
name: linked_name.clone(),
version: real_version.clone(),
integrity: None,
local_source: Some(local.clone()),
});
}
if !matches!(local, LocalSource::Link(_)) {
let mut child_ancestors = task.ancestors.clone();
child_ancestors.push((linked_name.clone(), real_version.clone()));
for (child_name, child_range) in target_deps {
queue.push_back(ResolveTask {
name: child_name,
range: child_range,
dep_type: DepType::Production,
is_root: false,
parent: Some(dep_path.clone()),
importer: task.importer.clone(),
original_specifier: None,
ancestors: child_ancestors.clone(),
});
}
}
}
if task.is_root {
note_root_done!();
}
continue;
}
if task.range.starts_with("workspace:")
&& let Some(ws_version) = workspace_packages.get(&task.name)
{
let dep_path = dep_path_for(&task.name, ws_version);
if task.is_root
&& let Some(deps) = importers.get_mut(&task.importer)
{
deps.push(DirectDep {
name: task.name.clone(),
dep_path: dep_path.clone(),
dep_type: task.dep_type,
specifier: task.original_specifier.clone(),
});
}
if let Some(ref parent_dp) = task.parent
&& let Some(parent_pkg) = resolved.get_mut(parent_dp)
{
parent_pkg
.dependencies
.insert(task.name.clone(), ws_version.clone());
}
if task.is_root {
note_root_done!();
}
continue;
}
if let Some(matched_ver) = resolved_versions.get(&task.name).and_then(|versions| {
versions
.iter()
.find(|v| version_satisfies(v, &task.range))
.cloned()
}) {
let dep_path = dep_path_for(&task.name, &matched_ver);
if task.is_root
&& let Some(deps) = importers.get_mut(&task.importer)
{
deps.push(DirectDep {
name: task.name.clone(),
dep_path: dep_path.clone(),
dep_type: task.dep_type,
specifier: task.original_specifier.clone(),
});
}
if let Some(ref parent_dp) = task.parent
&& let Some(parent_pkg) = resolved.get_mut(parent_dp)
{
parent_pkg
.dependencies
.insert(task.name.clone(), matched_ver);
}
if task.is_root {
note_root_done!();
}
continue;
}
{
if let Some(locked_pkg) = existing.and_then(|g| {
g.packages.values().find(|p| {
p.name == task.name && version_satisfies(&p.version, &task.range)
})
}) {
if task.dep_type == DepType::Optional
&& !is_supported(
&locked_pkg.os,
&locked_pkg.cpu,
&locked_pkg.libc,
&self.supported_architectures,
)
{
tracing::debug!(
"skipping optional dep {}@{}: platform mismatch",
task.name,
locked_pkg.version
);
if task.is_root
&& let Some(spec) = task.original_specifier.as_ref()
{
skipped_optional_dependencies
.entry(task.importer.clone())
.or_default()
.insert(task.name.clone(), spec.clone());
}
if task.is_root {
note_root_done!();
}
continue;
}
let version = locked_pkg.version.clone();
let dep_path = dep_path_for(&task.name, &version);
if task.is_root
&& let Some(deps) = importers.get_mut(&task.importer)
{
deps.push(DirectDep {
name: task.name.clone(),
dep_path: dep_path.clone(),
dep_type: task.dep_type,
specifier: task.original_specifier.clone(),
});
}
if let Some(ref parent_dp) = task.parent
&& let Some(parent_pkg) = resolved.get_mut(parent_dp)
{
parent_pkg
.dependencies
.insert(task.name.clone(), version.clone());
}
if !visited.contains(&dep_path) {
visited.insert(dep_path.clone());
resolved_versions
.entry(task.name.clone())
.or_default()
.push(version.clone());
if let Some(g) = existing
&& let Some(t) = g.times.get(&dep_path)
{
resolved_times.insert(dep_path.clone(), t.clone());
}
if let Some(ref tx) = self.resolved_tx {
let _ = tx.send(ResolvedPackage {
dep_path: dep_path.clone(),
name: task.name.clone(),
version: version.clone(),
integrity: locked_pkg.integrity.clone(),
local_source: locked_pkg.local_source.clone(),
});
}
resolved.insert(
dep_path.clone(),
LockedPackage {
name: task.name.clone(),
version: version.clone(),
integrity: locked_pkg.integrity.clone(),
dependencies: BTreeMap::new(),
peer_dependencies: locked_pkg.peer_dependencies.clone(),
peer_dependencies_meta: locked_pkg
.peer_dependencies_meta
.clone(),
dep_path: dep_path.clone(),
local_source: locked_pkg.local_source.clone(),
os: locked_pkg.os.clone(),
cpu: locked_pkg.cpu.clone(),
libc: locked_pkg.libc.clone(),
bundled_dependencies: locked_pkg.bundled_dependencies.clone(),
tarball_url: locked_pkg.tarball_url.clone(),
},
);
let mut child_ancestors = task.ancestors.clone();
child_ancestors.push((task.name.clone(), version.clone()));
for (dep_name, dep_version) in &locked_pkg.dependencies {
let canonical_version = dep_version
.split('(')
.next()
.unwrap_or(dep_version)
.to_string();
queue.push_back(ResolveTask {
name: dep_name.clone(),
range: canonical_version,
dep_type: DepType::Production,
is_root: false,
parent: Some(dep_path.clone()),
importer: task.importer.clone(),
original_specifier: None,
ancestors: child_ancestors.clone(),
});
}
}
lockfile_reuse_count += 1;
if task.is_root {
note_root_done!();
}
continue;
}
}
let wait_start = std::time::Instant::now();
while !self.cache.contains_key(&task.name) {
ensure_fetch!(&task.name);
match in_flight.join_next().await {
Some(Ok(Ok((name, packument)))) => {
in_flight_names.remove(&name);
self.cache.insert(name, packument);
packument_fetch_count += 1;
}
Some(Ok(Err(e))) => return Err(e),
Some(Err(join_err)) => {
return Err(Error::Registry(
"(join)".to_string(),
join_err.to_string(),
));
}
None => {
return Err(Error::Registry(
task.name.clone(),
"packument fetch disappeared before completing".to_string(),
));
}
}
}
packument_fetch_time += wait_start.elapsed();
if cutoff_pending && !task.is_root {
deferred_transitives.push(task);
continue;
}
let packument = self.cache.get(&task.name).ok_or_else(|| {
Error::Registry(task.name.clone(), "packument not in cache".to_string())
})?;
let locked_version = existing.and_then(|g| {
g.packages
.values()
.find(|p| p.name == task.name && version_satisfies(&p.version, &task.range))
.map(|p| p.version.as_str())
});
let pick_lowest = self.resolution_mode == ResolutionMode::TimeBased && task.is_root;
let cutoff_for_pkg = match self.minimum_release_age.as_ref() {
Some(mra) if mra.exclude.contains(&task.name) => None,
_ => published_by.as_deref(),
};
let strict = match self.minimum_release_age.as_ref() {
Some(m) => m.strict,
None => true,
};
let pick = pick_version(
packument,
&task.range,
locked_version,
pick_lowest,
cutoff_for_pkg,
strict,
);
let picked_ref = match pick {
PickResult::Found(meta) => meta,
PickResult::AgeGated => match self.minimum_release_age.as_ref() {
Some(mra) => {
return Err(Error::AgeGate {
name: task.name.clone(),
range: task.range.clone(),
minutes: mra.minutes,
});
}
None => {
return Err(Error::NoMatch(task.name.clone(), task.range.clone()));
}
},
PickResult::NoMatch => {
return Err(Error::NoMatch(task.name.clone(), task.range.clone()));
}
};
let mut picked_owned = picked_ref.clone();
let picked_publish_time = packument.time.get(&picked_ref.version).cloned();
let prehook_dep_path = dep_path_for(&task.name, &picked_ref.version);
let already_visited = visited.contains(&prehook_dep_path);
if !already_visited {
apply_package_extensions(
&mut picked_owned,
&self.dependency_policy.package_extensions,
);
}
if !already_visited && let Some(hook) = self.read_package_hook.as_mut() {
let before_name = picked_owned.name.clone();
let before_version = picked_owned.version.clone();
let before_dist = picked_owned.dist.clone();
let before_os = picked_owned.os.clone();
let before_cpu = picked_owned.cpu.clone();
let before_libc = picked_owned.libc.clone();
let before_bundled = picked_owned.bundled_dependencies.clone();
let before_has_install_script = picked_owned.has_install_script;
let before_deprecated = picked_owned.deprecated.clone();
let input = picked_owned.clone();
let mut after = hook.read_package(input).await.map_err(|e| {
Error::Registry(before_name.clone(), format!("readPackage hook: {e}"))
})?;
if after.name != before_name || after.version != before_version {
tracing::warn!(
"[pnpmfile] readPackage rewrote {}@{} identity to {}@{}; \
aube ignores identity edits",
before_name,
before_version,
after.name,
after.version,
);
}
after.name = before_name;
after.version = before_version;
after.dist = before_dist;
after.os = before_os;
after.cpu = before_cpu;
after.libc = before_libc;
after.bundled_dependencies = before_bundled;
after.has_install_script = before_has_install_script;
after.deprecated = before_deprecated;
picked_owned = after;
}
let version_meta = &picked_owned;
let platform_ok = is_supported(
&version_meta.os,
&version_meta.cpu,
&version_meta.libc,
&self.supported_architectures,
);
if !platform_ok {
if task.dep_type == DepType::Optional {
tracing::debug!(
"skipping optional dep {}@{}: unsupported platform (os={:?} cpu={:?} libc={:?})",
task.name,
version_meta.version,
version_meta.os,
version_meta.cpu,
version_meta.libc
);
if task.is_root
&& let Some(spec) = task.original_specifier.as_ref()
{
skipped_optional_dependencies
.entry(task.importer.clone())
.or_default()
.insert(task.name.clone(), spec.clone());
}
if task.is_root {
note_root_done!();
}
continue;
}
tracing::warn!(
"required dep {}@{} declares unsupported platform (os={:?} cpu={:?} libc={:?}); installing anyway",
task.name,
version_meta.version,
version_meta.os,
version_meta.cpu,
version_meta.libc
);
}
let version = version_meta.version.clone();
let dep_path = dep_path_for(&task.name, &version);
if let Some(t) = picked_publish_time.as_ref() {
resolved_times.insert(dep_path.clone(), t.clone());
}
if task.is_root
&& let Some(deps) = importers.get_mut(&task.importer)
{
deps.push(DirectDep {
name: task.name.clone(),
dep_path: dep_path.clone(),
dep_type: task.dep_type,
specifier: task.original_specifier.clone(),
});
}
if let Some(ref parent_dp) = task.parent
&& let Some(parent_pkg) = resolved.get_mut(parent_dp)
{
parent_pkg
.dependencies
.insert(task.name.clone(), version.clone());
}
if visited.contains(&dep_path) {
if task.is_root {
note_root_done!();
}
continue;
}
visited.insert(dep_path.clone());
tracing::trace!("resolved {}@{}", task.name, version);
if let Some(ref msg) = version_meta.deprecated {
let suppressed = self
.dependency_policy
.allowed_deprecated_versions
.get(&task.name)
.is_some_and(|range| {
node_semver::Range::parse(range).ok().is_some_and(|r| {
node_semver::Version::parse(&version)
.ok()
.is_some_and(|v| r.satisfies(&v))
})
});
if !suppressed {
tracing::warn!("{}@{} is deprecated: {}", task.name, version, msg);
}
}
resolved_versions
.entry(task.name.clone())
.or_default()
.push(version.clone());
let integrity = version_meta.dist.as_ref().and_then(|d| d.integrity.clone());
if let Some(ref tx) = self.resolved_tx {
let _ = tx.send(ResolvedPackage {
dep_path: dep_path.clone(),
name: task.name.clone(),
version: version.clone(),
integrity: integrity.clone(),
local_source: None,
});
}
let peer_deps = version_meta.peer_dependencies.clone();
let peer_meta: BTreeMap<String, aube_lockfile::PeerDepMeta> = version_meta
.peer_dependencies_meta
.iter()
.map(|(k, v)| {
(
k.clone(),
aube_lockfile::PeerDepMeta {
optional: v.optional,
},
)
})
.collect();
let bundled_names: std::collections::HashSet<String> = version_meta
.bundled_dependencies
.as_ref()
.map(|b| {
b.names(&version_meta.dependencies)
.into_iter()
.map(String::from)
.collect()
})
.unwrap_or_default();
resolved.insert(
dep_path.clone(),
LockedPackage {
name: task.name.clone(),
version: version.clone(),
integrity,
dependencies: BTreeMap::new(),
peer_dependencies: peer_deps,
peer_dependencies_meta: peer_meta,
dep_path: dep_path.clone(),
local_source: None,
os: version_meta.os.clone(),
cpu: version_meta.cpu.clone(),
libc: version_meta.libc.clone(),
bundled_dependencies: {
let mut v: Vec<String> = bundled_names.iter().cloned().collect();
v.sort();
v
},
tarball_url: None,
},
);
let mut child_ancestors = task.ancestors.clone();
child_ancestors.push((task.name.clone(), version.clone()));
for (dep_name, dep_range) in &version_meta.dependencies {
if bundled_names.contains(dep_name) {
continue;
}
if self.dependency_policy.block_exotic_subdeps
&& is_non_registry_specifier(dep_range)
{
return Err(Error::Registry(
dep_name.clone(),
format!(
"uses exotic specifier \"{dep_range}\" which is blocked \
by blockExoticSubdeps (declared by {})",
task.name
),
));
}
if !existing_names.contains(dep_name.as_str())
&& prefetchable!(dep_name.as_str(), dep_range.as_str())
{
ensure_fetch!(dep_name);
}
queue.push_back(ResolveTask {
name: dep_name.clone(),
range: dep_range.clone(),
dep_type: DepType::Production,
is_root: false,
parent: Some(dep_path.clone()),
importer: task.importer.clone(),
original_specifier: None,
ancestors: child_ancestors.clone(),
});
}
for (dep_name, dep_range) in &version_meta.optional_dependencies {
if bundled_names.contains(dep_name) {
continue;
}
if self.ignored_optional_dependencies.contains(dep_name) {
continue;
}
if self.dependency_policy.block_exotic_subdeps
&& is_non_registry_specifier(dep_range)
{
tracing::warn!(
"skipping optional dependency {dep_name} of {} — \
exotic specifier \"{dep_range}\" blocked by blockExoticSubdeps",
task.name
);
continue;
}
if !existing_names.contains(dep_name.as_str())
&& prefetchable!(dep_name.as_str(), dep_range.as_str())
{
ensure_fetch!(dep_name);
}
queue.push_back(ResolveTask {
name: dep_name.clone(),
range: dep_range.clone(),
dep_type: DepType::Optional,
is_root: false,
parent: Some(dep_path.clone()),
importer: task.importer.clone(),
original_specifier: None,
ancestors: child_ancestors.clone(),
});
}
if self.auto_install_peers {
for (dep_name, dep_range) in &version_meta.peer_dependencies {
let peer_optional = version_meta
.peer_dependencies_meta
.get(dep_name)
.map(|m| m.optional)
.unwrap_or(false);
if peer_optional {
continue;
}
let importer_declares_peer = importer_declared_dep_names
.get(&task.importer)
.is_some_and(|names| names.contains(dep_name));
let root_declares_peer = self.resolve_peers_from_workspace_root
&& task.importer != "."
&& importer_declared_dep_names
.get(".")
.is_some_and(|names| names.contains(dep_name));
let peer_dep_is_ancestor =
task.ancestors.iter().any(|(name, _)| name == dep_name);
if importer_declares_peer || root_declares_peer || peer_dep_is_ancestor {
continue;
}
if version_meta.dependencies.contains_key(dep_name)
|| version_meta.optional_dependencies.contains_key(dep_name)
|| bundled_names.contains(dep_name)
{
continue;
}
if self.dependency_policy.block_exotic_subdeps
&& is_non_registry_specifier(dep_range)
{
tracing::warn!(
"skipping peer dependency {dep_name} of {} — \
exotic specifier \"{dep_range}\" blocked \
by blockExoticSubdeps",
task.name
);
continue;
}
if !existing_names.contains(dep_name.as_str())
&& prefetchable!(dep_name.as_str(), dep_range.as_str())
{
ensure_fetch!(dep_name);
}
queue.push_back(ResolveTask {
name: dep_name.clone(),
range: dep_range.clone(),
dep_type: DepType::Production,
is_root: false,
parent: Some(dep_path.clone()),
importer: task.importer.clone(),
original_specifier: None,
ancestors: child_ancestors.clone(),
});
}
}
if task.is_root {
note_root_done!();
}
}
}
while in_flight.join_next().await.is_some() {}
let resolve_elapsed = resolve_start.elapsed();
tracing::debug!(
"resolver: {:.1?} total, {} packuments fetched ({:.1?} wall), {} reused from lockfile, {} packages resolved",
resolve_elapsed,
packument_fetch_count,
packument_fetch_time,
lockfile_reuse_count,
resolved.len()
);
let mut resolved_catalogs: BTreeMap<String, BTreeMap<String, aube_lockfile::CatalogEntry>> =
BTreeMap::new();
for (cat_name, entries) in catalog_picks {
let mut out: BTreeMap<String, aube_lockfile::CatalogEntry> = BTreeMap::new();
for (pkg, spec) in entries {
let resolved_for_pkg = resolved_versions.get(&pkg);
let version = resolved_for_pkg
.and_then(|vs| vs.iter().find(|v| version_satisfies(v, &spec)).cloned())
.or_else(|| resolved_for_pkg.and_then(|vs| vs.first().cloned()))
.unwrap_or_else(|| spec.clone());
out.insert(
pkg,
aube_lockfile::CatalogEntry {
specifier: spec,
version,
},
);
}
if !out.is_empty() {
resolved_catalogs.insert(cat_name, out);
}
}
let canonical = LockfileGraph {
importers,
packages: resolved,
settings: aube_lockfile::LockfileSettings {
auto_install_peers: self.auto_install_peers,
exclude_links_from_lockfile: self.exclude_links_from_lockfile,
lockfile_include_tarball_url: false,
},
overrides: self.overrides.clone(),
ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
times: resolved_times,
skipped_optional_dependencies,
catalogs: resolved_catalogs,
};
let hoisted = if self.auto_install_peers {
hoist_auto_installed_peers(canonical)
} else {
canonical
};
let peer_options = PeerContextOptions {
dedupe_peer_dependents: self.dedupe_peer_dependents,
dedupe_peers: self.dedupe_peers,
resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
peers_suffix_max_length: self.peers_suffix_max_length,
};
let contextualized = apply_peer_contexts(hoisted, &peer_options);
tracing::debug!(
"peer-context pass produced {} contextualized packages",
contextualized.packages.len()
);
Ok(contextualized)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnmetPeer {
pub from_dep_path: String,
pub from_name: String,
pub peer_name: String,
pub declared: String,
pub found: Option<String>,
}
pub fn detect_unmet_peers(graph: &LockfileGraph) -> Vec<UnmetPeer> {
let mut unmet = Vec::new();
for pkg in graph.packages.values() {
for (peer_name, declared_range) in &pkg.peer_dependencies {
let optional = pkg
.peer_dependencies_meta
.get(peer_name)
.map(|m| m.optional)
.unwrap_or(false);
if optional {
continue;
}
let found_tail = pkg.dependencies.get(peer_name);
let found_version = found_tail.map(|t| t.split('(').next().unwrap_or(t).to_string());
let satisfied = match &found_version {
Some(v) => version_satisfies(v, declared_range),
None => false,
};
if satisfied {
continue;
}
unmet.push(UnmetPeer {
from_dep_path: pkg.dep_path.clone(),
from_name: pkg.name.clone(),
peer_name: peer_name.clone(),
declared: declared_range.clone(),
found: found_version,
});
}
}
unmet.sort_by(|a, b| {
(a.from_dep_path.as_str(), a.peer_name.as_str())
.cmp(&(b.from_dep_path.as_str(), b.peer_name.as_str()))
});
unmet
}
fn hoist_auto_installed_peers(mut graph: LockfileGraph) -> LockfileGraph {
let importer_paths: Vec<String> = graph.importers.keys().cloned().collect();
for importer_path in importer_paths {
let Some(direct_deps) = graph.importers.get(&importer_path) else {
continue;
};
let mut satisfied: std::collections::HashSet<String> =
direct_deps.iter().map(|d| d.name.clone()).collect();
let mut queue: std::collections::VecDeque<String> =
direct_deps.iter().map(|d| d.dep_path.clone()).collect();
let mut walked: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut additions: Vec<DirectDep> = Vec::new();
while let Some(dep_path) = queue.pop_front() {
if !walked.insert(dep_path.clone()) {
continue;
}
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
for (peer_name, peer_range) in &pkg.peer_dependencies {
if satisfied.contains(peer_name) {
continue;
}
let resolved_via_pkg_deps = pkg.dependencies.contains_key(peer_name);
let resolved_version = pkg.dependencies.get(peer_name).cloned().or_else(|| {
graph
.packages
.values()
.filter(|p| p.name == *peer_name)
.filter_map(|p| {
node_semver::Version::parse(&p.version)
.ok()
.map(|v| (v, p.version.clone()))
})
.max_by(|a, b| a.0.cmp(&b.0))
.map(|(_, s)| s)
});
let Some(version) = resolved_version else {
continue;
};
let canonical_version = version.split('(').next().unwrap_or(&version).to_string();
let synth_dep_path = format!("{peer_name}@{canonical_version}");
if !graph.packages.contains_key(&synth_dep_path) {
continue;
}
satisfied.insert(peer_name.clone());
if !resolved_via_pkg_deps {
queue.push_back(synth_dep_path.clone());
}
additions.push(DirectDep {
name: peer_name.clone(),
dep_path: synth_dep_path,
dep_type: DepType::Production,
specifier: Some(peer_range.clone()),
});
}
for (child_name, child_version_tail) in &pkg.dependencies {
let canonical = child_version_tail
.split('(')
.next()
.unwrap_or(child_version_tail);
queue.push_back(format!("{child_name}@{canonical}"));
}
}
if !additions.is_empty() {
tracing::debug!(
"hoisted {} auto-installed peer(s) into importer {}",
additions.len(),
importer_path
);
if let Some(deps) = graph.importers.get_mut(&importer_path) {
deps.extend(additions);
deps.sort_by(|a, b| a.name.cmp(&b.name));
}
}
}
graph
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PeerContextOptions {
pub(crate) dedupe_peer_dependents: bool,
pub(crate) dedupe_peers: bool,
pub(crate) resolve_from_workspace_root: bool,
pub(crate) peers_suffix_max_length: usize,
}
impl Default for PeerContextOptions {
fn default() -> Self {
Self {
dedupe_peer_dependents: true,
dedupe_peers: false,
resolve_from_workspace_root: true,
peers_suffix_max_length: 1000,
}
}
}
fn apply_peer_contexts(canonical: LockfileGraph, options: &PeerContextOptions) -> LockfileGraph {
const MAX_ITERATIONS: usize = 16;
let mut current = canonical;
let mut previous_keys: Option<std::collections::BTreeSet<String>> = None;
let mut converged = false;
for i in 0..MAX_ITERATIONS {
let after_once = apply_peer_contexts_once(current, options);
let next = if options.dedupe_peer_dependents {
dedupe_peer_variants(after_once)
} else {
after_once
};
let next_keys: std::collections::BTreeSet<String> = next.packages.keys().cloned().collect();
if previous_keys.as_ref() == Some(&next_keys) {
tracing::debug!("peer-context pass converged after {i} iteration(s)");
current = next;
converged = true;
break;
}
previous_keys = Some(next_keys);
current = next;
}
if !converged {
tracing::warn!(
"peer-context pass hit MAX_ITERATIONS={MAX_ITERATIONS} without converging — \
lockfile may not be byte-identical to pnpm's nested form"
);
}
if options.dedupe_peers {
dedupe_peer_suffixes(current)
} else {
current
}
}
fn dedupe_peer_variants(graph: LockfileGraph) -> LockfileGraph {
let canonical_base = |key: &str| -> String { key.split('(').next().unwrap_or(key).to_string() };
let peer_base = |tail: &str| -> String { tail.split('(').next().unwrap_or(tail).to_string() };
let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
for key in graph.packages.keys() {
groups
.entry(canonical_base(key))
.or_default()
.push(key.clone());
}
let mut rewrite: BTreeMap<String, String> = BTreeMap::new();
for (_base, mut keys) in groups {
if keys.len() < 2 {
continue;
}
keys.sort();
let mut parent: Vec<usize> = (0..keys.len()).collect();
fn find(parent: &mut [usize], i: usize) -> usize {
if parent[i] == i {
i
} else {
let r = find(parent, parent[i]);
parent[i] = r;
r
}
}
for i in 0..keys.len() {
for j in (i + 1)..keys.len() {
let pa = &graph.packages[&keys[i]];
let pb = &graph.packages[&keys[j]];
if pa.version != pb.version {
continue;
}
let peer_names: BTreeSet<&String> = pa
.peer_dependencies
.keys()
.chain(pb.peer_dependencies.keys())
.collect();
let equivalent = peer_names.iter().all(|name| {
match (
pa.dependencies.get(name.as_str()),
pb.dependencies.get(name.as_str()),
) {
(Some(va), Some(vb)) => peer_base(va) == peer_base(vb),
(None, None) => true,
_ => false,
}
});
if equivalent {
let ri = find(&mut parent, i);
let rj = find(&mut parent, j);
if ri != rj {
parent[ri] = rj;
}
}
}
}
#[allow(clippy::needless_range_loop)]
{
let mut class_rep: BTreeMap<usize, String> = BTreeMap::new();
for i in 0..keys.len() {
let root = find(&mut parent, i);
class_rep
.entry(root)
.and_modify(|cur| {
if keys[i] < *cur {
*cur = keys[i].clone();
}
})
.or_insert_with(|| keys[i].clone());
}
for i in 0..keys.len() {
let root = find(&mut parent, i);
let canonical = class_rep[&root].clone();
if keys[i] != canonical {
rewrite.insert(keys[i].clone(), canonical);
}
}
}
}
if rewrite.is_empty() {
return graph;
}
let LockfileGraph {
importers,
packages,
settings,
overrides,
ignored_optional_dependencies,
times,
skipped_optional_dependencies,
catalogs,
} = graph;
let mut new_packages: BTreeMap<String, LockedPackage> = BTreeMap::new();
for (key, mut pkg) in packages {
if rewrite.contains_key(&key) {
continue;
}
for (dep_name, dep_tail) in pkg.dependencies.iter_mut() {
let dep_key = format!("{dep_name}@{dep_tail}");
if let Some(canonical) = rewrite.get(&dep_key) {
let new_tail = canonical
.strip_prefix(&format!("{dep_name}@"))
.map(|s| s.to_string())
.unwrap_or_else(|| canonical.clone());
*dep_tail = new_tail;
}
}
new_packages.insert(key, pkg);
}
let mut new_importers: BTreeMap<String, Vec<DirectDep>> = BTreeMap::new();
for (importer_path, deps) in importers {
let mut new_deps = Vec::with_capacity(deps.len());
for mut dep in deps {
if let Some(canonical) = rewrite.get(&dep.dep_path) {
dep.dep_path = canonical.clone();
}
new_deps.push(dep);
}
new_importers.insert(importer_path, new_deps);
}
LockfileGraph {
importers: new_importers,
packages: new_packages,
settings,
overrides,
ignored_optional_dependencies,
times,
skipped_optional_dependencies,
catalogs,
}
}
fn apply_peer_contexts_once(
canonical: LockfileGraph,
options: &PeerContextOptions,
) -> LockfileGraph {
let mut out_packages: BTreeMap<String, LockedPackage> = BTreeMap::new();
let mut new_importers: BTreeMap<String, Vec<DirectDep>> = BTreeMap::new();
let root_scope: BTreeMap<String, String> = canonical
.importers
.get(".")
.map(|deps| {
deps.iter()
.map(|d| {
let tail = d
.dep_path
.strip_prefix(&format!("{}@", d.name))
.map(|s| s.to_string())
.unwrap_or_else(|| d.dep_path.clone());
(d.name.clone(), tail)
})
.collect()
})
.unwrap_or_default();
for (importer_path, direct_deps) in &canonical.importers {
let importer_scope: BTreeMap<String, String> = direct_deps
.iter()
.map(|d| {
let tail = d
.dep_path
.strip_prefix(&format!("{}@", d.name))
.map(|s| s.to_string())
.unwrap_or_else(|| d.dep_path.clone());
(d.name.clone(), tail)
})
.collect();
let mut new_deps = Vec::with_capacity(direct_deps.len());
for dep in direct_deps {
let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
let new_dep_path = visit_peer_context(
&dep.dep_path,
&canonical,
&importer_scope,
&root_scope,
&mut out_packages,
&mut visiting,
options,
)
.unwrap_or_else(|| dep.dep_path.clone());
new_deps.push(DirectDep {
name: dep.name.clone(),
dep_path: new_dep_path,
dep_type: dep.dep_type,
specifier: dep.specifier.clone(),
});
}
new_importers.insert(importer_path.clone(), new_deps);
}
LockfileGraph {
importers: new_importers,
packages: out_packages,
settings: canonical.settings,
overrides: canonical.overrides,
ignored_optional_dependencies: canonical.ignored_optional_dependencies,
times: canonical.times,
skipped_optional_dependencies: canonical.skipped_optional_dependencies,
catalogs: canonical.catalogs,
}
}
fn strip_hashed_peer_suffix(s: &str) -> &str {
const MARKER_LEN: usize = 11; if s.len() < MARKER_LEN {
return s;
}
let tail = &s[s.len() - MARKER_LEN..];
if !tail.starts_with('_') {
return s;
}
if tail[1..]
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
{
&s[..s.len() - MARKER_LEN]
} else {
s
}
}
fn hash_peer_suffix(suffix: &str) -> String {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(suffix.as_bytes());
let mut out = String::with_capacity(11);
out.push('_');
for byte in digest.iter().take(5) {
use std::fmt::Write;
let _ = write!(out, "{byte:02x}");
}
out
}
fn contains_canonical_back_ref(value: &str, canonical: &str) -> bool {
let bytes = value.as_bytes();
let target = canonical.as_bytes();
if target.is_empty() || target.len() > bytes.len() {
return false;
}
let mut i = 0;
while i + target.len() <= bytes.len() {
if &bytes[i..i + target.len()] == target {
let before = if i == 0 { b'\0' } else { bytes[i - 1] };
let after = bytes.get(i + target.len()).copied().unwrap_or(b'\0');
let before_ok = before == b'(';
let after_ok = after == b'(' || after == b')' || after == b'\0';
if before_ok && after_ok {
return true;
}
}
i += 1;
}
false
}
fn dedupe_peer_suffixes(graph: LockfileGraph) -> LockfileGraph {
let mut target_counts: BTreeMap<String, usize> = BTreeMap::new();
let mut intended: BTreeMap<String, String> = BTreeMap::new();
for key in graph.packages.keys() {
let new_key = apply_dedupe_peers_to_key(key);
*target_counts.entry(new_key.clone()).or_insert(0) += 1;
intended.insert(key.clone(), new_key);
}
let rewrite: BTreeMap<String, String> = intended
.into_iter()
.map(|(old, new)| {
if target_counts.get(&new).copied().unwrap_or(0) > 1 {
tracing::warn!(
"dedupe-peers: collision on {new} — keeping {old} in full form to avoid \
dropping a distinct peer-variant"
);
(old.clone(), old)
} else {
(old, new)
}
})
.collect();
let rewrite_tail = |child_name: &str, tail: &str| -> String {
let old_key = format!("{child_name}@{tail}");
match rewrite.get(&old_key) {
Some(new_key) => new_key
.strip_prefix(&format!("{child_name}@"))
.map(|s| s.to_string())
.unwrap_or_else(|| tail.to_string()),
None => apply_dedupe_peers_to_tail(tail),
}
};
let mut new_packages: BTreeMap<String, LockedPackage> = BTreeMap::new();
for (old_key, pkg) in graph.packages {
let new_key = rewrite
.get(&old_key)
.cloned()
.unwrap_or_else(|| old_key.clone());
let new_dependencies: BTreeMap<String, String> = pkg
.dependencies
.into_iter()
.map(|(n, v)| {
let new_v = rewrite_tail(&n, &v);
(n, new_v)
})
.collect();
new_packages.insert(
new_key.clone(),
LockedPackage {
name: pkg.name,
version: pkg.version,
integrity: pkg.integrity,
dependencies: new_dependencies,
peer_dependencies: pkg.peer_dependencies,
peer_dependencies_meta: pkg.peer_dependencies_meta,
dep_path: new_key,
local_source: pkg.local_source,
os: pkg.os,
cpu: pkg.cpu,
libc: pkg.libc,
bundled_dependencies: pkg.bundled_dependencies,
tarball_url: pkg.tarball_url,
},
);
}
let new_importers: BTreeMap<String, Vec<DirectDep>> = graph
.importers
.into_iter()
.map(|(path, deps)| {
let rewritten = deps
.into_iter()
.map(|d| {
let new_dep_path = rewrite
.get(&d.dep_path)
.cloned()
.unwrap_or_else(|| apply_dedupe_peers_to_key(&d.dep_path));
DirectDep {
name: d.name,
dep_path: new_dep_path,
dep_type: d.dep_type,
specifier: d.specifier,
}
})
.collect();
(path, rewritten)
})
.collect();
LockfileGraph {
importers: new_importers,
packages: new_packages,
settings: graph.settings,
overrides: graph.overrides,
ignored_optional_dependencies: graph.ignored_optional_dependencies,
times: graph.times,
skipped_optional_dependencies: graph.skipped_optional_dependencies,
catalogs: graph.catalogs,
}
}
fn apply_dedupe_peers_to_key(key: &str) -> String {
let mut parts = key.split('(');
let Some(first) = parts.next() else {
return key.to_string();
};
let mut out = String::with_capacity(key.len());
out.push_str(first);
for part in parts {
out.push('(');
if let Some(at_idx) = part.rfind('@') {
let close_idx = part.find([')', '(']).unwrap_or(usize::MAX);
if at_idx < close_idx {
out.push_str(&part[at_idx + 1..]);
continue;
}
}
out.push_str(part);
}
out
}
fn apply_dedupe_peers_to_tail(tail: &str) -> String {
apply_dedupe_peers_to_key(tail)
}
fn visit_peer_context(
input_dep_path: &str,
graph: &LockfileGraph,
ancestor_scope: &BTreeMap<String, String>,
root_scope: &BTreeMap<String, String>,
out_packages: &mut BTreeMap<String, LockedPackage>,
visiting: &mut std::collections::HashSet<String>,
options: &PeerContextOptions,
) -> Option<String> {
let pkg = graph.packages.get(input_dep_path)?;
let canonical_base = input_dep_path.split('(').next().unwrap_or(input_dep_path);
let canonical_base = strip_hashed_peer_suffix(canonical_base).to_string();
let mut peer_context: Vec<(String, String)> = Vec::new();
for (peer_name, declared_range) in &pkg.peer_dependencies {
let satisfies_declared = |v: &str| -> bool {
let canonical = v.split('(').next().unwrap_or(v);
version_satisfies(canonical, declared_range)
};
let from_ancestor = ancestor_scope
.get(peer_name)
.filter(|v| satisfies_declared(v))
.cloned();
let from_ancestor_incompatible = ancestor_scope.get(peer_name).cloned();
let from_pkg_deps = pkg
.dependencies
.get(peer_name)
.filter(|v| satisfies_declared(v))
.cloned();
let from_pkg_deps_incompatible = pkg.dependencies.get(peer_name).cloned();
let from_root = if options.resolve_from_workspace_root {
root_scope
.get(peer_name)
.filter(|v| satisfies_declared(v))
.cloned()
} else {
None
};
let from_root_incompatible = if options.resolve_from_workspace_root {
root_scope.get(peer_name).cloned()
} else {
None
};
let from_graph_scan = || {
graph
.packages
.values()
.filter(|p| p.name == *peer_name)
.filter(|p| version_satisfies(&p.version, declared_range))
.filter_map(|p| {
let tail = p
.dep_path
.strip_prefix(&format!("{}@", p.name))
.map(|s| s.to_string())
.unwrap_or_else(|| p.version.clone());
node_semver::Version::parse(&p.version)
.ok()
.map(|ver| (ver, tail))
})
.max_by(|a, b| a.0.cmp(&b.0))
.map(|(_, tail)| tail)
};
if let Some(version) = from_ancestor
.or(from_pkg_deps)
.or(from_root)
.or_else(from_graph_scan)
.or(from_ancestor_incompatible)
.or(from_pkg_deps_incompatible)
.or(from_root_incompatible)
{
peer_context.push((peer_name.clone(), version));
}
}
peer_context.sort_by(|a, b| a.0.cmp(&b.0));
let suffix: String = peer_context
.iter()
.map(|(n, v)| {
let cycles_back = contains_canonical_back_ref(v, &canonical_base);
let display_v = if cycles_back {
v.split('(').next().unwrap_or(v).to_string()
} else {
v.clone()
};
format!("({n}@{display_v})")
})
.collect();
let effective_suffix = if suffix.len() > options.peers_suffix_max_length {
hash_peer_suffix(&suffix)
} else {
suffix
};
let contextualized = format!("{canonical_base}{effective_suffix}");
if out_packages.contains_key(&contextualized) || visiting.contains(&contextualized) {
return Some(contextualized);
}
visiting.insert(contextualized.clone());
let mut child_scope = ancestor_scope.clone();
for (name, version) in &pkg.dependencies {
child_scope.insert(name.clone(), version.clone());
}
for (name, version) in &peer_context {
child_scope.insert(name.clone(), version.clone());
}
let peer_context_versions: BTreeMap<String, String> = peer_context.iter().cloned().collect();
let mut new_dependencies: BTreeMap<String, String> = BTreeMap::new();
let mut visited_dep_names: std::collections::HashSet<String> = std::collections::HashSet::new();
for (child_name, child_version_tail) in &pkg.dependencies {
let lookup_tail = match peer_context_versions.get(child_name) {
Some(v) => v.clone(),
None => child_version_tail.clone(),
};
let child_canonical_dep_path = format!("{child_name}@{lookup_tail}");
let child_new = visit_peer_context(
&child_canonical_dep_path,
graph,
&child_scope,
root_scope,
out_packages,
visiting,
options,
);
let new_tail = match child_new {
Some(new_dep_path) => new_dep_path
.strip_prefix(&format!("{child_name}@"))
.map(|s| s.to_string())
.unwrap_or_else(|| lookup_tail.clone()),
None => lookup_tail.clone(),
};
new_dependencies.insert(child_name.clone(), new_tail);
visited_dep_names.insert(child_name.clone());
}
for (peer_name, peer_version) in &peer_context {
if visited_dep_names.contains(peer_name) {
continue;
}
let child_canonical_dep_path = format!("{peer_name}@{peer_version}");
let child_new = visit_peer_context(
&child_canonical_dep_path,
graph,
&child_scope,
root_scope,
out_packages,
visiting,
options,
);
if let Some(new_dep_path) = child_new {
let new_tail = new_dep_path
.strip_prefix(&format!("{peer_name}@"))
.map(|s| s.to_string())
.unwrap_or_else(|| peer_version.clone());
new_dependencies.insert(peer_name.clone(), new_tail);
}
}
visiting.remove(&contextualized);
out_packages.insert(
contextualized.clone(),
LockedPackage {
name: pkg.name.clone(),
version: pkg.version.clone(),
integrity: pkg.integrity.clone(),
dependencies: new_dependencies,
peer_dependencies: pkg.peer_dependencies.clone(),
peer_dependencies_meta: pkg.peer_dependencies_meta.clone(),
dep_path: contextualized.clone(),
local_source: pkg.local_source.clone(),
os: pkg.os.clone(),
cpu: pkg.cpu.clone(),
libc: pkg.libc.clone(),
bundled_dependencies: pkg.bundled_dependencies.clone(),
tarball_url: pkg.tarball_url.clone(),
},
);
Some(contextualized)
}
#[derive(Debug)]
pub(crate) enum PickResult<'a> {
Found(&'a aube_registry::VersionMetadata),
NoMatch,
AgeGated,
}
#[cfg(test)]
impl<'a> PickResult<'a> {
fn unwrap(self) -> &'a aube_registry::VersionMetadata {
match self {
PickResult::Found(m) => m,
other => panic!("expected PickResult::Found, got {other:?}"),
}
}
}
fn pick_version<'a>(
packument: &'a Packument,
range_str: &str,
locked: Option<&str>,
pick_lowest: bool,
cutoff: Option<&str>,
strict: bool,
) -> PickResult<'a> {
let effective_range = if let Some(tagged_version) = packument.dist_tags.get(range_str) {
tagged_version.clone()
} else {
range_str.to_string()
};
let range = match node_semver::Range::parse(&effective_range) {
Ok(r) => r,
Err(_) => return PickResult::NoMatch,
};
let passes_cutoff = |ver: &str| -> bool {
let Some(c) = cutoff else { return true };
match packument.time.get(ver) {
Some(t) => t.as_str() <= c,
None => true,
}
};
if let Some(locked_ver) = locked
&& let Ok(v) = node_semver::Version::parse(locked_ver)
&& v.satisfies(&range)
&& passes_cutoff(locked_ver)
&& let Some(meta) = packument.versions.get(locked_ver)
{
return PickResult::Found(meta);
}
let mut had_satisfying_but_age_gated = false;
let mut versions: Vec<(&String, &aube_registry::VersionMetadata)> =
packument.versions.iter().collect();
versions.sort_by(|(a, _), (b, _)| {
let va = node_semver::Version::parse(a);
let vb = node_semver::Version::parse(b);
match (va, vb) {
(Ok(va), Ok(vb)) => {
if pick_lowest {
va.cmp(&vb)
} else {
vb.cmp(&va)
}
}
_ => std::cmp::Ordering::Equal,
}
});
for (ver_str, meta) in &versions {
if let Ok(v) = node_semver::Version::parse(ver_str)
&& v.satisfies(&range)
{
if passes_cutoff(ver_str) {
return PickResult::Found(meta);
}
had_satisfying_but_age_gated = true;
}
}
if strict || cutoff.is_none() {
return if had_satisfying_but_age_gated {
PickResult::AgeGated
} else {
PickResult::NoMatch
};
}
let mut ascending: Vec<(&String, &aube_registry::VersionMetadata)> =
packument.versions.iter().collect();
ascending.sort_by(|(a, _), (b, _)| {
let va = node_semver::Version::parse(a);
let vb = node_semver::Version::parse(b);
match (va, vb) {
(Ok(va), Ok(vb)) => va.cmp(&vb),
_ => std::cmp::Ordering::Equal,
}
});
for (ver_str, meta) in ascending {
if let Ok(v) = node_semver::Version::parse(ver_str)
&& v.satisfies(&range)
{
return PickResult::Found(meta);
}
}
PickResult::NoMatch
}
fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
use std::path::{Component, PathBuf};
let mut out = PathBuf::new();
for comp in path.components() {
match comp {
Component::ParentDir => {
let prev_is_normal = out
.components()
.next_back()
.is_some_and(|c| matches!(c, Component::Normal(_)));
if prev_is_normal {
out.pop();
} else {
out.push("..");
}
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
fn rebase_local(
local: &LocalSource,
importer_root: &std::path::Path,
project_root: &std::path::Path,
) -> LocalSource {
if importer_root == project_root {
return local.clone();
}
let Some(local_path) = local.path() else {
return local.clone();
};
let abs = normalize_path(&importer_root.join(local_path));
let rebased = pathdiff::diff_paths(&abs, project_root).map_or(abs, |p| normalize_path(&p));
match local {
LocalSource::Directory(_) => LocalSource::Directory(rebased),
LocalSource::Tarball(_) => LocalSource::Tarball(rebased),
LocalSource::Link(_) => LocalSource::Link(rebased),
LocalSource::Git(_) | LocalSource::RemoteTarball(_) => local.clone(),
}
}
#[cfg(test)]
mod rebase_local_tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn workspace_file_climbs_out_of_importer_to_root_sibling() {
let local = LocalSource::Directory(PathBuf::from("../../vendor-dir"));
let rebased = rebase_local(&local, Path::new("packages/app"), Path::new(""));
match rebased {
LocalSource::Directory(p) => assert_eq!(p, PathBuf::from("vendor-dir")),
other => panic!("expected Directory, got {other:?}"),
}
}
#[test]
fn two_importers_referencing_same_target_collide_on_dep_path() {
let a = rebase_local(
&LocalSource::Directory(PathBuf::from("../../vendor-dir")),
Path::new("packages/app"),
Path::new(""),
);
let b = rebase_local(
&LocalSource::Directory(PathBuf::from("../vendor-dir")),
Path::new("packages"),
Path::new(""),
);
assert_eq!(a.dep_path("vendor-dir"), b.dep_path("vendor-dir"));
}
#[test]
fn normalize_preserves_unresolvable_leading_parent() {
assert_eq!(
normalize_path(Path::new("../vendor")),
PathBuf::from("../vendor")
);
}
#[test]
fn dep_path_and_specifier_use_posix_separators() {
let win = LocalSource::Directory(PathBuf::from("vendor\\nested\\dir"));
let unix = LocalSource::Directory(PathBuf::from("vendor/nested/dir"));
assert_eq!(win.dep_path("foo"), unix.dep_path("foo"));
assert_eq!(win.specifier(), "file:vendor/nested/dir");
assert_eq!(unix.specifier(), "file:vendor/nested/dir");
}
}
fn read_tarball_package_json(bytes: &[u8]) -> Result<Vec<u8>, String> {
use std::io::Read;
let gz = flate2::read::GzDecoder::new(bytes);
let mut archive = tar::Archive::new(gz);
for entry in archive.entries().map_err(|e| e.to_string())? {
let mut entry = entry.map_err(|e| e.to_string())?;
let entry_path = entry.path().map_err(|e| e.to_string())?.to_path_buf();
if entry_path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == "package.json")
&& entry_path.components().count() == 2
{
let mut buf = Vec::new();
entry.read_to_end(&mut buf).map_err(|e| e.to_string())?;
return Ok(buf);
}
}
Err("tarball has no top-level package.json".to_string())
}
fn read_local_manifest(
local: &LocalSource,
importer_root: &std::path::Path,
) -> Result<(String, String, BTreeMap<String, String>), Error> {
let Some(local_path) = local.path() else {
return Err(Error::Registry(
local.specifier(),
"read_local_manifest called on non-path source".to_string(),
));
};
let path = importer_root.join(local_path);
let content = match local {
LocalSource::Directory(_) | LocalSource::Link(_) => {
std::fs::read(path.join("package.json"))
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?
}
LocalSource::Tarball(_) => {
let bytes = std::fs::read(&path)
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?;
read_tarball_package_json(&bytes).map_err(|e| Error::Registry(local.specifier(), e))?
}
LocalSource::Git(_) | LocalSource::RemoteTarball(_) => {
return Err(Error::Registry(
local.specifier(),
"read_local_manifest: remote source handled separately".to_string(),
));
}
};
let pj: aube_manifest::PackageJson = serde_json::from_slice(&content)
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?;
Ok((
pj.name.unwrap_or_default(),
pj.version.unwrap_or_else(|| "0.0.0".to_string()),
pj.dependencies,
))
}
fn dep_path_for(name: &str, version: &str) -> String {
format!("{name}@{version}")
}
fn is_non_registry_specifier(s: &str) -> bool {
if s.starts_with("link:") {
return true;
}
if aube_lockfile::LocalSource::looks_like_remote_tarball_url(s) {
return true;
}
if aube_lockfile::parse_git_spec(s).is_some() {
return true;
}
s.starts_with("file:")
}
async fn resolve_git_source(
name: &str,
git: &aube_lockfile::GitSource,
shallow: bool,
) -> Result<(LocalSource, String, BTreeMap<String, String>), Error> {
let url = git.url.clone();
let committish = git.committish.clone();
let name_owned = name.to_string();
let (local, version, deps) = tokio::task::spawn_blocking(move || -> Result<_, Error> {
let resolved = aube_store::git_resolve_ref(&url, committish.as_deref())
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let clone_dir = aube_store::git_shallow_clone(&url, &resolved, shallow)
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let manifest_bytes = std::fs::read(clone_dir.join("package.json")).map_err(|e| {
Error::Registry(
name_owned.clone(),
format!("read package.json in clone: {e}"),
)
})?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
let deps = pj.dependencies;
Ok((
LocalSource::Git(aube_lockfile::GitSource {
url,
committish,
resolved,
}),
version,
deps,
))
})
.await
.map_err(|e| Error::Registry(name.to_string(), format!("git task panicked: {e}")))??;
Ok((local, version, deps))
}
async fn resolve_remote_tarball(
name: &str,
tarball: &aube_lockfile::RemoteTarballSource,
client: &RegistryClient,
) -> Result<(LocalSource, String, BTreeMap<String, String>), Error> {
let bytes = client
.fetch_tarball_bytes(&tarball.url)
.await
.map_err(|e| Error::Registry(name.to_string(), format!("fetch {}: {e}", tarball.url)))?;
let name_owned = name.to_string();
let url = tarball.url.clone();
let (integrity, version, deps) = tokio::task::spawn_blocking(move || -> Result<_, Error> {
use sha2::{Digest, Sha512};
let mut hasher = Sha512::new();
hasher.update(&bytes);
let digest = hasher.finalize();
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(digest);
let integrity = format!("sha512-{b64}");
let manifest_bytes = read_tarball_package_json(&bytes)
.map_err(|e| Error::Registry(name_owned.clone(), format!("tarball {url}: {e}")))?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
Ok((integrity, version, pj.dependencies))
})
.await
.map_err(|e| Error::Registry(name.to_string(), format!("tarball task panicked: {e}")))??;
Ok((
LocalSource::RemoteTarball(aube_lockfile::RemoteTarballSource {
url: tarball.url.clone(),
integrity,
}),
version,
deps,
))
}
fn pick_override_spec(
rules: &[override_rule::OverrideRule],
task_name: &str,
task_range: &str,
ancestors: &[(String, String)],
) -> Option<String> {
let frames: Vec<override_rule::AncestorFrame<'_>> = ancestors
.iter()
.map(|(n, v)| override_rule::AncestorFrame {
name: n,
version: v,
})
.collect();
rules
.iter()
.filter(|r| override_rule::matches(r, task_name, task_range, &frames))
.max_by_key(|r| {
let named_parents = r.parents.iter().filter(|p| !p.is_wildcard()).count();
named_parents * 2 + usize::from(r.target.version_req.is_some())
})
.map(|r| r.replacement.clone())
}
fn version_satisfies(version: &str, range_str: &str) -> bool {
let Ok(v) = node_semver::Version::parse(version) else {
return false;
};
let Ok(r) = node_semver::Range::parse(range_str) else {
return false;
};
v.satisfies(&r)
}
fn apply_package_extensions(
pkg: &mut aube_registry::VersionMetadata,
extensions: &[PackageExtension],
) {
for extension in extensions {
if !package_selector_matches(&extension.selector, &pkg.name, &pkg.version) {
continue;
}
extend_missing(&mut pkg.dependencies, &extension.dependencies);
extend_missing(
&mut pkg.optional_dependencies,
&extension.optional_dependencies,
);
extend_missing(&mut pkg.peer_dependencies, &extension.peer_dependencies);
extend_missing(
&mut pkg.peer_dependencies_meta,
&extension.peer_dependencies_meta,
);
}
}
fn extend_missing<K, V>(target: &mut BTreeMap<K, V>, additions: &BTreeMap<K, V>)
where
K: Ord + Clone,
V: Clone,
{
for (key, value) in additions {
target.entry(key.clone()).or_insert_with(|| value.clone());
}
}
fn package_selector_matches(selector: &str, name: &str, version: &str) -> bool {
let selector = selector.trim();
if selector == name {
return true;
}
let Some((selector_name, range)) = split_package_selector(selector) else {
return false;
};
selector_name == name && version_satisfies(version, range)
}
fn split_package_selector(selector: &str) -> Option<(&str, &str)> {
let at = selector.rfind('@')?;
if at == 0 {
return None;
}
if selector.starts_with('@') {
let slash = selector.find('/')?;
if at <= slash {
return None;
}
}
let (name, range) = selector.split_at(at);
let range = &range[1..];
(!name.is_empty() && !range.is_empty()).then_some((name, range))
}
#[cfg(test)]
fn is_deprecation_allowed(name: &str, version: &str, allowed: &BTreeMap<String, String>) -> bool {
allowed
.get(name)
.is_some_and(|range| version_satisfies(version, range))
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("no version of {0} matches range {1}")]
NoMatch(String, String),
#[error(
"no version of {name} matching {range} is older than {minutes} minute(s) (minimumReleaseAgeStrict=true)"
)]
AgeGate {
name: String,
range: String,
minutes: u64,
},
#[error("registry error for {0}: {1}")]
Registry(String, String),
#[error(
"{name}: catalog reference `{spec}` does not resolve — catalog `{catalog}` has no entry for `{name}`"
)]
UnknownCatalogRef {
name: String,
spec: String,
catalog: String,
},
#[error(
"blocked exotic transitive dependency {name}@{spec} from {parent} (blockExoticSubdeps=true)"
)]
BlockedExoticSubdep {
name: String,
spec: String,
parent: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
use aube_registry::{Dist, Packument, VersionMetadata};
#[test]
fn test_version_satisfies() {
assert!(version_satisfies("4.17.21", "^4.17.0"));
assert!(version_satisfies("4.17.21", "^4.0.0"));
assert!(!version_satisfies("3.10.0", "^4.0.0"));
assert!(version_satisfies("1.0.0", ">=1.0.0"));
assert!(version_satisfies("2.0.0", ">=1.0.0 <3.0.0"));
}
#[test]
fn test_version_satisfies_exact() {
assert!(version_satisfies("1.0.0", "1.0.0"));
assert!(!version_satisfies("1.0.1", "1.0.0"));
}
#[test]
fn test_version_satisfies_tilde() {
assert!(version_satisfies("1.2.3", "~1.2.0"));
assert!(version_satisfies("1.2.9", "~1.2.0"));
assert!(!version_satisfies("1.3.0", "~1.2.0"));
}
#[test]
fn test_version_satisfies_star() {
assert!(version_satisfies("1.0.0", "*"));
assert!(version_satisfies("99.99.99", "*"));
}
#[test]
fn test_version_satisfies_invalid() {
assert!(!version_satisfies("notaversion", "^1.0.0"));
assert!(!version_satisfies("1.0.0", "notarange"));
}
#[test]
fn dependency_policy_default_blocks_exotic_subdeps() {
assert!(DependencyPolicy::default().block_exotic_subdeps);
}
#[test]
fn package_extension_selector_matches_scoped_and_versioned_names() {
assert!(package_selector_matches(
"@scope/pkg@^1",
"@scope/pkg",
"1.2.3"
));
assert!(package_selector_matches("plain", "plain", "9.0.0"));
assert!(!package_selector_matches(
"@scope/pkg@^2",
"@scope/pkg",
"1.2.3"
));
}
#[test]
fn package_extensions_merge_dependency_maps() {
let mut pkg = make_version("host", "1.0.0");
let extension = PackageExtension {
selector: "host@1".to_string(),
dependencies: [("missing".to_string(), "^2.0.0".to_string())]
.into_iter()
.collect(),
optional_dependencies: BTreeMap::new(),
peer_dependencies: [("peer".to_string(), "^3.0.0".to_string())]
.into_iter()
.collect(),
peer_dependencies_meta: [(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
)]
.into_iter()
.collect(),
};
apply_package_extensions(&mut pkg, &[extension]);
assert_eq!(pkg.dependencies.get("missing").unwrap(), "^2.0.0");
assert_eq!(pkg.peer_dependencies.get("peer").unwrap(), "^3.0.0");
assert!(pkg.peer_dependencies_meta.get("peer").unwrap().optional);
}
#[test]
fn package_extensions_do_not_overwrite_existing_dependency_maps() {
let mut pkg = make_version("host", "1.0.0");
pkg.dependencies
.insert("dep".to_string(), "^1.0.0".to_string());
pkg.optional_dependencies
.insert("optional".to_string(), "^2.0.0".to_string());
pkg.peer_dependencies
.insert("peer".to_string(), "^3.0.0".to_string());
pkg.peer_dependencies_meta.insert(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: false },
);
let extension = PackageExtension {
selector: "host".to_string(),
dependencies: [
("dep".to_string(), "^9.0.0".to_string()),
("missing".to_string(), "^4.0.0".to_string()),
]
.into_iter()
.collect(),
optional_dependencies: [
("optional".to_string(), "^9.0.0".to_string()),
("missing-optional".to_string(), "^5.0.0".to_string()),
]
.into_iter()
.collect(),
peer_dependencies: [
("peer".to_string(), "^9.0.0".to_string()),
("missing-peer".to_string(), "^6.0.0".to_string()),
]
.into_iter()
.collect(),
peer_dependencies_meta: [
(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
),
(
"missing-peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
),
]
.into_iter()
.collect(),
};
apply_package_extensions(&mut pkg, &[extension]);
assert_eq!(pkg.dependencies.get("dep").unwrap(), "^1.0.0");
assert_eq!(pkg.dependencies.get("missing").unwrap(), "^4.0.0");
assert_eq!(pkg.optional_dependencies.get("optional").unwrap(), "^2.0.0");
assert_eq!(
pkg.optional_dependencies.get("missing-optional").unwrap(),
"^5.0.0"
);
assert_eq!(pkg.peer_dependencies.get("peer").unwrap(), "^3.0.0");
assert_eq!(pkg.peer_dependencies.get("missing-peer").unwrap(), "^6.0.0");
assert!(!pkg.peer_dependencies_meta.get("peer").unwrap().optional);
assert!(
pkg.peer_dependencies_meta
.get("missing-peer")
.unwrap()
.optional
);
}
#[test]
fn allowed_deprecated_versions_match_package_ranges() {
let allowed = [("old".to_string(), "<2".to_string())]
.into_iter()
.collect();
assert!(is_deprecation_allowed("old", "1.9.0", &allowed));
assert!(!is_deprecation_allowed("old", "2.0.0", &allowed));
assert!(!is_deprecation_allowed("other", "1.0.0", &allowed));
}
#[test]
fn test_dep_path_for() {
assert_eq!(dep_path_for("lodash", "4.17.21"), "lodash@4.17.21");
assert_eq!(dep_path_for("@babel/core", "7.24.0"), "@babel/core@7.24.0");
}
fn make_version(name: &str, version: &str) -> VersionMetadata {
VersionMetadata {
name: name.to_string(),
version: version.to_string(),
dependencies: BTreeMap::new(),
dev_dependencies: BTreeMap::new(),
peer_dependencies: BTreeMap::new(),
peer_dependencies_meta: BTreeMap::new(),
optional_dependencies: BTreeMap::new(),
bundled_dependencies: None,
dist: Some(Dist {
tarball: format!("https://registry.npmjs.org/{name}/-/{name}-{version}.tgz"),
integrity: Some(format!("sha512-fake-{name}-{version}")),
shasum: None,
}),
os: vec![],
cpu: vec![],
libc: vec![],
has_install_script: false,
deprecated: None,
}
}
fn make_packument(name: &str, versions: &[&str], latest: &str) -> Packument {
let mut ver_map = BTreeMap::new();
for v in versions {
ver_map.insert(v.to_string(), make_version(name, v));
}
let mut dist_tags = BTreeMap::new();
dist_tags.insert("latest".to_string(), latest.to_string());
Packument {
name: name.to_string(),
versions: ver_map,
dist_tags,
time: BTreeMap::new(),
}
}
#[test]
fn test_pick_version_highest_match() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.2.0");
}
#[test]
fn test_pick_version_exact() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let result = pick_version(&packument, "1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_no_match() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let result = pick_version(&packument, "^2.0.0", None, false, None, false);
assert!(matches!(result, PickResult::NoMatch));
}
#[test]
fn test_pick_version_strict_distinguishes_age_gate_from_no_match() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::AgeGated));
let result = pick_version(&packument, "^9.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::NoMatch));
}
#[test]
fn test_pick_version_prefers_locked() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
let result = pick_version(&packument, "^1.0.0", Some("1.1.0"), false, None, false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_locked_out_of_range() {
let packument = make_packument("foo", &["1.0.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^2.0.0", Some("1.0.0"), false, None, false).unwrap();
assert_eq!(result.version, "2.0.0");
}
#[test]
fn test_pick_version_dist_tag() {
let packument = make_packument("foo", &["1.0.0", "2.0.0-beta.1"], "1.0.0");
let result = pick_version(&packument, "latest", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_lowest_picks_smallest_satisfying() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^1.0.0", None, true, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_cutoff_filters_future_versions() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
packument
.time
.insert("1.0.0".into(), "2020-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2021-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.2.0".into(), "2023-01-01T00:00:00.000Z".into());
let cutoff = "2022-06-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_lenient_falls_back_to_lowest_when_cutoff_excludes_all() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
packument
.time
.insert("1.2.0".into(), "2025-01-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_strict_returns_age_gated_when_cutoff_excludes_all() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::AgeGated));
}
#[test]
fn test_minimum_release_age_cutoff_format() {
let mra = MinimumReleaseAge {
minutes: 60,
..Default::default()
};
let cutoff = mra.cutoff().expect("non-zero minutes produces a cutoff");
assert_eq!(cutoff.len(), 24, "ISO-8601 with millis is 24 chars");
assert!(cutoff.ends_with("Z"));
assert_eq!(&cutoff[4..5], "-");
assert_eq!(&cutoff[10..11], "T");
}
#[test]
fn test_minimum_release_age_zero_disables() {
let mra = MinimumReleaseAge::default();
assert!(mra.cutoff().is_none());
}
#[test]
fn test_format_iso8601_known_epoch() {
assert_eq!(
format_iso8601_utc(1_704_067_200),
"2024-01-01T00:00:00.000Z"
);
assert_eq!(format_iso8601_utc(0), "1970-01-01T00:00:00.000Z");
}
#[test]
fn test_pick_version_cutoff_allows_missing_time_entries() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let cutoff = "2000-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_with_deps() {
let mut packument = make_packument("foo", &["1.0.0"], "1.0.0");
packument
.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("bar".to_string(), "^2.0.0".to_string());
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.dependencies.get("bar").unwrap(), "^2.0.0");
}
fn mk_locked(
name: &str,
version: &str,
deps: &[(&str, &str)],
peer_deps: &[(&str, &str)],
) -> LockedPackage {
let mut dependencies = BTreeMap::new();
for (n, v) in deps {
dependencies.insert((*n).to_string(), (*v).to_string());
}
let mut peer_dependencies = BTreeMap::new();
for (n, r) in peer_deps {
peer_dependencies.insert((*n).to_string(), (*r).to_string());
}
LockedPackage {
name: name.to_string(),
version: version.to_string(),
integrity: None,
dependencies,
peer_dependencies,
peer_dependencies_meta: BTreeMap::new(),
dep_path: format!("{name}@{version}"),
..Default::default()
}
}
fn graph_has_package(graph: &LockfileGraph, name: &str, version: &str) -> bool {
graph
.packages
.values()
.any(|pkg| pkg.name == name && pkg.version == version)
}
#[test]
fn apply_peer_contexts_handles_mutual_peer_cycle() {
let a = mk_locked("a", "1.0.0", &[("b", "1.0.0")], &[("b", "^1")]);
let b = mk_locked("b", "1.0.0", &[("a", "1.0.0")], &[("a", "^1")]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@1.0.0".to_string(), b);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let canonical = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(canonical, &PeerContextOptions::default());
let a_key = "a@1.0.0(b@1.0.0)";
let b_key = "b@1.0.0(a@1.0.0)";
assert!(
out.packages.contains_key(a_key),
"expected {a_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key(b_key),
"expected {b_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
let root = out.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].dep_path, a_key);
}
#[test]
fn apply_peer_contexts_produces_nested_peer_suffixes() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default());
assert!(
out.packages.contains_key("adapter@1.0.0(core@1.0.0)"),
"expected nested adapter variant: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let consumer_key = "consumer@1.0.0(adapter@1.0.0(core@1.0.0))(core@1.0.0)";
assert!(
out.packages.contains_key(consumer_key),
"expected nested consumer key {consumer_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
}
#[test]
fn apply_peer_contexts_per_range_satisfaction() {
let mut consumer17 = mk_locked(
"consumer17",
"1.0.0",
&[("react", "17.0.2")],
&[("react", "^17")],
);
consumer17.dep_path = "consumer17@1.0.0".to_string();
let mut consumer18 = mk_locked(
"consumer18",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^18")],
);
consumer18.dep_path = "consumer18@1.0.0".to_string();
let react17 = mk_locked("react", "17.0.2", &[], &[]);
let react18 = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer17@1.0.0".to_string(), consumer17);
packages.insert("consumer18@1.0.0".to_string(), consumer18);
packages.insert("react@17.0.2".to_string(), react17);
packages.insert("react@18.2.0".to_string(), react18);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer17".to_string(),
dep_path: "consumer17@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "consumer18".to_string(),
dep_path: "consumer18@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("^17".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default());
assert!(
out.packages.contains_key("consumer17@1.0.0(react@17.0.2)"),
"consumer17 must pick react@17: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key("consumer18@1.0.0(react@18.2.0)"),
"consumer18 must fall back to react@18: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("consumer18@1.0.0(react@17.0.2)"),
"consumer18 was incorrectly pinned to react@17: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
}
#[test]
fn from_graph_scan_returns_full_dep_path_tail() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("helper", "^1")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut helper = mk_locked("helper", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
helper.dep_path = "helper@1.0.0(core@1.0.0)".to_string();
let mut core = mk_locked("core", "1.0.0", &[], &[]);
core.dep_path = "core@1.0.0".to_string();
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("helper@1.0.0(core@1.0.0)".to_string(), helper);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default());
assert!(
out.packages
.contains_key("consumer@1.0.0(helper@1.0.0(core@1.0.0))"),
"consumer must reference helper's contextualized tail: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let consumer_out = out
.packages
.get("consumer@1.0.0(helper@1.0.0(core@1.0.0))")
.unwrap();
let helper_tail = consumer_out
.dependencies
.get("helper")
.expect("consumer must wire helper as a dep");
assert_eq!(helper_tail, "1.0.0(core@1.0.0)");
let helper_key = format!("helper@{helper_tail}");
assert!(
out.packages.contains_key(&helper_key),
"consumer.dependencies[helper] must resolve to an existing package key"
);
}
#[test]
fn dedupe_peer_dependents_merges_equivalent_subtrees() {
let mut consumer_a = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.0.0")],
&[("react", "^18")],
);
consumer_a.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert(
"consumer@1.0.0(react@18.0.0)".to_string(),
LockedPackage {
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dependencies: {
let mut m = BTreeMap::new();
m.insert("react".to_string(), "18.0.0".to_string());
m
},
..consumer_a.clone()
},
);
let mut variant = consumer_a.clone();
variant.dep_path = "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string();
variant
.dependencies
.insert("react".to_string(), "18.0.0".to_string());
packages.insert(
"consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
variant,
);
packages.insert("react@18.0.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = dedupe_peer_variants(graph);
let consumer_keys: Vec<_> = out
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.collect();
assert_eq!(
consumer_keys.len(),
1,
"expected single canonical consumer variant after dedupe, got: {:?}",
consumer_keys
);
assert_eq!(
consumer_keys[0], "consumer@1.0.0(react@18.0.0)",
"canonical should be lex-smallest key"
);
let root = out.importers.get(".").unwrap();
assert_eq!(root[0].dep_path, "consumer@1.0.0(react@18.0.0)");
}
#[test]
fn dedupe_peer_dependents_disabled_keeps_variants() {
let consumer_a = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.0.0")],
&[("react", "^18")],
);
let react = mk_locked("react", "18.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert(
"consumer@1.0.0(react@18.0.0)".to_string(),
LockedPackage {
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dependencies: {
let mut m = BTreeMap::new();
m.insert("react".to_string(), "18.0.0".to_string());
m
},
..consumer_a.clone()
},
);
let mut variant = consumer_a.clone();
variant.dep_path = "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string();
variant
.dependencies
.insert("react".to_string(), "18.0.0".to_string());
packages.insert(
"consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
variant,
);
packages.insert("react@18.0.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let consumer_keys_off: Vec<_> = graph
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys_off.len(),
2,
"expected both variants to survive with dedupe_peer_dependents=false, got: {:?}",
consumer_keys_off
);
let merged = dedupe_peer_variants(graph);
let consumer_keys_on: Vec<_> = merged
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys_on.len(),
1,
"expected single canonical variant with dedupe_peer_dependents=true, got: {:?}",
consumer_keys_on
);
}
#[test]
fn dedupe_peers_suffix_is_version_only() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@18.2.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options);
assert!(
out.packages.contains_key("consumer@1.0.0(18.2.0)"),
"expected version-only suffix: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("consumer@1.0.0(react@18.2.0)"),
"name-based suffix should not appear under dedupe-peers=true"
);
}
#[test]
fn resolve_peers_from_workspace_root_prefers_root() {
let build_graph = || {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("react", ">=17")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let react17 = mk_locked("react", "17.0.2", &[], &[]);
let react18 = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@17.0.2".to_string(), react17);
packages.insert("react@18.2.0".to_string(), react18);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("^17".to_string()),
}],
);
importers.insert(
"packages/app".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
LockfileGraph {
importers,
packages,
..Default::default()
}
};
let options_on = PeerContextOptions {
resolve_from_workspace_root: true,
..PeerContextOptions::default()
};
let out_on = apply_peer_contexts(build_graph(), &options_on);
assert!(
out_on.packages.contains_key("consumer@1.0.0(react@17.0.2)"),
"with flag on, consumer should resolve peer from workspace root (17.0.2): {:?}",
out_on.packages.keys().collect::<Vec<_>>()
);
let options_off = PeerContextOptions {
resolve_from_workspace_root: false,
..PeerContextOptions::default()
};
let out_off = apply_peer_contexts(build_graph(), &options_off);
assert!(
out_off
.packages
.contains_key("consumer@1.0.0(react@18.2.0)"),
"with flag off, consumer should fall through to graph-wide scan (18.2.0): {:?}",
out_off.packages.keys().collect::<Vec<_>>()
);
}
#[test]
fn dedupe_peers_cycle_break_still_converges() {
let a = mk_locked("a", "1.0.0", &[("b", "1.0.0")], &[("b", "^1")]);
let b = mk_locked("b", "1.0.0", &[("a", "1.0.0")], &[("a", "^1")]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@1.0.0".to_string(), b);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let canonical = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(canonical, &options);
let a_key = "a@1.0.0(1.0.0)";
let b_key = "b@1.0.0(1.0.0)";
assert!(
out.packages.contains_key(a_key),
"expected {a_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key(b_key),
"expected {b_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
}
#[test]
fn dedupe_peers_no_false_positive_on_version_collision() {
let a = mk_locked("a", "1.0.0", &[("b", "2.0.0")], &[("b", "^2")]);
let b = mk_locked("b", "2.0.0", &[("c", "1.0.0")], &[("c", "^1")]);
let c = mk_locked("c", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@2.0.0".to_string(), b);
packages.insert("c@1.0.0".to_string(), c);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options);
assert!(
out.packages.contains_key("a@1.0.0(2.0.0(1.0.0))"),
"expected A's key to preserve B's nested peer chain: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("a@1.0.0(2.0.0)"),
"false-positive cycle break would produce the truncated form"
);
}
#[test]
fn apply_dedupe_peers_to_key_strips_names_in_suffix() {
assert_eq!(
apply_dedupe_peers_to_key("react-dom@18.2.0(react@18.2.0)"),
"react-dom@18.2.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(b@2.0.0(c@3.0.0))"),
"a@1.0.0(2.0.0(3.0.0))"
);
assert_eq!(apply_dedupe_peers_to_key("react@18.2.0"), "react@18.2.0");
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(18.2.0)"),
"a@1.0.0(18.2.0)"
);
}
#[test]
fn dedupe_peer_suffixes_preserves_full_form_on_name_collision() {
let consumer_foo = {
let mut pkg = mk_locked("consumer", "1.0.0", &[("foo", "1.0.0")], &[("foo", "^1")]);
pkg.dep_path = "consumer@1.0.0(foo@1.0.0)".to_string();
pkg
};
let consumer_bar = {
let mut pkg = mk_locked("consumer", "1.0.0", &[("bar", "1.0.0")], &[("bar", "^1")]);
pkg.dep_path = "consumer@1.0.0(bar@1.0.0)".to_string();
pkg
};
let foo = mk_locked("foo", "1.0.0", &[], &[]);
let bar = mk_locked("bar", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0(foo@1.0.0)".to_string(), consumer_foo);
packages.insert("consumer@1.0.0(bar@1.0.0)".to_string(), consumer_bar);
packages.insert("foo@1.0.0".to_string(), foo);
packages.insert("bar@1.0.0".to_string(), bar);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(foo@1.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(bar@1.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = dedupe_peer_suffixes(graph);
let consumer_keys: BTreeSet<_> = out
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys.len(),
2,
"both consumer variants must survive collision: {consumer_keys:?}"
);
assert!(consumer_keys.contains("consumer@1.0.0(foo@1.0.0)"));
assert!(consumer_keys.contains("consumer@1.0.0(bar@1.0.0)"));
let importer_keys: BTreeSet<_> = out
.importers
.get(".")
.unwrap()
.iter()
.map(|d| d.dep_path.clone())
.collect();
assert!(importer_keys.contains("consumer@1.0.0(foo@1.0.0)"));
assert!(importer_keys.contains("consumer@1.0.0(bar@1.0.0)"));
}
#[test]
fn apply_dedupe_peers_to_key_handles_scoped_packages() {
assert_eq!(
apply_dedupe_peers_to_key("consumer@1.0.0(@types/react@18.2.0)"),
"consumer@1.0.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("@foo/bar@1.0.0(@types/react@18.2.0)"),
"@foo/bar@1.0.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(@types/react@18.2.0(@babel/core@7.0.0))"),
"a@1.0.0(18.2.0(7.0.0))"
);
}
#[test]
fn contains_canonical_back_ref_respects_boundaries() {
assert!(contains_canonical_back_ref("1.0.0(a@1.0.0)", "a@1.0.0"));
assert!(contains_canonical_back_ref(
"1.0.0(a@1.0.0(b@1.0.0))",
"a@1.0.0"
));
assert!(!contains_canonical_back_ref("1.0.0(a@1.0.5)", "a@1.0"));
assert!(!contains_canonical_back_ref("1.0.0", "a@1.0.0"));
}
#[test]
fn hoist_auto_installed_peers_hoists_unmet_peers_to_importer() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^17 || ^18")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@18.2.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
assert_eq!(root[0].name, "consumer");
assert_eq!(root[1].name, "react");
assert_eq!(root[1].dep_path, "react@18.2.0");
assert_eq!(root[1].dep_type, DepType::Production);
assert_eq!(root[1].specifier.as_deref(), Some("^17 || ^18"));
}
#[test]
fn hoist_auto_installed_peers_leaves_already_satisfied_peers_alone() {
let consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "17.0.2")],
&[("react", "^17 || ^18")],
);
let react = mk_locked("react", "17.0.2", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@17.0.2".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("17.0.2".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
let react_dep = root.iter().find(|d| d.name == "react").unwrap();
assert_eq!(react_dep.specifier.as_deref(), Some("17.0.2"));
}
#[test]
fn detect_unmet_peers_flags_version_mismatch() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "15.7.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0(react@15.7.0)".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
let unmet = detect_unmet_peers(&graph);
assert_eq!(unmet.len(), 1, "expected one unmet peer, got {unmet:?}");
let u = &unmet[0];
assert_eq!(u.from_name, "consumer");
assert_eq!(u.peer_name, "react");
assert_eq!(u.declared, "^18");
assert_eq!(u.found.as_deref(), Some("15.7.0"));
}
#[test]
fn detect_unmet_peers_silent_when_satisfied() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^17 || ^18")],
);
consumer.dep_path = "consumer@1.0.0(react@18.2.0)".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
assert!(detect_unmet_peers(&graph).is_empty());
}
#[test]
fn detect_unmet_peers_flags_completely_missing_peer() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("react", "^18")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
let unmet = detect_unmet_peers(&graph);
assert_eq!(unmet.len(), 1);
let u = &unmet[0];
assert_eq!(u.from_name, "consumer");
assert_eq!(u.peer_name, "react");
assert_eq!(u.declared, "^18");
assert_eq!(u.found, None);
}
#[test]
fn detect_unmet_peers_skips_optional_peers() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "15.7.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0(react@15.7.0)".to_string();
consumer.peer_dependencies_meta.insert(
"react".to_string(),
aube_lockfile::PeerDepMeta { optional: true },
);
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
assert!(detect_unmet_peers(&graph).is_empty());
}
#[tokio::test]
async fn resolve_terminates_on_dependency_cycle() {
let mut a = make_packument("cycle-a", &["1.0.0"], "1.0.0");
a.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("cycle-b".to_string(), "1.0.0".to_string());
let mut b = make_packument("cycle-b", &["1.0.0"], "1.0.0");
b.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("cycle-a".to_string(), "1.0.0".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("cycle-a".to_string(), a);
resolver.cache.insert("cycle-b".to_string(), b);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("cycle-a".to_string(), "1.0.0".to_string());
let graph = tokio::time::timeout(
std::time::Duration::from_secs(5),
resolver.resolve(&manifest, None),
)
.await
.expect("resolver hung on dependency cycle")
.expect("resolve failed");
assert!(graph.packages.contains_key("cycle-a@1.0.0"));
assert!(graph.packages.contains_key("cycle-b@1.0.0"));
assert_eq!(
graph.packages["cycle-a@1.0.0"].dependencies.get("cycle-b"),
Some(&"1.0.0".to_string())
);
assert_eq!(
graph.packages["cycle-b@1.0.0"].dependencies.get("cycle-a"),
Some(&"1.0.0".to_string())
);
}
#[tokio::test]
async fn auto_install_peers_installs_missing_required_peer() {
let mut consumer = make_packument("consumer", &["1.0.0"], "1.0.0");
consumer
.versions
.get_mut("1.0.0")
.unwrap()
.peer_dependencies
.insert("react".to_string(), "^18".to_string());
let react = make_packument("react", &["18.2.0"], "18.2.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("consumer".to_string(), consumer);
resolver.cache.insert("react".to_string(), react);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("consumer".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "consumer", "1.0.0"));
assert!(
graph_has_package(&graph, "react", "18.2.0"),
"missing required peer should be auto-installed"
);
}
#[tokio::test]
async fn auto_install_peers_uses_importer_declared_peer_name_without_extra_version() {
let mut plugin = make_packument("plugin", &["1.0.0"], "1.0.0");
plugin
.versions
.get_mut("1.0.0")
.unwrap()
.peer_dependencies
.insert("eslint".to_string(), "^8.56.0".to_string());
let eslint = make_packument("eslint", &["8.57.1", "9.0.0"], "9.0.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("plugin".to_string(), plugin);
resolver.cache.insert("eslint".to_string(), eslint);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("eslint".to_string(), "^9".to_string());
manifest
.dependencies
.insert("plugin".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "eslint", "9.0.0"));
assert!(graph_has_package(&graph, "plugin", "1.0.0"));
assert!(
!graph_has_package(&graph, "eslint", "8.57.1"),
"importer-declared peer name should not pull a second compatible peer tree"
);
let unmet = detect_unmet_peers(&graph);
assert!(
unmet.iter().any(|unmet| unmet.from_name == "plugin"
&& unmet.peer_name == "eslint"
&& unmet.declared == "^8.56.0"
&& unmet.found.as_deref() == Some("9.0.0")),
"incompatible importer peer should surface as a version-mismatch warning"
);
}
#[tokio::test]
async fn auto_install_peers_skips_unrequested_optional_peer_alternatives() {
let mut loader = make_packument("loader", &["1.0.0"], "1.0.0");
let loader_meta = loader.versions.get_mut("1.0.0").unwrap();
loader_meta
.peer_dependencies
.insert("sass".to_string(), "^1".to_string());
loader_meta
.peer_dependencies
.insert("webpack".to_string(), "^5".to_string());
loader_meta
.peer_dependencies
.insert("@rspack/core".to_string(), "^1".to_string());
loader_meta
.peer_dependencies
.insert("node-sass".to_string(), "^9".to_string());
loader_meta.peer_dependencies_meta.insert(
"@rspack/core".to_string(),
aube_registry::PeerDepMeta { optional: true },
);
loader_meta.peer_dependencies_meta.insert(
"node-sass".to_string(),
aube_registry::PeerDepMeta { optional: true },
);
let sass = make_packument("sass", &["1.69.0"], "1.69.0");
let webpack = make_packument("webpack", &["5.0.0"], "5.0.0");
let rspack = make_packument("@rspack/core", &["1.0.0"], "1.0.0");
let node_sass = make_packument("node-sass", &["9.0.0"], "9.0.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("loader".to_string(), loader);
resolver.cache.insert("sass".to_string(), sass);
resolver.cache.insert("webpack".to_string(), webpack);
resolver.cache.insert("@rspack/core".to_string(), rspack);
resolver.cache.insert("node-sass".to_string(), node_sass);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("loader".to_string(), "1.0.0".to_string());
manifest
.dependencies
.insert("sass".to_string(), "^1".to_string());
manifest
.dependencies
.insert("webpack".to_string(), "^5".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "loader", "1.0.0"));
assert!(graph_has_package(&graph, "sass", "1.69.0"));
assert!(graph_has_package(&graph, "webpack", "5.0.0"));
assert!(
!graph_has_package(&graph, "@rspack/core", "1.0.0"),
"optional peer alternative should not be auto-installed"
);
assert!(
!graph_has_package(&graph, "node-sass", "9.0.0"),
"optional peer alternative should not be auto-installed"
);
}
#[tokio::test]
async fn resolve_handles_lockfile_reused_name_with_incompatible_transitive_range() {
let dep_a = make_packument("dep-a", &["1.0.0", "2.0.0"], "2.0.0");
let mut other_a = make_packument("other-a", &["2.0.0"], "2.0.0");
other_a
.versions
.get_mut("2.0.0")
.unwrap()
.dependencies
.insert("dep-a".to_string(), "^2".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("dep-a".to_string(), dep_a);
resolver.cache.insert("other-a".to_string(), other_a);
let mut existing_pkgs: BTreeMap<String, LockedPackage> = BTreeMap::new();
existing_pkgs.insert(
"dep-a@1.0.0".to_string(),
LockedPackage {
name: "dep-a".to_string(),
version: "1.0.0".to_string(),
dep_path: "dep-a@1.0.0".to_string(),
..Default::default()
},
);
let existing = LockfileGraph {
packages: existing_pkgs,
importers: BTreeMap::new(),
settings: Default::default(),
overrides: BTreeMap::new(),
ignored_optional_dependencies: BTreeSet::new(),
times: BTreeMap::new(),
skipped_optional_dependencies: BTreeMap::new(),
catalogs: BTreeMap::new(),
};
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("dep-a".to_string(), "^1".to_string());
manifest
.dependencies
.insert("other-a".to_string(), "^2".to_string());
let graph = tokio::time::timeout(
std::time::Duration::from_secs(5),
resolver.resolve(&manifest, Some(&existing)),
)
.await
.expect("resolver hung")
.expect("resolve failed");
assert!(
graph.packages.contains_key("dep-a@1.0.0"),
"dep-a@1.0.0 missing (lockfile reuse)"
);
assert!(
graph.packages.contains_key("dep-a@2.0.0"),
"dep-a@2.0.0 missing (transitive fetch fell through the ensure_fetch guard)"
);
assert!(graph.packages.contains_key("other-a@2.0.0"));
}
#[test]
fn hash_peer_suffix_matches_expected_format() {
let out = hash_peer_suffix("(react@18.2.0)");
assert!(out.starts_with('_'), "expected `_` prefix: {out:?}");
assert_eq!(out.len(), 11, "expected `_` + 10 hex chars: {out:?}");
assert!(
out[1..]
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"expected lowercase hex after `_`: {out:?}"
);
assert_eq!(hash_peer_suffix("(react@18.2.0)"), out);
}
#[test]
fn peer_suffix_is_hashed_when_exceeding_cap() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
peers_suffix_max_length: 10,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options);
let consumer_key = out
.packages
.keys()
.find(|k| k.starts_with("consumer@1.0.0"))
.cloned()
.expect("consumer@1.0.0 variant missing");
let suffix = consumer_key.strip_prefix("consumer@1.0.0").unwrap();
assert!(
suffix.starts_with('_') && suffix.len() == 11,
"expected hashed suffix _<10-hex>, got {suffix:?} from {consumer_key:?}"
);
}
#[test]
fn peer_suffix_unchanged_when_within_cap() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default());
assert!(
out.packages
.contains_key("consumer@1.0.0(adapter@1.0.0(core@1.0.0))(core@1.0.0)"),
"default cap corrupted output: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
}
}