summaryrefslogtreecommitdiff
path: root/ext/node
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node')
-rw-r--r--ext/node/Cargo.toml3
-rw-r--r--ext/node/analyze.rs16
-rw-r--r--ext/node/lib.rs107
-rw-r--r--ext/node/ops.rs26
-rw-r--r--ext/node/package_json.rs4
-rw-r--r--ext/node/polyfill.rs20
-rw-r--r--ext/node/resolution.rs20
-rw-r--r--ext/node/resolver.rs686
8 files changed, 841 insertions, 41 deletions
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml
index 0d647e4f0..576e62d55 100644
--- a/ext/node/Cargo.toml
+++ b/ext/node/Cargo.toml
@@ -18,6 +18,9 @@ aes.workspace = true
cbc.workspace = true
data-encoding = "2.3.3"
deno_core.workspace = true
+deno_media_type.workspace = true
+deno_npm.workspace = true
+deno_semver.workspace = true
digest = { version = "0.10.5", features = ["core-api", "std"] }
dsa = "0.6.1"
ecb.workspace = true
diff --git a/ext/node/analyze.rs b/ext/node/analyze.rs
index 03bf41995..a206f4425 100644
--- a/ext/node/analyze.rs
+++ b/ext/node/analyze.rs
@@ -17,9 +17,9 @@ use crate::NodeFs;
use crate::NodeModuleKind;
use crate::NodePermissions;
use crate::NodeResolutionMode;
+use crate::NpmResolver;
use crate::PackageJson;
use crate::PathClean;
-use crate::RequireNpmResolver;
use crate::NODE_GLOBAL_THIS_NAME;
static NODE_GLOBALS: &[&str] = &[
@@ -66,20 +66,18 @@ pub trait CjsEsmCodeAnalyzer {
pub struct NodeCodeTranslator<
TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer,
- TRequireNpmResolver: RequireNpmResolver,
+ TNpmResolver: NpmResolver,
> {
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer,
- npm_resolver: TRequireNpmResolver,
+ npm_resolver: TNpmResolver,
}
-impl<
- TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer,
- TRequireNpmResolver: RequireNpmResolver,
- > NodeCodeTranslator<TCjsEsmCodeAnalyzer, TRequireNpmResolver>
+impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
+ NodeCodeTranslator<TCjsEsmCodeAnalyzer, TNpmResolver>
{
pub fn new(
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer,
- npm_resolver: TRequireNpmResolver,
+ npm_resolver: TNpmResolver,
) -> Self {
Self {
cjs_esm_code_analyzer,
@@ -242,7 +240,7 @@ impl<
// todo(dsherret): use not_found error on not found here
let module_dir = self.npm_resolver.resolve_package_folder_from_package(
package_specifier.as_str(),
- &referrer_path,
+ referrer,
mode,
)?;
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index a521e161c..38772d0fc 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -5,12 +5,20 @@ use deno_core::located_script_name;
use deno_core::op;
use deno_core::serde_json;
use deno_core::JsRuntime;
+use deno_core::ModuleSpecifier;
+use deno_npm::resolution::PackageReqNotFoundError;
+use deno_npm::NpmPackageId;
+use deno_semver::npm::NpmPackageNv;
+use deno_semver::npm::NpmPackageNvReference;
+use deno_semver::npm::NpmPackageReq;
+use deno_semver::npm::NpmPackageReqReference;
use once_cell::sync::Lazy;
use std::collections::HashSet;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
+use std::sync::Arc;
pub mod analyze;
mod crypto;
@@ -21,14 +29,15 @@ mod package_json;
mod path;
mod polyfill;
mod resolution;
+mod resolver;
mod v8;
mod winerror;
mod zlib;
pub use package_json::PackageJson;
pub use path::PathClean;
-pub use polyfill::find_builtin_node_module;
pub use polyfill::is_builtin_node_module;
+pub use polyfill::resolve_builtin_node_module;
pub use polyfill::NodeModulePolyfill;
pub use polyfill::SUPPORTED_BUILTIN_NODE_MODULES;
pub use resolution::get_closest_package_json;
@@ -41,6 +50,8 @@ pub use resolution::path_to_declaration_path;
pub use resolution::NodeModuleKind;
pub use resolution::NodeResolutionMode;
pub use resolution::DEFAULT_CONDITIONS;
+pub use resolver::NodeResolution;
+pub use resolver::NodeResolver;
pub trait NodeEnv {
type P: NodePermissions;
@@ -51,6 +62,14 @@ pub trait NodePermissions {
fn check_read(&mut self, path: &Path) -> Result<(), AnyError>;
}
+pub(crate) struct AllowAllNodePermissions;
+
+impl NodePermissions for AllowAllNodePermissions {
+ fn check_read(&mut self, _path: &Path) -> Result<(), AnyError> {
+ Ok(())
+ }
+}
+
#[derive(Default, Clone)]
pub struct NodeFsMetadata {
pub is_file: bool,
@@ -114,20 +133,47 @@ impl NodeFs for RealFs {
}
}
-pub trait RequireNpmResolver {
+pub trait NpmResolver {
+ /// Resolves an npm package folder path from an npm package referrer.
fn resolve_package_folder_from_package(
&self,
specifier: &str,
- referrer: &Path,
+ referrer: &ModuleSpecifier,
mode: NodeResolutionMode,
) -> Result<PathBuf, AnyError>;
+ /// Resolves the npm package folder path from the specified path.
fn resolve_package_folder_from_path(
&self,
path: &Path,
) -> Result<PathBuf, AnyError>;
- fn in_npm_package(&self, path: &Path) -> bool;
+ /// Resolves an npm package folder path from a Deno module.
+ fn resolve_package_folder_from_deno_module(
+ &self,
+ pkg_nv: &NpmPackageNv,
+ ) -> Result<PathBuf, AnyError>;
+
+ fn resolve_pkg_id_from_pkg_req(
+ &self,
+ req: &NpmPackageReq,
+ ) -> Result<NpmPackageId, PackageReqNotFoundError>;
+
+ fn resolve_nv_ref_from_pkg_req_ref(
+ &self,
+ req_ref: &NpmPackageReqReference,
+ ) -> Result<NpmPackageNvReference, PackageReqNotFoundError>;
+
+ fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool;
+
+ fn in_npm_package_at_path(&self, path: &Path) -> bool {
+ let specifier =
+ match ModuleSpecifier::from_file_path(path.to_path_buf().clean()) {
+ Ok(p) => p,
+ Err(_) => return false,
+ };
+ self.in_npm_package(&specifier)
+ }
fn ensure_read_permission(
&self,
@@ -136,6 +182,57 @@ pub trait RequireNpmResolver {
) -> Result<(), AnyError>;
}
+impl<T: NpmResolver + ?Sized> NpmResolver for Arc<T> {
+ fn resolve_package_folder_from_package(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ mode: NodeResolutionMode,
+ ) -> Result<PathBuf, AnyError> {
+ (**self).resolve_package_folder_from_package(specifier, referrer, mode)
+ }
+
+ fn resolve_package_folder_from_path(
+ &self,
+ path: &Path,
+ ) -> Result<PathBuf, AnyError> {
+ (**self).resolve_package_folder_from_path(path)
+ }
+
+ fn resolve_package_folder_from_deno_module(
+ &self,
+ pkg_nv: &NpmPackageNv,
+ ) -> Result<PathBuf, AnyError> {
+ (**self).resolve_package_folder_from_deno_module(pkg_nv)
+ }
+
+ fn resolve_pkg_id_from_pkg_req(
+ &self,
+ req: &NpmPackageReq,
+ ) -> Result<NpmPackageId, PackageReqNotFoundError> {
+ (**self).resolve_pkg_id_from_pkg_req(req)
+ }
+
+ fn resolve_nv_ref_from_pkg_req_ref(
+ &self,
+ req_ref: &NpmPackageReqReference,
+ ) -> Result<NpmPackageNvReference, PackageReqNotFoundError> {
+ (**self).resolve_nv_ref_from_pkg_req_ref(req_ref)
+ }
+
+ fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool {
+ (**self).in_npm_package(specifier)
+ }
+
+ fn ensure_read_permission(
+ &self,
+ permissions: &mut dyn NodePermissions,
+ path: &Path,
+ ) -> Result<(), AnyError> {
+ (**self).ensure_read_permission(permissions, path)
+ }
+}
+
pub static NODE_GLOBAL_THIS_NAME: Lazy<String> = Lazy::new(|| {
let now = std::time::SystemTime::now();
let seconds = now
@@ -490,7 +587,7 @@ deno_core::extension!(deno_node,
"zlib.ts",
],
options = {
- maybe_npm_resolver: Option<Rc<dyn RequireNpmResolver>>,
+ maybe_npm_resolver: Option<Rc<dyn NpmResolver>>,
},
state = |state, options| {
if let Some(npm_resolver) = options.maybe_npm_resolver {
diff --git a/ext/node/ops.rs b/ext/node/ops.rs
index 3db23b5ea..662168acc 100644
--- a/ext/node/ops.rs
+++ b/ext/node/ops.rs
@@ -7,6 +7,7 @@ use deno_core::normalize_path;
use deno_core::op;
use deno_core::url::Url;
use deno_core::JsRuntimeInspector;
+use deno_core::ModuleSpecifier;
use deno_core::OpState;
use std::cell::RefCell;
use std::path::Path;
@@ -20,8 +21,8 @@ use super::resolution;
use super::NodeModuleKind;
use super::NodePermissions;
use super::NodeResolutionMode;
+use super::NpmResolver;
use super::PackageJson;
-use super::RequireNpmResolver;
fn ensure_read_permission<P>(
state: &mut OpState,
@@ -31,7 +32,7 @@ where
P: NodePermissions + 'static,
{
let resolver = {
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>();
resolver.clone()
};
let permissions = state.borrow_mut::<P>();
@@ -191,11 +192,11 @@ fn op_require_resolve_deno_dir(
request: String,
parent_filename: String,
) -> Option<String> {
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>();
resolver
.resolve_package_folder_from_package(
&request,
- &PathBuf::from(parent_filename),
+ &ModuleSpecifier::from_file_path(parent_filename).unwrap(),
NodeResolutionMode::Execution,
)
.ok()
@@ -204,8 +205,8 @@ fn op_require_resolve_deno_dir(
#[op]
fn op_require_is_deno_dir_package(state: &mut OpState, path: String) -> bool {
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>();
- resolver.in_npm_package(&PathBuf::from(path))
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>();
+ resolver.in_npm_package_at_path(&PathBuf::from(path))
}
#[op]
@@ -375,7 +376,7 @@ where
return Ok(None);
}
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>().clone();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
let pkg = resolution::get_package_scope_config::<Env::Fs>(
&Url::from_file_path(parent_path.unwrap()).unwrap(),
@@ -462,10 +463,11 @@ fn op_require_resolve_exports<Env>(
where
Env: NodeEnv + 'static,
{
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>().clone();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
- let pkg_path = if resolver.in_npm_package(&PathBuf::from(&modules_path))
+ let pkg_path = if resolver
+ .in_npm_package_at_path(&PathBuf::from(&modules_path))
&& !uses_local_node_modules_dir
{
modules_path
@@ -515,7 +517,7 @@ where
state,
PathBuf::from(&filename).parent().unwrap(),
)?;
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>().clone();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
resolution::get_closest_package_json::<Env::Fs>(
&Url::from_file_path(filename).unwrap(),
@@ -532,7 +534,7 @@ fn op_require_read_package_scope<Env>(
where
Env: NodeEnv + 'static,
{
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>().clone();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
let package_json_path = PathBuf::from(package_json_path);
PackageJson::load::<Env::Fs>(&*resolver, permissions, package_json_path).ok()
@@ -549,7 +551,7 @@ where
{
let parent_path = PathBuf::from(&parent_filename);
ensure_read_permission::<Env::P>(state, &parent_path)?;
- let resolver = state.borrow::<Rc<dyn RequireNpmResolver>>().clone();
+ let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
let pkg = PackageJson::load::<Env::Fs>(
&*resolver,
diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs
index 60f50ad78..08f78681a 100644
--- a/ext/node/package_json.rs
+++ b/ext/node/package_json.rs
@@ -4,7 +4,7 @@ use crate::NodeFs;
use crate::NodeModuleKind;
use crate::NodePermissions;
-use super::RequireNpmResolver;
+use super::NpmResolver;
use deno_core::anyhow;
use deno_core::anyhow::bail;
@@ -63,7 +63,7 @@ impl PackageJson {
}
pub fn load<Fs: NodeFs>(
- resolver: &dyn RequireNpmResolver,
+ resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
path: PathBuf,
) -> Result<PackageJson, AnyError> {
diff --git a/ext/node/polyfill.rs b/ext/node/polyfill.rs
index 1fbb4afa3..b334d2d34 100644
--- a/ext/node/polyfill.rs
+++ b/ext/node/polyfill.rs
@@ -1,8 +1,22 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-pub fn find_builtin_node_module(
- module_name: &str,
-) -> Option<&NodeModulePolyfill> {
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::url::Url;
+use deno_core::ModuleSpecifier;
+
+// TODO(bartlomieju): seems super wasteful to parse the specifier each time
+pub fn resolve_builtin_node_module(module_name: &str) -> Result<Url, AnyError> {
+ if let Some(module) = find_builtin_node_module(module_name) {
+ return Ok(ModuleSpecifier::parse(module.specifier).unwrap());
+ }
+
+ Err(generic_error(format!(
+ "Unknown built-in \"node:\" module: {module_name}"
+ )))
+}
+
+fn find_builtin_node_module(module_name: &str) -> Option<&NodeModulePolyfill> {
SUPPORTED_BUILTIN_NODE_MODULES
.iter()
.find(|m| m.name == module_name)
diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs
index 1422ba6b0..d324f4b4b 100644
--- a/ext/node/resolution.rs
+++ b/ext/node/resolution.rs
@@ -16,7 +16,7 @@ use crate::package_json::PackageJson;
use crate::path::PathClean;
use crate::NodeFs;
use crate::NodePermissions;
-use crate::RequireNpmResolver;
+use crate::NpmResolver;
pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"];
@@ -190,7 +190,7 @@ pub fn package_imports_resolve<Fs: NodeFs>(
referrer_kind: NodeModuleKind,
conditions: &[&str],
mode: NodeResolutionMode,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<PathBuf, AnyError> {
if name == "#" || name.starts_with("#/") || name.ends_with('/') {
@@ -328,7 +328,7 @@ fn resolve_package_target_string<Fs: NodeFs>(
internal: bool,
conditions: &[&str],
mode: NodeResolutionMode,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<PathBuf, AnyError> {
if !subpath.is_empty() && !pattern && !target.ends_with('/') {
@@ -438,7 +438,7 @@ fn resolve_package_target<Fs: NodeFs>(
internal: bool,
conditions: &[&str],
mode: NodeResolutionMode,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<Option<PathBuf>, AnyError> {
if let Some(target) = target.as_str() {
@@ -576,7 +576,7 @@ pub fn package_exports_resolve<Fs: NodeFs>(
referrer_kind: NodeModuleKind,
conditions: &[&str],
mode: NodeResolutionMode,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<PathBuf, AnyError> {
if package_exports.contains_key(&package_subpath)
@@ -733,7 +733,7 @@ pub fn package_resolve<Fs: NodeFs>(
referrer_kind: NodeModuleKind,
conditions: &[&str],
mode: NodeResolutionMode,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<Option<PathBuf>, AnyError> {
let (package_name, package_subpath, _is_scoped) =
@@ -763,7 +763,7 @@ pub fn package_resolve<Fs: NodeFs>(
let package_dir_path = npm_resolver.resolve_package_folder_from_package(
&package_name,
- &referrer.to_file_path().unwrap(),
+ referrer,
mode,
)?;
let package_json_path = package_dir_path.join("package.json");
@@ -815,7 +815,7 @@ pub fn package_resolve<Fs: NodeFs>(
pub fn get_package_scope_config<Fs: NodeFs>(
referrer: &ModuleSpecifier,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<PackageJson, AnyError> {
let root_folder = npm_resolver
@@ -826,7 +826,7 @@ pub fn get_package_scope_config<Fs: NodeFs>(
pub fn get_closest_package_json<Fs: NodeFs>(
url: &ModuleSpecifier,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
) -> Result<PackageJson, AnyError> {
let package_json_path =
@@ -836,7 +836,7 @@ pub fn get_closest_package_json<Fs: NodeFs>(
fn get_closest_package_json_path<Fs: NodeFs>(
url: &ModuleSpecifier,
- npm_resolver: &dyn RequireNpmResolver,
+ npm_resolver: &dyn NpmResolver,
) -> Result<PathBuf, AnyError> {
let file_path = url.to_file_path().unwrap();
let mut current_dir = file_path.parent().unwrap();
diff --git a/ext/node/resolver.rs b/ext/node/resolver.rs
new file mode 100644
index 000000000..41e1cf4d4
--- /dev/null
+++ b/ext/node/resolver.rs
@@ -0,0 +1,686 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use std::path::Path;
+use std::path::PathBuf;
+
+use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::serde_json::Value;
+use deno_core::url::Url;
+use deno_core::ModuleSpecifier;
+use deno_media_type::MediaType;
+use deno_semver::npm::NpmPackageNv;
+use deno_semver::npm::NpmPackageNvReference;
+use deno_semver::npm::NpmPackageReqReference;
+
+use crate::errors;
+use crate::get_closest_package_json;
+use crate::legacy_main_resolve;
+use crate::package_exports_resolve;
+use crate::package_imports_resolve;
+use crate::package_resolve;
+use crate::path_to_declaration_path;
+use crate::AllowAllNodePermissions;
+use crate::NodeFs;
+use crate::NodeModuleKind;
+use crate::NodePermissions;
+use crate::NodeResolutionMode;
+use crate::NpmResolver;
+use crate::PackageJson;
+use crate::DEFAULT_CONDITIONS;
+
+#[derive(Debug)]
+pub enum NodeResolution {
+ Esm(ModuleSpecifier),
+ CommonJs(ModuleSpecifier),
+ BuiltIn(String),
+}
+
+impl NodeResolution {
+ pub fn into_url(self) -> ModuleSpecifier {
+ match self {
+ Self::Esm(u) => u,
+ Self::CommonJs(u) => u,
+ Self::BuiltIn(specifier) => {
+ if specifier.starts_with("node:") {
+ ModuleSpecifier::parse(&specifier).unwrap()
+ } else {
+ ModuleSpecifier::parse(&format!("node:{specifier}")).unwrap()
+ }
+ }
+ }
+ }
+
+ pub fn into_specifier_and_media_type(
+ resolution: Option<Self>,
+ ) -> (ModuleSpecifier, MediaType) {
+ match resolution {
+ Some(NodeResolution::CommonJs(specifier)) => {
+ let media_type = MediaType::from_specifier(&specifier);
+ (
+ specifier,
+ match media_type {
+ MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs,
+ MediaType::TypeScript | MediaType::Tsx => MediaType::Cts,
+ MediaType::Dts => MediaType::Dcts,
+ _ => media_type,
+ },
+ )
+ }
+ Some(NodeResolution::Esm(specifier)) => {
+ let media_type = MediaType::from_specifier(&specifier);
+ (
+ specifier,
+ match media_type {
+ MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs,
+ MediaType::TypeScript | MediaType::Tsx => MediaType::Mts,
+ MediaType::Dts => MediaType::Dmts,
+ _ => media_type,
+ },
+ )
+ }
+ Some(resolution) => (resolution.into_url(), MediaType::Dts),
+ None => (
+ ModuleSpecifier::parse("internal:///missing_dependency.d.ts").unwrap(),
+ MediaType::Dts,
+ ),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct NodeResolver<TRequireNpmResolver: NpmResolver> {
+ npm_resolver: TRequireNpmResolver,
+}
+
+impl<TRequireNpmResolver: NpmResolver> NodeResolver<TRequireNpmResolver> {
+ pub fn new(require_npm_resolver: TRequireNpmResolver) -> Self {
+ Self {
+ npm_resolver: require_npm_resolver,
+ }
+ }
+
+ pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool {
+ self.npm_resolver.in_npm_package(specifier)
+ }
+
+ /// This function is an implementation of `defaultResolve` in
+ /// `lib/internal/modules/esm/resolve.js` from Node.
+ pub fn resolve<Fs: NodeFs>(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ // Note: if we are here, then the referrer is an esm module
+ // TODO(bartlomieju): skipped "policy" part as we don't plan to support it
+
+ if crate::is_builtin_node_module(specifier) {
+ return Ok(Some(NodeResolution::BuiltIn(specifier.to_string())));
+ }
+
+ if let Ok(url) = Url::parse(specifier) {
+ if url.scheme() == "data" {
+ return Ok(Some(NodeResolution::Esm(url)));
+ }
+
+ let protocol = url.scheme();
+
+ if protocol == "node" {
+ let split_specifier = url.as_str().split(':');
+ let specifier = split_specifier.skip(1).collect::<String>();
+
+ if crate::is_builtin_node_module(&specifier) {
+ return Ok(Some(NodeResolution::BuiltIn(specifier)));
+ }
+ }
+
+ if protocol != "file" && protocol != "data" {
+ return Err(errors::err_unsupported_esm_url_scheme(&url));
+ }
+
+ // todo(dsherret): this seems wrong
+ if referrer.scheme() == "data" {
+ let url = referrer.join(specifier).map_err(AnyError::from)?;
+ return Ok(Some(NodeResolution::Esm(url)));
+ }
+ }
+
+ let url = self.module_resolve::<Fs>(
+ specifier,
+ referrer,
+ DEFAULT_CONDITIONS,
+ mode,
+ permissions,
+ )?;
+ let url = match url {
+ Some(url) => url,
+ None => return Ok(None),
+ };
+ let url = match mode {
+ NodeResolutionMode::Execution => url,
+ NodeResolutionMode::Types => {
+ let path = url.to_file_path().unwrap();
+ // todo(16370): the module kind is not correct here. I think we need
+ // typescript to tell us if the referrer is esm or cjs
+ let path =
+ match path_to_declaration_path::<Fs>(path, NodeModuleKind::Esm) {
+ Some(path) => path,
+ None => return Ok(None),
+ };
+ ModuleSpecifier::from_file_path(path).unwrap()
+ }
+ };
+
+ let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(Some(resolve_response))
+ }
+
+ fn module_resolve<Fs: NodeFs>(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<ModuleSpecifier>, AnyError> {
+ // note: if we're here, the referrer is an esm module
+ let url = if should_be_treated_as_relative_or_absolute_path(specifier) {
+ let resolved_specifier = referrer.join(specifier)?;
+ if mode.is_types() {
+ let file_path = to_file_path(&resolved_specifier);
+ // todo(dsherret): the node module kind is not correct and we
+ // should use the value provided by typescript instead
+ let declaration_path =
+ path_to_declaration_path::<Fs>(file_path, NodeModuleKind::Esm);
+ declaration_path.map(|declaration_path| {
+ ModuleSpecifier::from_file_path(declaration_path).unwrap()
+ })
+ } else {
+ Some(resolved_specifier)
+ }
+ } else if specifier.starts_with('#') {
+ Some(
+ package_imports_resolve::<Fs>(
+ specifier,
+ referrer,
+ NodeModuleKind::Esm,
+ conditions,
+ mode,
+ &self.npm_resolver,
+ permissions,
+ )
+ .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?,
+ )
+ } else if let Ok(resolved) = Url::parse(specifier) {
+ Some(resolved)
+ } else {
+ package_resolve::<Fs>(
+ specifier,
+ referrer,
+ NodeModuleKind::Esm,
+ conditions,
+ mode,
+ &self.npm_resolver,
+ permissions,
+ )?
+ .map(|p| ModuleSpecifier::from_file_path(p).unwrap())
+ };
+ Ok(match url {
+ Some(url) => Some(finalize_resolution::<Fs>(url, referrer)?),
+ None => None,
+ })
+ }
+
+ pub fn resolve_npm_req_reference<Fs: NodeFs>(
+ &self,
+ reference: &NpmPackageReqReference,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ let reference = self
+ .npm_resolver
+ .resolve_nv_ref_from_pkg_req_ref(reference)?;
+ self.resolve_npm_reference::<Fs>(&reference, mode, permissions)
+ }
+
+ pub fn resolve_npm_reference<Fs: NodeFs>(
+ &self,
+ reference: &NpmPackageNvReference,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(&reference.nv)?;
+ let node_module_kind = NodeModuleKind::Esm;
+ let maybe_resolved_path = package_config_resolve::<Fs>(
+ &reference
+ .sub_path
+ .as_ref()
+ .map(|s| format!("./{s}"))
+ .unwrap_or_else(|| ".".to_string()),
+ &package_folder,
+ node_module_kind,
+ DEFAULT_CONDITIONS,
+ mode,
+ &self.npm_resolver,
+ permissions,
+ )
+ .with_context(|| {
+ format!("Error resolving package config for '{reference}'")
+ })?;
+ let resolved_path = match maybe_resolved_path {
+ Some(resolved_path) => resolved_path,
+ None => return Ok(None),
+ };
+ let resolved_path = match mode {
+ NodeResolutionMode::Execution => resolved_path,
+ NodeResolutionMode::Types => {
+ match path_to_declaration_path::<Fs>(resolved_path, node_module_kind) {
+ Some(path) => path,
+ None => return Ok(None),
+ }
+ }
+ };
+ let url = ModuleSpecifier::from_file_path(resolved_path).unwrap();
+ let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(Some(resolve_response))
+ }
+
+ pub fn resolve_binary_commands<Fs: NodeFs>(
+ &self,
+ pkg_nv: &NpmPackageNv,
+ ) -> Result<Vec<String>, AnyError> {
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(pkg_nv)?;
+ let package_json_path = package_folder.join("package.json");
+ let package_json = PackageJson::load::<Fs>(
+ &self.npm_resolver,
+ &mut AllowAllNodePermissions,
+ package_json_path,
+ )?;
+
+ Ok(match package_json.bin {
+ Some(Value::String(_)) => vec![pkg_nv.name.to_string()],
+ Some(Value::Object(o)) => {
+ o.into_iter().map(|(key, _)| key).collect::<Vec<_>>()
+ }
+ _ => Vec::new(),
+ })
+ }
+
+ pub fn resolve_binary_export<Fs: NodeFs>(
+ &self,
+ pkg_ref: &NpmPackageReqReference,
+ ) -> Result<NodeResolution, AnyError> {
+ let pkg_nv = self
+ .npm_resolver
+ .resolve_pkg_id_from_pkg_req(&pkg_ref.req)?
+ .nv;
+ let bin_name = pkg_ref.sub_path.as_deref();
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(&pkg_nv)?;
+ let package_json_path = package_folder.join("package.json");
+ let package_json = PackageJson::load::<Fs>(
+ &self.npm_resolver,
+ &mut AllowAllNodePermissions,
+ 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_nv.name,
+ ),
+ };
+ let bin_entry = resolve_bin_entry_value(&pkg_nv, bin_name, bin)?;
+ let url =
+ ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap();
+
+ let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(resolve_response)
+ }
+
+ pub fn url_to_node_resolution<Fs: NodeFs>(
+ &self,
+ url: ModuleSpecifier,
+ ) -> Result<NodeResolution, AnyError> {
+ let url_str = url.as_str().to_lowercase();
+ if url_str.starts_with("http") {
+ Ok(NodeResolution::Esm(url))
+ } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") {
+ let package_config = get_closest_package_json::<Fs>(
+ &url,
+ &self.npm_resolver,
+ &mut AllowAllNodePermissions,
+ )?;
+ if package_config.typ == "module" {
+ Ok(NodeResolution::Esm(url))
+ } else {
+ Ok(NodeResolution::CommonJs(url))
+ }
+ } else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") {
+ Ok(NodeResolution::Esm(url))
+ } else if url_str.ends_with(".ts") {
+ Err(generic_error(format!(
+ "TypeScript files are not supported in npm packages: {url}"
+ )))
+ } else {
+ Ok(NodeResolution::CommonJs(url))
+ }
+ }
+}
+
+fn resolve_bin_entry_value<'a>(
+ pkg_nv: &NpmPackageNv,
+ bin_name: Option<&str>,
+ bin: &'a Value,
+) -> Result<&'a str, AnyError> {
+ let bin_entry = match bin {
+ Value::String(_) => {
+ if bin_name.is_some() && bin_name.unwrap() != pkg_nv.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.len() > 1 && o.values().all(|v| v == o.values().next().unwrap()) {
+ o.values().next()
+ } else {
+ o.get(&pkg_nv.name)
+ }
+ },
+ _ => bail!("package '{}' did not have a bin property with a string or object value in its package.json", pkg_nv),
+ };
+ let bin_entry = match bin_entry {
+ Some(e) => e,
+ None => {
+ let keys = bin
+ .as_object()
+ .map(|o| {
+ o.keys()
+ .map(|k| format!(" * npm:{pkg_nv}/{k}"))
+ .collect::<Vec<_>>()
+ })
+ .unwrap_or_default();
+ bail!(
+ "package '{}' did not have a bin entry for '{}' in its package.json{}",
+ pkg_nv,
+ bin_name.unwrap_or(&pkg_nv.name),
+ if keys.is_empty() {
+ "".to_string()
+ } else {
+ format!("\n\nPossibilities:\n{}", keys.join("\n"))
+ }
+ )
+ }
+ };
+ match bin_entry {
+ Value::String(s) => Ok(s),
+ _ => bail!(
+ "package '{}' had a non-string sub property of bin in its package.json",
+ pkg_nv,
+ ),
+ }
+}
+
+fn package_config_resolve<Fs: NodeFs>(
+ package_subpath: &str,
+ package_dir: &Path,
+ referrer_kind: NodeModuleKind,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ npm_resolver: &dyn NpmResolver,
+ permissions: &mut dyn NodePermissions,
+) -> Result<Option<PathBuf>, AnyError> {
+ let package_json_path = package_dir.join("package.json");
+ let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap();
+ let package_config = PackageJson::load::<Fs>(
+ npm_resolver,
+ permissions,
+ package_json_path.clone(),
+ )?;
+ if let Some(exports) = &package_config.exports {
+ let result = package_exports_resolve::<Fs>(
+ &package_json_path,
+ package_subpath.to_string(),
+ exports,
+ &referrer,
+ referrer_kind,
+ conditions,
+ mode,
+ npm_resolver,
+ permissions,
+ );
+ match result {
+ Ok(found) => return Ok(Some(found)),
+ Err(exports_err) => {
+ if mode.is_types() && package_subpath == "." {
+ if let Ok(Some(path)) =
+ legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode)
+ {
+ return Ok(Some(path));
+ } else {
+ return Ok(None);
+ }
+ }
+ return Err(exports_err);
+ }
+ }
+ }
+ if package_subpath == "." {
+ return legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode);
+ }
+
+ Ok(Some(package_dir.join(package_subpath)))
+}
+
+fn finalize_resolution<Fs: NodeFs>(
+ resolved: ModuleSpecifier,
+ base: &ModuleSpecifier,
+) -> Result<ModuleSpecifier, AnyError> {
+ let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C");
+
+ if encoded_sep_re.is_match(resolved.path()) {
+ return Err(errors::err_invalid_module_specifier(
+ resolved.path(),
+ "must not include encoded \"/\" or \"\\\\\" characters",
+ Some(to_file_path_string(base)),
+ ));
+ }
+
+ let path = to_file_path(&resolved);
+
+ // TODO(bartlomieju): currently not supported
+ // if (getOptionValue('--experimental-specifier-resolution') === 'node') {
+ // ...
+ // }
+
+ let p_str = path.to_str().unwrap();
+ let p = if p_str.ends_with('/') {
+ p_str[p_str.len() - 1..].to_string()
+ } else {
+ p_str.to_string()
+ };
+
+ let (is_dir, is_file) = if let Ok(stats) = Fs::metadata(p) {
+ (stats.is_dir, stats.is_file)
+ } else {
+ (false, false)
+ };
+ if is_dir {
+ return Err(errors::err_unsupported_dir_import(
+ resolved.as_str(),
+ base.as_str(),
+ ));
+ } else if !is_file {
+ return Err(errors::err_module_not_found(
+ resolved.as_str(),
+ base.as_str(),
+ "module",
+ ));
+ }
+
+ Ok(resolved)
+}
+
+fn to_file_path(url: &ModuleSpecifier) -> PathBuf {
+ url
+ .to_file_path()
+ .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}"))
+}
+
+fn to_file_path_string(url: &ModuleSpecifier) -> String {
+ to_file_path(url).display().to_string()
+}
+
+fn should_be_treated_as_relative_or_absolute_path(specifier: &str) -> bool {
+ if specifier.is_empty() {
+ return false;
+ }
+
+ if specifier.starts_with('/') {
+ return true;
+ }
+
+ is_relative_specifier(specifier)
+}
+
+// TODO(ry) We very likely have this utility function elsewhere in Deno.
+fn is_relative_specifier(specifier: &str) -> bool {
+ let specifier_len = specifier.len();
+ let specifier_chars: Vec<_> = specifier.chars().collect();
+
+ if !specifier_chars.is_empty() && specifier_chars[0] == '.' {
+ if specifier_len == 1 || specifier_chars[1] == '/' {
+ return true;
+ }
+ if specifier_chars[1] == '.'
+ && (specifier_len == 2 || specifier_chars[2] == '/')
+ {
+ return true;
+ }
+ }
+ false
+}
+
+#[cfg(test)]
+mod tests {
+ use deno_core::serde_json::json;
+
+ use super::*;
+
+ #[test]
+ fn test_resolve_bin_entry_value() {
+ // should resolve the specified value
+ let value = json!({
+ "bin1": "./value1",
+ "bin2": "./value2",
+ "test": "./value3",
+ });
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ Some("bin1"),
+ &value
+ )
+ .unwrap(),
+ "./value1"
+ );
+
+ // should resolve the value with the same name when not specified
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ None,
+ &value
+ )
+ .unwrap(),
+ "./value3"
+ );
+
+ // should not resolve when specified value does not exist
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ Some("other"),
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ concat!(
+ "package 'test@1.1.1' did not have a bin entry for 'other' in its package.json\n",
+ "\n",
+ "Possibilities:\n",
+ " * npm:test@1.1.1/bin1\n",
+ " * npm:test@1.1.1/bin2\n",
+ " * npm:test@1.1.1/test"
+ )
+ );
+
+ // should not resolve when default value can't be determined
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("asdf@1.2.3").unwrap(),
+ None,
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ concat!(
+ "package 'asdf@1.2.3' did not have a bin entry for 'asdf' in its package.json\n",
+ "\n",
+ "Possibilities:\n",
+ " * npm:asdf@1.2.3/bin1\n",
+ " * npm:asdf@1.2.3/bin2\n",
+ " * npm:asdf@1.2.3/test"
+ )
+ );
+
+ // should resolve since all the values are the same
+ let value = json!({
+ "bin1": "./value",
+ "bin2": "./value",
+ });
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.2.3").unwrap(),
+ None,
+ &value
+ )
+ .unwrap(),
+ "./value"
+ );
+
+ // should not resolve when specified and is a string
+ let value = json!("./value");
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.2.3").unwrap(),
+ Some("path"),
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ "package 'test@1.2.3' did not have a bin entry for 'path' in its package.json"
+ );
+ }
+}