diff options
Diffstat (limited to 'cli/npm')
-rw-r--r-- | cli/npm/mod.rs | 1 | ||||
-rw-r--r-- | cli/npm/registry.rs | 97 | ||||
-rw-r--r-- | cli/npm/resolution.rs | 89 | ||||
-rw-r--r-- | cli/npm/version_req.rs | 219 |
4 files changed, 277 insertions, 129 deletions
diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 7de0f39ee..0e5c07914 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -4,6 +4,7 @@ mod cache; mod registry; mod resolution; mod tarball; +mod version_req; use std::io::ErrorKind; use std::path::Path; diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index e04531017..3b7dd4251 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -23,7 +23,7 @@ use crate::fs_util; use crate::http_cache::CACHE_PERM; use super::cache::NpmCache; -use super::resolution::NpmVersionMatcher; +use super::version_req::NpmVersionReq; // npm registry docs: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md @@ -320,98 +320,3 @@ impl NpmRegistryApi { name_folder_path.join("registry.json") } } - -/// A version requirement found in an npm package's dependencies. -pub struct NpmVersionReq { - raw_text: String, - comparators: Vec<semver::VersionReq>, -} - -impl NpmVersionReq { - pub fn parse(text: &str) -> Result<NpmVersionReq, AnyError> { - // semver::VersionReq doesn't support spaces between comparators - // and it doesn't support using || for "OR", so we pre-process - // the version requirement in order to make this work. - let raw_text = text.to_string(); - let part_texts = text.split("||").collect::<Vec<_>>(); - let mut comparators = Vec::with_capacity(part_texts.len()); - for part in part_texts { - comparators.push(npm_version_req_parse_part(part)?); - } - Ok(NpmVersionReq { - raw_text, - comparators, - }) - } -} - -impl NpmVersionMatcher for NpmVersionReq { - fn matches(&self, version: &semver::Version) -> bool { - self.comparators.iter().any(|c| c.matches(version)) - } - - fn version_text(&self) -> String { - self.raw_text.to_string() - } -} - -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() { - let (i, c) = chars.next().unwrap(); - let is_greater_or_less_than = c == '<' || c == '>'; - if is_greater_or_less_than || c == '=' { - if i > 0 { - final_text = final_text.trim().to_string(); - // add a comma to make semver::VersionReq parse this - final_text.push(','); - } - final_text.push(c); - let next_char = chars.peek().map(|(_, c)| c); - if is_greater_or_less_than && matches!(next_char, Some('=')) { - let c = chars.next().unwrap().1; // skip - final_text.push(c); - } - } else { - final_text.push(c); - } - } - Ok(semver::VersionReq::parse(&final_text)?) -} - -#[cfg(test)] -mod test { - use super::*; - - struct NpmVersionReqTester(NpmVersionReq); - - impl NpmVersionReqTester { - fn matches(&self, version: &str) -> bool { - self.0.matches(&semver::Version::parse(version).unwrap()) - } - } - - #[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(), - ); - assert!(!tester.matches("2.1.1")); - assert!(tester.matches("2.1.2")); - assert!(tester.matches("2.9.9")); - assert!(!tester.matches("3.0.0")); - assert!(tester.matches("5.0.0")); - assert!(tester.matches("5.1.0")); - assert!(!tester.matches("6.1.0")); - } -} diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs index d92004db0..b945a1e0b 100644 --- a/cli/npm/resolution.rs +++ b/cli/npm/resolution.rs @@ -8,12 +8,14 @@ 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::parking_lot::RwLock; use super::registry::NpmPackageInfo; use super::registry::NpmPackageVersionDistInfo; use super::registry::NpmPackageVersionInfo; use super::registry::NpmRegistryApi; +use super::version_req::SpecifierVersionReq; /// The version matcher used for npm schemed urls is more strict than /// the one used by npm packages. @@ -28,38 +30,6 @@ pub struct NpmPackageReference { pub sub_path: Option<String>, } -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] -pub struct NpmPackageReq { - pub name: String, - pub version_req: Option<semver::VersionReq>, -} - -impl std::fmt::Display for NpmPackageReq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.version_req { - Some(req) => write!(f, "{}@{}", self.name, req), - None => write!(f, "{}", self.name), - } - } -} - -impl NpmVersionMatcher for NpmPackageReq { - fn matches(&self, version: &semver::Version) -> bool { - match &self.version_req { - Some(req) => req.matches(version), - None => version.pre.is_empty(), - } - } - - fn version_text(&self) -> String { - self - .version_req - .as_ref() - .map(|v| format!("{}", v)) - .unwrap_or_else(|| "non-prerelease".to_string()) - } -} - impl NpmPackageReference { pub fn from_specifier( specifier: &ModuleSpecifier, @@ -77,7 +47,7 @@ impl NpmPackageReference { let (name, version_req) = match specifier.rsplit_once('@') { Some((name, version_req)) => ( name, - match semver::VersionReq::parse(version_req) { + match SpecifierVersionReq::parse(version_req) { Ok(v) => Some(v), Err(_) => None, // not a version requirement }, @@ -105,6 +75,38 @@ impl std::fmt::Display for NpmPackageReference { } } +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct NpmPackageReq { + pub name: String, + pub version_req: Option<SpecifierVersionReq>, +} + +impl std::fmt::Display for NpmPackageReq { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.version_req { + Some(req) => write!(f, "{}@{}", self.name, req), + None => write!(f, "{}", self.name), + } + } +} + +impl NpmVersionMatcher for NpmPackageReq { + fn matches(&self, version: &semver::Version) -> bool { + match &self.version_req { + Some(req) => req.matches(version), + None => version.pre.is_empty(), + } + } + + fn version_text(&self) -> String { + self + .version_req + .as_ref() + .map(|v| format!("{}", v)) + .unwrap_or_else(|| "non-prerelease".to_string()) + } +} + #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct NpmPackageId { pub name: String, @@ -314,6 +316,27 @@ impl NpmResolution { ordering => ordering, }); + // cache all the dependencies' registry infos in parallel when this env var isn't set + if std::env::var("DENO_UNSTABLE_NPM_SYNC_DOWNLOAD") != Ok("1".to_string()) + { + let handles = deps + .iter() + .map(|dep| { + let name = dep.name.clone(); + let api = self.api.clone(); + tokio::task::spawn(async move { + // it's ok to call this without storing the result, because + // NpmRegistryApi will cache the package info in memory + api.package_info(&name).await + }) + }) + .collect::<Vec<_>>(); + let results = futures::future::join_all(handles).await; + for result in results { + result??; // surface the first error + } + } + // now resolve them for dep in deps { // check if an existing dependency matches this diff --git a/cli/npm/version_req.rs b/cli/npm/version_req.rs new file mode 100644 index 000000000..24f3788ad --- /dev/null +++ b/cli/npm/version_req.rs @@ -0,0 +1,219 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; + +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use once_cell::sync::Lazy; +use regex::Regex; + +use super::resolution::NpmVersionMatcher; + +static MINOR_SPECIFIER_RE: Lazy<Regex> = + Lazy::new(|| Regex::new(r#"^[0-9]+\.[0-9]+$"#).unwrap()); + +/// Version requirement found in npm specifiers. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct SpecifierVersionReq(semver::VersionReq); + +impl std::fmt::Display for SpecifierVersionReq { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl SpecifierVersionReq { + // in order to keep using semver, we do some pre-processing to change the behavior + pub fn parse(text: &str) -> Result<Self, AnyError> { + // for now, we don't support these scenarios + if text.contains("||") { + bail!("not supported '||'"); + } + if text.contains(',') { + bail!("not supported ','"); + } + // force exact versions to be matched exactly + let text = if semver::Version::parse(text).is_ok() { + Cow::Owned(format!("={}", text)) + } else { + Cow::Borrowed(text) + }; + // force requirements like 1.2 to be ~1.2 instead of ^1.2 + let text = if MINOR_SPECIFIER_RE.is_match(&text) { + Cow::Owned(format!("~{}", text)) + } else { + text + }; + Ok(Self(semver::VersionReq::parse(&text)?)) + } + + pub fn matches(&self, version: &semver::Version) -> bool { + self.0.matches(version) + } +} + +/// A version requirement found in an npm package's dependencies. +pub struct NpmVersionReq { + raw_text: String, + comparators: Vec<semver::VersionReq>, +} + +impl NpmVersionReq { + pub fn parse(text: &str) -> Result<NpmVersionReq, AnyError> { + // semver::VersionReq doesn't support spaces between comparators + // and it doesn't support using || for "OR", so we pre-process + // the version requirement in order to make this work. + let raw_text = text.to_string(); + let part_texts = text.split("||").collect::<Vec<_>>(); + let mut comparators = Vec::with_capacity(part_texts.len()); + for part in part_texts { + comparators.push(npm_version_req_parse_part(part)?); + } + Ok(NpmVersionReq { + raw_text, + comparators, + }) + } +} + +impl NpmVersionMatcher for NpmVersionReq { + fn matches(&self, version: &semver::Version) -> bool { + self.comparators.iter().any(|c| c.matches(version)) + } + + fn version_text(&self) -> String { + self.raw_text.to_string() + } +} + +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); + // force exact versions to be matched exactly + let text = if semver::Version::parse(text).is_ok() { + Cow::Owned(format!("={}", text)) + } else { + Cow::Borrowed(text) + }; + // force requirements like 1.2 to be ~1.2 instead of ^1.2 + let text = if MINOR_SPECIFIER_RE.is_match(&text) { + Cow::Owned(format!("~{}", text)) + } else { + text + }; + let mut chars = text.chars().enumerate().peekable(); + let mut final_text = String::new(); + while chars.peek().is_some() { + let (i, c) = chars.next().unwrap(); + let is_greater_or_less_than = c == '<' || c == '>'; + if is_greater_or_less_than || c == '=' { + if i > 0 { + final_text = final_text.trim().to_string(); + // add a comma to make semver::VersionReq parse this + final_text.push(','); + } + final_text.push(c); + let next_char = chars.peek().map(|(_, c)| c); + if is_greater_or_less_than && matches!(next_char, Some('=')) { + let c = chars.next().unwrap().1; // skip + final_text.push(c); + } + } else { + final_text.push(c); + } + } + Ok(semver::VersionReq::parse(&final_text)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + struct VersionReqTester(SpecifierVersionReq); + + impl VersionReqTester { + fn new(text: &str) -> Self { + Self(SpecifierVersionReq::parse(text).unwrap()) + } + + fn matches(&self, version: &str) -> bool { + self.0.matches(&semver::Version::parse(version).unwrap()) + } + } + + #[test] + fn version_req_exact() { + let tester = VersionReqTester::new("1.0.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.0.1")); + assert!(!tester.matches("1.0.2")); + assert!(!tester.matches("1.1.1")); + } + + #[test] + fn version_req_minor() { + let tester = VersionReqTester::new("1.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(!tester.matches("1.2.0")); + assert!(!tester.matches("1.2.1")); + } + + struct NpmVersionReqTester(NpmVersionReq); + + impl NpmVersionReqTester { + fn new(text: &str) -> Self { + Self(NpmVersionReq::parse(text).unwrap()) + } + + fn matches(&self, version: &str) -> bool { + self.0.matches(&semver::Version::parse(version).unwrap()) + } + } + + #[test] + pub fn npm_version_req_with_v() { + assert!(NpmVersionReq::parse("v1.0.0").is_ok()); + } + + #[test] + pub fn npm_version_req_exact() { + let tester = NpmVersionReqTester::new("2.1.2"); + assert!(!tester.matches("2.1.1")); + assert!(tester.matches("2.1.2")); + assert!(!tester.matches("2.1.3")); + + let tester = NpmVersionReqTester::new("2.1.2 || 2.1.5"); + assert!(!tester.matches("2.1.1")); + assert!(tester.matches("2.1.2")); + assert!(!tester.matches("2.1.3")); + assert!(!tester.matches("2.1.4")); + assert!(tester.matches("2.1.5")); + assert!(!tester.matches("2.1.6")); + } + + #[test] + pub fn npm_version_req_minor() { + let tester = NpmVersionReqTester::new("1.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(!tester.matches("1.2.0")); + assert!(!tester.matches("1.2.1")); + } + + #[test] + pub fn npm_version_req_ranges() { + let tester = NpmVersionReqTester::new(">= 2.1.2 < 3.0.0 || 5.x"); + assert!(!tester.matches("2.1.1")); + assert!(tester.matches("2.1.2")); + assert!(tester.matches("2.9.9")); + assert!(!tester.matches("3.0.0")); + assert!(tester.matches("5.0.0")); + assert!(tester.matches("5.1.0")); + assert!(!tester.matches("6.1.0")); + } +} |