diff options
Diffstat (limited to 'cli/npm/resolution.rs')
-rw-r--r-- | cli/npm/resolution.rs | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs new file mode 100644 index 000000000..291acf4bc --- /dev/null +++ b/cli/npm/resolution.rs @@ -0,0 +1,312 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashSet; +use std::sync::Arc; + +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::parking_lot::RwLock; +use deno_core::TaskQueue; +use deno_lockfile::NpmPackageDependencyLockfileInfo; +use deno_lockfile::NpmPackageLockfileInfo; +use deno_npm::registry::NpmPackageInfo; +use deno_npm::resolution::NpmPackagesPartitioned; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::NpmPackageCacheFolderId; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmPackageReqReference; + +use crate::args::Lockfile; + +use super::registry::NpmRegistry; + +/// 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. +/// +/// This does not interact with the file system. +#[derive(Clone)] +pub struct NpmResolution(Arc<NpmResolutionInner>); + +struct NpmResolutionInner { + api: NpmRegistry, + snapshot: RwLock<NpmResolutionSnapshot>, + update_queue: TaskQueue, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, +} + +impl std::fmt::Debug for NpmResolution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let snapshot = self.0.snapshot.read(); + f.debug_struct("NpmResolution") + .field("snapshot", &snapshot) + .finish() + } +} + +impl NpmResolution { + pub fn new( + api: NpmRegistry, + initial_snapshot: Option<NpmResolutionSnapshot>, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, + ) -> Self { + Self(Arc::new(NpmResolutionInner { + api, + snapshot: RwLock::new(initial_snapshot.unwrap_or_default()), + update_queue: Default::default(), + maybe_lockfile, + })) + } + + pub async fn add_package_reqs( + &self, + package_reqs: Vec<NpmPackageReq>, + ) -> Result<(), AnyError> { + let inner = &self.0; + + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + package_reqs, + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + Ok(()) + } + + pub async fn set_package_reqs( + &self, + package_reqs: Vec<NpmPackageReq>, + ) -> Result<(), AnyError> { + let inner = &self.0; + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let reqs_set = package_reqs.iter().collect::<HashSet<_>>(); + let has_removed_package = !snapshot + .package_reqs() + .keys() + .all(|req| reqs_set.contains(req)); + // if any packages were removed, we need to completely recreate the npm resolution snapshot + let snapshot = if has_removed_package { + NpmResolutionSnapshot::default() + } else { + snapshot + }; + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + package_reqs, + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + + Ok(()) + } + + pub async fn resolve_pending(&self) -> Result<(), AnyError> { + let inner = &self.0; + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + Vec::new(), + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + + Ok(()) + } + + pub fn pkg_req_ref_to_nv_ref( + &self, + req_ref: NpmPackageReqReference, + ) -> Result<NpmPackageNvReference, AnyError> { + let node_id = self.resolve_pkg_id_from_pkg_req(&req_ref.req)?; + Ok(NpmPackageNvReference { + nv: node_id.nv, + sub_path: req_ref.sub_path, + }) + } + + pub fn resolve_package_cache_folder_id_from_id( + &self, + id: &NpmPackageId, + ) -> Option<NpmPackageCacheFolderId> { + self + .0 + .snapshot + .read() + .package_from_id(id) + .map(|p| p.get_package_cache_folder_id()) + } + + pub fn resolve_package_from_package( + &self, + name: &str, + referrer: &NpmPackageCacheFolderId, + ) -> Result<NpmResolutionPackage, AnyError> { + self + .0 + .snapshot + .read() + .resolve_package_from_package(name, referrer) + .cloned() + } + + /// Resolve a node package from a deno module. + pub fn resolve_pkg_id_from_pkg_req( + &self, + req: &NpmPackageReq, + ) -> Result<NpmPackageId, AnyError> { + self + .0 + .snapshot + .read() + .resolve_pkg_from_pkg_req(req) + .map(|pkg| pkg.pkg_id.clone()) + } + + pub fn resolve_pkg_id_from_deno_module( + &self, + id: &NpmPackageNv, + ) -> Result<NpmPackageId, AnyError> { + self + .0 + .snapshot + .read() + .resolve_package_from_deno_module(id) + .map(|pkg| 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 fn resolve_package_req_as_pending( + &self, + pkg_req: &NpmPackageReq, + ) -> Result<NpmPackageNv, AnyError> { + // we should always have this because it should have been cached before here + let package_info = + self.0.api.get_cached_package_info(&pkg_req.name).unwrap(); + self.resolve_package_req_as_pending_with_info(pkg_req, &package_info) + } + + /// 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 fn resolve_package_req_as_pending_with_info( + &self, + pkg_req: &NpmPackageReq, + package_info: &NpmPackageInfo, + ) -> Result<NpmPackageNv, AnyError> { + debug_assert_eq!(pkg_req.name, package_info.name); + let inner = &self.0; + let mut snapshot = inner.snapshot.write(); + let nv = snapshot.resolve_package_req_as_pending(pkg_req, package_info)?; + Ok(nv) + } + + pub fn all_packages_partitioned(&self) -> NpmPackagesPartitioned { + self.0.snapshot.read().all_packages_partitioned() + } + + pub fn has_packages(&self) -> bool { + !self.0.snapshot.read().is_empty() + } + + pub fn snapshot(&self) -> NpmResolutionSnapshot { + self.0.snapshot.read().clone() + } + + pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { + let snapshot = self.0.snapshot.read(); + populate_lockfile_from_snapshot(lockfile, &snapshot) + } +} + +async fn add_package_reqs_to_snapshot( + api: &NpmRegistry, + package_reqs: Vec<NpmPackageReq>, + snapshot: NpmResolutionSnapshot, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, +) -> Result<NpmResolutionSnapshot, AnyError> { + if !snapshot.has_pending() + && package_reqs + .iter() + .all(|req| snapshot.package_reqs().contains_key(req)) + { + return Ok(snapshot); // already up to date + } + + let result = snapshot.resolve_pending(package_reqs, api).await; + api.clear_memory_cache(); + let snapshot = result?; // propagate the error after clearing the memory cache + + if let Some(lockfile_mutex) = maybe_lockfile { + let mut lockfile = lockfile_mutex.lock(); + populate_lockfile_from_snapshot(&mut lockfile, &snapshot)?; + Ok(snapshot) + } else { + Ok(snapshot) + } +} + +fn populate_lockfile_from_snapshot( + lockfile: &mut Lockfile, + snapshot: &NpmResolutionSnapshot, +) -> Result<(), AnyError> { + for (package_req, nv) in snapshot.package_reqs() { + lockfile.insert_npm_specifier( + package_req.to_string(), + snapshot + .resolve_package_from_deno_module(nv) + .unwrap() + .pkg_id + .as_serialized(), + ); + } + for package in snapshot.all_packages() { + lockfile + .check_or_insert_npm_package(npm_package_to_lockfile_info(package))?; + } + Ok(()) +} + +fn npm_package_to_lockfile_info( + pkg: NpmResolutionPackage, +) -> NpmPackageLockfileInfo { + let dependencies = pkg + .dependencies + .into_iter() + .map(|(name, id)| NpmPackageDependencyLockfileInfo { + name, + id: id.as_serialized(), + }) + .collect(); + + NpmPackageLockfileInfo { + display_id: pkg.pkg_id.nv.to_string(), + serialized_id: pkg.pkg_id.as_serialized(), + integrity: pkg.dist.integrity().to_string(), + dependencies, + } +} |