0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-09 13:49:37 -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]]
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",

View file

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

View file

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

6
cli/cache/node.rs vendored
View file

@ -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();

View file

@ -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<T> Deferred<T> {
struct CliFactoryServices {
blob_store: Deferred<Arc<BlobStore>>,
caches: Deferred<Arc<Caches>>,
cjs_module_export_analyzer: Deferred<Arc<CliCjsModuleExportAnalyzer>>,
cjs_tracker: Deferred<Arc<CliCjsTracker>>,
cli_options: Deferred<Arc<CliOptions>>,
code_cache: Deferred<Arc<CodeCache>>,
@ -804,6 +806,28 @@ impl CliFactory {
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(
&self,
) -> Result<&Arc<CliNodeCodeTranslator>, 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<DenoCompileBinaryWriter, AnyError> {
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()?,

View file

@ -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<String>),
Error(String),
}
const HAS_TRANSPILED_FLAG: u8 = 1 << 0;

View file

@ -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<CliSys>;
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<String>,
reexports: Vec<String>,
},
Cjs(ModuleExportsAndReExports),
}
pub struct CliCjsCodeAnalyzer {
@ -75,6 +85,7 @@ impl CliCjsCodeAnalyzer {
&self,
specifier: &ModuleSpecifier,
source: &str,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<CliCjsAnalysis, JsErrorBox> {
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<Cow<'a, str>>,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<ExtNodeCjsAnalysis<'a>, 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,
}))
}
}

View file

@ -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<Cow<'a, str>>,
_esm_analysis_mode: EsmAnalysisMode,
) -> Result<CjsAnalysis<'a>, JsErrorBox> {
let source = match source {
Some(source) => source,

View file

@ -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<Url, ModuleLoaderError> {
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(".", &current_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(

View file

@ -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::<Vec<_>>(),
)
}
});
} 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::<Vec<_>>())
}
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 {

View file

@ -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<CjsAnalysisExports>),
Cjs(CjsAnalysisExports),
}
@ -46,6 +46,13 @@ pub struct CjsAnalysisExports {
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.
#[async_trait::async_trait(?Send)]
pub trait CjsCodeAnalyzer {
@ -60,31 +67,33 @@ pub trait CjsCodeAnalyzer {
&self,
specifier: &Url,
maybe_source: Option<Cow<'a, str>>,
esm_analysis_mode: EsmAnalysisMode,
) -> Result<CjsAnalysis<'a>, 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<String>),
}
#[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<Cow<'a, str>>,
) -> Result<Cow<'a, str>, TranslateCjsToEsmError> {
let mut temp_var_count = 0;
) -> Result<ResolvedCjsAnalysis<'a>, 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::<BTreeSet<_>>();
@ -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<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(|| {
HashSet::from([
"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");