summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuca Casonato <hello@lcas.dev>2023-07-19 10:30:04 +0200
committerGitHub <noreply@github.com>2023-07-19 10:30:04 +0200
commite511022c7445cc22193edb1626c77d9674935425 (patch)
tree521b30eac14cd19a506c9cdfa52cde1da7211dcf
parentbf4e99cbd77087706e7ea7034bd90079c2218e2b (diff)
feat(ext/node): properly segregate node globals (#19307)
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 <biwanczuk@gmail.com> Co-authored-by: Aapo Alasuutari <aapo.alasuutari@gmail.com>
-rw-r--r--cli/cache/node.rs106
-rw-r--r--cli/factory.rs4
-rw-r--r--cli/js.rs1
-rw-r--r--cli/module_loader.rs6
-rw-r--r--cli/node.rs117
-rw-r--r--cli/standalone/mod.rs4
-rw-r--r--cli/tests/node_compat/polyfill_globals.js25
-rw-r--r--cli/tests/node_compat/runner.ts1
-rw-r--r--cli/tests/testdata/npm/compare_globals/main.out17
-rw-r--r--cli/tests/testdata/npm/compare_globals/main.ts39
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts8
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js22
-rw-r--r--cli/tests/testdata/run/with_package_json/npm_binary/main.out3
-rw-r--r--ext/node/analyze.rs133
-rw-r--r--ext/node/build.rs10
-rw-r--r--ext/node/global.rs483
-rw-r--r--ext/node/lib.rs59
-rw-r--r--ext/node/polyfills/00_globals.js70
-rw-r--r--ext/node/polyfills/01_require.js16
-rw-r--r--ext/node/polyfills/02_init.js14
-rw-r--r--runtime/js/99_main.js8
21 files changed, 671 insertions, 475 deletions
diff --git a/cli/cache/node.rs b/cli/cache/node.rs
index 825bdfef4..1637cbc78 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<Vec<String>> {
- 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<String>,
- ) {
- 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<Option<Vec<String>>, 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<String> = 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<String>,
- ) -> 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 4e8da49d5..cb835181c 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 e3a5b94be..6a312a206 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 54efcd9dd..8395016b3 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 8b54d0d42..75e0d9ef9 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<CliCjsEsmCodeAnalyzer>;
+pub type CliNodeCodeTranslator = NodeCodeTranslator<CliCjsCodeAnalyzer>;
/// 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<HashSet<String>, 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::<Vec<_>>(),
- );
- Ok(top_level_decls)
- }
- }
-}
-
-fn analyze_top_level_decls(
- parsed_source: &ParsedSource,
-) -> Result<HashSet<String>, AnyError> {
- fn visit_children(
- node: Node,
- top_level_context: SyntaxContext,
- results: &mut HashSet<String>,
- ) {
- 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 15abf77ec..d08df4b12 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 000000000..493cec87a
--- /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 f12cc69b0..93fca6548 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 286834168..46900b882 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 0468404a8..8d3ae1ea0 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<T extends true> = 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 3f3eeb92a..1bbb82047 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 daac83c66..b946bbd2a 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 56cdae6f9..9e8e87bae 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 eed0ceb4f..d811122d5 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<CjsAnalysis, AnyError>;
-
- /// 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<HashSet<String>, AnyError>;
}
-pub struct NodeCodeTranslator<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer> {
- cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer,
+pub struct NodeCodeTranslator<TCjsCodeAnalyzer: CjsCodeAnalyzer> {
+ cjs_code_analyzer: TCjsCodeAnalyzer,
fs: deno_fs::FileSystemRc,
node_resolver: NodeResolverRc,
npm_resolver: NpmResolverRc,
}
-impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
- NodeCodeTranslator<TCjsEsmCodeAnalyzer>
-{
+impl<TCjsCodeAnalyzer: CjsCodeAnalyzer> NodeCodeTranslator<TCjsCodeAnalyzer> {
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<String, AnyError> {
- 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<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
let mut temp_var_count = 0;
let mut handled_reexports: HashSet<String> = 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<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
})?;
{
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<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
}
}
-fn esm_code_from_top_level_decls(
- file_text: &str,
- top_level_decls: &HashSet<String>,
-) -> 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<HashSet<&str>> = Lazy::new(|| {
HashSet::from([
"abstract",
@@ -527,43 +447,6 @@ 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;
let mut source = vec![];
diff --git a/ext/node/build.rs b/ext/node/build.rs
deleted file mode 100644
index e9b960cab..000000000
--- 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 000000000..cdcaee39b
--- /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<const N: usize>(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<v8::Name>,
+) -> bool {
+ let Ok(str): Result<v8::Local<v8::String>, _> = 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::<Rc<NodeResolver>>() 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::<v8::Object>::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<v8::Function> = 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<v8::Function> = 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 3698e5376..89ee87cd3 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<HashSet<String>> = 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 9952d86aa..c3f064a3f 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 3ca4ab428..696c1ef5c 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 8a6e0195f..d73d5d822 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 0c8989701..483ca9012 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,