mirror of
https://github.com/denoland/deno.git
synced 2024-10-30 09:08:00 -04:00
a65ce33fab
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").
254 lines
7.2 KiB
Rust
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)
|
|
}
|