summaryrefslogtreecommitdiff
path: root/cli/npm
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm')
-rw-r--r--cli/npm/cache.rs43
-rw-r--r--cli/npm/mod.rs171
-rw-r--r--cli/npm/registry.rs35
-rw-r--r--cli/npm/resolution.rs59
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, &registry_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, &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??;
+ }
}
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(&registry_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(&registry_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(&registry_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(&registry_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");
+ }
+}