diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 4d3cfe27fe..e280a8a3a0 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -4,6 +4,7 @@ use crate::emit::emit_parsed_source; use crate::emit::TsTypeLib; use crate::graph_util::ModuleEntry; use crate::node; +use crate::node::CjsToEsmTranslateKind; use crate::npm::NpmPackageResolver; use crate::proc_state::ProcState; use crate::text_encoding::code_without_source_map; @@ -23,6 +24,7 @@ use deno_core::ModuleType; use deno_core::OpState; use deno_core::SourceMapGetter; use deno_runtime::permissions::Permissions; +use std::borrow::Cow; use std::cell::RefCell; use std::pin::Pin; use std::rc::Rc; @@ -134,30 +136,51 @@ impl CliModuleLoader { maybe_referrer: Option, ) -> Result { let code_source = if self.ps.npm_resolver.in_npm_package(specifier) { - let file_path = specifier.to_file_path().unwrap(); + let is_cjs = self.ps.cjs_resolutions.lock().contains(specifier); + let (maybe_translate_kind, load_specifier) = if is_cjs { + let path = specifier.path(); + let mut specifier = specifier.clone(); + if let Some(new_path) = path.strip_suffix(node::CJS_TO_ESM_NODE_SUFFIX) + { + specifier.set_path(new_path); + (Some(CjsToEsmTranslateKind::Node), Cow::Owned(specifier)) + } else if let Some(new_path) = + path.strip_suffix(node::CJS_TO_ESM_DENO_SUFFIX) + { + specifier.set_path(new_path); + (Some(CjsToEsmTranslateKind::Deno), Cow::Owned(specifier)) + } else { + // all cjs code that goes through the loader should have been given a suffix + unreachable!("Unknown cjs specifier: {}", specifier); + } + } else { + (None, Cow::Borrowed(specifier)) + }; + + let file_path = load_specifier.to_file_path().unwrap(); let code = std::fs::read_to_string(&file_path).with_context(|| { let mut msg = "Unable to load ".to_string(); msg.push_str(&*file_path.to_string_lossy()); - if let Some(referrer) = maybe_referrer { + if let Some(referrer) = &maybe_referrer { msg.push_str(" imported from "); msg.push_str(referrer.as_str()); } msg })?; - let is_cjs = self.ps.cjs_resolutions.lock().contains(specifier); - let code = if is_cjs { + let code = if let Some(translate_kind) = maybe_translate_kind { // translate cjs to esm if it's cjs and inject node globals node::translate_cjs_to_esm( &self.ps.file_fetcher, - specifier, + &load_specifier, code, MediaType::Cjs, &self.ps.npm_resolver, + translate_kind, )? } else { // only inject node globals for esm - node::esm_code_with_node_globals(specifier, code)? + node::esm_code_with_node_globals(&load_specifier, code)? }; ModuleCodeSource { code, diff --git a/cli/node/mod.rs b/cli/node/mod.rs index 91fde1db5e..81a2f10e79 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -255,6 +255,11 @@ static NODE_COMPAT_URL: Lazy = Lazy::new(|| { pub static MODULE_ALL_URL: Lazy = Lazy::new(|| NODE_COMPAT_URL.join("node/module_all.ts").unwrap()); +/// Suffix used for the CJS to ESM translation for Node code +pub const CJS_TO_ESM_NODE_SUFFIX: &str = ".node_cjs_to_esm.mjs"; +/// Suffix used for the CJS to ESM translation for Deno code +pub const CJS_TO_ESM_DENO_SUFFIX: &str = ".deno_cjs_to_esm.mjs"; + fn find_builtin_node_module(specifier: &str) -> Option<&NodeModulePolyfill> { SUPPORTED_MODULES.iter().find(|m| m.name == specifier) } @@ -412,7 +417,15 @@ pub fn node_resolve( None => return Ok(None), }; - let resolve_response = url_to_node_resolution(url, npm_resolver)?; + let resolve_response = match url_to_node_resolution(url, npm_resolver)? { + NodeResolution::CommonJs(mut url) => { + // Use a suffix to say this cjs specifier should be translated from + // cjs to esm for consumption by Node ESM code + url.set_path(&format!("{}{}", url.path(), CJS_TO_ESM_NODE_SUFFIX)); + NodeResolution::CommonJs(url) + } + val => val, + }; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(Some(resolve_response)) @@ -440,7 +453,15 @@ pub fn node_resolve_npm_reference( })?; let url = ModuleSpecifier::from_file_path(resolved_path).unwrap(); - let resolve_response = url_to_node_resolution(url, npm_resolver)?; + let resolve_response = match url_to_node_resolution(url, npm_resolver)? { + NodeResolution::CommonJs(mut url) => { + // Use a suffix to say this cjs specifier should be translated from + // cjs to esm for consumption by Deno ESM code + url.set_path(&format!("{}{}", url.path(), CJS_TO_ESM_DENO_SUFFIX)); + NodeResolution::CommonJs(url) + } + val => val, + }; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(Some(resolve_response)) @@ -701,6 +722,12 @@ fn add_export( } } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CjsToEsmTranslateKind { + Node, + Deno, +} + /// Translates given CJS module into ESM. This function will perform static /// analysis on the file to find defined exports and reexports. /// @@ -713,6 +740,7 @@ pub fn translate_cjs_to_esm( code: String, media_type: MediaType, npm_resolver: &GlobalNpmPackageResolver, + translate_kind: CjsToEsmTranslateKind, ) -> Result { fn perform_cjs_analysis( specifier: &str, @@ -734,7 +762,6 @@ pub fn translate_cjs_to_esm( let mut handled_reexports: HashSet = HashSet::default(); let mut source = vec![ - r#"var window = undefined;"#.to_string(), r#"const require = Deno[Deno.internal].require.Module.createRequire(import.meta.url);"#.to_string(), ]; @@ -813,7 +840,9 @@ pub fn translate_cjs_to_esm( let mut had_default = false; for export in &all_exports { if export.as_str() == "default" { - if root_exports.contains("__esModule") { + if translate_kind == CjsToEsmTranslateKind::Deno + && root_exports.contains("__esModule") + { source.push(format!("export default mod[\"{}\"];", export)); had_default = true; } diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs index 6787343096..8bbb89d65e 100644 --- a/cli/tests/integration/npm_tests.rs +++ b/cli/tests/integration/npm_tests.rs @@ -33,6 +33,13 @@ itest!(esm_module_deno_test { http_server: true, }); +itest!(esm_import_cjs_default { + args: "run --allow-read --allow-env --unstable --quiet npm/esm_import_cjs_default/main.js", + output: "npm/esm_import_cjs_default/main.out", + envs: env_vars(), + http_server: true, +}); + itest!(cjs_with_deps { args: "run --allow-read --allow-env --unstable npm/cjs_with_deps/main.js", output: "npm/cjs_with_deps/main.out", diff --git a/cli/tests/testdata/npm/esm_import_cjs_default/main.js b/cli/tests/testdata/npm/esm_import_cjs_default/main.js new file mode 100644 index 0000000000..3be3cac5e7 --- /dev/null +++ b/cli/tests/testdata/npm/esm_import_cjs_default/main.js @@ -0,0 +1,22 @@ +import cjsDefault, { + MyClass as MyCjsClass, +} from "npm:@denotest/cjs-default-export"; +import * as cjsNamespace from "npm:@denotest/cjs-default-export"; +import esmDefault from "npm:@denotest/esm-import-cjs-default"; +import * as esmNamespace from "npm:@denotest/esm-import-cjs-default"; + +console.log("Deno esm importing node cjs"); +console.log("==========================="); +console.log(cjsDefault); +console.log(cjsNamespace); +console.log("==========================="); + +console.log("Deno esm importing node esm"); +console.log("==========================="); +console.log(esmDefault); +console.log(esmNamespace); +console.log("==========================="); + +console.log(cjsDefault()); +console.log(esmDefault()); +console.log(MyCjsClass.someStaticMethod()); diff --git a/cli/tests/testdata/npm/esm_import_cjs_default/main.out b/cli/tests/testdata/npm/esm_import_cjs_default/main.out new file mode 100644 index 0000000000..3b2b3b006a --- /dev/null +++ b/cli/tests/testdata/npm/esm_import_cjs_default/main.out @@ -0,0 +1,35 @@ +Node esm importing node cjs +=========================== +{ default: [Function], named: [Function], MyClass: [Function: MyClass] } +{ default: [Function], named: [Function] } +Module { + MyClass: [Function: MyClass], + __esModule: true, + default: { default: [Function], named: [Function], MyClass: [Function: MyClass] }, + named: [Function] +} +Module { + __esModule: true, + default: { default: [Function], named: [Function] }, + named: [Function] +} +=========================== +static method +Deno esm importing node cjs +=========================== +[Function] +Module { + MyClass: [Function: MyClass], + __esModule: true, + default: [Function], + named: [Function] +} +=========================== +Deno esm importing node esm +=========================== +[Function: default] +Module { default: [Function: default] } +=========================== +1 +5 +static method diff --git a/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.js b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.js new file mode 100644 index 0000000000..ec4ece6b33 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.js @@ -0,0 +1,17 @@ +Object.defineProperty(module.exports, "__esModule", { + value: true +}); +module.exports["default"] = function() { + return 1; +}; +module.exports["named"] = function() { + return 2; +}; + +class MyClass { + static someStaticMethod() { + return "static method"; + } +} + +module.exports.MyClass = MyClass; diff --git a/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json new file mode 100644 index 0000000000..4765d25d27 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "@denotest/cjs-default-export", + "version": "1.0.0" +} diff --git a/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/index.mjs b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/index.mjs new file mode 100644 index 0000000000..11e545ae54 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/index.mjs @@ -0,0 +1,17 @@ +import defaultImport, { MyClass } from "@denotest/cjs-default-export"; +import * as namespaceImport from "@denotest/cjs-default-export"; +import localDefaultImport from "./local.cjs"; +import * as localNamespaceImport from "./local.cjs"; + +console.log("Node esm importing node cjs"); +console.log("==========================="); +console.log(defaultImport); +console.log(localDefaultImport); +console.log(namespaceImport); +console.log(localNamespaceImport); +console.log("==========================="); +console.log(MyClass.someStaticMethod()); + +export default function() { + return defaultImport.default() * 5; +} diff --git a/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/local.cjs b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/local.cjs new file mode 100644 index 0000000000..8d2772dc64 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/local.cjs @@ -0,0 +1,9 @@ +Object.defineProperty(module.exports, "__esModule", { + value: true +}); +module.exports["default"] = function() { + return 3; +}; +module.exports["named"] = function() { + return 4; +}; diff --git a/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json new file mode 100644 index 0000000000..1840767994 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "@denotest/esm-import-cjs-default", + "version": "1.0.0", + "dependencies": { + "@denotest/cjs-default-export": "^1.0.0" + } +}