diff options
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/Cargo.toml | 3 | ||||
-rw-r--r-- | ext/node/analyze.rs | 16 | ||||
-rw-r--r-- | ext/node/lib.rs | 107 | ||||
-rw-r--r-- | ext/node/ops.rs | 26 | ||||
-rw-r--r-- | ext/node/package_json.rs | 4 | ||||
-rw-r--r-- | ext/node/polyfill.rs | 20 | ||||
-rw-r--r-- | ext/node/resolution.rs | 20 | ||||
-rw-r--r-- | ext/node/resolver.rs | 686 |
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" + ); + } +} |