From 75cc568db76314839388a739a0a5ddacd3a1f45e Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Mar 2018 13:45:43 +0100 Subject: [PATCH 1/5] util: introduce `formatWithOptions()` Identical to `format()` except that it takes an options argument that is passed through to `inspect()`. --- doc/api/util.md | 19 +++++++++++++++++++ lib/util.js | 33 ++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index effe5074d2be89..23a1774a1fa6bf 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -254,6 +254,24 @@ intended as a debugging tool. Some input values can have a significant performance overhead that can block the event loop. Use this function with care and never in a hot code path. +## util.formatWithOptions(inspectOptions, format[, ...args]) + + +* `inspectOptions` {Object} +* `format` {string} + +This function is identical to [`util.format()`][], except in that it takes +an `inspectOptions` argument which specifies options that are passed along to +[`util.inspect()`][]. + +```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. +``` + ## util.getSystemErrorName(err) + The `Console` class can be used to create a simple logger with configurable @@ -77,9 +78,18 @@ const { Console } = require('console'); const { Console } = console; ``` -### new Console(stdout[, stderr]) +### new Console(stdout[, stderr][, ignoreErrors]) + + * `stdout` {stream.Writable} * `stderr` {stream.Writable} +* `ignoreErrors` {boolean} Ignore errors when writing to the underlying streams. + Defaults to `true`. 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 From ec7741fa20d62907a8c617b974f9238610fa7493 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 17 Mar 2018 12:53:09 +0100 Subject: [PATCH 3/5] console: allow `options` object as constructor arg --- doc/api/console.md | 17 +++++++++++------ lib/console.js | 27 +++++++++++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/doc/api/console.md b/doc/api/console.md index db3a903eaf8a56..99c31df242a19b 100644 --- a/doc/api/console.md +++ b/doc/api/console.md @@ -79,17 +79,22 @@ const { Console } = console; ``` ### new Console(stdout[, stderr][, ignoreErrors]) +### new Console(options) -* `stdout` {stream.Writable} -* `stderr` {stream.Writable} -* `ignoreErrors` {boolean} Ignore errors when writing to the underlying streams. - Defaults to `true`. +* `options` {Object} + * `stdout` {stream.Writable} + * `stderr` {stream.Writable} + * `ignoreErrors` {boolean} Ignore errors when writing to the underlying + streams. **Default:** `true`. 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 @@ -99,7 +104,7 @@ error output. If `stderr` is not provided, `stdout` is used for `stderr`. const output = fs.createWriteStream('./stdout.log'); const errorOutput = fs.createWriteStream('./stderr.log'); // custom simple logger -const logger = new Console(output, errorOutput); +const logger = new Console({ stdout: output, stderr: errorOutput }); // use it like console const count = 5; logger.log('count: %d', count); @@ -110,7 +115,7 @@ The global `console` is a special `Console` whose output is sent to [`process.stdout`][] and [`process.stderr`][]. It is equivalent to calling: ```js -new Console(process.stdout, process.stderr); +new Console({ stdout: process.stdout, stderr: process.stderr }); ``` ### console.assert(value[, ...message]) diff --git a/lib/console.js b/lib/console.js index 456c0cc439f740..9557b27fbcabf9 100644 --- a/lib/console.js +++ b/lib/console.js @@ -51,16 +51,28 @@ const { // Track amount of indentation required via `console.group()`. const kGroupIndent = Symbol('groupIndent'); -function Console(stdout, stderr, ignoreErrors = true) { +function Console(options /* or: stdout, stderr, ignoreErrors = true */) { if (!(this instanceof Console)) { - return new Console(stdout, stderr, ignoreErrors); + return new Console(...arguments); } + + let stdout, stderr, ignoreErrors; + if (options && typeof options.write !== 'function') { + ({ + stdout, + stderr = stdout, + ignoreErrors = true + } = options); + } else { + stdout = options; + stderr = arguments[1]; + ignoreErrors = arguments[2] === undefined ? true : arguments[2]; + } + if (!stdout || typeof stdout.write !== 'function') { throw new ERR_CONSOLE_WRITABLE_STREAM('stdout'); } - if (!stderr) { - stderr = stdout; - } else if (typeof stderr.write !== 'function') { + if (!stderr || typeof stderr.write !== 'function') { throw new ERR_CONSOLE_WRITABLE_STREAM('stderr'); } @@ -369,7 +381,10 @@ Console.prototype.table = function(tabularData, properties) { return final(keys, values); }; -module.exports = new Console(process.stdout, process.stderr); +module.exports = new Console({ + stdout: process.stdout, + stderr: process.stderr +}); module.exports.Console = Console; function noop() {} From 313f29c34cd737e8a54b9ffdf405242d8e6f8db6 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Mar 2018 14:09:17 +0100 Subject: [PATCH 4/5] 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. --- 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 99c31df242a19b..cce2a4eb6edf1d 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 23a1774a1fa6bf..e8f97397d30b27 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 9557b27fbcabf9..2c35e9822346a7 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 00000000000000..945c21f28a27de --- /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 4bde18d8883b06..4bac35c65dfc70 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); }); From 0e52d4bf63a7b8d29e0b7f8eb8af471ddf733f91 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Mar 2018 14:10:43 +0100 Subject: [PATCH 5/5] console: auto-detect color support by default This makes Node pretty-print objects with color by default when `console.log()`-ing them. --- doc/api/console.md | 2 +- lib/console.js | 2 +- test/pseudo-tty/console_colors.js | 9 +++++++++ test/pseudo-tty/console_colors.out | 3 +++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 test/pseudo-tty/console_colors.js create mode 100644 test/pseudo-tty/console_colors.out diff --git a/doc/api/console.md b/doc/api/console.md index cce2a4eb6edf1d..59e5ae368569ff 100644 --- a/doc/api/console.md +++ b/doc/api/console.md @@ -100,7 +100,7 @@ changes: 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` + **Default:** `'auto'` 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/lib/console.js b/lib/console.js index 2c35e9822346a7..f4610f745ea870 100644 --- a/lib/console.js +++ b/lib/console.js @@ -68,7 +68,7 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) { stdout, stderr = stdout, ignoreErrors = true, - colorMode = false + colorMode = 'auto' } = options); } else { return new Console({ diff --git a/test/pseudo-tty/console_colors.js b/test/pseudo-tty/console_colors.js new file mode 100644 index 00000000000000..dd16a0c028dccd --- /dev/null +++ b/test/pseudo-tty/console_colors.js @@ -0,0 +1,9 @@ +'use strict'; +require('../common'); +// Make this test OS-independent by overriding stdio getColorDepth(). +process.stdout.getColorDepth = () => 8; +process.stderr.getColorDepth = () => 8; + +console.log({ foo: 'bar' }); +console.log('%s q', 'string'); +console.log('%o with object format param', { foo: 'bar' }); diff --git a/test/pseudo-tty/console_colors.out b/test/pseudo-tty/console_colors.out new file mode 100644 index 00000000000000..f0ee5e42d60db5 --- /dev/null +++ b/test/pseudo-tty/console_colors.out @@ -0,0 +1,3 @@ +{ foo: *[32m'bar'*[39m } +string q +{ foo: *[32m'bar'*[39m } with object format param