From 7cf15a4097880300a83db35d6cb5d30ccd7595cb Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 11 Jun 2024 08:55:12 -0400 Subject: [PATCH] fix(npm): resolve dynamic npm imports individually (#24170) * https://github.com/denoland/deno_npm/pull/57 * https://github.com/denoland/deno_graph/pull/498 Closes https://github.com/denoland/deno/issues/17802 --- Cargo.lock | 8 +- cli/Cargo.toml | 4 +- cli/factory.rs | 5 +- cli/graph_util.rs | 25 +-- cli/lsp/resolver.rs | 9 +- cli/module_loader.rs | 1 - cli/npm/managed/installer.rs | 123 ------------- cli/npm/managed/mod.rs | 128 ++++++------- cli/npm/managed/resolution.rs | 168 +++++++----------- cli/npm/mod.rs | 1 - cli/resolver.rs | 116 ++++++------ cli/standalone/mod.rs | 83 +++++---- cli/tools/task.rs | 1 - tests/integration/lsp_tests.rs | 8 +- tests/integration/npm_tests.rs | 1 + .../dep-cannot-parse/1.0.0/package.json | 7 + .../__test__.jsonc | 4 + .../dynamic_npm_resolution_failure/main.out | 15 ++ .../dynamic_npm_resolution_failure/main.ts | 9 + 19 files changed, 263 insertions(+), 453 deletions(-) delete mode 100644 cli/npm/managed/installer.rs create mode 100644 tests/registry/npm/@denotest/dep-cannot-parse/1.0.0/package.json create mode 100644 tests/specs/npm/dynamic_npm_resolution_failure/__test__.jsonc create mode 100644 tests/specs/npm/dynamic_npm_resolution_failure/main.out create mode 100644 tests/specs/npm/dynamic_npm_resolution_failure/main.ts diff --git a/Cargo.lock b/Cargo.lock index 346651dc5a..486d5c2a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,9 +1481,9 @@ dependencies = [ [[package]] name = "deno_graph" -version = "0.78.0" +version = "0.78.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ccd2755a805983f96aeccd211c1f7585b6bfec77471f502c47227abe375682" +checksum = "d13080829a06062a14e41e190f64a3407e4a0f63cf7db5dcecbc3cf500445df3" dependencies = [ "anyhow", "async-trait", @@ -1740,9 +1740,9 @@ dependencies = [ [[package]] name = "deno_npm" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78b95f0daab64d3cfb28b13be2b40d73c8b9563bbce3aa50fc322a7325ce0b9" +checksum = "9812c781ff6b2e0e45c32ccba9983bce84ecccf6f6a7006b750f8c5c9ac15e30" dependencies = [ "anyhow", "async-trait", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0292b025d5..a523ec6571 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -69,10 +69,10 @@ deno_config = "=0.16.4" deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_doc = { version = "=0.139.0", features = ["html", "syntect"] } deno_emit = "=0.42.0" -deno_graph = { version = "=0.78.0", features = ["tokio_executor"] } +deno_graph = { version = "=0.78.1", features = ["tokio_executor"] } deno_lint = { version = "=0.60.0", features = ["docs"] } deno_lockfile.workspace = true -deno_npm = "=0.21.3" +deno_npm = "=0.21.4" deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_semver = "=0.5.4" deno_task_shell = "=0.16.1" diff --git a/cli/factory.rs b/cli/factory.rs index 33786939c3..5cae24c7c3 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -35,7 +35,6 @@ use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedCreateOptions; -use crate::npm::CliNpmResolverManagedPackageJsonInstallerOption; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::resolver::CjsResolutionStore; use crate::resolver::CliGraphResolver; @@ -441,10 +440,8 @@ impl CliFactory { cache_setting: self.options.cache_setting(), text_only_progress_bar: self.text_only_progress_bar().clone(), maybe_node_modules_path: self.options.node_modules_dir_path().cloned(), - package_json_installer: - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( + package_json_deps_provider: self.package_json_deps_provider().clone(), - ), npm_system_info: self.options.npm_system_info(), npmrc: self.options.npmrc().clone() }) diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 502702b075..67c1792937 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -160,6 +160,10 @@ pub fn graph_valid( if let Some(error) = errors.next() { Err(error) } else { + // finally surface the npm resolution result + if let Err(err) = &graph.npm_dep_graph_result { + return Err(custom_error(get_error_class_name(err), format!("{}", err))); + } Ok(()) } } @@ -562,30 +566,9 @@ impl ModuleGraphBuilder { let initial_redirects_len = graph.redirects.len(); let initial_package_deps_len = graph.packages.package_deps_sum(); let initial_package_mappings_len = graph.packages.mappings().len(); - let initial_npm_packages = graph.npm_packages.len(); graph.build(roots, loader, options).await; - let has_npm_packages_changed = - graph.npm_packages.len() != initial_npm_packages; - // skip installing npm packages if we don't have to - if is_first_execution - && self.npm_resolver.root_node_modules_path().is_some() - || has_npm_packages_changed - { - if let Some(npm_resolver) = self.npm_resolver.as_managed() { - // ensure that the top level package.json is installed if a - // specifier was matched in the package.json - if self.resolver.found_package_json_dep() { - npm_resolver.ensure_top_level_package_json_install().await?; - } - - // resolve the dependencies of any pending dependencies - // that were inserted by building the graph - npm_resolver.resolve_pending().await?; - } - } - let has_redirects_changed = graph.redirects.len() != initial_redirects_len; let has_jsr_package_deps_changed = graph.packages.package_deps_sum() != initial_package_deps_len; diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index d0a515063b..bd09f0ad19 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -12,7 +12,6 @@ use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedCreateOptions; -use crate::npm::CliNpmResolverManagedPackageJsonInstallerOption; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::npm::ManagedCliNpmResolver; use crate::resolver::CliGraphResolver; @@ -347,9 +346,11 @@ async fn create_npm_resolver( cache_setting: CacheSetting::Only, text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), maybe_node_modules_path: config_data.node_modules_dir.clone(), - // do not install while resolving in the lsp—leave that to the cache command - package_json_installer: - CliNpmResolverManagedPackageJsonInstallerOption::NoInstall, + package_json_deps_provider: Arc::new(PackageJsonDepsProvider::new( + config_data.package_json.as_ref().map(|package_json| { + package_json::get_local_package_json_version_reqs(package_json) + }), + )), npmrc: config_data .npmrc .clone() diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 5bcc22c065..1da3f1f2af 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -75,7 +75,6 @@ pub async fn load_top_level_deps(factory: &CliFactory) -> Result<(), AnyError> { let npm_resolver = factory.npm_resolver().await?; if let Some(npm_resolver) = npm_resolver.as_managed() { npm_resolver.ensure_top_level_package_json_install().await?; - npm_resolver.resolve_pending().await?; } // cache as many entries in the import map as we can if let Some(import_map) = factory.maybe_import_map().await? { diff --git a/cli/npm/managed/installer.rs b/cli/npm/managed/installer.rs deleted file mode 100644 index 694e01206f..0000000000 --- a/cli/npm/managed/installer.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use std::future::Future; -use std::sync::Arc; - -use deno_core::error::AnyError; -use deno_core::futures::stream::FuturesOrdered; -use deno_core::futures::StreamExt; -use deno_npm::registry::NpmRegistryApi; -use deno_npm::registry::NpmRegistryPackageInfoLoadError; -use deno_semver::package::PackageReq; - -use crate::args::PackageJsonDepsProvider; -use crate::util::sync::AtomicFlag; - -use super::CliNpmRegistryApi; -use super::NpmResolution; - -#[derive(Debug)] -struct PackageJsonDepsInstallerInner { - deps_provider: Arc, - has_installed_flag: AtomicFlag, - npm_registry_api: Arc, - npm_resolution: Arc, -} - -impl PackageJsonDepsInstallerInner { - pub fn reqs_with_info_futures<'a>( - &self, - reqs: &'a [&'a PackageReq], - ) -> FuturesOrdered< - impl Future< - Output = Result< - (&'a PackageReq, Arc), - NpmRegistryPackageInfoLoadError, - >, - >, - > { - FuturesOrdered::from_iter(reqs.iter().map(|req| { - let api = self.npm_registry_api.clone(); - async move { - let info = api.package_info(&req.name).await?; - Ok::<_, NpmRegistryPackageInfoLoadError>((*req, info)) - } - })) - } -} - -/// Holds and controls installing dependencies from package.json. -#[derive(Debug, Default)] -pub struct PackageJsonDepsInstaller(Option); - -impl PackageJsonDepsInstaller { - pub fn new( - deps_provider: Arc, - npm_registry_api: Arc, - npm_resolution: Arc, - ) -> Self { - Self(Some(PackageJsonDepsInstallerInner { - deps_provider, - has_installed_flag: Default::default(), - npm_registry_api, - npm_resolution, - })) - } - - /// Creates an installer that never installs local packages during - /// resolution. A top level install will be a no-op. - pub fn no_op() -> Self { - Self(None) - } - - /// Installs the top level dependencies in the package.json file - /// without going through and resolving the descendant dependencies yet. - pub async fn ensure_top_level_install(&self) -> Result<(), AnyError> { - let inner = match &self.0 { - Some(inner) => inner, - None => return Ok(()), - }; - - if !inner.has_installed_flag.raise() { - return Ok(()); // already installed by something else - } - - let package_reqs = inner.deps_provider.reqs().unwrap_or_default(); - - // check if something needs resolving before bothering to load all - // the package information (which is slow) - if package_reqs.iter().all(|req| { - inner - .npm_resolution - .resolve_pkg_id_from_pkg_req(req) - .is_ok() - }) { - log::debug!( - "All package.json deps resolvable. Skipping top level install." - ); - return Ok(()); // everything is already resolvable - } - - let mut reqs_with_info_futures = - inner.reqs_with_info_futures(&package_reqs); - - while let Some(result) = reqs_with_info_futures.next().await { - let (req, info) = result?; - let result = inner - .npm_resolution - .resolve_pkg_req_as_pending_with_info(req, &info) - .await; - if let Err(err) = result { - if inner.npm_registry_api.mark_force_reload() { - log::debug!("Failed to resolve package. Retrying. Error: {err:#}"); - // re-initialize - reqs_with_info_futures = inner.reqs_with_info_futures(&package_reqs); - } else { - return Err(err.into()); - } - } - } - - Ok(()) - } -} diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs index d3045a1c9e..c086235c37 100644 --- a/cli/npm/managed/mod.rs +++ b/cli/npm/managed/mod.rs @@ -11,7 +11,6 @@ use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde_json; -use deno_graph::NpmPackageReqsResolution; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; @@ -26,6 +25,7 @@ use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NpmResolver; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; +use resolution::AddPkgReqsResult; use crate::args::Lockfile; use crate::args::NpmProcessState; @@ -35,9 +35,9 @@ use crate::cache::FastInsecureHasher; use crate::http_util::HttpClientProvider; use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; use crate::util::progress_bar::ProgressBar; +use crate::util::sync::AtomicFlag; use self::cache::NpmCache; -use self::installer::PackageJsonDepsInstaller; use self::registry::CliNpmRegistryApi; use self::resolution::NpmResolution; use self::resolvers::create_npm_fs_resolver; @@ -48,7 +48,6 @@ use super::InnerCliNpmResolverRef; use super::NpmCacheDir; mod cache; -mod installer; mod registry; mod resolution; mod resolvers; @@ -58,11 +57,6 @@ pub enum CliNpmResolverManagedSnapshotOption { Specified(Option), } -pub enum CliNpmResolverManagedPackageJsonInstallerOption { - ConditionalInstall(Arc), - NoInstall, -} - pub struct CliNpmResolverManagedCreateOptions { pub snapshot: CliNpmResolverManagedSnapshotOption, pub maybe_lockfile: Option>>, @@ -73,7 +67,7 @@ pub struct CliNpmResolverManagedCreateOptions { pub text_only_progress_bar: crate::util::progress_bar::ProgressBar, pub maybe_node_modules_path: Option, pub npm_system_info: NpmSystemInfo, - pub package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption, + pub package_json_deps_provider: Arc, pub npmrc: Arc, } @@ -98,7 +92,7 @@ pub async fn create_managed_npm_resolver_for_lsp( npm_api, npm_cache, options.npmrc, - options.package_json_installer, + options.package_json_deps_provider, options.text_only_progress_bar, options.maybe_node_modules_path, options.npm_system_info, @@ -122,7 +116,7 @@ pub async fn create_managed_npm_resolver( npm_api, npm_cache, options.npmrc, - options.package_json_installer, + options.package_json_deps_provider, options.text_only_progress_bar, options.maybe_node_modules_path, options.npm_system_info, @@ -138,7 +132,7 @@ fn create_inner( npm_api: Arc, npm_cache: Arc, npm_rc: Arc, - package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption, + package_json_deps_provider: Arc, text_only_progress_bar: crate::util::progress_bar::ProgressBar, node_modules_dir_path: Option, npm_system_info: NpmSystemInfo, @@ -165,25 +159,13 @@ fn create_inner( node_modules_dir_path, npm_system_info.clone(), ); - let package_json_deps_installer = match package_json_installer { - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( - provider, - ) => Arc::new(PackageJsonDepsInstaller::new( - provider, - npm_api.clone(), - resolution.clone(), - )), - CliNpmResolverManagedPackageJsonInstallerOption::NoInstall => { - Arc::new(PackageJsonDepsInstaller::no_op()) - } - }; Arc::new(ManagedCliNpmResolver::new( fs, fs_resolver, maybe_lockfile, npm_api, npm_cache, - package_json_deps_installer, + package_json_deps_provider, resolution, tarball_cache, text_only_progress_bar, @@ -271,11 +253,12 @@ pub struct ManagedCliNpmResolver { maybe_lockfile: Option>>, npm_api: Arc, npm_cache: Arc, - package_json_deps_installer: Arc, + package_json_deps_provider: Arc, resolution: Arc, tarball_cache: Arc, text_only_progress_bar: ProgressBar, npm_system_info: NpmSystemInfo, + top_level_install_flag: AtomicFlag, } impl std::fmt::Debug for ManagedCliNpmResolver { @@ -294,7 +277,7 @@ impl ManagedCliNpmResolver { maybe_lockfile: Option>>, npm_api: Arc, npm_cache: Arc, - package_json_deps_installer: Arc, + package_json_deps_provider: Arc, resolution: Arc, tarball_cache: Arc, text_only_progress_bar: ProgressBar, @@ -306,11 +289,12 @@ impl ManagedCliNpmResolver { maybe_lockfile, npm_api, npm_cache, - package_json_deps_installer, + package_json_deps_provider, text_only_progress_bar, resolution, tarball_cache, npm_system_info, + top_level_install_flag: Default::default(), } } @@ -385,14 +369,28 @@ impl ManagedCliNpmResolver { &self, packages: &[PackageReq], ) -> Result<(), AnyError> { + let result = self.add_package_reqs_raw(packages).await; + result.dependencies_result + } + + pub async fn add_package_reqs_raw( + &self, + packages: &[PackageReq], + ) -> AddPkgReqsResult { if packages.is_empty() { - return Ok(()); + return AddPkgReqsResult { + dependencies_result: Ok(()), + results: vec![], + }; } - self.resolution.add_package_reqs(packages).await?; - self.fs_resolver.cache_packages().await?; + let mut result = self.resolution.add_package_reqs(packages).await; + if result.dependencies_result.is_ok() { + result.dependencies_result = + self.cache_packages().await.map_err(AnyError::from); + } - Ok(()) + result } /// Sets package requirements to the resolver, removing old requirements and adding new ones. @@ -422,51 +420,17 @@ impl ManagedCliNpmResolver { &self, ) -> Result<(), AnyError> { // add and ensure this isn't added to the lockfile - let package_reqs = vec![PackageReq::from_str("@types/node").unwrap()]; - self.resolution.add_package_reqs(&package_reqs).await?; - self.cache_packages().await?; + self + .add_package_reqs(&[PackageReq::from_str("@types/node").unwrap()]) + .await?; Ok(()) } - pub async fn resolve_pending(&self) -> Result<(), AnyError> { - self.resolution.resolve_pending().await?; - self.cache_packages().await - } - pub async fn cache_packages(&self) -> Result<(), AnyError> { self.fs_resolver.cache_packages().await } - /// Resolves package requirements for deno graph. - pub async fn resolve_npm_for_deno_graph( - &self, - reqs_with_pkg_infos: &[(&PackageReq, Arc)], - ) -> NpmPackageReqsResolution { - let results = self - .resolution - .resolve_pkg_reqs_as_pending_with_info(reqs_with_pkg_infos) - .await; - - let mut resolutions = Vec::with_capacity(results.len()); - for result in results { - match result { - Ok(nv) => { - resolutions.push(Ok(nv)); - } - Err(err) => { - if self.npm_api.mark_force_reload() { - log::debug!("Restarting npm specifier resolution to check for new registry information. Error: {:#}", err); - return NpmPackageReqsResolution::ReloadRegistryInfo; - } else { - resolutions.push(Err(Arc::new(err.into()))); - } - } - } - } - NpmPackageReqsResolution::Resolutions(resolutions) - } - pub fn resolve_pkg_folder_from_deno_module( &self, nv: &PackageNv, @@ -485,10 +449,26 @@ impl ManagedCliNpmResolver { pub async fn ensure_top_level_package_json_install( &self, ) -> Result<(), AnyError> { - self - .package_json_deps_installer - .ensure_top_level_install() - .await + let Some(reqs) = self.package_json_deps_provider.reqs() else { + return Ok(()); + }; + if !self.top_level_install_flag.raise() { + return Ok(()); // already did this + } + // check if something needs resolving before bothering to load all + // the package information (which is slow) + if reqs + .iter() + .all(|req| self.resolution.resolve_pkg_id_from_pkg_req(req).is_ok()) + { + log::debug!( + "All package.json deps resolvable. Skipping top level install." + ); + return Ok(()); // everything is already resolvable + } + + let reqs = reqs.into_iter().cloned().collect::>(); + self.add_package_reqs(&reqs).await } pub async fn cache_package_info( @@ -582,7 +562,7 @@ impl CliNpmResolver for ManagedCliNpmResolver { self.maybe_lockfile.clone(), self.npm_api.clone(), self.npm_cache.clone(), - self.package_json_deps_installer.clone(), + self.package_json_deps_provider.clone(), npm_resolution, self.tarball_cache.clone(), self.text_only_progress_bar.clone(), diff --git a/cli/npm/managed/resolution.rs b/cli/npm/managed/resolution.rs index 7c27567492..c1d31325df 100644 --- a/cli/npm/managed/resolution.rs +++ b/cli/npm/managed/resolution.rs @@ -8,9 +8,7 @@ use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_lockfile::NpmPackageDependencyLockfileInfo; use deno_lockfile::NpmPackageLockfileInfo; -use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; -use deno_npm::resolution::NpmPackageVersionResolutionError; use deno_npm::resolution::NpmPackagesPartitioned; use deno_npm::resolution::NpmResolutionError; use deno_npm::resolution::NpmResolutionSnapshot; @@ -34,6 +32,16 @@ use crate::util::sync::SyncReadAsyncWriteLock; use super::CliNpmRegistryApi; +pub struct AddPkgReqsResult { + /// Results from adding the individual packages. + /// + /// The indexes of the results correspond to the indexes of the provided + /// package requirements. + pub results: Vec>, + /// The final result of resolving and caching all the package requirements. + pub dependencies_result: Result<(), AnyError>, +} + /// Handles updating and storing npm resolution in memory where the underlying /// snapshot can be updated concurrently. Additionally handles updating the lockfile /// based on changes to the resolution. @@ -80,19 +88,27 @@ impl NpmResolution { pub async fn add_package_reqs( &self, package_reqs: &[PackageReq], - ) -> Result<(), AnyError> { + ) -> AddPkgReqsResult { // only allow one thread in here at a time let snapshot_lock = self.snapshot.acquire().await; - let snapshot = add_package_reqs_to_snapshot( + let result = add_package_reqs_to_snapshot( &self.api, package_reqs, self.maybe_lockfile.clone(), || snapshot_lock.read().clone(), ) - .await?; + .await; - *snapshot_lock.write() = snapshot; - Ok(()) + AddPkgReqsResult { + results: result.results, + dependencies_result: match result.dep_graph_result { + Ok(snapshot) => { + *snapshot_lock.write() = snapshot; + Ok(()) + } + Err(err) => Err(err.into()), + }, + } } pub async fn set_package_reqs( @@ -121,24 +137,8 @@ impl NpmResolution { } }, ) - .await?; - - *snapshot_lock.write() = snapshot; - - Ok(()) - } - - pub async fn resolve_pending(&self) -> Result<(), AnyError> { - // only allow one thread in here at a time - let snapshot_lock = self.snapshot.acquire().await; - - let snapshot = add_package_reqs_to_snapshot( - &self.api, - &Vec::new(), - self.maybe_lockfile.clone(), - || snapshot_lock.read().clone(), - ) - .await?; + .await + .into_result()?; *snapshot_lock.write() = snapshot; @@ -217,49 +217,6 @@ impl NpmResolution { .map(|pkg| pkg.id.clone()) } - /// Resolves a package requirement for deno graph. This should only be - /// called by deno_graph's NpmResolver or for resolving packages in - /// a package.json - pub async fn resolve_pkg_req_as_pending_with_info( - &self, - pkg_req: &PackageReq, - pkg_info: &NpmPackageInfo, - ) -> Result { - debug_assert_eq!(pkg_req.name, pkg_info.name); - // only allow one thread in here at a time - let snapshot_lock = self.snapshot.acquire().await; - - let mut snapshot = snapshot_lock.write(); - let pending_resolver = get_npm_pending_resolver(&self.api); - let nv = pending_resolver.resolve_package_req_as_pending( - &mut snapshot, - pkg_req, - pkg_info, - )?; - Ok(nv) - } - - pub async fn resolve_pkg_reqs_as_pending_with_info( - &self, - reqs_with_pkg_infos: &[(&PackageReq, Arc)], - ) -> Vec> { - // only allow one thread in here at a time - let snapshot_lock = self.snapshot.acquire().await; - - let mut snapshot = snapshot_lock.write(); - let pending_resolver = get_npm_pending_resolver(&self.api); - let mut results = Vec::with_capacity(reqs_with_pkg_infos.len()); - for (pkg_req, pkg_info) in reqs_with_pkg_infos { - debug_assert_eq!(pkg_req.name, pkg_info.name); - results.push(pending_resolver.resolve_package_req_as_pending( - &mut snapshot, - pkg_req, - pkg_info, - )); - } - results - } - pub fn package_reqs(&self) -> HashMap { self.snapshot.read().package_reqs().clone() } @@ -307,52 +264,50 @@ async fn add_package_reqs_to_snapshot( package_reqs: &[PackageReq], maybe_lockfile: Option>>, get_new_snapshot: impl Fn() -> NpmResolutionSnapshot, -) -> Result { +) -> deno_npm::resolution::AddPkgReqsResult { let snapshot = get_new_snapshot(); - let snapshot = if !snapshot.has_pending() - && package_reqs - .iter() - .all(|req| snapshot.package_reqs().contains_key(req)) + if package_reqs + .iter() + .all(|req| snapshot.package_reqs().contains_key(req)) { - log::debug!( - "Snapshot already up to date. Skipping pending npm resolution." - ); - snapshot - } else { - log::debug!( - /* this string is used in tests!! */ - "Running pending npm resolution." - ); - let pending_resolver = get_npm_pending_resolver(api); - let result = pending_resolver - .resolve_pending(snapshot, package_reqs) - .await; - api.clear_memory_cache(); - match result { - Ok(snapshot) => snapshot, - Err(NpmResolutionError::Resolution(err)) if api.mark_force_reload() => { - log::debug!("{err:#}"); - log::debug!("npm resolution failed. Trying again..."); + log::debug!("Snapshot already up to date. Skipping npm resolution."); + return deno_npm::resolution::AddPkgReqsResult { + results: package_reqs + .iter() + .map(|req| Ok(snapshot.package_reqs().get(req).unwrap().clone())) + .collect(), + dep_graph_result: Ok(snapshot), + }; + } + log::debug!( + /* this string is used in tests */ + "Running npm resolution." + ); + let pending_resolver = get_npm_pending_resolver(api); + let result = pending_resolver.add_pkg_reqs(snapshot, package_reqs).await; + api.clear_memory_cache(); + let result = match &result.dep_graph_result { + Err(NpmResolutionError::Resolution(err)) if api.mark_force_reload() => { + log::debug!("{err:#}"); + log::debug!("npm resolution failed. Trying again..."); - // try again - let snapshot = get_new_snapshot(); - let result = pending_resolver - .resolve_pending(snapshot, package_reqs) - .await; - api.clear_memory_cache(); - // now surface the result after clearing the cache - result? - } - Err(err) => return Err(err.into()), + // try again + let snapshot = get_new_snapshot(); + let result = pending_resolver.add_pkg_reqs(snapshot, package_reqs).await; + api.clear_memory_cache(); + result } + _ => result, }; - if let Some(lockfile_mutex) = maybe_lockfile { - let mut lockfile = lockfile_mutex.lock(); - populate_lockfile_from_snapshot(&mut lockfile, &snapshot); + if let Ok(snapshot) = &result.dep_graph_result { + if let Some(lockfile_mutex) = maybe_lockfile { + let mut lockfile = lockfile_mutex.lock(); + populate_lockfile_from_snapshot(&mut lockfile, snapshot); + } } - Ok(snapshot) + result } fn get_npm_pending_resolver( @@ -374,7 +329,6 @@ fn populate_lockfile_from_snapshot( lockfile: &mut Lockfile, snapshot: &NpmResolutionSnapshot, ) { - assert!(!snapshot.has_pending()); for (package_req, nv) in snapshot.package_reqs() { lockfile.insert_package_specifier( format!("npm:{}", package_req), diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 8d801744b1..8ae81de246 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -25,7 +25,6 @@ pub use self::byonm::ByonmCliNpmResolver; pub use self::byonm::CliNpmResolverByonmCreateOptions; pub use self::cache_dir::NpmCacheDir; pub use self::managed::CliNpmResolverManagedCreateOptions; -pub use self::managed::CliNpmResolverManagedPackageJsonInstallerOption; pub use self::managed::CliNpmResolverManagedSnapshotOption; pub use self::managed::ManagedCliNpmResolver; diff --git a/cli/resolver.rs b/cli/resolver.rs index 994f03f36d..87a82dda03 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -7,9 +7,6 @@ use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::futures::future; -use deno_core::futures::future::LocalBoxFuture; -use deno_core::futures::FutureExt; use deno_core::ModuleSourceCode; use deno_core::ModuleSpecifier; use deno_graph::source::ResolutionMode; @@ -17,8 +14,9 @@ use deno_graph::source::ResolveError; use deno_graph::source::Resolver; use deno_graph::source::UnknownBuiltInNodeModuleError; use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE; -use deno_graph::NpmPackageReqsResolution; -use deno_npm::registry::NpmPackageInfo; +use deno_graph::NpmLoadError; +use deno_graph::NpmResolvePkgReqsResult; +use deno_npm::resolution::NpmResolutionError; use deno_runtime::deno_fs; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::is_builtin_node_module; @@ -34,8 +32,6 @@ use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use import_map::ImportMap; use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; @@ -440,7 +436,7 @@ pub struct CliGraphResolver { maybe_vendor_specifier: Option, node_resolver: Option>, npm_resolver: Option>, - found_package_json_dep_flag: Arc, + found_package_json_dep_flag: AtomicFlag, bare_node_builtins_enabled: bool, } @@ -501,15 +497,11 @@ impl CliGraphResolver { pub fn create_graph_npm_resolver(&self) -> WorkerCliNpmGraphResolver { WorkerCliNpmGraphResolver { npm_resolver: self.npm_resolver.as_ref(), - memory_cache: Default::default(), + found_package_json_dep_flag: &self.found_package_json_dep_flag, bare_node_builtins_enabled: self.bare_node_builtins_enabled, } } - pub fn found_package_json_dep(&self) -> bool { - self.found_package_json_dep_flag.is_raised() - } - fn check_surface_byonm_node_error( &self, specifier: &str, @@ -771,9 +763,7 @@ fn resolve_package_json_dep( #[derive(Debug)] pub struct WorkerCliNpmGraphResolver<'a> { npm_resolver: Option<&'a Arc>, - // use a cache per worker so that another worker doesn't clear the - // cache of another worker - memory_cache: Rc>>>, + found_package_json_dep_flag: &'a AtomicFlag, bare_node_builtins_enabled: bool, } @@ -810,36 +800,25 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { } } - fn load_and_cache_npm_package_info( - &self, - package_name: &str, - ) -> LocalBoxFuture<'static, Result<(), AnyError>> { + fn load_and_cache_npm_package_info(&self, package_name: &str) { match self.npm_resolver { Some(npm_resolver) if npm_resolver.as_managed().is_some() => { let npm_resolver = npm_resolver.clone(); let package_name = package_name.to_string(); - let memory_cache = self.memory_cache.clone(); - async move { + deno_core::unsync::spawn(async move { if let Some(managed) = npm_resolver.as_managed() { - let package_info = - managed.cache_package_info(&package_name).await?; - memory_cache.borrow_mut().insert(package_name, package_info); + let _ignore = managed.cache_package_info(&package_name).await; } - Ok(()) - } - .boxed_local() - } - _ => { - // return it succeeded and error at the import site below - Box::pin(future::ready(Ok(()))) + }); } + _ => {} } } async fn resolve_pkg_reqs( &self, - package_reqs: &[&PackageReq], - ) -> NpmPackageReqsResolution { + package_reqs: &[PackageReq], + ) -> NpmResolvePkgReqsResult { match &self.npm_resolver { Some(npm_resolver) => { let npm_resolver = match npm_resolver.as_inner() { @@ -849,37 +828,50 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { InnerCliNpmResolverRef::Byonm(_) => unreachable!(), }; - let reqs_with_package_infos = { - let memory_cache = self.memory_cache.borrow(); - package_reqs - .iter() - .map(|package_req| { - let package_info = memory_cache - .get(&package_req.name) - .unwrap() // ok because deno_graph would have called load_and_cache_npm_package_info - .clone(); - (*package_req, package_info) - }) - .collect::>() + let top_level_result = if self.found_package_json_dep_flag.is_raised() { + npm_resolver.ensure_top_level_package_json_install().await + } else { + Ok(()) }; - let result = npm_resolver - .resolve_npm_for_deno_graph(&reqs_with_package_infos) - .await; - if matches!(result, NpmPackageReqsResolution::ReloadRegistryInfo) { - self.memory_cache.borrow_mut().clear(); + + let result = npm_resolver.add_package_reqs_raw(package_reqs).await; + + NpmResolvePkgReqsResult { + results: result + .results + .into_iter() + .map(|r| { + r.map_err(|err| match err { + NpmResolutionError::Registry(e) => { + NpmLoadError::RegistryInfo(Arc::new(e.into())) + } + NpmResolutionError::Resolution(e) => { + NpmLoadError::PackageReqResolution(Arc::new(e.into())) + } + NpmResolutionError::DependencyEntry(e) => { + NpmLoadError::PackageReqResolution(Arc::new(e.into())) + } + }) + }) + .collect(), + dep_graph_result: match top_level_result { + Ok(()) => result.dependencies_result.map_err(Arc::new), + Err(err) => Err(Arc::new(err)), + }, + } + } + None => { + let err = Arc::new(anyhow!( + "npm specifiers were requested; but --no-npm is specified" + )); + NpmResolvePkgReqsResult { + results: package_reqs + .iter() + .map(|_| Err(NpmLoadError::RegistryInfo(err.clone()))) + .collect(), + dep_graph_result: Err(err), } - result } - None => NpmPackageReqsResolution::Resolutions( - package_reqs - .iter() - .map(|_| { - Err(Arc::new(anyhow!( - "npm specifiers were requested; but --no-npm is specified" - ))) - }) - .collect(), - ), } } diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 91e524ddaa..b71e47cebd 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -21,7 +21,6 @@ use crate::npm::create_cli_npm_resolver; use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedCreateOptions; -use crate::npm::CliNpmResolverManagedPackageJsonInstallerOption; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::npm::NpmCacheDir; use crate::resolver::CjsResolutionStore; @@ -373,27 +372,27 @@ pub async fn run( )); let fs = Arc::new(DenoCompileFileSystem::new(vfs)) as Arc; - let npm_resolver = create_cli_npm_resolver( - CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { - snapshot: CliNpmResolverManagedSnapshotOption::Specified(Some(snapshot)), - maybe_lockfile: None, - fs: fs.clone(), - http_client_provider: http_client_provider.clone(), - npm_global_cache_dir, - cache_setting, - text_only_progress_bar: progress_bar, - maybe_node_modules_path, - package_json_installer: - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( - package_json_deps_provider.clone(), - ), - npm_system_info: Default::default(), - // Packages from different registries are already inlined in the ESZip, - // so no need to create actual `.npmrc` configuration. - npmrc: create_default_npmrc(), - }), - ) - .await?; + let npm_resolver = + create_cli_npm_resolver(CliNpmResolverCreateOptions::Managed( + CliNpmResolverManagedCreateOptions { + snapshot: CliNpmResolverManagedSnapshotOption::Specified(Some( + snapshot, + )), + maybe_lockfile: None, + fs: fs.clone(), + http_client_provider: http_client_provider.clone(), + npm_global_cache_dir, + cache_setting, + text_only_progress_bar: progress_bar, + maybe_node_modules_path, + package_json_deps_provider: package_json_deps_provider.clone(), + npm_system_info: Default::default(), + // Packages from different registries are already inlined in the ESZip, + // so no need to create actual `.npmrc` configuration. + npmrc: create_default_npmrc(), + }, + )) + .await?; ( package_json_deps_provider, fs, @@ -431,27 +430,25 @@ pub async fn run( let package_json_deps_provider = Arc::new(PackageJsonDepsProvider::new(None)); let fs = Arc::new(deno_fs::RealFs) as Arc; - let npm_resolver = create_cli_npm_resolver( - CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { - snapshot: CliNpmResolverManagedSnapshotOption::Specified(None), - maybe_lockfile: None, - fs: fs.clone(), - http_client_provider: http_client_provider.clone(), - npm_global_cache_dir, - cache_setting, - text_only_progress_bar: progress_bar, - maybe_node_modules_path: None, - package_json_installer: - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( - package_json_deps_provider.clone(), - ), - npm_system_info: Default::default(), - // Packages from different registries are already inlined in the ESZip, - // so no need to create actual `.npmrc` configuration. - npmrc: create_default_npmrc(), - }), - ) - .await?; + let npm_resolver = + create_cli_npm_resolver(CliNpmResolverCreateOptions::Managed( + CliNpmResolverManagedCreateOptions { + snapshot: CliNpmResolverManagedSnapshotOption::Specified(None), + maybe_lockfile: None, + fs: fs.clone(), + http_client_provider: http_client_provider.clone(), + npm_global_cache_dir, + cache_setting, + text_only_progress_bar: progress_bar, + maybe_node_modules_path: None, + package_json_deps_provider: package_json_deps_provider.clone(), + npm_system_info: Default::default(), + // Packages from different registries are already inlined in the ESZip, + // so no need to create actual `.npmrc` configuration. + npmrc: create_default_npmrc(), + }, + )) + .await?; (package_json_deps_provider, fs, npm_resolver, None) } }; diff --git a/cli/tools/task.rs b/cli/tools/task.rs index 2429e56004..6b18e18bb1 100644 --- a/cli/tools/task.rs +++ b/cli/tools/task.rs @@ -122,7 +122,6 @@ pub async fn execute_script( if cli_options.has_node_modules_dir() { if let Some(npm_resolver) = npm_resolver.as_managed() { npm_resolver.ensure_top_level_package_json_install().await?; - npm_resolver.resolve_pending().await?; } } diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 25fb695b48..62cb84457b 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -12868,14 +12868,10 @@ fn lsp_uses_lockfile_for_npm_initialization() { client.initialize_default(); let mut skipping_count = 0; client.wait_until_stderr_line(|line| { - if line.contains("Skipping pending npm resolution.") { + if line.contains("Skipping npm resolution.") { skipping_count += 1; } - assert!( - !line.contains("Running pending npm resolution."), - "Line: {}", - line - ); + assert!(!line.contains("Running npm resolution."), "Line: {}", line); line.contains("Server ready.") }); assert_eq!(skipping_count, 1); diff --git a/tests/integration/npm_tests.rs b/tests/integration/npm_tests.rs index 2c074b86fe..1d60d9e353 100644 --- a/tests/integration/npm_tests.rs +++ b/tests/integration/npm_tests.rs @@ -1822,6 +1822,7 @@ fn reload_info_not_found_cache_but_exists_remote() { .run(); output.assert_matches_text(concat!( "error: Could not find npm package '@denotest/esm-import-cjs-default' matching '1.0.0'.\n", + " at file:///[WILDCARD]/main.ts:1:8\n", )); output.assert_exit_code(1); diff --git a/tests/registry/npm/@denotest/dep-cannot-parse/1.0.0/package.json b/tests/registry/npm/@denotest/dep-cannot-parse/1.0.0/package.json new file mode 100644 index 0000000000..8a069ef729 --- /dev/null +++ b/tests/registry/npm/@denotest/dep-cannot-parse/1.0.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "@denotest/dep-cannot-parse", + "version": "1.0.0", + "dependencies": { + "@denotest/esm-basic": "unknown-scheme:unknown" + } +} diff --git a/tests/specs/npm/dynamic_npm_resolution_failure/__test__.jsonc b/tests/specs/npm/dynamic_npm_resolution_failure/__test__.jsonc new file mode 100644 index 0000000000..f816bad869 --- /dev/null +++ b/tests/specs/npm/dynamic_npm_resolution_failure/__test__.jsonc @@ -0,0 +1,4 @@ +{ + "args": "run -A main.ts", + "output": "main.out" +} diff --git a/tests/specs/npm/dynamic_npm_resolution_failure/main.out b/tests/specs/npm/dynamic_npm_resolution_failure/main.out new file mode 100644 index 0000000000..03c733567b --- /dev/null +++ b/tests/specs/npm/dynamic_npm_resolution_failure/main.out @@ -0,0 +1,15 @@ +[UNORDERED_START] +Download http://localhost:4260/chalk +Download http://localhost:4260/@denotest/dep-cannot-parse +[UNORDERED_END] +Download http://localhost:4260/chalk/chalk-5.0.1.tgz +Hi +TypeError: Error in @denotest/dep-cannot-parse@1.0.0 parsing version requirement for dependency: @denotest/esm-basic@unknown-scheme:unknown + +Invalid npm version requirement. Unexpected character. + unknown-scheme:unknown + ~ + at async file:///[WILDLINE]main.ts:5:3 { + code: "ERR_MODULE_NOT_FOUND" +} +Bye diff --git a/tests/specs/npm/dynamic_npm_resolution_failure/main.ts b/tests/specs/npm/dynamic_npm_resolution_failure/main.ts new file mode 100644 index 0000000000..0096bca485 --- /dev/null +++ b/tests/specs/npm/dynamic_npm_resolution_failure/main.ts @@ -0,0 +1,9 @@ +import chalk from "npm:chalk"; + +console.log(chalk.green("Hi")); +try { + await import("npm:@denotest/dep-cannot-parse"); +} catch (err) { + console.log(err); +} +console.log(chalk.green("Bye"));