use std::path::{Path, PathBuf};
use std::sync::LazyLock as Lazy;
use aube_registry::NetworkMode;
use aube_registry::client::RegistryClient;
use aube_registry::config::NpmConfig;
use eyre::Result;
use crate::backend::VersionInfo;
use crate::backend::npm::is_semver_prerelease;
use crate::config::Settings;
static CLIENT: Lazy<RegistryClient> = Lazy::new(|| {
crate::backend::aube_host::init();
let config = NpmConfig::load(&meta_dir());
let settings = Settings::get();
let mode = if settings.offline() {
NetworkMode::Offline
} else if settings.prefer_offline() {
NetworkMode::PreferOffline
} else {
NetworkMode::Online
};
let _ = crate::file::create_dir_all(meta_dir());
RegistryClient::from_config(config).with_network_mode(mode)
});
fn meta_dir() -> PathBuf {
crate::dirs::CACHE.join("npm-meta")
}
async fn fetch_packument(name: &str) -> Result<aube_registry::Packument> {
Ok(CLIENT
.fetch_packument_with_time_cached(name, &meta_dir())
.await?)
}
fn all_versions_deprecated(packument: &aube_registry::Packument) -> bool {
packument
.versions
.values()
.all(|metadata| metadata.deprecated.is_some())
}
pub(crate) async fn list_versions(name: &str) -> Result<Vec<VersionInfo>> {
let packument = fetch_packument(name).await?;
let all_versions_deprecated = all_versions_deprecated(&packument);
let versions = packument.versions.iter().filter_map(|(version, metadata)| {
(all_versions_deprecated || metadata.deprecated.is_none()).then_some(version)
});
Ok(sort_versions(versions)
.into_iter()
.map(|version| VersionInfo {
version: version.clone(),
created_at: packument.time.get(version).cloned(),
prerelease: Some(is_semver_prerelease(version)),
..Default::default()
})
.collect())
}
fn sort_versions<'a>(versions: impl Iterator<Item = &'a String>) -> Vec<&'a String> {
let mut versions = versions
.map(|version| (version, semver::Version::parse(version).ok()))
.collect::<Vec<_>>();
versions.sort_by(|(_, a), (_, b)| match (a, b) {
(Some(a), Some(b)) => a.cmp_precedence(b),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
versions.into_iter().map(|(version, _)| version).collect()
}
pub(crate) async fn latest_dist_tag(name: &str) -> Result<Option<String>> {
let packument = fetch_packument(name).await?;
let all_versions_deprecated = all_versions_deprecated(&packument);
Ok(packument
.dist_tags
.get("latest")
.filter(|version| {
all_versions_deprecated
|| packument
.versions
.get(*version)
.is_none_or(|metadata| metadata.deprecated.is_none())
})
.cloned())
}
pub(crate) async fn download_tarball(name: &str, version: &str, path: &Path) -> Result<()> {
let metadata = CLIENT.fetch_single_version_metadata(name, version).await?;
let dist = metadata
.dist
.ok_or_else(|| eyre::eyre!("npm package {name}@{version} has no dist metadata"))?;
let bytes = CLIENT.fetch_tarball_bytes(&dist.tarball).await?;
crate::file::write(path, bytes)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::sort_versions;
#[test]
fn sort_versions_supports_npm_safe_integer_components() {
let versions = [
"0.52.0",
"0.0.8999999999999999",
"0.1.0",
"0.0.8888888888",
"0.0.899999999",
"0.0.9007199254740991",
]
.map(String::from);
let sorted = sort_versions(versions.iter())
.into_iter()
.map(String::as_str)
.collect::<Vec<_>>();
assert_eq!(
sorted,
[
"0.0.899999999",
"0.0.8888888888",
"0.0.8999999999999999",
"0.0.9007199254740991",
"0.1.0",
"0.52.0",
]
);
}
}