0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-09 21:57:40 -04:00

fix(node): support re-exported esm modules in cjs export analysis (#28379)

Adds support for re-exporting an ES module from a CJS one and then
importing the CJS module from ESM. Also fixes a bug where require esm
wasn't working in deno compile.
This commit is contained in:
David Sherret 2025-03-05 16:37:46 -05:00 committed by GitHub
parent 0c0757fe66
commit 2292eb1c92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 366 additions and 166 deletions

4
Cargo.lock generated
View file

@ -1609,9 +1609,9 @@ dependencies = [
[[package]] [[package]]
name = "deno_ast" name = "deno_ast"
version = "0.46.0" version = "0.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd3b6e14e5b1235dd613d9f5d955d7a80dec6de0fc00fa34b5d0ef5ca0a9ddb" checksum = "88393f34aaba238da6a3694aef7e046eec4d261c3bf98dc6669d397f1c274e5e"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"deno_error", "deno_error",

View file

@ -50,7 +50,7 @@ license = "MIT"
repository = "https://github.com/denoland/deno" repository = "https://github.com/denoland/deno"
[workspace.dependencies] [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_core = { version = "0.340.0" }
deno_bench_util = { version = "0.188.0", path = "./bench_util" } deno_bench_util = { version = "0.188.0", path = "./bench_util" }

View file

@ -67,7 +67,7 @@ winapi.workspace = true
winres.workspace = true winres.workspace = true
[dependencies] [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_cache_dir = { workspace = true, features = ["sync"] }
deno_config = { workspace = true, features = ["sync", "workspace"] } deno_config = { workspace = true, features = ["sync", "workspace"] }
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] }

6
cli/cache/node.rs vendored
View file

@ -137,6 +137,8 @@ impl NodeAnalysisCacheInner {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use deno_ast::ModuleExportsAndReExports;
use super::*; use super::*;
#[test] #[test]
@ -148,10 +150,10 @@ mod test {
.get_cjs_analysis("file.js", CacheDBHash::new(2)) .get_cjs_analysis("file.js", CacheDBHash::new(2))
.unwrap() .unwrap()
.is_none()); .is_none());
let cjs_analysis = CliCjsAnalysis::Cjs { let cjs_analysis = CliCjsAnalysis::Cjs(ModuleExportsAndReExports {
exports: vec!["export1".to_string()], exports: vec!["export1".to_string()],
reexports: vec!["re-export1".to_string()], reexports: vec!["re-export1".to_string()],
}; });
cache cache
.set_cjs_analysis("file.js", CacheDBHash::new(2), &cjs_analysis) .set_cjs_analysis("file.js", CacheDBHash::new(2), &cjs_analysis)
.unwrap(); .unwrap();

View file

@ -80,6 +80,7 @@ use crate::http_util::HttpClientProvider;
use crate::module_loader::CliModuleLoaderFactory; use crate::module_loader::CliModuleLoaderFactory;
use crate::module_loader::ModuleLoadPreparer; use crate::module_loader::ModuleLoadPreparer;
use crate::node::CliCjsCodeAnalyzer; use crate::node::CliCjsCodeAnalyzer;
use crate::node::CliCjsModuleExportAnalyzer;
use crate::node::CliNodeCodeTranslator; use crate::node::CliNodeCodeTranslator;
use crate::node::CliNodeResolver; use crate::node::CliNodeResolver;
use crate::node::CliPackageJsonResolver; use crate::node::CliPackageJsonResolver;
@ -261,6 +262,7 @@ impl<T> Deferred<T> {
struct CliFactoryServices { struct CliFactoryServices {
blob_store: Deferred<Arc<BlobStore>>, blob_store: Deferred<Arc<BlobStore>>,
caches: Deferred<Arc<Caches>>, caches: Deferred<Arc<Caches>>,
cjs_module_export_analyzer: Deferred<Arc<CliCjsModuleExportAnalyzer>>,
cjs_tracker: Deferred<Arc<CliCjsTracker>>, cjs_tracker: Deferred<Arc<CliCjsTracker>>,
cli_options: Deferred<Arc<CliOptions>>, cli_options: Deferred<Arc<CliOptions>>,
code_cache: Deferred<Arc<CodeCache>>, code_cache: Deferred<Arc<CodeCache>>,
@ -804,6 +806,28 @@ impl CliFactory {
Ok(()) Ok(())
} }
pub async fn cjs_module_export_analyzer(
&self,
) -> Result<&Arc<CliCjsModuleExportAnalyzer>, 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( pub async fn node_code_translator(
&self, &self,
) -> Result<&Arc<CliNodeCodeTranslator>, AnyError> { ) -> Result<&Arc<CliNodeCodeTranslator>, AnyError> {
@ -811,16 +835,9 @@ impl CliFactory {
.services .services
.node_code_translator .node_code_translator
.get_or_try_init_async(async { .get_or_try_init_async(async {
let node_resolver = self.node_resolver().await?.clone(); let module_export_analyzer = self.cjs_module_export_analyzer().await?;
let cjs_code_analyzer = self.create_cjs_code_analyzer()?;
Ok(Arc::new(NodeCodeTranslator::new( Ok(Arc::new(NodeCodeTranslator::new(
cjs_code_analyzer, module_export_analyzer.clone(),
self.in_npm_pkg_checker()?.clone(),
node_resolver,
self.npm_resolver().await?.clone(),
self.pkg_json_resolver()?.clone(),
self.sys(),
))) )))
}) })
.await .await
@ -1025,7 +1042,7 @@ impl CliFactory {
) -> Result<DenoCompileBinaryWriter, AnyError> { ) -> Result<DenoCompileBinaryWriter, AnyError> {
let cli_options = self.cli_options()?; let cli_options = self.cli_options()?;
Ok(DenoCompileBinaryWriter::new( Ok(DenoCompileBinaryWriter::new(
self.create_cjs_code_analyzer()?, self.cjs_module_export_analyzer().await?,
self.cjs_tracker()?, self.cjs_tracker()?,
self.cli_options()?, self.cli_options()?,
self.deno_dir()?, self.deno_dir()?,

View file

@ -9,7 +9,6 @@ use deno_runtime::deno_permissions::PermissionsOptions;
use deno_runtime::deno_telemetry::OtelConfig; use deno_runtime::deno_telemetry::OtelConfig;
use deno_semver::Version; use deno_semver::Version;
use indexmap::IndexMap; use indexmap::IndexMap;
use node_resolver::analyze::CjsAnalysisExports;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -130,7 +129,8 @@ impl<'a> DenoRtDeserializable<'a> for SpecifierId {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub enum CjsExportAnalysisEntry { pub enum CjsExportAnalysisEntry {
Esm, Esm,
Cjs(CjsAnalysisExports), Cjs(Vec<String>),
Error(String),
} }
const HAS_TRANSPILED_FLAG: u8 = 1 << 0; const HAS_TRANSPILED_FLAG: u8 = 1 << 0;

View file

@ -4,6 +4,7 @@ use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
use deno_ast::MediaType; use deno_ast::MediaType;
use deno_ast::ModuleExportsAndReExports;
use deno_ast::ModuleSpecifier; use deno_ast::ModuleSpecifier;
use deno_error::JsErrorBox; use deno_error::JsErrorBox;
use deno_graph::ParsedSourceStore; use deno_graph::ParsedSourceStore;
@ -12,6 +13,8 @@ use deno_runtime::deno_fs;
use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis;
use node_resolver::analyze::CjsAnalysisExports; use node_resolver::analyze::CjsAnalysisExports;
use node_resolver::analyze::CjsCodeAnalyzer; use node_resolver::analyze::CjsCodeAnalyzer;
use node_resolver::analyze::CjsModuleExportAnalyzer;
use node_resolver::analyze::EsmAnalysisMode;
use node_resolver::analyze::NodeCodeTranslator; use node_resolver::analyze::NodeCodeTranslator;
use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::DenoIsBuiltInNodeModuleChecker;
use serde::Deserialize; use serde::Deserialize;
@ -24,6 +27,13 @@ use crate::npm::CliNpmResolver;
use crate::resolver::CliCjsTracker; use crate::resolver::CliCjsTracker;
use crate::sys::CliSys; use crate::sys::CliSys;
pub type CliCjsModuleExportAnalyzer = CjsModuleExportAnalyzer<
CliCjsCodeAnalyzer,
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
CliNpmResolver,
CliSys,
>;
pub type CliNodeCodeTranslator = NodeCodeTranslator< pub type CliNodeCodeTranslator = NodeCodeTranslator<
CliCjsCodeAnalyzer, CliCjsCodeAnalyzer,
DenoInNpmPackageChecker, DenoInNpmPackageChecker,
@ -42,11 +52,11 @@ pub type CliPackageJsonResolver = node_resolver::PackageJsonResolver<CliSys>;
pub enum CliCjsAnalysis { pub enum CliCjsAnalysis {
/// The module was found to be an ES module. /// The module was found to be an ES module.
Esm, Esm,
/// The module was found to be an ES module and
/// it was analyzed for imports and exports.
EsmAnalysis(ModuleExportsAndReExports),
/// The module was CJS. /// The module was CJS.
Cjs { Cjs(ModuleExportsAndReExports),
exports: Vec<String>,
reexports: Vec<String>,
},
} }
pub struct CliCjsCodeAnalyzer { pub struct CliCjsCodeAnalyzer {
@ -75,6 +85,7 @@ impl CliCjsCodeAnalyzer {
&self, &self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: &str, source: &str,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<CliCjsAnalysis, JsErrorBox> { ) -> Result<CliCjsAnalysis, JsErrorBox> {
let source_hash = CacheDBHash::from_hashable(source); let source_hash = CacheDBHash::from_hashable(source);
if let Some(analysis) = if let Some(analysis) =
@ -85,17 +96,16 @@ impl CliCjsCodeAnalyzer {
let media_type = MediaType::from_specifier(specifier); let media_type = MediaType::from_specifier(specifier);
if media_type == MediaType::Json { if media_type == MediaType::Json {
return Ok(CliCjsAnalysis::Cjs { return Ok(CliCjsAnalysis::Cjs(Default::default()));
exports: vec![],
reexports: vec![],
});
} }
let cjs_tracker = self.cjs_tracker.clone(); let cjs_tracker = self.cjs_tracker.clone();
let is_maybe_cjs = cjs_tracker let is_maybe_cjs = cjs_tracker
.is_maybe_cjs(specifier, media_type) .is_maybe_cjs(specifier, media_type)
.map_err(JsErrorBox::from_err)?; .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 let maybe_parsed_source = self
.parsed_source_cache .parsed_source_cache
.as_ref() .as_ref()
@ -118,22 +128,27 @@ impl CliCjsCodeAnalyzer {
}) })
}) })
.map_err(JsErrorBox::from_err)?; .map_err(JsErrorBox::from_err)?;
let is_script = parsed_source.compute_is_script(); let is_script = is_maybe_cjs && parsed_source.compute_is_script();
let is_cjs = cjs_tracker let is_cjs = is_maybe_cjs
.is_cjs_with_known_is_script( && cjs_tracker
parsed_source.specifier(), .is_cjs_with_known_is_script(
media_type, parsed_source.specifier(),
is_script, media_type,
) is_script,
.map_err(JsErrorBox::from_err)?; )
.map_err(JsErrorBox::from_err)?;
if is_cjs { if is_cjs {
let analysis = parsed_source.analyze_cjs(); let analysis = parsed_source.analyze_cjs();
Ok(CliCjsAnalysis::Cjs { Ok(CliCjsAnalysis::Cjs(analysis))
exports: analysis.exports,
reexports: analysis.reexports,
})
} else { } 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, &self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: Option<Cow<'a, str>>, source: Option<Cow<'a, str>>,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<ExtNodeCjsAnalysis<'a>, JsErrorBox> { ) -> Result<ExtNodeCjsAnalysis<'a>, JsErrorBox> {
let source = match source { let source = match source {
Some(source) => 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 { match analysis {
CliCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source)), CliCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source, None)),
CliCjsAnalysis::Cjs { exports, reexports } => { CliCjsAnalysis::EsmAnalysis(analysis) => Ok(ExtNodeCjsAnalysis::Esm(
source,
Some(CjsAnalysisExports {
exports: analysis.exports,
reexports: analysis.reexports,
}),
)),
CliCjsAnalysis::Cjs(analysis) => {
Ok(ExtNodeCjsAnalysis::Cjs(CjsAnalysisExports { Ok(ExtNodeCjsAnalysis::Cjs(CjsAnalysisExports {
exports, exports: analysis.exports,
reexports, reexports: analysis.reexports,
})) }))
} }
} }

View file

@ -13,6 +13,7 @@ use deno_resolver::npm::NpmReqResolver;
use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_fs::FileSystem;
use node_resolver::analyze::CjsAnalysis; use node_resolver::analyze::CjsAnalysis;
use node_resolver::analyze::CjsAnalysisExports; use node_resolver::analyze::CjsAnalysisExports;
use node_resolver::analyze::EsmAnalysisMode;
use node_resolver::analyze::NodeCodeTranslator; use node_resolver::analyze::NodeCodeTranslator;
use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::DenoIsBuiltInNodeModuleChecker;
@ -96,11 +97,17 @@ impl CjsCodeAnalyzer {
match data { match data {
CjsExportAnalysisEntry::Esm => { CjsExportAnalysisEntry::Esm => {
cjs_tracker.set_is_known_script(specifier, false); 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); 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 // assume ESM as we don't have access to swc here
CjsAnalysis::Esm(source) CjsAnalysis::Esm(source, None)
} }
} }
} else { } else {
CjsAnalysis::Esm(source) CjsAnalysis::Esm(source, None)
}; };
Ok(analysis) Ok(analysis)
@ -136,6 +143,7 @@ impl node_resolver::analyze::CjsCodeAnalyzer for CjsCodeAnalyzer {
&self, &self,
specifier: &Url, specifier: &Url,
source: Option<Cow<'a, str>>, source: Option<Cow<'a, str>>,
_esm_analysis_mode: EsmAnalysisMode,
) -> Result<CjsAnalysis<'a>, JsErrorBox> { ) -> Result<CjsAnalysis<'a>, JsErrorBox> {
let source = match source { let source = match source {
Some(source) => source, Some(source) => source,

View file

@ -74,6 +74,7 @@ use deno_runtime::permissions::RuntimePermissionDescriptorParser;
use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerExecutionMode;
use deno_runtime::WorkerLogLevel; use deno_runtime::WorkerLogLevel;
use deno_semver::npm::NpmPackageReqReference; use deno_semver::npm::NpmPackageReqReference;
use node_resolver::analyze::CjsModuleExportAnalyzer;
use node_resolver::analyze::NodeCodeTranslator; use node_resolver::analyze::NodeCodeTranslator;
use node_resolver::cache::NodeResolutionSys; use node_resolver::cache::NodeResolutionSys;
use node_resolver::errors::ClosestPkgJsonError; use node_resolver::errors::ClosestPkgJsonError;
@ -154,18 +155,9 @@ impl ModuleLoader for EmbeddedModuleLoader {
&self, &self,
raw_specifier: &str, raw_specifier: &str,
referrer: &str, referrer: &str,
kind: ResolutionKind, _kind: ResolutionKind,
) -> Result<Url, ModuleLoaderError> { ) -> Result<Url, ModuleLoaderError> {
let referrer = if referrer == "." { 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(); let current_dir = std::env::current_dir().unwrap();
deno_core::resolve_path(".", &current_dir) deno_core::resolve_path(".", &current_dir)
.map_err(JsErrorBox::from_err)? .map_err(JsErrorBox::from_err)?
@ -815,7 +807,7 @@ pub async fn run(
})); }));
let cjs_esm_code_analyzer = let cjs_esm_code_analyzer =
CjsCodeAnalyzer::new(cjs_tracker.clone(), modules.clone(), sys.clone()); 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, cjs_esm_code_analyzer,
in_npm_pkg_checker, in_npm_pkg_checker,
node_resolver.clone(), node_resolver.clone(),
@ -823,6 +815,8 @@ pub async fn run(
pkg_json_resolver.clone(), pkg_json_resolver.clone(),
sys.clone(), sys.clone(),
)); ));
let node_code_translator =
Arc::new(NodeCodeTranslator::new(cjs_module_export_analyzer));
let workspace_resolver = { let workspace_resolver = {
let import_map = match metadata.workspace_resolver.import_map { let import_map = match metadata.workspace_resolver.import_map {
Some(import_map) => Some( Some(import_map) => Some(

View file

@ -52,8 +52,7 @@ use deno_path_util::url_from_directory_path;
use deno_path_util::url_to_file_path; use deno_path_util::url_to_file_path;
use deno_resolver::workspace::WorkspaceResolver; use deno_resolver::workspace::WorkspaceResolver;
use indexmap::IndexMap; use indexmap::IndexMap;
use node_resolver::analyze::CjsAnalysis; use node_resolver::analyze::ResolvedCjsAnalysis;
use node_resolver::analyze::CjsCodeAnalyzer;
use super::virtual_fs::output_vfs; use super::virtual_fs::output_vfs;
use crate::args::CliOptions; use crate::args::CliOptions;
@ -61,7 +60,7 @@ use crate::args::CompileFlags;
use crate::cache::DenoDir; use crate::cache::DenoDir;
use crate::emit::Emitter; use crate::emit::Emitter;
use crate::http_util::HttpClientProvider; use crate::http_util::HttpClientProvider;
use crate::node::CliCjsCodeAnalyzer; use crate::node::CliCjsModuleExportAnalyzer;
use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolver;
use crate::resolver::CliCjsTracker; use crate::resolver::CliCjsTracker;
use crate::sys::CliSys; use crate::sys::CliSys;
@ -190,7 +189,7 @@ pub struct WriteBinOptions<'a> {
} }
pub struct DenoCompileBinaryWriter<'a> { pub struct DenoCompileBinaryWriter<'a> {
cjs_code_analyzer: CliCjsCodeAnalyzer, cjs_module_export_analyzer: &'a CliCjsModuleExportAnalyzer,
cjs_tracker: &'a CliCjsTracker, cjs_tracker: &'a CliCjsTracker,
cli_options: &'a CliOptions, cli_options: &'a CliOptions,
deno_dir: &'a DenoDir, deno_dir: &'a DenoDir,
@ -204,7 +203,7 @@ pub struct DenoCompileBinaryWriter<'a> {
impl<'a> DenoCompileBinaryWriter<'a> { impl<'a> DenoCompileBinaryWriter<'a> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
cjs_code_analyzer: CliCjsCodeAnalyzer, cjs_module_export_analyzer: &'a CliCjsModuleExportAnalyzer,
cjs_tracker: &'a CliCjsTracker, cjs_tracker: &'a CliCjsTracker,
cli_options: &'a CliOptions, cli_options: &'a CliOptions,
deno_dir: &'a DenoDir, deno_dir: &'a DenoDir,
@ -215,7 +214,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
npm_system_info: NpmSystemInfo, npm_system_info: NpmSystemInfo,
) -> Self { ) -> Self {
Self { Self {
cjs_code_analyzer, cjs_module_export_analyzer,
cjs_tracker, cjs_tracker,
cli_options, cli_options,
deno_dir, deno_dir,
@ -423,16 +422,18 @@ impl<'a> DenoCompileBinaryWriter<'a> {
m.is_script, m.is_script,
)? { )? {
let cjs_analysis = self let cjs_analysis = self
.cjs_code_analyzer .cjs_module_export_analyzer
.analyze_cjs( .analyze_all_exports(
module.specifier(), module.specifier(),
Some(Cow::Borrowed(m.source.as_ref())), Some(Cow::Borrowed(m.source.as_ref())),
) )
.await?; .await?;
maybe_cjs_analysis = Some(match cjs_analysis { maybe_cjs_analysis = Some(match cjs_analysis {
CjsAnalysis::Esm(_) => CjsExportAnalysisEntry::Esm, ResolvedCjsAnalysis::Esm(_) => CjsExportAnalysisEntry::Esm,
CjsAnalysis::Cjs(exports) => { ResolvedCjsAnalysis::Cjs(exports) => {
CjsExportAnalysisEntry::Cjs(exports) CjsExportAnalysisEntry::Cjs(
exports.into_iter().collect::<Vec<_>>(),
)
} }
}); });
} else { } else {
@ -544,26 +545,24 @@ impl<'a> DenoCompileBinaryWriter<'a> {
.file_bytes(file.offset) .file_bytes(file.offset)
.map(|text| String::from_utf8_lossy(text)); .map(|text| String::from_utf8_lossy(text));
let cjs_analysis_result = self let cjs_analysis_result = self
.cjs_code_analyzer .cjs_module_export_analyzer
.analyze_cjs(&specifier, maybe_source) .analyze_all_exports(&specifier, maybe_source)
.await; .await;
let maybe_analysis = match cjs_analysis_result { let analysis = match cjs_analysis_result {
Ok(CjsAnalysis::Esm(_)) => Some(CjsExportAnalysisEntry::Esm), Ok(ResolvedCjsAnalysis::Esm(_)) => CjsExportAnalysisEntry::Esm,
Ok(CjsAnalysis::Cjs(exports)) => { Ok(ResolvedCjsAnalysis::Cjs(exports)) => {
Some(CjsExportAnalysisEntry::Cjs(exports)) CjsExportAnalysisEntry::Cjs(exports.into_iter().collect::<Vec<_>>())
} }
Err(err) => { Err(err) => {
log::debug!( log::debug!(
"Ignoring cjs export analysis for '{}': {}", "Had cjs export analysis error for '{}': {}",
specifier, specifier,
err 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 { for (file_path, analysis) in to_add {

View file

@ -36,7 +36,7 @@ use crate::UrlOrPathRef;
pub enum CjsAnalysis<'a> { pub enum CjsAnalysis<'a> {
/// File was found to be an ES module and the translator should /// File was found to be an ES module and the translator should
/// load the code as ESM. /// load the code as ESM.
Esm(Cow<'a, str>), Esm(Cow<'a, str>, Option<CjsAnalysisExports>),
Cjs(CjsAnalysisExports), Cjs(CjsAnalysisExports),
} }
@ -46,6 +46,13 @@ pub struct CjsAnalysisExports {
pub reexports: Vec<String>, pub reexports: Vec<String>,
} }
/// 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. /// Code analyzer for CJS and ESM files.
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait CjsCodeAnalyzer { pub trait CjsCodeAnalyzer {
@ -60,31 +67,33 @@ pub trait CjsCodeAnalyzer {
&self, &self,
specifier: &Url, specifier: &Url,
maybe_source: Option<Cow<'a, str>>, maybe_source: Option<Cow<'a, str>>,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<CjsAnalysis<'a>, JsErrorBox>; ) -> Result<CjsAnalysis<'a>, JsErrorBox>;
} }
#[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum ResolvedCjsAnalysis<'a> {
pub enum TranslateCjsToEsmError { Esm(Cow<'a, str>),
#[class(inherit)] Cjs(BTreeSet<String>),
#[error(transparent)]
CjsCodeAnalysis(JsErrorBox),
#[class(inherit)]
#[error(transparent)]
ExportAnalysis(JsErrorBox),
} }
#[derive(Debug, thiserror::Error, deno_error::JsError)] #[allow(clippy::disallowed_types)]
#[class(generic)] pub type CjsModuleExportAnalyzerRc<
#[error("Could not load '{reexport}' ({reexport_specifier}) referenced from {referrer}")] TCjsCodeAnalyzer,
pub struct CjsAnalysisCouldNotLoadError { TInNpmPackageChecker,
reexport: String, TIsBuiltInNodeModuleChecker,
reexport_specifier: Url, TNpmPackageFolderResolver,
referrer: Url, TSys,
#[source] > = crate::sync::MaybeArc<
source: JsErrorBox, CjsModuleExportAnalyzer<
} TCjsCodeAnalyzer,
TInNpmPackageChecker,
TIsBuiltInNodeModuleChecker,
TNpmPackageFolderResolver,
TSys,
>,
>;
pub struct NodeCodeTranslator< pub struct CjsModuleExportAnalyzer<
TCjsCodeAnalyzer: CjsCodeAnalyzer, TCjsCodeAnalyzer: CjsCodeAnalyzer,
TInNpmPackageChecker: InNpmPackageChecker, TInNpmPackageChecker: InNpmPackageChecker,
TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker,
@ -111,7 +120,7 @@ impl<
TNpmPackageFolderResolver: NpmPackageFolderResolver, TNpmPackageFolderResolver: NpmPackageFolderResolver,
TSys: FsCanonicalize + FsMetadata + FsRead, TSys: FsCanonicalize + FsMetadata + FsRead,
> >
NodeCodeTranslator< CjsModuleExportAnalyzer<
TCjsCodeAnalyzer, TCjsCodeAnalyzer,
TInNpmPackageChecker, TInNpmPackageChecker,
TIsBuiltInNodeModuleChecker, TIsBuiltInNodeModuleChecker,
@ -142,36 +151,24 @@ impl<
} }
} }
/// Translates given CJS module into ESM. This function will perform static pub async fn analyze_all_exports<'a>(
/// 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, &self,
entry_specifier: &Url, entry_specifier: &Url,
source: Option<Cow<'a, str>>, source: Option<Cow<'a, str>>,
) -> Result<Cow<'a, str>, TranslateCjsToEsmError> { ) -> Result<ResolvedCjsAnalysis<'a>, TranslateCjsToEsmError> {
let mut temp_var_count = 0;
let analysis = self let analysis = self
.cjs_code_analyzer .cjs_code_analyzer
.analyze_cjs(entry_specifier, source) .analyze_cjs(entry_specifier, source, EsmAnalysisMode::SourceOnly)
.await .await
.map_err(TranslateCjsToEsmError::CjsCodeAnalysis)?; .map_err(TranslateCjsToEsmError::CjsCodeAnalysis)?;
let analysis = match analysis { let analysis = match analysis {
CjsAnalysis::Esm(source) => return Ok(source), CjsAnalysis::Esm(source, _) => {
return Ok(ResolvedCjsAnalysis::Esm(source))
}
CjsAnalysis::Cjs(analysis) => analysis, 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 // use a BTreeSet to make the output deterministic for v8's code cache
let mut all_exports = analysis.exports.into_iter().collect::<BTreeSet<_>>(); let mut all_exports = analysis.exports.into_iter().collect::<BTreeSet<_>>();
@ -193,38 +190,7 @@ impl<
} }
} }
source.push(format!( Ok(ResolvedCjsAnalysis::Cjs(all_exports))
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))
} }
#[allow(clippy::needless_lifetimes)] #[allow(clippy::needless_lifetimes)]
@ -239,7 +205,6 @@ impl<
) { ) {
struct Analysis { struct Analysis {
reexport_specifier: url::Url, reexport_specifier: url::Url,
referrer: url::Url,
analysis: CjsAnalysis<'static>, analysis: CjsAnalysis<'static>,
} }
@ -288,7 +253,11 @@ impl<
let referrer = referrer.clone(); let referrer = referrer.clone();
let future = async move { let future = async move {
let analysis = cjs_code_analyzer let analysis = cjs_code_analyzer
.analyze_cjs(&reexport_specifier, None) .analyze_cjs(
&reexport_specifier,
None,
EsmAnalysisMode::SourceImportsAndExports,
)
.await .await
.map_err(|source| { .map_err(|source| {
JsErrorBox::from_err(CjsAnalysisCouldNotLoadError { JsErrorBox::from_err(CjsAnalysisCouldNotLoadError {
@ -301,7 +270,6 @@ impl<
Ok(Analysis { Ok(Analysis {
reexport_specifier, reexport_specifier,
referrer,
analysis, analysis,
}) })
} }
@ -321,7 +289,6 @@ impl<
// 2. Look at the analysis result and resolve its exports and re-exports // 2. Look at the analysis result and resolve its exports and re-exports
let Analysis { let Analysis {
reexport_specifier, reexport_specifier,
referrer,
analysis, analysis,
} = match analysis_result { } = match analysis_result {
Ok(analysis) => analysis, Ok(analysis) => analysis,
@ -331,14 +298,7 @@ impl<
} }
}; };
match analysis { match analysis {
CjsAnalysis::Esm(_) => { CjsAnalysis::Cjs(analysis) | CjsAnalysis::Esm(_, Some(analysis)) => {
// 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) => {
if !analysis.reexports.is_empty() { if !analysis.reexports.is_empty() {
handle_reexports( handle_reexports(
reexport_specifier.clone(), reexport_specifier.clone(),
@ -355,6 +315,10 @@ impl<
.filter(|e| e.as_str() != "default"), .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<Cow<'a, str>>,
) -> Result<Cow<'a, str>, 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<HashSet<&str>> = Lazy::new(|| { static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| {
HashSet::from([ HashSet::from([
"abstract", "abstract",

View file

@ -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"
}]
}
}
}

View file

@ -0,0 +1,3 @@
export function add(a, b) {
return a + b;
}

View file

@ -0,0 +1,4 @@
import mod, { add } from "./mod1.cjs";
console.log(mod);
console.log(mod.add(1, 2));
console.log(add(1, 2));

View file

@ -0,0 +1 @@
module.exports = require("./mod2.cjs");

View file

@ -0,0 +1 @@
module.exports = require("./add.mjs");

View file

@ -0,0 +1,3 @@
[Module: null prototype] { add: [Function: add] }
3
3

View file

@ -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"
}]
}
}
}

View file

@ -0,0 +1,5 @@
function add(a, b) {
return a + b;
}
export { add as "module.exports" };

View file

@ -0,0 +1,2 @@
import add from "./mod1.cjs";
console.log(add(1, 2));

View file

@ -0,0 +1 @@
module.exports = require("./mod2.cjs");

View file

@ -0,0 +1 @@
module.exports = require("./add.mjs");