diff options
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | cli/Cargo.toml | 3 | ||||
-rw-r--r-- | cli/compat/mod.rs | 97 | ||||
-rw-r--r-- | cli/graph_util.rs | 23 | ||||
-rw-r--r-- | cli/proc_state.rs | 37 | ||||
-rw-r--r-- | cli/tests/integration/compat_tests.rs | 6 | ||||
-rw-r--r-- | cli/tests/testdata/compat/import_cjs_from_esm.out | 1 | ||||
-rw-r--r-- | cli/tests/testdata/compat/import_cjs_from_esm/imported.js | 9 | ||||
-rw-r--r-- | cli/tests/testdata/compat/import_cjs_from_esm/main.mjs | 1 | ||||
-rw-r--r-- | cli/tests/testdata/compat/import_cjs_from_esm/reexports.js | 1 | ||||
-rw-r--r-- | cli/tests/testdata/compat/import_cjs_from_esm/reexports2.js | 2 |
11 files changed, 190 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock index f7a7e8cf6..c160cf8d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,6 +772,7 @@ dependencies = [ "log", "lspower", "nix", + "node_resolver", "notify", "once_cell", "os_pipe", @@ -2467,6 +2468,17 @@ dependencies = [ ] [[package]] +name = "node_resolver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35ed1604f6f4e33b51926d6be3bc09423f35eec776165c460c313dc2970ea5a" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + +[[package]] name = "notify" version = "5.0.0-pre.12" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b50efa09e..36a446dab 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,7 +45,7 @@ winapi = "=0.3.9" winres = "=0.1.11" [dependencies] -deno_ast = { version = "0.12.0", features = ["bundler", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "transpiling", "typescript", "view", "visit"] } +deno_ast = { version = "0.12.0", features = ["bundler", "cjs", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "transpiling", "typescript", "view", "visit"] } deno_core = { version = "0.120.0", path = "../core" } deno_doc = "0.32.0" deno_graph = "0.24.0" @@ -74,6 +74,7 @@ jsonc-parser = { version = "=0.19.0", features = ["serde"] } libc = "=0.2.106" log = { version = "=0.4.14", features = ["serde"] } lspower = "=1.4.0" +node_resolver = "0.1.0" notify = "=5.0.0-pre.12" once_cell = "=1.9.0" percent-encoding = "=2.1.0" 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) +} diff --git a/cli/graph_util.rs b/cli/graph_util.rs index f0a38b7a1..4b01f54e0 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -53,6 +53,7 @@ pub(crate) struct GraphData { /// error messages. referrer_map: HashMap<ModuleSpecifier, Range>, configurations: HashSet<ModuleSpecifier>, + cjs_esm_translations: HashMap<ModuleSpecifier, String>, } impl GraphData { @@ -254,6 +255,7 @@ impl GraphData { modules, referrer_map, configurations: self.configurations.clone(), + cjs_esm_translations: Default::default(), }) } @@ -412,6 +414,27 @@ impl GraphData { ) -> Option<&'a ModuleEntry> { self.modules.get(specifier) } + + // TODO(bartlomieju): after saving translated source + // it's never removed, potentially leading to excessive + // memory consumption + pub(crate) fn add_cjs_esm_translation( + &mut self, + specifier: &ModuleSpecifier, + source: String, + ) { + let prev = self + .cjs_esm_translations + .insert(specifier.to_owned(), source); + assert!(prev.is_none()); + } + + pub(crate) fn get_cjs_esm_translation<'a>( + &'a self, + specifier: &ModuleSpecifier, + ) -> Option<&'a String> { + self.cjs_esm_translations.get(specifier) + } } impl From<&ModuleGraph> for GraphData { diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 36c7e08d4..8933cb986 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -340,6 +340,34 @@ impl ProcState { None, ) .await; + + let needs_cjs_esm_translation = graph + .modules() + .iter() + .any(|m| m.kind == ModuleKind::CommonJs); + + if needs_cjs_esm_translation { + for module in graph.modules() { + // TODO(bartlomieju): this is overly simplistic heuristic, once we are + // in compat mode, all files ending with plain `.js` extension are + // considered CommonJs modules. Which leads to situation where valid + // ESM modules with `.js` extension might undergo translation (it won't + // work in this situation). + if module.kind == ModuleKind::CommonJs { + let translated_source = compat::translate_cjs_to_esm( + &self.file_fetcher, + &module.specifier, + module.maybe_source.as_ref().unwrap().to_string(), + module.media_type, + ) + .await?; + let mut graph_data = self.graph_data.write(); + graph_data + .add_cjs_esm_translation(&module.specifier, translated_source); + } + } + } + // If there was a locker, validate the integrity of all the modules in the // locker. graph_lock_or_exit(&graph); @@ -506,7 +534,14 @@ impl ProcState { | MediaType::Unknown | MediaType::Cjs | MediaType::Mjs - | MediaType::Json => code.as_ref().clone(), + | MediaType::Json => { + if let Some(source) = graph_data.get_cjs_esm_translation(&specifier) + { + source.to_owned() + } else { + code.as_ref().clone() + } + } MediaType::Dts => "".to_string(), _ => { let emit_path = self diff --git a/cli/tests/integration/compat_tests.rs b/cli/tests/integration/compat_tests.rs index 189e1eb41..c8fc1c0a0 100644 --- a/cli/tests/integration/compat_tests.rs +++ b/cli/tests/integration/compat_tests.rs @@ -95,6 +95,12 @@ itest!(compat_worker { output: "compat/worker/worker_test.out", }); +itest!(cjs_esm_interop { + args: + "run --compat --unstable -A --quiet --no-check compat/import_cjs_from_esm/main.mjs", + output: "compat/import_cjs_from_esm.out", +}); + #[test] fn globals_in_repl() { let (out, _err) = util::run_and_collect_output_with_args( diff --git a/cli/tests/testdata/compat/import_cjs_from_esm.out b/cli/tests/testdata/compat/import_cjs_from_esm.out new file mode 100644 index 000000000..ffaa5e406 --- /dev/null +++ b/cli/tests/testdata/compat/import_cjs_from_esm.out @@ -0,0 +1 @@ +{ a: "A", b: "B", foo: "foo", bar: "bar", fizz: { buzz: "buzz", fizz: "FIZZ" } } diff --git a/cli/tests/testdata/compat/import_cjs_from_esm/imported.js b/cli/tests/testdata/compat/import_cjs_from_esm/imported.js new file mode 100644 index 000000000..49ab4c782 --- /dev/null +++ b/cli/tests/testdata/compat/import_cjs_from_esm/imported.js @@ -0,0 +1,9 @@ +exports = { + a: "A", + b: "B", +}; +exports.foo = "foo"; +exports.bar = "bar"; +exports.fizz = require("./reexports.js"); + +console.log(exports); diff --git a/cli/tests/testdata/compat/import_cjs_from_esm/main.mjs b/cli/tests/testdata/compat/import_cjs_from_esm/main.mjs new file mode 100644 index 000000000..6fbed1b7c --- /dev/null +++ b/cli/tests/testdata/compat/import_cjs_from_esm/main.mjs @@ -0,0 +1 @@ +import "./imported.js"; diff --git a/cli/tests/testdata/compat/import_cjs_from_esm/reexports.js b/cli/tests/testdata/compat/import_cjs_from_esm/reexports.js new file mode 100644 index 000000000..62edb7708 --- /dev/null +++ b/cli/tests/testdata/compat/import_cjs_from_esm/reexports.js @@ -0,0 +1 @@ +module.exports = require("./reexports2.js"); diff --git a/cli/tests/testdata/compat/import_cjs_from_esm/reexports2.js b/cli/tests/testdata/compat/import_cjs_from_esm/reexports2.js new file mode 100644 index 000000000..183d833b0 --- /dev/null +++ b/cli/tests/testdata/compat/import_cjs_from_esm/reexports2.js @@ -0,0 +1,2 @@ +exports.buzz = "buzz"; +exports.fizz = "FIZZ"; |