• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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