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