1'use strict'; 2 3// The Console constructor is not actually used to construct the global 4// console. It's exported for backwards compatibility. 5 6const { 7 ArrayFrom, 8 ArrayIsArray, 9 ArrayPrototypeForEach, 10 ArrayPrototypePush, 11 ArrayPrototypeUnshift, 12 Boolean, 13 ErrorCaptureStackTrace, 14 FunctionPrototypeBind, 15 MathFloor, 16 Number, 17 NumberPrototypeToFixed, 18 ObjectCreate, 19 ObjectDefineProperties, 20 ObjectDefineProperty, 21 ObjectKeys, 22 ObjectPrototypeHasOwnProperty, 23 ObjectValues, 24 ReflectApply, 25 ReflectConstruct, 26 ReflectOwnKeys, 27 RegExpPrototypeSymbolReplace, 28 SafeArrayIterator, 29 SafeMap, 30 SafeWeakMap, 31 SafeSet, 32 StringPrototypeIncludes, 33 StringPrototypePadStart, 34 StringPrototypeRepeat, 35 StringPrototypeSlice, 36 StringPrototypeSplit, 37 Symbol, 38 SymbolHasInstance, 39 SymbolToStringTag, 40} = primordials; 41 42const { trace } = internalBinding('trace_events'); 43const { 44 isStackOverflowError, 45 codes: { 46 ERR_CONSOLE_WRITABLE_STREAM, 47 ERR_INVALID_ARG_VALUE, 48 ERR_INCOMPATIBLE_OPTION_PAIR, 49 }, 50} = require('internal/errors'); 51const { 52 validateArray, 53 validateInteger, 54 validateObject, 55} = require('internal/validators'); 56const { previewEntries } = internalBinding('util'); 57const { Buffer: { isBuffer } } = require('buffer'); 58const { 59 inspect, 60 formatWithOptions, 61} = require('internal/util/inspect'); 62const { 63 isTypedArray, isSet, isMap, isSetIterator, isMapIterator, 64} = require('internal/util/types'); 65const { 66 CHAR_LOWERCASE_B: kTraceBegin, 67 CHAR_LOWERCASE_E: kTraceEnd, 68 CHAR_LOWERCASE_N: kTraceInstant, 69 CHAR_UPPERCASE_C: kTraceCount, 70} = require('internal/constants'); 71const kCounts = Symbol('counts'); 72 73const kTraceConsoleCategory = 'node,node.console'; 74 75const kSecond = 1000; 76const kMinute = 60 * kSecond; 77const kHour = 60 * kMinute; 78const kMaxGroupIndentation = 1000; 79 80// Lazy loaded for startup performance. 81let cliTable; 82 83let utilColors; 84function lazyUtilColors() { 85 utilColors ??= require('internal/util/colors'); 86 return utilColors; 87} 88 89// Track amount of indentation required via `console.group()`. 90const kGroupIndent = Symbol('kGroupIndent'); 91const kGroupIndentationWidth = Symbol('kGroupIndentWidth'); 92const kFormatForStderr = Symbol('kFormatForStderr'); 93const kFormatForStdout = Symbol('kFormatForStdout'); 94const kGetInspectOptions = Symbol('kGetInspectOptions'); 95const kColorMode = Symbol('kColorMode'); 96const kIsConsole = Symbol('kIsConsole'); 97const kWriteToConsole = Symbol('kWriteToConsole'); 98const kBindProperties = Symbol('kBindProperties'); 99const kBindStreamsEager = Symbol('kBindStreamsEager'); 100const kBindStreamsLazy = Symbol('kBindStreamsLazy'); 101const kUseStdout = Symbol('kUseStdout'); 102const kUseStderr = Symbol('kUseStderr'); 103 104const optionsMap = new SafeWeakMap(); 105function Console(options /* or: stdout, stderr, ignoreErrors = true */) { 106 // We have to test new.target here to see if this function is called 107 // with new, because we need to define a custom instanceof to accommodate 108 // the global console. 109 if (new.target === undefined) { 110 return ReflectConstruct(Console, arguments); 111 } 112 113 if (!options || typeof options.write === 'function') { 114 options = { 115 stdout: options, 116 stderr: arguments[1], 117 ignoreErrors: arguments[2], 118 }; 119 } 120 121 const { 122 stdout, 123 stderr = stdout, 124 ignoreErrors = true, 125 colorMode = 'auto', 126 inspectOptions, 127 groupIndentation, 128 } = options; 129 130 if (!stdout || typeof stdout.write !== 'function') { 131 throw new ERR_CONSOLE_WRITABLE_STREAM('stdout'); 132 } 133 if (!stderr || typeof stderr.write !== 'function') { 134 throw new ERR_CONSOLE_WRITABLE_STREAM('stderr'); 135 } 136 137 if (typeof colorMode !== 'boolean' && colorMode !== 'auto') 138 throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode); 139 140 if (groupIndentation !== undefined) { 141 validateInteger(groupIndentation, 'groupIndentation', 142 0, kMaxGroupIndentation); 143 } 144 145 if (inspectOptions !== undefined) { 146 validateObject(inspectOptions, 'options.inspectOptions'); 147 148 if (inspectOptions.colors !== undefined && 149 options.colorMode !== undefined) { 150 throw new ERR_INCOMPATIBLE_OPTION_PAIR( 151 'options.inspectOptions.color', 'colorMode'); 152 } 153 optionsMap.set(this, inspectOptions); 154 } 155 156 // Bind the prototype functions to this Console instance 157 ArrayPrototypeForEach(ObjectKeys(Console.prototype), (key) => { 158 // We have to bind the methods grabbed from the instance instead of from 159 // the prototype so that users extending the Console can override them 160 // from the prototype chain of the subclass. 161 this[key] = FunctionPrototypeBind(this[key], this); 162 ObjectDefineProperty(this[key], 'name', { 163 __proto__: null, 164 value: key, 165 }); 166 }); 167 168 this[kBindStreamsEager](stdout, stderr); 169 this[kBindProperties](ignoreErrors, colorMode, groupIndentation); 170} 171 172const consolePropAttributes = { 173 writable: true, 174 enumerable: false, 175 configurable: true, 176}; 177 178// Fixup global.console instanceof global.console.Console 179ObjectDefineProperty(Console, SymbolHasInstance, { 180 __proto__: null, 181 value(instance) { 182 return instance[kIsConsole]; 183 }, 184}); 185 186const kColorInspectOptions = { colors: true }; 187const kNoColorInspectOptions = {}; 188 189ObjectDefineProperties(Console.prototype, { 190 [kBindStreamsEager]: { 191 __proto__: null, 192 ...consolePropAttributes, 193 // Eager version for the Console constructor 194 value: function(stdout, stderr) { 195 ObjectDefineProperties(this, { 196 '_stdout': { __proto__: null, ...consolePropAttributes, value: stdout }, 197 '_stderr': { __proto__: null, ...consolePropAttributes, value: stderr }, 198 }); 199 }, 200 }, 201 [kBindStreamsLazy]: { 202 __proto__: null, 203 ...consolePropAttributes, 204 // Lazily load the stdout and stderr from an object so we don't 205 // create the stdio streams when they are not even accessed 206 value: function(object) { 207 let stdout; 208 let stderr; 209 ObjectDefineProperties(this, { 210 '_stdout': { 211 __proto__: null, 212 enumerable: false, 213 configurable: true, 214 get() { 215 if (!stdout) stdout = object.stdout; 216 return stdout; 217 }, 218 set(value) { stdout = value; }, 219 }, 220 '_stderr': { 221 __proto__: null, 222 enumerable: false, 223 configurable: true, 224 get() { 225 if (!stderr) { stderr = object.stderr; } 226 return stderr; 227 }, 228 set(value) { stderr = value; }, 229 }, 230 }); 231 }, 232 }, 233 [kBindProperties]: { 234 __proto__: null, 235 ...consolePropAttributes, 236 value: function(ignoreErrors, colorMode, groupIndentation = 2) { 237 ObjectDefineProperties(this, { 238 '_stdoutErrorHandler': { 239 __proto__: null, 240 ...consolePropAttributes, 241 value: createWriteErrorHandler(this, kUseStdout), 242 }, 243 '_stderrErrorHandler': { 244 ...consolePropAttributes, 245 __proto__: null, 246 value: createWriteErrorHandler(this, kUseStderr), 247 }, 248 '_ignoreErrors': { 249 __proto__: null, 250 ...consolePropAttributes, 251 value: Boolean(ignoreErrors), 252 }, 253 '_times': { __proto__: null, ...consolePropAttributes, value: new SafeMap() }, 254 // Corresponds to https://console.spec.whatwg.org/#count-map 255 [kCounts]: { __proto__: null, ...consolePropAttributes, value: new SafeMap() }, 256 [kColorMode]: { __proto__: null, ...consolePropAttributes, value: colorMode }, 257 [kIsConsole]: { __proto__: null, ...consolePropAttributes, value: true }, 258 [kGroupIndent]: { __proto__: null, ...consolePropAttributes, value: '' }, 259 [kGroupIndentationWidth]: { 260 __proto__: null, 261 ...consolePropAttributes, 262 value: groupIndentation, 263 }, 264 [SymbolToStringTag]: { 265 __proto__: null, 266 writable: false, 267 enumerable: false, 268 configurable: true, 269 value: 'console', 270 }, 271 }); 272 }, 273 }, 274 [kWriteToConsole]: { 275 __proto__: null, 276 ...consolePropAttributes, 277 value: function(streamSymbol, string) { 278 const ignoreErrors = this._ignoreErrors; 279 const groupIndent = this[kGroupIndent]; 280 281 const useStdout = streamSymbol === kUseStdout; 282 const stream = useStdout ? this._stdout : this._stderr; 283 const errorHandler = useStdout ? 284 this._stdoutErrorHandler : this._stderrErrorHandler; 285 286 if (groupIndent.length !== 0) { 287 if (StringPrototypeIncludes(string, '\n')) { 288 string = RegExpPrototypeSymbolReplace(/\n/g, string, `\n${groupIndent}`); 289 } 290 string = groupIndent + string; 291 } 292 string += '\n'; 293 294 if (ignoreErrors === false) return stream.write(string); 295 296 // There may be an error occurring synchronously (e.g. for files or TTYs 297 // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so 298 // handle both situations. 299 try { 300 // Add and later remove a noop error handler to catch synchronous 301 // errors. 302 if (stream.listenerCount('error') === 0) 303 stream.once('error', noop); 304 305 stream.write(string, errorHandler); 306 } catch (e) { 307 // Console is a debugging utility, so it swallowing errors is not 308 // desirable even in edge cases such as low stack space. 309 if (isStackOverflowError(e)) 310 throw e; 311 // Sorry, there's no proper way to pass along the error here. 312 } finally { 313 stream.removeListener('error', noop); 314 } 315 }, 316 }, 317 [kGetInspectOptions]: { 318 __proto__: null, 319 ...consolePropAttributes, 320 value: function(stream) { 321 let color = this[kColorMode]; 322 if (color === 'auto') { 323 color = lazyUtilColors().shouldColorize(stream); 324 } 325 326 const options = optionsMap.get(this); 327 if (options) { 328 if (options.colors === undefined) { 329 options.colors = color; 330 } 331 return options; 332 } 333 334 return color ? kColorInspectOptions : kNoColorInspectOptions; 335 }, 336 }, 337 [kFormatForStdout]: { 338 __proto__: null, 339 ...consolePropAttributes, 340 value: function(args) { 341 const opts = this[kGetInspectOptions](this._stdout); 342 ArrayPrototypeUnshift(args, opts); 343 return ReflectApply(formatWithOptions, null, args); 344 }, 345 }, 346 [kFormatForStderr]: { 347 __proto__: null, 348 ...consolePropAttributes, 349 value: function(args) { 350 const opts = this[kGetInspectOptions](this._stderr); 351 ArrayPrototypeUnshift(args, opts); 352 return ReflectApply(formatWithOptions, null, args); 353 }, 354 }, 355}); 356 357// Make a function that can serve as the callback passed to `stream.write()`. 358function createWriteErrorHandler(instance, streamSymbol) { 359 return (err) => { 360 // This conditional evaluates to true if and only if there was an error 361 // that was not already emitted (which happens when the _write callback 362 // is invoked asynchronously). 363 const stream = streamSymbol === kUseStdout ? 364 instance._stdout : instance._stderr; 365 if (err !== null && !stream._writableState.errorEmitted) { 366 // If there was an error, it will be emitted on `stream` as 367 // an `error` event. Adding a `once` listener will keep that error 368 // from becoming an uncaught exception, but since the handler is 369 // removed after the event, non-console.* writes won't be affected. 370 // we are only adding noop if there is no one else listening for 'error' 371 if (stream.listenerCount('error') === 0) { 372 stream.once('error', noop); 373 } 374 } 375 }; 376} 377 378const consoleMethods = { 379 log(...args) { 380 this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args)); 381 }, 382 383 384 warn(...args) { 385 this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); 386 }, 387 388 389 dir(object, options) { 390 this[kWriteToConsole](kUseStdout, inspect(object, { 391 customInspect: false, 392 ...this[kGetInspectOptions](this._stdout), 393 ...options, 394 })); 395 }, 396 397 time(label = 'default') { 398 // Coerces everything other than Symbol to a string 399 label = `${label}`; 400 if (this._times.has(label)) { 401 process.emitWarning(`Label '${label}' already exists for console.time()`); 402 return; 403 } 404 trace(kTraceBegin, kTraceConsoleCategory, `time::${label}`, 0); 405 this._times.set(label, process.hrtime()); 406 }, 407 408 timeEnd(label = 'default') { 409 // Coerces everything other than Symbol to a string 410 label = `${label}`; 411 const found = timeLogImpl(this, 'timeEnd', label); 412 trace(kTraceEnd, kTraceConsoleCategory, `time::${label}`, 0); 413 if (found) { 414 this._times.delete(label); 415 } 416 }, 417 418 timeLog(label = 'default', ...data) { 419 // Coerces everything other than Symbol to a string 420 label = `${label}`; 421 timeLogImpl(this, 'timeLog', label, data); 422 trace(kTraceInstant, kTraceConsoleCategory, `time::${label}`, 0); 423 }, 424 425 trace: function trace(...args) { 426 const err = { 427 name: 'Trace', 428 message: this[kFormatForStderr](args), 429 }; 430 ErrorCaptureStackTrace(err, trace); 431 this.error(err.stack); 432 }, 433 434 assert(expression, ...args) { 435 if (!expression) { 436 args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`; 437 // The arguments will be formatted in warn() again 438 ReflectApply(this.warn, this, args); 439 } 440 }, 441 442 // Defined by: https://console.spec.whatwg.org/#clear 443 clear() { 444 // It only makes sense to clear if _stdout is a TTY. 445 // Otherwise, do nothing. 446 if (this._stdout.isTTY && process.env.TERM !== 'dumb') { 447 // The require is here intentionally to avoid readline being 448 // required too early when console is first loaded. 449 const { 450 cursorTo, 451 clearScreenDown, 452 } = require('internal/readline/callbacks'); 453 cursorTo(this._stdout, 0, 0); 454 clearScreenDown(this._stdout); 455 } 456 }, 457 458 // Defined by: https://console.spec.whatwg.org/#count 459 count(label = 'default') { 460 // Ensures that label is a string, and only things that can be 461 // coerced to strings. e.g. Symbol is not allowed 462 label = `${label}`; 463 const counts = this[kCounts]; 464 let count = counts.get(label); 465 if (count === undefined) 466 count = 1; 467 else 468 count++; 469 counts.set(label, count); 470 trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, count); 471 this.log(`${label}: ${count}`); 472 }, 473 474 // Defined by: https://console.spec.whatwg.org/#countreset 475 countReset(label = 'default') { 476 const counts = this[kCounts]; 477 if (!counts.has(label)) { 478 process.emitWarning(`Count for '${label}' does not exist`); 479 return; 480 } 481 trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, 0); 482 counts.delete(`${label}`); 483 }, 484 485 group(...data) { 486 if (data.length > 0) { 487 ReflectApply(this.log, this, data); 488 } 489 this[kGroupIndent] += 490 StringPrototypeRepeat(' ', this[kGroupIndentationWidth]); 491 }, 492 493 groupEnd() { 494 this[kGroupIndent] = StringPrototypeSlice( 495 this[kGroupIndent], 496 0, 497 this[kGroupIndent].length - this[kGroupIndentationWidth], 498 ); 499 }, 500 501 // https://console.spec.whatwg.org/#table 502 table(tabularData, properties) { 503 if (properties !== undefined) 504 validateArray(properties, 'properties'); 505 506 if (tabularData === null || typeof tabularData !== 'object') 507 return this.log(tabularData); 508 509 cliTable ??= require('internal/cli_table'); 510 const final = (k, v) => this.log(cliTable(k, v)); 511 512 const _inspect = (v) => { 513 const depth = v !== null && 514 typeof v === 'object' && 515 !isArray(v) && 516 ObjectKeys(v).length > 2 ? -1 : 0; 517 const opt = { 518 depth, 519 maxArrayLength: 3, 520 breakLength: Infinity, 521 ...this[kGetInspectOptions](this._stdout), 522 }; 523 return inspect(v, opt); 524 }; 525 const getIndexArray = (length) => ArrayFrom( 526 { length }, (_, i) => _inspect(i)); 527 528 const mapIter = isMapIterator(tabularData); 529 let isKeyValue = false; 530 let i = 0; 531 if (mapIter) { 532 const res = previewEntries(tabularData, true); 533 tabularData = res[0]; 534 isKeyValue = res[1]; 535 } 536 537 if (isKeyValue || isMap(tabularData)) { 538 const keys = []; 539 const values = []; 540 let length = 0; 541 if (mapIter) { 542 for (; i < tabularData.length / 2; ++i) { 543 ArrayPrototypePush(keys, _inspect(tabularData[i * 2])); 544 ArrayPrototypePush(values, _inspect(tabularData[i * 2 + 1])); 545 length++; 546 } 547 } else { 548 for (const { 0: k, 1: v } of tabularData) { 549 ArrayPrototypePush(keys, _inspect(k)); 550 ArrayPrototypePush(values, _inspect(v)); 551 length++; 552 } 553 } 554 return final([ 555 iterKey, keyKey, valuesKey, 556 ], [ 557 getIndexArray(length), 558 keys, 559 values, 560 ]); 561 } 562 563 const setIter = isSetIterator(tabularData); 564 if (setIter) 565 tabularData = previewEntries(tabularData); 566 567 const setlike = setIter || mapIter || isSet(tabularData); 568 if (setlike) { 569 const values = []; 570 let length = 0; 571 for (const v of tabularData) { 572 ArrayPrototypePush(values, _inspect(v)); 573 length++; 574 } 575 return final([iterKey, valuesKey], [getIndexArray(length), values]); 576 } 577 578 const map = ObjectCreate(null); 579 let hasPrimitives = false; 580 const valuesKeyArray = []; 581 const indexKeyArray = ObjectKeys(tabularData); 582 583 for (; i < indexKeyArray.length; i++) { 584 const item = tabularData[indexKeyArray[i]]; 585 const primitive = item === null || 586 (typeof item !== 'function' && typeof item !== 'object'); 587 if (properties === undefined && primitive) { 588 hasPrimitives = true; 589 valuesKeyArray[i] = _inspect(item); 590 } else { 591 const keys = properties || ObjectKeys(item); 592 for (const key of keys) { 593 map[key] ??= []; 594 if ((primitive && properties) || 595 !ObjectPrototypeHasOwnProperty(item, key)) 596 map[key][i] = ''; 597 else 598 map[key][i] = _inspect(item[key]); 599 } 600 } 601 } 602 603 const keys = ObjectKeys(map); 604 const values = ObjectValues(map); 605 if (hasPrimitives) { 606 ArrayPrototypePush(keys, valuesKey); 607 ArrayPrototypePush(values, valuesKeyArray); 608 } 609 ArrayPrototypeUnshift(keys, indexKey); 610 ArrayPrototypeUnshift(values, indexKeyArray); 611 612 return final(keys, values); 613 }, 614}; 615 616// Returns true if label was found 617function timeLogImpl(self, name, label, data) { 618 const time = self._times.get(label); 619 if (time === undefined) { 620 process.emitWarning(`No such label '${label}' for console.${name}()`); 621 return false; 622 } 623 const duration = process.hrtime(time); 624 const ms = duration[0] * 1000 + duration[1] / 1e6; 625 626 const formatted = formatTime(ms); 627 628 if (data === undefined) { 629 self.log('%s: %s', label, formatted); 630 } else { 631 self.log('%s: %s', label, formatted, ...new SafeArrayIterator(data)); 632 } 633 return true; 634} 635 636function pad(value) { 637 return StringPrototypePadStart(`${value}`, 2, '0'); 638} 639 640function formatTime(ms) { 641 let hours = 0; 642 let minutes = 0; 643 let seconds = 0; 644 645 if (ms >= kSecond) { 646 if (ms >= kMinute) { 647 if (ms >= kHour) { 648 hours = MathFloor(ms / kHour); 649 ms = ms % kHour; 650 } 651 minutes = MathFloor(ms / kMinute); 652 ms = ms % kMinute; 653 } 654 seconds = ms / kSecond; 655 } 656 657 if (hours !== 0 || minutes !== 0) { 658 ({ 0: seconds, 1: ms } = StringPrototypeSplit( 659 NumberPrototypeToFixed(seconds, 3), 660 '.', 661 )); 662 const res = hours !== 0 ? `${hours}:${pad(minutes)}` : minutes; 663 return `${res}:${pad(seconds)}.${ms} (${hours !== 0 ? 'h:m' : ''}m:ss.mmm)`; 664 } 665 666 if (seconds !== 0) { 667 return `${NumberPrototypeToFixed(seconds, 3)}s`; 668 } 669 670 return `${Number(NumberPrototypeToFixed(ms, 3))}ms`; 671} 672 673const keyKey = 'Key'; 674const valuesKey = 'Values'; 675const indexKey = '(index)'; 676const iterKey = '(iteration index)'; 677 678const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v); 679 680function noop() {} 681 682for (const method of ReflectOwnKeys(consoleMethods)) 683 Console.prototype[method] = consoleMethods[method]; 684 685Console.prototype.debug = Console.prototype.log; 686Console.prototype.info = Console.prototype.log; 687Console.prototype.dirxml = Console.prototype.log; 688Console.prototype.error = Console.prototype.warn; 689Console.prototype.groupCollapsed = Console.prototype.group; 690 691function initializeGlobalConsole(globalConsole) { 692 globalConsole[kBindStreamsLazy](process); 693 globalConsole[kBindProperties](true, 'auto'); 694 695 const { 696 namespace: { 697 addSerializeCallback, 698 isBuildingSnapshot, 699 }, 700 } = require('internal/v8/startup_snapshot'); 701 702 if (!internalBinding('config').hasInspector || !isBuildingSnapshot()) { 703 return; 704 } 705 const { console: consoleFromVM } = internalBinding('inspector'); 706 const nodeConsoleKeys = ObjectKeys(Console.prototype); 707 const vmConsoleKeys = ObjectKeys(consoleFromVM); 708 const originalKeys = new SafeSet(vmConsoleKeys.concat(nodeConsoleKeys)); 709 const inspectorConsoleKeys = new SafeSet(); 710 for (const key of ObjectKeys(globalConsole)) { 711 if (!originalKeys.has(key)) { 712 inspectorConsoleKeys.add(key); 713 } 714 } 715 // During deserialization these should be reinstalled to console by 716 // V8 when the inspector client is created. 717 addSerializeCallback(() => { 718 for (const key of inspectorConsoleKeys) { 719 globalConsole[key] = undefined; 720 } 721 }); 722} 723 724module.exports = { 725 Console, 726 kBindStreamsLazy, 727 kBindProperties, 728 initializeGlobalConsole, 729 formatTime, // exported for tests 730}; 731