diff options
Diffstat (limited to 'cli/npm/resolvers')
-rw-r--r-- | cli/npm/resolvers/common.rs | 89 | ||||
-rw-r--r-- | cli/npm/resolvers/global.rs | 138 | ||||
-rw-r--r-- | cli/npm/resolvers/mod.rs | 158 |
3 files changed, 385 insertions, 0 deletions
diff --git a/cli/npm/resolvers/common.rs b/cli/npm/resolvers/common.rs new file mode 100644 index 000000000..f0231859a --- /dev/null +++ b/cli/npm/resolvers/common.rs @@ -0,0 +1,89 @@ +use std::path::Path; +use std::path::PathBuf; + +use deno_ast::ModuleSpecifier; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::future::BoxFuture; +use deno_core::url::Url; + +use crate::npm::NpmCache; +use crate::npm::NpmPackageId; +use crate::npm::NpmPackageReq; +use crate::npm::NpmResolutionPackage; + +/// Information about the local npm package. +pub struct LocalNpmPackageInfo { + /// Unique identifier. + pub id: NpmPackageId, + /// Local folder path of the npm package. + pub folder_path: PathBuf, +} + +pub trait InnerNpmPackageResolver: Send + Sync { + fn resolve_package_from_deno_module( + &self, + pkg_req: &NpmPackageReq, + ) -> Result<LocalNpmPackageInfo, AnyError>; + + fn resolve_package_from_package( + &self, + name: &str, + referrer: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError>; + + fn resolve_package_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError>; + + fn has_packages(&self) -> bool; + + fn add_package_reqs( + &self, + packages: Vec<NpmPackageReq>, + ) -> BoxFuture<'static, Result<(), AnyError>>; + + fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError>; +} + +/// Caches all the packages in parallel. +pub async fn cache_packages( + mut packages: Vec<NpmResolutionPackage>, + cache: &NpmCache, + registry_url: &Url, +) -> Result<(), AnyError> { + if std::env::var("DENO_UNSTABLE_NPM_SYNC_DOWNLOAD") == Ok("1".to_string()) { + // for some of the tests, we want downloading of packages + // to be deterministic so that the output is always the same + packages.sort_by(|a, b| a.id.cmp(&b.id)); + for package in packages { + cache + .ensure_package(&package.id, &package.dist, registry_url) + .await + .with_context(|| { + format!("Failed caching npm package '{}'.", package.id) + })?; + } + } else { + let handles = packages.into_iter().map(|package| { + let cache = cache.clone(); + let registry_url = registry_url.clone(); + tokio::task::spawn(async move { + cache + .ensure_package(&package.id, &package.dist, ®istry_url) + .await + .with_context(|| { + format!("Failed caching npm package '{}'.", package.id) + }) + }) + }); + let results = futures::future::join_all(handles).await; + for result in results { + // surface the first error + result??; + } + } + Ok(()) +} diff --git a/cli/npm/resolvers/global.rs b/cli/npm/resolvers/global.rs new file mode 100644 index 000000000..259d9b9a0 --- /dev/null +++ b/cli/npm/resolvers/global.rs @@ -0,0 +1,138 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::io::ErrorKind; +use std::path::Path; +use std::sync::Arc; + +use deno_ast::ModuleSpecifier; +use deno_core::error::AnyError; +use deno_core::futures::future::BoxFuture; +use deno_core::futures::FutureExt; +use deno_core::url::Url; + +use crate::npm::resolution::NpmResolution; +use crate::npm::resolvers::common::cache_packages; +use crate::npm::NpmCache; +use crate::npm::NpmPackageId; +use crate::npm::NpmPackageReq; +use crate::npm::NpmRegistryApi; + +use super::common::InnerNpmPackageResolver; +use super::common::LocalNpmPackageInfo; + +#[derive(Debug, Clone)] +pub struct GlobalNpmPackageResolver { + cache: NpmCache, + resolution: Arc<NpmResolution>, + registry_url: Url, +} + +impl GlobalNpmPackageResolver { + pub fn new(cache: NpmCache, api: NpmRegistryApi) -> Self { + let registry_url = api.base_url().to_owned(); + let resolution = Arc::new(NpmResolution::new(api)); + + Self { + cache, + resolution, + registry_url, + } + } + + fn local_package_info(&self, id: &NpmPackageId) -> LocalNpmPackageInfo { + LocalNpmPackageInfo { + folder_path: self.cache.package_folder(id, &self.registry_url), + id: id.clone(), + } + } +} + +impl InnerNpmPackageResolver for GlobalNpmPackageResolver { + fn resolve_package_from_deno_module( + &self, + pkg_req: &NpmPackageReq, + ) -> Result<LocalNpmPackageInfo, AnyError> { + let pkg = self.resolution.resolve_package_from_deno_module(pkg_req)?; + Ok(self.local_package_info(&pkg.id)) + } + + fn resolve_package_from_package( + &self, + name: &str, + referrer: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError> { + let referrer_pkg_id = self + .cache + .resolve_package_id_from_specifier(referrer, &self.registry_url)?; + let pkg = self + .resolution + .resolve_package_from_package(name, &referrer_pkg_id)?; + Ok(self.local_package_info(&pkg.id)) + } + + fn resolve_package_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError> { + let pkg_id = self + .cache + .resolve_package_id_from_specifier(specifier, &self.registry_url)?; + Ok(self.local_package_info(&pkg_id)) + } + + fn has_packages(&self) -> bool { + self.resolution.has_packages() + } + + fn add_package_reqs( + &self, + packages: Vec<NpmPackageReq>, + ) -> BoxFuture<'static, Result<(), AnyError>> { + let resolver = self.clone(); + async move { + resolver.resolution.add_package_reqs(packages).await?; + cache_packages( + resolver.resolution.all_packages(), + &resolver.cache, + &resolver.registry_url, + ) + .await + } + .boxed() + } + + fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError> { + let registry_path = self.cache.registry_folder(&self.registry_url); + ensure_read_permission(®istry_path, path) + } +} + +fn ensure_read_permission( + registry_path: &Path, + path: &Path, +) -> Result<(), AnyError> { + // allow reading if it's in the deno_dir node modules + if path.starts_with(®istry_path) + && path + .components() + .all(|c| !matches!(c, std::path::Component::ParentDir)) + { + // todo(dsherret): cache this? + if let Ok(registry_path) = std::fs::canonicalize(registry_path) { + match std::fs::canonicalize(path) { + Ok(path) if path.starts_with(registry_path) => { + return Ok(()); + } + Err(e) if e.kind() == ErrorKind::NotFound => { + return Ok(()); + } + _ => {} // ignore + } + } + } + + Err(deno_core::error::custom_error( + "PermissionDenied", + format!("Reading {} is not allowed", path.display()), + )) +} diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs new file mode 100644 index 000000000..02e5be983 --- /dev/null +++ b/cli/npm/resolvers/mod.rs @@ -0,0 +1,158 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +mod common; +mod global; + +use deno_core::anyhow::bail; +use deno_core::error::custom_error; +use deno_runtime::deno_node::RequireNpmResolver; +use global::GlobalNpmPackageResolver; + +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_ast::ModuleSpecifier; +use deno_core::error::AnyError; + +use self::common::InnerNpmPackageResolver; +use super::NpmCache; +use super::NpmPackageReq; +use super::NpmRegistryApi; + +pub use self::common::LocalNpmPackageInfo; + +#[derive(Clone)] +pub struct NpmPackageResolver { + unstable: bool, + no_npm: bool, + inner: Arc<dyn InnerNpmPackageResolver>, +} + +impl NpmPackageResolver { + pub fn new( + cache: NpmCache, + api: NpmRegistryApi, + unstable: bool, + no_npm: bool, + ) -> Self { + // For now, always create a GlobalNpmPackageResolver, but in the future + // this might be a local node_modules folder + let inner = Arc::new(GlobalNpmPackageResolver::new(cache, api)); + Self { + unstable, + no_npm, + inner, + } + } + + /// Resolves an npm package from a Deno module. + pub fn resolve_package_from_deno_module( + &self, + pkg_req: &NpmPackageReq, + ) -> Result<LocalNpmPackageInfo, AnyError> { + self.inner.resolve_package_from_deno_module(pkg_req) + } + + /// Resolves an npm package from an npm package referrer. + pub fn resolve_package_from_package( + &self, + name: &str, + referrer: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError> { + self.inner.resolve_package_from_package(name, referrer) + } + + /// 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. + pub fn resolve_package_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<LocalNpmPackageInfo, AnyError> { + self.inner.resolve_package_from_specifier(specifier) + } + + /// Gets if the provided specifier is in an npm package. + pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { + self.resolve_package_from_specifier(specifier).is_ok() + } + + /// If the resolver has resolved any npm packages. + pub fn has_packages(&self) -> bool { + self.inner.has_packages() + } + + /// Adds a package requirement to the resolver and ensures everything is setup. + pub async fn add_package_reqs( + &self, + packages: Vec<NpmPackageReq>, + ) -> Result<(), AnyError> { + assert!(!packages.is_empty()); + + if !self.unstable { + bail!( + "Unstable use of npm specifiers. The --unstable flag must be provided." + ) + } + + if self.no_npm { + let fmt_reqs = packages + .iter() + .map(|p| format!("\"{}\"", p)) + .collect::<Vec<_>>() + .join(", "); + return Err(custom_error( + "NoNpm", + format!( + "Following npm specifiers were requested: {}; but --no-npm is specified.", + fmt_reqs + ), + )); + } + + self.inner.add_package_reqs(packages).await + } +} + +impl RequireNpmResolver for NpmPackageResolver { + fn resolve_package_folder_from_package( + &self, + specifier: &str, + referrer: &std::path::Path, + ) -> Result<PathBuf, AnyError> { + let referrer = specifier_to_path(referrer)?; + self + .resolve_package_from_package(specifier, &referrer) + .map(|p| p.folder_path) + } + + fn resolve_package_folder_from_path( + &self, + path: &Path, + ) -> Result<PathBuf, AnyError> { + let specifier = specifier_to_path(path)?; + self + .resolve_package_from_specifier(&specifier) + .map(|p| p.folder_path) + } + + fn in_npm_package(&self, path: &Path) -> bool { + let specifier = match ModuleSpecifier::from_file_path(path) { + Ok(p) => p, + Err(_) => return false, + }; + self.resolve_package_from_specifier(&specifier).is_ok() + } + + fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError> { + self.inner.ensure_read_permission(path) + } +} + +fn specifier_to_path(path: &Path) -> Result<ModuleSpecifier, AnyError> { + match ModuleSpecifier::from_file_path(&path) { + Ok(specifier) => Ok(specifier), + Err(()) => bail!("Could not convert '{}' to url.", path.display()), + } +} |