From 57e8793c4393aa3fafd87f289b19078b1918c166 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Mar 2018 14:09:17 +0100 Subject: console: add color support Add a way to tell `Console` instances to either always use, never use or auto-detect color support and inspect objects accordingly. PR-URL: https://github.com/nodejs/node/pull/19372 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell --- doc/api/console.md | 8 +++- doc/api/util.md | 4 +- lib/console.js | 63 +++++++++++++++++++++++++------- test/parallel/test-console-tty-colors.js | 46 +++++++++++++++++++++++ test/parallel/test-console.js | 2 + 5 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 test/parallel/test-console-tty-colors.js diff --git a/doc/api/console.md b/doc/api/console.md index 99c31df242..cce2a4eb6e 100644 --- a/doc/api/console.md +++ b/doc/api/console.md @@ -87,7 +87,8 @@ changes: description: The `ignoreErrors` option was introduced. - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/19372 - description: The `Console` constructor now supports an `options` argument. + description: The `Console` constructor now supports an `options` argument, + and the `colorMode` option was introduced. --> * `options` {Object} @@ -95,6 +96,11 @@ changes: * `stderr` {stream.Writable} * `ignoreErrors` {boolean} Ignore errors when writing to the underlying streams. **Default:** `true`. + * `colorMode` {boolean|string} Set color support for this `Console` instance. + Setting to `true` enables coloring while inspecting values, setting to + `'auto'` will make color support depend on the value of the `isTTY` property + and the value returned by `getColorDepth()` on the respective stream. + **Default:** `false` Creates a new `Console` with one or two writable stream instances. `stdout` is a writable stream to print log or info output. `stderr` is used for warning or diff --git a/doc/api/util.md b/doc/api/util.md index 23a1774a1f..e8f97397d3 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -268,8 +268,8 @@ an `inspectOptions` argument which specifies options that are passed along to ```js util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); - // Returns 'See object { foo: 42 }', where `42` is colored as a number - // when printed to a terminal. +// Returns 'See object { foo: 42 }', where `42` is colored as a number +// when printed to a terminal. ``` ## util.getSystemErrorName(err) diff --git a/lib/console.js b/lib/console.js index 9557b27fbc..2c35e98223 100644 --- a/lib/console.js +++ b/lib/console.js @@ -26,6 +26,7 @@ const { codes: { ERR_CONSOLE_WRITABLE_STREAM, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, }, } = require('internal/errors'); const { previewMapIterator, previewSetIterator } = require('internal/v8'); @@ -49,24 +50,32 @@ const { } = Array; // Track amount of indentation required via `console.group()`. -const kGroupIndent = Symbol('groupIndent'); +const kGroupIndent = Symbol('kGroupIndent'); + +const kFormatForStderr = Symbol('kFormatForStderr'); +const kFormatForStdout = Symbol('kFormatForStdout'); +const kGetInspectOptions = Symbol('kGetInspectOptions'); +const kColorMode = Symbol('kColorMode'); function Console(options /* or: stdout, stderr, ignoreErrors = true */) { if (!(this instanceof Console)) { return new Console(...arguments); } - let stdout, stderr, ignoreErrors; + let stdout, stderr, ignoreErrors, colorMode; if (options && typeof options.write !== 'function') { ({ stdout, stderr = stdout, - ignoreErrors = true + ignoreErrors = true, + colorMode = false } = options); } else { - stdout = options; - stderr = arguments[1]; - ignoreErrors = arguments[2] === undefined ? true : arguments[2]; + return new Console({ + stdout: options, + stderr: arguments[1], + ignoreErrors: arguments[2] + }); } if (!stdout || typeof stdout.write !== 'function') { @@ -94,7 +103,11 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) { prop.value = createWriteErrorHandler(stderr); Object.defineProperty(this, '_stderrErrorHandler', prop); + if (typeof colorMode !== 'boolean' && colorMode !== 'auto') + throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode); + this[kCounts] = new Map(); + this[kColorMode] = colorMode; Object.defineProperty(this, kGroupIndent, { writable: true }); this[kGroupIndent] = ''; @@ -156,13 +169,33 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) { } } +const kColorInspectOptions = { colors: true }; +const kNoColorInspectOptions = {}; +Console.prototype[kGetInspectOptions] = function(stream) { + let color = this[kColorMode]; + if (color === 'auto') { + color = stream.isTTY && ( + typeof stream.getColorDepth === 'function' ? + stream.getColorDepth() > 2 : true); + } + + return color ? kColorInspectOptions : kNoColorInspectOptions; +}; + +Console.prototype[kFormatForStdout] = function(args) { + const opts = this[kGetInspectOptions](this._stdout); + return util.formatWithOptions(opts, ...args); +}; + +Console.prototype[kFormatForStderr] = function(args) { + const opts = this[kGetInspectOptions](this._stderr); + return util.formatWithOptions(opts, ...args); +}; + Console.prototype.log = function log(...args) { write(this._ignoreErrors, this._stdout, - // The performance of .apply and the spread operator seems on par in V8 - // 6.3 but the spread operator, unlike .apply(), pushes the elements - // onto the stack. That is, it makes stack overflows more likely. - util.format.apply(null, args), + this[kFormatForStdout](args), this._stdoutErrorHandler, this[kGroupIndent]); }; @@ -173,14 +206,16 @@ Console.prototype.dirxml = Console.prototype.log; Console.prototype.warn = function warn(...args) { write(this._ignoreErrors, this._stderr, - util.format.apply(null, args), + this[kFormatForStderr](args), this._stderrErrorHandler, this[kGroupIndent]); }; Console.prototype.error = Console.prototype.warn; Console.prototype.dir = function dir(object, options) { - options = Object.assign({ customInspect: false }, options); + options = Object.assign({ + customInspect: false + }, this[kGetInspectOptions](this._stdout), options); write(this._ignoreErrors, this._stdout, util.inspect(object, options), @@ -211,7 +246,7 @@ Console.prototype.timeEnd = function timeEnd(label = 'default') { Console.prototype.trace = function trace(...args) { const err = { name: 'Trace', - message: util.format.apply(null, args) + message: this[kFormatForStderr](args) }; Error.captureStackTrace(err, trace); this.error(err.stack); @@ -220,7 +255,7 @@ Console.prototype.trace = function trace(...args) { Console.prototype.assert = function assert(expression, ...args) { if (!expression) { args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`; - this.warn(util.format.apply(null, args)); + this.warn(this[kFormatForStderr](args)); } }; diff --git a/test/parallel/test-console-tty-colors.js b/test/parallel/test-console-tty-colors.js new file mode 100644 index 0000000000..945c21f28a --- /dev/null +++ b/test/parallel/test-console-tty-colors.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { Writable } = require('stream'); +const { Console } = require('console'); + +function check(isTTY, colorMode, expectedColorMode) { + const items = [ + 1, + { a: 2 }, + [ 'foo' ], + { '\\a': '\\bar' } + ]; + + let i = 0; + const stream = new Writable({ + write: common.mustCall((chunk, enc, cb) => { + assert.strictEqual(chunk.trim(), + util.inspect(items[i++], { + colors: expectedColorMode + })); + cb(); + }, items.length), + decodeStrings: false + }); + stream.isTTY = isTTY; + + // Set ignoreErrors to `false` here so that we see assertion failures + // from the `write()` call happen. + const testConsole = new Console({ + stdout: stream, + ignoreErrors: false, + colorMode + }); + for (const item of items) { + testConsole.log(item); + } +} + +check(true, 'auto', true); +check(false, 'auto', false); +check(true, true, true); +check(false, true, true); +check(true, false, false); +check(false, false, false); diff --git a/test/parallel/test-console.js b/test/parallel/test-console.js index 4bde18d888..4bac35c65d 100644 --- a/test/parallel/test-console.js +++ b/test/parallel/test-console.js @@ -51,9 +51,11 @@ const custom_inspect = { foo: 'bar', inspect: () => 'inspect' }; const strings = []; const errStrings = []; +process.stdout.isTTY = false; common.hijackStdout(function(data) { strings.push(data); }); +process.stderr.isTTY = false; common.hijackStderr(function(data) { errStrings.push(data); }); -- cgit v1.2.3