mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 16:19:12 -05:00
feat: binary npm commands (#15542)
This commit is contained in:
parent
362af63c6f
commit
e7367044d9
21 changed files with 2389 additions and 196 deletions
|
@ -112,24 +112,6 @@ pub fn try_resolve_builtin_module(specifier: &str) -> Option<Url> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn load_cjs_module_from_ext_node(
|
||||
js_runtime: &mut JsRuntime,
|
||||
module: &str,
|
||||
main: bool,
|
||||
) -> Result<(), AnyError> {
|
||||
let source_code = &format!(
|
||||
r#"(function loadCjsModule(module) {{
|
||||
Deno[Deno.internal].require.Module._load(module, null, {main});
|
||||
}})('{module}');"#,
|
||||
main = main,
|
||||
module = escape_for_single_quote_string(module),
|
||||
);
|
||||
|
||||
js_runtime.execute_script(&located_script_name!(), source_code)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_cjs_module(
|
||||
js_runtime: &mut JsRuntime,
|
||||
module: &str,
|
||||
|
|
|
@ -164,7 +164,8 @@ async fn test_specifier(
|
|||
stdout: StdioPipe::File(sender.stdout()),
|
||||
stderr: StdioPipe::File(sender.stderr()),
|
||||
},
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
worker.run_lsp_test_specifier(mode).await?;
|
||||
}
|
||||
|
||||
|
|
26
cli/main.rs
26
cli/main.rs
|
@ -91,6 +91,7 @@ use deno_runtime::permissions::Permissions;
|
|||
use deno_runtime::tokio_util::run_local;
|
||||
use log::debug;
|
||||
use log::info;
|
||||
use npm::NpmPackageReference;
|
||||
use std::env;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
@ -323,7 +324,8 @@ async fn install_command(
|
|||
permissions,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
// First, fetch and compile the module; this step ensures that the module exists.
|
||||
worker.preload_main_module().await?;
|
||||
tools::installer::install(flags, install_flags)?;
|
||||
|
@ -412,7 +414,8 @@ async fn eval_command(
|
|||
permissions,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
// Create a dummy source file.
|
||||
let source_code = if eval_flags.print {
|
||||
format!("console.log({})", eval_flags.code)
|
||||
|
@ -694,7 +697,8 @@ async fn repl_command(
|
|||
Permissions::from_options(&ps.options.permissions_options())?,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
worker.setup_repl().await?;
|
||||
tools::repl::run(
|
||||
&ps,
|
||||
|
@ -714,7 +718,8 @@ async fn run_from_stdin(flags: Flags) -> Result<i32, AnyError> {
|
|||
Permissions::from_options(&ps.options.permissions_options())?,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut source = Vec::new();
|
||||
std::io::stdin().read_to_end(&mut source)?;
|
||||
|
@ -758,7 +763,8 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<i32, AnyError> {
|
|||
permissions,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
worker.run_for_watcher().await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -797,8 +803,13 @@ async fn run_command(
|
|||
// TODO(bartlomieju): actually I think it will also fail if there's an import
|
||||
// map specified and bare specifier is used on the command line - this should
|
||||
// probably call `ProcState::resolve` instead
|
||||
let main_module = resolve_url_or_path(&run_flags.script)?;
|
||||
let ps = ProcState::build(flags).await?;
|
||||
let main_module = if NpmPackageReference::from_str(&run_flags.script).is_ok()
|
||||
{
|
||||
ModuleSpecifier::parse(&run_flags.script)?
|
||||
} else {
|
||||
resolve_url_or_path(&run_flags.script)?
|
||||
};
|
||||
let permissions =
|
||||
Permissions::from_options(&ps.options.permissions_options())?;
|
||||
let mut worker = create_main_worker(
|
||||
|
@ -807,7 +818,8 @@ async fn run_command(
|
|||
permissions,
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
let exit_code = worker.run().await?;
|
||||
Ok(exit_code)
|
||||
|
|
|
@ -74,7 +74,15 @@ pub fn esm_code_with_node_globals(
|
|||
write!(result, "var {0} = {1}.{0};", global, global_this_expr).unwrap();
|
||||
}
|
||||
|
||||
result.push_str(parsed_source.text_info().text_str());
|
||||
let file_text = parsed_source.text_info().text_str();
|
||||
// strip the shebang
|
||||
let file_text = if file_text.starts_with("#!/") {
|
||||
let start_index = file_text.find('\n').unwrap_or(file_text.len());
|
||||
&file_text[start_index..]
|
||||
} else {
|
||||
file_text
|
||||
};
|
||||
result.push_str(file_text);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -158,4 +166,24 @@ mod tests {
|
|||
assert!(r.contains("var process = globalThis.process;"));
|
||||
assert!(r.contains("export const x = 1;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_esm_code_with_node_globals_with_shebang() {
|
||||
let r = esm_code_with_node_globals(
|
||||
&ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(),
|
||||
"#!/usr/bin/env node\nexport const x = 1;".to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r,
|
||||
concat!(
|
||||
"var globalThis = Deno[Deno.internal].node.globalThis;var Buffer = globalThis.Buffer;",
|
||||
"var clearImmediate = globalThis.clearImmediate;var clearInterval = globalThis.clearInterval;",
|
||||
"var clearTimeout = globalThis.clearTimeout;var global = globalThis.global;",
|
||||
"var process = globalThis.process;var setImmediate = globalThis.setImmediate;",
|
||||
"var setInterval = globalThis.setInterval;var setTimeout = globalThis.setTimeout;\n",
|
||||
"export const x = 1;"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::path::PathBuf;
|
|||
|
||||
use deno_ast::MediaType;
|
||||
use deno_ast::ModuleSpecifier;
|
||||
use deno_core::anyhow::bail;
|
||||
use deno_core::anyhow::Context;
|
||||
use deno_core::error::generic_error;
|
||||
use deno_core::error::AnyError;
|
||||
|
@ -31,6 +32,7 @@ use crate::compat;
|
|||
use crate::file_fetcher::FileFetcher;
|
||||
use crate::npm::GlobalNpmPackageResolver;
|
||||
use crate::npm::NpmPackageReference;
|
||||
use crate::npm::NpmPackageReq;
|
||||
use crate::npm::NpmPackageResolver;
|
||||
|
||||
mod analyze;
|
||||
|
@ -185,6 +187,87 @@ pub fn node_resolve_npm_reference(
|
|||
Ok(Some(resolve_response))
|
||||
}
|
||||
|
||||
pub fn node_resolve_binary_export(
|
||||
pkg_req: &NpmPackageReq,
|
||||
bin_name: Option<&str>,
|
||||
npm_resolver: &GlobalNpmPackageResolver,
|
||||
) -> Result<ResolveResponse, AnyError> {
|
||||
let pkg = npm_resolver.resolve_package_from_deno_module(pkg_req)?;
|
||||
let package_folder = pkg.folder_path;
|
||||
let package_json_path = package_folder.join("package.json");
|
||||
let package_json = PackageJson::load(npm_resolver, package_json_path)?;
|
||||
let bin = match &package_json.bin {
|
||||
Some(bin) => bin,
|
||||
None => bail!(
|
||||
"package {} did not have a 'bin' property in its package.json",
|
||||
pkg.id
|
||||
),
|
||||
};
|
||||
let bin_entry = match bin {
|
||||
Value::String(_) => {
|
||||
if bin_name.is_some() && bin_name.unwrap() != pkg_req.name {
|
||||
None
|
||||
} else {
|
||||
Some(bin)
|
||||
}
|
||||
}
|
||||
Value::Object(o) => {
|
||||
if let Some(bin_name) = bin_name {
|
||||
o.get(bin_name)
|
||||
} else if o.len() == 1 {
|
||||
o.values().next()
|
||||
} else {
|
||||
o.get(&pkg_req.name)
|
||||
}
|
||||
},
|
||||
_ => bail!("package {} did not have a 'bin' property with a string or object value in its package.json", pkg.id),
|
||||
};
|
||||
let bin_entry = match bin_entry {
|
||||
Some(e) => e,
|
||||
None => bail!(
|
||||
"package {} did not have a 'bin' entry for {} in its package.json",
|
||||
pkg.id,
|
||||
bin_name.unwrap_or(&pkg_req.name),
|
||||
),
|
||||
};
|
||||
let bin_entry = match bin_entry {
|
||||
Value::String(s) => s,
|
||||
_ => bail!(
|
||||
"package {} had a non-string sub property of 'bin' in its package.json",
|
||||
pkg.id
|
||||
),
|
||||
};
|
||||
|
||||
let url =
|
||||
ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap();
|
||||
|
||||
let resolve_response = url_to_resolve_response(url, npm_resolver)?;
|
||||
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
||||
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
||||
Ok(resolve_response)
|
||||
}
|
||||
|
||||
pub fn load_cjs_module_from_ext_node(
|
||||
js_runtime: &mut JsRuntime,
|
||||
module: &str,
|
||||
main: bool,
|
||||
) -> Result<(), AnyError> {
|
||||
fn escape_for_single_quote_string(text: &str) -> String {
|
||||
text.replace('\\', r"\\").replace('\'', r"\'")
|
||||
}
|
||||
|
||||
let source_code = &format!(
|
||||
r#"(function loadCjsModule(module) {{
|
||||
Deno[Deno.internal].require.Module._load(module, null, {main});
|
||||
}})('{module}');"#,
|
||||
main = main,
|
||||
module = escape_for_single_quote_string(module),
|
||||
);
|
||||
|
||||
js_runtime.execute_script(&located_script_name!(), source_code)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_config_resolve(
|
||||
package_subpath: &str,
|
||||
package_dir: &Path,
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -443,12 +443,7 @@ impl ProcState {
|
|||
.add_package_reqs(npm_package_references)
|
||||
.await?;
|
||||
self.npm_resolver.cache_packages().await?;
|
||||
|
||||
// add the builtin node modules to the graph data
|
||||
let node_std_graph = self
|
||||
.create_graph(vec![(compat::MODULE_ALL_URL.clone(), ModuleKind::Esm)])
|
||||
.await?;
|
||||
self.graph_data.write().add_graph(&node_std_graph, false);
|
||||
self.prepare_node_std_graph().await?;
|
||||
}
|
||||
|
||||
// type check if necessary
|
||||
|
@ -492,6 +487,15 @@ impl ProcState {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Add the builtin node modules to the graph data.
|
||||
pub async fn prepare_node_std_graph(&self) -> Result<(), AnyError> {
|
||||
let node_std_graph = self
|
||||
.create_graph(vec![(compat::MODULE_ALL_URL.clone(), ModuleKind::Esm)])
|
||||
.await?;
|
||||
self.graph_data.write().add_graph(&node_std_graph, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_node_resolve_result(
|
||||
&self,
|
||||
result: Result<Option<ResolveResponse>, AnyError>,
|
||||
|
|
|
@ -151,18 +151,47 @@ fn cached_only_after_first_run() {
|
|||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
eprintln!("DENO DIR: {}", deno_dir.path().display());
|
||||
std::mem::forget(deno_dir);
|
||||
let output = deno.wait_with_output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
eprintln!("stderr {}", stderr);
|
||||
eprintln!("stdout {}", stdout);
|
||||
assert!(output.status.success());
|
||||
assert!(stderr.is_empty());
|
||||
assert_contains!(stdout, "createChalk: chalk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deno_run_cjs_module() {
|
||||
let _server = http_server();
|
||||
|
||||
let deno_dir = util::new_deno_dir();
|
||||
|
||||
let deno = util::deno_cmd_with_deno_dir(&deno_dir)
|
||||
.current_dir(deno_dir.path())
|
||||
.arg("run")
|
||||
.arg("--unstable")
|
||||
.arg("--allow-read")
|
||||
.arg("--allow-env")
|
||||
.arg("--allow-write")
|
||||
.arg("npm:mkdirp@1.0.4")
|
||||
.arg("test_dir")
|
||||
.env("NO_COLOR", "1")
|
||||
.envs(env_vars())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let output = deno.wait_with_output().unwrap();
|
||||
assert!(output.status.success());
|
||||
|
||||
assert!(deno_dir.path().join("test_dir").exists());
|
||||
}
|
||||
|
||||
itest!(deno_run_non_existent {
|
||||
args: "run --unstable npm:mkdirp@0.5.125",
|
||||
output: "npm/deno_run_non_existent.out",
|
||||
envs: env_vars(),
|
||||
http_server: true,
|
||||
exit_code: 1,
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn ensure_registry_files_local() {
|
||||
// ensures the registry files all point at local tarballs
|
||||
|
|
2
cli/tests/testdata/npm/cjs_sub_path/main.js
vendored
2
cli/tests/testdata/npm/cjs_sub_path/main.js
vendored
|
@ -2,7 +2,7 @@
|
|||
// and also get the parent directory index.js file using require("..")
|
||||
import Ajv from "npm:ajv@~8.11";
|
||||
import addFormats from "npm:ajv-formats@2.1.1";
|
||||
import { expect } from "npm:chai@4.2";
|
||||
import { expect } from "npm:chai@4.3";
|
||||
|
||||
const ajv = new Ajv();
|
||||
addFormats(ajv);
|
||||
|
|
2
cli/tests/testdata/npm/cjs_with_deps/main.js
vendored
2
cli/tests/testdata/npm/cjs_with_deps/main.js
vendored
|
@ -1,5 +1,5 @@
|
|||
import chalk from "npm:chalk@4";
|
||||
import { expect } from "npm:chai@4.2";
|
||||
import { expect } from "npm:chai@4.3";
|
||||
|
||||
console.log(chalk.green("chalk cjs loads"));
|
||||
|
||||
|
|
2
cli/tests/testdata/npm/deno_run_non_existent.out
vendored
Normal file
2
cli/tests/testdata/npm/deno_run_non_existent.out
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
Download http://localhost:4545/npm/registry/mkdirp
|
||||
error: Could not find npm package 'mkdirp' matching =0.5.125. Try retreiving the latest npm package information by running with --reload
|
BIN
cli/tests/testdata/npm/registry/mkdirp/mkdirp-1.0.4.tgz
vendored
Normal file
BIN
cli/tests/testdata/npm/registry/mkdirp/mkdirp-1.0.4.tgz
vendored
Normal file
Binary file not shown.
1865
cli/tests/testdata/npm/registry/mkdirp/registry.json
vendored
Normal file
1865
cli/tests/testdata/npm/registry/mkdirp/registry.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -362,7 +362,8 @@ async fn bench_specifier(
|
|||
ps.options.unstable(),
|
||||
)],
|
||||
Default::default(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
worker.run_bench_specifier().await
|
||||
}
|
||||
|
|
|
@ -723,7 +723,8 @@ async fn test_specifier(
|
|||
stdout: StdioPipe::File(sender.stdout()),
|
||||
stderr: StdioPipe::File(sender.stderr()),
|
||||
},
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
worker.run_test_specifier(mode).await
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use deno_core::located_script_name;
|
|||
use deno_core::serde_json::json;
|
||||
use deno_core::Extension;
|
||||
use deno_core::ModuleId;
|
||||
use deno_graph::source::ResolveResponse;
|
||||
use deno_runtime::colors;
|
||||
use deno_runtime::ops::worker_host::CreateWebWorkerCb;
|
||||
use deno_runtime::ops::worker_host::WorkerEventCb;
|
||||
|
@ -26,6 +27,7 @@ use crate::errors;
|
|||
use crate::fmt_errors::format_js_error;
|
||||
use crate::module_loader::CliModuleLoader;
|
||||
use crate::node;
|
||||
use crate::npm::NpmPackageReference;
|
||||
use crate::ops;
|
||||
use crate::proc_state::ProcState;
|
||||
use crate::tools;
|
||||
|
@ -35,6 +37,7 @@ use crate::version;
|
|||
|
||||
pub struct CliMainWorker {
|
||||
main_module: ModuleSpecifier,
|
||||
is_main_cjs: bool,
|
||||
worker: MainWorker,
|
||||
ps: ProcState,
|
||||
}
|
||||
|
@ -99,8 +102,14 @@ impl CliMainWorker {
|
|||
true,
|
||||
)?;
|
||||
}
|
||||
} else if self.is_main_cjs {
|
||||
node::initialize_runtime(&mut self.worker.js_runtime).await?;
|
||||
node::load_cjs_module_from_ext_node(
|
||||
&mut self.worker.js_runtime,
|
||||
&self.main_module.to_file_path().unwrap().to_string_lossy(),
|
||||
true,
|
||||
)?;
|
||||
} else {
|
||||
// Regular ES module execution
|
||||
self.execute_main_module_possibly_with_npm().await?;
|
||||
}
|
||||
|
||||
|
@ -438,13 +447,31 @@ impl CliMainWorker {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_main_worker(
|
||||
pub async fn create_main_worker(
|
||||
ps: &ProcState,
|
||||
main_module: ModuleSpecifier,
|
||||
permissions: Permissions,
|
||||
mut custom_extensions: Vec<Extension>,
|
||||
stdio: deno_runtime::ops::io::Stdio,
|
||||
) -> CliMainWorker {
|
||||
) -> Result<CliMainWorker, AnyError> {
|
||||
let (main_module, is_main_cjs) = if let Ok(package_ref) =
|
||||
NpmPackageReference::from_specifier(&main_module)
|
||||
{
|
||||
ps.npm_resolver
|
||||
.add_package_reqs(vec![package_ref.req.clone()])
|
||||
.await?;
|
||||
ps.npm_resolver.cache_packages().await?;
|
||||
ps.prepare_node_std_graph().await?;
|
||||
let resolve_response = node::node_resolve_binary_export(
|
||||
&package_ref.req,
|
||||
package_ref.sub_path.as_deref(),
|
||||
&ps.npm_resolver,
|
||||
)?;
|
||||
let is_main_cjs = matches!(resolve_response, ResolveResponse::CommonJs(_));
|
||||
(resolve_response.to_result()?, is_main_cjs)
|
||||
} else {
|
||||
(main_module, false)
|
||||
};
|
||||
let module_loader = CliModuleLoader::new(ps.clone());
|
||||
|
||||
let maybe_inspector_server = ps.maybe_inspector_server.clone();
|
||||
|
@ -518,11 +545,12 @@ pub fn create_main_worker(
|
|||
permissions,
|
||||
options,
|
||||
);
|
||||
CliMainWorker {
|
||||
Ok(CliMainWorker {
|
||||
main_module,
|
||||
is_main_cjs,
|
||||
worker,
|
||||
ps: ps.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_web_worker_preload_module_callback(
|
||||
|
|
|
@ -403,11 +403,14 @@
|
|||
paths.push(denoDirPath);
|
||||
}
|
||||
}
|
||||
paths.push(...ops.op_require_resolve_lookup_paths(
|
||||
const lookupPathsResult = ops.op_require_resolve_lookup_paths(
|
||||
request,
|
||||
parent?.paths,
|
||||
parent?.filename ?? "",
|
||||
));
|
||||
);
|
||||
if (lookupPathsResult) {
|
||||
paths.push(...lookupPathsResult);
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
|
|
|
@ -963,7 +963,7 @@ async fn main_server(
|
|||
return Ok(file_resp);
|
||||
} else if should_download_npm_packages() {
|
||||
if let Err(err) =
|
||||
download_npm_registry_file(&file_path, is_tarball).await
|
||||
download_npm_registry_file(req.uri(), &file_path, is_tarball).await
|
||||
{
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
|
@ -992,15 +992,21 @@ fn should_download_npm_packages() -> bool {
|
|||
}
|
||||
|
||||
async fn download_npm_registry_file(
|
||||
uri: &hyper::Uri,
|
||||
file_path: &PathBuf,
|
||||
is_tarball: bool,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let package_name = file_path
|
||||
.parent()
|
||||
let url_parts = uri
|
||||
.path()
|
||||
.strip_prefix("/npm/registry/")
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy();
|
||||
.split('/')
|
||||
.collect::<Vec<_>>();
|
||||
let package_name = if url_parts[0].starts_with('@') {
|
||||
url_parts.into_iter().take(2).collect::<Vec<_>>().join("/")
|
||||
} else {
|
||||
url_parts.into_iter().take(1).collect::<Vec<_>>().join("/")
|
||||
};
|
||||
let url = if is_tarball {
|
||||
let file_name = file_path.file_name().unwrap().to_string_lossy();
|
||||
format!(
|
||||
|
|
Loading…
Reference in a new issue