From e511022c7445cc22193edb1626c77d9674935425 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Wed, 19 Jul 2023 10:30:04 +0200 Subject: [PATCH] feat(ext/node): properly segregate node globals (#19307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code run within Deno-mode and Node-mode should have access to a slightly different set of globals. Previously this was done through a compile time code-transform for Node-mode, but this is not ideal and has many edge cases, for example Node's globalThis having a different identity than Deno's globalThis. This commit makes the `globalThis` of the entire runtime a semi-proxy. This proxy returns a different set of globals depending on the caller's mode. This is not a full proxy, because it is shadowed by "real" properties on globalThis. This is done to avoid the overhead of a full proxy for all globalThis operations. The globals between Deno-mode and Node-mode are now properly segregated. This means that code running in Deno-mode will not have access to Node's globals, and vice versa. Deleting a managed global in Deno-mode will NOT delete the corresponding global in Node-mode, and vice versa. --------- Co-authored-by: Bartek IwaƄczuk Co-authored-by: Aapo Alasuutari --- cli/cache/node.rs | 106 +--- cli/factory.rs | 4 +- cli/js.rs | 1 - cli/module_loader.rs | 6 +- cli/node.rs | 117 +---- cli/standalone/mod.rs | 4 +- cli/tests/node_compat/polyfill_globals.js | 25 + cli/tests/node_compat/runner.ts | 1 + .../testdata/npm/compare_globals/main.out | 17 +- .../testdata/npm/compare_globals/main.ts | 39 +- .../@denotest/globals/1.0.0/index.d.ts | 8 +- .../registry/@denotest/globals/1.0.0/index.js | 22 +- .../run/with_package_json/npm_binary/main.out | 3 + ext/node/analyze.rs | 133 +---- ext/node/build.rs | 10 - ext/node/global.rs | 483 ++++++++++++++++++ ext/node/lib.rs | 59 ++- ext/node/polyfills/00_globals.js | 70 +-- ext/node/polyfills/01_require.js | 16 +- ext/node/polyfills/02_init.js | 14 +- runtime/js/99_main.js | 8 +- 21 files changed, 671 insertions(+), 475 deletions(-) create mode 100644 cli/tests/node_compat/polyfill_globals.js delete mode 100644 ext/node/build.rs create mode 100644 ext/node/global.rs diff --git a/cli/cache/node.rs b/cli/cache/node.rs index 825bdfef4a..1637cbc786 100644 --- a/cli/cache/node.rs +++ b/cli/cache/node.rs @@ -12,26 +12,12 @@ use super::FastInsecureHasher; pub static NODE_ANALYSIS_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { - table_initializer: concat!( - "CREATE TABLE IF NOT EXISTS cjsanalysiscache ( + table_initializer: "CREATE TABLE IF NOT EXISTS cjsanalysiscache ( specifier TEXT PRIMARY KEY, source_hash TEXT NOT NULL, data TEXT NOT NULL );", - "CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx - ON cjsanalysiscache(specifier);", - "CREATE TABLE IF NOT EXISTS esmglobalscache ( - specifier TEXT PRIMARY KEY, - source_hash TEXT NOT NULL, - data TEXT NOT NULL - );", - "CREATE UNIQUE INDEX IF NOT EXISTS esmglobalscacheidx - ON esmglobalscache(specifier);", - ), - on_version_change: concat!( - "DELETE FROM cjsanalysiscache;", - "DELETE FROM esmglobalscache;", - ), + on_version_change: "DELETE FROM cjsanalysiscache;", preheat_queries: &[], on_failure: CacheFailure::InMemory, }; @@ -91,29 +77,6 @@ impl NodeAnalysisCache { cjs_analysis, )); } - - pub fn get_esm_analysis( - &self, - specifier: &str, - expected_source_hash: &str, - ) -> Option> { - Self::ensure_ok( - self.inner.get_esm_analysis(specifier, expected_source_hash), - ) - } - - pub fn set_esm_analysis( - &self, - specifier: &str, - source_hash: &str, - top_level_decls: &Vec, - ) { - Self::ensure_ok(self.inner.set_esm_analysis( - specifier, - source_hash, - top_level_decls, - )) - } } #[derive(Clone)] @@ -172,54 +135,6 @@ impl NodeAnalysisCacheInner { )?; Ok(()) } - - pub fn get_esm_analysis( - &self, - specifier: &str, - expected_source_hash: &str, - ) -> Result>, AnyError> { - let query = " - SELECT - data - FROM - esmglobalscache - WHERE - specifier=?1 - AND source_hash=?2 - LIMIT 1"; - let res = self.conn.query_row( - query, - params![specifier, &expected_source_hash], - |row| { - let top_level_decls: String = row.get(0)?; - let decls: Vec = serde_json::from_str(&top_level_decls)?; - Ok(decls) - }, - )?; - Ok(res) - } - - pub fn set_esm_analysis( - &self, - specifier: &str, - source_hash: &str, - top_level_decls: &Vec, - ) -> Result<(), AnyError> { - let sql = " - INSERT OR REPLACE INTO - esmglobalscache (specifier, source_hash, data) - VALUES - (?1, ?2, ?3)"; - self.conn.execute( - sql, - params![ - specifier, - &source_hash, - &serde_json::to_string(top_level_decls)?, - ], - )?; - Ok(()) - } } #[cfg(test)] @@ -245,23 +160,10 @@ mod test { assert_eq!(actual_cjs_analysis.exports, cjs_analysis.exports); assert_eq!(actual_cjs_analysis.reexports, cjs_analysis.reexports); - assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none()); - let esm_analysis = vec!["esm1".to_string()]; - cache - .set_esm_analysis("file.js", "2", &esm_analysis) - .unwrap(); - assert!(cache.get_esm_analysis("file.js", "3").unwrap().is_none()); // different hash - let actual_esm_analysis = - cache.get_esm_analysis("file.js", "2").unwrap().unwrap(); - assert_eq!(actual_esm_analysis, esm_analysis); - // adding when already exists should not cause issue cache .set_cjs_analysis("file.js", "2", &cjs_analysis) .unwrap(); - cache - .set_esm_analysis("file.js", "2", &esm_analysis) - .unwrap(); // recreating with same cli version should still have it let conn = cache.conn.recreate_with_version("1.0.0"); @@ -270,14 +172,10 @@ mod test { cache.get_cjs_analysis("file.js", "2").unwrap().unwrap(); assert_eq!(actual_analysis.exports, cjs_analysis.exports); assert_eq!(actual_analysis.reexports, cjs_analysis.reexports); - let actual_esm_analysis = - cache.get_esm_analysis("file.js", "2").unwrap().unwrap(); - assert_eq!(actual_esm_analysis, esm_analysis); // now changing the cli version should clear it let conn = cache.conn.recreate_with_version("2.0.0"); let cache = NodeAnalysisCacheInner::new(conn); assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none()); - assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none()); } } diff --git a/cli/factory.rs b/cli/factory.rs index 4e8da49d5e..cb835181c1 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -25,7 +25,7 @@ use crate::module_loader::CjsResolutionStore; use crate::module_loader::CliModuleLoaderFactory; use crate::module_loader::ModuleLoadPreparer; use crate::module_loader::NpmModuleLoader; -use crate::node::CliCjsEsmCodeAnalyzer; +use crate::node::CliCjsCodeAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::npm::create_npm_fs_resolver; use crate::npm::CliNpmRegistryApi; @@ -475,7 +475,7 @@ impl CliFactory { let caches = self.caches()?; let node_analysis_cache = NodeAnalysisCache::new(caches.node_analysis_db()); - let cjs_esm_analyzer = CliCjsEsmCodeAnalyzer::new(node_analysis_cache); + let cjs_esm_analyzer = CliCjsCodeAnalyzer::new(node_analysis_cache); Ok(Arc::new(NodeCodeTranslator::new( cjs_esm_analyzer, diff --git a/cli/js.rs b/cli/js.rs index e3a5b94be7..6a312a2066 100644 --- a/cli/js.rs +++ b/cli/js.rs @@ -28,7 +28,6 @@ mod tests { if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) { throw Error("bad"); } - console.log("we have console.log!!!"); "#, ) .unwrap(); diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 54efcd9dd8..8395016b3a 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -786,10 +786,8 @@ impl NpmModuleLoader { permissions, )? } else { - // only inject node globals for esm - self - .node_code_translator - .esm_code_with_node_globals(specifier, &code)? + // esm code is untouched + code }; Ok(ModuleCodeSource { code: code.into(), diff --git a/cli/node.rs b/cli/node.rs index 8b54d0d422..75e0d9ef91 100644 --- a/cli/node.rs +++ b/cli/node.rs @@ -1,24 +1,17 @@ // 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::CjsAnalysis; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; -use deno_ast::ParsedSource; -use deno_ast::SourceRanged; use deno_core::error::AnyError; use deno_runtime::deno_node::analyze::CjsAnalysis as ExtNodeCjsAnalysis; -use deno_runtime::deno_node::analyze::CjsEsmCodeAnalyzer; +use deno_runtime::deno_node::analyze::CjsCodeAnalyzer; use deno_runtime::deno_node::analyze::NodeCodeTranslator; use crate::cache::NodeAnalysisCache; use crate::util::fs::canonicalize_path_maybe_not_exists; -pub type CliNodeCodeTranslator = NodeCodeTranslator; +pub type CliNodeCodeTranslator = NodeCodeTranslator; /// Resolves a specifier that is pointing into a node_modules folder. /// @@ -39,11 +32,11 @@ pub fn resolve_specifier_into_node_modules( .unwrap_or_else(|| specifier.clone()) } -pub struct CliCjsEsmCodeAnalyzer { +pub struct CliCjsCodeAnalyzer { cache: NodeAnalysisCache, } -impl CliCjsEsmCodeAnalyzer { +impl CliCjsCodeAnalyzer { pub fn new(cache: NodeAnalysisCache) -> Self { Self { cache } } @@ -86,7 +79,7 @@ impl CliCjsEsmCodeAnalyzer { } } -impl CjsEsmCodeAnalyzer for CliCjsEsmCodeAnalyzer { +impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { fn analyze_cjs( &self, specifier: &ModuleSpecifier, @@ -98,104 +91,4 @@ impl CjsEsmCodeAnalyzer for CliCjsEsmCodeAnalyzer { reexports: analysis.reexports, }) } - - fn analyze_esm_top_level_decls( - &self, - specifier: &ModuleSpecifier, - source: &str, - ) -> Result, AnyError> { - // 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. - let source_hash = NodeAnalysisCache::compute_source_hash(source); - if let Some(decls) = self - .cache - .get_esm_analysis(specifier.as_str(), &source_hash) - { - Ok(HashSet::from_iter(decls)) - } else { - let parsed_source = deno_ast::parse_program(deno_ast::ParseParams { - specifier: specifier.to_string(), - text_info: deno_ast::SourceTextInfo::from_string(source.to_string()), - 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)?; - self.cache.set_esm_analysis( - specifier.as_str(), - &source_hash, - &top_level_decls.clone().into_iter().collect::>(), - ); - Ok(top_level_decls) - } - } -} - -fn analyze_top_level_decls( - parsed_source: &ParsedSource, -) -> Result, AnyError> { - 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); - } - } - - 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 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 - } } diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 15abf77ec8..d08df4b12a 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -13,7 +13,7 @@ use crate::file_fetcher::get_source_from_data_url; use crate::http_util::HttpClient; use crate::module_loader::CjsResolutionStore; use crate::module_loader::NpmModuleLoader; -use crate::node::CliCjsEsmCodeAnalyzer; +use crate::node::CliCjsCodeAnalyzer; use crate::npm::create_npm_fs_resolver; use crate::npm::CliNpmRegistryApi; use crate::npm::CliNpmResolver; @@ -366,7 +366,7 @@ pub async fn run( let cjs_resolutions = Arc::new(CjsResolutionStore::default()); let cache_db = Caches::new(deno_dir_provider.clone()); let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); - let cjs_esm_code_analyzer = CliCjsEsmCodeAnalyzer::new(node_analysis_cache); + let cjs_esm_code_analyzer = CliCjsCodeAnalyzer::new(node_analysis_cache); let node_code_translator = Arc::new(NodeCodeTranslator::new( cjs_esm_code_analyzer, fs.clone(), diff --git a/cli/tests/node_compat/polyfill_globals.js b/cli/tests/node_compat/polyfill_globals.js new file mode 100644 index 0000000000..493cec87a9 --- /dev/null +++ b/cli/tests/node_compat/polyfill_globals.js @@ -0,0 +1,25 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import process from "node:process"; +import { Buffer } from "node:buffer"; +import { + clearImmediate, + clearInterval, + clearTimeout, + setImmediate, + setInterval, + setTimeout, +} from "node:timers"; +import { performance } from "node:perf_hooks"; +import console from "node:console"; +globalThis.Buffer = Buffer; +globalThis.clearImmediate = clearImmediate; +globalThis.clearInterval = clearInterval; +globalThis.clearTimeout = clearTimeout; +globalThis.console = console; +globalThis.global = globalThis; +globalThis.performance = performance; +globalThis.process = process; +globalThis.setImmediate = setImmediate; +globalThis.setInterval = setInterval; +globalThis.setTimeout = setTimeout; +delete globalThis.window; diff --git a/cli/tests/node_compat/runner.ts b/cli/tests/node_compat/runner.ts index f12cc69b02..93fca65482 100644 --- a/cli/tests/node_compat/runner.ts +++ b/cli/tests/node_compat/runner.ts @@ -1,4 +1,5 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import "./polyfill_globals.js"; import { createRequire } from "node:module"; const file = Deno.args[0]; if (!file) { diff --git a/cli/tests/testdata/npm/compare_globals/main.out b/cli/tests/testdata/npm/compare_globals/main.out index 2868341683..46900b8828 100644 --- a/cli/tests/testdata/npm/compare_globals/main.out +++ b/cli/tests/testdata/npm/compare_globals/main.out @@ -4,7 +4,20 @@ Download http://localhost:4545/npm/registry/@denotest/globals/1.0.0.tgz Download http://localhost:4545/npm/registry/@types/node/node-18.8.2.tgz Check file:///[WILDCARD]/npm/compare_globals/main.ts true +true [] -5 -undefined +false +function +function +function undefined +false +false +true +true +true +true +false +false +bar +bar diff --git a/cli/tests/testdata/npm/compare_globals/main.ts b/cli/tests/testdata/npm/compare_globals/main.ts index 0468404a88..8d3ae1ea03 100644 --- a/cli/tests/testdata/npm/compare_globals/main.ts +++ b/cli/tests/testdata/npm/compare_globals/main.ts @@ -2,6 +2,8 @@ import * as globals from "npm:@denotest/globals"; console.log(globals.global === globals.globalThis); +// @ts-expect-error even though these are the same object, they have different types +console.log(globals.globalThis === globalThis); console.log(globals.process.execArgv); type AssertTrue = never; @@ -13,15 +15,36 @@ type _TestHasNodeJsGlobal = NodeJS.Architecture; const controller = new AbortController(); controller.abort("reason"); // in the NodeJS declaration it doesn't have a reason +// Some globals are not the same between Node and Deno. +// @ts-expect-error incompatible types between Node and Deno +console.log(globalThis.setTimeout === globals.getSetTimeout()); + // Super edge case where some Node code deletes a global where the // Node code has its own global and the Deno code has the same global, // but it's different. Basically if some Node code deletes // one of these globals then we don't want it to suddenly inherit -// the Deno global. -globals.withNodeGlobalThis((nodeGlobalThis: any) => { - (globalThis as any).setTimeout = 5; - console.log(setTimeout); - delete nodeGlobalThis["setTimeout"]; - console.log(nodeGlobalThis["setTimeout"]); // should be undefined - console.log(globalThis["setTimeout"]); // should be undefined -}); +// the Deno global (or touch the Deno global at all). +console.log(typeof globalThis.setTimeout); +console.log(typeof globals.getSetTimeout()); +globals.deleteSetTimeout(); +console.log(typeof globalThis.setTimeout); +console.log(typeof globals.getSetTimeout()); + +// In Deno, the process global is not defined, but in Node it is. +console.log("process" in globalThis); +console.log( + Object.getOwnPropertyDescriptor(globalThis, "process") !== undefined, +); +globals.checkProcessGlobal(); + +// In Deno, the window global is defined, but in Node it is not. +console.log("window" in globalThis); +console.log( + Object.getOwnPropertyDescriptor(globalThis, "window") !== undefined, +); +globals.checkWindowGlobal(); + +// "Non-managed" globals are shared between Node and Deno. +(globalThis as any).foo = "bar"; +console.log((globalThis as any).foo); +console.log(globals.getFoo()); diff --git a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts index 3f3eeb92af..1bbb820470 100644 --- a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts +++ b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts @@ -12,4 +12,10 @@ type _TestHasProcessGlobal = AssertTrue< typeof globalThis extends { process: any } ? true : false >; -export function withNodeGlobalThis(action: (global: typeof globalThis) => void): void; +export function deleteSetTimeout(): void; +export function getSetTimeout(): typeof setTimeout; + +export function checkProcessGlobal(): void; +export function checkWindowGlobal(): void; + +export function getFoo(): string; \ No newline at end of file diff --git a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js index daac83c664..b946bbd2aa 100644 --- a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js +++ b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js @@ -2,6 +2,24 @@ exports.globalThis = globalThis; exports.global = global; exports.process = process; -exports.withNodeGlobalThis = function (action) { - action(globalThis); +exports.deleteSetTimeout = function () { + delete globalThis.setTimeout; }; + +exports.getSetTimeout = function () { + return globalThis.setTimeout; +}; + +exports.checkProcessGlobal = function () { + console.log("process" in globalThis); + console.log(Object.getOwnPropertyDescriptor(globalThis, "process") !== undefined); +}; + +exports.checkWindowGlobal = function () { + console.log("window" in globalThis); + console.log(Object.getOwnPropertyDescriptor(globalThis, "window") !== undefined); +} + +exports.getFoo = function () { + return globalThis.foo; +} \ No newline at end of file diff --git a/cli/tests/testdata/run/with_package_json/npm_binary/main.out b/cli/tests/testdata/run/with_package_json/npm_binary/main.out index 56cdae6f94..9e8e87baeb 100644 --- a/cli/tests/testdata/run/with_package_json/npm_binary/main.out +++ b/cli/tests/testdata/run/with_package_json/npm_binary/main.out @@ -1,6 +1,9 @@ [WILDCARD]package.json file found at '[WILDCARD]with_package_json[WILDCARD]npm_binary[WILDCARD]package.json' [WILDCARD] this +[WILDCARD] is +[WILDCARD] a +[WILDCARD] test diff --git a/ext/node/analyze.rs b/ext/node/analyze.rs index eed0ceb4fe..d811122d5c 100644 --- a/ext/node/analyze.rs +++ b/ext/node/analyze.rs @@ -2,7 +2,6 @@ use std::collections::HashSet; use std::collections::VecDeque; -use std::fmt::Write; use std::path::Path; use std::path::PathBuf; @@ -19,21 +18,6 @@ use crate::NodeResolutionMode; use crate::NpmResolverRc; use crate::PackageJson; use crate::PathClean; -use crate::NODE_GLOBAL_THIS_NAME; - -static NODE_GLOBALS: &[&str] = &[ - "Buffer", - "clearImmediate", - "clearInterval", - "clearTimeout", - "console", - "global", - "process", - "setImmediate", - "setInterval", - "setTimeout", - "performance", -]; #[derive(Debug, Clone)] pub struct CjsAnalysis { @@ -42,7 +26,7 @@ pub struct CjsAnalysis { } /// Code analyzer for CJS and ESM files. -pub trait CjsEsmCodeAnalyzer { +pub trait CjsCodeAnalyzer { /// Analyzes CommonJs code for exports and reexports, which is /// then used to determine the wrapper ESM module exports. fn analyze_cjs( @@ -50,58 +34,30 @@ pub trait CjsEsmCodeAnalyzer { specifier: &ModuleSpecifier, source: &str, ) -> Result; - - /// Analyzes ESM code for top level declarations. This is used - /// to help inform injecting node specific globals into Node ESM - /// code. For example, if a top level `setTimeout` function exists - /// then we don't want to inject a `setTimeout` declaration. - /// - /// Note: This will go away in the future once we do this all in v8. - fn analyze_esm_top_level_decls( - &self, - specifier: &ModuleSpecifier, - source: &str, - ) -> Result, AnyError>; } -pub struct NodeCodeTranslator { - cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer, +pub struct NodeCodeTranslator { + cjs_code_analyzer: TCjsCodeAnalyzer, fs: deno_fs::FileSystemRc, node_resolver: NodeResolverRc, npm_resolver: NpmResolverRc, } -impl - NodeCodeTranslator -{ +impl NodeCodeTranslator { pub fn new( - cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer, + cjs_code_analyzer: TCjsCodeAnalyzer, fs: deno_fs::FileSystemRc, node_resolver: NodeResolverRc, npm_resolver: NpmResolverRc, ) -> Self { Self { - cjs_esm_code_analyzer, + cjs_code_analyzer, fs, node_resolver, npm_resolver, } } - /// Resolves the code to be used when executing Node specific ESM code. - /// - /// Note: This will go away in the future once we do this all in v8. - pub fn esm_code_with_node_globals( - &self, - specifier: &ModuleSpecifier, - source: &str, - ) -> Result { - let top_level_decls = self - .cjs_esm_code_analyzer - .analyze_esm_top_level_decls(specifier, source)?; - Ok(esm_code_from_top_level_decls(source, &top_level_decls)) - } - /// Translates given CJS module into ESM. This function will perform static /// analysis on the file to find defined exports and reexports. /// @@ -117,7 +73,7 @@ impl let mut temp_var_count = 0; let mut handled_reexports: HashSet = HashSet::default(); - let analysis = self.cjs_esm_code_analyzer.analyze_cjs(specifier, source)?; + let analysis = self.cjs_code_analyzer.analyze_cjs(specifier, source)?; let mut source = vec![ r#"import {createRequire as __internalCreateRequire} from "node:module"; @@ -169,7 +125,7 @@ impl })?; { let analysis = self - .cjs_esm_code_analyzer + .cjs_code_analyzer .analyze_cjs(&reexport_specifier, &reexport_file_text)?; for reexport in analysis.reexports { @@ -328,42 +284,6 @@ impl } } -fn esm_code_from_top_level_decls( - file_text: &str, - top_level_decls: &HashSet, -) -> String { - 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; - 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(); - } - - // 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); - - result -} - static RESERVED_WORDS: Lazy> = Lazy::new(|| { HashSet::from([ "abstract", @@ -526,43 +446,6 @@ fn escape_for_double_quote_string(text: &str) -> String { mod tests { use super::*; - #[test] - fn test_esm_code_with_node_globals() { - let r = esm_code_from_top_level_decls( - "export const x = 1;", - &HashSet::from(["x".to_string()]), - ); - assert!( - r.contains(&format!("var globalThis = {};", NODE_GLOBAL_THIS_NAME,)) - ); - 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_from_top_level_decls( - "#!/usr/bin/env node\nexport const x = 1;", - &HashSet::from(["x".to_string()]), - ); - 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;var performance = globalThis.performance;\n", - "export const x = 1;" - ), - NODE_GLOBAL_THIS_NAME, - ) - ); - } - #[test] fn test_add_export() { let mut temp_var_count = 0; diff --git a/ext/node/build.rs b/ext/node/build.rs deleted file mode 100644 index e9b960cab2..0000000000 --- a/ext/node/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -fn main() { - // we use a changing variable name to make it harder to depend on this - let crate_version = env!("CARGO_PKG_VERSION"); - println!( - "cargo:rustc-env=NODE_GLOBAL_THIS_NAME=__DENO_NODE_GLOBAL_THIS_{}__", - crate_version.replace('.', "_") - ); -} diff --git a/ext/node/global.rs b/ext/node/global.rs new file mode 100644 index 0000000000..cdcaee39b1 --- /dev/null +++ b/ext/node/global.rs @@ -0,0 +1,483 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::rc::Rc; + +use deno_core::v8; +use deno_core::v8::GetPropertyNamesArgs; +use deno_core::v8::MapFnTo; + +use crate::NodeResolver; + +// NOTE(bartlomieju): somehow calling `.map_fn_to()` multiple times on a function +// returns two different pointers. That shouldn't be the case as `.map_fn_to()` +// creates a thin wrapper that is a pure function. @piscisaureus suggests it +// might be a bug in Rust compiler; so for now we just create and store +// these mapped functions per-thread. We should revisit it in the future and +// ideally remove altogether. +thread_local! { + pub static GETTER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = getter.map_fn_to(); + pub static SETTER_MAP_FN: v8::GenericNamedPropertySetterCallback<'static> = setter.map_fn_to(); + pub static QUERY_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = query.map_fn_to(); + pub static DELETER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = deleter.map_fn_to(); + pub static ENUMERATOR_MAP_FN: v8::GenericNamedPropertyEnumeratorCallback<'static> = enumerator.map_fn_to(); + pub static DEFINER_MAP_FN: v8::GenericNamedPropertyDefinerCallback<'static> = definer.map_fn_to(); + pub static DESCRIPTOR_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = descriptor.map_fn_to(); +} + +/// Convert an ASCII string to a UTF-16 byte encoding of the string. +const fn str_to_utf16(s: &str) -> [u16; N] { + let mut out = [0_u16; N]; + let mut i = 0; + let bytes = s.as_bytes(); + assert!(N == bytes.len()); + while i < bytes.len() { + assert!(bytes[i] < 128, "only works for ASCII strings"); + out[i] = bytes[i] as u16; + i += 1; + } + out +} + +// ext/node changes the global object to be a proxy object that intercepts all +// property accesses for globals that are different between Node and Deno and +// dynamically returns a different value depending on if the accessing code is +// in node_modules/ or not. +// +// To make this performant, a v8 named property handler is used, that only +// intercepts property accesses for properties that are not already present on +// the global object (it is non-masking). This means that in the common case, +// when a user accesses a global that is the same between Node and Deno (like +// Uint8Array or fetch), the proxy overhead is avoided. +// +// The Deno and Node specific globals are stored in objects in the internal +// fields of the proxy object. The first internal field is the object storing +// the Deno specific globals, the second internal field is the object storing +// the Node specific globals. +// +// These are the globals that are handled: +// - Buffer (node only) +// - clearImmediate (node only) +// - clearInterval (both, but different implementation) +// - clearTimeout (both, but different implementation) +// - console (both, but different implementation) +// - global (node only) +// - performance (both, but different implementation) +// - process (node only) +// - setImmediate (node only) +// - setInterval (both, but different implementation) +// - setTimeout (both, but different implementation) +// - window (deno only) + +// UTF-16 encodings of the managed globals. THIS LIST MUST BE SORTED. +#[rustfmt::skip] +const MANAGED_GLOBALS: [&[u16]; 12] = [ + &str_to_utf16::<6>("Buffer"), + &str_to_utf16::<14>("clearImmediate"), + &str_to_utf16::<13>("clearInterval"), + &str_to_utf16::<12>("clearTimeout"), + &str_to_utf16::<7>("console"), + &str_to_utf16::<6>("global"), + &str_to_utf16::<11>("performance"), + &str_to_utf16::<7>("process"), + &str_to_utf16::<12>("setImmediate"), + &str_to_utf16::<11>("setInterval"), + &str_to_utf16::<10>("setTimeout"), + &str_to_utf16::<6>("window"), +]; + +const SHORTEST_MANAGED_GLOBAL: usize = 6; +const LONGEST_MANAGED_GLOBAL: usize = 14; + +#[derive(Debug, Clone, Copy)] +enum Mode { + Deno, + Node, +} + +pub fn global_template_middleware<'s>( + _scope: &mut v8::HandleScope<'s, ()>, + template: v8::Local<'s, v8::ObjectTemplate>, +) -> v8::Local<'s, v8::ObjectTemplate> { + // The internal field layout is as follows: + // 0: Reflect.get + // 1: Reflect.set + // 2: An object containing the Deno specific globals + // 3: An object containing the Node specific globals + assert_eq!(template.internal_field_count(), 0); + template.set_internal_field_count(4); + + let mut config = v8::NamedPropertyHandlerConfiguration::new().flags( + v8::PropertyHandlerFlags::NON_MASKING + | v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT, + ); + + config = GETTER_MAP_FN.with(|getter| config.getter_raw(*getter)); + config = SETTER_MAP_FN.with(|setter| config.setter_raw(*setter)); + config = QUERY_MAP_FN.with(|query| config.query_raw(*query)); + config = DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter)); + config = + ENUMERATOR_MAP_FN.with(|enumerator| config.enumerator_raw(*enumerator)); + config = DEFINER_MAP_FN.with(|definer| config.definer_raw(*definer)); + config = + DESCRIPTOR_MAP_FN.with(|descriptor| config.descriptor_raw(*descriptor)); + + template.set_named_property_handler(config); + + template +} + +pub fn global_object_middleware<'s>( + scope: &mut v8::HandleScope<'s>, + global: v8::Local<'s, v8::Object>, +) { + // ensure the global object is not Object.prototype + let object_key = + v8::String::new_external_onebyte_static(scope, b"Object").unwrap(); + let object = global + .get(scope, object_key.into()) + .unwrap() + .to_object(scope) + .unwrap(); + let prototype_key = + v8::String::new_external_onebyte_static(scope, b"prototype").unwrap(); + let object_prototype = object + .get(scope, prototype_key.into()) + .unwrap() + .to_object(scope) + .unwrap(); + assert_ne!(global, object_prototype); + + // get the Reflect.get and Reflect.set functions + let reflect_key = + v8::String::new_external_onebyte_static(scope, b"Reflect").unwrap(); + let reflect = global + .get(scope, reflect_key.into()) + .unwrap() + .to_object(scope) + .unwrap(); + let get_key = v8::String::new_external_onebyte_static(scope, b"get").unwrap(); + let reflect_get = reflect.get(scope, get_key.into()).unwrap(); + assert!(reflect_get.is_function()); + let set_key = v8::String::new_external_onebyte_static(scope, b"set").unwrap(); + let reflect_set = reflect.get(scope, set_key.into()).unwrap(); + assert!(reflect_set.is_function()); + + // globalThis.__bootstrap.ext_node_denoGlobals and + // globalThis.__bootstrap.ext_node_nodeGlobals are the objects that contain + // the Deno and Node specific globals respectively. If they do not yet exist + // on the global object, create them as null prototype objects. + let bootstrap_key = + v8::String::new_external_onebyte_static(scope, b"__bootstrap").unwrap(); + let bootstrap = match global.get(scope, bootstrap_key.into()) { + Some(value) if value.is_object() => value.to_object(scope).unwrap(), + Some(value) if value.is_undefined() => { + let null = v8::null(scope); + let obj = + v8::Object::with_prototype_and_properties(scope, null.into(), &[], &[]); + global.set(scope, bootstrap_key.into(), obj.into()); + obj + } + _ => panic!("__bootstrap should not be tampered with"), + }; + let deno_globals_key = + v8::String::new_external_onebyte_static(scope, b"ext_node_denoGlobals") + .unwrap(); + let deno_globals = match bootstrap.get(scope, deno_globals_key.into()) { + Some(value) if value.is_object() => value, + Some(value) if value.is_undefined() => { + let null = v8::null(scope); + let obj = + v8::Object::with_prototype_and_properties(scope, null.into(), &[], &[]) + .into(); + bootstrap.set(scope, deno_globals_key.into(), obj); + obj + } + _ => panic!("__bootstrap.ext_node_denoGlobals should not be tampered with"), + }; + let node_globals_key = + v8::String::new_external_onebyte_static(scope, b"ext_node_nodeGlobals") + .unwrap(); + let node_globals = match bootstrap.get(scope, node_globals_key.into()) { + Some(value) if value.is_object() => value, + Some(value) if value.is_undefined() => { + let null = v8::null(scope); + let obj = + v8::Object::with_prototype_and_properties(scope, null.into(), &[], &[]) + .into(); + bootstrap.set(scope, node_globals_key.into(), obj); + obj + } + _ => panic!("__bootstrap.ext_node_nodeGlobals should not be tampered with"), + }; + + // set the internal fields + assert!(global.set_internal_field(0, reflect_get)); + assert!(global.set_internal_field(1, reflect_set)); + assert!(global.set_internal_field(2, deno_globals)); + assert!(global.set_internal_field(3, node_globals)); +} + +fn is_managed_key( + scope: &mut v8::HandleScope, + key: v8::Local, +) -> bool { + let Ok(str): Result, _> = key.try_into() else { + return false; + }; + let len = str.length(); + + #[allow(clippy::manual_range_contains)] + if len < SHORTEST_MANAGED_GLOBAL || len > LONGEST_MANAGED_GLOBAL { + return false; + } + let buf = &mut [0u16; LONGEST_MANAGED_GLOBAL]; + let written = str.write( + scope, + buf.as_mut_slice(), + 0, + v8::WriteOptions::NO_NULL_TERMINATION, + ); + assert_eq!(written, len); + MANAGED_GLOBALS.binary_search(&&buf[..len]).is_ok() +} + +fn current_mode(scope: &mut v8::HandleScope) -> Mode { + let Some(v8_string) = v8::StackTrace::current_script_name_or_source_url(scope) else { + return Mode::Deno; + }; + let string = v8_string.to_rust_string_lossy(scope); + // TODO: don't require parsing the specifier + let Ok(specifier) = deno_core::ModuleSpecifier::parse(&string) else { + return Mode::Deno; + }; + let op_state = deno_core::JsRuntime::op_state_from(scope); + let op_state = op_state.borrow(); + let Some(node_resolver) = op_state.try_borrow::>() else { + return Mode::Deno; + }; + if node_resolver.in_npm_package(&specifier) { + Mode::Node + } else { + Mode::Deno + } +} + +fn inner_object<'s>( + scope: &mut v8::HandleScope<'s>, + real_global_object: v8::Local<'s, v8::Object>, + mode: Mode, +) -> v8::Local<'s, v8::Object> { + let value = match mode { + Mode::Deno => real_global_object.get_internal_field(scope, 2).unwrap(), + Mode::Node => real_global_object.get_internal_field(scope, 3).unwrap(), + }; + v8::Local::::try_from(value).unwrap() +} + +fn real_global_object<'s>( + scope: &mut v8::HandleScope<'s>, +) -> v8::Local<'s, v8::Object> { + let context = scope.get_current_context(); + let global = context.global(scope); + let global = global + .get_prototype(scope) + .unwrap() + .to_object(scope) + .unwrap(); + global +} + +pub fn getter<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + + let this = args.this(); + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let reflect_get: v8::Local = real_global_object + .get_internal_field(scope, 0) + .unwrap() + .try_into() + .unwrap(); + + let inner = inner_object(scope, real_global_object, mode); + let undefined = v8::undefined(scope); + let Some(value) = reflect_get.call( + scope, + undefined.into(), + &[inner.into(), key.into(), this.into()], + ) else { + return; + }; + + rv.set(value); +} + +pub fn setter<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + value: v8::Local<'s, v8::Value>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + + let this = args.this(); + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let reflect_set: v8::Local = real_global_object + .get_internal_field(scope, 1) + .unwrap() + .try_into() + .unwrap(); + let inner = inner_object(scope, real_global_object, mode); + let undefined = v8::undefined(scope); + + let Some(success) = reflect_set.call( + scope, + undefined.into(), + &[inner.into(), key.into(), value, this.into()], + ) else { + return; + }; + + rv.set(success); +} + +pub fn query<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + _args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let inner = inner_object(scope, real_global_object, mode); + + let Some(true) = inner.has_own_property(scope, key) else { + return; + }; + + let Some(attributes) = inner.get_property_attributes(scope, key.into()) else { + return; + }; + + rv.set_uint32(attributes.as_u32()); +} + +pub fn deleter<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let inner = inner_object(scope, real_global_object, mode); + + let Some(success) = inner.delete(scope, key.into()) else { + return; + }; + + if args.should_throw_on_error() && !success { + let message = v8::String::new(scope, "Cannot delete property").unwrap(); + let exception = v8::Exception::type_error(scope, message); + scope.throw_exception(exception); + return; + } + + rv.set_bool(success); +} + +pub fn enumerator<'s>( + scope: &mut v8::HandleScope<'s>, + _args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let inner = inner_object(scope, real_global_object, mode); + + let Some(array) = inner.get_property_names(scope, GetPropertyNamesArgs::default()) else { + return; + }; + + rv.set(array.into()); +} + +pub fn definer<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let inner = inner_object(scope, real_global_object, mode); + let Some(success) = inner.define_property(scope, key, descriptor) else { + return; + }; + + if args.should_throw_on_error() && !success { + let message = v8::String::new(scope, "Cannot define property").unwrap(); + let exception = v8::Exception::type_error(scope, message); + scope.throw_exception(exception); + return; + } + + rv.set_bool(success); +} + +pub fn descriptor<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + _args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + if !is_managed_key(scope, key) { + return; + }; + + let real_global_object = real_global_object(scope); + let mode = current_mode(scope); + + let scope = &mut v8::TryCatch::new(scope); + + let inner = inner_object(scope, real_global_object, mode); + let Some(descriptor) = inner.get_own_property_descriptor(scope, key) else { + scope.rethrow().expect("to have caught an exception"); + return; + }; + + if descriptor.is_undefined() { + return; + } + + rv.set(descriptor); +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 3698e5376a..89ee87cd32 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -13,6 +13,7 @@ use deno_core::serde_v8; use deno_core::url::Url; #[allow(unused_imports)] use deno_core::v8; +use deno_core::v8::ExternalReference; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_fs::sync::MaybeSend; @@ -25,6 +26,7 @@ use once_cell::sync::Lazy; pub mod analyze; pub mod errors; +mod global; mod ops; mod package_json; mod path; @@ -41,6 +43,9 @@ pub use resolution::NodeResolution; pub use resolution::NodeResolutionMode; pub use resolution::NodeResolver; +use crate::global::global_object_middleware; +use crate::global::global_template_middleware; + pub trait NodePermissions { fn check_net_url( &mut self, @@ -112,8 +117,6 @@ pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { ) -> Result<(), AnyError>; } -pub const NODE_GLOBAL_THIS_NAME: &str = env!("NODE_GLOBAL_THIS_NAME"); - pub static NODE_ENV_VAR_ALLOWLIST: Lazy> = Lazy::new(|| { // The full list of environment variables supported by Node.js is available // at https://nodejs.org/api/cli.html#environment-variables @@ -510,7 +513,49 @@ deno_core::extension!(deno_node, npm_resolver, ))) } - } + }, + global_template_middleware = global_template_middleware, + global_object_middleware = global_object_middleware, + customizer = |ext: &mut deno_core::ExtensionBuilder| { + let mut external_references = Vec::with_capacity(7); + + global::GETTER_MAP_FN.with(|getter| { + external_references.push(ExternalReference { + named_getter: *getter, + }); + }); + global::SETTER_MAP_FN.with(|setter| { + external_references.push(ExternalReference { + named_setter: *setter, + }); + }); + global::QUERY_MAP_FN.with(|query| { + external_references.push(ExternalReference { + named_getter: *query, + }); + }); + global::DELETER_MAP_FN.with(|deleter| { + external_references.push(ExternalReference { + named_getter: *deleter, + },); + }); + global::ENUMERATOR_MAP_FN.with(|enumerator| { + external_references.push(ExternalReference { + enumerator: *enumerator, + }); + }); + global::DEFINER_MAP_FN.with(|definer| { + external_references.push(ExternalReference { + named_definer: *definer, + }); + }); + global::DESCRIPTOR_MAP_FN.with(|descriptor| { + external_references.push(ExternalReference { + named_getter: *descriptor, + }); + }); + ext.external_references(external_references); + }, ); pub fn initialize_runtime( @@ -524,16 +569,12 @@ pub fn initialize_runtime( "undefined".to_string() }; let source_code = format!( - r#"(function loadBuiltinNodeModules(nodeGlobalThisName, usesLocalNodeModulesDir, argv0) {{ + r#"(function loadBuiltinNodeModules(usesLocalNodeModulesDir, argv0) {{ Deno[Deno.internal].node.initialize( - nodeGlobalThisName, usesLocalNodeModulesDir, argv0 ); - // Make the nodeGlobalThisName unconfigurable here. - Object.defineProperty(globalThis, nodeGlobalThisName, {{ configurable: false }}); - }})('{}', {}, {});"#, - NODE_GLOBAL_THIS_NAME, uses_local_node_modules_dir, argv0 + }})({uses_local_node_modules_dir}, {argv0});"#, ); js_runtime.execute_script(located_script_name!(), source_code.into())?; diff --git a/ext/node/polyfills/00_globals.js b/ext/node/polyfills/00_globals.js index 9952d86aa7..c3f064a3f9 100644 --- a/ext/node/polyfills/00_globals.js +++ b/ext/node/polyfills/00_globals.js @@ -2,71 +2,5 @@ // deno-lint-ignore-file -const primordials = globalThis.__bootstrap.primordials; -const { - ArrayPrototypeFilter, - Proxy, - ReflectDefineProperty, - ReflectDeleteProperty, - ReflectGet, - ReflectGetOwnPropertyDescriptor, - ReflectHas, - ReflectOwnKeys, - ReflectSet, - Set, - SetPrototypeHas, -} = primordials; - -const nodeGlobals = {}; -const nodeGlobalThis = new Proxy(globalThis, { - get(target, prop) { - if (ReflectHas(nodeGlobals, prop)) { - return ReflectGet(nodeGlobals, prop); - } else { - return ReflectGet(target, prop); - } - }, - set(target, prop, value) { - if (ReflectHas(nodeGlobals, prop)) { - return ReflectSet(nodeGlobals, prop, value); - } else { - return ReflectSet(target, prop, value); - } - }, - has(target, prop) { - return ReflectHas(nodeGlobals, prop) || ReflectHas(target, prop); - }, - deleteProperty(target, prop) { - const nodeDeleted = ReflectDeleteProperty(nodeGlobals, prop); - const targetDeleted = ReflectDeleteProperty(target, prop); - return nodeDeleted || targetDeleted; - }, - ownKeys(target) { - const targetKeys = ReflectOwnKeys(target); - const nodeGlobalsKeys = ReflectOwnKeys(nodeGlobals); - const nodeGlobalsKeySet = new Set(nodeGlobalsKeys); - return [ - ...ArrayPrototypeFilter( - targetKeys, - (k) => !SetPrototypeHas(nodeGlobalsKeySet, k), - ), - ...nodeGlobalsKeys, - ]; - }, - defineProperty(target, prop, desc) { - if (ReflectHas(nodeGlobals, prop)) { - return ReflectDefineProperty(nodeGlobals, prop, desc); - } else { - return ReflectDefineProperty(target, prop, desc); - } - }, - getOwnPropertyDescriptor(target, prop) { - if (ReflectHas(nodeGlobals, prop)) { - return ReflectGetOwnPropertyDescriptor(nodeGlobals, prop); - } else { - return ReflectGetOwnPropertyDescriptor(target, prop); - } - }, -}); - -export { nodeGlobals, nodeGlobalThis }; +export const denoGlobals = globalThis.__bootstrap.ext_node_denoGlobals; +export const nodeGlobals = globalThis.__bootstrap.ext_node_nodeGlobals; diff --git a/ext/node/polyfills/01_require.js b/ext/node/polyfills/01_require.js index 3ca4ab4283..696c1ef5c9 100644 --- a/ext/node/polyfills/01_require.js +++ b/ext/node/polyfills/01_require.js @@ -40,7 +40,6 @@ const { Error, TypeError, } = primordials; -import { nodeGlobalThis } from "ext:deno_node/00_globals.js"; import _httpAgent from "ext:deno_node/_http_agent.mjs"; import _httpOutgoing from "ext:deno_node/_http_outgoing.ts"; import _streamDuplex from "ext:deno_node/internal/streams/duplex.mjs"; @@ -342,7 +341,7 @@ function tryPackage(requestPath, exts, isMain, originalPath) { err.requestPath = originalPath; throw err; } else { - nodeGlobalThis.process.emitWarning( + process.emitWarning( `Invalid 'main' field in '${packageJsonPath}' of '${pkg}'. ` + "Please either fix that or report it to the module author", "DeprecationWarning", @@ -414,7 +413,7 @@ function getExportsForCircularRequire(module) { } function emitCircularRequireWarning(prop) { - nodeGlobalThis.process.emitWarning( + process.emitWarning( `Accessing non-existent property '${String(prop)}' of module exports ` + "inside circular dependency", ); @@ -704,7 +703,7 @@ Module._load = function (request, parent, isMain) { const module = cachedModule || new Module(filename, parent); if (isMain) { - nodeGlobalThis.process.mainModule = module; + process.mainModule = module; mainModule = module; module.id = "."; } @@ -913,9 +912,7 @@ Module.prototype.require = function (id) { }; Module.wrapper = [ - // We provide the non-standard APIs in the CommonJS wrapper - // to avoid exposing them in global namespace. - "(function (exports, require, module, __filename, __dirname, globalThis) { const { Buffer, clearImmediate, clearInterval, clearTimeout, console, global, process, setImmediate, setInterval, setTimeout, performance} = globalThis; var window = undefined; (function () {", + "(function (exports, require, module, __filename, __dirname) { (function () {", "\n}).call(this); })", ]; Module.wrap = function (script) { @@ -950,7 +947,7 @@ function wrapSafe( const wrapper = Module.wrap(content); const [f, err] = core.evalContext(wrapper, `file://${filename}`); if (err) { - if (nodeGlobalThis.process.mainModule === cjsModuleInstance) { + if (process.mainModule === cjsModuleInstance) { enrichCJSError(err.thrown); } if (isEsmSyntaxError(err.thrown)) { @@ -988,7 +985,6 @@ Module.prototype._compile = function (content, filename) { this, filename, dirname, - nodeGlobalThis, ); if (requireDepth === 0) { statCache = null; @@ -1050,7 +1046,7 @@ Module._extensions[".node"] = function (module, filename) { if (filename.endsWith("fsevents.node")) { throw new Error("Using fsevents module is currently not supported"); } - module.exports = ops.op_napi_open(filename, nodeGlobalThis); + module.exports = ops.op_napi_open(filename, globalThis); }; function createRequireFromPath(filename) { diff --git a/ext/node/polyfills/02_init.js b/ext/node/polyfills/02_init.js index 8a6e0195f9..d73d5d822a 100644 --- a/ext/node/polyfills/02_init.js +++ b/ext/node/polyfills/02_init.js @@ -4,15 +4,12 @@ const internals = globalThis.__bootstrap.internals; const requireImpl = internals.requireImpl; -const primordials = globalThis.__bootstrap.primordials; -const { ObjectDefineProperty } = primordials; -import { nodeGlobals, nodeGlobalThis } from "ext:deno_node/00_globals.js"; +import { nodeGlobals } from "ext:deno_node/00_globals.js"; import "node:module"; let initialized = false; function initialize( - nodeGlobalThisName, usesLocalNodeModulesDir, argv0, ) { @@ -29,20 +26,13 @@ function initialize( nodeGlobals.clearInterval = nativeModuleExports["timers"].clearInterval; nodeGlobals.clearTimeout = nativeModuleExports["timers"].clearTimeout; nodeGlobals.console = nativeModuleExports["console"]; - nodeGlobals.global = nodeGlobalThis; + nodeGlobals.global = globalThis; nodeGlobals.process = nativeModuleExports["process"]; nodeGlobals.setImmediate = nativeModuleExports["timers"].setImmediate; nodeGlobals.setInterval = nativeModuleExports["timers"].setInterval; nodeGlobals.setTimeout = nativeModuleExports["timers"].setTimeout; nodeGlobals.performance = nativeModuleExports["perf_hooks"].performance; - // add a hidden global for the esm code to use in order to reliably - // get node's globalThis - ObjectDefineProperty(globalThis, nodeGlobalThisName, { - enumerable: false, - configurable: true, - value: nodeGlobalThis, - }); // FIXME(bartlomieju): not nice to depend on `Deno` namespace here // but it's the only way to get `args` and `version` and this point. internals.__bootstrapNodeProcess(argv0, Deno.args, Deno.version); diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 0c89897017..483ca9012c 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -413,14 +413,16 @@ function promiseRejectMacrotaskCallback() { } let hasBootstrapped = false; +// Delete the `console` object that V8 automaticaly adds onto the global wrapper +// object on context creation. We don't want this console object to shadow the +// `console` object exposed by the ext/node globalThis proxy. +delete globalThis.console; // Set up global properties shared by main and worker runtime. ObjectDefineProperties(globalThis, windowOrWorkerGlobalScope); // FIXME(bartlomieju): temporarily add whole `Deno.core` to // `Deno[Deno.internal]` namespace. It should be removed and only necessary // methods should be left there. -ObjectAssign(internals, { - core, -}); +ObjectAssign(internals, { core }); const internalSymbol = Symbol("Deno.internal"); const finalDenoNs = { internal: internalSymbol,