diff options
Diffstat (limited to 'cli/node')
-rw-r--r-- | cli/node/analyze.rs | 534 | ||||
-rw-r--r-- | cli/node/mod.rs | 460 |
2 files changed, 520 insertions, 474 deletions
diff --git a/cli/node/analyze.rs b/cli/node/analyze.rs index 4040c5a2b..f93e9fa91 100644 --- a/cli/node/analyze.rs +++ b/cli/node/analyze.rs @@ -1,18 +1,36 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use std::collections::HashSet; +use std::collections::VecDeque; +use std::fmt::Write; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; use deno_ast::swc::common::SyntaxContext; use deno_ast::view::Node; use deno_ast::view::NodeTrait; +use deno_ast::CjsAnalysis; +use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_ast::ParsedSource; use deno_ast::SourceRanged; +use deno_core::anyhow::anyhow; use deno_core::error::AnyError; +use deno_runtime::deno_node::package_exports_resolve; +use deno_runtime::deno_node::NodeModuleKind; +use deno_runtime::deno_node::NodePermissions; +use deno_runtime::deno_node::NodeResolutionMode; +use deno_runtime::deno_node::PackageJson; +use deno_runtime::deno_node::PathClean; +use deno_runtime::deno_node::RealFs; +use deno_runtime::deno_node::RequireNpmResolver; use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME; -use std::fmt::Write; +use once_cell::sync::Lazy; use crate::cache::NodeAnalysisCache; +use crate::file_fetcher::FileFetcher; +use crate::npm::NpmPackageResolver; static NODE_GLOBALS: &[&str] = &[ "Buffer", @@ -27,18 +45,287 @@ static NODE_GLOBALS: &[&str] = &[ "setTimeout", ]; -// TODO(dsherret): this code is way more inefficient than it needs to be. -// -// In the future, we should disable capturing tokens & scope analysis -// and instead only use swc's APIs to go through the portions of the tree -// that we know will affect the global scope while still ensuring that -// `var` decls are taken into consideration. +pub struct NodeCodeTranslator { + analysis_cache: NodeAnalysisCache, + file_fetcher: Arc<FileFetcher>, + npm_resolver: Arc<NpmPackageResolver>, +} + +impl NodeCodeTranslator { + pub fn new( + analysis_cache: NodeAnalysisCache, + file_fetcher: Arc<FileFetcher>, + npm_resolver: Arc<NpmPackageResolver>, + ) -> Self { + Self { + analysis_cache, + file_fetcher, + npm_resolver, + } + } + + pub fn esm_code_with_node_globals( + &self, + specifier: &ModuleSpecifier, + code: String, + ) -> Result<String, AnyError> { + esm_code_with_node_globals(&self.analysis_cache, specifier, code) + } + + /// Translates given CJS module into ESM. This function will perform static + /// analysis on the file to find defined exports and reexports. + /// + /// For all discovered reexports the analysis will be performed recursively. + /// + /// If successful a source code for equivalent ES module is returned. + pub fn translate_cjs_to_esm( + &self, + specifier: &ModuleSpecifier, + code: String, + media_type: MediaType, + permissions: &mut dyn NodePermissions, + ) -> Result<String, AnyError> { + let mut temp_var_count = 0; + let mut handled_reexports: HashSet<String> = HashSet::default(); + + let mut source = vec![ + r#"import {createRequire as __internalCreateRequire} from "node:module"; + const require = __internalCreateRequire(import.meta.url);"# + .to_string(), + ]; + + let analysis = + self.perform_cjs_analysis(specifier.as_str(), media_type, code)?; + + let mut all_exports = analysis + .exports + .iter() + .map(|s| s.to_string()) + .collect::<HashSet<_>>(); + + // (request, referrer) + let mut reexports_to_handle = VecDeque::new(); + for reexport in analysis.reexports { + reexports_to_handle.push_back((reexport, specifier.clone())); + } + + while let Some((reexport, referrer)) = reexports_to_handle.pop_front() { + if handled_reexports.contains(&reexport) { + continue; + } + + handled_reexports.insert(reexport.to_string()); + + // First, resolve relate reexport specifier + let resolved_reexport = self.resolve( + &reexport, + &referrer, + // FIXME(bartlomieju): check if these conditions are okay, probably + // should be `deno-require`, because `deno` is already used in `esm_resolver.rs` + &["deno", "require", "default"], + NodeResolutionMode::Execution, + permissions, + )?; + let reexport_specifier = + ModuleSpecifier::from_file_path(resolved_reexport).unwrap(); + // Second, read the source code from disk + let reexport_file = self + .file_fetcher + .get_source(&reexport_specifier) + .ok_or_else(|| { + anyhow!( + "Could not find '{}' ({}) referenced from {}", + reexport, + reexport_specifier, + referrer + ) + })?; + + { + let analysis = self.perform_cjs_analysis( + reexport_specifier.as_str(), + reexport_file.media_type, + reexport_file.source.to_string(), + )?; + + for reexport in analysis.reexports { + reexports_to_handle.push_back((reexport, reexport_specifier.clone())); + } + + all_exports.extend( + analysis + .exports + .into_iter() + .filter(|e| e.as_str() != "default"), + ); + } + } + + source.push(format!( + "const mod = require(\"{}\");", + specifier + .to_file_path() + .unwrap() + .to_str() + .unwrap() + .replace('\\', "\\\\") + .replace('\'', "\\\'") + .replace('\"', "\\\"") + )); + + for export in &all_exports { + if export.as_str() != "default" { + add_export( + &mut source, + export, + &format!("mod[\"{export}\"]"), + &mut temp_var_count, + ); + } + } + + source.push("export default mod;".to_string()); + + let translated_source = source.join("\n"); + Ok(translated_source) + } + + fn perform_cjs_analysis( + &self, + specifier: &str, + media_type: MediaType, + code: String, + ) -> Result<CjsAnalysis, AnyError> { + let source_hash = NodeAnalysisCache::compute_source_hash(&code); + if let Some(analysis) = self + .analysis_cache + .get_cjs_analysis(specifier, &source_hash) + { + return Ok(analysis); + } + + if media_type == MediaType::Json { + return Ok(CjsAnalysis { + exports: vec![], + reexports: vec![], + }); + } + + let parsed_source = deno_ast::parse_script(deno_ast::ParseParams { + specifier: specifier.to_string(), + text_info: deno_ast::SourceTextInfo::new(code.into()), + media_type, + capture_tokens: true, + scope_analysis: false, + maybe_syntax: None, + })?; + let analysis = parsed_source.analyze_cjs(); + self + .analysis_cache + .set_cjs_analysis(specifier, &source_hash, &analysis); + + Ok(analysis) + } + + fn resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + conditions: &[&str], + mode: NodeResolutionMode, + permissions: &mut dyn NodePermissions, + ) -> Result<PathBuf, AnyError> { + if specifier.starts_with('/') { + todo!(); + } + + let referrer_path = referrer.to_file_path().unwrap(); + if specifier.starts_with("./") || specifier.starts_with("../") { + if let Some(parent) = referrer_path.parent() { + return file_extension_probe(parent.join(specifier), &referrer_path); + } else { + todo!(); + } + } + + // We've got a bare specifier or maybe bare_specifier/blah.js" + + let (package_specifier, package_subpath) = + parse_specifier(specifier).unwrap(); + + // todo(dsherret): use not_found error on not found here + let resolver = self.npm_resolver.as_require_npm_resolver(); + let module_dir = resolver.resolve_package_folder_from_package( + package_specifier.as_str(), + &referrer_path, + mode, + )?; + + let package_json_path = module_dir.join("package.json"); + if package_json_path.exists() { + let package_json = PackageJson::load::<RealFs>( + &self.npm_resolver.as_require_npm_resolver(), + permissions, + package_json_path.clone(), + )?; + + if let Some(exports) = &package_json.exports { + return package_exports_resolve::<RealFs>( + &package_json_path, + package_subpath, + exports, + referrer, + NodeModuleKind::Esm, + conditions, + mode, + &self.npm_resolver.as_require_npm_resolver(), + permissions, + ); + } + + // old school + if package_subpath != "." { + let d = module_dir.join(package_subpath); + if let Ok(m) = d.metadata() { + if m.is_dir() { + // subdir might have a package.json that specifies the entrypoint + let package_json_path = d.join("package.json"); + if package_json_path.exists() { + let package_json = PackageJson::load::<RealFs>( + &self.npm_resolver.as_require_npm_resolver(), + permissions, + package_json_path, + )?; + if let Some(main) = package_json.main(NodeModuleKind::Cjs) { + return Ok(d.join(main).clean()); + } + } + + return Ok(d.join("index.js").clean()); + } + } + return file_extension_probe(d, &referrer_path); + } else if let Some(main) = package_json.main(NodeModuleKind::Cjs) { + return Ok(module_dir.join(main).clean()); + } else { + return Ok(module_dir.join("index.js").clean()); + } + } + Err(not_found(specifier, &referrer_path)) + } +} -pub fn esm_code_with_node_globals( +fn esm_code_with_node_globals( analysis_cache: &NodeAnalysisCache, specifier: &ModuleSpecifier, code: String, ) -> Result<String, AnyError> { + // TODO(dsherret): this code is way more inefficient than it needs to be. + // + // In the future, we should disable capturing tokens & scope analysis + // and instead only use swc's APIs to go through the portions of the tree + // that we know will affect the global scope while still ensuring that + // `var` decls are taken into consideration. let source_hash = NodeAnalysisCache::compute_source_hash(&code); let text_info = deno_ast::SourceTextInfo::from_string(code); let top_level_decls = if let Some(decls) = @@ -63,6 +350,16 @@ pub fn esm_code_with_node_globals( top_level_decls }; + Ok(esm_code_from_top_level_decls( + text_info.text_str(), + &top_level_decls, + )) +} + +fn esm_code_from_top_level_decls( + file_text: &str, + top_level_decls: &HashSet<String>, +) -> String { let mut globals = Vec::with_capacity(NODE_GLOBALS.len()); let has_global_this = top_level_decls.contains("globalThis"); for global in NODE_GLOBALS.iter() { @@ -83,7 +380,6 @@ pub fn esm_code_with_node_globals( write!(result, "var {global} = {global_this_expr}.{global};").unwrap(); } - let file_text = text_info.text_str(); // strip the shebang let file_text = if file_text.starts_with("#!/") { let start_index = file_text.find('\n').unwrap_or(file_text.len()); @@ -93,12 +389,28 @@ pub fn esm_code_with_node_globals( }; result.push_str(file_text); - Ok(result) + result } fn analyze_top_level_decls( parsed_source: &ParsedSource, ) -> Result<HashSet<String>, AnyError> { + fn visit_children( + node: Node, + top_level_context: SyntaxContext, + results: &mut HashSet<String>, + ) { + if let Node::Ident(ident) = node { + if ident.ctxt() == top_level_context && is_local_declaration_ident(node) { + results.insert(ident.sym().to_string()); + } + } + + for child in node.children() { + visit_children(child, top_level_context, results); + } + } + let top_level_context = parsed_source.top_level_context(); parsed_source.with_view(|program| { @@ -108,22 +420,6 @@ fn analyze_top_level_decls( }) } -fn visit_children( - node: Node, - top_level_context: SyntaxContext, - results: &mut HashSet<String>, -) { - if let Node::Ident(ident) = node { - if ident.ctxt() == top_level_context && is_local_declaration_ident(node) { - results.insert(ident.sym().to_string()); - } - } - - for child in node.children() { - visit_children(child, top_level_context, results); - } -} - fn is_local_declaration_ident(node: Node) -> bool { if let Some(parent) = node.parent() { match parent { @@ -160,6 +456,162 @@ fn is_local_declaration_ident(node: Node) -> bool { } } +static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| { + HashSet::from([ + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + "let", + "enum", + "implements", + "interface", + "package", + "private", + "protected", + "public", + "static", + ]) +}); + +fn add_export( + source: &mut Vec<String>, + name: &str, + initializer: &str, + temp_var_count: &mut usize, +) { + fn is_valid_var_decl(name: &str) -> bool { + // it's ok to be super strict here + name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') + } + + // TODO(bartlomieju): Node actually checks if a given export exists in `exports` object, + // but it might not be necessary here since our analysis is more detailed? + if RESERVED_WORDS.contains(name) || !is_valid_var_decl(name) { + *temp_var_count += 1; + // we can't create an identifier with a reserved word or invalid identifier name, + // so assign it to a temporary variable that won't have a conflict, then re-export + // it as a string + source.push(format!( + "const __deno_export_{temp_var_count}__ = {initializer};" + )); + source.push(format!( + "export {{ __deno_export_{temp_var_count}__ as \"{name}\" }};" + )); + } else { + source.push(format!("export const {name} = {initializer};")); + } +} + +fn parse_specifier(specifier: &str) -> Option<(String, String)> { + 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(|i| i + index + 1); + } 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 None; + } + + let package_subpath = if let Some(index) = separator_index { + format!(".{}", specifier.chars().skip(index).collect::<String>()) + } else { + ".".to_string() + }; + + Some((package_name, package_subpath)) +} + +fn file_extension_probe( + p: PathBuf, + referrer: &Path, +) -> Result<PathBuf, AnyError> { + let p = p.clean(); + if p.exists() { + let file_name = p.file_name().unwrap(); + let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap())); + if p_js.exists() && p_js.is_file() { + return Ok(p_js); + } else if p.is_dir() { + return Ok(p.join("index.js")); + } else { + return Ok(p); + } + } else if let Some(file_name) = p.file_name() { + let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap())); + if p_js.exists() && p_js.is_file() { + return Ok(p_js); + } + } + Err(not_found(&p.to_string_lossy(), referrer)) +} + +fn not_found(path: &str, referrer: &Path) -> AnyError { + let msg = format!( + "[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"", + path, + referrer.to_string_lossy() + ); + std::io::Error::new(std::io::ErrorKind::NotFound, msg).into() +} + #[cfg(test)] mod tests { use super::*; @@ -205,4 +657,34 @@ mod tests { ) ); } + + #[test] + fn test_add_export() { + let mut temp_var_count = 0; + let mut source = vec![]; + + let exports = vec!["static", "server", "app", "dashed-export"]; + for export in exports { + add_export(&mut source, export, "init", &mut temp_var_count); + } + assert_eq!( + source, + vec![ + "const __deno_export_1__ = init;".to_string(), + "export { __deno_export_1__ as \"static\" };".to_string(), + "export const server = init;".to_string(), + "export const app = init;".to_string(), + "const __deno_export_2__ = init;".to_string(), + "export { __deno_export_2__ as \"dashed-export\" };".to_string(), + ] + ) + } + + #[test] + fn test_parse_specifier() { + assert_eq!( + parse_specifier("@some-package/core/actions"), + Some(("@some-package/core".to_string(), "./actions".to_string())) + ); + } } diff --git a/cli/node/mod.rs b/cli/node/mod.rs index 2207ce04e..99df672fc 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -1,14 +1,11 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use std::collections::HashSet; -use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; -use deno_ast::CjsAnalysis; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::generic_error; @@ -28,23 +25,19 @@ use deno_runtime::deno_node::NodeModuleKind; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::deno_node::PackageJson; -use deno_runtime::deno_node::PathClean; use deno_runtime::deno_node::RealFs; use deno_runtime::deno_node::RequireNpmResolver; use deno_runtime::deno_node::DEFAULT_CONDITIONS; use deno_runtime::permissions::PermissionsContainer; use deno_semver::npm::NpmPackageNv; use deno_semver::npm::NpmPackageNvReference; -use once_cell::sync::Lazy; -use crate::cache::NodeAnalysisCache; -use crate::file_fetcher::FileFetcher; use crate::npm::NpmPackageResolver; use crate::util::fs::canonicalize_path_maybe_not_exists; mod analyze; -pub use analyze::esm_code_with_node_globals; +pub use analyze::NodeCodeTranslator; #[derive(Debug)] pub enum NodeResolution { @@ -116,56 +109,6 @@ pub fn resolve_builtin_node_module(module_name: &str) -> Result<Url, AnyError> { ))) } -static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| { - HashSet::from([ - "break", - "case", - "catch", - "class", - "const", - "continue", - "debugger", - "default", - "delete", - "do", - "else", - "export", - "extends", - "false", - "finally", - "for", - "function", - "if", - "import", - "in", - "instanceof", - "new", - "null", - "return", - "super", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "var", - "void", - "while", - "with", - "yield", - "let", - "enum", - "implements", - "interface", - "package", - "private", - "protected", - "public", - "static", - ]) -}); - /// This function is an implementation of `defaultResolve` in /// `lib/internal/modules/esm/resolve.js` from Node. pub fn node_resolve( @@ -245,7 +188,7 @@ pub fn node_resolve( pub fn node_resolve_npm_reference( reference: &NpmPackageNvReference, mode: NodeResolutionMode, - npm_resolver: &NpmPackageResolver, + npm_resolver: &Arc<NpmPackageResolver>, permissions: &mut dyn NodePermissions, ) -> Result<Option<NodeResolution>, AnyError> { let package_folder = @@ -261,7 +204,7 @@ pub fn node_resolve_npm_reference( node_module_kind, DEFAULT_CONDITIONS, mode, - npm_resolver, + &npm_resolver.as_require_npm_resolver(), permissions, ) .with_context(|| { @@ -282,7 +225,8 @@ pub fn node_resolve_npm_reference( } }; let url = ModuleSpecifier::from_file_path(resolved_path).unwrap(); - let resolve_response = url_to_node_resolution(url, npm_resolver)?; + let resolve_response = + url_to_node_resolution(url, &npm_resolver.as_require_npm_resolver())?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(Some(resolve_response)) @@ -309,13 +253,13 @@ pub fn resolve_specifier_into_node_modules( pub fn node_resolve_binary_commands( pkg_nv: &NpmPackageNv, - npm_resolver: &NpmPackageResolver, + npm_resolver: &Arc<NpmPackageResolver>, ) -> Result<Vec<String>, AnyError> { let package_folder = npm_resolver.resolve_package_folder_from_deno_module(pkg_nv)?; let package_json_path = package_folder.join("package.json"); let package_json = PackageJson::load::<RealFs>( - npm_resolver, + &npm_resolver.as_require_npm_resolver(), &mut PermissionsContainer::allow_all(), package_json_path, )?; @@ -332,13 +276,13 @@ pub fn node_resolve_binary_commands( pub fn node_resolve_binary_export( pkg_nv: &NpmPackageNv, bin_name: Option<&str>, - npm_resolver: &NpmPackageResolver, + npm_resolver: &Arc<NpmPackageResolver>, ) -> Result<NodeResolution, AnyError> { let package_folder = npm_resolver.resolve_package_folder_from_deno_module(pkg_nv)?; let package_json_path = package_folder.join("package.json"); let package_json = PackageJson::load::<RealFs>( - npm_resolver, + &npm_resolver.as_require_npm_resolver(), &mut PermissionsContainer::allow_all(), package_json_path, )?; @@ -353,7 +297,8 @@ pub fn node_resolve_binary_export( let url = ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap(); - let resolve_response = url_to_node_resolution(url, npm_resolver)?; + let resolve_response = + url_to_node_resolution(url, &npm_resolver.as_require_npm_resolver())?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(resolve_response) @@ -600,324 +545,6 @@ fn module_resolve( }) } -fn add_export( - source: &mut Vec<String>, - name: &str, - initializer: &str, - temp_var_count: &mut usize, -) { - fn is_valid_var_decl(name: &str) -> bool { - // it's ok to be super strict here - name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') - } - - // TODO(bartlomieju): Node actually checks if a given export exists in `exports` object, - // but it might not be necessary here since our analysis is more detailed? - if RESERVED_WORDS.contains(name) || !is_valid_var_decl(name) { - *temp_var_count += 1; - // we can't create an identifier with a reserved word or invalid identifier name, - // so assign it to a temporary variable that won't have a conflict, then re-export - // it as a string - source.push(format!( - "const __deno_export_{temp_var_count}__ = {initializer};" - )); - source.push(format!( - "export {{ __deno_export_{temp_var_count}__ as \"{name}\" }};" - )); - } else { - source.push(format!("export const {name} = {initializer};")); - } -} - -/// Translates given CJS module into ESM. This function will perform static -/// analysis on the file to find defined exports and reexports. -/// -/// For all discovered reexports the analysis will be performed recursively. -/// -/// If successful a source code for equivalent ES module is returned. -pub fn translate_cjs_to_esm( - file_fetcher: &FileFetcher, - specifier: &ModuleSpecifier, - code: String, - media_type: MediaType, - npm_resolver: &NpmPackageResolver, - node_analysis_cache: &NodeAnalysisCache, - permissions: &mut dyn NodePermissions, -) -> Result<String, AnyError> { - fn perform_cjs_analysis( - analysis_cache: &NodeAnalysisCache, - specifier: &str, - media_type: MediaType, - code: String, - ) -> Result<CjsAnalysis, AnyError> { - let source_hash = NodeAnalysisCache::compute_source_hash(&code); - if let Some(analysis) = - analysis_cache.get_cjs_analysis(specifier, &source_hash) - { - return Ok(analysis); - } - - if media_type == MediaType::Json { - return Ok(CjsAnalysis { - exports: vec![], - reexports: vec![], - }); - } - - let parsed_source = deno_ast::parse_script(deno_ast::ParseParams { - specifier: specifier.to_string(), - text_info: deno_ast::SourceTextInfo::new(code.into()), - media_type, - capture_tokens: true, - scope_analysis: false, - maybe_syntax: None, - })?; - let analysis = parsed_source.analyze_cjs(); - analysis_cache.set_cjs_analysis(specifier, &source_hash, &analysis); - - Ok(analysis) - } - - let mut temp_var_count = 0; - let mut handled_reexports: HashSet<String> = HashSet::default(); - - let mut source = vec![ - r#"import {createRequire as __internalCreateRequire} from "node:module"; - const require = __internalCreateRequire(import.meta.url);"# - .to_string(), - ]; - - let analysis = perform_cjs_analysis( - node_analysis_cache, - specifier.as_str(), - media_type, - code, - )?; - - let mut all_exports = analysis - .exports - .iter() - .map(|s| s.to_string()) - .collect::<HashSet<_>>(); - - // (request, referrer) - let mut reexports_to_handle = VecDeque::new(); - for reexport in analysis.reexports { - reexports_to_handle.push_back((reexport, specifier.clone())); - } - - while let Some((reexport, referrer)) = reexports_to_handle.pop_front() { - if handled_reexports.contains(&reexport) { - continue; - } - - handled_reexports.insert(reexport.to_string()); - - // First, resolve relate reexport specifier - let resolved_reexport = resolve( - &reexport, - &referrer, - // FIXME(bartlomieju): check if these conditions are okay, probably - // should be `deno-require`, because `deno` is already used in `esm_resolver.rs` - &["deno", "require", "default"], - NodeResolutionMode::Execution, - npm_resolver, - permissions, - )?; - let reexport_specifier = - ModuleSpecifier::from_file_path(resolved_reexport).unwrap(); - // Second, read the source code from disk - let reexport_file = file_fetcher - .get_source(&reexport_specifier) - .ok_or_else(|| { - anyhow!( - "Could not find '{}' ({}) referenced from {}", - reexport, - reexport_specifier, - referrer - ) - })?; - - { - let analysis = perform_cjs_analysis( - node_analysis_cache, - reexport_specifier.as_str(), - reexport_file.media_type, - reexport_file.source.to_string(), - )?; - - for reexport in analysis.reexports { - reexports_to_handle.push_back((reexport, reexport_specifier.clone())); - } - - all_exports.extend( - analysis - .exports - .into_iter() - .filter(|e| e.as_str() != "default"), - ); - } - } - - source.push(format!( - "const mod = require(\"{}\");", - specifier - .to_file_path() - .unwrap() - .to_str() - .unwrap() - .replace('\\', "\\\\") - .replace('\'', "\\\'") - .replace('\"', "\\\"") - )); - - for export in &all_exports { - if export.as_str() != "default" { - add_export( - &mut source, - export, - &format!("mod[\"{export}\"]"), - &mut temp_var_count, - ); - } - } - - source.push("export default mod;".to_string()); - - let translated_source = source.join("\n"); - Ok(translated_source) -} - -fn resolve( - specifier: &str, - referrer: &ModuleSpecifier, - conditions: &[&str], - mode: NodeResolutionMode, - npm_resolver: &dyn RequireNpmResolver, - permissions: &mut dyn NodePermissions, -) -> Result<PathBuf, AnyError> { - if specifier.starts_with('/') { - todo!(); - } - - let referrer_path = referrer.to_file_path().unwrap(); - if specifier.starts_with("./") || specifier.starts_with("../") { - if let Some(parent) = referrer_path.parent() { - return file_extension_probe(parent.join(specifier), &referrer_path); - } else { - todo!(); - } - } - - // We've got a bare specifier or maybe bare_specifier/blah.js" - - let (package_specifier, package_subpath) = - parse_specifier(specifier).unwrap(); - - // todo(dsherret): use not_found error on not found here - let module_dir = npm_resolver.resolve_package_folder_from_package( - package_specifier.as_str(), - &referrer_path, - mode, - )?; - - let package_json_path = module_dir.join("package.json"); - if package_json_path.exists() { - let package_json = PackageJson::load::<RealFs>( - npm_resolver, - permissions, - package_json_path.clone(), - )?; - - if let Some(exports) = &package_json.exports { - return package_exports_resolve::<deno_node::RealFs>( - &package_json_path, - package_subpath, - exports, - referrer, - NodeModuleKind::Esm, - conditions, - mode, - npm_resolver, - permissions, - ); - } - - // old school - if package_subpath != "." { - let d = module_dir.join(package_subpath); - if let Ok(m) = d.metadata() { - if m.is_dir() { - // subdir might have a package.json that specifies the entrypoint - let package_json_path = d.join("package.json"); - if package_json_path.exists() { - let package_json = PackageJson::load::<RealFs>( - npm_resolver, - permissions, - package_json_path, - )?; - if let Some(main) = package_json.main(NodeModuleKind::Cjs) { - return Ok(d.join(main).clean()); - } - } - - return Ok(d.join("index.js").clean()); - } - } - return file_extension_probe(d, &referrer_path); - } else if let Some(main) = package_json.main(NodeModuleKind::Cjs) { - return Ok(module_dir.join(main).clean()); - } else { - return Ok(module_dir.join("index.js").clean()); - } - } - Err(not_found(specifier, &referrer_path)) -} - -fn parse_specifier(specifier: &str) -> Option<(String, String)> { - 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(|i| i + index + 1); - } 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 None; - } - - let package_subpath = if let Some(index) = separator_index { - format!(".{}", specifier.chars().skip(index).collect::<String>()) - } else { - ".".to_string() - }; - - Some((package_name, package_subpath)) -} - fn to_file_path(url: &ModuleSpecifier) -> PathBuf { url .to_file_path() @@ -958,39 +585,6 @@ fn is_relative_specifier(specifier: &str) -> bool { false } -fn file_extension_probe( - p: PathBuf, - referrer: &Path, -) -> Result<PathBuf, AnyError> { - let p = p.clean(); - if p.exists() { - let file_name = p.file_name().unwrap(); - let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap())); - if p_js.exists() && p_js.is_file() { - return Ok(p_js); - } else if p.is_dir() { - return Ok(p.join("index.js")); - } else { - return Ok(p); - } - } else if let Some(file_name) = p.file_name() { - let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap())); - if p_js.exists() && p_js.is_file() { - return Ok(p_js); - } - } - Err(not_found(&p.to_string_lossy(), referrer)) -} - -fn not_found(path: &str, referrer: &Path) -> AnyError { - let msg = format!( - "[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"", - path, - referrer.to_string_lossy() - ); - std::io::Error::new(std::io::ErrorKind::NotFound, msg).into() -} - #[cfg(test)] mod tests { use deno_core::serde_json::json; @@ -998,36 +592,6 @@ mod tests { use super::*; #[test] - fn test_add_export() { - let mut temp_var_count = 0; - let mut source = vec![]; - - let exports = vec!["static", "server", "app", "dashed-export"]; - for export in exports { - add_export(&mut source, export, "init", &mut temp_var_count); - } - assert_eq!( - source, - vec![ - "const __deno_export_1__ = init;".to_string(), - "export { __deno_export_1__ as \"static\" };".to_string(), - "export const server = init;".to_string(), - "export const app = init;".to_string(), - "const __deno_export_2__ = init;".to_string(), - "export { __deno_export_2__ as \"dashed-export\" };".to_string(), - ] - ) - } - - #[test] - fn test_parse_specifier() { - assert_eq!( - parse_specifier("@some-package/core/actions"), - Some(("@some-package/core".to_string(), "./actions".to_string())) - ); - } - - #[test] fn test_resolve_bin_entry_value() { // should resolve the specified value let value = json!({ |