1use crate::types::{NpmPackage, NpmVersion};
10use deps_core::{DepsError, HttpCache, Result};
11use serde::Deserialize;
12use std::any::Any;
13use std::sync::Arc;
14
15const REGISTRY_BASE: &str = "https://registry.npmjs.org";
16
17pub const NPMJS_URL: &str = "https://www.npmjs.com/package";
19
20pub fn package_url(name: &str) -> String {
24 format!("{}/{}", NPMJS_URL, urlencoding::encode(name))
25}
26
27#[derive(Clone)]
32pub struct NpmRegistry {
33 cache: Arc<HttpCache>,
34}
35
36impl NpmRegistry {
37 pub const fn new(cache: Arc<HttpCache>) -> Self {
39 Self { cache }
40 }
41
42 pub async fn get_versions(&self, name: &str) -> Result<Vec<NpmVersion>> {
70 let url = format!("{REGISTRY_BASE}/{name}");
71 let data = self.cache.get_cached(&url).await?;
72
73 parse_package_metadata(&data)
74 }
75
76 pub async fn get_latest_matching(
102 &self,
103 name: &str,
104 req_str: &str,
105 ) -> Result<Option<NpmVersion>> {
106 let versions = self.get_versions(name).await?;
107
108 let req = node_semver::Range::parse(req_str)
110 .map_err(|e| DepsError::InvalidVersionReq(e.to_string()))?;
111
112 Ok(versions.into_iter().find(|v| {
113 let version = node_semver::Version::parse(&v.version).ok();
114 version.is_some_and(|ver| req.satisfies(&ver) && !v.deprecated)
115 }))
116 }
117
118 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<NpmPackage>> {
144 let url = format!(
145 "{}/-/v1/search?text={}&size={}",
146 REGISTRY_BASE,
147 urlencoding::encode(query),
148 limit
149 );
150
151 let data = self.cache.get_cached(&url).await?;
152 parse_search_response(&data)
153 }
154}
155
156#[derive(Deserialize)]
158struct PackageMetadata {
159 versions: std::collections::HashMap<String, VersionMetadata>,
160}
161
162#[derive(Deserialize)]
164struct VersionMetadata {
165 #[serde(default)]
166 deprecated: Option<String>,
167}
168
169fn parse_package_metadata(data: &[u8]) -> Result<Vec<NpmVersion>> {
171 let metadata: PackageMetadata = serde_json::from_slice(data)?;
172
173 let mut versions_with_parsed: Vec<(NpmVersion, node_semver::Version)> = metadata
175 .versions
176 .into_iter()
177 .filter_map(|(version, meta)| {
178 let parsed = node_semver::Version::parse(&version).ok()?;
179 Some((
180 NpmVersion {
181 version,
182 deprecated: meta.deprecated.is_some(),
183 },
184 parsed,
185 ))
186 })
187 .collect();
188
189 versions_with_parsed.sort_unstable_by(|a, b| b.1.cmp(&a.1));
191
192 Ok(versions_with_parsed.into_iter().map(|(v, _)| v).collect())
194}
195
196#[derive(Deserialize)]
198struct SearchResponse {
199 objects: Vec<SearchObject>,
200}
201
202#[derive(Deserialize)]
204struct SearchObject {
205 package: SearchPackage,
206}
207
208#[derive(Deserialize)]
210struct SearchPackage {
211 name: String,
212 #[serde(default)]
213 description: Option<String>,
214 #[serde(default)]
215 links: Option<PackageLinks>,
216 version: String,
217}
218
219#[derive(Deserialize)]
221struct PackageLinks {
222 #[serde(default)]
223 homepage: Option<String>,
224 #[serde(default)]
225 repository: Option<String>,
226}
227
228fn parse_search_response(data: &[u8]) -> Result<Vec<NpmPackage>> {
230 let response: SearchResponse = serde_json::from_slice(data)?;
231
232 Ok(response
233 .objects
234 .into_iter()
235 .map(|obj| {
236 let pkg = obj.package;
237 NpmPackage {
238 name: pkg.name,
239 description: pkg.description,
240 homepage: pkg.links.as_ref().and_then(|l| l.homepage.clone()),
241 repository: pkg.links.as_ref().and_then(|l| l.repository.clone()),
242 latest_version: pkg.version,
243 }
244 })
245 .collect())
246}
247
248#[async_trait::async_trait]
250impl deps_core::PackageRegistry for NpmRegistry {
251 type Version = NpmVersion;
252 type Metadata = NpmPackage;
253 type VersionReq = node_semver::Range;
254
255 async fn get_versions(&self, name: &str) -> Result<Vec<Self::Version>> {
256 self.get_versions(name).await
257 }
258
259 async fn get_latest_matching(
260 &self,
261 name: &str,
262 req: &Self::VersionReq,
263 ) -> Result<Option<Self::Version>> {
264 self.get_latest_matching(name, &req.to_string()).await
265 }
266
267 async fn search(&self, query: &str, limit: usize) -> Result<Vec<Self::Metadata>> {
268 self.search(query, limit).await
269 }
270}
271
272#[async_trait::async_trait]
274impl deps_core::Registry for NpmRegistry {
275 async fn get_versions(&self, name: &str) -> Result<Vec<Box<dyn deps_core::Version>>> {
276 let versions = self.get_versions(name).await?;
277 Ok(versions
278 .into_iter()
279 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
280 .collect())
281 }
282
283 async fn get_latest_matching(
284 &self,
285 name: &str,
286 req: &str,
287 ) -> Result<Option<Box<dyn deps_core::Version>>> {
288 let version = self.get_latest_matching(name, req).await?;
289 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
290 }
291
292 async fn search(&self, query: &str, limit: usize) -> Result<Vec<Box<dyn deps_core::Metadata>>> {
293 let packages = self.search(query, limit).await?;
294 Ok(packages
295 .into_iter()
296 .map(|p| Box::new(p) as Box<dyn deps_core::Metadata>)
297 .collect())
298 }
299
300 fn package_url(&self, name: &str) -> String {
301 package_url(name)
302 }
303
304 fn as_any(&self) -> &dyn Any {
305 self
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_parse_package_metadata() {
315 let json = r#"{
316 "versions": {
317 "1.0.0": {},
318 "1.0.1": {"deprecated": "Use 1.0.2 instead"},
319 "1.0.2": {}
320 },
321 "dist-tags": {
322 "latest": "1.0.2"
323 }
324}"#;
325
326 let versions = parse_package_metadata(json.as_bytes()).unwrap();
327 assert_eq!(versions.len(), 3);
328
329 assert_eq!(versions[0].version, "1.0.2");
331 assert!(!versions[0].deprecated);
332
333 assert_eq!(versions[1].version, "1.0.1");
334 assert!(versions[1].deprecated);
335
336 assert_eq!(versions[2].version, "1.0.0");
337 assert!(!versions[2].deprecated);
338 }
339
340 #[test]
341 fn test_parse_search_response() {
342 let json = r#"{
343 "objects": [
344 {
345 "package": {
346 "name": "express",
347 "description": "Fast, unopinionated web framework",
348 "version": "4.18.2",
349 "links": {
350 "homepage": "http://expressjs.com/",
351 "repository": "https://github.com/expressjs/express"
352 }
353 }
354 }
355 ]
356}"#;
357
358 let packages = parse_search_response(json.as_bytes()).unwrap();
359 assert_eq!(packages.len(), 1);
360
361 let pkg = &packages[0];
362 assert_eq!(pkg.name, "express");
363 assert_eq!(
364 pkg.description,
365 Some("Fast, unopinionated web framework".into())
366 );
367 assert_eq!(pkg.latest_version, "4.18.2");
368 assert_eq!(pkg.homepage, Some("http://expressjs.com/".into()));
369 }
370
371 #[test]
372 fn test_parse_search_response_minimal() {
373 let json = r#"{
374 "objects": [
375 {
376 "package": {
377 "name": "minimal-pkg",
378 "version": "1.0.0"
379 }
380 }
381 ]
382}"#;
383
384 let packages = parse_search_response(json.as_bytes()).unwrap();
385 assert_eq!(packages.len(), 1);
386 assert_eq!(packages[0].name, "minimal-pkg");
387 assert_eq!(packages[0].description, None);
388 }
389
390 #[tokio::test]
391 #[ignore]
392 async fn test_fetch_real_express_versions() {
393 let cache = Arc::new(HttpCache::new());
394 let registry = NpmRegistry::new(cache);
395 let versions = registry.get_versions("express").await.unwrap();
396
397 assert!(!versions.is_empty());
398 assert!(versions.iter().any(|v| v.version.starts_with("4.")));
399 }
400
401 #[tokio::test]
402 #[ignore]
403 async fn test_search_real() {
404 let cache = Arc::new(HttpCache::new());
405 let registry = NpmRegistry::new(cache);
406 let results = registry.search("express", 5).await.unwrap();
407
408 assert!(!results.is_empty());
409 assert!(results.iter().any(|r| r.name == "express"));
410 }
411
412 #[tokio::test]
413 #[ignore]
414 async fn test_get_latest_matching_real() {
415 let cache = Arc::new(HttpCache::new());
416 let registry = NpmRegistry::new(cache);
417 let latest = registry
418 .get_latest_matching("express", "^4.0.0")
419 .await
420 .unwrap();
421
422 assert!(latest.is_some());
423 let version = latest.unwrap();
424 assert!(version.version.starts_with("4."));
425 assert!(!version.deprecated);
426 }
427}