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 <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Anna Henningsen 2018-03-15 14:09:17 +01:00
parent ce58df58d0
commit 57e8793c43
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
5 changed files with 106 additions and 17 deletions

View File

@ -87,7 +87,8 @@ changes:
description: The `ignoreErrors` option was introduced. description: The `ignoreErrors` option was introduced.
- version: REPLACEME - version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19372 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} * `options` {Object}
@ -95,6 +96,11 @@ changes:
* `stderr` {stream.Writable} * `stderr` {stream.Writable}
* `ignoreErrors` {boolean} Ignore errors when writing to the underlying * `ignoreErrors` {boolean} Ignore errors when writing to the underlying
streams. **Default:** `true`. 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 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 writable stream to print log or info output. `stderr` is used for warning or

View File

@ -268,8 +268,8 @@ an `inspectOptions` argument which specifies options that are passed along to
```js ```js
util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// Returns 'See object { foo: 42 }', where `42` is colored as a number // Returns 'See object { foo: 42 }', where `42` is colored as a number
// when printed to a terminal. // when printed to a terminal.
``` ```
## util.getSystemErrorName(err) ## util.getSystemErrorName(err)

View File

@ -26,6 +26,7 @@ const {
codes: { codes: {
ERR_CONSOLE_WRITABLE_STREAM, ERR_CONSOLE_WRITABLE_STREAM,
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
}, },
} = require('internal/errors'); } = require('internal/errors');
const { previewMapIterator, previewSetIterator } = require('internal/v8'); const { previewMapIterator, previewSetIterator } = require('internal/v8');
@ -49,24 +50,32 @@ const {
} = Array; } = Array;
// Track amount of indentation required via `console.group()`. // 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 */) { function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
if (!(this instanceof Console)) { if (!(this instanceof Console)) {
return new Console(...arguments); return new Console(...arguments);
} }
let stdout, stderr, ignoreErrors; let stdout, stderr, ignoreErrors, colorMode;
if (options && typeof options.write !== 'function') { if (options && typeof options.write !== 'function') {
({ ({
stdout, stdout,
stderr = stdout, stderr = stdout,
ignoreErrors = true ignoreErrors = true,
colorMode = false
} = options); } = options);
} else { } else {
stdout = options; return new Console({
stderr = arguments[1]; stdout: options,
ignoreErrors = arguments[2] === undefined ? true : arguments[2]; stderr: arguments[1],
ignoreErrors: arguments[2]
});
} }
if (!stdout || typeof stdout.write !== 'function') { if (!stdout || typeof stdout.write !== 'function') {
@ -94,7 +103,11 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
prop.value = createWriteErrorHandler(stderr); prop.value = createWriteErrorHandler(stderr);
Object.defineProperty(this, '_stderrErrorHandler', prop); Object.defineProperty(this, '_stderrErrorHandler', prop);
if (typeof colorMode !== 'boolean' && colorMode !== 'auto')
throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode);
this[kCounts] = new Map(); this[kCounts] = new Map();
this[kColorMode] = colorMode;
Object.defineProperty(this, kGroupIndent, { writable: true }); Object.defineProperty(this, kGroupIndent, { writable: true });
this[kGroupIndent] = ''; 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) { Console.prototype.log = function log(...args) {
write(this._ignoreErrors, write(this._ignoreErrors,
this._stdout, this._stdout,
// The performance of .apply and the spread operator seems on par in V8 this[kFormatForStdout](args),
// 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._stdoutErrorHandler, this._stdoutErrorHandler,
this[kGroupIndent]); this[kGroupIndent]);
}; };
@ -173,14 +206,16 @@ Console.prototype.dirxml = Console.prototype.log;
Console.prototype.warn = function warn(...args) { Console.prototype.warn = function warn(...args) {
write(this._ignoreErrors, write(this._ignoreErrors,
this._stderr, this._stderr,
util.format.apply(null, args), this[kFormatForStderr](args),
this._stderrErrorHandler, this._stderrErrorHandler,
this[kGroupIndent]); this[kGroupIndent]);
}; };
Console.prototype.error = Console.prototype.warn; Console.prototype.error = Console.prototype.warn;
Console.prototype.dir = function dir(object, options) { 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, write(this._ignoreErrors,
this._stdout, this._stdout,
util.inspect(object, options), util.inspect(object, options),
@ -211,7 +246,7 @@ Console.prototype.timeEnd = function timeEnd(label = 'default') {
Console.prototype.trace = function trace(...args) { Console.prototype.trace = function trace(...args) {
const err = { const err = {
name: 'Trace', name: 'Trace',
message: util.format.apply(null, args) message: this[kFormatForStderr](args)
}; };
Error.captureStackTrace(err, trace); Error.captureStackTrace(err, trace);
this.error(err.stack); this.error(err.stack);
@ -220,7 +255,7 @@ Console.prototype.trace = function trace(...args) {
Console.prototype.assert = function assert(expression, ...args) { Console.prototype.assert = function assert(expression, ...args) {
if (!expression) { if (!expression) {
args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`; args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`;
this.warn(util.format.apply(null, args)); this.warn(this[kFormatForStderr](args));
} }
}; };

View File

@ -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);

View File

@ -51,9 +51,11 @@ const custom_inspect = { foo: 'bar', inspect: () => 'inspect' };
const strings = []; const strings = [];
const errStrings = []; const errStrings = [];
process.stdout.isTTY = false;
common.hijackStdout(function(data) { common.hijackStdout(function(data) {
strings.push(data); strings.push(data);
}); });
process.stderr.isTTY = false;
common.hijackStderr(function(data) { common.hijackStderr(function(data) {
errStrings.push(data); errStrings.push(data);
}); });