// node:console: the Console class, and the global console this runtime installs over // process.stdout/stderr (see main.ts). Methods publish to the same diagnostics_channel // names Node's own console does ('console.log' etc.), so a subscriber sees every call // whether or not anything is listening. import { AsyncResource } from "./async_hooks"; import { channel } from "./diagnostics_channel"; import { formatWithOptions, inspect, type InspectOptions } from "./inspect"; import { clearScreenDown, cursorTo } from "./readline/callbacks"; import { getStringWidth } from "./readline/utils"; import { shouldColorize } from "./tty"; const onLog = channel("console.log"); const onInfo = channel("console.info"); const onDebug = channel("console.debug"); const onWarn = channel("console.warn"); const onError = channel("console.error"); const MAX_GROUP_INDENTATION = 1000; /** count/countReset/time/timeEnd/timeLog warn through process.emitWarning (not * through the instance's own stderr), so this file needs it the way readline/ * interface.ts needs its deps: installed once by builtins.ts, before any Console * method that could warn runs. */ let emitWarning: (msg: string) => void = () => {}; export function installConsole(deps: { emitWarning: (msg: string) => void }): void { emitWarning = deps.emitWarning; } interface Writer { write(data: string, cb?: (err?: Error | null) => void): boolean; isTTY?: boolean; listenerCount(event: string): number; once(event: string, fn: (...args: unknown[]) => void): unknown; removeListener(event: string, fn: (...args: unknown[]) => void): unknown; } function invalidArgType(name: string, expected: string, value: unknown): TypeError { const got = value === null || value === undefined ? ` Received ${value}` : typeof value === "object" ? ` Received an instance of ${(value as object).constructor?.name ?? "Object"}` : ` Received type ${typeof value}`; return Object.assign(new TypeError(`The "${name}" argument must be ${expected}.${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } function writableStreamError(name: string): Error { return Object.assign(new TypeError(`Console expects a writable stream instance for ${name}`), { code: "ERR_CONSOLE_WRITABLE_STREAM" }); } const noop = (): void => {}; export interface ConsoleOptions { stdout?: Writer; stderr?: Writer; ignoreErrors?: boolean; colorMode?: "auto" | boolean; inspectOptions?: InspectOptions; groupIndentation?: number; } export class Console { #stdout: Writer; #stderr: Writer; #ignoreErrors: boolean; #colorMode: "auto" | boolean; #inspectOptions: InspectOptions | undefined; #groupIndentWidth: number; #groupIndent = ""; #times = new Map(); #counts = new Map(); constructor(options?: Writer | ConsoleOptions, stderrArg?: Writer, ignoreErrorsArg?: boolean) { const opts: ConsoleOptions = options && typeof (options as Writer).write === "function" ? { stdout: options as Writer, stderr: stderrArg, ignoreErrors: ignoreErrorsArg } : (options as ConsoleOptions | undefined) ?? {}; const { stdout, stderr = stdout, ignoreErrors = true, colorMode = "auto", inspectOptions, groupIndentation = 2 } = opts; if (!stdout || typeof stdout.write !== "function") throw writableStreamError("stdout"); if (!stderr || typeof stderr.write !== "function") throw writableStreamError("stderr"); if (colorMode !== "auto" && colorMode !== true && colorMode !== false) { const shown = typeof colorMode === "string" ? `'${colorMode}'` : String(colorMode); throw Object.assign(new TypeError(`The argument 'colorMode' must be one of: 'auto', true, false. Received ${shown}`), { code: "ERR_INVALID_ARG_VALUE", }); } if (groupIndentation < 0 || groupIndentation > MAX_GROUP_INDENTATION || !Number.isInteger(groupIndentation)) { throw Object.assign(new RangeError(`The value of "groupIndentation" is out of range. It must be >= 0 && <= ${MAX_GROUP_INDENTATION}. Received ${groupIndentation}`), { code: "ERR_OUT_OF_RANGE", }); } this.#stdout = stdout; this.#stderr = stderr; this.#ignoreErrors = Boolean(ignoreErrors); this.#colorMode = colorMode; this.#inspectOptions = inspectOptions; this.#groupIndentWidth = groupIndentation; // Bind every own method so `const { log } = console` keeps working, and so a // subclass's prototype override is still seen through the bound copy. for (const key of Object.getOwnPropertyNames(Console.prototype)) { const value = (this as unknown as Record)[key]; if (typeof value === "function" && key !== "constructor") { (this as unknown as Record)[key] = value.bind(this); } } } #inspectOptionsFor(stream: Writer): InspectOptions { let color = this.#colorMode; if (color === "auto") color = shouldColorize(stream); if (this.#inspectOptions) return { ...this.#inspectOptions, colors: this.#inspectOptions.colors ?? color }; return color ? { colors: true } : {}; } #format(stream: Writer, args: unknown[]): string { if (args.length === 1 && typeof args[0] === "string") return args[0]; return formatWithOptions(this.#inspectOptionsFor(stream), ...args); } #write(stream: Writer, useStdout: boolean, text: string): void { if (this.#groupIndent.length) text = this.#groupIndent + text.replace(/\n/g, `\n${this.#groupIndent}`); text += "\n"; if (!this.#ignoreErrors) { stream.write(text); return; } try { if (stream.listenerCount("error") === 0) stream.once("error", noop); stream.write(text); } catch { // A debugging utility swallowing a synchronous write error (e.g. EPIPE) // is Node's own behavior; there is no way to report it further here. } finally { stream.removeListener("error", noop); void useStdout; } } log(...args: unknown[]): void { if (onLog.hasSubscribers) onLog.publish(args); this.#write(this.#stdout, true, this.#format(this.#stdout, args)); } info(...args: unknown[]): void { if (onInfo.hasSubscribers) onInfo.publish(args); this.#write(this.#stdout, true, this.#format(this.#stdout, args)); } debug(...args: unknown[]): void { if (onDebug.hasSubscribers) onDebug.publish(args); this.#write(this.#stdout, true, this.#format(this.#stdout, args)); } warn(...args: unknown[]): void { if (onWarn.hasSubscribers) onWarn.publish(args); this.#write(this.#stderr, false, this.#format(this.#stderr, args)); } error(...args: unknown[]): void { if (onError.hasSubscribers) onError.publish(args); this.#write(this.#stderr, false, this.#format(this.#stderr, args)); } dir(obj: unknown, options?: InspectOptions): void { this.#write(this.#stdout, true, inspect(obj, { customInspect: false, ...this.#inspectOptionsFor(this.#stdout), ...options })); } dirxml(...args: unknown[]): void { this.log(...args); } trace(...args: unknown[]): void { const err = new Error(this.#format(this.#stderr, args)); err.name = "Trace"; Error.captureStackTrace(err, this.trace); this.error(err.stack); } assert(expression: unknown, ...args: unknown[]): void { if (expression) return; if (args.length && typeof args[0] === "string") args[0] = `Assertion failed: ${args[0]}`; else args.unshift("Assertion failed"); this.warn(...args); } clear(): void { if (this.#stdout.isTTY) { cursorTo(this.#stdout as never, 0, 0); clearScreenDown(this.#stdout as never); } } count(label = "default"): void { const key = `${label}`; const next = (this.#counts.get(key) ?? 0) + 1; this.#counts.set(key, next); this.log(`${key}: ${next}`); } countReset(label = "default"): void { const key = `${label}`; if (!this.#counts.has(key)) { emitWarning(`Count for '${key}' does not exist`); return; } this.#counts.delete(key); } group(...data: unknown[]): void { if (data.length) this.log(...data); this.#groupIndent += " ".repeat(this.#groupIndentWidth); } groupCollapsed(...data: unknown[]): void { this.group(...data); } groupEnd(): void { this.#groupIndent = this.#groupIndent.slice(0, -this.#groupIndentWidth || undefined); } time(label = "default"): void { const key = `${label}`; if (this.#times.has(key)) { emitWarning(`Label '${key}' already exists for console.time()`); return; } this.#times.set(key, performance.now()); } timeEnd(label = "default"): void { const key = `${label}`; this.#timeLogImpl("console.timeEnd()", key, undefined); this.#times.delete(key); } timeLog(label = "default", ...data: unknown[]): void { this.#timeLogImpl("console.timeLog()", `${label}`, data); } #timeLogImpl(implementation: string, label: string, data: unknown[] | undefined): void { const started = this.#times.get(label); if (started === undefined) { emitWarning(`No such label '${label}' for ${implementation}`); return; } const formatted = formatElapsed(performance.now() - started); if (data === undefined) this.log("%s: %s", label, formatted); else this.log("%s: %s", label, formatted, ...data); } table(tabularData: unknown, properties?: string[]): void { if (properties !== undefined && !Array.isArray(properties)) throw invalidArgType("properties", "an instance of Array", properties); if (tabularData === null || typeof tabularData !== "object") { this.log(tabularData); return; } this.log(renderTable(tabularData, properties, this.#inspectOptionsFor(this.#stdout))); } } function formatElapsed(ms: number): string { if (ms >= 1000) return `${(ms / 1000).toFixed(3)}s`; return `${Number(ms.toFixed(3))}ms`; } // --- console.table: Node's box-drawing renderer (lib/internal/cli_table.js). --- const BOX = { topLeft: "┌", topMid: "┬", topRight: "┐", mid: "─", rowMid: "┼", leftMid: "├", rightMid: "┤", bottomLeft: "└", bottomMid: "┴", bottomRight: "┘", left: "│ ", right: " │", sep: " │ ", }; function renderRow(row: string[], widths: number[]): string { let out = BOX.left; for (let i = 0; i < row.length; i++) { const cell = row[i] ?? ""; out += cell + " ".repeat(Math.ceil(widths[i]! - getStringWidth(cell))); if (i !== row.length - 1) out += BOX.sep; } return out + BOX.right; } function asciiTable(head: string[], columns: string[][]): string { const widths = head.map((h) => getStringWidth(h)); const longest = Math.max(0, ...columns.map((c) => c.length)); const rows: string[][] = []; for (let i = 0; i < head.length; i++) { const column = columns[i]!; for (let j = 0; j < longest; j++) { rows[j] ??= []; const value = (rows[j]![i] = Object.hasOwn(column, j) ? column[j]! : ""); widths[i] = Math.max(widths[i] ?? 0, getStringWidth(value)); } } const divider = widths.map((w) => BOX.mid.repeat(w + 2)); let out = `${BOX.topLeft}${divider.join(BOX.topMid)}${BOX.topRight}\n${renderRow(head, widths)}\n` + `${BOX.leftMid}${divider.join(BOX.rowMid)}${BOX.rightMid}\n`; for (const row of rows) out += `${renderRow(row, widths)}\n`; return `${out}${BOX.bottomLeft}${divider.join(BOX.bottomMid)}${BOX.bottomRight}`; } function renderTable(tabularData: object, properties: string[] | undefined, inspectOpts: InspectOptions): string { const show = (v: unknown): string => { const depth = v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 2 ? -1 : 0; return inspect(v, { depth, maxArrayLength: 3, breakLength: Infinity, ...inspectOpts }); }; const indexArray = (length: number): string[] => Array.from({ length }, (_, i) => show(i)); if (tabularData instanceof Map) { const keys: string[] = []; const values: string[] = []; for (const [k, v] of tabularData) { keys.push(show(k)); values.push(show(v)); } return asciiTable(["(iteration index)", "Key", "Values"], [indexArray(keys.length), keys, values]); } if (tabularData instanceof Set) { const values = [...tabularData].map(show); return asciiTable(["(iteration index)", "Values"], [indexArray(values.length), values]); } const map: Record = Object.create(null); const valuesCol: string[] = []; let hasPrimitives = false; const indexKeys = Object.keys(tabularData); for (let i = 0; i < indexKeys.length; i++) { const item = (tabularData as Record)[indexKeys[i]!]; const primitive = item === null || (typeof item !== "function" && typeof item !== "object"); if (properties === undefined && primitive) { hasPrimitives = true; valuesCol[i] = show(item); } else { const keys = properties ?? Object.keys(item as object); for (const key of keys) { map[key] ??= []; map[key]![i] = (primitive && properties) || !Object.hasOwn(item as object, key) ? "" : show((item as Record)[key]); } } } const keys = Object.keys(map); const values = keys.map((k) => map[k]!); if (hasPrimitives) { keys.push("Values"); values.push(valuesCol); } keys.unshift("(index)"); values.unshift(indexKeys); return asciiTable(keys, values); } /** The global console this runtime installs: Console bound to its streams, each own * method named and enumerable like Node's (see main.ts, which installs it over * process.stdout/stderr). profile/profileEnd/timeStamp/createTask/context are * singleton-only in Node too — not Console.prototype methods, so `new Console()` * does not have them either (they come from the V8 Inspector binding, which the * global console alone wires up). */ export function makeConsole(stdout: Writer, stderr: Writer): Console & { Console: typeof Console; profile(label?: string): void; profileEnd(label?: string): void; timeStamp(label?: string): void; createTask(name: string): { run(fn: () => R): R }; context(): unknown; } { const instance = new Console({ stdout, stderr }); const createTask = (name: string): { run(fn: () => R): R } => { const resource = new AsyncResource(typeof name === "string" ? name : "Task"); return { run: (fn) => resource.runInAsyncScope(fn) }; }; // No V8 CPU profiler/Inspector protocol is reachable from inside a worker; Node // itself also no-ops these without an attached inspector. const extended = Object.assign(instance, { Console, createTask, context: () => extended, profile: () => {}, profileEnd: () => {}, timeStamp: () => {}, }); return extended as unknown as ReturnType; }