Skip to main content

oxi/storage/packages/
npm.rs

1//! npm registry client for the package system.
2//!
3//! `NpmPackageInfo` fetches a package's manifest (versions + dist-tags)
4//! from `registry.npmjs.org`, exposes the latest version via
5//! `latest_version`, and supports best-match semver resolution against
6//! the version map. The standalone `get_latest_npm_version` helper is
7//! kept for the package manager's quick update check.
8
9use crate::util::http_client::shared_http_client;
10use anyhow::{Context, Result, bail};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14/// Information fetched from the npm registry
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct NpmPackageInfo {
17    /// pub.
18    pub name: String,
19    /// pub.
20    pub versions: BTreeMap<String, serde_json::Value>,
21    /// pub.
22    #[serde(rename = "dist-tags")]
23    pub dist_tags: BTreeMap<String, String>,
24}
25
26impl NpmPackageInfo {
27    /// Fetch package info from the npm registry
28    pub async fn fetch(name: &str) -> Result<Self> {
29        let url = format!("https://registry.npmjs.org/{}", name);
30        let client = shared_http_client();
31
32        let resp = client
33            .get(&url)
34            .header("Accept", "application/json")
35            .send()
36            .await
37            .with_context(|| format!("Failed to fetch npm info for '{}'", name))?;
38
39        if !resp.status().is_success() {
40            bail!("npm registry returned {} for '{}'", resp.status(), name);
41        }
42
43        let info: NpmPackageInfo = resp
44            .json()
45            .await
46            .with_context(|| format!("Failed to parse npm registry response for '{}'", name))?;
47
48        Ok(info)
49    }
50
51    /// Get the latest version from dist-tags
52    pub fn latest_version(&self) -> Option<&str> {
53        self.dist_tags.get("latest").map(|s| s.as_str())
54    }
55
56    /// Find the best matching version for a constraint
57    pub fn resolve_version(&self, constraint: &str) -> Option<String> {
58        if constraint == "latest" || constraint.is_empty() {
59            return self.latest_version().map(|s| s.to_string());
60        }
61
62        // Try exact match first
63        if self.versions.contains_key(constraint) {
64            return Some(constraint.to_string());
65        }
66
67        // Try semver range matching
68        if let Ok(req) = semver::VersionReq::parse(constraint) {
69            let mut best: Option<semver::Version> = None;
70            for ver_str in self.versions.keys() {
71                if let Ok(ver) = semver::Version::parse(ver_str)
72                    && req.matches(&ver)
73                {
74                    match &best {
75                        Some(b) if ver > *b => best = Some(ver),
76                        None => best = Some(ver),
77                        _ => {}
78                    }
79                }
80            }
81            if let Some(v) = best {
82                return Some(v.to_string());
83            }
84        }
85
86        None
87    }
88}
89
90/// Get the latest version of an npm package
91pub async fn get_latest_npm_version(name: &str) -> Result<String> {
92    let info = NpmPackageInfo::fetch(name).await?;
93    info.latest_version()
94        .map(|s| s.to_string())
95        .context(format!("No latest version found for '{}'", name))
96}