diff options
Diffstat (limited to 'cli/npm')
-rw-r--r-- | cli/npm/cache.rs | 43 | ||||
-rw-r--r-- | cli/npm/mod.rs | 171 | ||||
-rw-r--r-- | cli/npm/registry.rs | 35 | ||||
-rw-r--r-- | cli/npm/resolution.rs | 59 |
4 files changed, 247 insertions, 61 deletions
diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index 7270fad2f..0efbe93f7 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; +use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::url::Url; use deno_runtime::colors; @@ -37,20 +38,23 @@ impl Default for ReadonlyNpmCache { // This only gets used when creating the tsc runtime and for testing, and so // it shouldn't ever actually access the DenoDir, so it doesn't support a // custom root. - Self::from_deno_dir(&crate::deno_dir::DenoDir::new(None).unwrap()) + Self::from_deno_dir(&crate::deno_dir::DenoDir::new(None).unwrap()).unwrap() } } impl ReadonlyNpmCache { - pub fn new(root_dir: PathBuf) -> Self { + pub fn new(root_dir: PathBuf) -> Result<Self, AnyError> { + std::fs::create_dir_all(&root_dir) + .with_context(|| format!("Error creating {}", root_dir.display()))?; + let root_dir = crate::fs_util::canonicalize_path(&root_dir)?; let root_dir_url = Url::from_directory_path(&root_dir).unwrap(); - Self { + Ok(Self { root_dir, root_dir_url, - } + }) } - pub fn from_deno_dir(dir: &DenoDir) -> Self { + pub fn from_deno_dir(dir: &DenoDir) -> Result<Self, AnyError> { Self::new(dir.root.join("npm")) } @@ -65,16 +69,14 @@ impl ReadonlyNpmCache { } pub fn package_name_folder(&self, name: &str, registry_url: &Url) -> PathBuf { - let mut dir = self - .root_dir - .join(fs_util::root_url_to_safe_local_dirname(registry_url)); + let mut dir = self.registry_folder(registry_url); let mut parts = name.split('/').map(Cow::Borrowed).collect::<Vec<_>>(); // package names were not always enforced to be lowercase and so we need // to ensure package names, which are therefore case sensitive, are stored // on a case insensitive file system to not have conflicts. We do this by // first putting it in a "_" folder then hashing the package name. if name.to_lowercase() != name { - let mut last_part = parts.last_mut().unwrap(); + let last_part = parts.last_mut().unwrap(); *last_part = Cow::Owned(crate::checksum::gen(&[last_part.as_bytes()])); // We can't just use the hash as part of the directory because it may // have a collision with an actual package name in case someone wanted @@ -90,6 +92,12 @@ impl ReadonlyNpmCache { dir } + pub fn registry_folder(&self, registry_url: &Url) -> PathBuf { + self + .root_dir + .join(fs_util::root_url_to_safe_local_dirname(registry_url)) + } + pub fn resolve_package_id_from_specifier( &self, specifier: &ModuleSpecifier, @@ -147,12 +155,8 @@ impl ReadonlyNpmCache { pub struct NpmCache(ReadonlyNpmCache); impl NpmCache { - pub fn new(root_dir: PathBuf) -> Self { - Self(ReadonlyNpmCache::new(root_dir)) - } - - pub fn from_deno_dir(dir: &DenoDir) -> Self { - Self(ReadonlyNpmCache::from_deno_dir(dir)) + pub fn from_deno_dir(dir: &DenoDir) -> Result<Self, AnyError> { + Ok(Self(ReadonlyNpmCache::from_deno_dir(dir)?)) } pub fn as_readonly(&self) -> ReadonlyNpmCache { @@ -228,6 +232,10 @@ impl NpmCache { self.0.package_name_folder(name, registry_url) } + pub fn registry_folder(&self, registry_url: &Url) -> PathBuf { + self.0.registry_folder(registry_url) + } + pub fn resolve_package_id_from_specifier( &self, specifier: &ModuleSpecifier, @@ -242,7 +250,6 @@ impl NpmCache { #[cfg(test)] mod test { use deno_core::url::Url; - use std::path::PathBuf; use super::ReadonlyNpmCache; use crate::npm::NpmPackageId; @@ -250,7 +257,7 @@ mod test { #[test] fn should_get_lowercase_package_folder() { let root_dir = crate::deno_dir::DenoDir::new(None).unwrap().root; - let cache = ReadonlyNpmCache::new(root_dir.clone()); + let cache = ReadonlyNpmCache::new(root_dir.clone()).unwrap(); let registry_url = Url::parse("https://registry.npmjs.org/").unwrap(); // all lowercase should be as-is @@ -273,7 +280,7 @@ mod test { fn should_handle_non_all_lowercase_package_names() { // it was possible at one point for npm packages to not just be lowercase let root_dir = crate::deno_dir::DenoDir::new(None).unwrap().root; - let cache = ReadonlyNpmCache::new(root_dir.clone()); + let cache = ReadonlyNpmCache::new(root_dir.clone()).unwrap(); let registry_url = Url::parse("https://registry.npmjs.org/").unwrap(); let json_uppercase_hash = "db1a21a0bc2ef8fbe13ac4cf044e8c9116d29137d5ed8b916ab63dcb2d4290df"; diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index d0ffaff2f..810cee645 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -5,15 +5,19 @@ mod registry; mod resolution; mod tarball; +use std::io::ErrorKind; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use deno_ast::ModuleSpecifier; +use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures; use deno_core::url::Url; +use deno_runtime::deno_node::DenoDirNpmResolver; pub use resolution::NpmPackageId; pub use resolution::NpmPackageReference; pub use resolution::NpmPackageReq; @@ -65,7 +69,7 @@ pub trait NpmPackageResolver { } } -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct GlobalNpmPackageResolver { cache: NpmCache, resolution: Arc<NpmResolution>, @@ -73,12 +77,8 @@ pub struct GlobalNpmPackageResolver { } impl GlobalNpmPackageResolver { - pub fn new(root_cache_dir: PathBuf, reload: bool) -> Self { - Self::from_cache(NpmCache::new(root_cache_dir), reload) - } - - pub fn from_deno_dir(dir: &DenoDir, reload: bool) -> Self { - Self::from_cache(NpmCache::from_deno_dir(dir), reload) + pub fn from_deno_dir(dir: &DenoDir, reload: bool) -> Result<Self, AnyError> { + Ok(Self::from_cache(NpmCache::from_deno_dir(dir)?, reload)) } fn from_cache(cache: NpmCache, reload: bool) -> Self { @@ -98,11 +98,6 @@ impl GlobalNpmPackageResolver { self.resolution.has_packages() } - /// Gets all the packages. - pub fn all_packages(&self) -> Vec<NpmResolutionPackage> { - self.resolution.all_packages() - } - /// Adds a package requirement to the resolver. pub async fn add_package_reqs( &self, @@ -113,22 +108,38 @@ impl GlobalNpmPackageResolver { /// Caches all the packages in parallel. pub async fn cache_packages(&self) -> Result<(), AnyError> { - let handles = self.resolution.all_packages().into_iter().map(|package| { - let cache = self.cache.clone(); - let registry_url = self.registry_url.clone(); - tokio::task::spawn(async move { - cache - .ensure_package(&package.id, &package.dist, ®istry_url) + 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 + let mut packages = self.resolution.all_packages(); + packages.sort_by(|a, b| a.id.cmp(&b.id)); + for package in packages { + self + .cache + .ensure_package(&package.id, &package.dist, &self.registry_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??; + })?; + } + } else { + let handles = self.resolution.all_packages().into_iter().map(|package| { + let cache = self.cache.clone(); + let registry_url = self.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(()) } @@ -141,6 +152,7 @@ impl GlobalNpmPackageResolver { } /// Creates an inner clone. + #[allow(unused)] pub fn snapshot(&self) -> NpmPackageResolverSnapshot { NpmPackageResolverSnapshot { cache: self.cache.as_readonly(), @@ -246,3 +258,112 @@ impl NpmPackageResolver for NpmPackageResolverSnapshot { Ok(self.local_package_info(&pkg_id)) } } + +impl DenoDirNpmResolver for GlobalNpmPackageResolver { + 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> { + let registry_path = self.cache.registry_folder(&self.registry_url); + ensure_read_permission(®istry_path, path) + } +} + +impl DenoDirNpmResolver for NpmPackageResolverSnapshot { + 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> { + let registry_path = self.cache.registry_folder(&self.registry_url); + ensure_read_permission(®istry_path, 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()), + } +} + +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/registry.rs b/cli/npm/registry.rs index 016fd6e4a..5da5b6c7f 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -12,6 +12,7 @@ use deno_core::parking_lot::Mutex; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::url::Url; +use deno_runtime::colors; use deno_runtime::deno_fetch::reqwest; use serde::Serialize; @@ -63,8 +64,13 @@ impl NpmPackageVersionInfo { } else { (entry.0.clone(), entry.1.clone()) }; - let version_req = NpmVersionReq::parse(&version_req) - .with_context(|| format!("Dependency: {}", bare_specifier))?; + let version_req = + NpmVersionReq::parse(&version_req).with_context(|| { + format!( + "error parsing version requirement for dependency: {}@{}", + bare_specifier, version_req + ) + })?; Ok(NpmDependencyEntry { bare_specifier, name, @@ -98,7 +104,22 @@ pub struct NpmRegistryApi { impl NpmRegistryApi { pub fn default_url() -> Url { - Url::parse("https://registry.npmjs.org").unwrap() + let env_var_name = "DENO_NPM_REGISTRY"; + if let Ok(registry_url) = std::env::var(env_var_name) { + // ensure there is a trailing slash for the directory + let registry_url = format!("{}/", registry_url.trim_end_matches('/')); + match Url::parse(®istry_url) { + Ok(url) => url, + Err(err) => { + eprintln!("{}: Invalid {} environment variable. Please provide a valid url.\n\n{:#}", + colors::red_bold("error"), + env_var_name, err); + std::process::exit(1); + } + } + } else { + Url::parse("https://registry.npmjs.org").unwrap() + } } pub fn new(cache: NpmCache, reload: bool) -> Self { @@ -125,7 +146,7 @@ impl NpmRegistryApi { let maybe_package_info = self.maybe_package_info(name).await?; match maybe_package_info { Some(package_info) => Ok(package_info), - None => bail!("package '{}' does not exist", name), + None => bail!("npm package '{}' does not exist", name), } } @@ -271,6 +292,7 @@ fn npm_version_req_parse_part( text: &str, ) -> Result<semver::VersionReq, AnyError> { let text = text.trim(); + let text = text.strip_prefix('v').unwrap_or(text); let mut chars = text.chars().enumerate().peekable(); let mut final_text = String::new(); while chars.peek().is_some() { @@ -308,6 +330,11 @@ mod test { } #[test] + pub fn npm_version_req_with_v() { + assert!(NpmVersionReq::parse("v1.0.0").is_ok()); + } + + #[test] pub fn npm_version_req_ranges() { let tester = NpmVersionReqTester( NpmVersionReq::parse(">= 2.1.2 < 3.0.0 || 5.x").unwrap(), diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs index 4caa27330..d92004db0 100644 --- a/cli/npm/resolution.rs +++ b/cli/npm/resolution.rs @@ -105,13 +105,14 @@ impl std::fmt::Display for NpmPackageReference { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct NpmPackageId { pub name: String, pub version: semver::Version, } impl NpmPackageId { + #[allow(unused)] pub fn scope(&self) -> Option<&str> { if self.name.starts_with('@') && self.name.contains('/') { self.name.split('/').next() @@ -169,17 +170,19 @@ impl NpmResolutionSnapshot { referrer: &NpmPackageId, ) -> Result<&NpmResolutionPackage, AnyError> { match self.packages.get(referrer) { - Some(referrer_package) => match referrer_package.dependencies.get(name) { - Some(id) => Ok(self.packages.get(id).unwrap()), - None => { - bail!( - "could not find package '{}' referenced by '{}'", - name, - referrer - ) + Some(referrer_package) => { + match referrer_package.dependencies.get(name_without_path(name)) { + Some(id) => Ok(self.packages.get(id).unwrap()), + None => { + bail!( + "could not find npm package '{}' referenced by '{}'", + name, + referrer + ) + } } - }, - None => bail!("could not find referrer package '{}'", referrer), + } + None => bail!("could not find referrer npm package '{}'", referrer), } } @@ -276,7 +279,7 @@ impl NpmResolution { let dependencies = version_and_info .info .dependencies_as_entries() - .with_context(|| format!("Package: {}", id))?; + .with_context(|| format!("npm package: {}", id))?; pending_dependencies.push_back((id.clone(), dependencies)); snapshot.packages.insert( @@ -334,7 +337,7 @@ impl NpmResolution { .info .dependencies_as_entries() .with_context(|| { - format!("Package: {}@{}", dep.name, version_and_info.version) + format!("npm package: {}@{}", dep.name, version_and_info.version) })?; let id = NpmPackageId { @@ -452,7 +455,7 @@ fn get_resolved_package_version_and_info( // version, but next time to a different version because it has new information. None => bail!( concat!( - "Could not find package '{}' matching {}{}. ", + "Could not find npm package '{}' matching {}{}. ", "Try retreiving the latest npm package information by running with --reload", ), pkg_name, @@ -464,3 +467,31 @@ fn get_resolved_package_version_and_info( ), } } + +fn name_without_path(name: &str) -> &str { + let mut search_start_index = 0; + if name.starts_with('@') { + if let Some(slash_index) = name.find('/') { + search_start_index = slash_index + 1; + } + } + if let Some(slash_index) = &name[search_start_index..].find('/') { + // get the name up until the path slash + &name[0..search_start_index + slash_index] + } else { + name + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name_without_path() { + assert_eq!(name_without_path("foo"), "foo"); + assert_eq!(name_without_path("@foo/bar"), "@foo/bar"); + assert_eq!(name_without_path("@foo/bar/baz"), "@foo/bar"); + assert_eq!(name_without_path("@hello"), "@hello"); + } +} |