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