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:
parent
0c0757fe66
commit
2292eb1c92
23 changed files with 366 additions and 166 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
6
cli/cache/node.rs
vendored
|
@ -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();
|
||||
|
|
|
@ -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()?,
|
||||
|
|
|
@ -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;
|
||||
|
|
67
cli/node.rs
67
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<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,8 +128,9 @@ impl CliCjsCodeAnalyzer {
|
|||
})
|
||||
})
|
||||
.map_err(JsErrorBox::from_err)?;
|
||||
let is_script = parsed_source.compute_is_script();
|
||||
let is_cjs = cjs_tracker
|
||||
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,
|
||||
|
@ -128,12 +139,16 @@ impl CliCjsCodeAnalyzer {
|
|||
.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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(".", ¤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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
19
tests/specs/node/require_esm_reexport_esm/__test__.jsonc
Normal file
19
tests/specs/node/require_esm_reexport_esm/__test__.jsonc
Normal 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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
3
tests/specs/node/require_esm_reexport_esm/add.mjs
Normal file
3
tests/specs/node/require_esm_reexport_esm/add.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function add(a, b) {
|
||||
return a + b;
|
||||
}
|
4
tests/specs/node/require_esm_reexport_esm/main.mjs
Normal file
4
tests/specs/node/require_esm_reexport_esm/main.mjs
Normal 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));
|
1
tests/specs/node/require_esm_reexport_esm/mod1.cjs
Normal file
1
tests/specs/node/require_esm_reexport_esm/mod1.cjs
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require("./mod2.cjs");
|
1
tests/specs/node/require_esm_reexport_esm/mod2.cjs
Normal file
1
tests/specs/node/require_esm_reexport_esm/mod2.cjs
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require("./add.mjs");
|
3
tests/specs/node/require_esm_reexport_esm/run.out
Normal file
3
tests/specs/node/require_esm_reexport_esm/run.out
Normal file
|
@ -0,0 +1,3 @@
|
|||
[Module: null prototype] { add: [Function: add] }
|
||||
3
|
||||
3
|
|
@ -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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export { add as "module.exports" };
|
|
@ -0,0 +1,2 @@
|
|||
import add from "./mod1.cjs";
|
||||
console.log(add(1, 2));
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./mod2.cjs");
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./add.mjs");
|
|
@ -0,0 +1 @@
|
|||
3
|
Loading…
Add table
Reference in a new issue