mirror of
https://github.com/denoland/deno.git
synced 2025-08-03 18:38:33 +00:00
feat: binary npm commands (#15542)
This commit is contained in:
parent
362af63c6f
commit
e7367044d9
21 changed files with 2389 additions and 196 deletions
|
@ -4,6 +4,7 @@ mod cache;
|
|||
mod registry;
|
||||
mod resolution;
|
||||
mod tarball;
|
||||
mod version_req;
|
||||
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
219
cli/npm/version_req.rs
Normal 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"));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue