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

From the WHATWG console spec:

> 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.

This patch changes the prototype chain of the global console object,
so the console.Console.prototype is not in the global console prototype
chain anymore.

```
const proto = Object.getPrototypeOf(global.console);
// Before this patch
proto.constructor === global.console.Console
// After this patch
proto.constructor === Object
```

But, we still maintain that

```
global.console instanceof global.console.Console
```

through a custom Symbol.hasInstance function of Console that tests
for a special symbol kIsConsole for backwards compatibility.

This fixes a case in the console Web Platform Test that we commented
out.

PR-URL: https://github.com/nodejs/node/pull/23509
Refs: https://github.com/whatwg/console/issues/3
Refs: https://console.spec.whatwg.org/#console-namespace
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
This commit is contained in:
Joyee Cheung 2018-10-12 08:38:40 -07:00 committed by Daniel Bevenius
parent 817e2e8a76
commit 6223236151
3 changed files with 101 additions and 42 deletions

View File

@ -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,
@ -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);
}
}
@ -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];
}
// 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;

View File

@ -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('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(
@ -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);
// Console() detects if it is called without `new` keyword.
Console(out, err);
// Extending Console works.
class MyConsole extends Console {
hello() {}
[1, 2, 3].forEach(c.log);
}
// Test calling Console without the `new` keyword.
{
const withoutNew = Console(out, err);
assert.ok(withoutNew instanceof Console);
}
// 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$/);
}

View File

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