Skip to content

Commit 73c3a3d

Browse files
joyeecheungBridgeAR
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 <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>
1 parent 453bd18 commit 73c3a3d

File tree

2 files changed

+98
-37
lines changed

2 files changed

+98
-37
lines changed

lib/console.js

+50-6
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,
@@ -128,7 +132,7 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
128132
var keys = Object.keys(Console.prototype);
129133
for (var v = 0; v < keys.length; v++) {
130134
var k = keys[v];
131-
this[k] = this[k].bind(this);
135+
this[k] = Console.prototype[k].bind(this);
132136
}
133137
}
134138

@@ -470,10 +474,50 @@ Console.prototype.table = function(tabularData, properties) {
470474
return final(keys, values);
471475
};
472476

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

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

test/parallel/test-console-instance.js

+48-31
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(typeof Console, 'function');
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+
}

0 commit comments

Comments
 (0)