diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2022-02-27 14:38:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-27 14:38:45 +0100 |
commit | a65ce33fabb44bb2d9ed04773f7f334ed9c9a6b5 (patch) | |
tree | f5c169945377c3f806b514162408b81b5611ad44 /cli/compat/mod.rs | |
parent | 4bea1d06c7ddb177ed20e0f32b70d7ff889871ab (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.rs | 97 |
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) +} |