diff options
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/analyze.rs | 213 |
1 files changed, 148 insertions, 65 deletions
diff --git a/ext/node/analyze.rs b/ext/node/analyze.rs index 2d1c169fc..b7adfd5ce 100644 --- a/ext/node/analyze.rs +++ b/ext/node/analyze.rs @@ -1,12 +1,15 @@ // Copyright 2018-2024 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 deno_core::anyhow; use deno_core::anyhow::Context; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::FutureExt; +use deno_core::futures::StreamExt; use deno_core::ModuleSpecifier; use once_cell::sync::Lazy; @@ -82,16 +85,15 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer> NodeCodeTranslator<TCjsCodeAnalyzer> { /// If successful a source code for equivalent ES module is returned. pub async fn translate_cjs_to_esm( &self, - specifier: &ModuleSpecifier, + entry_specifier: &ModuleSpecifier, source: Option<String>, permissions: &dyn NodePermissions, ) -> Result<String, AnyError> { let mut temp_var_count = 0; - let mut handled_reexports: HashSet<ModuleSpecifier> = HashSet::default(); let analysis = self .cjs_code_analyzer - .analyze_cjs(specifier, source) + .analyze_cjs(entry_specifier, source) .await?; let analysis = match analysis { @@ -105,73 +107,30 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer> NodeCodeTranslator<TCjsCodeAnalyzer> { .to_string(), ]; - 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())); - } - - // todo(dsherret): we could run this analysis concurrently in a FuturesOrdered - while let Some((reexport, referrer)) = reexports_to_handle.pop_front() { - // First, resolve the reexport specifier - let reexport_specifier = 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, - )?; - - if !handled_reexports.insert(reexport_specifier.clone()) { - continue; - } - - // Second, resolve its exports and re-exports - let analysis = self - .cjs_code_analyzer - .analyze_cjs(&reexport_specifier, None) - .await - .with_context(|| { - format!( - "Could not load '{}' ({}) referenced from {}", - reexport, reexport_specifier, referrer - ) - })?; - let analysis = match analysis { - CjsAnalysis::Esm(_) => { - // todo(dsherret): support this once supporting requiring ES modules - return Err(anyhow::anyhow!( - "Cannot require ES module '{}' from '{}'", - reexport_specifier, - specifier - )); - } - CjsAnalysis::Cjs(analysis) => analysis, - }; + let mut all_exports = analysis.exports.into_iter().collect::<HashSet<_>>(); - for reexport in analysis.reexports { - reexports_to_handle.push_back((reexport, reexport_specifier.clone())); + if !analysis.reexports.is_empty() { + let mut errors = Vec::new(); + self + .analyze_reexports( + entry_specifier, + analysis.reexports, + permissions, + &mut all_exports, + &mut errors, + ) + .await; + + // surface errors afterwards in a deterministic way + if !errors.is_empty() { + errors.sort_by_cached_key(|e| e.to_string()); + return Err(errors.remove(0)); } - - all_exports.extend( - analysis - .exports - .into_iter() - .filter(|e| e.as_str() != "default"), - ); } source.push(format!( "const mod = require(\"{}\");", - specifier + entry_specifier .to_file_path() .unwrap() .to_str() @@ -198,6 +157,130 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer> NodeCodeTranslator<TCjsCodeAnalyzer> { Ok(translated_source) } + async fn analyze_reexports<'a>( + &'a self, + entry_specifier: &url::Url, + reexports: Vec<String>, + permissions: &dyn NodePermissions, + all_exports: &mut HashSet<String>, + // this goes through the modules concurrently, so collect + // the errors in order to be deterministic + errors: &mut Vec<anyhow::Error>, + ) { + struct Analysis { + reexport_specifier: url::Url, + referrer: url::Url, + analysis: CjsAnalysis, + } + + type AnalysisFuture<'a> = LocalBoxFuture<'a, Result<Analysis, AnyError>>; + + let mut handled_reexports: HashSet<ModuleSpecifier> = HashSet::default(); + handled_reexports.insert(entry_specifier.clone()); + let mut analyze_futures: FuturesUnordered<AnalysisFuture<'a>> = + FuturesUnordered::new(); + let cjs_code_analyzer = &self.cjs_code_analyzer; + let mut handle_reexports = + |referrer: url::Url, + reexports: Vec<String>, + analyze_futures: &mut FuturesUnordered<AnalysisFuture<'a>>, + errors: &mut Vec<anyhow::Error>| { + // 1. Resolve the re-exports and start a future to analyze each one + for reexport in reexports { + let result = 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 = match result { + Ok(specifier) => specifier, + Err(err) => { + errors.push(err); + continue; + } + }; + + if !handled_reexports.insert(reexport_specifier.clone()) { + continue; + } + + let referrer = referrer.clone(); + let future = async move { + let analysis = cjs_code_analyzer + .analyze_cjs(&reexport_specifier, None) + .await + .with_context(|| { + format!( + "Could not load '{}' ({}) referenced from {}", + reexport, reexport_specifier, referrer + ) + })?; + + Ok(Analysis { + reexport_specifier, + referrer, + analysis, + }) + } + .boxed_local(); + analyze_futures.push(future); + } + }; + + handle_reexports( + entry_specifier.clone(), + reexports, + &mut analyze_futures, + errors, + ); + + while let Some(analysis_result) = analyze_futures.next().await { + // 2. Look at the analysis result and resolve its exports and re-exports + let Analysis { + reexport_specifier, + referrer, + analysis, + } = match analysis_result { + Ok(analysis) => analysis, + Err(err) => { + errors.push(err); + continue; + } + }; + match analysis { + CjsAnalysis::Esm(_) => { + // todo(dsherret): support this once supporting requiring ES modules + errors.push(anyhow::anyhow!( + "Cannot require ES module '{}' from '{}'", + reexport_specifier, + referrer, + )); + } + CjsAnalysis::Cjs(analysis) => { + if !analysis.reexports.is_empty() { + handle_reexports( + reexport_specifier.clone(), + analysis.reexports, + &mut analyze_futures, + errors, + ); + } + + all_exports.extend( + analysis + .exports + .into_iter() + .filter(|e| e.as_str() != "default"), + ); + } + } + } + } + fn resolve( &self, specifier: &str, |