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/inspect.js b/lib/internal/util/inspect.js index c611eb97fc0755..7a4202952a6074 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -195,6 +195,8 @@ const { kValidateObjectAllowArray, } = require('internal/validators'); +const temporal = require('internal/util/temporal'); + let hexSlice; let internalUrl; @@ -1404,6 +1406,54 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { if (keys.length === 0 && protoProps === undefined) { return base; } + } else if (temporal.isTemporalDuration(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.Duration'); + base = `${prefix}${temporal.TemporalDurationPrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalInstant(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.Instant'); + base = `${prefix}${temporal.TemporalInstantPrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalPlainDate(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.PlainDate'); + base = `${prefix}${temporal.TemporalPlainDatePrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalPlainDateTime(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.PlainDateTime'); + base = `${prefix}${temporal.TemporalPlainDateTimePrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalPlainMonthDay(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.PlainMonthDay'); + base = `${prefix}${temporal.TemporalPlainMonthDayPrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalPlainTime(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.PlainTime'); + base = `${prefix}${temporal.TemporalPlainTimePrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalPlainYearMonth(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.PlainYearMonth'); + base = `${prefix}${temporal.TemporalPlainYearMonthPrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } + } else if (temporal.isTemporalZonedDateTime(value)) { + const prefix = getPrefix(constructor, tag, 'Temporal.ZonedDateTime'); + base = `${prefix}${temporal.TemporalZonedDateTimePrototypeToString(value)}`; + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, 'date'); + } } else { if (keys.length === 0 && protoProps === undefined) { if (isExternal(value)) { diff --git a/lib/internal/util/temporal.js b/lib/internal/util/temporal.js new file mode 100644 index 00000000000000..fa948bc044903f --- /dev/null +++ b/lib/internal/util/temporal.js @@ -0,0 +1,93 @@ +'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 unless +// imported lazily. + +// 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) { + for (const name of constructors.keys()) { + exports[`isTemporal${name}`] = () => false; + } + return; + } + + for (const { 0: name, 1: brandedGetter } of constructors.entries()) { + const constructor = Temporal[name]; + exports[`Temporal${name}`] = constructor; + + ArrayPrototypeForEach( + ObjectEntries(ObjectGetOwnPropertyDescriptors(constructor)), + ({ 0: key, 1: desc }) => { + if (typeof key !== 'string' || typeof desc.value !== 'function') return; + + exports[`Temporal${name}${capitalizeKey(key)}`] = desc.value; + }, + ); + + ArrayPrototypeForEach( + ObjectEntries(ObjectGetOwnPropertyDescriptors(constructor.prototype)), + ({ 0: key, 1: desc }) => { + if (typeof key !== 'string') return; + + if ('get' in desc) { + exports[`Temporal${name}PrototypeGet${capitalizeKey(key)}`] = uncurryThis(desc.get); + if (key === brandedGetter) { + exports[`isTemporal${name}`] = makeTypeCheckMethod(desc.get); + } + } else { + exports[`Temporal${name}Prototype${capitalizeKey(key)}`] = typeof desc.value === 'function' ? + uncurryThis(desc.value) : + desc.value; + } + }, + ); + } +}; + +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-util-inspect-temporal.js b/test/parallel/test-util-inspect-temporal.js new file mode 100644 index 00000000000000..915d3fb01aa4db --- /dev/null +++ b/test/parallel/test-util-inspect-temporal.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +const { suite, test } = require('node:test'); +const util = require('util'); + +if (!common.hasTemporal) { + common.skip('Temporal is not enabled'); +} + +const colorRegex = new RegExp(`^\u001b\\[${util.inspect.colors[util.inspect.styles.date][0]}m`); + +createSuite('Duration', [1, 2, 3, 4, 5, 6, 7, 8, 9], 'P1Y2M3W4DT5H6M7.008009S'); +createSuite('Instant', [112233445566778899n], '1973-07-22T23:57:25.566778899Z'); +createSuite('PlainDate', [1901, 2, 3], '1901-02-03'); +createSuite('PlainDateTime', [1901, 2, 3, 4, 5, 6, 7, 8, 9], '1901-02-03T04:05:06.007008009'); +createSuite('PlainMonthDay', [1, 2], '01-02'); +createSuite('PlainTime', [1, 2, 3, 4, 5, 6], '01:02:03.004005006'); +createSuite('PlainYearMonth', [1901, 2], '1901-02'); +createSuite('ZonedDateTime', [112233445566778899n, 'UTC'], '1973-07-22T23:57:25.566778899+00:00[UTC]'); + +function createSuite(name, params, expected) { + const constructor = Temporal[name]; + + suite(`Temporal.${name}`, () => { + test('basic instance', (t) => { + t.assert.strictEqual( + util.inspect(new constructor(...params)), + `Temporal.${name} ${expected}`, + ); + }); + + test('derived class instance', (t) => { + class X extends constructor {} + t.assert.strictEqual( + util.inspect(new X(...params)), + `X [Temporal.${name}] ${expected}`, + ); + }); + + test('instance with additional properties', (t) => { + t.assert.strictEqual( + util.inspect(Object.assign(new constructor(...params), { foo: 'bar' }), { breakLength: Infinity }), + `Temporal.${name} ${expected} { foo: 'bar' }`, + ); + }); + + test('prototype object', (t) => { + t.assert.strictEqual( + util.inspect(constructor.prototype), + `Object [Temporal.${name}] {}`, + ); + }); + + test('instance with null prototype', (t) => { + t.assert.strictEqual( + util.inspect( + Object.setPrototypeOf( + new constructor(...params), + null, + ), + ), + `[Temporal.${name}: null prototype] ${expected}`, + ); + }); + + test('instance with colors: true', (t) => { + t.assert.match( + util.inspect(new constructor(...params), { colors: true }), + colorRegex, + ); + }); + }); +}