// 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 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.132.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 = 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 = Lazy::new(|| format!("{}node/global.ts", NODE_COMPAT_URL.as_str())); pub static GLOBAL_URL: Lazy = Lazy::new(|| Url::parse(&GLOBAL_URL_STR).unwrap()); static MODULE_URL_STR: Lazy = Lazy::new(|| format!("{}node/module.ts", NODE_COMPAT_URL.as_str())); pub static MODULE_URL: Lazy = Lazy::new(|| Url::parse(&MODULE_URL_STR).unwrap()); static COMPAT_IMPORT_URL: Lazy = Lazy::new(|| Url::parse("flags:compat").unwrap()); /// Provide imports into a module graph when the compat flag is true. pub fn get_node_imports() -> Vec<(Url, Vec)> { vec![(COMPAT_IMPORT_URL.clone(), vec![GLOBAL_URL_STR.clone()])] } fn try_resolve_builtin_module(specifier: &str) -> Option { 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 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 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 { 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::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) }