diff --git a/Cargo.lock b/Cargo.lock index 75a302af65..4e90ccd5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1609,9 +1609,9 @@ dependencies = [ [[package]] name = "deno_ast" -version = "0.46.0" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd3b6e14e5b1235dd613d9f5d955d7a80dec6de0fc00fa34b5d0ef5ca0a9ddb" +checksum = "88393f34aaba238da6a3694aef7e046eec4d261c3bf98dc6669d397f1c274e5e" dependencies = [ "base64 0.21.7", "deno_error", diff --git a/Cargo.toml b/Cargo.toml index 3b7a05aec5..834a8f4d04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ license = "MIT" repository = "https://github.com/denoland/deno" [workspace.dependencies] -deno_ast = { version = "=0.46.0", features = ["transpiling"] } +deno_ast = { version = "=0.46.1", features = ["transpiling"] } deno_core = { version = "0.340.0" } deno_bench_util = { version = "0.188.0", path = "./bench_util" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f96127a262..778fe1888e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -67,7 +67,7 @@ winapi.workspace = true winres.workspace = true [dependencies] -deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } +deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit", "utils"] } deno_cache_dir = { workspace = true, features = ["sync"] } deno_config = { workspace = true, features = ["sync", "workspace"] } deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } diff --git a/cli/cache/node.rs b/cli/cache/node.rs index 89e372de43..d5d132649d 100644 --- a/cli/cache/node.rs +++ b/cli/cache/node.rs @@ -137,6 +137,8 @@ impl NodeAnalysisCacheInner { #[cfg(test)] mod test { + use deno_ast::ModuleExportsAndReExports; + use super::*; #[test] @@ -148,10 +150,10 @@ mod test { .get_cjs_analysis("file.js", CacheDBHash::new(2)) .unwrap() .is_none()); - let cjs_analysis = CliCjsAnalysis::Cjs { + let cjs_analysis = CliCjsAnalysis::Cjs(ModuleExportsAndReExports { exports: vec!["export1".to_string()], reexports: vec!["re-export1".to_string()], - }; + }); cache .set_cjs_analysis("file.js", CacheDBHash::new(2), &cjs_analysis) .unwrap(); diff --git a/cli/factory.rs b/cli/factory.rs index 660280c498..f8248ef828 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -80,6 +80,7 @@ use crate::http_util::HttpClientProvider; use crate::module_loader::CliModuleLoaderFactory; use crate::module_loader::ModuleLoadPreparer; use crate::node::CliCjsCodeAnalyzer; +use crate::node::CliCjsModuleExportAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::node::CliNodeResolver; use crate::node::CliPackageJsonResolver; @@ -261,6 +262,7 @@ impl Deferred { struct CliFactoryServices { blob_store: Deferred>, caches: Deferred>, + cjs_module_export_analyzer: Deferred>, cjs_tracker: Deferred>, cli_options: Deferred>, code_cache: Deferred>, @@ -804,6 +806,28 @@ impl CliFactory { Ok(()) } + pub async fn cjs_module_export_analyzer( + &self, + ) -> Result<&Arc, AnyError> { + self + .services + .cjs_module_export_analyzer + .get_or_try_init_async(async { + let node_resolver = self.node_resolver().await?.clone(); + let cjs_code_analyzer = self.create_cjs_code_analyzer()?; + + Ok(Arc::new(CliCjsModuleExportAnalyzer::new( + cjs_code_analyzer, + self.in_npm_pkg_checker()?.clone(), + node_resolver, + self.npm_resolver().await?.clone(), + self.pkg_json_resolver()?.clone(), + self.sys(), + ))) + }) + .await + } + pub async fn node_code_translator( &self, ) -> Result<&Arc, AnyError> { @@ -811,16 +835,9 @@ impl CliFactory { .services .node_code_translator .get_or_try_init_async(async { - let node_resolver = self.node_resolver().await?.clone(); - let cjs_code_analyzer = self.create_cjs_code_analyzer()?; - + let module_export_analyzer = self.cjs_module_export_analyzer().await?; Ok(Arc::new(NodeCodeTranslator::new( - cjs_code_analyzer, - self.in_npm_pkg_checker()?.clone(), - node_resolver, - self.npm_resolver().await?.clone(), - self.pkg_json_resolver()?.clone(), - self.sys(), + module_export_analyzer.clone(), ))) }) .await @@ -1025,7 +1042,7 @@ impl CliFactory { ) -> Result { let cli_options = self.cli_options()?; Ok(DenoCompileBinaryWriter::new( - self.create_cjs_code_analyzer()?, + self.cjs_module_export_analyzer().await?, self.cjs_tracker()?, self.cli_options()?, self.deno_dir()?, diff --git a/cli/lib/standalone/binary.rs b/cli/lib/standalone/binary.rs index 516d120a20..aa72a881b0 100644 --- a/cli/lib/standalone/binary.rs +++ b/cli/lib/standalone/binary.rs @@ -9,7 +9,6 @@ use deno_runtime::deno_permissions::PermissionsOptions; use deno_runtime::deno_telemetry::OtelConfig; use deno_semver::Version; use indexmap::IndexMap; -use node_resolver::analyze::CjsAnalysisExports; use serde::Deserialize; use serde::Serialize; use url::Url; @@ -130,7 +129,8 @@ impl<'a> DenoRtDeserializable<'a> for SpecifierId { #[derive(Deserialize, Serialize)] pub enum CjsExportAnalysisEntry { Esm, - Cjs(CjsAnalysisExports), + Cjs(Vec), + Error(String), } const HAS_TRANSPILED_FLAG: u8 = 1 << 0; diff --git a/cli/node.rs b/cli/node.rs index 2e87035cae..eb864f2dcb 100644 --- a/cli/node.rs +++ b/cli/node.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::sync::Arc; use deno_ast::MediaType; +use deno_ast::ModuleExportsAndReExports; use deno_ast::ModuleSpecifier; use deno_error::JsErrorBox; use deno_graph::ParsedSourceStore; @@ -12,6 +13,8 @@ use deno_runtime::deno_fs; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; use node_resolver::analyze::CjsAnalysisExports; use node_resolver::analyze::CjsCodeAnalyzer; +use node_resolver::analyze::CjsModuleExportAnalyzer; +use node_resolver::analyze::EsmAnalysisMode; use node_resolver::analyze::NodeCodeTranslator; use node_resolver::DenoIsBuiltInNodeModuleChecker; use serde::Deserialize; @@ -24,6 +27,13 @@ use crate::npm::CliNpmResolver; use crate::resolver::CliCjsTracker; use crate::sys::CliSys; +pub type CliCjsModuleExportAnalyzer = CjsModuleExportAnalyzer< + CliCjsCodeAnalyzer, + DenoInNpmPackageChecker, + DenoIsBuiltInNodeModuleChecker, + CliNpmResolver, + CliSys, +>; pub type CliNodeCodeTranslator = NodeCodeTranslator< CliCjsCodeAnalyzer, DenoInNpmPackageChecker, @@ -42,11 +52,11 @@ pub type CliPackageJsonResolver = node_resolver::PackageJsonResolver; pub enum CliCjsAnalysis { /// The module was found to be an ES module. Esm, + /// The module was found to be an ES module and + /// it was analyzed for imports and exports. + EsmAnalysis(ModuleExportsAndReExports), /// The module was CJS. - Cjs { - exports: Vec, - reexports: Vec, - }, + Cjs(ModuleExportsAndReExports), } pub struct CliCjsCodeAnalyzer { @@ -75,6 +85,7 @@ impl CliCjsCodeAnalyzer { &self, specifier: &ModuleSpecifier, source: &str, + esm_analysis_mode: EsmAnalysisMode, ) -> Result { let source_hash = CacheDBHash::from_hashable(source); if let Some(analysis) = @@ -85,17 +96,16 @@ impl CliCjsCodeAnalyzer { let media_type = MediaType::from_specifier(specifier); if media_type == MediaType::Json { - return Ok(CliCjsAnalysis::Cjs { - exports: vec![], - reexports: vec![], - }); + return Ok(CliCjsAnalysis::Cjs(Default::default())); } let cjs_tracker = self.cjs_tracker.clone(); let is_maybe_cjs = cjs_tracker .is_maybe_cjs(specifier, media_type) .map_err(JsErrorBox::from_err)?; - let analysis = if is_maybe_cjs { + let analysis = if is_maybe_cjs + || esm_analysis_mode == EsmAnalysisMode::SourceImportsAndExports + { let maybe_parsed_source = self .parsed_source_cache .as_ref() @@ -118,22 +128,27 @@ impl CliCjsCodeAnalyzer { }) }) .map_err(JsErrorBox::from_err)?; - let is_script = parsed_source.compute_is_script(); - let is_cjs = cjs_tracker - .is_cjs_with_known_is_script( - parsed_source.specifier(), - media_type, - is_script, - ) - .map_err(JsErrorBox::from_err)?; + let is_script = is_maybe_cjs && parsed_source.compute_is_script(); + let is_cjs = is_maybe_cjs + && cjs_tracker + .is_cjs_with_known_is_script( + parsed_source.specifier(), + media_type, + is_script, + ) + .map_err(JsErrorBox::from_err)?; if is_cjs { let analysis = parsed_source.analyze_cjs(); - Ok(CliCjsAnalysis::Cjs { - exports: analysis.exports, - reexports: analysis.reexports, - }) + Ok(CliCjsAnalysis::Cjs(analysis)) } else { - Ok(CliCjsAnalysis::Esm) + match esm_analysis_mode { + EsmAnalysisMode::SourceOnly => Ok(CliCjsAnalysis::Esm), + EsmAnalysisMode::SourceImportsAndExports => { + Ok(CliCjsAnalysis::EsmAnalysis( + parsed_source.analyze_es_runtime_exports(), + )) + } + } } } }) @@ -157,6 +172,7 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { &self, specifier: &ModuleSpecifier, source: Option>, + esm_analysis_mode: EsmAnalysisMode, ) -> Result, JsErrorBox> { let source = match source { Some(source) => source, @@ -180,13 +196,22 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { } } }; - let analysis = self.inner_cjs_analysis(specifier, &source).await?; + let analysis = self + .inner_cjs_analysis(specifier, &source, esm_analysis_mode) + .await?; match analysis { - CliCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source)), - CliCjsAnalysis::Cjs { exports, reexports } => { + CliCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source, None)), + CliCjsAnalysis::EsmAnalysis(analysis) => Ok(ExtNodeCjsAnalysis::Esm( + source, + Some(CjsAnalysisExports { + exports: analysis.exports, + reexports: analysis.reexports, + }), + )), + CliCjsAnalysis::Cjs(analysis) => { Ok(ExtNodeCjsAnalysis::Cjs(CjsAnalysisExports { - exports, - reexports, + exports: analysis.exports, + reexports: analysis.reexports, })) } } diff --git a/cli/rt/node.rs b/cli/rt/node.rs index 5d2ba5c4e8..dca6cbfab4 100644 --- a/cli/rt/node.rs +++ b/cli/rt/node.rs @@ -13,6 +13,7 @@ use deno_resolver::npm::NpmReqResolver; use deno_runtime::deno_fs::FileSystem; use node_resolver::analyze::CjsAnalysis; use node_resolver::analyze::CjsAnalysisExports; +use node_resolver::analyze::EsmAnalysisMode; use node_resolver::analyze::NodeCodeTranslator; use node_resolver::DenoIsBuiltInNodeModuleChecker; @@ -96,11 +97,17 @@ impl CjsCodeAnalyzer { match data { CjsExportAnalysisEntry::Esm => { cjs_tracker.set_is_known_script(specifier, false); - CjsAnalysis::Esm(source) + CjsAnalysis::Esm(source, None) } - CjsExportAnalysisEntry::Cjs(analysis) => { + CjsExportAnalysisEntry::Cjs(exports) => { cjs_tracker.set_is_known_script(specifier, true); - CjsAnalysis::Cjs(analysis) + CjsAnalysis::Cjs(CjsAnalysisExports { + exports, + reexports: Vec::new(), // already resolved + }) + } + CjsExportAnalysisEntry::Error(err) => { + return Err(JsErrorBox::generic(err)); } } } @@ -119,11 +126,11 @@ impl CjsCodeAnalyzer { } } // assume ESM as we don't have access to swc here - CjsAnalysis::Esm(source) + CjsAnalysis::Esm(source, None) } } } else { - CjsAnalysis::Esm(source) + CjsAnalysis::Esm(source, None) }; Ok(analysis) @@ -136,6 +143,7 @@ impl node_resolver::analyze::CjsCodeAnalyzer for CjsCodeAnalyzer { &self, specifier: &Url, source: Option>, + _esm_analysis_mode: EsmAnalysisMode, ) -> Result, JsErrorBox> { let source = match source { Some(source) => source, diff --git a/cli/rt/run.rs b/cli/rt/run.rs index e78851caaf..9c7091fa76 100644 --- a/cli/rt/run.rs +++ b/cli/rt/run.rs @@ -74,6 +74,7 @@ use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; +use node_resolver::analyze::CjsModuleExportAnalyzer; use node_resolver::analyze::NodeCodeTranslator; use node_resolver::cache::NodeResolutionSys; use node_resolver::errors::ClosestPkgJsonError; @@ -154,18 +155,9 @@ impl ModuleLoader for EmbeddedModuleLoader { &self, raw_specifier: &str, referrer: &str, - kind: ResolutionKind, + _kind: ResolutionKind, ) -> Result { let referrer = if referrer == "." { - if kind != ResolutionKind::MainModule { - return Err( - JsErrorBox::generic(format!( - "Expected to resolve main module, got {:?} instead.", - kind - )) - .into(), - ); - } let current_dir = std::env::current_dir().unwrap(); deno_core::resolve_path(".", ¤t_dir) .map_err(JsErrorBox::from_err)? @@ -815,7 +807,7 @@ pub async fn run( })); let cjs_esm_code_analyzer = CjsCodeAnalyzer::new(cjs_tracker.clone(), modules.clone(), sys.clone()); - let node_code_translator = Arc::new(NodeCodeTranslator::new( + let cjs_module_export_analyzer = Arc::new(CjsModuleExportAnalyzer::new( cjs_esm_code_analyzer, in_npm_pkg_checker, node_resolver.clone(), @@ -823,6 +815,8 @@ pub async fn run( pkg_json_resolver.clone(), sys.clone(), )); + let node_code_translator = + Arc::new(NodeCodeTranslator::new(cjs_module_export_analyzer)); let workspace_resolver = { let import_map = match metadata.workspace_resolver.import_map { Some(import_map) => Some( diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index c1eaebe926..4d730b9644 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -52,8 +52,7 @@ use deno_path_util::url_from_directory_path; use deno_path_util::url_to_file_path; use deno_resolver::workspace::WorkspaceResolver; use indexmap::IndexMap; -use node_resolver::analyze::CjsAnalysis; -use node_resolver::analyze::CjsCodeAnalyzer; +use node_resolver::analyze::ResolvedCjsAnalysis; use super::virtual_fs::output_vfs; use crate::args::CliOptions; @@ -61,7 +60,7 @@ use crate::args::CompileFlags; use crate::cache::DenoDir; use crate::emit::Emitter; use crate::http_util::HttpClientProvider; -use crate::node::CliCjsCodeAnalyzer; +use crate::node::CliCjsModuleExportAnalyzer; use crate::npm::CliNpmResolver; use crate::resolver::CliCjsTracker; use crate::sys::CliSys; @@ -190,7 +189,7 @@ pub struct WriteBinOptions<'a> { } pub struct DenoCompileBinaryWriter<'a> { - cjs_code_analyzer: CliCjsCodeAnalyzer, + cjs_module_export_analyzer: &'a CliCjsModuleExportAnalyzer, cjs_tracker: &'a CliCjsTracker, cli_options: &'a CliOptions, deno_dir: &'a DenoDir, @@ -204,7 +203,7 @@ pub struct DenoCompileBinaryWriter<'a> { impl<'a> DenoCompileBinaryWriter<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - cjs_code_analyzer: CliCjsCodeAnalyzer, + cjs_module_export_analyzer: &'a CliCjsModuleExportAnalyzer, cjs_tracker: &'a CliCjsTracker, cli_options: &'a CliOptions, deno_dir: &'a DenoDir, @@ -215,7 +214,7 @@ impl<'a> DenoCompileBinaryWriter<'a> { npm_system_info: NpmSystemInfo, ) -> Self { Self { - cjs_code_analyzer, + cjs_module_export_analyzer, cjs_tracker, cli_options, deno_dir, @@ -423,16 +422,18 @@ impl<'a> DenoCompileBinaryWriter<'a> { m.is_script, )? { let cjs_analysis = self - .cjs_code_analyzer - .analyze_cjs( + .cjs_module_export_analyzer + .analyze_all_exports( module.specifier(), Some(Cow::Borrowed(m.source.as_ref())), ) .await?; maybe_cjs_analysis = Some(match cjs_analysis { - CjsAnalysis::Esm(_) => CjsExportAnalysisEntry::Esm, - CjsAnalysis::Cjs(exports) => { - CjsExportAnalysisEntry::Cjs(exports) + ResolvedCjsAnalysis::Esm(_) => CjsExportAnalysisEntry::Esm, + ResolvedCjsAnalysis::Cjs(exports) => { + CjsExportAnalysisEntry::Cjs( + exports.into_iter().collect::>(), + ) } }); } else { @@ -544,26 +545,24 @@ impl<'a> DenoCompileBinaryWriter<'a> { .file_bytes(file.offset) .map(|text| String::from_utf8_lossy(text)); let cjs_analysis_result = self - .cjs_code_analyzer - .analyze_cjs(&specifier, maybe_source) + .cjs_module_export_analyzer + .analyze_all_exports(&specifier, maybe_source) .await; - let maybe_analysis = match cjs_analysis_result { - Ok(CjsAnalysis::Esm(_)) => Some(CjsExportAnalysisEntry::Esm), - Ok(CjsAnalysis::Cjs(exports)) => { - Some(CjsExportAnalysisEntry::Cjs(exports)) + let analysis = match cjs_analysis_result { + Ok(ResolvedCjsAnalysis::Esm(_)) => CjsExportAnalysisEntry::Esm, + Ok(ResolvedCjsAnalysis::Cjs(exports)) => { + CjsExportAnalysisEntry::Cjs(exports.into_iter().collect::>()) } Err(err) => { log::debug!( - "Ignoring cjs export analysis for '{}': {}", + "Had cjs export analysis error for '{}': {}", specifier, err ); - None + CjsExportAnalysisEntry::Error(err.to_string()) } }; - if let Some(analysis) = &maybe_analysis { - to_add.push((file_path, bincode::serialize(analysis)?)); - } + to_add.push((file_path, bincode::serialize(&analysis)?)); } } for (file_path, analysis) in to_add { diff --git a/resolvers/node/analyze.rs b/resolvers/node/analyze.rs index 79d442ffd2..5c402fbb3c 100644 --- a/resolvers/node/analyze.rs +++ b/resolvers/node/analyze.rs @@ -36,7 +36,7 @@ use crate::UrlOrPathRef; pub enum CjsAnalysis<'a> { /// File was found to be an ES module and the translator should /// load the code as ESM. - Esm(Cow<'a, str>), + Esm(Cow<'a, str>, Option), Cjs(CjsAnalysisExports), } @@ -46,6 +46,13 @@ pub struct CjsAnalysisExports { pub reexports: Vec, } +/// What parts of an ES module should be analyzed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EsmAnalysisMode { + SourceOnly, + SourceImportsAndExports, +} + /// Code analyzer for CJS and ESM files. #[async_trait::async_trait(?Send)] pub trait CjsCodeAnalyzer { @@ -60,31 +67,33 @@ pub trait CjsCodeAnalyzer { &self, specifier: &Url, maybe_source: Option>, + esm_analysis_mode: EsmAnalysisMode, ) -> Result, JsErrorBox>; } -#[derive(Debug, thiserror::Error, deno_error::JsError)] -pub enum TranslateCjsToEsmError { - #[class(inherit)] - #[error(transparent)] - CjsCodeAnalysis(JsErrorBox), - #[class(inherit)] - #[error(transparent)] - ExportAnalysis(JsErrorBox), +pub enum ResolvedCjsAnalysis<'a> { + Esm(Cow<'a, str>), + Cjs(BTreeSet), } -#[derive(Debug, thiserror::Error, deno_error::JsError)] -#[class(generic)] -#[error("Could not load '{reexport}' ({reexport_specifier}) referenced from {referrer}")] -pub struct CjsAnalysisCouldNotLoadError { - reexport: String, - reexport_specifier: Url, - referrer: Url, - #[source] - source: JsErrorBox, -} +#[allow(clippy::disallowed_types)] +pub type CjsModuleExportAnalyzerRc< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, +> = crate::sync::MaybeArc< + CjsModuleExportAnalyzer< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + >, +>; -pub struct NodeCodeTranslator< +pub struct CjsModuleExportAnalyzer< TCjsCodeAnalyzer: CjsCodeAnalyzer, TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, @@ -111,7 +120,7 @@ impl< TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: FsCanonicalize + FsMetadata + FsRead, > - NodeCodeTranslator< + CjsModuleExportAnalyzer< TCjsCodeAnalyzer, TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, @@ -142,36 +151,24 @@ impl< } } - /// 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 async fn translate_cjs_to_esm<'a>( + pub async fn analyze_all_exports<'a>( &self, entry_specifier: &Url, source: Option>, - ) -> Result, TranslateCjsToEsmError> { - let mut temp_var_count = 0; - + ) -> Result, TranslateCjsToEsmError> { let analysis = self .cjs_code_analyzer - .analyze_cjs(entry_specifier, source) + .analyze_cjs(entry_specifier, source, EsmAnalysisMode::SourceOnly) .await .map_err(TranslateCjsToEsmError::CjsCodeAnalysis)?; let analysis = match analysis { - CjsAnalysis::Esm(source) => return Ok(source), + CjsAnalysis::Esm(source, _) => { + return Ok(ResolvedCjsAnalysis::Esm(source)) + } CjsAnalysis::Cjs(analysis) => analysis, }; - let mut source = vec![ - r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module"; - const require = __internalCreateRequire(import.meta.url);"# - .to_string(), - ]; - // use a BTreeSet to make the output deterministic for v8's code cache let mut all_exports = analysis.exports.into_iter().collect::>(); @@ -193,38 +190,7 @@ impl< } } - source.push(format!( - r#"let mod; - if (import.meta.main) {{ - mod = __internalModule._load("{0}", null, true) - }} else {{ - mod = require("{0}"); - }}"#, - url_to_file_path(entry_specifier) - .unwrap() - .to_str() - .unwrap() - .replace('\\', "\\\\") - .replace('\'', "\\\'") - .replace('\"', "\\\"") - )); - - for export in &all_exports { - if !matches!(export.as_str(), "default" | "module.exports") { - add_export( - &mut source, - export, - &format!("mod[{}]", to_double_quote_string(export)), - &mut temp_var_count, - ); - } - } - - source.push("export default mod;".to_string()); - add_export(&mut source, "module.exports", "mod", &mut temp_var_count); - - let translated_source = source.join("\n"); - Ok(Cow::Owned(translated_source)) + Ok(ResolvedCjsAnalysis::Cjs(all_exports)) } #[allow(clippy::needless_lifetimes)] @@ -239,7 +205,6 @@ impl< ) { struct Analysis { reexport_specifier: url::Url, - referrer: url::Url, analysis: CjsAnalysis<'static>, } @@ -288,7 +253,11 @@ impl< let referrer = referrer.clone(); let future = async move { let analysis = cjs_code_analyzer - .analyze_cjs(&reexport_specifier, None) + .analyze_cjs( + &reexport_specifier, + None, + EsmAnalysisMode::SourceImportsAndExports, + ) .await .map_err(|source| { JsErrorBox::from_err(CjsAnalysisCouldNotLoadError { @@ -301,7 +270,6 @@ impl< Ok(Analysis { reexport_specifier, - referrer, analysis, }) } @@ -321,7 +289,6 @@ impl< // 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, @@ -331,14 +298,7 @@ impl< } }; match analysis { - CjsAnalysis::Esm(_) => { - // todo(dsherret): support this once supporting requiring ES modules - errors.push(JsErrorBox::generic(format!( - "Cannot require ES module '{}' from '{}'", - reexport_specifier, referrer, - ))); - } - CjsAnalysis::Cjs(analysis) => { + CjsAnalysis::Cjs(analysis) | CjsAnalysis::Esm(_, Some(analysis)) => { if !analysis.reexports.is_empty() { handle_reexports( reexport_specifier.clone(), @@ -355,6 +315,10 @@ impl< .filter(|e| e.as_str() != "default"), ); } + CjsAnalysis::Esm(_, None) => { + // should not hit this due to EsmAnalysisMode::SourceImportsAndExports + debug_assert!(false); + } } } } @@ -526,6 +490,137 @@ impl< } } +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum TranslateCjsToEsmError { + #[class(inherit)] + #[error(transparent)] + CjsCodeAnalysis(JsErrorBox), + #[class(inherit)] + #[error(transparent)] + ExportAnalysis(JsErrorBox), +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +#[class(generic)] +#[error("Could not load '{reexport}' ({reexport_specifier}) referenced from {referrer}")] +pub struct CjsAnalysisCouldNotLoadError { + reexport: String, + reexport_specifier: Url, + referrer: Url, + #[source] + source: JsErrorBox, +} + +pub struct NodeCodeTranslator< + TCjsCodeAnalyzer: CjsCodeAnalyzer, + TInNpmPackageChecker: InNpmPackageChecker, + TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver: NpmPackageFolderResolver, + TSys: FsCanonicalize + FsMetadata + FsRead, +> { + module_export_analyzer: CjsModuleExportAnalyzerRc< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + >, +} + +impl< + TCjsCodeAnalyzer: CjsCodeAnalyzer, + TInNpmPackageChecker: InNpmPackageChecker, + TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver: NpmPackageFolderResolver, + TSys: FsCanonicalize + FsMetadata + FsRead, + > + NodeCodeTranslator< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + > +{ + pub fn new( + module_export_analyzer: CjsModuleExportAnalyzerRc< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + >, + ) -> Self { + Self { + module_export_analyzer, + } + } + + /// 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 async fn translate_cjs_to_esm<'a>( + &self, + entry_specifier: &Url, + source: Option>, + ) -> Result, TranslateCjsToEsmError> { + let analysis = self + .module_export_analyzer + .analyze_all_exports(entry_specifier, source) + .await?; + + let all_exports = match analysis { + ResolvedCjsAnalysis::Esm(source) => return Ok(source), + ResolvedCjsAnalysis::Cjs(all_exports) => all_exports, + }; + + // todo(dsherret): use capacity_builder here to remove all these heap + // allocations and make the string writing faster + let mut temp_var_count = 0; + let mut source = vec![ + r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module"; + const require = __internalCreateRequire(import.meta.url);"# + .to_string(), + ]; + + source.push(format!( + r#"let mod; + if (import.meta.main) {{ + mod = __internalModule._load("{0}", null, true) + }} else {{ + mod = require("{0}"); + }}"#, + url_to_file_path(entry_specifier) + .unwrap() + .to_str() + .unwrap() + .replace('\\', "\\\\") + .replace('\'', "\\\'") + .replace('\"', "\\\"") + )); + + for export in &all_exports { + if !matches!(export.as_str(), "default" | "module.exports") { + add_export( + &mut source, + export, + &format!("mod[{}]", to_double_quote_string(export)), + &mut temp_var_count, + ); + } + } + + source.push("export default mod;".to_string()); + add_export(&mut source, "module.exports", "mod", &mut temp_var_count); + + let translated_source = source.join("\n"); + Ok(Cow::Owned(translated_source)) + } +} + static RESERVED_WORDS: Lazy> = Lazy::new(|| { HashSet::from([ "abstract", diff --git a/tests/specs/node/require_esm_reexport_esm/__test__.jsonc b/tests/specs/node/require_esm_reexport_esm/__test__.jsonc new file mode 100644 index 0000000000..c16ec25c0a --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/__test__.jsonc @@ -0,0 +1,19 @@ +{ + "tests": { + "run": { + "args": "run -A main.mjs", + "output": "run.out" + }, + "compile": { + "tempDir": true, + "steps": [{ + "args": "compile -A --output out main.mjs", + "output": "[WILDCARD]" + }, { + "commandName": "./out", + "args": [], + "output": "run.out" + }] + } + } +} diff --git a/tests/specs/node/require_esm_reexport_esm/add.mjs b/tests/specs/node/require_esm_reexport_esm/add.mjs new file mode 100644 index 0000000000..7d658310b0 --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/add.mjs @@ -0,0 +1,3 @@ +export function add(a, b) { + return a + b; +} diff --git a/tests/specs/node/require_esm_reexport_esm/main.mjs b/tests/specs/node/require_esm_reexport_esm/main.mjs new file mode 100644 index 0000000000..8388eb5a47 --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/main.mjs @@ -0,0 +1,4 @@ +import mod, { add } from "./mod1.cjs"; +console.log(mod); +console.log(mod.add(1, 2)); +console.log(add(1, 2)); diff --git a/tests/specs/node/require_esm_reexport_esm/mod1.cjs b/tests/specs/node/require_esm_reexport_esm/mod1.cjs new file mode 100644 index 0000000000..b3ef3e24dd --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/mod1.cjs @@ -0,0 +1 @@ +module.exports = require("./mod2.cjs"); diff --git a/tests/specs/node/require_esm_reexport_esm/mod2.cjs b/tests/specs/node/require_esm_reexport_esm/mod2.cjs new file mode 100644 index 0000000000..e9346a862d --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/mod2.cjs @@ -0,0 +1 @@ +module.exports = require("./add.mjs"); diff --git a/tests/specs/node/require_esm_reexport_esm/run.out b/tests/specs/node/require_esm_reexport_esm/run.out new file mode 100644 index 0000000000..b846dfdaa3 --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm/run.out @@ -0,0 +1,3 @@ +[Module: null prototype] { add: [Function: add] } +3 +3 diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/__test__.jsonc b/tests/specs/node/require_esm_reexport_esm_module_exports/__test__.jsonc new file mode 100644 index 0000000000..c16ec25c0a --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/__test__.jsonc @@ -0,0 +1,19 @@ +{ + "tests": { + "run": { + "args": "run -A main.mjs", + "output": "run.out" + }, + "compile": { + "tempDir": true, + "steps": [{ + "args": "compile -A --output out main.mjs", + "output": "[WILDCARD]" + }, { + "commandName": "./out", + "args": [], + "output": "run.out" + }] + } + } +} diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/add.mjs b/tests/specs/node/require_esm_reexport_esm_module_exports/add.mjs new file mode 100644 index 0000000000..ce7c8bbfbe --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/add.mjs @@ -0,0 +1,5 @@ +function add(a, b) { + return a + b; +} + +export { add as "module.exports" }; diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/main.mjs b/tests/specs/node/require_esm_reexport_esm_module_exports/main.mjs new file mode 100644 index 0000000000..3237d7030a --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/main.mjs @@ -0,0 +1,2 @@ +import add from "./mod1.cjs"; +console.log(add(1, 2)); diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/mod1.cjs b/tests/specs/node/require_esm_reexport_esm_module_exports/mod1.cjs new file mode 100644 index 0000000000..b3ef3e24dd --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/mod1.cjs @@ -0,0 +1 @@ +module.exports = require("./mod2.cjs"); diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/mod2.cjs b/tests/specs/node/require_esm_reexport_esm_module_exports/mod2.cjs new file mode 100644 index 0000000000..e9346a862d --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/mod2.cjs @@ -0,0 +1 @@ +module.exports = require("./add.mjs"); diff --git a/tests/specs/node/require_esm_reexport_esm_module_exports/run.out b/tests/specs/node/require_esm_reexport_esm_module_exports/run.out new file mode 100644 index 0000000000..00750edc07 --- /dev/null +++ b/tests/specs/node/require_esm_reexport_esm_module_exports/run.out @@ -0,0 +1 @@ +3