summaryrefslogtreecommitdiff
path: root/cli/compat/mod.rs
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2022-02-27 14:38:45 +0100
committerGitHub <noreply@github.com>2022-02-27 14:38:45 +0100
commita65ce33fabb44bb2d9ed04773f7f334ed9c9a6b5 (patch)
treef5c169945377c3f806b514162408b81b5611ad44 /cli/compat/mod.rs
parent4bea1d06c7ddb177ed20e0f32b70d7ff889871ab (diff)
feat(compat): CJS/ESM interoperability (#13553)
This commit adds CJS/ESM interoperability when running in --compat mode. Before executing files, they are analyzed and all CommonJS modules are transformed on the fly to a ES modules. This is done by utilizing analyze_cjs() functionality from deno_ast. After discovering exports and reexports, an ES module is rendered and saved in memory for later use. There's a caveat that all files ending with ".js" extension are considered as CommonJS modules (unless there's a related "package.json" with "type": "module").
Diffstat (limited to 'cli/compat/mod.rs')
-rw-r--r--cli/compat/mod.rs97
1 files changed, 97 insertions, 0 deletions
diff --git a/cli/compat/mod.rs b/cli/compat/mod.rs
index 0c30a58fc..0f1c36084 100644
--- a/cli/compat/mod.rs
+++ b/cli/compat/mod.rs
@@ -3,11 +3,15 @@
mod errors;
mod esm_resolver;
+use crate::file_fetcher::FileFetcher;
+use deno_ast::MediaType;
use deno_core::error::AnyError;
use deno_core::located_script_name;
use deno_core::url::Url;
use deno_core::JsRuntime;
+use deno_core::ModuleSpecifier;
use once_cell::sync::Lazy;
+use std::sync::Arc;
pub use esm_resolver::check_if_should_use_esm_loader;
pub(crate) use esm_resolver::NodeEsmResolver;
@@ -155,3 +159,96 @@ pub fn setup_builtin_modules(
js_runtime.execute_script("setup_node_builtins.js", &script)?;
Ok(())
}
+
+/// 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(
+ file_fetcher: &FileFetcher,
+ specifier: &ModuleSpecifier,
+ code: String,
+ media_type: MediaType,
+) -> Result<String, AnyError> {
+ let parsed_source = deno_ast::parse_script(deno_ast::ParseParams {
+ specifier: specifier.to_string(),
+ source: deno_ast::SourceTextInfo::new(Arc::new(code)),
+ media_type,
+ capture_tokens: true,
+ scope_analysis: false,
+ maybe_syntax: None,
+ })?;
+ let analysis = parsed_source.analyze_cjs();
+
+ let mut source = vec![
+ r#"import { createRequire } from "node:module";"#.to_string(),
+ r#"const require = createRequire(import.meta.url);"#.to_string(),
+ ];
+
+ // if there are reexports, handle them first
+ for (idx, reexport) in analysis.reexports.iter().enumerate() {
+ // Firstly, resolve relate reexport specifier
+ let resolved_reexport = node_resolver::node_resolve(
+ reexport,
+ &specifier.to_file_path().unwrap(),
+ // FIXME(bartlomieju): check if these conditions are okay, probably
+ // should be `deno-require`, because `deno` is already used in `esm_resolver.rs`
+ &["deno", "require", "default"],
+ )?;
+ let reexport_specifier =
+ ModuleSpecifier::from_file_path(&resolved_reexport).unwrap();
+ // Secondly, read the source code from disk
+ let reexport_file = file_fetcher.get_source(&reexport_specifier).unwrap();
+ // Now perform analysis again
+ {
+ let parsed_source = deno_ast::parse_script(deno_ast::ParseParams {
+ specifier: reexport_specifier.to_string(),
+ source: deno_ast::SourceTextInfo::new(reexport_file.source),
+ media_type: reexport_file.media_type,
+ capture_tokens: true,
+ scope_analysis: false,
+ maybe_syntax: None,
+ })?;
+ let analysis = parsed_source.analyze_cjs();
+
+ source.push(format!(
+ "const reexport{} = require(\"{}\");",
+ idx, reexport
+ ));
+
+ for export in analysis.exports.iter().filter(|e| e.as_str() != "default")
+ {
+ // TODO(bartlomieju): Node actually checks if a given export exists in `exports` object,
+ // but it might not be necessary here since our analysis is more detailed?
+ source.push(format!(
+ "export const {} = reexport{}.{};",
+ export, idx, export
+ ));
+ }
+ }
+ }
+
+ source.push(format!(
+ "const mod = require(\"{}\");",
+ specifier
+ .to_file_path()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .replace('\\', "\\\\")
+ .replace('\'', "\\\'")
+ .replace('\"', "\\\"")
+ ));
+ source.push("export default mod".to_string());
+
+ for export in analysis.exports.iter().filter(|e| e.as_str() != "default") {
+ // TODO(bartlomieju): Node actually checks if a given export exists in `exports` object,
+ // but it might not be necessary here since our analysis is more detailed?
+ source.push(format!("export const {} = mod.{};", export, export));
+ }
+
+ let translated_source = source.join("\n");
+ Ok(translated_source)
+}