use aube_registry::Packument;
#[derive(Debug)]
pub(crate) 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:?}"),
}
}
}
#[inline]
pub(crate) fn pick_version<'a>(
packument: &'a Packument,
range_str: &str,
locked: Option<&str>,
pick_lowest: bool,
cutoff: Option<&str>,
strict: 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_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);
}
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)
&& 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 fallback_lowest.as_ref().is_none_or(|(cur, _)| v < *cur) {
fallback_lowest = Some((v.clone(), meta));
}
if passes_cutoff(ver_str) {
let replace = best
.as_ref()
.is_none_or(|(cur, _)| if pick_lowest { v < *cur } else { v > *cur });
if replace {
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);
}
PickResult::NoMatch
}
#[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 {
let Ok(v) = node_semver::Version::parse(version) 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<rustc_hash::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))
})
}