diff --git a/.eslintrc.js b/.eslintrc.js index 5a63c79371c984..43e41026b2007e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -328,6 +328,7 @@ module.exports = { DecompressionStream: 'readable', fetch: 'readable', FormData: 'readable', + navigator: 'readable', ReadableStream: 'readable', ReadableStreamDefaultReader: 'readable', ReadableStreamBYOBReader: 'readable', diff --git a/doc/api/globals.md b/doc/api/globals.md index 8285064c2e25e4..83aa62c86ea790 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -583,6 +583,41 @@ The `MessagePort` class. See [`MessagePort`][] for more details. This variable may appear to be global but is not. See [`module`][]. +## `Navigator` + +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +An implementation of the [Navigator API][]. + +## `navigator` + +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +An implementation of [`window.navigator`][]. + +### `navigator.hardwareConcurrency` + +<!-- YAML +added: REPLACEME +--> + +* {number} + +The `navigator.hardwareConcurrency` read-only property returns the number of +logical processors available to the current Node.js instance. + +```js +console.log(`This process is running on ${navigator.hardwareConcurrency}`); +``` + ## `PerformanceEntry` <!-- YAML @@ -998,6 +1033,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][]. [CommonJS module]: modules.md [ECMAScript module]: esm.md +[Navigator API]: https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object [Web Crypto API]: webcrypto.md [`--no-experimental-fetch`]: cli.md#--no-experimental-fetch [`--no-experimental-global-customevent`]: cli.md#--no-experimental-global-customevent @@ -1057,6 +1093,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][]. [`setInterval`]: timers.md#setintervalcallback-delay-args [`setTimeout`]: timers.md#settimeoutcallback-delay-args [`structuredClone`]: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone +[`window.navigator`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator [buffer section]: buffer.md [built-in objects]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects [module system documentation]: modules.md diff --git a/lib/.eslintrc.yaml b/lib/.eslintrc.yaml index cc4fa1975016eb..2b77dce6967d7b 100644 --- a/lib/.eslintrc.yaml +++ b/lib/.eslintrc.yaml @@ -75,6 +75,10 @@ rules: message: Use `const { MessageEvent } = require('internal/worker/io');` instead of the global. - name: MessagePort message: Use `const { MessagePort } = require('internal/worker/io');` instead of the global. + - name: Navigator + message: Use `const { Navigator } = require('internal/navigator');` instead of the global. + - name: navigator + message: Use `const { navigator } = require('internal/navigator');` instead of the global. - name: PerformanceEntry message: Use `const { PerformanceEntry } = require('perf_hooks');` instead of the global. - name: PerformanceMark diff --git a/lib/internal/bootstrap/web/exposed-window-or-worker.js b/lib/internal/bootstrap/web/exposed-window-or-worker.js index 8dc77493e1f152..3cc555b82f35a7 100644 --- a/lib/internal/bootstrap/web/exposed-window-or-worker.js +++ b/lib/internal/bootstrap/web/exposed-window-or-worker.js @@ -2,7 +2,7 @@ /** * This file exposes web interfaces that is defined with the WebIDL - * Exposed=(Window,Worker) extended attribute or exposed in + * Exposed=Window + Exposed=(Window,Worker) extended attribute or exposed in * WindowOrWorkerGlobalScope mixin. * See more details at https://webidl.spec.whatwg.org/#Exposed and * https://html.spec.whatwg.org/multipage/webappapis.html#windoworworkerglobalscope. @@ -55,6 +55,10 @@ 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/navigator.js b/lib/internal/navigator.js new file mode 100644 index 00000000000000..3b8343cd7ed6f8 --- /dev/null +++ b/lib/internal/navigator.js @@ -0,0 +1,49 @@ +'use strict'; + +const { + ObjectDefineProperties, + Symbol, +} = primordials; + +const { + ERR_ILLEGAL_CONSTRUCTOR, +} = require('internal/errors').codes; + +const { + kEnumerableProperty, +} = require('internal/util'); + +const { + getAvailableParallelism, +} = internalBinding('os'); + +const kInitialize = Symbol('kInitialize'); + +class Navigator { + // Private properties are used to avoid brand validations. + #availableParallelism; + + constructor() { + if (arguments[0] === kInitialize) { + return; + } + throw ERR_ILLEGAL_CONSTRUCTOR(); + } + + /** + * @return {number} + */ + get hardwareConcurrency() { + this.#availableParallelism ??= getAvailableParallelism(); + return this.#availableParallelism; + } +} + +ObjectDefineProperties(Navigator.prototype, { + hardwareConcurrency: kEnumerableProperty, +}); + +module.exports = { + navigator: new Navigator(kInitialize), + Navigator, +}; diff --git a/test/common/index.js b/test/common/index.js index 2a8ef3a3b183cc..c10dea59319264 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -309,6 +309,14 @@ if (global.gc) { knownGlobals.push(global.gc); } +if (global.navigator) { + knownGlobals.push(global.navigator); +} + +if (global.Navigator) { + knownGlobals.push(global.Navigator); +} + if (global.Performance) { knownGlobals.push(global.Performance); } diff --git a/test/parallel/test-global.js b/test/parallel/test-global.js index 9ac9b4f7287327..0a8fecd240c8a1 100644 --- a/test/parallel/test-global.js +++ b/test/parallel/test-global.js @@ -58,6 +58,7 @@ builtinModules.forEach((moduleName) => { 'structuredClone', 'fetch', 'crypto', + 'navigator', ]; assert.deepStrictEqual(new Set(Object.keys(global)), new Set(expected)); } diff --git a/test/parallel/test-navigator.js b/test/parallel/test-navigator.js new file mode 100644 index 00000000000000..300446f2c5dcdc --- /dev/null +++ b/test/parallel/test-navigator.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const is = { + number: (value, key) => { + assert(!Number.isNaN(value), `${key} should not be NaN`); + assert.strictEqual(typeof value, 'number'); + }, +}; + +is.number(+navigator.hardwareConcurrency, 'hardwareConcurrency'); +is.number(navigator.hardwareConcurrency, 'hardwareConcurrency'); +assert.ok(navigator.hardwareConcurrency > 0);