// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // util.inspect and util.format, following Node's lib/internal/util/inspect.js // algorithm (compact=3 line joining, array grouping, circular refs, error stacks) // over the subset of value kinds JavaScript can observe without engine internals. // Promise state and Proxy targets are not observable from JS: a Promise prints as // `Promise { }` and a Proxy prints as its target would. export const customInspect: unique symbol = Symbol.for("nodejs.util.inspect.custom") as never; export interface InspectOptions { showHidden?: boolean; depth?: number | null; colors?: boolean; customInspect?: boolean; showProxy?: boolean; maxArrayLength?: number | null; maxStringLength?: number | null; breakLength?: number; compact?: boolean | number; sorted?: boolean | ((a: string, b: string) => number); getters?: boolean | "get" | "set"; numericSeparator?: boolean; stylize?: (text: string, style: string) => string; } interface Ctx { showHidden: boolean; depth: number; colors: boolean; customInspect: boolean; maxArrayLength: number; maxStringLength: number; breakLength: number; compact: boolean | number; sorted: boolean | ((a: string, b: string) => number); getters: boolean | "get" | "set"; numericSeparator: boolean; stylize: (text: string, style: string) => string; seen: unknown[]; circular: Map | undefined; indentationLvl: number; currentDepth: number; userOptions: InspectOptions; } export const defaultOptions = { showHidden: false, depth: 2, colors: false, customInspect: true, showProxy: false, maxArrayLength: 100, maxStringLength: 10000, breakLength: 80, compact: 3, sorted: false, getters: false, numericSeparator: false, } satisfies InspectOptions; /** inspect.colors: Node's SGR open/close pairs, its aliases non-enumerable as in Node. */ const COLORS: Record = { reset: [0, 0], bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], blink: [5, 25], inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29], doubleunderline: [21, 24], black: [30, 39], red: [31, 39], green: [32, 39], yellow: [33, 39], blue: [34, 39], magenta: [35, 39], cyan: [36, 39], white: [37, 39], bgBlack: [40, 49], bgRed: [41, 49], bgGreen: [42, 49], bgYellow: [43, 49], bgBlue: [44, 49], bgMagenta: [45, 49], bgCyan: [46, 49], bgWhite: [47, 49], framed: [51, 54], overlined: [53, 55], gray: [90, 39], redBright: [91, 39], greenBright: [92, 39], yellowBright: [93, 39], blueBright: [94, 39], magentaBright: [95, 39], cyanBright: [96, 39], whiteBright: [97, 39], bgGray: [100, 49], bgRedBright: [101, 49], bgGreenBright: [102, 49], bgYellowBright: [103, 49], bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49], }; for (const [alias, name] of [ ["grey", "gray"], ["blackBright", "gray"], ["bgGrey", "bgGray"], ["bgBlackBright", "bgGray"], ["faint", "dim"], ["crossedout", "strikethrough"], ["strikeThrough", "strikethrough"], ["crossedOut", "strikethrough"], ["conceal", "hidden"], ["swapColors", "inverse"], ["swapcolors", "inverse"], ["doubleUnderline", "doubleunderline"], ] as const) { Object.defineProperty(COLORS, alias, { get: () => COLORS[name], set(value) { Object.defineProperty(COLORS, alias, { value, configurable: true, enumerable: true, writable: true }); }, configurable: true, enumerable: false, }); } const STYLES: Record = { special: "cyan", number: "yellow", bigint: "yellow", boolean: "yellow", undefined: "grey", null: "bold", string: "green", symbol: "green", date: "magenta", regexp: "red", module: "underline", }; function stylizeWithColor(text: string, style: string): string { const color = COLORS[STYLES[style] ?? ""]; return color ? `\u001b[${color[0]}m${text}\u001b[${color[1]}m` : text; } const stylizeNoColor = (text: string): string => text; const ANSI = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g; export function stripVTControlCharacters(str: string): string { return str.replace(ANSI, ""); } const visibleLength = (ctx: Ctx, s: string): number => (ctx.colors ? stripVTControlCharacters(s).length : s.length); export function inspect(value: unknown, opts?: InspectOptions | boolean, ...legacy: unknown[]): string { let user: InspectOptions = {}; if (typeof opts === "boolean") { user = { showHidden: opts }; if (legacy[0] !== undefined) user.depth = legacy[0] as number | null; if (legacy[1] !== undefined) user.colors = legacy[1] as boolean; } else if (opts) { user = opts; } const merged = { ...inspect.defaultOptions, ...user }; const ctx: Ctx = { showHidden: Boolean(merged.showHidden), depth: merged.depth === null ? Infinity : merged.depth, colors: Boolean(merged.colors), customInspect: merged.customInspect !== false, maxArrayLength: merged.maxArrayLength === null ? Infinity : merged.maxArrayLength, maxStringLength: merged.maxStringLength === null ? Infinity : merged.maxStringLength, breakLength: merged.breakLength, compact: merged.compact, sorted: merged.sorted, getters: merged.getters, numericSeparator: Boolean(merged.numericSeparator), stylize: user.stylize ?? (merged.colors ? stylizeWithColor : stylizeNoColor), seen: [], circular: undefined, indentationLvl: 0, currentDepth: 0, userOptions: user, }; return formatValue(ctx, value, 0); } inspect.custom = customInspect; inspect.defaultOptions = { ...defaultOptions } as typeof defaultOptions; inspect.colors = COLORS; inspect.styles = STYLES; // ---- strings ---- const META: string[] = []; for (let i = 0; i < 32; i++) META[i] = `\\x${i.toString(16).toUpperCase().padStart(2, "0")}`; Object.assign(META, { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }); for (let i = 0x7f; i <= 0x9f; i++) META[i] = `\\x${i.toString(16).toUpperCase()}`; /** Node's quote choice: single quotes, else double, else backticks, escaping the one used. */ function strEscape(str: string): string { let quote = "'"; if (str.includes("'")) { if (!str.includes('"')) quote = '"'; else if (!str.includes("`") && !str.includes("${")) quote = "`"; } let out = ""; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (str[i] === quote && quote === "'") out += "\\'"; else if (c === 92) out += "\\\\"; else if (c < 32 || (c >= 0x7f && c <= 0x9f)) out += META[c]; else if (c >= 0xd800 && c <= 0xdfff) { const next = str.charCodeAt(i + 1); if (c <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) { out += str[i]! + str[i + 1]!; i++; } else { out += `\\u${c.toString(16).toUpperCase()}`; } } else out += str[i]; } return quote + out + quote; } function formatNumber(fn: Ctx["stylize"], n: number, separator: boolean): string { if (Object.is(n, -0)) return fn("-0", "number"); if (!separator || !Number.isFinite(n)) return fn(`${n}`, "number"); const [int, frac] = String(n).split("."); const grouped = int!.replace(/\B(?=(\d{3})+(?!\d))/g, "_"); return fn(frac === undefined ? grouped : `${grouped}.${frac.replace(/(\d{3})(?=\d)/g, "$1_")}`, "number"); } function formatBigInt(fn: Ctx["stylize"], n: bigint, separator: boolean): string { const s = separator ? String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "_") : String(n); return fn(`${s}n`, "bigint"); } function formatPrimitive(ctx: Ctx, value: unknown): string { const fn = ctx.stylize; if (typeof value === "string") { let trailer = ""; let str = value; if (str.length > ctx.maxStringLength) { const remaining = str.length - ctx.maxStringLength; str = str.slice(0, ctx.maxStringLength); trailer = `... ${remaining} more character${remaining > 1 ? "s" : ""}`; } if (ctx.compact !== true && str.length > 16 && str.length > ctx.breakLength - ctx.indentationLvl - 4) { return str.split(/(?<=\n)/).map((line) => fn(strEscape(line), "string")) .join(` +\n${" ".repeat(ctx.indentationLvl + 2)}`) + trailer; } return fn(strEscape(str), "string") + trailer; } if (typeof value === "number") return formatNumber(fn, value, ctx.numericSeparator); if (typeof value === "bigint") return formatBigInt(fn, value, ctx.numericSeparator); if (typeof value === "boolean") return fn(`${value}`, "boolean"); if (value === undefined) return fn("undefined", "undefined"); return fn(String(value), "symbol"); } // ---- objects ---- const tagOf = (v: unknown): string => Object.prototype.toString.call(v).slice(8, -1); function getConstructorName(obj: object): string | null { let o: object | null = obj; while (o !== null) { const d = Object.getOwnPropertyDescriptor(o, "constructor"); if (d && typeof d.value === "function" && d.value.name !== "") { try { if (obj instanceof d.value) return d.value.name as string; } catch { /* a throwing Symbol.hasInstance: keep walking */ } } o = Object.getPrototypeOf(o); } return null; } function getPrefix(constructor: string | null, tag: string, fallback: string, size = ""): string { if (constructor === null) { if (tag !== "" && fallback !== tag) return `[${fallback}${size}: null prototype] [${tag}] `; return `[${fallback}${size}: null prototype] `; } if (tag !== "" && constructor !== tag) return `${constructor}${size} [${tag}] `; return `${constructor}${size} `; } function getKeys(value: object, showHidden: boolean): Array { const keys: Array = showHidden ? Object.getOwnPropertyNames(value) : Object.keys(value); const symbols = Object.getOwnPropertySymbols(value); for (const s of symbols) if (showHidden || Object.prototype.propertyIsEnumerable.call(value, s)) keys.push(s); return keys; } const isIndex = (k: string): boolean => /^(?:0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1; function getNonIndexKeys(value: object, showHidden: boolean): Array { return getKeys(value, showHidden).filter((k) => typeof k === "symbol" || !isIndex(k)); } function isError(v: unknown): v is Error { return v instanceof Error || tagOf(v) === "Error"; } const BUILTIN_CTORS = new Set(Object.getOwnPropertyNames(globalThis).filter( (n) => /^[A-Z]/.test(n) && typeof (globalThis as Record)[n] === "function", )); /** True unless the object (or a user-defined class in its chain) overrides toString. */ function hasBuiltInToString(value: object): boolean { if (typeof (value as { toString?: unknown }).toString !== "function") return true; if (Object.hasOwn(value, "toString")) return false; let pointer: object | null = value; do { pointer = Object.getPrototypeOf(pointer); } while (pointer !== null && !Object.hasOwn(pointer, "toString")); if (pointer === null) return true; const d = Object.getOwnPropertyDescriptor(pointer, "constructor"); return d !== undefined && typeof d.value === "function" && BUILTIN_CTORS.has(d.value.name); } function formatValue(ctx: Ctx, value: unknown, recurseTimes: number): string { if (typeof value !== "object" && typeof value !== "function") return formatPrimitive(ctx, value); if (value === null) return ctx.stylize("null", "null"); if (ctx.customInspect) { const custom = (value as Record)[customInspect]; const ctor = (value as { constructor?: { prototype?: unknown } }).constructor; if (typeof custom === "function" && custom !== inspect && !(ctor && ctor.prototype === value)) { const depth = ctx.depth - recurseTimes; const options = { ...inspect.defaultOptions, ...ctx.userOptions, depth, stylize: ctx.stylize }; const ret = custom.call(value, depth, options, inspect); if (ret !== value) { if (typeof ret !== "string") return formatValue(ctx, ret, recurseTimes); return ret.replaceAll("\n", `\n${" ".repeat(ctx.indentationLvl)}`); } } } if (ctx.seen.includes(value)) { ctx.circular ??= new Map(); let index = ctx.circular.get(value); if (index === undefined) { index = ctx.circular.size + 1; ctx.circular.set(value, index); } return ctx.stylize(`[Circular *${index}]`, "special"); } return formatRaw(ctx, value, recurseTimes); } const OBJECT = 0; const ARRAY_LIKE = 1; type Formatter = (ctx: Ctx, value: any, recurseTimes: number) => string[]; const noEntries: Formatter = () => []; function formatRaw(ctx: Ctx, value: object, recurseTimes: number): string { const constructor = getConstructorName(value); let tag = (value as Record)[Symbol.toStringTag]; if (typeof tag !== "string" || (tag !== "" && Object.prototype.propertyIsEnumerable.call(value, Symbol.toStringTag))) tag = ""; const t = tag as string; let keys: Array; let base = ""; let formatter: Formatter = noEntries; let braces: [string, string]; let extrasType = OBJECT; let iterable = true; if (Array.isArray(value)) { const prefix = constructor !== "Array" || t !== "" ? getPrefix(constructor, t, "Array", `(${value.length})`) : ""; keys = getNonIndexKeys(value, ctx.showHidden); braces = [`${prefix}[`, "]"]; if (value.length === 0 && keys.length === 0) return `${braces[0]}]`; extrasType = ARRAY_LIKE; formatter = formatArray; } else if (value instanceof Set) { const prefix = getPrefix(constructor, t, "Set", `(${value.size})`); keys = getKeys(value, ctx.showHidden); formatter = formatSet; if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, "}"]; } else if (value instanceof Map) { const prefix = getPrefix(constructor, t, "Map", `(${value.size})`); keys = getKeys(value, ctx.showHidden); formatter = formatMap; if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, "}"]; } else if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { const ta = value as unknown as ArrayLike; keys = getNonIndexKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, t, tagOf(value), `(${ta.length})`); braces = [`${prefix}[`, "]"]; if (ta.length === 0 && keys.length === 0 && !ctx.showHidden) return `${braces[0]}]`; formatter = formatTypedArray; extrasType = ARRAY_LIKE; } else { iterable = false; } if (!iterable) { keys = getKeys(value, ctx.showHidden); braces = ["{", "}"]; if (typeof value === "function") { base = getFunctionBase(value, constructor, t); if (keys.length === 0) return ctx.stylize(base, "special"); } else if (constructor === "Object") { if (tagOf(value) === "Arguments") braces[0] = "[Arguments] ["; else if (t !== "") braces[0] = `${getPrefix(constructor, t, "Object")}{`; if (keys.length === 0) return `${braces[0]}}`; if (braces[0] === "[Arguments] [") { extrasType = ARRAY_LIKE; braces[1] = "]"; formatter = formatArray; keys = getNonIndexKeys(value, ctx.showHidden); } } else if (value instanceof RegExp) { base = RegExp.prototype.toString.call(value); const prefix = getPrefix(constructor, t, "RegExp"); if (prefix !== "RegExp ") base = `${prefix}${base}`; if (keys.length === 0 || recurseTimes > ctx.depth) return ctx.stylize(base, "regexp"); } else if (value instanceof Date) { base = Number.isNaN(value.getTime()) ? Date.prototype.toString.call(value) : value.toISOString(); const prefix = getPrefix(constructor, t, "Date"); if (prefix !== "Date ") base = `${prefix}${base}`; if (keys.length === 0) return ctx.stylize(base, "date"); } else if (isError(value)) { base = formatError(value, constructor, t, ctx, keys); if (keys.length === 0) return base; } else if (value instanceof ArrayBuffer) { keys.unshift("byteLength"); const bytes = new Uint8Array(value); const prefix = getPrefix(constructor, t, "ArrayBuffer"); braces[0] = `${prefix}{`; formatter = () => [`[Uint8Contents]: <${hexBytes(bytes, ctx.maxArrayLength)}>`]; if (keys.length === 1) keys = ["byteLength"]; } else if (tagOf(value) === "Promise") { braces[0] = `${getPrefix(constructor, t, "Promise")}{`; formatter = (c) => [c.stylize("", "special")]; } else if (value instanceof WeakSet || value instanceof WeakMap) { const kind = value instanceof WeakSet ? "WeakSet" : "WeakMap"; braces[0] = `${getPrefix(constructor, t, kind)}{`; formatter = (c) => [c.stylize("", "special")]; } else if (isBoxed(value)) { if (value instanceof String) keys = keys.filter((k) => typeof k === "symbol" || !isIndex(k)); base = boxedBase(ctx, value, constructor, t); if (keys.length === 0) return base; } else { if (keys.length === 0) return `${getPrefix(constructor, t, "Object")}{}`; braces[0] = `${getPrefix(constructor, t, "Object")}{`; } } if (recurseTimes > ctx.depth) { let name = getPrefix(constructor, t, "Object").slice(0, -1); if (constructor !== null) name = `[${name}]`; return ctx.stylize(name, "special"); } recurseTimes += 1; ctx.seen.push(value); ctx.currentDepth = recurseTimes; const output = formatter(ctx, value, recurseTimes); for (const key of keys!) output.push(formatProperty(ctx, value, recurseTimes, key, extrasType)); if (ctx.circular !== undefined) { const index = ctx.circular.get(value); if (index !== undefined) { const reference = ctx.stylize(``, "special"); if (ctx.compact !== true) base = base === "" ? reference : `${reference} ${base}`; else braces![0] = `${reference} ${braces![0]}`; } } ctx.seen.pop(); if (ctx.sorted) { const cmp = ctx.sorted === true ? undefined : ctx.sorted; if (extrasType === OBJECT) output.sort(cmp); } return reduceToSingleString(ctx, output, base, braces!, extrasType, recurseTimes, value); } function hexBytes(bytes: Uint8Array, max: number): string { const shown = Array.from(bytes.subarray(0, Math.min(max, bytes.length)), (b) => b.toString(16).padStart(2, "0")).join(" "); const remaining = bytes.length - Math.min(max, bytes.length); return remaining > 0 ? `${shown} ... ${remaining} more byte${remaining > 1 ? "s" : ""}` : shown; } function isBoxed(v: object): boolean { return v instanceof Number || v instanceof String || v instanceof Boolean || (typeof BigInt === "function" && tagOf(v) === "BigInt") || tagOf(v) === "Symbol"; } function boxedBase(ctx: Ctx, value: object, constructor: string | null, tag: string): string { let type: string; let shown: string; if (value instanceof Number) { type = "Number"; shown = formatNumber(stylizeNoColor, value.valueOf(), false); } else if (value instanceof String) { type = "String"; shown = strEscape(value.valueOf()); } else if (value instanceof Boolean) { type = "Boolean"; shown = String(value.valueOf()); } else if (tagOf(value) === "BigInt") { type = "BigInt"; shown = `${(value as { valueOf(): bigint }).valueOf()}n`; } else { type = "Symbol"; shown = String((value as { valueOf(): symbol }).valueOf()); } let base = `[${type}`; if (type !== constructor) base += constructor === null ? " (null prototype)" : ` (${constructor})`; base += `: ${shown}]`; if (tag !== "" && tag !== constructor) base += ` [${tag}]`; return ctx.stylize(base, type.toLowerCase() === "string" ? "string" : type.toLowerCase()); } function getFunctionBase(fn: Function, constructor: string | null, tag: string): string { const source = Function.prototype.toString.call(fn); if (source.startsWith("class") && source.endsWith("}")) { const body = source.slice(5, -1); const brace = body.indexOf("{"); if (brace !== -1 && !body.slice(0, brace).includes("(")) { const name = (Object.hasOwn(fn, "name") && fn.name) || "(anonymous)"; let base = `class ${name}`; if (constructor !== "Function" && constructor !== null) base += ` [${constructor}]`; if (tag !== "" && constructor !== tag) base += ` [${tag}]`; if (constructor !== null) { const superName = (Object.getPrototypeOf(fn) as Function | null)?.name; if (superName) base += ` extends ${superName}`; } else { base += " extends [null prototype]"; } return `[${base}]`; } } let type = "Function"; const kind = tagOf(fn); if (kind === "GeneratorFunction" || kind === "AsyncGeneratorFunction") type = `Generator${type}`; if (kind === "AsyncFunction" || kind === "AsyncGeneratorFunction") type = `Async${type}`; let base = `[${type}`; if (constructor === null) base += " (null prototype)"; base += fn.name === "" ? " (anonymous)" : `: ${fn.name}`; base += "]"; if (constructor !== type && constructor !== null) base += ` ${constructor}`; if (tag !== "" && constructor !== tag) base += ` [${tag}]`; return base; } function formatError(err: Error, constructor: string | null, tag: string, ctx: Ctx, keys: Array): string { const name = err.name != null ? String(err.name) : "Error"; let stack = err.stack ? String(err.stack) : Error.prototype.toString.call(err); if (!ctx.showHidden && keys.length !== 0) { for (const prop of ["name", "message", "stack"] as const) { const i = keys.indexOf(prop); if (i !== -1 && (typeof err[prop] !== "string" || stack.includes(err[prop] as string))) keys.splice(i, 1); } } if ("cause" in err && !keys.includes("cause")) keys.push("cause"); if (Array.isArray((err as { errors?: unknown }).errors) && !keys.includes("errors")) keys.push("errors"); stack = improveStack(stack, constructor, name, tag); let pos = (err.message && stack.indexOf(err.message)) || -1; if (pos !== -1) pos += err.message.length; const stackStart = stack.indexOf("\n at", pos); if (stackStart === -1) stack = `[${stack}]`; if (ctx.indentationLvl !== 0) stack = stack.replaceAll("\n", `\n${" ".repeat(ctx.indentationLvl)}`); return stack; } function improveStack(stack: string, constructor: string | null, name: string, tag: string): string { let len = name.length; if (constructor === null || (name.endsWith("Error") && stack.startsWith(name) && (stack.length === len || stack[len] === ":" || stack[len] === "\n"))) { let fallback = "Error"; if (constructor === null) { const start = /^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n\s+at)/.exec(stack) ?? /^([a-z_A-Z0-9-]*Error)$/.exec(stack); fallback = start?.[1] ?? ""; len = fallback.length; fallback ||= "Error"; } const prefix = getPrefix(constructor, tag, fallback).slice(0, -1); if (name !== prefix) { if (prefix.includes(name)) stack = len === 0 ? `${prefix}: ${stack}` : `${prefix}${stack.slice(len)}`; else stack = `${prefix} [${name}]${stack.slice(len)}`; } } return stack; } function formatArray(ctx: Ctx, value: ArrayLike, recurseTimes: number): string[] { const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const output: string[] = []; let holes = 0; const flushHoles = (): void => { if (holes > 0) output.push(ctx.stylize(`<${holes} empty item${holes > 1 ? "s" : ""}>`, "undefined")); holes = 0; }; for (let i = 0; i < len; i++) { if (!Object.hasOwn(value, i)) { holes++; continue; } flushHoles(); output.push(formatProperty(ctx, value as object, recurseTimes, String(i), ARRAY_LIKE)); } flushHoles(); const remaining = value.length - len; if (remaining > 0) output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); return output; } function formatTypedArray(ctx: Ctx, value: ArrayLike, _recurseTimes: number): string[] { const max = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const output: string[] = []; for (let i = 0; i < max; i++) { const v = value[i]!; output.push(typeof v === "bigint" ? formatBigInt(ctx.stylize, v, ctx.numericSeparator) : formatNumber(ctx.stylize, v, ctx.numericSeparator)); } const remaining = value.length - max; if (remaining > 0) output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); return output; } function formatSet(ctx: Ctx, value: Set, recurseTimes: number): string[] { const output: string[] = []; ctx.indentationLvl += 2; for (const v of value) output.push(formatValue(ctx, v, recurseTimes)); ctx.indentationLvl -= 2; return output; } function formatMap(ctx: Ctx, value: Map, recurseTimes: number): string[] { const output: string[] = []; ctx.indentationLvl += 2; for (const [k, v] of value) output.push(`${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`); ctx.indentationLvl -= 2; return output; } const KEY_PLAIN = /^[a-zA-Z_][a-zA-Z_0-9]*$/; function formatProperty(ctx: Ctx, value: object, recurseTimes: number, key: string | symbol, type: number): string { let str: string; const desc = Object.getOwnPropertyDescriptor(value, key) ?? { value: (value as Record)[key], enumerable: false }; if (desc.value !== undefined) { const diff = ctx.compact !== true || type !== OBJECT ? 2 : 3; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes); ctx.indentationLvl -= diff; } else if (desc.get !== undefined) { const label = desc.set !== undefined ? "Getter/Setter" : "Getter"; if (ctx.getters && (ctx.getters === true || (ctx.getters === "get" && desc.set === undefined) || (ctx.getters === "set" && desc.set !== undefined))) { try { const got = desc.get.call(value); ctx.indentationLvl += 2; str = `${ctx.stylize(`[${label}:`, "special")} ${formatValue(ctx, got, recurseTimes)}${ctx.stylize("]", "special")}`; ctx.indentationLvl -= 2; } catch (e) { str = ctx.stylize(`[${label}: ]`, "special"); } } else { str = ctx.stylize(`[${label}]`, "special"); } } else if (desc.set !== undefined) { str = ctx.stylize("[Setter]", "special"); } else { str = ctx.stylize("undefined", "undefined"); } if (type === ARRAY_LIKE && typeof key === "string" && isIndex(key)) return str; let name: string; if (typeof key === "symbol") name = ctx.stylize(String(key), "symbol"); else if (key === "__proto__") name = "['__proto__']"; else if (desc.enumerable === false) name = `[${key}]`; else if (KEY_PLAIN.test(key)) name = ctx.stylize(key, "name"); else name = ctx.stylize(strEscape(key), "string"); return `${name}: ${str}`; } function isBelowBreakLength(ctx: Ctx, output: string[], start: number, base: string): boolean { let total = output.length + start; if (total + output.length > ctx.breakLength) return false; for (const entry of output) { total += visibleLength(ctx, entry); if (total > ctx.breakLength) return false; } return base === "" || !base.includes("\n"); } function reduceToSingleString( ctx: Ctx, output: string[], base: string, braces: [string, string], extrasType: number, recurseTimes: number, value: unknown, ): string { if (ctx.compact !== true) { if (typeof ctx.compact === "number" && ctx.compact >= 1) { const entries = output.length; if (extrasType === ARRAY_LIKE && entries > 6) output = groupArrayElements(ctx, output, value as ArrayLike); if (ctx.currentDepth - recurseTimes < ctx.compact && entries === output.length) { const start = output.length + ctx.indentationLvl + braces[0].length + base.length + 10; if (isBelowBreakLength(ctx, output, start, base)) { const joined = output.join(", "); if (!joined.includes("\n")) return `${base ? `${base} ` : ""}${braces[0]} ${joined} ${braces[1]}`; } } } const indentation = `\n${" ".repeat(ctx.indentationLvl)}`; return `${base ? `${base} ` : ""}${braces[0]}${indentation} ${output.join(`,${indentation} `)}${indentation}${braces[1]}`; } if (isBelowBreakLength(ctx, output, 0, base)) { return `${braces[0]}${base ? ` ${base}` : ""} ${output.join(", ")} ${braces[1]}`; } const indentation = `\n${" ".repeat(ctx.indentationLvl)}`; const ln = base === "" && braces[0].length === 1 ? " " : `${base ? ` ${base}` : ""}\n${indentation} `; return `${braces[0]}${ln}${output.join(`,\n${indentation} `)} ${braces[1]}`; } /** Node's column layout for arrays of more than six short entries. */ function groupArrayElements(ctx: Ctx, output: string[], value: ArrayLike | undefined): string[] { let totalLength = 0; let maxLength = 0; let outputLength = output.length; if (value && ctx.maxArrayLength < value.length) outputLength--; // the "... more items" entry const separatorSpace = 2; const dataLen: number[] = new Array(outputLength); for (let i = 0; i < outputLength; i++) { const len = visibleLength(ctx, output[i]!); dataLen[i] = len; totalLength += len + separatorSpace; if (maxLength < len) maxLength = len; } const actualMax = maxLength + separatorSpace; if (actualMax * 3 + ctx.indentationLvl < ctx.breakLength && (totalLength / actualMax > 5 || maxLength <= 6)) { const averageBias = Math.sqrt(actualMax - totalLength / output.length); const biasedMax = Math.max(actualMax - 3 - averageBias, 1); const columns = Math.min( Math.round(Math.sqrt(2.5 * biasedMax * outputLength) / biasedMax), Math.floor((ctx.breakLength - ctx.indentationLvl) / actualMax), (ctx.compact as number) * 4, 15, ); if (columns <= 1) return output; const tmp: string[] = []; const maxLineLength: number[] = []; for (let i = 0; i < columns; i++) { let lineLength = 0; for (let j = i; j < output.length; j += columns) if (dataLen[j]! > lineLength) lineLength = dataLen[j]!; maxLineLength.push(lineLength + separatorSpace); } let padStart = true; if (value !== undefined) { for (let i = 0; i < output.length; i++) { const v = value[i]; if (typeof v !== "number" && typeof v !== "bigint") { padStart = false; break; } } } for (let i = 0; i < outputLength; i += columns) { const max = Math.min(i + columns, outputLength); let str = ""; let j = i; for (; j < max - 1; j++) { const padding = maxLineLength[j - i]! + output[j]!.length - dataLen[j]!; str += padStart ? `${output[j]}, `.padStart(padding, " ") : `${output[j]}, `.padEnd(padding, " "); } if (padStart) { const padding = maxLineLength[j - i]! + output[j]!.length - dataLen[j]! - separatorSpace; str += output[j]!.padStart(padding, " "); } else { str += output[j]; } tmp.push(str); } if (value && ctx.maxArrayLength < value.length) tmp.push(output[outputLength]!); return tmp; } return output; } // ---- format ---- function tryStringify(v: unknown): string { try { return JSON.stringify(v); } catch (e) { if ((e as Error).message.includes("circular")) return "[Circular]"; throw e; } } export function formatWithOptions(inspectOptions: InspectOptions, ...args: unknown[]): string { const first = args[0]; let a = 0; let str = ""; let join = ""; if (typeof first === "string") { if (args.length === 1) return first; let lastPos = 0; let tempStr = ""; for (let i = 0; i < first.length - 1; i++) { if (first.charCodeAt(i) !== 37) continue; // '%' const next = first.charCodeAt(++i); if (a + 1 !== args.length) { switch (next) { case 115: { // s const arg = args[++a]; if (typeof arg === "number") tempStr = formatNumber(stylizeNoColor, arg, false); else if (typeof arg === "bigint") tempStr = formatBigInt(stylizeNoColor, arg, false); else if (typeof arg !== "object" || arg === null || !hasBuiltInToString(arg)) tempStr = String(arg); else tempStr = inspect(arg, { ...inspectOptions, depth: 0, colors: false, compact: 3 }); break; } case 106: // j tempStr = tryStringify(args[++a]); break; case 100: { // d const n = args[++a]; if (typeof n === "bigint") tempStr = formatBigInt(stylizeNoColor, n, false); else if (typeof n === "symbol") tempStr = "NaN"; else tempStr = formatNumber(stylizeNoColor, Number(n), false); break; } case 79: // O tempStr = inspect(args[++a], inspectOptions); break; case 111: // o tempStr = inspect(args[++a], { ...inspectOptions, showHidden: true, showProxy: true, depth: 4 }); break; case 105: { // i const n = args[++a]; if (typeof n === "bigint") tempStr = formatBigInt(stylizeNoColor, n, false); else if (typeof n === "symbol") tempStr = "NaN"; else tempStr = formatNumber(stylizeNoColor, Number.parseInt(n as string), false); break; } case 102: { // f const n = args[++a]; tempStr = typeof n === "symbol" ? "NaN" : formatNumber(stylizeNoColor, Number.parseFloat(n as string), false); break; } case 99: // c a += 1; tempStr = ""; break; case 37: // % str += first.slice(lastPos, i); lastPos = i + 1; continue; default: continue; } if (lastPos !== i - 1) str += first.slice(lastPos, i - 1); str += tempStr; lastPos = i + 1; } else if (next === 37) { str += first.slice(lastPos, i); lastPos = i + 1; } } if (lastPos !== 0) { a++; join = " "; if (lastPos < first.length) str += first.slice(lastPos); } } while (a < args.length) { const value = args[a]; str += join; str += typeof value !== "string" ? inspect(value, inspectOptions) : value; join = " "; a++; } return str; } export function format(...args: unknown[]): string { return formatWithOptions({}, ...args); }