Skip to content

Commit 6223236

Browse files
joyeecheungdanbev
authored andcommitted
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: #23509 Refs: whatwg/console#3 Refs: https://console.spec.whatwg.org/#console-namespace Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]> Reviewed-By: Denys Otrishko <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Sakthipriyan Vairamani <[email protected]> Reviewed-By: John-David Dalton <[email protected]>
1 parent 817e2e8 commit 6223236

File tree

3 files changed

+99
-40
lines changed

3 files changed

+99
-40
lines changed

lib/console.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,21 @@ let cliTable;
6060

6161
// Track amount of indentation required via `console.group()`.
6262
const kGroupIndent = Symbol('kGroupIndent');
63-
6463
const kFormatForStderr = Symbol('kFormatForStderr');
6564
const kFormatForStdout = Symbol('kFormatForStdout');
6665
const kGetInspectOptions = Symbol('kGetInspectOptions');
6766
const kColorMode = Symbol('kColorMode');
67+
const kIsConsole = Symbol('kIsConsole');
6868

6969
function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
70-
if (!(this instanceof Console)) {
70+
// We have to test new.target here to see if this function is called
71+
// with new, because we need to define a custom instanceof to accommodate
72+
// the global console.
73+
if (!new.target) {
7174
return new Console(...arguments);
7275
}
7376

77+
this[kIsConsole] = true;
7478
if (!options || typeof options.write === 'function') {
7579
options = {
7680
stdout: options,
@@ -125,7 +129,7 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
125129
var keys = Object.keys(Console.prototype);
126130
for (var v = 0; v < keys.length; v++) {
127131
var k = keys[v];
128-
this[k] = this[k].bind(this);
132+
this[k] = Console.prototype[k].bind(this);
129133
}
130134
}
131135

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

468-
module.exports = new Console({
472+
function noop() {}
473+
474+
// See https://console.spec.whatwg.org/#console-namespace
475+
// > For historical web-compatibility reasons, the namespace object
476+
// > for console must have as its [[Prototype]] an empty object,
477+
// > created as if by ObjectCreate(%ObjectPrototype%),
478+
// > instead of %ObjectPrototype%.
479+
480+
// Since in Node.js, the Console constructor has been exposed through
481+
// require('console'), we need to keep the Console constructor but
482+
// we cannot actually use `new Console` to construct the global console.
483+
// Therefore, the console.Console.prototype is not
484+
// in the global console prototype chain anymore.
485+
const globalConsole = Object.create({});
486+
const tempConsole = new Console({
469487
stdout: process.stdout,
470488
stderr: process.stderr
471489
});
472-
module.exports.Console = Console;
473490

474-
function noop() {}
491+
// Since Console is not on the prototype chain of the global console,
492+
// the symbol properties on Console.prototype have to be looked up from
493+
// the global console itself.
494+
for (const prop of Object.getOwnPropertySymbols(Console.prototype)) {
495+
globalConsole[prop] = Console.prototype[prop];
496+
}
497+
498+
// Reflect.ownKeys() is used here for retrieving Symbols
499+
for (const prop of Reflect.ownKeys(tempConsole)) {
500+
const desc = { ...(Reflect.getOwnPropertyDescriptor(tempConsole, prop)) };
501+
// Since Console would bind method calls onto the instance,
502+
// make sure the methods are called on globalConsole instead of
503+
// tempConsole.
504+
if (typeof Console.prototype[prop] === 'function') {
505+
desc.value = Console.prototype[prop].bind(globalConsole);
506+
}
507+
Reflect.defineProperty(globalConsole, prop, desc);
508+
}
509+
510+
globalConsole.Console = Console;
511+
512+
Object.defineProperty(Console, Symbol.hasInstance, {
513+
value(instance) {
514+
return instance[kIsConsole];
515+
}
516+
});
517+
518+
module.exports = globalConsole;

test/parallel/test-console-instance.js

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
const common = require('../common');
2424
const assert = require('assert');
2525
const Stream = require('stream');
26-
const Console = require('console').Console;
26+
const requiredConsole = require('console');
27+
const Console = requiredConsole.Console;
2728

2829
const out = new Stream();
2930
const err = new Stream();
@@ -35,6 +36,11 @@ process.stdout.write = process.stderr.write = common.mustNotCall();
3536
// Make sure that the "Console" function exists.
3637
assert.strictEqual('function', typeof Console);
3738

39+
assert.strictEqual(requiredConsole, global.console);
40+
// Make sure the custom instanceof of Console works
41+
assert.ok(global.console instanceof Console);
42+
assert.ok(!({} instanceof Console));
43+
3844
// Make sure that the Console constructor throws
3945
// when not given a writable stream instance.
4046
common.expectsError(
@@ -62,46 +68,57 @@ common.expectsError(
6268

6369
out.write = err.write = (d) => {};
6470

65-
const c = new Console(out, err);
71+
{
72+
const c = new Console(out, err);
73+
assert.ok(c instanceof Console);
6674

67-
out.write = err.write = common.mustCall((d) => {
68-
assert.strictEqual(d, 'test\n');
69-
}, 2);
75+
out.write = err.write = common.mustCall((d) => {
76+
assert.strictEqual(d, 'test\n');
77+
}, 2);
7078

71-
c.log('test');
72-
c.error('test');
79+
c.log('test');
80+
c.error('test');
7381

74-
out.write = common.mustCall((d) => {
75-
assert.strictEqual(d, '{ foo: 1 }\n');
76-
});
82+
out.write = common.mustCall((d) => {
83+
assert.strictEqual(d, '{ foo: 1 }\n');
84+
});
7785

78-
c.dir({ foo: 1 });
86+
c.dir({ foo: 1 });
7987

80-
// Ensure that the console functions are bound to the console instance.
81-
let called = 0;
82-
out.write = common.mustCall((d) => {
83-
called++;
84-
assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`);
85-
}, 3);
88+
// Ensure that the console functions are bound to the console instance.
89+
let called = 0;
90+
out.write = common.mustCall((d) => {
91+
called++;
92+
assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`);
93+
}, 3);
8694

87-
[1, 2, 3].forEach(c.log);
95+
[1, 2, 3].forEach(c.log);
96+
}
8897

89-
// Console() detects if it is called without `new` keyword.
90-
Console(out, err);
98+
// Test calling Console without the `new` keyword.
99+
{
100+
const withoutNew = Console(out, err);
101+
assert.ok(withoutNew instanceof Console);
102+
}
91103

92-
// Extending Console works.
93-
class MyConsole extends Console {
94-
hello() {}
104+
// Test extending Console
105+
{
106+
class MyConsole extends Console {
107+
hello() {}
108+
}
109+
const myConsole = new MyConsole(process.stdout);
110+
assert.strictEqual(typeof myConsole.hello, 'function');
111+
assert.ok(myConsole instanceof Console);
95112
}
96-
const myConsole = new MyConsole(process.stdout);
97-
assert.strictEqual(typeof myConsole.hello, 'function');
98113

99114
// Instance that does not ignore the stream errors.
100-
const c2 = new Console(out, err, false);
115+
{
116+
const c2 = new Console(out, err, false);
101117

102-
out.write = () => { throw new Error('out'); };
103-
err.write = () => { throw new Error('err'); };
118+
out.write = () => { throw new Error('out'); };
119+
err.write = () => { throw new Error('err'); };
104120

105-
assert.throws(() => c2.log('foo'), /^Error: out$/);
106-
assert.throws(() => c2.warn('foo'), /^Error: err$/);
107-
assert.throws(() => c2.dir('foo'), /^Error: out$/);
121+
assert.throws(() => c2.log('foo'), /^Error: out$/);
122+
assert.throws(() => c2.warn('foo'), /^Error: err$/);
123+
assert.throws(() => c2.dir('foo'), /^Error: out$/);
124+
}

test/parallel/test-whatwg-console-is-a-namespace.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ test(() => {
3838
const prototype1 = Object.getPrototypeOf(console);
3939
const prototype2 = Object.getPrototypeOf(prototype1);
4040

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

0 commit comments

Comments
 (0)