use aube_registry::Packument;
#[derive(Debug)]
pub enum PickResult<'a> {
Found(&'a aube_registry::VersionMetadata),
NoMatch,
AgeGated,
}
#[cfg(test)]
impl<'a> PickResult<'a> {
pub(crate) fn unwrap(self) -> &'a aube_registry::VersionMetadata {
match self {
PickResult::Found(m) => m,
other => panic!("expected PickResult::Found, got {other:?}"),
}
}
}
pub fn pick_version_for_add<'a>(
packument: &'a Packument,
registry_name: &str,
range: &str,
minimum_release_age: Option<&crate::MinimumReleaseAge>,
) -> PickResult<'a> {
let cutoff = minimum_release_age.and_then(|m| m.cutoff());
let range = registry_alias_range(range);
let range = if range == "latest" && cutoff.is_some() {
"*"
} else {
range
};
let strict = minimum_release_age.is_some_and(|m| m.strict);
let exclude = minimum_release_age.map(|m| &m.exclude);
let is_age_exempt = |ver: &str, parsed: Option<&node_semver::Version>| {
exclude.is_some_and(|ex| match parsed {
Some(v) => ex.matches(registry_name, v),
None => match node_semver::Version::parse(ver) {
Ok(v) => ex.matches(registry_name, &v),
Err(_) => ex.matches_name_only(registry_name),
},
})
};
pick_version(
packument,
range,
None,
false,
cutoff.as_deref(),
None,
strict,
is_age_exempt,
)
}
fn registry_alias_range(range: &str) -> &str {
if let Some(rest) = range.strip_prefix("npm:") {
match rest.rfind('@') {
Some(at) if at > 0 => &rest[at + 1..],
_ => "latest",
}
} else {
range
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub(crate) fn pick_version<'a>(
packument: &'a Packument,
range_str: &str,
locked: Option<&str>,
pick_lowest: bool,
cutoff: Option<&str>,
exempt_cutoff: Option<&str>,
strict: bool,
is_age_exempt: impl Fn(&str, Option<&node_semver::Version>) -> bool,
) -> PickResult<'a> {
let range = match node_semver::Range::parse(normalize_range(range_str)) {
Ok(r) => r,
Err(_) => {
if looks_like_protocol_range(range_str) {
return PickResult::NoMatch;
}
let effective_range = if let Some(tagged_version) = packument.dist_tags.get(range_str) {
tagged_version.clone()
} else if range_str == "latest" {
match highest_stable_version(packument) {
Some(v) => v,
None => return PickResult::NoMatch,
}
} else {
return PickResult::NoMatch;
};
match node_semver::Range::parse(normalize_range(&effective_range)) {
Ok(r) => r,
Err(_) => return PickResult::NoMatch,
}
}
};
let passes_effective_cutoff = |ver: &str, effective: Option<&str>| -> bool {
let Some(c) = effective else { return true };
match packument.time.get(ver) {
Some(t) => t.as_str() <= c,
None => true,
}
};
let passes_cutoff = |ver: &str, parsed: Option<&node_semver::Version>| -> bool {
let effective = if is_age_exempt(ver, parsed) {
exempt_cutoff
} else {
cutoff
};
passes_effective_cutoff(ver, effective)
};
if let Some(locked_ver) = locked
&& let Ok(v) = node_semver::Version::parse(locked_ver)
&& v.satisfies(&range)
&& passes_cutoff(locked_ver, Some(&v))
&& let Some(meta) = packument.versions.get(locked_ver)
{
return PickResult::Found(meta);
}
if !pick_lowest
&& let Some(latest_ver) = packument.dist_tags.get("latest")
&& let Ok(v) = node_semver::Version::parse(latest_ver)
&& v.satisfies(&range)
&& passes_cutoff(latest_ver, Some(&v))
&& let Some(meta) = packument.versions.get(latest_ver)
{
return PickResult::Found(meta);
}
let mut had_satisfying_but_age_gated = false;
let mut best: Option<(node_semver::Version, &'a aube_registry::VersionMetadata)> = None;
let mut fallback_lowest: Option<(node_semver::Version, &'a aube_registry::VersionMetadata)> =
None;
for (ver_str, meta) in &packument.versions {
let Ok(v) = node_semver::Version::parse(ver_str) else {
continue;
};
if !v.satisfies(&range) {
continue;
}
if passes_effective_cutoff(ver_str, exempt_cutoff)
&& outranks(&v, meta, fallback_lowest.as_ref(), true)
{
fallback_lowest = Some((v.clone(), meta));
}
if passes_cutoff(ver_str, Some(&v)) {
if outranks(&v, meta, best.as_ref(), pick_lowest) {
best = Some((v, meta));
}
} else {
had_satisfying_but_age_gated = true;
}
}
if let Some((_, meta)) = best {
return PickResult::Found(meta);
}
if strict || cutoff.is_none() {
return if had_satisfying_but_age_gated {
PickResult::AgeGated
} else {
PickResult::NoMatch
};
}
if let Some((_, meta)) = fallback_lowest {
return PickResult::Found(meta);
}
if had_satisfying_but_age_gated {
PickResult::AgeGated
} else {
PickResult::NoMatch
}
}
#[inline]
pub(crate) fn outranks(
v: &node_semver::Version,
meta: &aube_registry::VersionMetadata,
incumbent: Option<&(node_semver::Version, &aube_registry::VersionMetadata)>,
lowest: bool,
) -> bool {
let Some((cur_v, cur_meta)) = incumbent else {
return true;
};
let live = meta.deprecated.is_none();
if live != cur_meta.deprecated.is_none() {
return live;
}
if lowest { v < cur_v } else { v > cur_v }
}
#[inline]
fn looks_like_protocol_range(range_str: &str) -> bool {
let Some(idx) = range_str.find(':') else {
return false;
};
let prefix = range_str[..idx].to_ascii_lowercase();
matches!(
prefix.as_str(),
"workspace"
| "catalog"
| "npm"
| "jsr"
| "file"
| "link"
| "git"
| "git+ssh"
| "git+http"
| "git+https"
| "git+file"
| "ssh"
| "http"
| "https"
| "github"
| "gitlab"
| "bitbucket"
| "gist"
)
}
#[inline]
pub(crate) fn highest_stable_version(packument: &Packument) -> Option<String> {
let mut best: Option<(node_semver::Version, String)> = None;
for key in packument.versions.keys() {
let Ok(v) = node_semver::Version::parse(key) else {
continue;
};
if !v.pre_release.is_empty() {
continue;
}
match &best {
None => best = Some((v, key.clone())),
Some((cur, _)) if v > *cur => best = Some((v, key.clone())),
_ => {}
}
}
best.map(|(_, k)| k)
}
#[inline]
pub(crate) fn strip_alias_prefix(range: &str) -> &str {
for prefix in ["npm:", "jsr:"] {
if let Some(rest) = range.strip_prefix(prefix) {
return match rest.rfind('@') {
Some(at) if at > 0 => &rest[at + 1..],
_ => rest,
};
}
}
range
}
#[inline]
pub(crate) fn version_satisfies(version: &str, range_str: &str) -> bool {
with_cached_version(version, |v| {
let Some(v) = v else { return false };
with_cached_range(normalize_range(range_str), |r| match r {
Some(r) => v.satisfies(r),
None => false,
})
})
}
pub(crate) fn normalize_range(range_str: &str) -> &str {
if range_str.trim().is_empty() {
"*"
} else {
range_str
}
}
fn with_cached_range<R>(range_str: &str, f: impl FnOnce(Option<&node_semver::Range>) -> R) -> R {
thread_local! {
static CACHE: std::cell::RefCell<crate::FxHashMap<String, Option<node_semver::Range>>> =
std::cell::RefCell::default();
}
CACHE.with(|cell| {
let mut map = cell.borrow_mut();
if !map.contains_key(range_str) {
let parsed = node_semver::Range::parse(range_str).ok();
map.insert(range_str.to_string(), parsed);
}
f(map.get(range_str).and_then(Option::as_ref))
})
}
fn with_cached_version<R>(version: &str, f: impl FnOnce(Option<&node_semver::Version>) -> R) -> R {
thread_local! {
static CACHE: std::cell::RefCell<crate::FxHashMap<String, Option<node_semver::Version>>> =
std::cell::RefCell::default();
}
CACHE.with(|cell| {
let mut map = cell.borrow_mut();
if !map.contains_key(version) {
let parsed = node_semver::Version::parse(version).ok();
map.insert(version.to_string(), parsed);
}
f(map.get(version).and_then(Option::as_ref))
})
}