diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-09-30 12:06:38 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-30 12:06:38 -0400 |
commit | 8d24be1a59714761665516e0d78d25059608c29b (patch) | |
tree | aed0140b63441008cb9b549d44948f7a36a4f5f1 /cli/npm/managed/mod.rs | |
parent | 1cda3840ff673512f7c6d58fa8402c35c760bc3b (diff) |
refactor(npm): create `cli::npm::managed` module (#20740)
Creates the `cli::npm::managed` module and starts moving more
functionality into it.
Diffstat (limited to 'cli/npm/managed/mod.rs')
-rw-r--r-- | cli/npm/managed/mod.rs | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs new file mode 100644 index 000000000..c5ba3d3af --- /dev/null +++ b/cli/npm/managed/mod.rs @@ -0,0 +1,394 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_ast::ModuleSpecifier; +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::serde_json; +use deno_core::url::Url; +use deno_graph::NpmPackageReqResolution; +use deno_npm::registry::NpmRegistryApi; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::resolution::PackageReqNotFoundError; +use deno_npm::resolution::SerializedNpmResolutionSnapshot; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; +use deno_npm::NpmSystemInfo; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_node::NodePermissions; +use deno_runtime::deno_node::NodeResolutionMode; +use deno_runtime::deno_node::NpmResolver; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageNv; +use deno_semver::package::PackageNvReference; +use deno_semver::package::PackageReq; +use serde::Deserialize; +use serde::Serialize; + +use crate::args::Lockfile; +use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; + +use super::CliNpmRegistryApi; +use super::CliNpmResolver; +use super::InnerCliNpmResolverRef; + +pub use self::installer::PackageJsonDepsInstaller; +pub use self::resolution::NpmResolution; +pub use self::resolvers::create_npm_fs_resolver; +pub use self::resolvers::NpmPackageFsResolver; + +mod installer; +mod resolution; +mod resolvers; + +/// State provided to the process via an environment variable. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NpmProcessState { + pub snapshot: SerializedNpmResolutionSnapshot, + pub local_node_modules_path: Option<String>, +} + +/// An npm resolver where the resolution is managed by Deno rather than +/// the user bringing their own node_modules (BYONM) on the file system. +pub struct ManagedCliNpmResolver { + api: Arc<CliNpmRegistryApi>, + fs: Arc<dyn FileSystem>, + fs_resolver: Arc<dyn NpmPackageFsResolver>, + resolution: Arc<NpmResolution>, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, + package_json_deps_installer: Arc<PackageJsonDepsInstaller>, +} + +impl std::fmt::Debug for ManagedCliNpmResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ManagedNpmResolver") + .field("api", &"<omitted>") + .field("fs", &"<omitted>") + .field("fs_resolver", &"<omitted>") + .field("resolution", &"<omitted>") + .field("maybe_lockfile", &"<omitted>") + .field("package_json_deps_installer", &"<omitted>") + .finish() + } +} + +impl ManagedCliNpmResolver { + pub fn new( + api: Arc<CliNpmRegistryApi>, + fs: Arc<dyn FileSystem>, + resolution: Arc<NpmResolution>, + fs_resolver: Arc<dyn NpmPackageFsResolver>, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, + package_json_deps_installer: Arc<PackageJsonDepsInstaller>, + ) -> Self { + Self { + api, + fs, + fs_resolver, + resolution, + maybe_lockfile, + package_json_deps_installer, + } + } + + pub fn resolve_pkg_folder_from_pkg_id( + &self, + pkg_id: &NpmPackageId, + ) -> Result<PathBuf, AnyError> { + let path = self.fs_resolver.package_folder(pkg_id)?; + let path = canonicalize_path_maybe_not_exists_with_fs(&path, |path| { + self + .fs + .realpath_sync(path) + .map_err(|err| err.into_io_error()) + })?; + log::debug!( + "Resolved package folder of {} to {}", + pkg_id.as_serialized(), + path.display() + ); + Ok(path) + } + + /// Resolves the package nv from the provided specifier. + pub fn resolve_pkg_id_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<Option<NpmPackageId>, AnyError> { + let Some(cache_folder_id) = self + .fs_resolver + .resolve_package_cache_folder_id_from_specifier(specifier)? + else { + return Ok(None); + }; + Ok(Some( + self + .resolution + .resolve_pkg_id_from_pkg_cache_folder_id(&cache_folder_id)?, + )) + } + + pub fn resolve_pkg_reqs_from_pkg_id( + &self, + id: &NpmPackageId, + ) -> Vec<PackageReq> { + self.resolution.resolve_pkg_reqs_from_pkg_id(id) + } + + /// Attempts to get the package size in bytes. + pub fn package_size( + &self, + package_id: &NpmPackageId, + ) -> Result<u64, AnyError> { + let package_folder = self.fs_resolver.package_folder(package_id)?; + Ok(crate::util::fs::dir_size(&package_folder)?) + } + + pub fn all_system_packages( + &self, + system_info: &NpmSystemInfo, + ) -> Vec<NpmResolutionPackage> { + self.resolution.all_system_packages(system_info) + } + + /// Adds package requirements to the resolver and ensures everything is setup. + pub async fn add_package_reqs( + &self, + packages: &[PackageReq], + ) -> Result<(), AnyError> { + if packages.is_empty() { + return Ok(()); + } + + self.resolution.add_package_reqs(packages).await?; + self.fs_resolver.cache_packages().await?; + + // If there's a lock file, update it with all discovered npm packages + if let Some(lockfile_mutex) = &self.maybe_lockfile { + let mut lockfile = lockfile_mutex.lock(); + self.lock(&mut lockfile)?; + } + + Ok(()) + } + + /// Sets package requirements to the resolver, removing old requirements and adding new ones. + /// + /// This will retrieve and resolve package information, but not cache any package files. + pub async fn set_package_reqs( + &self, + packages: &[PackageReq], + ) -> Result<(), AnyError> { + self.resolution.set_package_reqs(packages).await + } + + pub fn snapshot(&self) -> NpmResolutionSnapshot { + self.resolution.snapshot() + } + + pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { + self.resolution.lock(lockfile) + } + + pub async fn inject_synthetic_types_node_package( + &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.fs_resolver.cache_packages().await?; + + Ok(()) + } + + pub async fn resolve_pending(&self) -> Result<(), AnyError> { + self.resolution.resolve_pending().await?; + self.fs_resolver.cache_packages().await?; + Ok(()) + } + + fn resolve_pkg_id_from_pkg_req( + &self, + req: &PackageReq, + ) -> Result<NpmPackageId, PackageReqNotFoundError> { + self.resolution.resolve_pkg_id_from_pkg_req(req) + } + + pub async fn ensure_top_level_package_json_install( + &self, + ) -> Result<(), AnyError> { + self + .package_json_deps_installer + .ensure_top_level_install() + .await + } + + pub async fn cache_package_info( + &self, + package_name: &str, + ) -> Result<(), AnyError> { + // this will internally cache the package information + self + .api + .package_info(package_name) + .await + .map(|_| ()) + .map_err(|err| err.into()) + } +} + +impl NpmResolver for ManagedCliNpmResolver { + fn resolve_package_folder_from_package( + &self, + name: &str, + referrer: &ModuleSpecifier, + mode: NodeResolutionMode, + ) -> Result<PathBuf, AnyError> { + let path = self + .fs_resolver + .resolve_package_folder_from_package(name, referrer, mode)?; + log::debug!("Resolved {} from {} to {}", name, referrer, path.display()); + Ok(path) + } + + fn resolve_package_folder_from_path( + &self, + specifier: &ModuleSpecifier, + ) -> Result<Option<PathBuf>, AnyError> { + self.resolve_pkg_folder_from_specifier(specifier) + } + + fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { + let root_dir_url = self.fs_resolver.root_dir_url(); + debug_assert!(root_dir_url.as_str().ends_with('/')); + specifier.as_ref().starts_with(root_dir_url.as_str()) + } + + fn ensure_read_permission( + &self, + permissions: &dyn NodePermissions, + path: &Path, + ) -> Result<(), AnyError> { + self.fs_resolver.ensure_read_permission(permissions, path) + } +} + +impl CliNpmResolver for ManagedCliNpmResolver { + fn into_npm_resolver(self: Arc<Self>) -> Arc<dyn NpmResolver> { + self + } + + fn root_dir_url(&self) -> &Url { + self.fs_resolver.root_dir_url() + } + + fn as_inner(&self) -> InnerCliNpmResolverRef { + InnerCliNpmResolverRef::Managed(self) + } + + fn node_modules_path(&self) -> Option<PathBuf> { + self.fs_resolver.node_modules_path() + } + + /// Checks if the provided package req's folder is cached. + fn is_pkg_req_folder_cached(&self, req: &PackageReq) -> bool { + self + .resolve_pkg_id_from_pkg_req(req) + .ok() + .and_then(|id| self.fs_resolver.package_folder(&id).ok()) + .map(|folder| folder.exists()) + .unwrap_or(false) + } + + fn resolve_npm_for_deno_graph( + &self, + pkg_req: &PackageReq, + ) -> NpmPackageReqResolution { + let result = self.resolution.resolve_pkg_req_as_pending(pkg_req); + match result { + Ok(nv) => NpmPackageReqResolution::Ok(nv), + Err(err) => { + if self.api.mark_force_reload() { + log::debug!("Restarting npm specifier resolution to check for new registry information. Error: {:#}", err); + NpmPackageReqResolution::ReloadRegistryInfo(err.into()) + } else { + NpmPackageReqResolution::Err(err.into()) + } + } + } + } + + fn resolve_pkg_nv_ref_from_pkg_req_ref( + &self, + req_ref: &NpmPackageReqReference, + ) -> Result<NpmPackageNvReference, PackageReqNotFoundError> { + let pkg_nv = self + .resolve_pkg_id_from_pkg_req(req_ref.req()) + .map(|id| id.nv)?; + Ok(NpmPackageNvReference::new(PackageNvReference { + nv: pkg_nv, + sub_path: req_ref.sub_path().map(|s| s.to_string()), + })) + } + + /// Resolve the root folder of the package the provided specifier is in. + /// + /// This will error when the provided specifier is not in an npm package. + fn resolve_pkg_folder_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<Option<PathBuf>, AnyError> { + let Some(path) = self + .fs_resolver + .resolve_package_folder_from_specifier(specifier)? + else { + return Ok(None); + }; + log::debug!( + "Resolved package folder of {} to {}", + specifier, + path.display() + ); + Ok(Some(path)) + } + + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + ) -> Result<PathBuf, AnyError> { + let pkg_id = self.resolve_pkg_id_from_pkg_req(req)?; + self.resolve_pkg_folder_from_pkg_id(&pkg_id) + } + + fn resolve_pkg_folder_from_deno_module( + &self, + nv: &PackageNv, + ) -> Result<PathBuf, AnyError> { + let pkg_id = self.resolution.resolve_pkg_id_from_deno_module(nv)?; + self.resolve_pkg_folder_from_pkg_id(&pkg_id) + } + + /// Gets the state of npm for the process. + fn get_npm_process_state(&self) -> String { + serde_json::to_string(&NpmProcessState { + snapshot: self + .resolution + .serialized_valid_snapshot() + .into_serialized(), + local_node_modules_path: self + .fs_resolver + .node_modules_path() + .map(|p| p.to_string_lossy().to_string()), + }) + .unwrap() + } + + fn package_reqs(&self) -> HashMap<PackageReq, PackageNv> { + self.resolution.package_reqs() + } +} |