// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use std::collections::HashSet; use deno_ast::swc::common::SyntaxContext; use deno_ast::view::Node; use deno_ast::view::NodeTrait; use deno_ast::ModuleSpecifier; use deno_ast::ParsedSource; use deno_ast::SourceRanged; use deno_core::error::AnyError; use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME; use std::fmt::Write; use crate::cache::NodeAnalysisCache; static NODE_GLOBALS: &[&str] = &[ "Buffer", "clearImmediate", "clearInterval", "clearTimeout", "console", "global", "process", "setImmediate", "setInterval", "setTimeout", ]; // TODO(dsherret): this code is way more inefficient than it needs to be. // // In the future, we should disable capturing tokens & scope analysis // and instead only use swc's APIs to go through the portions of the tree // that we know will affect the global scope while still ensuring that // `var` decls are taken into consideration. pub fn esm_code_with_node_globals( analysis_cache: &NodeAnalysisCache, specifier: &ModuleSpecifier, code: String, ) -> Result { let source_hash = NodeAnalysisCache::compute_source_hash(&code); let text_info = deno_ast::SourceTextInfo::from_string(code); let top_level_decls = if let Some(decls) = analysis_cache.get_esm_analysis(specifier.as_str(), &source_hash) { HashSet::from_iter(decls) } else { let parsed_source = deno_ast::parse_program(deno_ast::ParseParams { specifier: specifier.to_string(), text_info: text_info.clone(), media_type: deno_ast::MediaType::from_specifier(specifier), capture_tokens: true, scope_analysis: true, maybe_syntax: None, })?; let top_level_decls = analyze_top_level_decls(&parsed_source)?; analysis_cache.set_esm_analysis( specifier.as_str(), &source_hash, &top_level_decls.clone().into_iter().collect(), ); top_level_decls }; let mut globals = Vec::with_capacity(NODE_GLOBALS.len()); let has_global_this = top_level_decls.contains("globalThis"); for global in NODE_GLOBALS.iter() { if !top_level_decls.contains(&global.to_string()) { globals.push(*global); } } let mut result = String::new(); let global_this_expr = NODE_GLOBAL_THIS_NAME.as_str(); let global_this_expr = if has_global_this { global_this_expr } else { write!(result, "var globalThis = {global_this_expr};").unwrap(); "globalThis" }; for global in globals { write!(result, "var {global} = {global_this_expr}.{global};").unwrap(); } let file_text = text_info.text_str(); // strip the shebang let file_text = if file_text.starts_with("#!/") { let start_index = file_text.find('\n').unwrap_or(file_text.len()); &file_text[start_index..] } else { file_text }; result.push_str(file_text); Ok(result) } fn analyze_top_level_decls( parsed_source: &ParsedSource, ) -> Result, AnyError> { let top_level_context = parsed_source.top_level_context(); parsed_source.with_view(|program| { let mut results = HashSet::new(); visit_children(program.into(), top_level_context, &mut results); Ok(results) }) } fn visit_children( node: Node, top_level_context: SyntaxContext, results: &mut HashSet, ) { if let Node::Ident(ident) = node { if ident.ctxt() == top_level_context && is_local_declaration_ident(node) { results.insert(ident.sym().to_string()); } } for child in node.children() { visit_children(child, top_level_context, results); } } fn is_local_declaration_ident(node: Node) -> bool { if let Some(parent) = node.parent() { match parent { Node::BindingIdent(decl) => decl.id.range().contains(&node.range()), Node::ClassDecl(decl) => decl.ident.range().contains(&node.range()), Node::ClassExpr(decl) => decl .ident .as_ref() .map(|i| i.range().contains(&node.range())) .unwrap_or(false), Node::TsInterfaceDecl(decl) => decl.id.range().contains(&node.range()), Node::FnDecl(decl) => decl.ident.range().contains(&node.range()), Node::FnExpr(decl) => decl .ident .as_ref() .map(|i| i.range().contains(&node.range())) .unwrap_or(false), Node::TsModuleDecl(decl) => decl.id.range().contains(&node.range()), Node::TsNamespaceDecl(decl) => decl.id.range().contains(&node.range()), Node::VarDeclarator(decl) => decl.name.range().contains(&node.range()), Node::ImportNamedSpecifier(decl) => { decl.local.range().contains(&node.range()) } Node::ImportDefaultSpecifier(decl) => { decl.local.range().contains(&node.range()) } Node::ImportStarAsSpecifier(decl) => decl.range().contains(&node.range()), Node::KeyValuePatProp(decl) => decl.key.range().contains(&node.range()), Node::AssignPatProp(decl) => decl.key.range().contains(&node.range()), _ => false, } } else { false } } #[cfg(test)] mod tests { use super::*; #[test] fn test_esm_code_with_node_globals() { let r = esm_code_with_node_globals( &NodeAnalysisCache::new_in_memory(), &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(), "export const x = 1;".to_string(), ) .unwrap(); assert!(r.contains(&format!( "var globalThis = {};", NODE_GLOBAL_THIS_NAME.as_str() ))); assert!(r.contains("var process = globalThis.process;")); assert!(r.contains("export const x = 1;")); } #[test] fn test_esm_code_with_node_globals_with_shebang() { let r = esm_code_with_node_globals( &NodeAnalysisCache::new_in_memory(), &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(), "#!/usr/bin/env node\nexport const x = 1;".to_string(), ) .unwrap(); assert_eq!( r, format!( concat!( "var globalThis = {}", ";var Buffer = globalThis.Buffer;", "var clearImmediate = globalThis.clearImmediate;var clearInterval = globalThis.clearInterval;", "var clearTimeout = globalThis.clearTimeout;var console = globalThis.console;", "var global = globalThis.global;var process = globalThis.process;", "var setImmediate = globalThis.setImmediate;var setInterval = globalThis.setInterval;", "var setTimeout = globalThis.setTimeout;\n", "export const x = 1;" ), NODE_GLOBAL_THIS_NAME.as_str(), ) ); } }