diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-04-24 19:44:35 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-24 19:44:35 -0400 |
commit | aa286fdecb15461ef8ddd4c372f5a13e01e1cb7b (patch) | |
tree | 47ddc4e428b650fae536c3c1eb73ae5a64c4fe6a /ext/node/resolution.rs | |
parent | bb74e75a049768c2949aa08de6752a16813b97de (diff) |
refactor(ext/node): allow injecting `NodeFs` from CLI (#18829)
This allows providing a `NodeFs` as part of the `WorkerOptions`.
Diffstat (limited to 'ext/node/resolution.rs')
-rw-r--r-- | ext/node/resolution.rs | 2099 |
1 files changed, 1375 insertions, 724 deletions
diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs index d324f4b4b..e5db6b3ac 100644 --- a/ext/node/resolution.rs +++ b/ext/node/resolution.rs @@ -2,21 +2,28 @@ use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; 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::Map; 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::package_json::PackageJson; -use crate::path::PathClean; +use crate::AllowAllNodePermissions; use crate::NodeFs; use crate::NodePermissions; use crate::NpmResolver; +use crate::PackageJson; +use crate::PathClean; pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; @@ -39,53 +46,1260 @@ impl NodeResolutionMode { } } -/// Checks if the resolved file has a corresponding declaration file. -pub fn path_to_declaration_path<Fs: NodeFs>( - path: PathBuf, - referrer_kind: NodeModuleKind, -) -> Option<PathBuf> { - fn probe_extensions<Fs: NodeFs>( - path: &Path, +#[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 { + fs: Arc<dyn NodeFs>, + npm_resolver: Arc<dyn NpmResolver>, +} + +impl NodeResolver { + pub fn new(fs: Arc<dyn NodeFs>, npm_resolver: Arc<dyn NpmResolver>) -> Self { + Self { fs, 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( + &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( + 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 self.path_to_declaration_path(path, NodeModuleKind::Esm) { + Some(path) => path, + None => return Ok(None), + }; + ModuleSpecifier::from_file_path(path).unwrap() + } + }; + + let resolve_response = self.url_to_node_resolution(url)?; + // TODO(bartlomieju): skipped checking errors for commonJS resolution and + // "preserveSymlinksMain"/"preserveSymlinks" options. + Ok(Some(resolve_response)) + } + + fn module_resolve( + &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 = + self.path_to_declaration_path(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( + self + .package_imports_resolve( + specifier, + referrer, + NodeModuleKind::Esm, + conditions, + mode, + permissions, + ) + .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?, + ) + } else if let Ok(resolved) = Url::parse(specifier) { + Some(resolved) + } else { + self + .package_resolve( + specifier, + referrer, + NodeModuleKind::Esm, + conditions, + mode, + permissions, + )? + .map(|p| ModuleSpecifier::from_file_path(p).unwrap()) + }; + Ok(match url { + Some(url) => Some(self.finalize_resolution(url, referrer)?), + None => None, + }) + } + + fn finalize_resolution( + &self, + 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) = self.fs.metadata(Path::new(&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) + } + + pub fn resolve_npm_req_reference( + &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(&reference, mode, permissions) + } + + pub fn resolve_npm_reference( + &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 = self + .package_config_resolve( + &reference + .sub_path + .as_ref() + .map(|s| format!("./{s}")) + .unwrap_or_else(|| ".".to_string()), + &package_folder, + node_module_kind, + DEFAULT_CONDITIONS, + mode, + 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 self.path_to_declaration_path(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(url)?; + // TODO(bartlomieju): skipped checking errors for commonJS resolution and + // "preserveSymlinksMain"/"preserveSymlinks" options. + Ok(Some(resolve_response)) + } + + pub fn resolve_binary_commands( + &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 = self + .load_package_json(&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( + &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 = self + .load_package_json(&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(url)?; + // TODO(bartlomieju): skipped checking errors for commonJS resolution and + // "preserveSymlinksMain"/"preserveSymlinks" options. + Ok(resolve_response) + } + + pub fn url_to_node_resolution( + &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 = + self.get_closest_package_json(&url, &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 package_config_resolve( + &self, + package_subpath: &str, + package_dir: &Path, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + 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 = + self.load_package_json(permissions, package_json_path.clone())?; + if let Some(exports) = &package_config.exports { + let result = self.package_exports_resolve( + &package_json_path, + package_subpath.to_string(), + exports, + &referrer, + referrer_kind, + conditions, + mode, + permissions, + ); + match result { + Ok(found) => return Ok(Some(found)), + Err(exports_err) => { + if mode.is_types() && package_subpath == "." { + if let Ok(Some(path)) = + self.legacy_main_resolve(&package_config, referrer_kind, mode) + { + return Ok(Some(path)); + } else { + return Ok(None); + } + } + return Err(exports_err); + } + } + } + if package_subpath == "." { + return self.legacy_main_resolve(&package_config, referrer_kind, mode); + } + + Ok(Some(package_dir.join(package_subpath))) + } + + /// Checks if the resolved file has a corresponding declaration file. + pub(super) fn path_to_declaration_path( + &self, + path: PathBuf, referrer_kind: NodeModuleKind, ) -> Option<PathBuf> { - let specific_dts_path = match referrer_kind { - NodeModuleKind::Cjs => with_known_extension(path, "d.cts"), - NodeModuleKind::Esm => with_known_extension(path, "d.mts"), + fn probe_extensions( + fs: &dyn NodeFs, + path: &Path, + referrer_kind: NodeModuleKind, + ) -> Option<PathBuf> { + let specific_dts_path = match referrer_kind { + NodeModuleKind::Cjs => with_known_extension(path, "d.cts"), + NodeModuleKind::Esm => with_known_extension(path, "d.mts"), + }; + if fs.exists(&specific_dts_path) { + return Some(specific_dts_path); + } + let dts_path = with_known_extension(path, "d.ts"); + if fs.exists(&dts_path) { + Some(dts_path) + } else { + None + } + } + + let lowercase_path = path.to_string_lossy().to_lowercase(); + if lowercase_path.ends_with(".d.ts") + || lowercase_path.ends_with(".d.cts") + || lowercase_path.ends_with(".d.ts") + { + return Some(path); + } + if let Some(path) = probe_extensions(&*self.fs, &path, referrer_kind) { + return Some(path); + } + if self.fs.is_dir(&path) { + if let Some(path) = + probe_extensions(&*self.fs, &path.join("index"), referrer_kind) + { + return Some(path); + } + } + None + } + + pub(super) fn package_imports_resolve( + &self, + name: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<PathBuf, AnyError> { + if name == "#" || name.starts_with("#/") || name.ends_with('/') { + let reason = "is not a valid internal imports specifier name"; + return Err(errors::err_invalid_module_specifier( + name, + reason, + Some(to_specifier_display_string(referrer)), + )); + } + + let package_config = + self.get_package_scope_config(referrer, permissions)?; + let mut package_json_path = None; + if package_config.exists { + package_json_path = Some(package_config.path.clone()); + if let Some(imports) = &package_config.imports { + if imports.contains_key(name) && !name.contains('*') { + let maybe_resolved = self.resolve_package_target( + package_json_path.as_ref().unwrap(), + imports.get(name).unwrap().to_owned(), + "".to_string(), + name.to_string(), + referrer, + referrer_kind, + false, + true, + conditions, + mode, + permissions, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } else { + let mut best_match = ""; + let mut best_match_subpath = None; + for key in imports.keys() { + let pattern_index = key.find('*'); + if let Some(pattern_index) = pattern_index { + let key_sub = &key[0..=pattern_index]; + if name.starts_with(key_sub) { + let pattern_trailer = &key[pattern_index + 1..]; + if name.len() > key.len() + && name.ends_with(&pattern_trailer) + && pattern_key_compare(best_match, key) == 1 + && key.rfind('*') == Some(pattern_index) + { + best_match = key; + best_match_subpath = Some( + name[pattern_index..=(name.len() - pattern_trailer.len())] + .to_string(), + ); + } + } + } + } + + if !best_match.is_empty() { + let target = imports.get(best_match).unwrap().to_owned(); + let maybe_resolved = self.resolve_package_target( + package_json_path.as_ref().unwrap(), + target, + best_match_subpath.unwrap(), + best_match.to_string(), + referrer, + referrer_kind, + true, + true, + conditions, + mode, + permissions, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } + } + } + } + + Err(throw_import_not_defined( + name, + package_json_path.as_deref(), + referrer, + )) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_target_string( + &self, + target: String, + subpath: String, + match_: String, + package_json_path: &Path, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + pattern: bool, + internal: bool, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<PathBuf, AnyError> { + if !subpath.is_empty() && !pattern && !target.ends_with('/') { + return Err(throw_invalid_package_target( + match_, + target, + package_json_path, + internal, + referrer, + )); + } + let invalid_segment_re = + lazy_regex::regex!(r"(^|\\|/)(\.\.?|node_modules)(\\|/|$)"); + let pattern_re = lazy_regex::regex!(r"\*"); + if !target.starts_with("./") { + if internal && !target.starts_with("../") && !target.starts_with('/') { + let is_url = Url::parse(&target).is_ok(); + if !is_url { + let export_target = if pattern { + pattern_re + .replace(&target, |_caps: ®ex::Captures| subpath.clone()) + .to_string() + } else { + format!("{target}{subpath}") + }; + let package_json_url = + ModuleSpecifier::from_file_path(package_json_path).unwrap(); + return match self.package_resolve( + &export_target, + &package_json_url, + referrer_kind, + conditions, + mode, + permissions, + ) { + Ok(Some(path)) => Ok(path), + Ok(None) => Err(generic_error("not found")), + Err(err) => Err(err), + }; + } + } + return Err(throw_invalid_package_target( + match_, + target, + package_json_path, + internal, + referrer, + )); + } + if invalid_segment_re.is_match(&target[2..]) { + return Err(throw_invalid_package_target( + match_, + target, + package_json_path, + internal, + referrer, + )); + } + let package_path = package_json_path.parent().unwrap(); + let resolved_path = package_path.join(&target).clean(); + if !resolved_path.starts_with(package_path) { + return Err(throw_invalid_package_target( + match_, + target, + package_json_path, + internal, + referrer, + )); + } + if subpath.is_empty() { + return Ok(resolved_path); + } + if invalid_segment_re.is_match(&subpath) { + let request = if pattern { + match_.replace('*', &subpath) + } else { + format!("{match_}{subpath}") + }; + return Err(throw_invalid_subpath( + request, + package_json_path, + internal, + referrer, + )); + } + if pattern { + let resolved_path_str = resolved_path.to_string_lossy(); + let replaced = pattern_re + .replace(&resolved_path_str, |_caps: ®ex::Captures| { + subpath.clone() + }); + return Ok(PathBuf::from(replaced.to_string())); + } + Ok(resolved_path.join(&subpath).clean()) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_target( + &self, + package_json_path: &Path, + target: Value, + subpath: String, + package_subpath: String, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + pattern: bool, + internal: bool, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<Option<PathBuf>, AnyError> { + if let Some(target) = target.as_str() { + return self + .resolve_package_target_string( + target.to_string(), + subpath, + package_subpath, + package_json_path, + referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + permissions, + ) + .map(|path| { + if mode.is_types() { + self.path_to_declaration_path(path, referrer_kind) + } else { + Some(path) + } + }); + } else if let Some(target_arr) = target.as_array() { + if target_arr.is_empty() { + return Ok(None); + } + + let mut last_error = None; + for target_item in target_arr { + let resolved_result = self.resolve_package_target( + package_json_path, + target_item.to_owned(), + subpath.clone(), + package_subpath.clone(), + referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + permissions, + ); + + match resolved_result { + Ok(Some(resolved)) => return Ok(Some(resolved)), + Ok(None) => { + last_error = None; + continue; + } + Err(e) => { + let err_string = e.to_string(); + last_error = Some(e); + if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") { + continue; + } + return Err(last_error.unwrap()); + } + } + } + if last_error.is_none() { + return Ok(None); + } + return Err(last_error.unwrap()); + } else if let Some(target_obj) = target.as_object() { + for key in target_obj.keys() { + // TODO(bartlomieju): verify that keys are not numeric + // return Err(errors::err_invalid_package_config( + // to_file_path_string(package_json_url), + // Some(base.as_str().to_string()), + // Some("\"exports\" cannot contain numeric property keys.".to_string()), + // )); + + if key == "default" + || conditions.contains(&key.as_str()) + || mode.is_types() && key.as_str() == "types" + { + let condition_target = target_obj.get(key).unwrap().to_owned(); + + let resolved = self.resolve_package_target( + package_json_path, + condition_target, + subpath.clone(), + package_subpath.clone(), + referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + permissions, + )?; + match resolved { + Some(resolved) => return Ok(Some(resolved)), + None => { + continue; + } + } + } + } + } else if target.is_null() { + return Ok(None); + } + + Err(throw_invalid_package_target( + package_subpath, + target.to_string(), + package_json_path, + internal, + referrer, + )) + } + + #[allow(clippy::too_many_arguments)] + pub fn package_exports_resolve( + &self, + package_json_path: &Path, + package_subpath: String, + package_exports: &Map<String, Value>, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<PathBuf, AnyError> { + if package_exports.contains_key(&package_subpath) + && package_subpath.find('*').is_none() + && !package_subpath.ends_with('/') + { + let target = package_exports.get(&package_subpath).unwrap().to_owned(); + let resolved = self.resolve_package_target( + package_json_path, + target, + "".to_string(), + package_subpath.to_string(), + referrer, + referrer_kind, + false, + false, + conditions, + mode, + permissions, + )?; + if resolved.is_none() { + return Err(throw_exports_not_found( + package_subpath, + package_json_path, + referrer, + )); + } + return Ok(resolved.unwrap()); + } + + let mut best_match = ""; + let mut best_match_subpath = None; + for key in package_exports.keys() { + let pattern_index = key.find('*'); + if let Some(pattern_index) = pattern_index { + let key_sub = &key[0..pattern_index]; + if package_subpath.starts_with(key_sub) { + // When this reaches EOL, this can throw at the top of the whole function: + // + // if (StringPrototypeEndsWith(packageSubpath, '/')) + // throwInvalidSubpath(packageSubpath) + // + // To match "imports" and the spec. + if package_subpath.ends_with('/') { + // TODO(bartlomieju): + // emitTrailingSlashPatternDeprecation(); + } + let pattern_trailer = &key[pattern_index + 1..]; + if package_subpath.len() > key.len() + && package_subpath.ends_with(&pattern_trailer) + && pattern_key_compare(best_match, key) == 1 + && key.rfind('*') == Some(pattern_index) + { + best_match = key; + best_match_subpath = Some( + package_subpath[pattern_index + ..(package_subpath.len() - pattern_trailer.len())] + .to_string(), + ); + } + } + } + } + + if !best_match.is_empty() { + let target = package_exports.get(best_match).unwrap().to_owned(); + let maybe_resolved = self.resolve_package_target( + package_json_path, + target, + best_match_subpath.unwrap(), + best_match.to_string(), + referrer, + referrer_kind, + true, + false, + conditions, + mode, + permissions, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } else { + return Err(throw_exports_not_found( + package_subpath, + package_json_path, + referrer, + )); + } + } + + Err(throw_exports_not_found( + package_subpath, + package_json_path, + referrer, + )) + } + + pub(super) fn package_resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<Option<PathBuf>, AnyError> { + let (package_name, package_subpath, _is_scoped) = + parse_package_name(specifier, referrer)?; + + // ResolveSelf + let package_config = + self.get_package_scope_config(referrer, permissions)?; + if package_config.exists + && package_config.name.as_ref() == Some(&package_name) + { + if let Some(exports) = &package_config.exports { + return self + .package_exports_resolve( + &package_config.path, + package_subpath, + exports, + referrer, + referrer_kind, + conditions, + mode, + permissions, + ) + .map(Some); + } + } + + let package_dir_path = self + .npm_resolver + .resolve_package_folder_from_package(&package_name, referrer, mode)?; + let package_json_path = package_dir_path.join("package.json"); + + // todo: error with this instead when can't find package + // Err(errors::err_module_not_found( + // &package_json_url + // .join(".") + // .unwrap() + // .to_file_path() + // .unwrap() + // .display() + // .to_string(), + // &to_file_path_string(referrer), + // "package", + // )) + + // Package match. + let package_json = + self.load_package_json(permissions, package_json_path)?; + if let Some(exports) = &package_json.exports { + return self + .package_exports_resolve( + &package_json.path, + package_subpath, + exports, + referrer, + referrer_kind, + conditions, + mode, + permissions, + ) + .map(Some); + } + if package_subpath == "." { + return self.legacy_main_resolve(&package_json, referrer_kind, mode); + } + + let file_path = package_json.path.parent().unwrap().join(&package_subpath); + + if mode.is_types() { + let maybe_declaration_path = + self.path_to_declaration_path(file_path, referrer_kind); + Ok(maybe_declaration_path) + } else { + Ok(Some(file_path)) + } + } + + pub(super) fn get_package_scope_config( + &self, + referrer: &ModuleSpecifier, + permissions: &mut dyn NodePermissions, + ) -> Result<PackageJson, AnyError> { + let root_folder = self + .npm_resolver + .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?; + let package_json_path = root_folder.join("package.json"); + self.load_package_json(permissions, package_json_path) + } + + pub(super) fn get_closest_package_json( + &self, + url: &ModuleSpecifier, + permissions: &mut dyn NodePermissions, + ) -> Result<PackageJson, AnyError> { + let package_json_path = self.get_closest_package_json_path(url)?; + self.load_package_json(permissions, package_json_path) + } + + fn get_closest_package_json_path( + &self, + url: &ModuleSpecifier, + ) -> Result<PathBuf, AnyError> { + let file_path = url.to_file_path().unwrap(); + let mut current_dir = file_path.parent().unwrap(); + let package_json_path = current_dir.join("package.json"); + if self.fs.exists(&package_json_path) { + return Ok(package_json_path); + } + let root_pkg_folder = self + .npm_resolver + .resolve_package_folder_from_path(&url.to_file_path().unwrap())?; + while current_dir.starts_with(&root_pkg_folder) { + current_dir = current_dir.parent().unwrap(); + let package_json_path = current_dir.join("package.json"); + if self.fs.exists(&package_json_path) { + return Ok(package_json_path); + } + } + + bail!("did not find package.json in {}", root_pkg_folder.display()) + } + + pub(super) fn load_package_json( + &self, + permissions: &mut dyn NodePermissions, + package_json_path: PathBuf, + ) -> Result<PackageJson, AnyError> { + PackageJson::load( + &*self.fs, + &*self.npm_resolver, + permissions, + package_json_path, + ) + } + + pub(super) fn legacy_main_resolve( + &self, + package_json: &PackageJson, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Option<PathBuf>, AnyError> { + let maybe_main = if mode.is_types() { + match package_json.types.as_ref() { + Some(types) => Some(types), + None => { + // fallback to checking the main entrypoint for + // a corresponding declaration file + if let Some(main) = package_json.main(referrer_kind) { + let main = package_json.path.parent().unwrap().join(main).clean(); + if let Some(path) = + self.path_to_declaration_path(main, referrer_kind) + { + return Ok(Some(path)); + } + } + None + } + } + } else { + package_json.main(referrer_kind) }; - if Fs::exists(&specific_dts_path) { - return Some(specific_dts_path); + + if let Some(main) = maybe_main { + let guess = package_json.path.parent().unwrap().join(main).clean(); + if self.fs.is_file(&guess) { + return Ok(Some(guess)); + } + + // todo(dsherret): investigate exactly how node and typescript handles this + let endings = if mode.is_types() { + match referrer_kind { + NodeModuleKind::Cjs => { + vec![".d.ts", ".d.cts", "/index.d.ts", "/index.d.cts"] + } + NodeModuleKind::Esm => vec![ + ".d.ts", + ".d.mts", + "/index.d.ts", + "/index.d.mts", + ".d.cts", + "/index.d.cts", + ], + } + } else { + vec![".js", "/index.js"] + }; + for ending in endings { + let guess = package_json + .path + .parent() + .unwrap() + .join(format!("{main}{ending}")) + .clean(); + if self.fs.is_file(&guess) { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(Some(guess)); + } + } } - let dts_path = with_known_extension(path, "d.ts"); - if Fs::exists(&dts_path) { - Some(dts_path) + + let index_file_names = if mode.is_types() { + // todo(dsherret): investigate exactly how typescript does this + match referrer_kind { + NodeModuleKind::Cjs => vec!["index.d.ts", "index.d.cts"], + NodeModuleKind::Esm => vec!["index.d.ts", "index.d.mts", "index.d.cts"], + } } else { - None + vec!["index.js"] + }; + for index_file_name in index_file_names { + let guess = package_json + .path + .parent() + .unwrap() + .join(index_file_name) + .clean(); + if self.fs.is_file(&guess) { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(Some(guess)); + } + } + + Ok(None) + } +} + +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, + ), } +} - let lowercase_path = path.to_string_lossy().to_lowercase(); - if lowercase_path.ends_with(".d.ts") - || lowercase_path.ends_with(".d.cts") - || lowercase_path.ends_with(".d.ts") - { - return Some(path); +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 let Some(path) = probe_extensions::<Fs>(&path, referrer_kind) { - return Some(path); + + if specifier.starts_with('/') { + return true; } - if Fs::is_dir(&path) { - if let Some(path) = - probe_extensions::<Fs>(&path.join("index"), referrer_kind) + + 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 Some(path); + return true; } } - None + false } /// Alternate `PathBuf::with_extension` that will handle known extensions /// more intelligently. -pub fn with_known_extension(path: &Path, ext: &str) -> PathBuf { +fn with_known_extension(path: &Path, ext: &str) -> PathBuf { const NON_DECL_EXTS: &[&str] = &["cjs", "js", "json", "jsx", "mjs", "tsx"]; const DECL_EXTS: &[&str] = &["cts", "mts", "ts"]; @@ -142,145 +1356,6 @@ fn throw_import_not_defined( ) } -fn pattern_key_compare(a: &str, b: &str) -> i32 { - let a_pattern_index = a.find('*'); - let b_pattern_index = b.find('*'); - - let base_len_a = if let Some(index) = a_pattern_index { - index + 1 - } else { - a.len() - }; - let base_len_b = if let Some(index) = b_pattern_index { - index + 1 - } else { - b.len() - }; - - if base_len_a > base_len_b { - return -1; - } - - if base_len_b > base_len_a { - return 1; - } - - if a_pattern_index.is_none() { - return 1; - } - - if b_pattern_index.is_none() { - return -1; - } - - if a.len() > b.len() { - return -1; - } - - if b.len() > a.len() { - return 1; - } - - 0 -} - -pub fn package_imports_resolve<Fs: NodeFs>( - name: &str, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PathBuf, AnyError> { - if name == "#" || name.starts_with("#/") || name.ends_with('/') { - let reason = "is not a valid internal imports specifier name"; - return Err(errors::err_invalid_module_specifier( - name, - reason, - Some(to_specifier_display_string(referrer)), - )); - } - - let package_config = - get_package_scope_config::<Fs>(referrer, npm_resolver, permissions)?; - let mut package_json_path = None; - if package_config.exists { - package_json_path = Some(package_config.path.clone()); - if let Some(imports) = &package_config.imports { - if imports.contains_key(name) && !name.contains('*') { - let maybe_resolved = resolve_package_target::<Fs>( - package_json_path.as_ref().unwrap(), - imports.get(name).unwrap().to_owned(), - "".to_string(), - name.to_string(), - referrer, - referrer_kind, - false, - true, - conditions, - mode, - npm_resolver, - permissions, - )?; - if let Some(resolved) = maybe_resolved { - return Ok(resolved); - } - } else { - let mut best_match = ""; - let mut best_match_subpath = None; - for key in imports.keys() { - let pattern_index = key.find('*'); - if let Some(pattern_index) = pattern_index { - let key_sub = &key[0..=pattern_index]; - if name.starts_with(key_sub) { - let pattern_trailer = &key[pattern_index + 1..]; - if name.len() > key.len() - && name.ends_with(&pattern_trailer) - && pattern_key_compare(best_match, key) == 1 - && key.rfind('*') == Some(pattern_index) - { - best_match = key; - best_match_subpath = Some( - name[pattern_index..=(name.len() - pattern_trailer.len())] - .to_string(), - ); - } - } - } - } - - if !best_match.is_empty() { - let target = imports.get(best_match).unwrap().to_owned(); - let maybe_resolved = resolve_package_target::<Fs>( - package_json_path.as_ref().unwrap(), - target, - best_match_subpath.unwrap(), - best_match.to_string(), - referrer, - referrer_kind, - true, - true, - conditions, - mode, - npm_resolver, - permissions, - )?; - if let Some(resolved) = maybe_resolved { - return Ok(resolved); - } - } - } - } - } - - Err(throw_import_not_defined( - name, - package_json_path.as_deref(), - referrer, - )) -} - fn throw_invalid_package_target( subpath: String, target: String, @@ -316,245 +1391,6 @@ fn throw_invalid_subpath( ) } -#[allow(clippy::too_many_arguments)] -fn resolve_package_target_string<Fs: NodeFs>( - target: String, - subpath: String, - match_: String, - package_json_path: &Path, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - pattern: bool, - internal: bool, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PathBuf, AnyError> { - if !subpath.is_empty() && !pattern && !target.ends_with('/') { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); - } - let invalid_segment_re = - lazy_regex::regex!(r"(^|\\|/)(\.\.?|node_modules)(\\|/|$)"); - let pattern_re = lazy_regex::regex!(r"\*"); - if !target.starts_with("./") { - if internal && !target.starts_with("../") && !target.starts_with('/') { - let is_url = Url::parse(&target).is_ok(); - if !is_url { - let export_target = if pattern { - pattern_re - .replace(&target, |_caps: ®ex::Captures| subpath.clone()) - .to_string() - } else { - format!("{target}{subpath}") - }; - let package_json_url = - ModuleSpecifier::from_file_path(package_json_path).unwrap(); - return match package_resolve::<Fs>( - &export_target, - &package_json_url, - referrer_kind, - conditions, - mode, - npm_resolver, - permissions, - ) { - Ok(Some(path)) => Ok(path), - Ok(None) => Err(generic_error("not found")), - Err(err) => Err(err), - }; - } - } - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); - } - if invalid_segment_re.is_match(&target[2..]) { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); - } - let package_path = package_json_path.parent().unwrap(); - let resolved_path = package_path.join(&target).clean(); - if !resolved_path.starts_with(package_path) { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); - } - if subpath.is_empty() { - return Ok(resolved_path); - } - if invalid_segment_re.is_match(&subpath) { - let request = if pattern { - match_.replace('*', &subpath) - } else { - format!("{match_}{subpath}") - }; - return Err(throw_invalid_subpath( - request, - package_json_path, - internal, - referrer, - )); - } - if pattern { - let resolved_path_str = resolved_path.to_string_lossy(); - let replaced = pattern_re - .replace(&resolved_path_str, |_caps: ®ex::Captures| { - subpath.clone() - }); - return Ok(PathBuf::from(replaced.to_string())); - } - Ok(resolved_path.join(&subpath).clean()) -} - -#[allow(clippy::too_many_arguments)] -fn resolve_package_target<Fs: NodeFs>( - package_json_path: &Path, - target: Value, - subpath: String, - package_subpath: String, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - pattern: bool, - internal: bool, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<Option<PathBuf>, AnyError> { - if let Some(target) = target.as_str() { - return resolve_package_target_string::<Fs>( - target.to_string(), - subpath, - package_subpath, - package_json_path, - referrer, - referrer_kind, - pattern, - internal, - conditions, - mode, - npm_resolver, - permissions, - ) - .map(|path| { - if mode.is_types() { - path_to_declaration_path::<Fs>(path, referrer_kind) - } else { - Some(path) - } - }); - } else if let Some(target_arr) = target.as_array() { - if target_arr.is_empty() { - return Ok(None); - } - - let mut last_error = None; - for target_item in target_arr { - let resolved_result = resolve_package_target::<Fs>( - package_json_path, - target_item.to_owned(), - subpath.clone(), - package_subpath.clone(), - referrer, - referrer_kind, - pattern, - internal, - conditions, - mode, - npm_resolver, - permissions, - ); - - match resolved_result { - Ok(Some(resolved)) => return Ok(Some(resolved)), - Ok(None) => { - last_error = None; - continue; - } - Err(e) => { - let err_string = e.to_string(); - last_error = Some(e); - if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") { - continue; - } - return Err(last_error.unwrap()); - } - } - } - if last_error.is_none() { - return Ok(None); - } - return Err(last_error.unwrap()); - } else if let Some(target_obj) = target.as_object() { - for key in target_obj.keys() { - // TODO(bartlomieju): verify that keys are not numeric - // return Err(errors::err_invalid_package_config( - // to_file_path_string(package_json_url), - // Some(base.as_str().to_string()), - // Some("\"exports\" cannot contain numeric property keys.".to_string()), - // )); - - if key == "default" - || conditions.contains(&key.as_str()) - || mode.is_types() && key.as_str() == "types" - { - let condition_target = target_obj.get(key).unwrap().to_owned(); - - let resolved = resolve_package_target::<Fs>( - package_json_path, - condition_target, - subpath.clone(), - package_subpath.clone(), - referrer, - referrer_kind, - pattern, - internal, - conditions, - mode, - npm_resolver, - permissions, - )?; - match resolved { - Some(resolved) => return Ok(Some(resolved)), - None => { - continue; - } - } - } - } - } else if target.is_null() { - return Ok(None); - } - - Err(throw_invalid_package_target( - package_subpath, - target.to_string(), - package_json_path, - internal, - referrer, - )) -} - fn throw_exports_not_found( subpath: String, package_json_path: &Path, @@ -567,115 +1403,6 @@ fn throw_exports_not_found( ) } -#[allow(clippy::too_many_arguments)] -pub fn package_exports_resolve<Fs: NodeFs>( - package_json_path: &Path, - package_subpath: String, - package_exports: &Map<String, Value>, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PathBuf, AnyError> { - if package_exports.contains_key(&package_subpath) - && package_subpath.find('*').is_none() - && !package_subpath.ends_with('/') - { - let target = package_exports.get(&package_subpath).unwrap().to_owned(); - let resolved = resolve_package_target::<Fs>( - package_json_path, - target, - "".to_string(), - package_subpath.to_string(), - referrer, - referrer_kind, - false, - false, - conditions, - mode, - npm_resolver, - permissions, - )?; - if resolved.is_none() { - return Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, - )); - } - return Ok(resolved.unwrap()); - } - - let mut best_match = ""; - let mut best_match_subpath = None; - for key in package_exports.keys() { - let pattern_index = key.find('*'); - if let Some(pattern_index) = pattern_index { - let key_sub = &key[0..pattern_index]; - if package_subpath.starts_with(key_sub) { - // When this reaches EOL, this can throw at the top of the whole function: - // - // if (StringPrototypeEndsWith(packageSubpath, '/')) - // throwInvalidSubpath(packageSubpath) - // - // To match "imports" and the spec. - if package_subpath.ends_with('/') { - // TODO(bartlomieju): - // emitTrailingSlashPatternDeprecation(); - } - let pattern_trailer = &key[pattern_index + 1..]; - if package_subpath.len() > key.len() - && package_subpath.ends_with(&pattern_trailer) - && pattern_key_compare(best_match, key) == 1 - && key.rfind('*') == Some(pattern_index) - { - best_match = key; - best_match_subpath = Some( - package_subpath - [pattern_index..(package_subpath.len() - pattern_trailer.len())] - .to_string(), - ); - } - } - } - } - - if !best_match.is_empty() { - let target = package_exports.get(best_match).unwrap().to_owned(); - let maybe_resolved = resolve_package_target::<Fs>( - package_json_path, - target, - best_match_subpath.unwrap(), - best_match.to_string(), - referrer, - referrer_kind, - true, - false, - conditions, - mode, - npm_resolver, - permissions, - )?; - if let Some(resolved) = maybe_resolved { - return Ok(resolved); - } else { - return Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, - )); - } - } - - Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, - )) -} - fn parse_package_name( specifier: &str, referrer: &ModuleSpecifier, @@ -727,230 +1454,154 @@ fn parse_package_name( Ok((package_name, package_subpath, is_scoped)) } -pub fn package_resolve<Fs: NodeFs>( - specifier: &str, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<Option<PathBuf>, AnyError> { - let (package_name, package_subpath, _is_scoped) = - parse_package_name(specifier, referrer)?; - - // ResolveSelf - let package_config = - get_package_scope_config::<Fs>(referrer, npm_resolver, permissions)?; - if package_config.exists - && package_config.name.as_ref() == Some(&package_name) - { - if let Some(exports) = &package_config.exports { - return package_exports_resolve::<Fs>( - &package_config.path, - package_subpath, - exports, - referrer, - referrer_kind, - conditions, - mode, - npm_resolver, - permissions, - ) - .map(Some); - } - } - - let package_dir_path = npm_resolver.resolve_package_folder_from_package( - &package_name, - referrer, - mode, - )?; - let package_json_path = package_dir_path.join("package.json"); - - // todo: error with this instead when can't find package - // Err(errors::err_module_not_found( - // &package_json_url - // .join(".") - // .unwrap() - // .to_file_path() - // .unwrap() - // .display() - // .to_string(), - // &to_file_path_string(referrer), - // "package", - // )) - - // Package match. - let package_json = - PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path)?; - if let Some(exports) = &package_json.exports { - return package_exports_resolve::<Fs>( - &package_json.path, - package_subpath, - exports, - referrer, - referrer_kind, - conditions, - mode, - npm_resolver, - permissions, - ) - .map(Some); - } - if package_subpath == "." { - return legacy_main_resolve::<Fs>(&package_json, referrer_kind, mode); - } - - let file_path = package_json.path.parent().unwrap().join(&package_subpath); +fn pattern_key_compare(a: &str, b: &str) -> i32 { + let a_pattern_index = a.find('*'); + let b_pattern_index = b.find('*'); - if mode.is_types() { - let maybe_declaration_path = - path_to_declaration_path::<Fs>(file_path, referrer_kind); - Ok(maybe_declaration_path) + let base_len_a = if let Some(index) = a_pattern_index { + index + 1 } else { - Ok(Some(file_path)) - } -} - -pub fn get_package_scope_config<Fs: NodeFs>( - referrer: &ModuleSpecifier, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PackageJson, AnyError> { - let root_folder = npm_resolver - .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?; - let package_json_path = root_folder.join("package.json"); - PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path) -} - -pub fn get_closest_package_json<Fs: NodeFs>( - url: &ModuleSpecifier, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PackageJson, AnyError> { - let package_json_path = - get_closest_package_json_path::<Fs>(url, npm_resolver)?; - PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path) -} + a.len() + }; + let base_len_b = if let Some(index) = b_pattern_index { + index + 1 + } else { + b.len() + }; -fn get_closest_package_json_path<Fs: NodeFs>( - url: &ModuleSpecifier, - npm_resolver: &dyn NpmResolver, -) -> Result<PathBuf, AnyError> { - let file_path = url.to_file_path().unwrap(); - let mut current_dir = file_path.parent().unwrap(); - let package_json_path = current_dir.join("package.json"); - if Fs::exists(&package_json_path) { - return Ok(package_json_path); - } - let root_pkg_folder = npm_resolver - .resolve_package_folder_from_path(&url.to_file_path().unwrap())?; - while current_dir.starts_with(&root_pkg_folder) { - current_dir = current_dir.parent().unwrap(); - let package_json_path = current_dir.join("package.json"); - if Fs::exists(&package_json_path) { - return Ok(package_json_path); - } + if base_len_a > base_len_b { + return -1; } - bail!("did not find package.json in {}", root_pkg_folder.display()) -} + if base_len_b > base_len_a { + return 1; + } -pub fn legacy_main_resolve<Fs: NodeFs>( - package_json: &PackageJson, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, -) -> Result<Option<PathBuf>, AnyError> { - let maybe_main = if mode.is_types() { - match package_json.types.as_ref() { - Some(types) => Some(types), - None => { - // fallback to checking the main entrypoint for - // a corresponding declaration file - if let Some(main) = package_json.main(referrer_kind) { - let main = package_json.path.parent().unwrap().join(main).clean(); - if let Some(path) = - path_to_declaration_path::<Fs>(main, referrer_kind) - { - return Ok(Some(path)); - } - } - None - } - } - } else { - package_json.main(referrer_kind) - }; + if a_pattern_index.is_none() { + return 1; + } - if let Some(main) = maybe_main { - let guess = package_json.path.parent().unwrap().join(main).clean(); - if Fs::is_file(&guess) { - return Ok(Some(guess)); - } + if b_pattern_index.is_none() { + return -1; + } - // todo(dsherret): investigate exactly how node and typescript handles this - let endings = if mode.is_types() { - match referrer_kind { - NodeModuleKind::Cjs => { - vec![".d.ts", ".d.cts", "/index.d.ts", "/index.d.cts"] - } - NodeModuleKind::Esm => vec![ - ".d.ts", - ".d.mts", - "/index.d.ts", - "/index.d.mts", - ".d.cts", - "/index.d.cts", - ], - } - } else { - vec![".js", "/index.js"] - }; - for ending in endings { - let guess = package_json - .path - .parent() - .unwrap() - .join(format!("{main}{ending}")) - .clean(); - if Fs::is_file(&guess) { - // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(Some(guess)); - } - } + if a.len() > b.len() { + return -1; } - let index_file_names = if mode.is_types() { - // todo(dsherret): investigate exactly how typescript does this - match referrer_kind { - NodeModuleKind::Cjs => vec!["index.d.ts", "index.d.cts"], - NodeModuleKind::Esm => vec!["index.d.ts", "index.d.mts", "index.d.cts"], - } - } else { - vec!["index.js"] - }; - for index_file_name in index_file_names { - let guess = package_json - .path - .parent() - .unwrap() - .join(index_file_name) - .clean(); - if Fs::is_file(&guess) { - // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(Some(guess)); - } + if b.len() > a.len() { + return 1; } - Ok(None) + 0 } #[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" + ); + } + + #[test] fn test_parse_package_name() { let dummy_referrer = Url::parse("http://example.com").unwrap(); |