Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v11.x] backport console related changes #25420

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
211 changes: 153 additions & 58 deletions lib/console.js
Expand Up @@ -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);
}

Expand All @@ -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
Expand All @@ -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}`);
Expand All @@ -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.
Expand All @@ -179,7 +251,7 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) {
} finally {
stream.removeListener('error', noop);
}
}
};

const kColorInspectOptions = { colors: true };
const kNoColorInspectOptions = {};
Expand All @@ -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) {
Expand All @@ -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') {
Expand Down Expand Up @@ -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
}
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
18 changes: 10 additions & 8 deletions 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')
);