1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00
denoland-deno/cli/compat/mod.rs
Bartek Iwańczuk a65ce33fab
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").
2022-02-27 14:38:45 +01:00

254 lines
7.2 KiB
Rust

// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
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;
// TODO(bartlomieju): this needs to be bumped manually for
// each release, a better mechanism is preferable, but it's a quick and dirty
// solution to avoid printing `X-Deno-Warning` headers when the compat layer is
// downloaded
static STD_URL_STR: &str = "https://deno.land/std@0.127.0/";
static SUPPORTED_MODULES: &[&str] = &[
"assert",
"assert/strict",
"async_hooks",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"dns",
"domain",
"events",
"fs",
"fs/promises",
"http",
"https",
"module",
"net",
"os",
"path",
"path/posix",
"path/win32",
"perf_hooks",
"process",
"querystring",
"readline",
"stream",
"stream/promises",
"stream/web",
"string_decoder",
"sys",
"timers",
"timers/promises",
"tls",
"tty",
"url",
"util",
"util/types",
"v8",
"vm",
"zlib",
];
static NODE_COMPAT_URL: Lazy<String> = Lazy::new(|| {
std::env::var("DENO_NODE_COMPAT_URL")
.map(String::into)
.ok()
.unwrap_or_else(|| STD_URL_STR.to_string())
});
static GLOBAL_URL_STR: Lazy<String> =
Lazy::new(|| format!("{}node/global.ts", NODE_COMPAT_URL.as_str()));
pub(crate) static GLOBAL_URL: Lazy<Url> =
Lazy::new(|| Url::parse(&GLOBAL_URL_STR).unwrap());
static MODULE_URL_STR: Lazy<String> =
Lazy::new(|| format!("{}node/module.ts", NODE_COMPAT_URL.as_str()));
pub(crate) static MODULE_URL: Lazy<Url> =
Lazy::new(|| Url::parse(&MODULE_URL_STR).unwrap());
static COMPAT_IMPORT_URL: Lazy<Url> =
Lazy::new(|| Url::parse("flags:compat").unwrap());
/// Provide imports into a module graph when the compat flag is true.
pub(crate) fn get_node_imports() -> Vec<(Url, Vec<String>)> {
vec![(COMPAT_IMPORT_URL.clone(), vec![GLOBAL_URL_STR.clone()])]
}
fn try_resolve_builtin_module(specifier: &str) -> Option<Url> {
if SUPPORTED_MODULES.contains(&specifier) {
let module_url =
format!("{}node/{}.ts", NODE_COMPAT_URL.as_str(), specifier);
Some(Url::parse(&module_url).unwrap())
} else {
None
}
}
pub(crate) fn load_cjs_module(
js_runtime: &mut JsRuntime,
module: &str,
main: bool,
) -> Result<(), AnyError> {
let source_code = &format!(
r#"(async function loadCjsModule(module) {{
const Module = await import("{module_loader}");
Module.default._load(module, null, {main});
}})('{module}');"#,
module_loader = MODULE_URL_STR.as_str(),
main = main,
module = escape_for_single_quote_string(module),
);
js_runtime.execute_script(&located_script_name!(), source_code)?;
Ok(())
}
pub(crate) fn add_global_require(
js_runtime: &mut JsRuntime,
main_module: &str,
) -> Result<(), AnyError> {
let source_code = &format!(
r#"(async function setupGlobalRequire(main) {{
const Module = await import("{}");
const require = Module.createRequire(main);
globalThis.require = require;
}})('{}');"#,
MODULE_URL_STR.as_str(),
escape_for_single_quote_string(main_module),
);
js_runtime.execute_script(&located_script_name!(), source_code)?;
Ok(())
}
fn escape_for_single_quote_string(text: &str) -> String {
text.replace('\\', r"\\").replace('\'', r"\'")
}
pub fn setup_builtin_modules(
js_runtime: &mut JsRuntime,
) -> Result<(), AnyError> {
let mut script = String::new();
for module in SUPPORTED_MODULES {
// skipping the modules that contains '/' as they are not available in NodeJS repl as well
if !module.contains('/') {
script = format!("{}const {} = require('{}');\n", script, module, module);
}
}
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)
}