1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

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>
This commit is contained in:
Luca Casonato 2023-07-19 10:30:04 +02:00 committed by Bartek Iwańczuk
parent 30feee81e1
commit b8021744b3
No known key found for this signature in database
GPG key ID: 0C6BCDDC3B3AD750
21 changed files with 671 additions and 475 deletions

106
cli/cache/node.rs vendored
View file

@ -12,26 +12,12 @@ use super::FastInsecureHasher;
pub static NODE_ANALYSIS_CACHE_DB: CacheDBConfiguration = pub static NODE_ANALYSIS_CACHE_DB: CacheDBConfiguration =
CacheDBConfiguration { CacheDBConfiguration {
table_initializer: concat!( table_initializer: "CREATE TABLE IF NOT EXISTS cjsanalysiscache (
"CREATE TABLE IF NOT EXISTS cjsanalysiscache (
specifier TEXT PRIMARY KEY, specifier TEXT PRIMARY KEY,
source_hash TEXT NOT NULL, source_hash TEXT NOT NULL,
data TEXT NOT NULL data TEXT NOT NULL
);", );",
"CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx on_version_change: "DELETE FROM cjsanalysiscache;",
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;",
),
preheat_queries: &[], preheat_queries: &[],
on_failure: CacheFailure::InMemory, on_failure: CacheFailure::InMemory,
}; };
@ -91,29 +77,6 @@ impl NodeAnalysisCache {
cjs_analysis, 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)] #[derive(Clone)]
@ -172,54 +135,6 @@ impl NodeAnalysisCacheInner {
)?; )?;
Ok(()) 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)] #[cfg(test)]
@ -245,23 +160,10 @@ mod test {
assert_eq!(actual_cjs_analysis.exports, cjs_analysis.exports); assert_eq!(actual_cjs_analysis.exports, cjs_analysis.exports);
assert_eq!(actual_cjs_analysis.reexports, cjs_analysis.reexports); 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 // adding when already exists should not cause issue
cache cache
.set_cjs_analysis("file.js", "2", &cjs_analysis) .set_cjs_analysis("file.js", "2", &cjs_analysis)
.unwrap(); .unwrap();
cache
.set_esm_analysis("file.js", "2", &esm_analysis)
.unwrap();
// recreating with same cli version should still have it // recreating with same cli version should still have it
let conn = cache.conn.recreate_with_version("1.0.0"); 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(); cache.get_cjs_analysis("file.js", "2").unwrap().unwrap();
assert_eq!(actual_analysis.exports, cjs_analysis.exports); assert_eq!(actual_analysis.exports, cjs_analysis.exports);
assert_eq!(actual_analysis.reexports, cjs_analysis.reexports); 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 // now changing the cli version should clear it
let conn = cache.conn.recreate_with_version("2.0.0"); let conn = cache.conn.recreate_with_version("2.0.0");
let cache = NodeAnalysisCacheInner::new(conn); let cache = NodeAnalysisCacheInner::new(conn);
assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none()); assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none());
assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none());
} }
} }

View file

@ -25,7 +25,7 @@ use crate::module_loader::CjsResolutionStore;
use crate::module_loader::CliModuleLoaderFactory; use crate::module_loader::CliModuleLoaderFactory;
use crate::module_loader::ModuleLoadPreparer; use crate::module_loader::ModuleLoadPreparer;
use crate::module_loader::NpmModuleLoader; use crate::module_loader::NpmModuleLoader;
use crate::node::CliCjsEsmCodeAnalyzer; use crate::node::CliCjsCodeAnalyzer;
use crate::node::CliNodeCodeTranslator; use crate::node::CliNodeCodeTranslator;
use crate::npm::create_npm_fs_resolver; use crate::npm::create_npm_fs_resolver;
use crate::npm::CliNpmRegistryApi; use crate::npm::CliNpmRegistryApi;
@ -475,7 +475,7 @@ impl CliFactory {
let caches = self.caches()?; let caches = self.caches()?;
let node_analysis_cache = let node_analysis_cache =
NodeAnalysisCache::new(caches.node_analysis_db()); 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( Ok(Arc::new(NodeCodeTranslator::new(
cjs_esm_analyzer, cjs_esm_analyzer,

View file

@ -28,7 +28,6 @@ mod tests {
if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) { if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) {
throw Error("bad"); throw Error("bad");
} }
console.log("we have console.log!!!");
"#, "#,
) )
.unwrap(); .unwrap();

View file

@ -786,10 +786,8 @@ impl NpmModuleLoader {
permissions, permissions,
)? )?
} else { } else {
// only inject node globals for esm // esm code is untouched
self code
.node_code_translator
.esm_code_with_node_globals(specifier, &code)?
}; };
Ok(ModuleCodeSource { Ok(ModuleCodeSource {
code: code.into(), code: code.into(),

View file

@ -1,24 +1,17 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // 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::CjsAnalysis;
use deno_ast::MediaType; use deno_ast::MediaType;
use deno_ast::ModuleSpecifier; use deno_ast::ModuleSpecifier;
use deno_ast::ParsedSource;
use deno_ast::SourceRanged;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_runtime::deno_node::analyze::CjsAnalysis as ExtNodeCjsAnalysis; 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 deno_runtime::deno_node::analyze::NodeCodeTranslator;
use crate::cache::NodeAnalysisCache; use crate::cache::NodeAnalysisCache;
use crate::util::fs::canonicalize_path_maybe_not_exists; 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. /// 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()) .unwrap_or_else(|| specifier.clone())
} }
pub struct CliCjsEsmCodeAnalyzer { pub struct CliCjsCodeAnalyzer {
cache: NodeAnalysisCache, cache: NodeAnalysisCache,
} }
impl CliCjsEsmCodeAnalyzer { impl CliCjsCodeAnalyzer {
pub fn new(cache: NodeAnalysisCache) -> Self { pub fn new(cache: NodeAnalysisCache) -> Self {
Self { cache } Self { cache }
} }
@ -86,7 +79,7 @@ impl CliCjsEsmCodeAnalyzer {
} }
} }
impl CjsEsmCodeAnalyzer for CliCjsEsmCodeAnalyzer { impl CjsCodeAnalyzer for CliCjsCodeAnalyzer {
fn analyze_cjs( fn analyze_cjs(
&self, &self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
@ -98,104 +91,4 @@ impl CjsEsmCodeAnalyzer for CliCjsEsmCodeAnalyzer {
reexports: analysis.reexports, 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
}
} }

View file

@ -13,7 +13,7 @@ use crate::file_fetcher::get_source_from_data_url;
use crate::http_util::HttpClient; use crate::http_util::HttpClient;
use crate::module_loader::CjsResolutionStore; use crate::module_loader::CjsResolutionStore;
use crate::module_loader::NpmModuleLoader; use crate::module_loader::NpmModuleLoader;
use crate::node::CliCjsEsmCodeAnalyzer; use crate::node::CliCjsCodeAnalyzer;
use crate::npm::create_npm_fs_resolver; use crate::npm::create_npm_fs_resolver;
use crate::npm::CliNpmRegistryApi; use crate::npm::CliNpmRegistryApi;
use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolver;
@ -366,7 +366,7 @@ pub async fn run(
let cjs_resolutions = Arc::new(CjsResolutionStore::default()); let cjs_resolutions = Arc::new(CjsResolutionStore::default());
let cache_db = Caches::new(deno_dir_provider.clone()); let cache_db = Caches::new(deno_dir_provider.clone());
let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); 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( let node_code_translator = Arc::new(NodeCodeTranslator::new(
cjs_esm_code_analyzer, cjs_esm_code_analyzer,
fs.clone(), fs.clone(),

View file

@ -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;

View file

@ -1,4 +1,5 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import "./polyfill_globals.js";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
const file = Deno.args[0]; const file = Deno.args[0];
if (!file) { if (!file) {

View file

@ -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 Download http://localhost:4545/npm/registry/@types/node/node-18.8.2.tgz
Check file:///[WILDCARD]/npm/compare_globals/main.ts Check file:///[WILDCARD]/npm/compare_globals/main.ts
true true
true
[] []
5 false
undefined function
function
function
undefined undefined
false
false
true
true
true
true
false
false
bar
bar

View file

@ -2,6 +2,8 @@
import * as globals from "npm:@denotest/globals"; import * as globals from "npm:@denotest/globals";
console.log(globals.global === globals.globalThis); 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); console.log(globals.process.execArgv);
type AssertTrue<T extends true> = never; type AssertTrue<T extends true> = never;
@ -13,15 +15,36 @@ type _TestHasNodeJsGlobal = NodeJS.Architecture;
const controller = new AbortController(); const controller = new AbortController();
controller.abort("reason"); // in the NodeJS declaration it doesn't have a reason 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 // 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, // Node code has its own global and the Deno code has the same global,
// but it's different. Basically if some Node code deletes // but it's different. Basically if some Node code deletes
// one of these globals then we don't want it to suddenly inherit // one of these globals then we don't want it to suddenly inherit
// the Deno global. // the Deno global (or touch the Deno global at all).
globals.withNodeGlobalThis((nodeGlobalThis: any) => { console.log(typeof globalThis.setTimeout);
(globalThis as any).setTimeout = 5; console.log(typeof globals.getSetTimeout());
console.log(setTimeout); globals.deleteSetTimeout();
delete nodeGlobalThis["setTimeout"]; console.log(typeof globalThis.setTimeout);
console.log(nodeGlobalThis["setTimeout"]); // should be undefined console.log(typeof globals.getSetTimeout());
console.log(globalThis["setTimeout"]); // should be undefined
}); // 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());

View file

@ -12,4 +12,10 @@ type _TestHasProcessGlobal = AssertTrue<
typeof globalThis extends { process: any } ? true : false 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;

View file

@ -2,6 +2,24 @@ exports.globalThis = globalThis;
exports.global = global; exports.global = global;
exports.process = process; exports.process = process;
exports.withNodeGlobalThis = function (action) { exports.deleteSetTimeout = function () {
action(globalThis); 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;
}

View file

@ -1,6 +1,9 @@
[WILDCARD]package.json file found at '[WILDCARD]with_package_json[WILDCARD]npm_binary[WILDCARD]package.json' [WILDCARD]package.json file found at '[WILDCARD]with_package_json[WILDCARD]npm_binary[WILDCARD]package.json'
[WILDCARD] [WILDCARD]
this this
[WILDCARD]
is is
[WILDCARD]
a a
[WILDCARD]
test test

View file

@ -2,7 +2,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -19,21 +18,6 @@ use crate::NodeResolutionMode;
use crate::NpmResolverRc; use crate::NpmResolverRc;
use crate::PackageJson; use crate::PackageJson;
use crate::PathClean; 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)] #[derive(Debug, Clone)]
pub struct CjsAnalysis { pub struct CjsAnalysis {
@ -42,7 +26,7 @@ pub struct CjsAnalysis {
} }
/// Code analyzer for CJS and ESM files. /// Code analyzer for CJS and ESM files.
pub trait CjsEsmCodeAnalyzer { pub trait CjsCodeAnalyzer {
/// Analyzes CommonJs code for exports and reexports, which is /// Analyzes CommonJs code for exports and reexports, which is
/// then used to determine the wrapper ESM module exports. /// then used to determine the wrapper ESM module exports.
fn analyze_cjs( fn analyze_cjs(
@ -50,58 +34,30 @@ pub trait CjsEsmCodeAnalyzer {
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: &str, source: &str,
) -> Result<CjsAnalysis, AnyError>; ) -> 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> { pub struct NodeCodeTranslator<TCjsCodeAnalyzer: CjsCodeAnalyzer> {
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer, cjs_code_analyzer: TCjsCodeAnalyzer,
fs: deno_fs::FileSystemRc, fs: deno_fs::FileSystemRc,
node_resolver: NodeResolverRc, node_resolver: NodeResolverRc,
npm_resolver: NpmResolverRc, npm_resolver: NpmResolverRc,
} }
impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer> impl<TCjsCodeAnalyzer: CjsCodeAnalyzer> NodeCodeTranslator<TCjsCodeAnalyzer> {
NodeCodeTranslator<TCjsEsmCodeAnalyzer>
{
pub fn new( pub fn new(
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer, cjs_code_analyzer: TCjsCodeAnalyzer,
fs: deno_fs::FileSystemRc, fs: deno_fs::FileSystemRc,
node_resolver: NodeResolverRc, node_resolver: NodeResolverRc,
npm_resolver: NpmResolverRc, npm_resolver: NpmResolverRc,
) -> Self { ) -> Self {
Self { Self {
cjs_esm_code_analyzer, cjs_code_analyzer,
fs, fs,
node_resolver, node_resolver,
npm_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 /// Translates given CJS module into ESM. This function will perform static
/// analysis on the file to find defined exports and reexports. /// 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 temp_var_count = 0;
let mut handled_reexports: HashSet<String> = HashSet::default(); 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![ let mut source = vec![
r#"import {createRequire as __internalCreateRequire} from "node:module"; r#"import {createRequire as __internalCreateRequire} from "node:module";
@ -169,7 +125,7 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
})?; })?;
{ {
let analysis = self let analysis = self
.cjs_esm_code_analyzer .cjs_code_analyzer
.analyze_cjs(&reexport_specifier, &reexport_file_text)?; .analyze_cjs(&reexport_specifier, &reexport_file_text)?;
for reexport in analysis.reexports { 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(|| { static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| {
HashSet::from([ HashSet::from([
"abstract", "abstract",
@ -526,43 +446,6 @@ fn escape_for_double_quote_string(text: &str) -> String {
mod tests { mod tests {
use super::*; 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] #[test]
fn test_add_export() { fn test_add_export() {
let mut temp_var_count = 0; let mut temp_var_count = 0;

View file

@ -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('.', "_")
);
}

483
ext/node/global.rs Normal file
View file

@ -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);
}

View file

@ -13,6 +13,7 @@ use deno_core::serde_v8;
use deno_core::url::Url; use deno_core::url::Url;
#[allow(unused_imports)] #[allow(unused_imports)]
use deno_core::v8; use deno_core::v8;
use deno_core::v8::ExternalReference;
use deno_core::JsRuntime; use deno_core::JsRuntime;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_fs::sync::MaybeSend; use deno_fs::sync::MaybeSend;
@ -25,6 +26,7 @@ use once_cell::sync::Lazy;
pub mod analyze; pub mod analyze;
pub mod errors; pub mod errors;
mod global;
mod ops; mod ops;
mod package_json; mod package_json;
mod path; mod path;
@ -41,6 +43,9 @@ pub use resolution::NodeResolution;
pub use resolution::NodeResolutionMode; pub use resolution::NodeResolutionMode;
pub use resolution::NodeResolver; pub use resolution::NodeResolver;
use crate::global::global_object_middleware;
use crate::global::global_template_middleware;
pub trait NodePermissions { pub trait NodePermissions {
fn check_net_url( fn check_net_url(
&mut self, &mut self,
@ -112,8 +117,6 @@ pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync {
) -> Result<(), AnyError>; ) -> 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(|| { pub static NODE_ENV_VAR_ALLOWLIST: Lazy<HashSet<String>> = Lazy::new(|| {
// The full list of environment variables supported by Node.js is available // The full list of environment variables supported by Node.js is available
// at https://nodejs.org/api/cli.html#environment-variables // at https://nodejs.org/api/cli.html#environment-variables
@ -510,7 +513,49 @@ deno_core::extension!(deno_node,
npm_resolver, 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( pub fn initialize_runtime(
@ -524,16 +569,12 @@ pub fn initialize_runtime(
"undefined".to_string() "undefined".to_string()
}; };
let source_code = format!( let source_code = format!(
r#"(function loadBuiltinNodeModules(nodeGlobalThisName, usesLocalNodeModulesDir, argv0) {{ r#"(function loadBuiltinNodeModules(usesLocalNodeModulesDir, argv0) {{
Deno[Deno.internal].node.initialize( Deno[Deno.internal].node.initialize(
nodeGlobalThisName,
usesLocalNodeModulesDir, usesLocalNodeModulesDir,
argv0 argv0
); );
// Make the nodeGlobalThisName unconfigurable here. }})({uses_local_node_modules_dir}, {argv0});"#,
Object.defineProperty(globalThis, nodeGlobalThisName, {{ configurable: false }});
}})('{}', {}, {});"#,
NODE_GLOBAL_THIS_NAME, uses_local_node_modules_dir, argv0
); );
js_runtime.execute_script(located_script_name!(), source_code.into())?; js_runtime.execute_script(located_script_name!(), source_code.into())?;

View file

@ -2,71 +2,5 @@
// deno-lint-ignore-file // deno-lint-ignore-file
const primordials = globalThis.__bootstrap.primordials; export const denoGlobals = globalThis.__bootstrap.ext_node_denoGlobals;
const { export const nodeGlobals = globalThis.__bootstrap.ext_node_nodeGlobals;
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 };

View file

@ -40,7 +40,6 @@ const {
Error, Error,
TypeError, TypeError,
} = primordials; } = primordials;
import { nodeGlobalThis } from "ext:deno_node/00_globals.js";
import _httpAgent from "ext:deno_node/_http_agent.mjs"; import _httpAgent from "ext:deno_node/_http_agent.mjs";
import _httpOutgoing from "ext:deno_node/_http_outgoing.ts"; import _httpOutgoing from "ext:deno_node/_http_outgoing.ts";
import _streamDuplex from "ext:deno_node/internal/streams/duplex.mjs"; import _streamDuplex from "ext:deno_node/internal/streams/duplex.mjs";
@ -342,7 +341,7 @@ function tryPackage(requestPath, exts, isMain, originalPath) {
err.requestPath = originalPath; err.requestPath = originalPath;
throw err; throw err;
} else { } else {
nodeGlobalThis.process.emitWarning( process.emitWarning(
`Invalid 'main' field in '${packageJsonPath}' of '${pkg}'. ` + `Invalid 'main' field in '${packageJsonPath}' of '${pkg}'. ` +
"Please either fix that or report it to the module author", "Please either fix that or report it to the module author",
"DeprecationWarning", "DeprecationWarning",
@ -414,7 +413,7 @@ function getExportsForCircularRequire(module) {
} }
function emitCircularRequireWarning(prop) { function emitCircularRequireWarning(prop) {
nodeGlobalThis.process.emitWarning( process.emitWarning(
`Accessing non-existent property '${String(prop)}' of module exports ` + `Accessing non-existent property '${String(prop)}' of module exports ` +
"inside circular dependency", "inside circular dependency",
); );
@ -704,7 +703,7 @@ Module._load = function (request, parent, isMain) {
const module = cachedModule || new Module(filename, parent); const module = cachedModule || new Module(filename, parent);
if (isMain) { if (isMain) {
nodeGlobalThis.process.mainModule = module; process.mainModule = module;
mainModule = module; mainModule = module;
module.id = "."; module.id = ".";
} }
@ -913,9 +912,7 @@ Module.prototype.require = function (id) {
}; };
Module.wrapper = [ Module.wrapper = [
// We provide the non-standard APIs in the CommonJS wrapper "(function (exports, require, module, __filename, __dirname) { (function () {",
// 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 () {",
"\n}).call(this); })", "\n}).call(this); })",
]; ];
Module.wrap = function (script) { Module.wrap = function (script) {
@ -950,7 +947,7 @@ function wrapSafe(
const wrapper = Module.wrap(content); const wrapper = Module.wrap(content);
const [f, err] = core.evalContext(wrapper, `file://${filename}`); const [f, err] = core.evalContext(wrapper, `file://${filename}`);
if (err) { if (err) {
if (nodeGlobalThis.process.mainModule === cjsModuleInstance) { if (process.mainModule === cjsModuleInstance) {
enrichCJSError(err.thrown); enrichCJSError(err.thrown);
} }
if (isEsmSyntaxError(err.thrown)) { if (isEsmSyntaxError(err.thrown)) {
@ -988,7 +985,6 @@ Module.prototype._compile = function (content, filename) {
this, this,
filename, filename,
dirname, dirname,
nodeGlobalThis,
); );
if (requireDepth === 0) { if (requireDepth === 0) {
statCache = null; statCache = null;
@ -1050,7 +1046,7 @@ Module._extensions[".node"] = function (module, filename) {
if (filename.endsWith("fsevents.node")) { if (filename.endsWith("fsevents.node")) {
throw new Error("Using fsevents module is currently not supported"); 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) { function createRequireFromPath(filename) {

View file

@ -4,15 +4,12 @@
const internals = globalThis.__bootstrap.internals; const internals = globalThis.__bootstrap.internals;
const requireImpl = internals.requireImpl; const requireImpl = internals.requireImpl;
const primordials = globalThis.__bootstrap.primordials; import { nodeGlobals } from "ext:deno_node/00_globals.js";
const { ObjectDefineProperty } = primordials;
import { nodeGlobals, nodeGlobalThis } from "ext:deno_node/00_globals.js";
import "node:module"; import "node:module";
let initialized = false; let initialized = false;
function initialize( function initialize(
nodeGlobalThisName,
usesLocalNodeModulesDir, usesLocalNodeModulesDir,
argv0, argv0,
) { ) {
@ -29,20 +26,13 @@ function initialize(
nodeGlobals.clearInterval = nativeModuleExports["timers"].clearInterval; nodeGlobals.clearInterval = nativeModuleExports["timers"].clearInterval;
nodeGlobals.clearTimeout = nativeModuleExports["timers"].clearTimeout; nodeGlobals.clearTimeout = nativeModuleExports["timers"].clearTimeout;
nodeGlobals.console = nativeModuleExports["console"]; nodeGlobals.console = nativeModuleExports["console"];
nodeGlobals.global = nodeGlobalThis; nodeGlobals.global = globalThis;
nodeGlobals.process = nativeModuleExports["process"]; nodeGlobals.process = nativeModuleExports["process"];
nodeGlobals.setImmediate = nativeModuleExports["timers"].setImmediate; nodeGlobals.setImmediate = nativeModuleExports["timers"].setImmediate;
nodeGlobals.setInterval = nativeModuleExports["timers"].setInterval; nodeGlobals.setInterval = nativeModuleExports["timers"].setInterval;
nodeGlobals.setTimeout = nativeModuleExports["timers"].setTimeout; nodeGlobals.setTimeout = nativeModuleExports["timers"].setTimeout;
nodeGlobals.performance = nativeModuleExports["perf_hooks"].performance; 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 // FIXME(bartlomieju): not nice to depend on `Deno` namespace here
// but it's the only way to get `args` and `version` and this point. // but it's the only way to get `args` and `version` and this point.
internals.__bootstrapNodeProcess(argv0, Deno.args, Deno.version); internals.__bootstrapNodeProcess(argv0, Deno.args, Deno.version);

View file

@ -413,14 +413,16 @@ function promiseRejectMacrotaskCallback() {
} }
let hasBootstrapped = false; 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. // Set up global properties shared by main and worker runtime.
ObjectDefineProperties(globalThis, windowOrWorkerGlobalScope); ObjectDefineProperties(globalThis, windowOrWorkerGlobalScope);
// FIXME(bartlomieju): temporarily add whole `Deno.core` to // FIXME(bartlomieju): temporarily add whole `Deno.core` to
// `Deno[Deno.internal]` namespace. It should be removed and only necessary // `Deno[Deno.internal]` namespace. It should be removed and only necessary
// methods should be left there. // methods should be left there.
ObjectAssign(internals, { ObjectAssign(internals, { core });
core,
});
const internalSymbol = Symbol("Deno.internal"); const internalSymbol = Symbol("Deno.internal");
const finalDenoNs = { const finalDenoNs = {
internal: internalSymbol, internal: internalSymbol,