Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,47 @@ const builtInObjects = new SafeSet(
// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
const isUndetectableObject = (v) => typeof v === 'undefined' && v !== undefined;

const temporalConstructors = [
{ name: 'Duration', fullName: 'Temporal.Duration' },
{ name: 'Instant', fullName: 'Temporal.Instant' },
{ name: 'PlainDate', fullName: 'Temporal.PlainDate' },
{ name: 'PlainDateTime', fullName: 'Temporal.PlainDateTime' },
{ name: 'PlainMonthDay', fullName: 'Temporal.PlainMonthDay' },
{ name: 'PlainTime', fullName: 'Temporal.PlainTime' },
{ name: 'PlainYearMonth', fullName: 'Temporal.PlainYearMonth' },
{ name: 'ZonedDateTime', fullName: 'Temporal.ZonedDateTime' },
];

// Best-effort, using instanceof. Polyfills will likely pass these checks.
// Returns void if either the type checks fail or the attempt at calling
// .toString() fails, so that format() can fall back to standard object
// formatting in either case.
// TODO: Hopefully the V8 API will expose typechecks at some point.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporal itself exposes type checks via internal slots and builtin methods.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but we can't guarantee a non-mutable path to get our hands on those methods.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course we can, via primordials

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not while Temporal remains an optional feature, unfortunately.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not? node 26 ships it unconditionally.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's enabled by default, but remains behind both runtime (cf. --no-harmony-temporal) and build flags, so this is unlikely to change any time soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh gotcha. surely we can still capture the builtins at startup, though, even if they're not real primordials yet?

Copy link
Copy Markdown
Member Author

@Renegade334 Renegade334 May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too early. Attempting to capture the global with something like const { Temporal } = globalThis in the header of inspect.js always hits thin air (I guess it's snapshotted?). We have to do it lazily from inspect(), at which point anything that intends to modify the Temporal global (polyfills etc.) will almost certainly already have run.

We could go full ham to hack around this, but it's not really a huge issue in the context of a function which only uses those methods to decide what flavour of human-readable output gets generated.

function getTemporalInfo(value) {
try {
const { Temporal } = globalThis;
if (Temporal === undefined) {
return;
}
for (let i = 0; i < temporalConstructors.length; i++) {
const entry = temporalConstructors[i];
const constructor = Temporal[entry.name];
if (typeof constructor !== 'function') {
return;
}
if (!FunctionPrototypeSymbolHasInstance(constructor, value)) {
continue;
}
const { prototype } = constructor;
if (!ObjectPrototypeHasOwnProperty(prototype, 'toString')) {
return;
}
const result = FunctionPrototypeCall(prototype.toString, value);
return { ...entry, result };
}
} catch { /* void */ }
}

// These options must stay in sync with `getUserOptions`. So if any option will
// be added or removed, `getUserOptions` must also be updated accordingly.
const inspectDefaultOptions = ObjectSeal({
Expand Down Expand Up @@ -1321,6 +1362,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
if (noIterator) {
keys = getKeys(value, ctx.showHidden);
braces = ['{', '}'];
let temporalInfo;
if (typeof value === 'function') {
base = getFunctionBase(ctx, value, constructor, tag);
if (keys.length === 0 && protoProps === undefined)
Expand Down Expand Up @@ -1404,6 +1446,14 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
if (keys.length === 0 && protoProps === undefined) {
return base;
}
} else if ((temporalInfo = getTemporalInfo(value))) {
const prefix = constructor === temporalInfo.name && tag === temporalInfo.fullName ?
(temporalInfo.fullName + ' ') :
getPrefix(constructor, tag, temporalInfo.fullName);
base = `${prefix}${temporalInfo.result}`;
if (keys.length === 0 && protoProps === undefined) {
return ctx.stylize(base, 'date');
}
} else {
if (keys.length === 0 && protoProps === undefined) {
if (isExternal(value)) {
Expand Down
Loading