feat: binary npm commands (#15542)

This commit is contained in:
David Sherret 2022-08-23 10:39:19 -04:00 committed by GitHub
parent 362af63c6f
commit e7367044d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2389 additions and 196 deletions

View file

@ -4,6 +4,7 @@ mod cache;
mod registry;
mod resolution;
mod tarball;
mod version_req;
use std::io::ErrorKind;
use std::path::Path;

View file

@ -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"));
}
}

View file

@ -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,10 +30,55 @@ pub struct NpmPackageReference {
pub sub_path: Option<String>,
}
impl NpmPackageReference {
pub fn from_specifier(
specifier: &ModuleSpecifier,
) -> Result<NpmPackageReference, AnyError> {
Self::from_str(specifier.as_str())
}
pub fn from_str(specifier: &str) -> Result<NpmPackageReference, AnyError> {
let specifier = match specifier.strip_prefix("npm:") {
Some(s) => s,
None => {
bail!("Not an npm specifier: '{}'", specifier);
}
};
let (name, version_req) = match specifier.rsplit_once('@') {
Some((name, version_req)) => (
name,
match SpecifierVersionReq::parse(version_req) {
Ok(v) => Some(v),
Err(_) => None, // not a version requirement
},
),
None => (specifier, None),
};
Ok(NpmPackageReference {
req: NpmPackageReq {
name: name.to_string(),
version_req,
},
// todo: implement and support this
sub_path: None,
})
}
}
impl std::fmt::Display for NpmPackageReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(sub_path) = &self.sub_path {
write!(f, "{}/{}", self.req, sub_path)
} else {
write!(f, "{}", self.req)
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct NpmPackageReq {
pub name: String,
pub version_req: Option<semver::VersionReq>,
pub version_req: Option<SpecifierVersionReq>,
}
impl std::fmt::Display for NpmPackageReq {
@ -60,51 +107,6 @@ impl NpmVersionMatcher for NpmPackageReq {
}
}
impl NpmPackageReference {
pub fn from_specifier(
specifier: &ModuleSpecifier,
) -> Result<NpmPackageReference, AnyError> {
Self::from_str(specifier.as_str())
}
pub fn from_str(specifier: &str) -> Result<NpmPackageReference, AnyError> {
let specifier = match specifier.strip_prefix("npm:") {
Some(s) => s,
None => {
bail!("Not an npm specifier: '{}'", specifier);
}
};
let (name, version_req) = match specifier.rsplit_once('@') {
Some((name, version_req)) => (
name,
match semver::VersionReq::parse(version_req) {
Ok(v) => Some(v),
Err(_) => None, // not a version requirement
},
),
None => (specifier, None),
};
Ok(NpmPackageReference {
req: NpmPackageReq {
name: name.to_string(),
version_req,
},
// todo: implement and support this
sub_path: None,
})
}
}
impl std::fmt::Display for NpmPackageReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(sub_path) = &self.sub_path {
write!(f, "{}/{}", self.req, sub_path)
} else {
write!(f, "{}", self.req)
}
}
}
#[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

219
cli/npm/version_req.rs Normal file
View file

@ -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"));
}
}