oxi/storage/packages/
npm.rs1use crate::util::http_client::shared_http_client;
10use anyhow::{Context, Result, bail};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct NpmPackageInfo {
17 pub name: String,
19 pub versions: BTreeMap<String, serde_json::Value>,
21 #[serde(rename = "dist-tags")]
23 pub dist_tags: BTreeMap<String, String>,
24}
25
26impl NpmPackageInfo {
27 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 pub fn latest_version(&self) -> Option<&str> {
53 self.dist_tags.get("latest").map(|s| s.as_str())
54 }
55
56 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 if self.versions.contains_key(constraint) {
64 return Some(constraint.to_string());
65 }
66
67 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
90pub 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}