diff options
Diffstat (limited to 'ext/node/resolver.rs')
-rw-r--r-- | ext/node/resolver.rs | 686 |
1 files changed, 0 insertions, 686 deletions
diff --git a/ext/node/resolver.rs b/ext/node/resolver.rs deleted file mode 100644 index 41e1cf4d4..000000000 --- a/ext/node/resolver.rs +++ /dev/null @@ -1,686 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::path::Path; -use std::path::PathBuf; - -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::serde_json::Value; -use deno_core::url::Url; -use deno_core::ModuleSpecifier; -use deno_media_type::MediaType; -use deno_semver::npm::NpmPackageNv; -use deno_semver::npm::NpmPackageNvReference; -use deno_semver::npm::NpmPackageReqReference; - -use crate::errors; -use crate::get_closest_package_json; -use crate::legacy_main_resolve; -use crate::package_exports_resolve; -use crate::package_imports_resolve; -use crate::package_resolve; -use crate::path_to_declaration_path; -use crate::AllowAllNodePermissions; -use crate::NodeFs; -use crate::NodeModuleKind; -use crate::NodePermissions; -use crate::NodeResolutionMode; -use crate::NpmResolver; -use crate::PackageJson; -use crate::DEFAULT_CONDITIONS; - -#[derive(Debug)] -pub enum NodeResolution { - Esm(ModuleSpecifier), - CommonJs(ModuleSpecifier), - BuiltIn(String), -} - -impl NodeResolution { - pub fn into_url(self) -> ModuleSpecifier { - match self { - Self::Esm(u) => u, - Self::CommonJs(u) => u, - Self::BuiltIn(specifier) => { - if specifier.starts_with("node:") { - ModuleSpecifier::parse(&specifier).unwrap() - } else { - ModuleSpecifier::parse(&format!("node:{specifier}")).unwrap() - } - } - } - } - - pub fn into_specifier_and_media_type( - resolution: Option<Self>, - ) -> (ModuleSpecifier, MediaType) { - match resolution { - Some(NodeResolution::CommonJs(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Cts, - MediaType::Dts => MediaType::Dcts, - _ => media_type, - }, - ) - } - Some(NodeResolution::Esm(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Mts, - MediaType::Dts => MediaType::Dmts, - _ => media_type, - }, - ) - } - Some(resolution) => (resolution.into_url(), MediaType::Dts), - None => ( - ModuleSpecifier::parse("internal:///missing_dependency.d.ts").unwrap(), - MediaType::Dts, - ), - } - } -} - -#[derive(Debug)] -pub struct NodeResolver<TRequireNpmResolver: NpmResolver> { - npm_resolver: TRequireNpmResolver, -} - -impl<TRequireNpmResolver: NpmResolver> NodeResolver<TRequireNpmResolver> { - pub fn new(require_npm_resolver: TRequireNpmResolver) -> Self { - Self { - npm_resolver: require_npm_resolver, - } - } - - pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { - self.npm_resolver.in_npm_package(specifier) - } - - /// This function is an implementation of `defaultResolve` in - /// `lib/internal/modules/esm/resolve.js` from Node. - pub fn resolve<Fs: NodeFs>( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - permissions: &mut dyn NodePermissions, - ) -> Result<Option<NodeResolution>, AnyError> { - // Note: if we are here, then the referrer is an esm module - // TODO(bartlomieju): skipped "policy" part as we don't plan to support it - - if crate::is_builtin_node_module(specifier) { - return Ok(Some(NodeResolution::BuiltIn(specifier.to_string()))); - } - - if let Ok(url) = Url::parse(specifier) { - if url.scheme() == "data" { - return Ok(Some(NodeResolution::Esm(url))); - } - - let protocol = url.scheme(); - - if protocol == "node" { - let split_specifier = url.as_str().split(':'); - let specifier = split_specifier.skip(1).collect::<String>(); - - if crate::is_builtin_node_module(&specifier) { - return Ok(Some(NodeResolution::BuiltIn(specifier))); - } - } - - if protocol != "file" && protocol != "data" { - return Err(errors::err_unsupported_esm_url_scheme(&url)); - } - - // todo(dsherret): this seems wrong - if referrer.scheme() == "data" { - let url = referrer.join(specifier).map_err(AnyError::from)?; - return Ok(Some(NodeResolution::Esm(url))); - } - } - - let url = self.module_resolve::<Fs>( - specifier, - referrer, - DEFAULT_CONDITIONS, - mode, - permissions, - )?; - let url = match url { - Some(url) => url, - None => return Ok(None), - }; - let url = match mode { - NodeResolutionMode::Execution => url, - NodeResolutionMode::Types => { - let path = url.to_file_path().unwrap(); - // todo(16370): the module kind is not correct here. I think we need - // typescript to tell us if the referrer is esm or cjs - let path = - match path_to_declaration_path::<Fs>(path, NodeModuleKind::Esm) { - Some(path) => path, - None => return Ok(None), - }; - ModuleSpecifier::from_file_path(path).unwrap() - } - }; - - let resolve_response = self.url_to_node_resolution::<Fs>(url)?; - // TODO(bartlomieju): skipped checking errors for commonJS resolution and - // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(Some(resolve_response)) - } - - fn module_resolve<Fs: NodeFs>( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - conditions: &[&str], - mode: NodeResolutionMode, - permissions: &mut dyn NodePermissions, - ) -> Result<Option<ModuleSpecifier>, AnyError> { - // note: if we're here, the referrer is an esm module - let url = if should_be_treated_as_relative_or_absolute_path(specifier) { - let resolved_specifier = referrer.join(specifier)?; - if mode.is_types() { - let file_path = to_file_path(&resolved_specifier); - // todo(dsherret): the node module kind is not correct and we - // should use the value provided by typescript instead - let declaration_path = - path_to_declaration_path::<Fs>(file_path, NodeModuleKind::Esm); - declaration_path.map(|declaration_path| { - ModuleSpecifier::from_file_path(declaration_path).unwrap() - }) - } else { - Some(resolved_specifier) - } - } else if specifier.starts_with('#') { - Some( - package_imports_resolve::<Fs>( - specifier, - referrer, - NodeModuleKind::Esm, - conditions, - mode, - &self.npm_resolver, - permissions, - ) - .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?, - ) - } else if let Ok(resolved) = Url::parse(specifier) { - Some(resolved) - } else { - package_resolve::<Fs>( - specifier, - referrer, - NodeModuleKind::Esm, - conditions, - mode, - &self.npm_resolver, - permissions, - )? - .map(|p| ModuleSpecifier::from_file_path(p).unwrap()) - }; - Ok(match url { - Some(url) => Some(finalize_resolution::<Fs>(url, referrer)?), - None => None, - }) - } - - pub fn resolve_npm_req_reference<Fs: NodeFs>( - &self, - reference: &NpmPackageReqReference, - mode: NodeResolutionMode, - permissions: &mut dyn NodePermissions, - ) -> Result<Option<NodeResolution>, AnyError> { - let reference = self - .npm_resolver - .resolve_nv_ref_from_pkg_req_ref(reference)?; - self.resolve_npm_reference::<Fs>(&reference, mode, permissions) - } - - pub fn resolve_npm_reference<Fs: NodeFs>( - &self, - reference: &NpmPackageNvReference, - mode: NodeResolutionMode, - permissions: &mut dyn NodePermissions, - ) -> Result<Option<NodeResolution>, AnyError> { - let package_folder = self - .npm_resolver - .resolve_package_folder_from_deno_module(&reference.nv)?; - let node_module_kind = NodeModuleKind::Esm; - let maybe_resolved_path = package_config_resolve::<Fs>( - &reference - .sub_path - .as_ref() - .map(|s| format!("./{s}")) - .unwrap_or_else(|| ".".to_string()), - &package_folder, - node_module_kind, - DEFAULT_CONDITIONS, - mode, - &self.npm_resolver, - permissions, - ) - .with_context(|| { - format!("Error resolving package config for '{reference}'") - })?; - let resolved_path = match maybe_resolved_path { - Some(resolved_path) => resolved_path, - None => return Ok(None), - }; - let resolved_path = match mode { - NodeResolutionMode::Execution => resolved_path, - NodeResolutionMode::Types => { - match path_to_declaration_path::<Fs>(resolved_path, node_module_kind) { - Some(path) => path, - None => return Ok(None), - } - } - }; - let url = ModuleSpecifier::from_file_path(resolved_path).unwrap(); - let resolve_response = self.url_to_node_resolution::<Fs>(url)?; - // TODO(bartlomieju): skipped checking errors for commonJS resolution and - // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(Some(resolve_response)) - } - - pub fn resolve_binary_commands<Fs: NodeFs>( - &self, - pkg_nv: &NpmPackageNv, - ) -> Result<Vec<String>, AnyError> { - let package_folder = self - .npm_resolver - .resolve_package_folder_from_deno_module(pkg_nv)?; - let package_json_path = package_folder.join("package.json"); - let package_json = PackageJson::load::<Fs>( - &self.npm_resolver, - &mut AllowAllNodePermissions, - package_json_path, - )?; - - Ok(match package_json.bin { - Some(Value::String(_)) => vec![pkg_nv.name.to_string()], - Some(Value::Object(o)) => { - o.into_iter().map(|(key, _)| key).collect::<Vec<_>>() - } - _ => Vec::new(), - }) - } - - pub fn resolve_binary_export<Fs: NodeFs>( - &self, - pkg_ref: &NpmPackageReqReference, - ) -> Result<NodeResolution, AnyError> { - let pkg_nv = self - .npm_resolver - .resolve_pkg_id_from_pkg_req(&pkg_ref.req)? - .nv; - let bin_name = pkg_ref.sub_path.as_deref(); - let package_folder = self - .npm_resolver - .resolve_package_folder_from_deno_module(&pkg_nv)?; - let package_json_path = package_folder.join("package.json"); - let package_json = PackageJson::load::<Fs>( - &self.npm_resolver, - &mut AllowAllNodePermissions, - package_json_path, - )?; - let bin = match &package_json.bin { - Some(bin) => bin, - None => bail!( - "package '{}' did not have a bin property in its package.json", - &pkg_nv.name, - ), - }; - let bin_entry = resolve_bin_entry_value(&pkg_nv, bin_name, bin)?; - let url = - ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap(); - - let resolve_response = self.url_to_node_resolution::<Fs>(url)?; - // TODO(bartlomieju): skipped checking errors for commonJS resolution and - // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(resolve_response) - } - - pub fn url_to_node_resolution<Fs: NodeFs>( - &self, - url: ModuleSpecifier, - ) -> Result<NodeResolution, AnyError> { - let url_str = url.as_str().to_lowercase(); - if url_str.starts_with("http") { - Ok(NodeResolution::Esm(url)) - } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") { - let package_config = get_closest_package_json::<Fs>( - &url, - &self.npm_resolver, - &mut AllowAllNodePermissions, - )?; - if package_config.typ == "module" { - Ok(NodeResolution::Esm(url)) - } else { - Ok(NodeResolution::CommonJs(url)) - } - } else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") { - Ok(NodeResolution::Esm(url)) - } else if url_str.ends_with(".ts") { - Err(generic_error(format!( - "TypeScript files are not supported in npm packages: {url}" - ))) - } else { - Ok(NodeResolution::CommonJs(url)) - } - } -} - -fn resolve_bin_entry_value<'a>( - pkg_nv: &NpmPackageNv, - bin_name: Option<&str>, - bin: &'a Value, -) -> Result<&'a str, AnyError> { - let bin_entry = match bin { - Value::String(_) => { - if bin_name.is_some() && bin_name.unwrap() != pkg_nv.name { - None - } else { - Some(bin) - } - } - Value::Object(o) => { - if let Some(bin_name) = bin_name { - o.get(bin_name) - } else if o.len() == 1 || o.len() > 1 && o.values().all(|v| v == o.values().next().unwrap()) { - o.values().next() - } else { - o.get(&pkg_nv.name) - } - }, - _ => bail!("package '{}' did not have a bin property with a string or object value in its package.json", pkg_nv), - }; - let bin_entry = match bin_entry { - Some(e) => e, - None => { - let keys = bin - .as_object() - .map(|o| { - o.keys() - .map(|k| format!(" * npm:{pkg_nv}/{k}")) - .collect::<Vec<_>>() - }) - .unwrap_or_default(); - bail!( - "package '{}' did not have a bin entry for '{}' in its package.json{}", - pkg_nv, - bin_name.unwrap_or(&pkg_nv.name), - if keys.is_empty() { - "".to_string() - } else { - format!("\n\nPossibilities:\n{}", keys.join("\n")) - } - ) - } - }; - match bin_entry { - Value::String(s) => Ok(s), - _ => bail!( - "package '{}' had a non-string sub property of bin in its package.json", - pkg_nv, - ), - } -} - -fn package_config_resolve<Fs: NodeFs>( - package_subpath: &str, - package_dir: &Path, - referrer_kind: NodeModuleKind, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn NpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<Option<PathBuf>, AnyError> { - let package_json_path = package_dir.join("package.json"); - let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap(); - let package_config = PackageJson::load::<Fs>( - npm_resolver, - permissions, - package_json_path.clone(), - )?; - if let Some(exports) = &package_config.exports { - let result = package_exports_resolve::<Fs>( - &package_json_path, - package_subpath.to_string(), - exports, - &referrer, - referrer_kind, - conditions, - mode, - npm_resolver, - permissions, - ); - match result { - Ok(found) => return Ok(Some(found)), - Err(exports_err) => { - if mode.is_types() && package_subpath == "." { - if let Ok(Some(path)) = - legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode) - { - return Ok(Some(path)); - } else { - return Ok(None); - } - } - return Err(exports_err); - } - } - } - if package_subpath == "." { - return legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode); - } - - Ok(Some(package_dir.join(package_subpath))) -} - -fn finalize_resolution<Fs: NodeFs>( - resolved: ModuleSpecifier, - base: &ModuleSpecifier, -) -> Result<ModuleSpecifier, AnyError> { - let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C"); - - if encoded_sep_re.is_match(resolved.path()) { - return Err(errors::err_invalid_module_specifier( - resolved.path(), - "must not include encoded \"/\" or \"\\\\\" characters", - Some(to_file_path_string(base)), - )); - } - - let path = to_file_path(&resolved); - - // TODO(bartlomieju): currently not supported - // if (getOptionValue('--experimental-specifier-resolution') === 'node') { - // ... - // } - - let p_str = path.to_str().unwrap(); - let p = if p_str.ends_with('/') { - p_str[p_str.len() - 1..].to_string() - } else { - p_str.to_string() - }; - - let (is_dir, is_file) = if let Ok(stats) = Fs::metadata(p) { - (stats.is_dir, stats.is_file) - } else { - (false, false) - }; - if is_dir { - return Err(errors::err_unsupported_dir_import( - resolved.as_str(), - base.as_str(), - )); - } else if !is_file { - return Err(errors::err_module_not_found( - resolved.as_str(), - base.as_str(), - "module", - )); - } - - Ok(resolved) -} - -fn to_file_path(url: &ModuleSpecifier) -> PathBuf { - url - .to_file_path() - .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}")) -} - -fn to_file_path_string(url: &ModuleSpecifier) -> String { - to_file_path(url).display().to_string() -} - -fn should_be_treated_as_relative_or_absolute_path(specifier: &str) -> bool { - if specifier.is_empty() { - return false; - } - - if specifier.starts_with('/') { - return true; - } - - is_relative_specifier(specifier) -} - -// TODO(ry) We very likely have this utility function elsewhere in Deno. -fn is_relative_specifier(specifier: &str) -> bool { - let specifier_len = specifier.len(); - let specifier_chars: Vec<_> = specifier.chars().collect(); - - if !specifier_chars.is_empty() && specifier_chars[0] == '.' { - if specifier_len == 1 || specifier_chars[1] == '/' { - return true; - } - if specifier_chars[1] == '.' - && (specifier_len == 2 || specifier_chars[2] == '/') - { - return true; - } - } - false -} - -#[cfg(test)] -mod tests { - use deno_core::serde_json::json; - - use super::*; - - #[test] - fn test_resolve_bin_entry_value() { - // should resolve the specified value - let value = json!({ - "bin1": "./value1", - "bin2": "./value2", - "test": "./value3", - }); - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("test@1.1.1").unwrap(), - Some("bin1"), - &value - ) - .unwrap(), - "./value1" - ); - - // should resolve the value with the same name when not specified - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("test@1.1.1").unwrap(), - None, - &value - ) - .unwrap(), - "./value3" - ); - - // should not resolve when specified value does not exist - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("test@1.1.1").unwrap(), - Some("other"), - &value - ) - .err() - .unwrap() - .to_string(), - concat!( - "package 'test@1.1.1' did not have a bin entry for 'other' in its package.json\n", - "\n", - "Possibilities:\n", - " * npm:test@1.1.1/bin1\n", - " * npm:test@1.1.1/bin2\n", - " * npm:test@1.1.1/test" - ) - ); - - // should not resolve when default value can't be determined - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("asdf@1.2.3").unwrap(), - None, - &value - ) - .err() - .unwrap() - .to_string(), - concat!( - "package 'asdf@1.2.3' did not have a bin entry for 'asdf' in its package.json\n", - "\n", - "Possibilities:\n", - " * npm:asdf@1.2.3/bin1\n", - " * npm:asdf@1.2.3/bin2\n", - " * npm:asdf@1.2.3/test" - ) - ); - - // should resolve since all the values are the same - let value = json!({ - "bin1": "./value", - "bin2": "./value", - }); - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("test@1.2.3").unwrap(), - None, - &value - ) - .unwrap(), - "./value" - ); - - // should not resolve when specified and is a string - let value = json!("./value"); - assert_eq!( - resolve_bin_entry_value( - &NpmPackageNv::from_str("test@1.2.3").unwrap(), - Some("path"), - &value - ) - .err() - .unwrap() - .to_string(), - "package 'test@1.2.3' did not have a bin entry for 'path' in its package.json" - ); - } -} |