summaryrefslogtreecommitdiff
path: root/cli/npm
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm')
-rw-r--r--cli/npm/mod.rs1
-rw-r--r--cli/npm/registry.rs97
-rw-r--r--cli/npm/resolution.rs89
-rw-r--r--cli/npm/version_req.rs219
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"));
+ }
+}