diff --git a/cli/args/mod.rs b/cli/args/mod.rs index a4904d39dc..af681104ce 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -75,7 +75,7 @@ use deno_config::FmtConfig; use deno_config::LintConfig; use deno_config::TestConfig; -pub fn npm_registry_default_url() -> &'static Url { +pub fn npm_registry_url() -> &'static Url { static NPM_REGISTRY_DEFAULT_URL: Lazy = Lazy::new(|| { let env_var_name = "NPM_CONFIG_REGISTRY"; if let Ok(registry_url) = std::env::var(env_var_name) { diff --git a/cli/factory.rs b/cli/factory.rs index eb025a5585..5bd5fe149f 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -443,7 +443,7 @@ impl CliFactory { self.package_json_deps_provider().clone(), ), npm_system_info: self.options.npm_system_info(), - npm_registry_url: crate::args::npm_registry_default_url().to_owned(), + npm_registry_url: crate::args::npm_registry_url().to_owned(), }) }).await }.boxed_local()) diff --git a/cli/jsr.rs b/cli/jsr.rs index d741e6ace1..1f030ce70b 100644 --- a/cli/jsr.rs +++ b/cli/jsr.rs @@ -226,7 +226,7 @@ impl JsrFetchResolver { if let Some(info) = self.info_by_name.get(name) { return info.value().clone(); } - let read_cached_package_info = || async { + let fetch_package_info = || async { let meta_url = jsr_url().join(&format!("{}/meta.json", name)).ok()?; let file = self .file_fetcher @@ -235,7 +235,7 @@ impl JsrFetchResolver { .ok()?; serde_json::from_slice::(&file.source).ok() }; - let info = read_cached_package_info().await.map(Arc::new); + let info = fetch_package_info().await.map(Arc::new); self.info_by_name.insert(name.to_string(), info.clone()); info } @@ -247,7 +247,7 @@ impl JsrFetchResolver { if let Some(info) = self.info_by_nv.get(nv) { return info.value().clone(); } - let read_cached_package_version_info = || async { + let fetch_package_version_info = || async { let meta_url = jsr_url() .join(&format!("{}/{}_meta.json", &nv.name, &nv.version)) .ok()?; @@ -258,7 +258,7 @@ impl JsrFetchResolver { .ok()?; partial_jsr_package_version_info_from_slice(&file.source).ok() }; - let info = read_cached_package_version_info().await.map(Arc::new); + let info = fetch_package_version_info().await.map(Arc::new); self.info_by_nv.insert(nv.clone(), info.clone()); info } diff --git a/cli/lsp/jsr.rs b/cli/lsp/jsr.rs index 29ecec60bd..a9c35aad6c 100644 --- a/cli/lsp/jsr.rs +++ b/cli/lsp/jsr.rs @@ -15,20 +15,18 @@ use std::sync::Arc; use super::search::PackageSearchApi; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct CliJsrSearchApi { file_fetcher: FileFetcher, - /// We only store this here so the completion system has access to a resolver - /// that always uses the global cache. - resolver: Arc, - search_cache: Arc>>>, - versions_cache: Arc>>>, - exports_cache: Arc>>>, + resolver: JsrFetchResolver, + search_cache: DashMap>>, + versions_cache: DashMap>>, + exports_cache: DashMap>>, } impl CliJsrSearchApi { pub fn new(file_fetcher: FileFetcher) -> Self { - let resolver = Arc::new(JsrFetchResolver::new(file_fetcher.clone())); + let resolver = JsrFetchResolver::new(file_fetcher.clone()); Self { file_fetcher, resolver, @@ -38,7 +36,7 @@ impl CliJsrSearchApi { } } - pub fn get_resolver(&self) -> &Arc { + pub fn get_resolver(&self) -> &JsrFetchResolver { &self.resolver } } @@ -49,12 +47,7 @@ impl PackageSearchApi for CliJsrSearchApi { if let Some(names) = self.search_cache.get(query) { return Ok(names.clone()); } - let mut search_url = jsr_api_url().clone(); - search_url - .path_segments_mut() - .map_err(|_| anyhow!("Custom jsr URL cannot be a base."))? - .pop_if_empty() - .push("packages"); + let mut search_url = jsr_api_url().join("packages")?; search_url.query_pairs_mut().append_pair("query", query); let file = self .file_fetcher diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index c22752e9ed..3555a05452 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -875,9 +875,8 @@ impl Inner { None, ); deps_file_fetcher.set_download_log_level(super::logging::lsp_log_level()); - self.jsr_search_api = CliJsrSearchApi::new(deps_file_fetcher); - self.npm.search_api = - CliNpmSearchApi::new(self.module_registries.file_fetcher.clone()); + self.jsr_search_api = CliJsrSearchApi::new(deps_file_fetcher.clone()); + self.npm.search_api = CliNpmSearchApi::new(deps_file_fetcher); let maybe_local_cache = self.config.maybe_vendor_dir_path().map(|local_path| { Arc::new(LocalLspHttpCache::new(local_path, global_cache.clone())) @@ -1182,7 +1181,7 @@ async fn create_npm_resolver( // do not install while resolving in the lsp—leave that to the cache command package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption::NoInstall, - npm_registry_url: crate::args::npm_registry_default_url().to_owned(), + npm_registry_url: crate::args::npm_registry_url().to_owned(), npm_system_info: NpmSystemInfo::default(), }) }) diff --git a/cli/lsp/npm.rs b/cli/lsp/npm.rs index 59156fe882..33cf48675b 100644 --- a/cli/lsp/npm.rs +++ b/cli/lsp/npm.rs @@ -4,29 +4,32 @@ use dashmap::DashMap; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::serde_json; -use deno_npm::registry::NpmPackageInfo; use deno_runtime::permissions::PermissionsContainer; use deno_semver::package::PackageNv; use deno_semver::Version; use serde::Deserialize; use std::sync::Arc; -use crate::args::npm_registry_default_url; +use crate::args::npm_registry_url; use crate::file_fetcher::FileFetcher; +use crate::npm::NpmFetchResolver; use super::search::PackageSearchApi; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct CliNpmSearchApi { file_fetcher: FileFetcher, - search_cache: Arc>>>, - versions_cache: Arc>>>, + resolver: NpmFetchResolver, + search_cache: DashMap>>, + versions_cache: DashMap>>, } impl CliNpmSearchApi { pub fn new(file_fetcher: FileFetcher) -> Self { + let resolver = NpmFetchResolver::new(file_fetcher.clone()); Self { file_fetcher, + resolver, search_cache: Default::default(), versions_cache: Default::default(), } @@ -39,12 +42,7 @@ impl PackageSearchApi for CliNpmSearchApi { if let Some(names) = self.search_cache.get(query) { return Ok(names.clone()); } - let mut search_url = npm_registry_default_url().clone(); - search_url - .path_segments_mut() - .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))? - .pop_if_empty() - .extend("-/v1/search".split('/')); + let mut search_url = npm_registry_url().join("-/v1/search")?; search_url .query_pairs_mut() .append_pair("text", &format!("{} boost-exact:false", query)); @@ -62,18 +60,12 @@ impl PackageSearchApi for CliNpmSearchApi { if let Some(versions) = self.versions_cache.get(name) { return Ok(versions.clone()); } - let mut info_url = npm_registry_default_url().clone(); - info_url - .path_segments_mut() - .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))? - .pop_if_empty() - .push(name); - let file = self - .file_fetcher - .fetch(&info_url, PermissionsContainer::allow_all()) - .await?; - let info = serde_json::from_slice::(&file.source)?; - let mut versions = info.versions.into_keys().collect::>(); + 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::>(); versions.sort(); versions.reverse(); let versions = Arc::new(versions); diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index cc14bec0a0..08c15941ab 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -8,11 +8,19 @@ mod managed; use std::path::PathBuf; use std::sync::Arc; +use dashmap::DashMap; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_npm::registry::NpmPackageInfo; use deno_runtime::deno_node::NpmResolver; +use deno_runtime::permissions::PermissionsContainer; +use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; +use crate::args::npm_registry_url; +use crate::file_fetcher::FileFetcher; + pub use self::byonm::ByonmCliNpmResolver; pub use self::byonm::CliNpmResolverByonmCreateOptions; pub use self::cache_dir::NpmCacheDir; @@ -87,3 +95,60 @@ pub trait CliNpmResolver: NpmResolver { /// or `None` if the state currently can't be determined. fn check_state_hash(&self) -> Option; } + +#[derive(Debug)] +pub struct NpmFetchResolver { + nv_by_req: DashMap>, + info_by_name: DashMap>>, + file_fetcher: FileFetcher, +} + +impl NpmFetchResolver { + pub fn new(file_fetcher: FileFetcher) -> Self { + Self { + nv_by_req: Default::default(), + info_by_name: Default::default(), + file_fetcher, + } + } + + pub async fn req_to_nv(&self, req: &PackageReq) -> Option { + if let Some(nv) = self.nv_by_req.get(req) { + return nv.value().clone(); + } + let maybe_get_nv = || async { + let name = req.name.clone(); + let package_info = self.package_info(&name).await?; + // Find the first matching version of the package which is cached. + let mut versions = package_info.versions.keys().collect::>(); + versions.sort(); + let version = versions + .into_iter() + .rev() + .find(|v| req.version_req.tag().is_none() && req.version_req.matches(v)) + .cloned()?; + Some(PackageNv { name, version }) + }; + let nv = maybe_get_nv().await; + self.nv_by_req.insert(req.clone(), nv.clone()); + nv + } + + pub async fn package_info(&self, name: &str) -> Option> { + if let Some(info) = self.info_by_name.get(name) { + return info.value().clone(); + } + let fetch_package_info = || async { + let info_url = npm_registry_url().join(name).ok()?; + let file = self + .file_fetcher + .fetch(&info_url, PermissionsContainer::allow_all()) + .await + .ok()?; + serde_json::from_slice::(&file.source).ok() + }; + let info = fetch_package_info().await.map(Arc::new); + self.info_by_name.insert(name.to_string(), info.clone()); + info + } +} diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index 0c10c4993b..e9044a5d11 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -23,6 +23,7 @@ use crate::args::Flags; use crate::factory::CliFactory; use crate::file_fetcher::FileFetcher; use crate::jsr::JsrFetchResolver; +use crate::npm::NpmFetchResolver; pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { let cli_factory = CliFactory::from_flags(flags.clone()).await?; @@ -77,13 +78,18 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { None, ); deps_file_fetcher.set_download_log_level(log::Level::Trace); - let jsr_resolver = Arc::new(JsrFetchResolver::new(deps_file_fetcher)); + let jsr_resolver = Arc::new(JsrFetchResolver::new(deps_file_fetcher.clone())); + let npm_resolver = Arc::new(NpmFetchResolver::new(deps_file_fetcher)); let package_futures = package_reqs .into_iter() .map(move |package_req| { - find_package_and_select_version_for_req(jsr_resolver.clone(), package_req) - .boxed_local() + find_package_and_select_version_for_req( + jsr_resolver.clone(), + npm_resolver.clone(), + package_req, + ) + .boxed_local() }) .collect::>(); @@ -183,6 +189,7 @@ enum PackageAndVersion { async fn find_package_and_select_version_for_req( jsr_resolver: Arc, + npm_resolver: Arc, add_package_req: AddPackageReq, ) -> Result { match add_package_req { @@ -203,11 +210,22 @@ async fn find_package_and_select_version_for_req( version_req: format!("{}{}", range_symbol, &nv.version), })) } - AddPackageReq::Npm(pkg_req) => { - bail!( - "Adding npm: packages is currently not supported. Package: npm:{}", - pkg_req.req().name - ); + AddPackageReq::Npm(pkg_ref) => { + let req = pkg_ref.req(); + let npm_prefixed_name = format!("npm:{}", &req.name); + let Some(nv) = npm_resolver.req_to_nv(req).await else { + return Ok(PackageAndVersion::NotFound(npm_prefixed_name)); + }; + let range_symbol = if req.version_req.version_text().starts_with('~') { + '~' + } else { + '^' + }; + Ok(PackageAndVersion::Selected(SelectedPackage { + import_name: req.name.to_string(), + package_name: npm_prefixed_name, + version_req: format!("{}{}", range_symbol, &nv.version), + })) } } } diff --git a/tests/integration/pm_tests.rs b/tests/integration/pm_tests.rs index cc5527c40a..668519bdc3 100644 --- a/tests/integration/pm_tests.rs +++ b/tests/integration/pm_tests.rs @@ -2,9 +2,7 @@ use deno_core::serde_json::json; use test_util::assert_contains; -use test_util::env_vars_for_jsr_tests; -// use test_util::env_vars_for_npm_tests; -// use test_util::itest; +use test_util::env_vars_for_jsr_npm_tests; use test_util::TestContextBuilder; #[test] @@ -110,21 +108,24 @@ fn add_multiple() { } #[test] -fn add_not_supported_npm() { +fn add_npm() { let context = pm_context_builder().build(); + let temp_dir = context.temp_dir().path(); - let output = context - .new_command() - .args("add @denotest/add npm:express") - .run(); - output.assert_exit_code(1); + let output = context.new_command().args("add npm:chalk@4.1").run(); + output.assert_exit_code(0); let output = output.combined_output(); - assert_contains!(output, "error: Adding npm: packages is currently not supported. Package: npm:express"); + assert_contains!(output, "Add chalk"); + temp_dir.join("deno.json").assert_matches_json(json!({ + "imports": { + "chalk": "npm:chalk@^4.1.2" + } + })); } fn pm_context_builder() -> TestContextBuilder { TestContextBuilder::new() .use_http_server() - .envs(env_vars_for_jsr_tests()) + .envs(env_vars_for_jsr_npm_tests()) .use_temp_cwd() }