1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use dashmap::DashMap;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_npm::npm_rc::NpmRc;
use deno_semver::package::PackageNv;
use deno_semver::Version;
use serde::Deserialize;
use std::sync::Arc;
use crate::args::npm_registry_url;
use crate::file_fetcher::FileFetcher;
use crate::npm::NpmFetchResolver;
use super::search::PackageSearchApi;
#[derive(Debug)]
pub struct CliNpmSearchApi {
file_fetcher: Arc<FileFetcher>,
resolver: NpmFetchResolver,
search_cache: DashMap<String, Arc<Vec<String>>>,
versions_cache: DashMap<String, Arc<Vec<Version>>>,
}
impl CliNpmSearchApi {
pub fn new(file_fetcher: Arc<FileFetcher>) -> Self {
let resolver = NpmFetchResolver::new(
file_fetcher.clone(),
Arc::new(NpmRc::default().as_resolved(npm_registry_url()).unwrap()),
);
Self {
file_fetcher,
resolver,
search_cache: Default::default(),
versions_cache: Default::default(),
}
}
pub fn clear_cache(&self) {
self.file_fetcher.clear_memory_files();
self.search_cache.clear();
self.versions_cache.clear();
}
}
#[async_trait::async_trait]
impl PackageSearchApi for CliNpmSearchApi {
async fn search(&self, query: &str) -> Result<Arc<Vec<String>>, AnyError> {
if let Some(names) = self.search_cache.get(query) {
return Ok(names.clone());
}
let mut search_url = npm_registry_url().join("-/v1/search")?;
search_url
.query_pairs_mut()
.append_pair("text", &format!("{} boost-exact:false", query));
let file_fetcher = self.file_fetcher.clone();
let file = deno_core::unsync::spawn(async move {
file_fetcher
.fetch_bypass_permissions(&search_url)
.await?
.into_text_decoded()
})
.await??;
let names = Arc::new(parse_npm_search_response(&file.source)?);
self.search_cache.insert(query.to_string(), names.clone());
Ok(names)
}
async fn versions(&self, name: &str) -> Result<Arc<Vec<Version>>, AnyError> {
if let Some(versions) = self.versions_cache.get(name) {
return Ok(versions.clone());
}
let info = self
.resolver
.package_info(name)
.await
.ok_or_else(|| anyhow!("npm package info not found: {}", name))?;
let mut versions = info.versions.keys().cloned().collect::<Vec<_>>();
versions.sort();
versions.reverse();
let versions = Arc::new(versions);
self
.versions_cache
.insert(name.to_string(), versions.clone());
Ok(versions)
}
async fn exports(
&self,
_nv: &PackageNv,
) -> Result<Arc<Vec<String>>, AnyError> {
Ok(Default::default())
}
}
fn parse_npm_search_response(source: &str) -> Result<Vec<String>, AnyError> {
#[derive(Debug, Deserialize)]
struct Package {
name: String,
}
#[derive(Debug, Deserialize)]
struct Object {
package: Package,
}
#[derive(Debug, Deserialize)]
struct Response {
objects: Vec<Object>,
}
let objects = serde_json::from_str::<Response>(source)?.objects;
Ok(objects.into_iter().map(|o| o.package.name).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_npm_search_response() {
// This is a subset of a realistic response only containing data currently
// used by our parser. It's enough to catch regressions.
let names = parse_npm_search_response(r#"{"objects":[{"package":{"name":"puppeteer"}},{"package":{"name":"puppeteer-core"}},{"package":{"name":"puppeteer-extra-plugin-stealth"}},{"package":{"name":"puppeteer-extra-plugin"}}]}"#).unwrap();
assert_eq!(
names,
vec![
"puppeteer".to_string(),
"puppeteer-core".to_string(),
"puppeteer-extra-plugin-stealth".to_string(),
"puppeteer-extra-plugin".to_string()
]
);
}
}
|