diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-04-14 16:22:33 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-14 16:22:33 -0400 |
commit | 136dce67cec749dce5989ea29e88359ef79a0045 (patch) | |
tree | 38e96bbbf22dc06cdba418a35467b215f1335549 /cli/node/mod.rs | |
parent | a4111442191fff300132259752e6d2d5613d1871 (diff) |
refactor: break up `ProcState` (#18707)
1. Breaks up functionality within `ProcState` into several other structs
to break out the responsibilities (`ProcState` is only a data struct
now).
2. Moves towards being able to inject dependencies more easily and have
functionality only require what it needs.
3. Exposes `Arc<T>` around the "service structs" instead of it being
embedded within them. The idea behind embedding them was to reduce the
verbosity of needing to pass around `Arc<...>`, but I don't think it was
exactly working and as we move more of these structs to be more
injectable I don't think the extra verbosity will be a big deal.
Diffstat (limited to 'cli/node/mod.rs')
-rw-r--r-- | cli/node/mod.rs | 460 |
1 files changed, 12 insertions, 448 deletions
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!({ |