diff --git a/lib/console.js b/lib/console.js index 496280d72c3a83..f0b9a74cfc7df8 100644 --- a/lib/console.js +++ b/lib/console.js @@ -60,14 +60,25 @@ let cliTable; // Track amount of indentation required via `console.group()`. const kGroupIndent = Symbol('kGroupIndent'); - const kFormatForStderr = Symbol('kFormatForStderr'); const kFormatForStdout = Symbol('kFormatForStdout'); const kGetInspectOptions = Symbol('kGetInspectOptions'); const kColorMode = Symbol('kColorMode'); - +const kIsConsole = Symbol('kIsConsole'); +const kWriteToConsole = Symbol('kWriteToConsole'); +const kBindProperties = Symbol('kBindProperties'); +const kBindStreamsEager = Symbol('kBindStreamsEager'); +const kBindStreamsLazy = Symbol('kBindStreamsLazy'); +const kUseStdout = Symbol('kUseStdout'); +const kUseStderr = Symbol('kUseStderr'); + +// This constructor is not used to construct the global console. +// It's exported for backwards compatibility. function Console(options /* or: stdout, stderr, ignoreErrors = true */) { - if (!(this instanceof Console)) { + // We have to test new.target here to see if this function is called + // with new, because we need to define a custom instanceof to accommodate + // the global console. + if (!new.target) { return new Console(...arguments); } @@ -93,51 +104,104 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) { throw new ERR_CONSOLE_WRITABLE_STREAM('stderr'); } - const prop = { - writable: true, - enumerable: false, - configurable: true - }; - Object.defineProperty(this, '_stdout', { ...prop, value: stdout }); - Object.defineProperty(this, '_stderr', { ...prop, value: stderr }); - Object.defineProperty(this, '_ignoreErrors', { - ...prop, - value: Boolean(ignoreErrors), - }); - Object.defineProperty(this, '_times', { ...prop, value: new Map() }); - Object.defineProperty(this, '_stdoutErrorHandler', { - ...prop, - value: createWriteErrorHandler(stdout), - }); - Object.defineProperty(this, '_stderrErrorHandler', { - ...prop, - value: createWriteErrorHandler(stderr), - }); - if (typeof colorMode !== 'boolean' && colorMode !== 'auto') throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode); - // Corresponds to https://console.spec.whatwg.org/#count-map - this[kCounts] = new Map(); - this[kColorMode] = colorMode; - - Object.defineProperty(this, kGroupIndent, { writable: true }); - this[kGroupIndent] = ''; - // Bind the prototype functions to this Console instance var keys = Object.keys(Console.prototype); for (var v = 0; v < keys.length; v++) { var k = keys[v]; + // We have to bind the methods grabbed from the instance instead of from + // the prototype so that users extending the Console can override them + // from the prototype chain of the subclass. this[k] = this[k].bind(this); } + + this[kBindStreamsEager](stdout, stderr); + this[kBindProperties](ignoreErrors, colorMode); } +const consolePropAttributes = { + writable: true, + enumerable: false, + configurable: true +}; + +// Fixup global.console instanceof global.console.Console +Object.defineProperty(Console, Symbol.hasInstance, { + value(instance) { + return instance[kIsConsole]; + } +}); + +// Eager version for the Console constructor +Console.prototype[kBindStreamsEager] = function(stdout, stderr) { + Object.defineProperties(this, { + '_stdout': { ...consolePropAttributes, value: stdout }, + '_stderr': { ...consolePropAttributes, value: stderr } + }); +}; + +// Lazily load the stdout and stderr from an object so we don't +// create the stdio streams when they are not even accessed +Console.prototype[kBindStreamsLazy] = function(object) { + let stdout; + let stderr; + Object.defineProperties(this, { + '_stdout': { + enumerable: false, + configurable: true, + get() { + if (!stdout) stdout = object.stdout; + return stdout; + }, + set(value) { stdout = value; } + }, + '_stderr': { + enumerable: false, + configurable: true, + get() { + if (!stderr) { stderr = object.stderr; } + return stderr; + }, + set(value) { stderr = value; } + } + }); +}; + +Console.prototype[kBindProperties] = function(ignoreErrors, colorMode) { + Object.defineProperties(this, { + '_stdoutErrorHandler': { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStdout) + }, + '_stderrErrorHandler': { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStderr) + }, + '_ignoreErrors': { + ...consolePropAttributes, + value: Boolean(ignoreErrors) + }, + '_times': { ...consolePropAttributes, value: new Map() } + }); + + // TODO(joyeecheung): use consolePropAttributes for these + // Corresponds to https://console.spec.whatwg.org/#count-map + this[kCounts] = new Map(); + this[kColorMode] = colorMode; + this[kIsConsole] = true; + this[kGroupIndent] = ''; +}; + // Make a function that can serve as the callback passed to `stream.write()`. -function createWriteErrorHandler(stream) { +function createWriteErrorHandler(instance, streamSymbol) { return (err) => { // This conditional evaluates to true if and only if there was an error // that was not already emitted (which happens when the _write callback // is invoked asynchronously). + const stream = streamSymbol === kUseStdout ? + instance._stdout : instance._stderr; if (err !== null && !stream._writableState.errorEmitted) { // If there was an error, it will be emitted on `stream` as // an `error` event. Adding a `once` listener will keep that error @@ -151,7 +215,15 @@ function createWriteErrorHandler(stream) { }; } -function write(ignoreErrors, stream, string, errorhandler, groupIndent) { +Console.prototype[kWriteToConsole] = function(streamSymbol, string) { + const ignoreErrors = this._ignoreErrors; + const groupIndent = this[kGroupIndent]; + + const useStdout = streamSymbol === kUseStdout; + const stream = useStdout ? this._stdout : this._stderr; + const errorHandler = useStdout ? + this._stdoutErrorHandler : this._stderrErrorHandler; + if (groupIndent.length !== 0) { if (string.indexOf('\n') !== -1) { string = string.replace(/\n/g, `\n${groupIndent}`); @@ -169,7 +241,7 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) { // Add and later remove a noop error handler to catch synchronous errors. stream.once('error', noop); - stream.write(string, errorhandler); + stream.write(string, errorHandler); } catch (e) { // Console is a debugging utility, so it swallowing errors is not desirable // even in edge cases such as low stack space. @@ -179,7 +251,7 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) { } finally { stream.removeListener('error', noop); } -} +}; const kColorInspectOptions = { colors: true }; const kNoColorInspectOptions = {}; @@ -205,23 +277,17 @@ Console.prototype[kFormatForStderr] = function(args) { }; Console.prototype.log = function log(...args) { - write(this._ignoreErrors, - this._stdout, - this[kFormatForStdout](args), - this._stdoutErrorHandler, - this[kGroupIndent]); + this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args)); }; + Console.prototype.debug = Console.prototype.log; Console.prototype.info = Console.prototype.log; Console.prototype.dirxml = Console.prototype.log; Console.prototype.warn = function warn(...args) { - write(this._ignoreErrors, - this._stderr, - this[kFormatForStderr](args), - this._stderrErrorHandler, - this[kGroupIndent]); + this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); }; + Console.prototype.error = Console.prototype.warn; Console.prototype.dir = function dir(object, options) { @@ -230,11 +296,7 @@ Console.prototype.dir = function dir(object, options) { ...this[kGetInspectOptions](this._stdout), ...options }; - write(this._ignoreErrors, - this._stdout, - util.inspect(object, options), - this._stdoutErrorHandler, - this[kGroupIndent]); + this[kWriteToConsole](kUseStdout, util.inspect(object, options)); }; Console.prototype.time = function time(label = 'default') { @@ -294,7 +356,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(this[kFormatForStderr](args)); + this.warn(...args); // the arguments will be formatted in warn() again } }; @@ -356,7 +418,6 @@ const valuesKey = 'Values'; const indexKey = '(index)'; const iterKey = '(iteration index)'; - const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v); // https://console.spec.whatwg.org/#table @@ -470,10 +531,44 @@ Console.prototype.table = function(tabularData, properties) { return final(keys, values); }; -module.exports = new Console({ - stdout: process.stdout, - stderr: process.stderr -}); -module.exports.Console = Console; - function noop() {} + +// See https://console.spec.whatwg.org/#console-namespace +// > For historical web-compatibility reasons, the namespace object +// > for console must have as its [[Prototype]] an empty object, +// > created as if by ObjectCreate(%ObjectPrototype%), +// > instead of %ObjectPrototype%. + +// Since in Node.js, the Console constructor has been exposed through +// require('console'), we need to keep the Console constructor but +// we cannot actually use `new Console` to construct the global console. +// Therefore, the console.Console.prototype is not +// in the global console prototype chain anymore. + +// TODO(joyeecheung): +// - Move the Console constructor into internal/console.js +// - Move the global console creation code along with the inspector console +// wrapping code in internal/bootstrap/node.js into a separate file. +// - Make this file a simple re-export of those two files. +// This is only here for v11.x conflict resolution. +const globalConsole = Object.create(Console.prototype); + +// Since Console is not on the prototype chain of the global console, +// the symbol properties on Console.prototype have to be looked up from +// the global console itself. In addition, we need to make the global +// console a namespace by binding the console methods directly onto +// the global console with the receiver fixed. +for (const prop of Reflect.ownKeys(Console.prototype)) { + if (prop === 'constructor') { continue; } + const desc = Reflect.getOwnPropertyDescriptor(Console.prototype, prop); + if (typeof desc.value === 'function') { // fix the receiver + desc.value = desc.value.bind(globalConsole); + } + Reflect.defineProperty(globalConsole, prop, desc); +} + +globalConsole[kBindStreamsLazy](process); +globalConsole[kBindProperties](true, 'auto'); + +module.exports = globalConsole; +module.exports.Console = Console; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 70011637e08af4..272dec40060624 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -1,14 +1,16 @@ -/* eslint-disable node-core/required-modules */ - +// Flags: --expose-internals 'use strict'; -// Ordinarily test files must require('common') but that action causes -// the global console to be compiled, defeating the purpose of this test. -// This makes sure no additional files are added without carefully considering -// lazy loading. Please adjust the value if necessary. - +// This list must be computed before we require any modules to +// to eliminate the noise. const list = process.moduleLoadList.slice(); +const common = require('../common'); const assert = require('assert'); -assert(list.length <= 78, list); +const isMainThread = common.isMainThread; +const kMaxModuleCount = isMainThread ? 56 : 78; + +assert(list.length <= kMaxModuleCount, + `Total length: ${list.length}\n` + list.join('\n') +); diff --git a/test/parallel/test-console-instance.js b/test/parallel/test-console-instance.js index 91d130f260184b..127e59e6e5b981 100644 --- a/test/parallel/test-console-instance.js +++ b/test/parallel/test-console-instance.js @@ -23,7 +23,8 @@ const common = require('../common'); const assert = require('assert'); const Stream = require('stream'); -const Console = require('console').Console; +const requiredConsole = require('console'); +const Console = requiredConsole.Console; const out = new Stream(); const err = new Stream(); @@ -35,6 +36,11 @@ process.stdout.write = process.stderr.write = common.mustNotCall(); // Make sure that the "Console" function exists. assert.strictEqual(typeof Console, 'function'); +assert.strictEqual(requiredConsole, global.console); +// Make sure the custom instanceof of Console works +assert.ok(global.console instanceof Console); +assert.ok(!({} instanceof Console)); + // Make sure that the Console constructor throws // when not given a writable stream instance. common.expectsError( @@ -62,46 +68,63 @@ common.expectsError( out.write = err.write = (d) => {}; -const c = new Console(out, err); +{ + const c = new Console(out, err); + assert.ok(c instanceof Console); + + out.write = err.write = common.mustCall((d) => { + assert.strictEqual(d, 'test\n'); + }, 2); -out.write = err.write = common.mustCall((d) => { - assert.strictEqual(d, 'test\n'); -}, 2); + c.log('test'); + c.error('test'); -c.log('test'); -c.error('test'); + out.write = common.mustCall((d) => { + assert.strictEqual(d, '{ foo: 1 }\n'); + }); -out.write = common.mustCall((d) => { - assert.strictEqual(d, '{ foo: 1 }\n'); -}); + c.dir({ foo: 1 }); -c.dir({ foo: 1 }); + // Ensure that the console functions are bound to the console instance. + let called = 0; + out.write = common.mustCall((d) => { + called++; + assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`); + }, 3); -// Ensure that the console functions are bound to the console instance. -let called = 0; -out.write = common.mustCall((d) => { - called++; - assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`); -}, 3); + [1, 2, 3].forEach(c.log); +} -[1, 2, 3].forEach(c.log); +// Test calling Console without the `new` keyword. +{ + const withoutNew = Console(out, err); + assert.ok(withoutNew instanceof Console); +} -// Console() detects if it is called without `new` keyword. -Console(out, err); +// Test extending Console +{ + class MyConsole extends Console { + hello() {} + // See if the methods on Console.prototype are overridable. + log() { return 'overridden'; } + } + const myConsole = new MyConsole(process.stdout); + assert.strictEqual(typeof myConsole.hello, 'function'); + assert.ok(myConsole instanceof Console); + assert.strictEqual(myConsole.log(), 'overridden'); -// Extending Console works. -class MyConsole extends Console { - hello() {} + const log = myConsole.log; + assert.strictEqual(log(), 'overridden'); } -const myConsole = new MyConsole(process.stdout); -assert.strictEqual(typeof myConsole.hello, 'function'); // Instance that does not ignore the stream errors. -const c2 = new Console(out, err, false); +{ + const c2 = new Console(out, err, false); -out.write = () => { throw new Error('out'); }; -err.write = () => { throw new Error('err'); }; + out.write = () => { throw new Error('out'); }; + err.write = () => { throw new Error('err'); }; -assert.throws(() => c2.log('foo'), /^Error: out$/); -assert.throws(() => c2.warn('foo'), /^Error: err$/); -assert.throws(() => c2.dir('foo'), /^Error: out$/); + assert.throws(() => c2.log('foo'), /^Error: out$/); + assert.throws(() => c2.warn('foo'), /^Error: err$/); + assert.throws(() => c2.dir('foo'), /^Error: out$/); +} diff --git a/test/pseudo-tty/test-stderr-stdout-handle-sigwinch.out b/test/pseudo-tty/test-stderr-stdout-handle-sigwinch.out index dffbe030404487..4023b51f479747 100644 --- a/test/pseudo-tty/test-stderr-stdout-handle-sigwinch.out +++ b/test/pseudo-tty/test-stderr-stdout-handle-sigwinch.out @@ -1,2 +1,2 @@ -calling stdout._refreshSize calling stderr._refreshSize +calling stdout._refreshSize