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

lib: make the global console [[Prototype]] an empty object #23509

Closed
wants to merge 1 commit into from
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions lib/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,21 @@ 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');

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

this[kIsConsole] = true;
if (!options || typeof options.write === 'function') {
options = {
stdout: options,
Expand Down Expand Up @@ -125,7 +129,7 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
var keys = Object.keys(Console.prototype);
for (var v = 0; v < keys.length; v++) {
var k = keys[v];
this[k] = this[k].bind(this);
this[k] = Console.prototype[k].bind(this);
}
}

Expand Down Expand Up @@ -465,10 +469,50 @@ Console.prototype.table = function(tabularData, properties) {
return final(keys, values);
};

module.exports = new 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.
const globalConsole = Object.create({});
const tempConsole = new Console({
stdout: process.stdout,
stderr: process.stderr
});
module.exports.Console = Console;

function noop() {}
// 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.
for (const prop of Object.getOwnPropertySymbols(Console.prototype)) {
globalConsole[prop] = Console.prototype[prop];
}
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

// Reflect.ownKeys() is used here for retrieving Symbols
for (const prop of Reflect.ownKeys(tempConsole)) {
const desc = { ...(Reflect.getOwnPropertyDescriptor(tempConsole, prop)) };
// Since Console would bind method calls onto the instance,
// make sure the methods are called on globalConsole instead of
// tempConsole.
if (typeof Console.prototype[prop] === 'function') {
desc.value = Console.prototype[prop].bind(globalConsole);
}
Reflect.defineProperty(globalConsole, prop, desc);
}

globalConsole.Console = Console;

Object.defineProperty(Console, Symbol.hasInstance, {
value(instance) {
return instance[kIsConsole];
}
});

module.exports = globalConsole;
79 changes: 48 additions & 31 deletions test/parallel/test-console-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -35,6 +36,11 @@ process.stdout.write = process.stderr.write = common.mustNotCall();
// Make sure that the "Console" function exists.
assert.strictEqual('function', typeof Console);

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(
Expand Down Expand Up @@ -62,46 +68,57 @@ 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);
}

// Console() detects if it is called without `new` keyword.
Console(out, err);
// Test calling Console without the `new` keyword.
{
const withoutNew = Console(out, err);
assert.ok(withoutNew instanceof Console);
}

// Extending Console works.
class MyConsole extends Console {
hello() {}
// Test extending Console
{
class MyConsole extends Console {
hello() {}
}
const myConsole = new MyConsole(process.stdout);
assert.strictEqual(typeof myConsole.hello, 'function');
assert.ok(myConsole instanceof Console);
}
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$/);
}
4 changes: 1 addition & 3 deletions test/parallel/test-whatwg-console-is-a-namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ test(() => {
const prototype1 = Object.getPrototypeOf(console);
const prototype2 = Object.getPrototypeOf(prototype1);

// This got commented out from the original test because in Node.js all
// functions are declared on the prototype.
// assert_equals(Object.getOwnPropertyNames(prototype1).length, 0, "The [[Prototype]] must have no properties");
assert_equals(Object.getOwnPropertyNames(prototype1).length, 0, "The [[Prototype]] must have no properties");
assert_equals(prototype2, Object.prototype, "The [[Prototype]]'s [[Prototype]] must be %ObjectPrototype%");
}, "The prototype chain must be correct");

Expand Down