diff options
Diffstat (limited to 'resolvers/node/resolution.rs')
-rw-r--r-- | resolvers/node/resolution.rs | 2023 |
1 files changed, 2023 insertions, 0 deletions
diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs new file mode 100644 index 000000000..ad9dbb710 --- /dev/null +++ b/resolvers/node/resolution.rs @@ -0,0 +1,2023 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::bail; +use anyhow::Error as AnyError; +use deno_media_type::MediaType; +use deno_package_json::PackageJsonRc; +use serde_json::Map; +use serde_json::Value; +use url::Url; + +use crate::env::NodeResolverEnv; +use crate::errors; +use crate::errors::CanonicalizingPkgJsonDirError; +use crate::errors::ClosestPkgJsonError; +use crate::errors::DataUrlReferrerError; +use crate::errors::FinalizeResolutionError; +use crate::errors::InvalidModuleSpecifierError; +use crate::errors::InvalidPackageTargetError; +use crate::errors::LegacyResolveError; +use crate::errors::ModuleNotFoundError; +use crate::errors::NodeJsErrorCode; +use crate::errors::NodeJsErrorCoded; +use crate::errors::NodeResolveError; +use crate::errors::NodeResolveRelativeJoinError; +use crate::errors::PackageExportsResolveError; +use crate::errors::PackageImportNotDefinedError; +use crate::errors::PackageImportsResolveError; +use crate::errors::PackageImportsResolveErrorKind; +use crate::errors::PackageJsonLoadError; +use crate::errors::PackagePathNotExportedError; +use crate::errors::PackageResolveError; +use crate::errors::PackageSubpathResolveError; +use crate::errors::PackageSubpathResolveErrorKind; +use crate::errors::PackageTargetNotFoundError; +use crate::errors::PackageTargetResolveError; +use crate::errors::PackageTargetResolveErrorKind; +use crate::errors::ResolveBinaryCommandsError; +use crate::errors::ResolvePkgJsonBinExportError; +use crate::errors::ResolvePkgSubpathFromDenoModuleError; +use crate::errors::TypeScriptNotSupportedInNpmError; +use crate::errors::TypesNotFoundError; +use crate::errors::TypesNotFoundErrorData; +use crate::errors::UnsupportedDirImportError; +use crate::errors::UnsupportedEsmUrlSchemeError; +use crate::errors::UrlToNodeResolutionError; +use crate::path::strip_unc_prefix; +use crate::path::to_file_specifier; +use crate::NpmResolverRc; +use crate::PathClean; +use deno_package_json::PackageJson; + +pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; +pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; +static TYPES_ONLY_CONDITIONS: &[&str] = &["types"]; + +pub type NodeModuleKind = deno_package_json::NodeModuleKind; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NodeResolutionMode { + Execution, + Types, +} + +impl NodeResolutionMode { + pub fn is_types(&self) -> bool { + matches!(self, NodeResolutionMode::Types) + } +} + +#[derive(Debug)] +pub enum NodeResolution { + Esm(Url), + CommonJs(Url), + BuiltIn(String), +} + +impl NodeResolution { + pub fn into_url(self) -> Url { + match self { + Self::Esm(u) => u, + Self::CommonJs(u) => u, + Self::BuiltIn(specifier) => { + if specifier.starts_with("node:") { + Url::parse(&specifier).unwrap() + } else { + Url::parse(&format!("node:{specifier}")).unwrap() + } + } + } + } + + pub fn into_specifier_and_media_type( + resolution: Option<Self>, + ) -> (Url, 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 => ( + Url::parse("internal:///missing_dependency.d.ts").unwrap(), + MediaType::Dts, + ), + } + } +} + +#[allow(clippy::disallowed_types)] +pub type NodeResolverRc<TEnv> = crate::sync::MaybeArc<NodeResolver<TEnv>>; + +#[derive(Debug)] +pub struct NodeResolver<TEnv: NodeResolverEnv> { + env: TEnv, + npm_resolver: NpmResolverRc, +} + +impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { + pub fn new(env: TEnv, npm_resolver: NpmResolverRc) -> Self { + Self { env, npm_resolver } + } + + pub fn in_npm_package(&self, specifier: &Url) -> 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: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<NodeResolution, NodeResolveError> { + // 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 self.env.is_builtin_node_module(specifier) { + return Ok(NodeResolution::BuiltIn(specifier.to_string())); + } + + if let Ok(url) = Url::parse(specifier) { + if url.scheme() == "data" { + return Ok(NodeResolution::Esm(url)); + } + + if let Some(module_name) = + get_module_name_from_builtin_node_module_specifier(&url) + { + return Ok(NodeResolution::BuiltIn(module_name.to_string())); + } + + let protocol = url.scheme(); + + if protocol != "file" && protocol != "data" { + return Err( + UnsupportedEsmUrlSchemeError { + url_scheme: protocol.to_string(), + } + .into(), + ); + } + + // todo(dsherret): this seems wrong + if referrer.scheme() == "data" { + let url = referrer + .join(specifier) + .map_err(|source| DataUrlReferrerError { source })?; + return Ok(NodeResolution::Esm(url)); + } + } + + let url = self.module_resolve( + specifier, + referrer, + referrer_kind, + // even though the referrer may be CJS, if we're here that means we're doing ESM resolution + DEFAULT_CONDITIONS, + mode, + )?; + + let url = if mode.is_types() { + let file_path = to_file_path(&url); + self.path_to_declaration_url(&file_path, Some(referrer), referrer_kind)? + } else { + url + }; + + let url = self.finalize_resolution(url, Some(referrer))?; + let resolve_response = self.url_to_node_resolution(url)?; + // TODO(bartlomieju): skipped checking errors for commonJS resolution and + // "preserveSymlinksMain"/"preserveSymlinks" options. + Ok(resolve_response) + } + + fn module_resolve( + &self, + specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, NodeResolveError> { + if should_be_treated_as_relative_or_absolute_path(specifier) { + Ok(referrer.join(specifier).map_err(|err| { + NodeResolveRelativeJoinError { + path: specifier.to_string(), + base: referrer.clone(), + source: err, + } + })?) + } else if specifier.starts_with('#') { + let pkg_config = self + .get_closest_package_json(referrer) + .map_err(PackageImportsResolveErrorKind::ClosestPkgJson) + .map_err(|err| PackageImportsResolveError(Box::new(err)))?; + Ok(self.package_imports_resolve( + specifier, + Some(referrer), + referrer_kind, + pkg_config.as_deref(), + conditions, + mode, + )?) + } else if let Ok(resolved) = Url::parse(specifier) { + Ok(resolved) + } else { + Ok(self.package_resolve( + specifier, + referrer, + referrer_kind, + conditions, + mode, + )?) + } + } + + fn finalize_resolution( + &self, + resolved: Url, + maybe_referrer: Option<&Url>, + ) -> Result<Url, FinalizeResolutionError> { + let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C"); + + if encoded_sep_re.is_match(resolved.path()) { + return Err( + errors::InvalidModuleSpecifierError { + request: resolved.to_string(), + reason: Cow::Borrowed( + "must not include encoded \"/\" or \"\\\\\" characters", + ), + maybe_referrer: maybe_referrer.map(to_file_path_string), + } + .into(), + ); + } + + if resolved.scheme() == "node" { + return Ok(resolved); + } + + 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.env.stat_sync(Path::new(&p)) + { + (stats.is_dir, stats.is_file) + } else { + (false, false) + }; + if is_dir { + return Err( + UnsupportedDirImportError { + dir_url: resolved.clone(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ); + } else if !is_file { + return Err( + ModuleNotFoundError { + specifier: resolved, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + typ: "module", + } + .into(), + ); + } + + Ok(resolved) + } + + pub fn resolve_package_subpath_from_deno_module( + &self, + package_dir: &Path, + package_subpath: Option<&str>, + maybe_referrer: Option<&Url>, + mode: NodeResolutionMode, + ) -> Result<NodeResolution, ResolvePkgSubpathFromDenoModuleError> { + let node_module_kind = NodeModuleKind::Esm; + let package_subpath = package_subpath + .map(|s| format!("./{s}")) + .unwrap_or_else(|| ".".to_string()); + let resolved_url = self.resolve_package_dir_subpath( + package_dir, + &package_subpath, + maybe_referrer, + node_module_kind, + DEFAULT_CONDITIONS, + mode, + )?; + let resolve_response = self.url_to_node_resolution(resolved_url)?; + // TODO(bartlomieju): skipped checking errors for commonJS resolution and + // "preserveSymlinksMain"/"preserveSymlinks" options. + Ok(resolve_response) + } + + pub fn resolve_binary_commands( + &self, + package_folder: &Path, + ) -> Result<Vec<String>, ResolveBinaryCommandsError> { + let pkg_json_path = package_folder.join("package.json"); + let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + return Ok(Vec::new()); + }; + + Ok(match &package_json.bin { + Some(Value::String(_)) => { + let Some(name) = &package_json.name else { + return Err(ResolveBinaryCommandsError::MissingPkgJsonName { + pkg_json_path, + }); + }; + let name = name.split("/").last().unwrap(); + vec![name.to_string()] + } + Some(Value::Object(o)) => { + o.iter().map(|(key, _)| key.clone()).collect::<Vec<_>>() + } + _ => Vec::new(), + }) + } + + pub fn resolve_binary_export( + &self, + package_folder: &Path, + sub_path: Option<&str>, + ) -> Result<NodeResolution, ResolvePkgJsonBinExportError> { + let pkg_json_path = package_folder.join("package.json"); + let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + return Err(ResolvePkgJsonBinExportError::MissingPkgJson { + pkg_json_path, + }); + }; + let bin_entry = + resolve_bin_entry_value(&package_json, sub_path).map_err(|err| { + ResolvePkgJsonBinExportError::InvalidBinProperty { + message: err.to_string(), + } + })?; + let url = to_file_specifier(&package_folder.join(bin_entry)); + + 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: Url, + ) -> Result<NodeResolution, UrlToNodeResolutionError> { + let url_str = url.as_str().to_lowercase(); + if url_str.starts_with("http") || url_str.ends_with(".json") { + Ok(NodeResolution::Esm(url)) + } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") { + let maybe_package_config = self.get_closest_package_json(&url)?; + match maybe_package_config { + Some(c) if c.typ == "module" => Ok(NodeResolution::Esm(url)), + Some(_) => Ok(NodeResolution::CommonJs(url)), + None => Ok(NodeResolution::Esm(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") || url_str.ends_with(".mts") { + if self.in_npm_package(&url) { + Err(TypeScriptNotSupportedInNpmError { specifier: url }.into()) + } else { + Ok(NodeResolution::Esm(url)) + } + } else { + Ok(NodeResolution::CommonJs(url)) + } + } + + /// Checks if the resolved file has a corresponding declaration file. + fn path_to_declaration_url( + &self, + path: &Path, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + ) -> Result<Url, TypesNotFoundError> { + fn probe_extensions<TEnv: NodeResolverEnv>( + fs: &TEnv, + path: &Path, + lowercase_path: &str, + referrer_kind: NodeModuleKind, + ) -> Option<PathBuf> { + let mut searched_for_d_mts = false; + let mut searched_for_d_cts = false; + if lowercase_path.ends_with(".mjs") { + let d_mts_path = with_known_extension(path, "d.mts"); + if fs.exists_sync(&d_mts_path) { + return Some(d_mts_path); + } + searched_for_d_mts = true; + } else if lowercase_path.ends_with(".cjs") { + let d_cts_path = with_known_extension(path, "d.cts"); + if fs.exists_sync(&d_cts_path) { + return Some(d_cts_path); + } + searched_for_d_cts = true; + } + + let dts_path = with_known_extension(path, "d.ts"); + if fs.exists_sync(&dts_path) { + return Some(dts_path); + } + + let specific_dts_path = match referrer_kind { + NodeModuleKind::Cjs if !searched_for_d_cts => { + Some(with_known_extension(path, "d.cts")) + } + NodeModuleKind::Esm if !searched_for_d_mts => { + Some(with_known_extension(path, "d.mts")) + } + _ => None, // already searched above + }; + if let Some(specific_dts_path) = specific_dts_path { + if fs.exists_sync(&specific_dts_path) { + return Some(specific_dts_path); + } + } + 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.mts") + { + return Ok(to_file_specifier(path)); + } + if let Some(path) = + probe_extensions(&self.env, path, &lowercase_path, referrer_kind) + { + return Ok(to_file_specifier(&path)); + } + if self.env.is_dir_sync(path) { + let resolution_result = self.resolve_package_dir_subpath( + path, + /* sub path */ ".", + maybe_referrer, + referrer_kind, + match referrer_kind { + NodeModuleKind::Esm => DEFAULT_CONDITIONS, + NodeModuleKind::Cjs => REQUIRE_CONDITIONS, + }, + NodeResolutionMode::Types, + ); + if let Ok(resolution) = resolution_result { + return Ok(resolution); + } + let index_path = path.join("index.js"); + if let Some(path) = probe_extensions( + &self.env, + &index_path, + &index_path.to_string_lossy().to_lowercase(), + referrer_kind, + ) { + return Ok(to_file_specifier(&path)); + } + } + // allow resolving .css files for types resolution + if lowercase_path.ends_with(".css") { + return Ok(to_file_specifier(path)); + } + Err(TypesNotFoundError(Box::new(TypesNotFoundErrorData { + code_specifier: to_file_specifier(path), + maybe_referrer: maybe_referrer.cloned(), + }))) + } + + #[allow(clippy::too_many_arguments)] + pub fn package_imports_resolve( + &self, + name: &str, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + referrer_pkg_json: Option<&PackageJson>, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageImportsResolveError> { + if name == "#" || name.starts_with("#/") || name.ends_with('/') { + let reason = "is not a valid internal imports specifier name"; + return Err( + errors::InvalidModuleSpecifierError { + request: name.to_string(), + reason: Cow::Borrowed(reason), + maybe_referrer: maybe_referrer.map(to_specifier_display_string), + } + .into(), + ); + } + + let mut package_json_path = None; + if let Some(pkg_json) = &referrer_pkg_json { + package_json_path = Some(pkg_json.path.clone()); + if let Some(imports) = &pkg_json.imports { + if imports.contains_key(name) && !name.contains('*') { + let target = imports.get(name).unwrap(); + let maybe_resolved = self.resolve_package_target( + package_json_path.as_ref().unwrap(), + target, + "", + name, + maybe_referrer, + referrer_kind, + false, + true, + conditions, + mode, + )?; + 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())], + ); + } + } + } + } + + if !best_match.is_empty() { + let target = imports.get(best_match).unwrap(); + let maybe_resolved = self.resolve_package_target( + package_json_path.as_ref().unwrap(), + target, + best_match_subpath.unwrap(), + best_match, + maybe_referrer, + referrer_kind, + true, + true, + conditions, + mode, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } + } + } + } + + Err( + PackageImportNotDefinedError { + name: name.to_string(), + package_json_path, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_target_string( + &self, + target: &str, + subpath: &str, + match_: &str, + package_json_path: &Path, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + pattern: bool, + internal: bool, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageTargetResolveError> { + if !subpath.is_empty() && !pattern && !target.ends_with('/') { + return Err( + InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ); + } + 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 target_url = Url::parse(target); + match target_url { + Ok(url) => { + if get_module_name_from_builtin_node_module_specifier(&url) + .is_some() + { + return Ok(url); + } + } + Err(_) => { + let export_target = if pattern { + pattern_re + .replace(target, |_caps: ®ex::Captures| subpath) + .to_string() + } else { + format!("{target}{subpath}") + }; + let package_json_url = to_file_specifier(package_json_path); + let result = match self.package_resolve( + &export_target, + &package_json_url, + referrer_kind, + conditions, + mode, + ) { + Ok(url) => Ok(url), + Err(err) => match err.code() { + NodeJsErrorCode::ERR_INVALID_MODULE_SPECIFIER + | NodeJsErrorCode::ERR_INVALID_PACKAGE_CONFIG + | NodeJsErrorCode::ERR_INVALID_PACKAGE_TARGET + | NodeJsErrorCode::ERR_PACKAGE_IMPORT_NOT_DEFINED + | NodeJsErrorCode::ERR_PACKAGE_PATH_NOT_EXPORTED + | NodeJsErrorCode::ERR_UNKNOWN_FILE_EXTENSION + | NodeJsErrorCode::ERR_UNSUPPORTED_DIR_IMPORT + | NodeJsErrorCode::ERR_UNSUPPORTED_ESM_URL_SCHEME + | NodeJsErrorCode::ERR_TYPES_NOT_FOUND => { + Err(PackageTargetResolveErrorKind::PackageResolve(err).into()) + } + NodeJsErrorCode::ERR_MODULE_NOT_FOUND => Err( + PackageTargetResolveErrorKind::NotFound( + PackageTargetNotFoundError { + pkg_json_path: package_json_path.to_path_buf(), + target: export_target.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + referrer_kind, + mode, + }, + ) + .into(), + ), + }, + }; + + return match result { + Ok(url) => Ok(url), + Err(err) => { + if self.env.is_builtin_node_module(target) { + Ok(Url::parse(&format!("node:{}", target)).unwrap()) + } else { + Err(err) + } + } + }; + } + } + } + return Err( + InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ); + } + if invalid_segment_re.is_match(&target[2..]) { + return Err( + InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ); + } + 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( + InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ); + } + if subpath.is_empty() { + return Ok(to_file_specifier(&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, + maybe_referrer, + ) + .into(), + ); + } + if pattern { + let resolved_path_str = resolved_path.to_string_lossy(); + let replaced = pattern_re + .replace(&resolved_path_str, |_caps: ®ex::Captures| subpath); + return Ok(to_file_specifier(&PathBuf::from(replaced.to_string()))); + } + Ok(to_file_specifier(&resolved_path.join(subpath).clean())) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_target( + &self, + package_json_path: &Path, + target: &Value, + subpath: &str, + package_subpath: &str, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + pattern: bool, + internal: bool, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Option<Url>, PackageTargetResolveError> { + let result = self.resolve_package_target_inner( + package_json_path, + target, + subpath, + package_subpath, + maybe_referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + ); + match result { + Ok(maybe_resolved) => Ok(maybe_resolved), + Err(err) => { + if mode.is_types() + && err.code() == NodeJsErrorCode::ERR_TYPES_NOT_FOUND + && conditions != TYPES_ONLY_CONDITIONS + { + // try resolving with just "types" conditions for when someone misconfigures + // and puts the "types" condition in the wrong place + if let Ok(Some(resolved)) = self.resolve_package_target_inner( + package_json_path, + target, + subpath, + package_subpath, + maybe_referrer, + referrer_kind, + pattern, + internal, + TYPES_ONLY_CONDITIONS, + mode, + ) { + return Ok(Some(resolved)); + } + } + + Err(err) + } + } + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_target_inner( + &self, + package_json_path: &Path, + target: &Value, + subpath: &str, + package_subpath: &str, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + pattern: bool, + internal: bool, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Option<Url>, PackageTargetResolveError> { + if let Some(target) = target.as_str() { + let url = self.resolve_package_target_string( + target, + subpath, + package_subpath, + package_json_path, + maybe_referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + )?; + if mode.is_types() && url.scheme() == "file" { + let path = url.to_file_path().unwrap(); + return Ok(Some(self.path_to_declaration_url( + &path, + maybe_referrer, + referrer_kind, + )?)); + } else { + return Ok(Some(url)); + } + } 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, + subpath, + package_subpath, + maybe_referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + ); + + match resolved_result { + Ok(Some(resolved)) => return Ok(Some(resolved)), + Ok(None) => { + last_error = None; + continue; + } + Err(e) => { + if e.code() == NodeJsErrorCode::ERR_INVALID_PACKAGE_TARGET { + last_error = Some(e); + continue; + } else { + return Err(e); + } + } + } + } + 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(); + + let resolved = self.resolve_package_target( + package_json_path, + condition_target, + subpath, + package_subpath, + maybe_referrer, + referrer_kind, + pattern, + internal, + conditions, + mode, + )?; + match resolved { + Some(resolved) => return Ok(Some(resolved)), + None => { + continue; + } + } + } + } + } else if target.is_null() { + return Ok(None); + } + + Err( + InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: package_subpath.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into(), + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn package_exports_resolve( + &self, + package_json_path: &Path, + package_subpath: &str, + package_exports: &Map<String, Value>, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageExportsResolveError> { + if package_exports.contains_key(package_subpath) + && package_subpath.find('*').is_none() + && !package_subpath.ends_with('/') + { + let target = package_exports.get(package_subpath).unwrap(); + let resolved = self.resolve_package_target( + package_json_path, + target, + "", + package_subpath, + maybe_referrer, + referrer_kind, + false, + false, + conditions, + mode, + )?; + return match resolved { + Some(resolved) => Ok(resolved), + None => Err( + PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + mode, + } + .into(), + ), + }; + } + + 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(); + let maybe_resolved = self.resolve_package_target( + package_json_path, + target, + &best_match_subpath.unwrap(), + best_match, + maybe_referrer, + referrer_kind, + true, + false, + conditions, + mode, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } else { + return Err( + PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + mode, + } + .into(), + ); + } + } + + Err( + PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + mode, + } + .into(), + ) + } + + pub(super) fn package_resolve( + &self, + specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageResolveError> { + let (package_name, package_subpath, _is_scoped) = + parse_npm_pkg_name(specifier, referrer)?; + + if let Some(package_config) = self.get_closest_package_json(referrer)? { + // ResolveSelf + if 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, + Some(referrer), + referrer_kind, + conditions, + mode, + ) + .map_err(|err| err.into()); + } + } + } + + self.resolve_package_subpath_for_package( + &package_name, + &package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_subpath_for_package( + &self, + package_name: &str, + package_subpath: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageResolveError> { + let result = self.resolve_package_subpath_for_package_inner( + package_name, + package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ); + if mode.is_types() && !matches!(result, Ok(Url { .. })) { + // try to resolve with the @types package + let package_name = types_package_name(package_name); + if let Ok(result) = self.resolve_package_subpath_for_package_inner( + &package_name, + package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ) { + return Ok(result); + } + } + result + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_subpath_for_package_inner( + &self, + package_name: &str, + package_subpath: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageResolveError> { + let package_dir_path = self + .npm_resolver + .resolve_package_folder_from_package(package_name, referrer)?; + + // 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. + self + .resolve_package_dir_subpath( + &package_dir_path, + package_subpath, + Some(referrer), + referrer_kind, + conditions, + mode, + ) + .map_err(|err| err.into()) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_dir_subpath( + &self, + package_dir_path: &Path, + package_subpath: &str, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageSubpathResolveError> { + let package_json_path = package_dir_path.join("package.json"); + match self.load_package_json(&package_json_path)? { + Some(pkg_json) => self.resolve_package_subpath( + &pkg_json, + package_subpath, + maybe_referrer, + referrer_kind, + conditions, + mode, + ), + None => self + .resolve_package_subpath_no_pkg_json( + package_dir_path, + package_subpath, + maybe_referrer, + referrer_kind, + mode, + ) + .map_err(|err| { + PackageSubpathResolveErrorKind::LegacyResolve(err).into() + }), + } + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_subpath( + &self, + package_json: &PackageJson, + package_subpath: &str, + referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result<Url, PackageSubpathResolveError> { + if let Some(exports) = &package_json.exports { + let result = self.package_exports_resolve( + &package_json.path, + package_subpath, + exports, + referrer, + referrer_kind, + conditions, + mode, + ); + match result { + Ok(found) => return Ok(found), + Err(exports_err) => { + if mode.is_types() && package_subpath == "." { + return self + .legacy_main_resolve(package_json, referrer, referrer_kind, mode) + .map_err(|err| { + PackageSubpathResolveErrorKind::LegacyResolve(err).into() + }); + } + return Err( + PackageSubpathResolveErrorKind::Exports(exports_err).into(), + ); + } + } + } + + if package_subpath == "." { + return self + .legacy_main_resolve(package_json, referrer, referrer_kind, mode) + .map_err(|err| { + PackageSubpathResolveErrorKind::LegacyResolve(err).into() + }); + } + + self + .resolve_subpath_exact( + package_json.path.parent().unwrap(), + package_subpath, + referrer, + referrer_kind, + mode, + ) + .map_err(|err| { + PackageSubpathResolveErrorKind::LegacyResolve(err.into()).into() + }) + } + + fn resolve_subpath_exact( + &self, + directory: &Path, + package_subpath: &str, + referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, TypesNotFoundError> { + assert_ne!(package_subpath, "."); + let file_path = directory.join(package_subpath); + if mode.is_types() { + Ok(self.path_to_declaration_url(&file_path, referrer, referrer_kind)?) + } else { + Ok(to_file_specifier(&file_path)) + } + } + + fn resolve_package_subpath_no_pkg_json( + &self, + directory: &Path, + package_subpath: &str, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, LegacyResolveError> { + if package_subpath == "." { + self.legacy_index_resolve(directory, maybe_referrer, referrer_kind, mode) + } else { + self + .resolve_subpath_exact( + directory, + package_subpath, + maybe_referrer, + referrer_kind, + mode, + ) + .map_err(|err| err.into()) + } + } + + pub fn get_closest_package_json( + &self, + url: &Url, + ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { + let Ok(file_path) = url.to_file_path() else { + return Ok(None); + }; + self.get_closest_package_json_from_path(&file_path) + } + + pub fn get_closest_package_json_from_path( + &self, + file_path: &Path, + ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { + // we use this for deno compile using byonm because the script paths + // won't be in virtual file system, but the package.json paths will be + fn canonicalize_first_ancestor_exists( + dir_path: &Path, + env: &dyn NodeResolverEnv, + ) -> Result<Option<PathBuf>, std::io::Error> { + for ancestor in dir_path.ancestors() { + match env.realpath_sync(ancestor) { + Ok(dir_path) => return Ok(Some(dir_path)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // keep searching + } + Err(err) => return Err(err), + } + } + Ok(None) + } + + let parent_dir = file_path.parent().unwrap(); + let Some(start_dir) = canonicalize_first_ancestor_exists( + parent_dir, &self.env, + ) + .map_err(|source| CanonicalizingPkgJsonDirError { + dir_path: parent_dir.to_path_buf(), + source, + })? + else { + return Ok(None); + }; + let start_dir = strip_unc_prefix(start_dir); + for current_dir in start_dir.ancestors() { + let package_json_path = current_dir.join("package.json"); + if let Some(pkg_json) = self.load_package_json(&package_json_path)? { + return Ok(Some(pkg_json)); + } + } + + Ok(None) + } + + pub fn load_package_json( + &self, + package_json_path: &Path, + ) -> Result<Option<PackageJsonRc>, PackageJsonLoadError> { + crate::package_json::load_pkg_json( + self.env.pkg_json_fs(), + package_json_path, + ) + } + + pub(super) fn legacy_main_resolve( + &self, + package_json: &PackageJson, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, LegacyResolveError> { + let maybe_main = if mode.is_types() { + match package_json.types.as_ref() { + Some(types) => Some(types.as_str()), + 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(); + let decl_url_result = self.path_to_declaration_url( + &main, + maybe_referrer, + referrer_kind, + ); + // don't surface errors, fallback to checking the index now + if let Ok(url) = decl_url_result { + return Ok(url); + } + } + None + } + } + } else { + package_json.main(referrer_kind) + }; + + if let Some(main) = maybe_main { + let guess = package_json.path.parent().unwrap().join(main).clean(); + if self.env.is_file_sync(&guess) { + return Ok(to_file_specifier(&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.env.is_file_sync(&guess) { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(to_file_specifier(&guess)); + } + } + } + + self.legacy_index_resolve( + package_json.path.parent().unwrap(), + maybe_referrer, + referrer_kind, + mode, + ) + } + + fn legacy_index_resolve( + &self, + directory: &Path, + maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, LegacyResolveError> { + 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 = directory.join(index_file_name).clean(); + if self.env.is_file_sync(&guess) { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(to_file_specifier(&guess)); + } + } + + if mode.is_types() { + Err( + TypesNotFoundError(Box::new(TypesNotFoundErrorData { + code_specifier: to_file_specifier(&directory.join("index.js")), + maybe_referrer: maybe_referrer.cloned(), + })) + .into(), + ) + } else { + Err( + ModuleNotFoundError { + specifier: to_file_specifier(&directory.join("index.js")), + typ: "module", + maybe_referrer: maybe_referrer.cloned(), + } + .into(), + ) + } + } +} + +fn resolve_bin_entry_value<'a>( + package_json: &'a PackageJson, + bin_name: Option<&str>, +) -> Result<&'a str, AnyError> { + let bin = match &package_json.bin { + Some(bin) => bin, + None => bail!( + "'{}' did not have a bin property", + package_json.path.display(), + ), + }; + let bin_entry = match bin { + Value::String(_) => { + if bin_name.is_some() + && bin_name + != package_json + .name + .as_deref() + .map(|name| name.rsplit_once('/').map_or(name, |(_, name)| 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 { + package_json.name.as_ref().and_then(|n| o.get(n)) + } + } + _ => bail!( + "'{}' did not have a bin property with a string or object value", + package_json.path.display() + ), + }; + let bin_entry = match bin_entry { + Some(e) => e, + None => { + let prefix = package_json + .name + .as_ref() + .map(|n| { + let mut prefix = format!("npm:{}", n); + if let Some(version) = &package_json.version { + prefix.push('@'); + prefix.push_str(version); + } + prefix.push('/'); + prefix + }) + .unwrap_or_default(); + let keys = bin + .as_object() + .map(|o| { + o.keys() + .map(|k| format!(" * {prefix}{k}")) + .collect::<Vec<_>>() + }) + .unwrap_or_default(); + bail!( + "'{}' did not have a bin entry{}{}", + package_json.path.display(), + bin_name + .or(package_json.name.as_deref()) + .map(|name| format!(" for '{}'", name)) + .unwrap_or_default(), + if keys.is_empty() { + "".to_string() + } else { + format!("\n\nPossibilities:\n{}", keys.join("\n")) + } + ) + } + }; + match bin_entry { + Value::String(s) => Ok(s), + _ => bail!( + "'{}' had a non-string sub property of bin", + package_json.path.display(), + ), + } +} + +fn to_file_path(url: &Url) -> PathBuf { + url + .to_file_path() + .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}")) +} + +fn to_file_path_string(url: &Url) -> 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().take(3).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 +} + +/// Alternate `PathBuf::with_extension` that will handle known extensions +/// more intelligently. +fn with_known_extension(path: &Path, ext: &str) -> PathBuf { + const NON_DECL_EXTS: &[&str] = &[ + "cjs", "js", "json", "jsx", "mjs", "tsx", /* ex. types.d */ "d", + ]; + const DECL_EXTS: &[&str] = &["cts", "mts", "ts"]; + + let file_name = match path.file_name() { + Some(value) => value.to_string_lossy(), + None => return path.to_path_buf(), + }; + let lowercase_file_name = file_name.to_lowercase(); + let period_index = lowercase_file_name.rfind('.').and_then(|period_index| { + let ext = &lowercase_file_name[period_index + 1..]; + if DECL_EXTS.contains(&ext) { + if let Some(next_period_index) = + lowercase_file_name[..period_index].rfind('.') + { + if &lowercase_file_name[next_period_index + 1..period_index] == "d" { + Some(next_period_index) + } else { + Some(period_index) + } + } else { + Some(period_index) + } + } else if NON_DECL_EXTS.contains(&ext) { + Some(period_index) + } else { + None + } + }); + + let file_name = match period_index { + Some(period_index) => &file_name[..period_index], + None => &file_name, + }; + path.with_file_name(format!("{file_name}.{ext}")) +} + +fn to_specifier_display_string(url: &Url) -> String { + if let Ok(path) = url.to_file_path() { + path.display().to_string() + } else { + url.to_string() + } +} + +fn throw_invalid_subpath( + subpath: String, + package_json_path: &Path, + internal: bool, + maybe_referrer: Option<&Url>, +) -> InvalidModuleSpecifierError { + let ie = if internal { "imports" } else { "exports" }; + let reason = format!( + "request is not a valid subpath for the \"{}\" resolution of {}", + ie, + package_json_path.display(), + ); + InvalidModuleSpecifierError { + request: subpath, + reason: Cow::Owned(reason), + maybe_referrer: maybe_referrer.map(to_specifier_display_string), + } +} + +pub fn parse_npm_pkg_name( + specifier: &str, + referrer: &Url, +) -> Result<(String, String, bool), InvalidModuleSpecifierError> { + let mut separator_index = specifier.find('/'); + let mut valid_package_name = true; + let mut is_scoped = false; + if specifier.is_empty() { + valid_package_name = false; + } else if specifier.starts_with('@') { + is_scoped = true; + if let Some(index) = separator_index { + separator_index = specifier[index + 1..] + .find('/') + .map(|new_index| index + 1 + new_index); + } else { + valid_package_name = false; + } + } + + let package_name = if let Some(index) = separator_index { + specifier[0..index].to_string() + } else { + specifier.to_string() + }; + + // Package name cannot have leading . and cannot have percent-encoding or separators. + for ch in package_name.chars() { + if ch == '%' || ch == '\\' { + valid_package_name = false; + break; + } + } + + if !valid_package_name { + return Err(errors::InvalidModuleSpecifierError { + request: specifier.to_string(), + reason: Cow::Borrowed("is not a valid package name"), + maybe_referrer: Some(to_specifier_display_string(referrer)), + }); + } + + let package_subpath = if let Some(index) = separator_index { + format!(".{}", specifier.chars().skip(index).collect::<String>()) + } else { + ".".to_string() + }; + + Ok((package_name, package_subpath, is_scoped)) +} + +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 +} + +/// Gets the corresponding @types package for the provided package name. +fn types_package_name(package_name: &str) -> String { + debug_assert!(!package_name.starts_with("@types/")); + // Scoped packages will get two underscores for each slash + // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/15f1ece08f7b498f4b9a2147c2a46e94416ca777#what-about-scoped-packages + format!("@types/{}", package_name.replace('/', "__")) +} + +/// Ex. returns `fs` for `node:fs` +fn get_module_name_from_builtin_node_module_specifier( + specifier: &Url, +) -> Option<&str> { + if specifier.scheme() != "node" { + return None; + } + + let (_, specifier) = specifier.as_str().split_once(':')?; + Some(specifier) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + fn build_package_json(json: Value) -> PackageJson { + PackageJson::load_from_value(PathBuf::from("/package.json"), json) + } + + #[test] + fn test_resolve_bin_entry_value() { + // should resolve the specified value + let pkg_json = build_package_json(json!({ + "name": "pkg", + "version": "1.1.1", + "bin": { + "bin1": "./value1", + "bin2": "./value2", + "pkg": "./value3", + } + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, Some("bin1")).unwrap(), + "./value1" + ); + + // should resolve the value with the same name when not specified + assert_eq!( + resolve_bin_entry_value(&pkg_json, None).unwrap(), + "./value3" + ); + + // should not resolve when specified value does not exist + assert_eq!( + resolve_bin_entry_value(&pkg_json, Some("other"),) + .err() + .unwrap() + .to_string(), + concat!( + "'/package.json' did not have a bin entry for 'other'\n", + "\n", + "Possibilities:\n", + " * npm:pkg@1.1.1/bin1\n", + " * npm:pkg@1.1.1/bin2\n", + " * npm:pkg@1.1.1/pkg" + ) + ); + + // should not resolve when default value can't be determined + let pkg_json = build_package_json(json!({ + "name": "pkg", + "version": "1.1.1", + "bin": { + "bin": "./value1", + "bin2": "./value2", + } + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, None) + .err() + .unwrap() + .to_string(), + concat!( + "'/package.json' did not have a bin entry for 'pkg'\n", + "\n", + "Possibilities:\n", + " * npm:pkg@1.1.1/bin\n", + " * npm:pkg@1.1.1/bin2", + ) + ); + + // should resolve since all the values are the same + let pkg_json = build_package_json(json!({ + "name": "pkg", + "version": "1.2.3", + "bin": { + "bin1": "./value", + "bin2": "./value", + } + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, None,).unwrap(), + "./value" + ); + + // should not resolve when specified and is a string + let pkg_json = build_package_json(json!({ + "name": "pkg", + "version": "1.2.3", + "bin": "./value", + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, Some("path"),) + .err() + .unwrap() + .to_string(), + "'/package.json' did not have a bin entry for 'path'" + ); + + // no version in the package.json + let pkg_json = build_package_json(json!({ + "name": "pkg", + "bin": { + "bin1": "./value1", + "bin2": "./value2", + } + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, None) + .err() + .unwrap() + .to_string(), + concat!( + "'/package.json' did not have a bin entry for 'pkg'\n", + "\n", + "Possibilities:\n", + " * npm:pkg/bin1\n", + " * npm:pkg/bin2", + ) + ); + + // no name or version in the package.json + let pkg_json = build_package_json(json!({ + "bin": { + "bin1": "./value1", + "bin2": "./value2", + } + })); + assert_eq!( + resolve_bin_entry_value(&pkg_json, None) + .err() + .unwrap() + .to_string(), + concat!( + "'/package.json' did not have a bin entry\n", + "\n", + "Possibilities:\n", + " * bin1\n", + " * bin2", + ) + ); + } + + #[test] + fn test_parse_package_name() { + let dummy_referrer = Url::parse("http://example.com").unwrap(); + + assert_eq!( + parse_npm_pkg_name("fetch-blob", &dummy_referrer).unwrap(), + ("fetch-blob".to_string(), ".".to_string(), false) + ); + assert_eq!( + parse_npm_pkg_name("@vue/plugin-vue", &dummy_referrer).unwrap(), + ("@vue/plugin-vue".to_string(), ".".to_string(), true) + ); + assert_eq!( + parse_npm_pkg_name("@astrojs/prism/dist/highlighter", &dummy_referrer) + .unwrap(), + ( + "@astrojs/prism".to_string(), + "./dist/highlighter".to_string(), + true + ) + ); + } + + #[test] + fn test_with_known_extension() { + let cases = &[ + ("test", "d.ts", "test.d.ts"), + ("test.d.ts", "ts", "test.ts"), + ("test.worker", "d.ts", "test.worker.d.ts"), + ("test.d.mts", "js", "test.js"), + ]; + for (path, ext, expected) in cases { + let actual = with_known_extension(&PathBuf::from(path), ext); + assert_eq!(actual.to_string_lossy(), *expected); + } + } + + #[test] + fn test_types_package_name() { + assert_eq!(types_package_name("name"), "@types/name"); + assert_eq!( + types_package_name("@scoped/package"), + "@types/@scoped__package" + ); + } +} |