diff --git a/lib/internal/bootstrap/web/exposed-window-or-worker.js b/lib/internal/bootstrap/web/exposed-window-or-worker.js index f7298a9a9028df..37e4518a5400b5 100644 --- a/lib/internal/bootstrap/web/exposed-window-or-worker.js +++ b/lib/internal/bootstrap/web/exposed-window-or-worker.js @@ -52,10 +52,6 @@ exposeLazyInterfaces(globalThis, 'perf_hooks', [ defineReplaceableLazyAttribute(globalThis, 'perf_hooks', ['performance']); -// https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object -exposeLazyInterfaces(globalThis, 'internal/navigator', ['Navigator']); -defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false); - // https://w3c.github.io/FileAPI/#creating-revoking const { installObjectURLMethods } = require('internal/url'); installObjectURLMethods(); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 917ba90a1c8bbb..f35c75b73b84fa 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -2,6 +2,7 @@ const { ArrayPrototypeForEach, + ArrayPrototypeMap, Date, DatePrototypeGetDate, DatePrototypeGetFullYear, @@ -14,8 +15,10 @@ const { ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, SafeMap, + SafeSet, String, StringPrototypeStartsWith, + StringPrototypeToLowerCase, Symbol, SymbolAsyncDispose, SymbolDispose, @@ -78,9 +81,7 @@ function prepareExecution(options) { setupTraceCategoryState(); setupInspectorHooks(); setupWarningHandler(); - setupUndici(); - setupWebCrypto(); - setupCustomEvent(); + setupWebGlobals(); setupCodeCoverage(); setupDebugEnv(); // Process initial diagnostic reporting configuration, if present. @@ -278,13 +279,51 @@ function setupWarningHandler() { } } -// https://fetch.spec.whatwg.org/ -// https://websockets.spec.whatwg.org/ -function setupUndici() { +function setupWebGlobals() { if (getEmbedderOptions().noBrowserGlobals) { return; } + if (!getOptionValue('--no-experimental-global-webcrypto')) { + // `--no-experimental-global-webcrypto` doesn't disable the `crypto` global, it just changes what + // `globalThis.crypto` refers to. There is currently no way to disable the `crypto` global. + setupWebCrypto(); + } + + const disabledGlobalsLowercased = new SafeSet(ArrayPrototypeMap( + getOptionValue('--disable-global'), value => StringPrototypeToLowerCase(value))); + + // `CustomEvent` + if (!getOptionValue('--no-experimental-global-customevent') && + !disabledGlobalsLowercased.has('customevent')) { + setupCustomEvent(); + } + + // `navigator` + if (!disabledGlobalsLowercased.has('navigator')) { + setupNavigator(); + } + + // `fetch`, `FormData`, `Headers`, `Request`, `Response` + const setupFetch = !getOptionValue('--no-experimental-fetch') && + !disabledGlobalsLowercased.has('fetch'); + // `WebSocket` + const setupWebSocket = getOptionValue('--experimental-websocket') && + !disabledGlobalsLowercased.has('websocket'); + if (setupFetch || setupWebSocket) { + setupUndici(setupFetch, setupWebSocket); + } +} + +function setupNavigator() { + // https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object + exposeLazyInterfaces(globalThis, 'internal/navigator', ['Navigator']); + defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false); +} + +// https://fetch.spec.whatwg.org/ +// https://websockets.spec.whatwg.org/ +function setupUndici(setupFetch = true, setupWebSocket = true) { let undici; function lazyUndici() { if (undici) { @@ -308,7 +347,7 @@ function setupUndici() { }; } - if (!getOptionValue('--no-experimental-fetch')) { + if (setupFetch) { // Fetch is meant to return a Promise, but not be async. function fetch(input, init = undefined) { return lazyUndici().fetch(input, init); @@ -329,21 +368,14 @@ function setupUndici() { }); } - if (getOptionValue('--experimental-websocket')) { + if (setupWebSocket) { ObjectDefineProperties(globalThis, { WebSocket: lazyInterface('WebSocket'), }); } } -// TODO(aduh95): move this to internal/bootstrap/web/* when the CLI flag is -// removed. function setupWebCrypto() { - if (getEmbedderOptions().noBrowserGlobals || - getOptionValue('--no-experimental-global-webcrypto')) { - return; - } - if (internalBinding('config').hasOpenSSL) { defineReplaceableLazyAttribute( globalThis, @@ -384,13 +416,7 @@ function setupCodeCoverage() { } } -// TODO(daeyeon): move this to internal/bootstrap/web/* when the CLI flag is -// removed. function setupCustomEvent() { - if (getEmbedderOptions().noBrowserGlobals || - getOptionValue('--no-experimental-global-customevent')) { - return; - } const { CustomEvent } = require('internal/event_target'); exposeInterface(globalThis, 'CustomEvent', CustomEvent); } diff --git a/src/node_options.cc b/src/node_options.cc index 29cb7fc6b29b89..7cfd0f965f10bc 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -357,6 +357,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { " (default: current working directory)", &EnvironmentOptions::diagnostic_dir, kAllowedInEnvvar); + AddOption("--disable-global", + "remove global variable", + &EnvironmentOptions::disable_global, + kAllowedInEnvvar); AddOption("--dns-result-order", "set default value of verbatim in dns.lookup. Options are " "'ipv4first' (IPv4 addresses are placed before IPv6 addresses) " diff --git a/src/node_options.h b/src/node_options.h index 30955c779714ce..f473c782a6f461 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -105,6 +105,7 @@ class EnvironmentOptions : public Options { bool abort_on_uncaught_exception = false; std::vector conditions; bool detect_module = false; + std::vector disable_global; std::string dns_result_order; bool enable_source_maps = false; bool experimental_fetch = true; diff --git a/test/parallel/test-cli-disable-global.mjs b/test/parallel/test-cli-disable-global.mjs new file mode 100644 index 00000000000000..3a9f143c979eaf --- /dev/null +++ b/test/parallel/test-cli-disable-global.mjs @@ -0,0 +1,56 @@ +import { spawnPromisified } from '../common/index.mjs'; +import { describe, it } from 'node:test'; +import { strictEqual } from 'node:assert'; + + +describe('--disable-global', { concurrency: true }, () => { + it('disables one global', async () => { + const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [ + '--disable-global', 'navigator', + '--eval', 'console.log(typeof navigator)', + ]); + + strictEqual(stderr, ''); + strictEqual(stdout, 'undefined\n'); + strictEqual(code, 0); + strictEqual(signal, null); + }); + + it('disables all the globals in the supported list', async () => { + const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [ + '--disable-global', 'CustomEvent', + '--disable-global', 'fetch', + '--disable-global', 'navigator', + '--disable-global', 'WebSocket', + '--print', + `[ + 'CustomEvent', + 'fetch', 'FormData', 'Headers', 'Request', 'Response', + 'navigator', + 'WebSocket', + ].filter(name => typeof globalThis[name] !== 'undefined')`, + ]); + + strictEqual(stderr, ''); + strictEqual(stdout, '[]\n'); + strictEqual(code, 0); + strictEqual(signal, null); + }); + + it('is case insensitive', async () => { + const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [ + '--disable-global', 'customevent', + '--disable-global', 'websocket', + '--print', + `[ + 'CustomEvent', + 'WebSocket', + ].filter(name => typeof globalThis[name] !== 'undefined')`, + ]); + + strictEqual(stderr, ''); + strictEqual(stdout, '[]\n'); + strictEqual(code, 0); + strictEqual(signal, null); + }); +});