// Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; use std::path::PathBuf; use std::sync::Arc; use deno_error::JsErrorBox; use deno_npm::NpmSystemInfo; use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryPackageInfoLoadError; use deno_npm_cache::NpmCache; use deno_npm_cache::NpmCacheHttpClient; use deno_resolver::lockfile::LockfileLock; use deno_resolver::npm::managed::NpmResolutionCell; use deno_resolver::workspace::WorkspaceNpmLinkPackages; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; mod bin_entries; mod extra_info; mod factory; mod flag; mod fs; mod global; pub mod graph; pub mod initializer; pub mod lifecycle_scripts; mod local; pub mod package_json; pub mod process_state; pub mod resolution; mod rt; pub use bin_entries::BinEntries; pub use bin_entries::BinEntriesError; use deno_terminal::colors; use deno_unsync::sync::AtomicFlag; use deno_unsync::sync::TaskQueue; use parking_lot::Mutex; use rustc_hash::FxHashSet; pub use self::extra_info::CachedNpmPackageExtraInfoProvider; pub use self::extra_info::ExpectedExtraInfo; pub use self::extra_info::NpmPackageExtraInfoProvider; use self::extra_info::NpmPackageExtraInfoProviderSys; pub use self::factory::InstallReporter; pub use self::factory::NpmInstallerFactory; pub use self::factory::NpmInstallerFactoryOptions; pub use self::factory::NpmInstallerFactorySys; use self::global::GlobalNpmPackageInstaller; use self::initializer::NpmResolutionInitializer; use self::lifecycle_scripts::LifecycleScriptsExecutor; use self::local::LocalNpmInstallSys; use self::local::LocalNpmPackageInstaller; pub use self::local::LocalSetupCache; use self::package_json::NpmInstallDepsProvider; use self::package_json::PackageJsonDepValueParseWithLocationError; use self::resolution::AddPkgReqsResult; use self::resolution::NpmResolutionInstaller; use self::resolution::NpmResolutionInstallerSys; #[derive(Debug, Clone, PartialEq, Eq)] pub enum PackageCaching<'a> { Only(Cow<'a, [PackageReq]>), All, } #[derive(Debug, Clone, Eq, PartialEq, Default)] /// The set of npm packages that are allowed to run lifecycle scripts. pub enum PackagesAllowedScripts { All, Some(Vec), #[default] None, } /// Info needed to run NPM lifecycle scripts #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct LifecycleScriptsConfig { pub allowed: PackagesAllowedScripts, pub initial_cwd: PathBuf, pub root_dir: PathBuf, /// Part of an explicit `deno install` pub explicit_install: bool, } pub trait InstallProgressReporter: std::fmt::Debug + Send + Sync + 'static { fn blocking(&self, message: &str); fn initializing(&self, nv: &PackageNv); fn initialized(&self, nv: &PackageNv); fn scripts_not_run_warning( &self, warning: crate::lifecycle_scripts::LifecycleScriptsWarning, ); fn deprecated_message(&self, message: String); } pub trait Reporter: std::fmt::Debug + Send + Sync + 'static + dyn_clone::DynClone { type Guard; type ClearGuard; fn on_blocking(&self, message: &str) -> Self::Guard; fn on_initializing(&self, message: &str) -> Self::Guard; fn clear_guard(&self) -> Self::ClearGuard; } #[derive(Debug, Clone)] pub struct LogReporter; impl Reporter for LogReporter { type Guard = (); type ClearGuard = (); fn on_blocking(&self, message: &str) -> Self::Guard { log::info!("{} {}", deno_terminal::colors::cyan("Blocking"), message); } fn on_initializing(&self, message: &str) -> Self::Guard { log::info!("{} {}", deno_terminal::colors::green("Initialize"), message); } fn clear_guard(&self) -> Self::ClearGuard {} } /// Part of the resolution that interacts with the file system. #[async_trait::async_trait(?Send)] pub(crate) trait NpmPackageFsInstaller: std::fmt::Debug + Send + Sync { async fn cache_packages<'a>( &self, caching: PackageCaching<'a>, ) -> Result<(), JsErrorBox>; } #[sys_traits::auto_impl] pub trait NpmInstallerSys: NpmResolutionInstallerSys + LocalNpmInstallSys + NpmPackageExtraInfoProviderSys { } #[derive(Debug)] pub struct NpmInstaller< TNpmCacheHttpClient: NpmCacheHttpClient, TSys: NpmInstallerSys, > { fs_installer: Arc, npm_install_deps_provider: Arc, npm_resolution_initializer: Arc>, npm_resolution_installer: Arc>, maybe_lockfile: Option>>, npm_resolution: Arc, top_level_install_flag: AtomicFlag, install_queue: TaskQueue, cached_reqs: Mutex>, } impl NpmInstaller { #[allow(clippy::too_many_arguments)] pub fn new( lifecycle_scripts_executor: Arc, npm_cache: Arc>, npm_install_deps_provider: Arc, npm_registry_info_provider: Arc< dyn deno_npm::registry::NpmRegistryApi + Send + Sync, >, npm_resolution: Arc, npm_resolution_initializer: Arc>, npm_resolution_installer: Arc< NpmResolutionInstaller, >, reporter: &TReporter, sys: TSys, tarball_cache: Arc>, maybe_lockfile: Option>>, maybe_node_modules_path: Option, lifecycle_scripts: LifecycleScriptsConfig, system_info: NpmSystemInfo, workspace_link_packages: Arc, install_reporter: Option>, ) -> Self { let fs_installer: Arc = match maybe_node_modules_path { Some(node_modules_folder) => Arc::new(LocalNpmPackageInstaller::new( lifecycle_scripts_executor, npm_cache.clone(), Arc::new(NpmPackageExtraInfoProvider::new( npm_registry_info_provider, Arc::new(sys.clone()), workspace_link_packages, )), npm_install_deps_provider.clone(), dyn_clone::clone(reporter), npm_resolution.clone(), sys, tarball_cache, node_modules_folder, lifecycle_scripts, system_info, install_reporter, )), None => Arc::new(GlobalNpmPackageInstaller::new( npm_cache, tarball_cache, sys, npm_resolution.clone(), lifecycle_scripts, system_info, install_reporter, )), }; Self { fs_installer, npm_install_deps_provider, npm_resolution, npm_resolution_initializer, npm_resolution_installer, maybe_lockfile, top_level_install_flag: Default::default(), install_queue: Default::default(), cached_reqs: Default::default(), } } /// Adds package requirements to the resolver and ensures everything is setup. /// This includes setting up the `node_modules` directory, if applicable. pub async fn add_and_cache_package_reqs( &self, packages: &[PackageReq], ) -> Result<(), JsErrorBox> { self.npm_resolution_initializer.ensure_initialized().await?; self .add_package_reqs_raw( packages, Some(PackageCaching::Only(packages.into())), ) .await .dependencies_result } pub async fn add_package_reqs_no_cache( &self, packages: &[PackageReq], ) -> Result<(), JsErrorBox> { self.npm_resolution_initializer.ensure_initialized().await?; self .add_package_reqs_raw(packages, None) .await .dependencies_result } pub async fn add_package_reqs( &self, packages: &[PackageReq], caching: PackageCaching<'_>, ) -> Result<(), JsErrorBox> { self.npm_resolution_initializer.ensure_initialized().await?; self .add_package_reqs_raw(packages, Some(caching)) .await .dependencies_result } pub async fn add_package_reqs_raw( &self, packages: &[PackageReq], caching: Option>, ) -> AddPkgReqsResult { if packages.is_empty() { return AddPkgReqsResult { dependencies_result: Ok(()), results: vec![], }; } #[cfg(debug_assertions)] self.npm_resolution_initializer.debug_assert_initialized(); let mut result = self .npm_resolution_installer .add_package_reqs(packages) .await; if result.dependencies_result.is_ok() && let Some(lockfile) = self.maybe_lockfile.as_ref() { result.dependencies_result = lockfile.error_if_changed(); } if result.dependencies_result.is_ok() && let Some(caching) = caching { // the async mutex is unfortunate, but needed to handle the edge case where two workers // try to cache the same package at the same time. we need to hold the lock while we cache // and since that crosses an await point, we need the async mutex. // // should have a negligible perf impact because acquiring the lock is still in the order of nanoseconds // while caching typically takes micro or milli seconds. let _permit = self.install_queue.acquire().await; let uncached = { let cached_reqs = self.cached_reqs.lock(); packages .iter() .filter(|req| !cached_reqs.contains(req)) .collect::>() }; if !uncached.is_empty() { result.dependencies_result = self.cache_packages(caching).await; if result.dependencies_result.is_ok() { let mut cached_reqs = self.cached_reqs.lock(); for req in uncached { cached_reqs.insert(req.clone()); } } } } result } pub async fn inject_synthetic_types_node_package( &self, ) -> Result<(), JsErrorBox> { self.npm_resolution_initializer.ensure_initialized().await?; // don't inject this if it's already been added if self .npm_resolution .any_top_level_package(|id| id.nv.name == "@types/node") { return Ok(()); } let reqs = &[PackageReq::from_str("@types/node").unwrap()]; self .add_package_reqs(reqs, PackageCaching::Only(reqs.into())) .await?; Ok(()) } pub async fn cache_package_info( &self, package_name: &str, ) -> Result, NpmRegistryPackageInfoLoadError> { self .npm_resolution_installer .cache_package_info(package_name) .await } pub async fn cache_packages( &self, caching: PackageCaching<'_>, ) -> Result<(), JsErrorBox> { self.npm_resolution_initializer.ensure_initialized().await?; self.fs_installer.cache_packages(caching).await } pub fn ensure_no_pkg_json_dep_errors( &self, ) -> Result<(), Box> { for err in self.npm_install_deps_provider.pkg_json_dep_errors() { match err.source.as_kind() { deno_package_json::PackageJsonDepValueParseErrorKind::VersionReq(_) | deno_package_json::PackageJsonDepValueParseErrorKind::JsrVersionReq( _, ) => { return Err(Box::new(err.clone())); } deno_package_json::PackageJsonDepValueParseErrorKind::Unsupported { scheme, } if scheme == "jsr" => { return Err(Box::new(err.clone())); } deno_package_json::PackageJsonDepValueParseErrorKind::Unsupported { .. } => { // only warn for this one log::warn!( "{} {}\n at {}", colors::yellow("Warning"), err.source, err.location, ) } } } Ok(()) } /// Ensures that the top level `package.json` dependencies are installed. /// This may set up the `node_modules` directory. /// /// Returns `true` if the top level packages are already installed. A /// return value of `false` means that new packages were added to the NPM resolution. pub async fn ensure_top_level_package_json_install( &self, ) -> Result { if !self.top_level_install_flag.raise() { return Ok(true); // already did this } self.npm_resolution_initializer.ensure_initialized().await?; let pkg_json_remote_pkgs = self.npm_install_deps_provider.remote_pkgs(); if pkg_json_remote_pkgs.is_empty() { return Ok(true); } // check if something needs resolving before bothering to load all // the package information (which is slow) if pkg_json_remote_pkgs.iter().all(|pkg| { self .npm_resolution .resolve_pkg_id_from_pkg_req(&pkg.req) .is_ok() }) { log::debug!( "All package.json deps resolvable. Skipping top level install." ); return Ok(true); // everything is already resolvable } let pkg_reqs = pkg_json_remote_pkgs .iter() .map(|pkg| pkg.req.clone()) .collect::>(); self.add_package_reqs_no_cache(&pkg_reqs).await?; Ok(false) } }