diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 16a80c2d4f410f..65e9ef39be1e77 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -138,6 +138,8 @@ function prepareExecution(options) { require('internal/dns/utils').initializeDns(); + require('internal/util/temporal').initialize(); + if (isMainThread) { assert(internalBinding('worker').isMainThread); // Worker threads will get the manifest in the message handler. diff --git a/lib/internal/util/temporal.js b/lib/internal/util/temporal.js new file mode 100644 index 00000000000000..e6b704dd3a6aa6 --- /dev/null +++ b/lib/internal/util/temporal.js @@ -0,0 +1,100 @@ +'use strict'; + +// This module is initialized in the pre-execution phase, before user code is +// run. Since the module namespace is not populated at snapshot time, this +// module should be not be imported with a destructuring pattern in a module +// header or used within top-level module code, as the properties may not yet +// be initialized. Either import this module lazily, or import the namespace +// object without destructuring and access its properties at the point of use. + +// TODO(Renegade334): Remove typecheck methods once available via V8 API. +// Replace with primordials if/when temporal_rs becomes a mandatory V8 +// build dependency and the harmony_temporal flag is removed. + +const { + ArrayPrototypeForEach, + FunctionPrototypeCall, + ObjectEntries, + ObjectGetOwnPropertyDescriptors, + SafeMap, + StringPrototypeSubstring, + StringPrototypeToUpperCase, + globalThis, + uncurryThis, +} = primordials; + +// The keys of this Map are the Temporal constructor names. +// The values are the names of the most lightweight brand-aware getter within +// each prototype, which can be called speculatively as a brand check. +const constructors = new SafeMap() + .set('Duration', 'nanoseconds') + .set('Instant', 'epochNanoseconds') + .set('PlainDate', 'calendarId') + .set('PlainDateTime', 'calendarId') + .set('PlainMonthDay', 'calendarId') + .set('PlainTime', 'nanosecond') + .set('PlainYearMonth', 'calendarId') + .set('ZonedDateTime', 'calendarId'); + +exports.initialize = () => { + const { Temporal } = globalThis; + + if (Temporal === undefined) { + exports.hasTemporal = false; + + for (const name of constructors.keys()) { + exports[`isTemporal${name}`] = () => false; + } + + return; + } + + exports.hasTemporal = true; + + for (const { 0: name, 1: brandedGetter } of constructors) { + const constructor = Temporal[name]; + exports[`Temporal${name}`] = constructor; + + ArrayPrototypeForEach( + ObjectEntries(ObjectGetOwnPropertyDescriptors(constructor)), + ({ 0: key, 1: desc }) => { + if (typeof key === 'string' && typeof desc.value === 'function') { + exports[`Temporal${name}${capitalizeKey(key)}`] = desc.value; + } + }, + ); + + const descriptors = ObjectGetOwnPropertyDescriptors(constructor.prototype); + ArrayPrototypeForEach( + ObjectEntries(descriptors), + ({ 0: key, 1: desc }) => { + if (typeof key !== 'string') return; + + if ('get' in desc) { + exports[`Temporal${name}PrototypeGet${capitalizeKey(key)}`] = uncurryThis(desc.get); + } else { + exports[`Temporal${name}Prototype${capitalizeKey(key)}`] = typeof desc.value === 'function' ? + uncurryThis(desc.value) : + desc.value; + } + }, + ); + + exports[`isTemporal${name}`] = makeTypeCheckMethod(descriptors[brandedGetter].get); + } +}; + +function makeTypeCheckMethod(getter) { + return (value) => { + try { + FunctionPrototypeCall(getter, value); + return true; + } catch { + return false; + } + }; +} + +function capitalizeKey(key) { + return `${StringPrototypeToUpperCase(key[0])}${StringPrototypeSubstring(key, 1)}`; +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 92bf3be1f612ff..04050767ad60c1 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -121,6 +121,7 @@ expected.beforePreExec = new Set([ expected.atRunTime = new Set([ 'NativeModule internal/process/pre_execution', + 'NativeModule internal/util/temporal', ]); const { isMainThread } = require('worker_threads'); diff --git a/test/parallel/test-temporal-utils-no-temporal.js b/test/parallel/test-temporal-utils-no-temporal.js new file mode 100644 index 00000000000000..d1ca2c4c0ea401 --- /dev/null +++ b/test/parallel/test-temporal-utils-no-temporal.js @@ -0,0 +1,18 @@ +// Flags: --expose-internals --no-harmony-temporal --no-warnings + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +common.allowGlobals(globalThis.Temporal = { + Instant: class Instant {}, +}); + +const instant = new Temporal.Instant(); + +// Module contents should be independent of global state at require time +const temporalUtils = require('internal/util/temporal'); + +assert.strictEqual(temporalUtils.hasTemporal, false); +assert.strictEqual(temporalUtils.isTemporalInstant(instant), false); diff --git a/test/parallel/test-temporal-utils.js b/test/parallel/test-temporal-utils.js new file mode 100644 index 00000000000000..d7354a651311a6 --- /dev/null +++ b/test/parallel/test-temporal-utils.js @@ -0,0 +1,25 @@ +// Flags: --expose-internals --no-warnings + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasTemporal) { + common.skip('No Temporal support'); +} + +const instant = Temporal.Now.instant(); + +delete globalThis.Temporal; +assert.strictEqual(typeof Temporal, 'undefined'); + +// Module contents should be independent of global state at require time +const temporalUtils = require('internal/util/temporal'); + +assert.strictEqual(temporalUtils.hasTemporal, true); +assert.strictEqual(instant.constructor, temporalUtils.TemporalInstant); +assert.strictEqual(instant.epochNanoseconds, + temporalUtils.TemporalInstantPrototypeGetEpochNanoseconds(instant)); +assert.strictEqual(temporalUtils.isTemporalInstant(instant), true); +assert.strictEqual(temporalUtils.isTemporalZonedDateTime(instant), false);