Skip to content
Open
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
20 changes: 19 additions & 1 deletion lib/internal/console/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ const kUseStdout = Symbol('kUseStdout');
const kUseStderr = Symbol('kUseStderr');

const optionsMap = new SafeWeakMap();
const afterConsoleWriteCallbacks = new SafeWeakMap();

function setAfterConsoleWrite(console, callback) {
afterConsoleWriteCallbacks.set(console, callback);
}

function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
// 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
Expand Down Expand Up @@ -297,7 +303,15 @@ ObjectDefineProperties(Console.prototype, {
}
string += '\n';

if (ignoreErrors === false) return stream.write(string);
const afterWrite = afterConsoleWriteCallbacks.get(this);

if (ignoreErrors === false) {
const result = stream.write(string);
if (afterWrite !== undefined) {
afterWrite(stream);
}
return result;
}

// There may be an error occurring synchronously (e.g. for files or TTYs
// on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so
Expand All @@ -309,6 +323,9 @@ ObjectDefineProperties(Console.prototype, {
stream.once('error', noop);

stream.write(string, errorHandler);
if (afterWrite !== undefined) {
afterWrite(stream);
}
} catch (e) {
// Console is a debugging utility, so it swallowing errors is not
// desirable even in edge cases such as low stack space.
Expand Down Expand Up @@ -713,5 +730,6 @@ module.exports = {
Console,
kBindStreamsLazy,
kBindProperties,
setAfterConsoleWrite,
initializeGlobalConsole,
};
25 changes: 25 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ const {
commonPrefix,
} = require('internal/readline/utils');
const { Console } = require('console');
const globalConsole = require('internal/console/global');
const {
setAfterConsoleWrite,
} = require('internal/console/constructor');
const { shouldColorize } = require('internal/util/colors');
const { highlight } = require('internal/repl/highlight');
const CJSModule = require('internal/modules/cjs/loader').Module;
Expand Down Expand Up @@ -187,6 +191,23 @@ const kPendingClose = Symbol('pendingClose');
const kHasPendingWork = Symbol('hasPendingWork');
const kDrainOnClose = Symbol('drainOnClose');

function redrawPromptAfterConsoleWrite(stream) {
const repl = replContext.getStore()?.replServer;
if (repl === undefined ||
repl.closed ||
!repl.terminal ||
repl[kHasPendingWork]() ||
stream !== repl.output) {
return;
}

repl.displayPrompt(true);
}

function setupConsolePromptRedraw(console) {
setAfterConsoleWrite(console, redrawPromptAfterConsoleWrite);
}

fixReplRequire(module);

// This is the default "writer" value, if none is passed in the REPL options,
Expand Down Expand Up @@ -309,6 +330,9 @@ class REPLServer extends Interface {
this[kLastCommandErrored] = false;
this[kPendingClose] = false;
this[kDrainOnClose] = false;
if (this.useGlobal) {
setupConsolePromptRedraw(globalConsole);
}
// Readline calls close() for both explicit programmatic closure and input
// EOF. Only EOF should wait for queued evaluations to drain.
this.input.prependListener('end', () => {
Expand Down Expand Up @@ -922,6 +946,7 @@ class REPLServer extends Interface {
});
context.global = context;
const _console = new Console(this.output);
setupConsolePromptRedraw(_console);
ObjectDefineProperty(context, 'console', {
__proto__: null,
configurable: true,
Expand Down
85 changes: 85 additions & 0 deletions test/parallel/test-repl-async-console-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

// Flags: --expose-internals

// Regression test for https://github.com/nodejs/node/issues/43074.

const common = require('../common');
const assert = require('assert');
const globalConsole = require('internal/console/global');
const { startNewREPLServer } = require('../common/repl');

// Ignore terminal settings so readline uses its terminal redraw path.
process.env.TERM = '';

const prompt = '> ';
const refresh = (line, cursor) =>
`\x1b[1G\x1b[0J${prompt}${line}\x1b[${prompt.length + cursor + 1}G`;

async function test(useGlobal) {
const originalStdout = globalConsole._stdout;
const {
replServer,
output,
waitForIdle,
} = startNewREPLServer({
ignoreUndefined: true,
preview: false,
prompt,
terminal: true,
useGlobal,
});

if (useGlobal) {
globalConsole._stdout = output;
}

try {
output.accumulator = '';
replServer.emit('line', "console.log('sync')");
await waitForIdle();
assert.strictEqual(output.accumulator, `sync\n${refresh('', 0)}`);

const logged = Promise.withResolvers();
replServer.context.__resetAsyncConsoleOutput = () => {
output.accumulator = '';
};
replServer.context.__asyncConsoleLogged = logged.resolve;

output.accumulator = '';
replServer.emit(
'line',
'void setTimeout(() => { __resetAsyncConsoleOutput(); ' +
"console.log('async'); __asyncConsoleLogged(); }, 0)",
);

// These edits are buffered while the timer is being evaluated, then
// replayed before the event loop can invoke its callback.
replServer.write('good');
replServer.write('', { name: 'left' });
replServer.write('', { name: 'left' });

await logged.promise;

// Match REPL-managed asynchronous errors: write the output first, then
// redraw the editable line while preserving its contents and cursor.
assert.strictEqual(
output.accumulator,
`async\n${refresh('good', 2)}`,
);
assert.strictEqual(replServer.line, 'good');
assert.strictEqual(replServer.cursor, 2);
} finally {
delete replServer.context.__resetAsyncConsoleOutput;
delete replServer.context.__asyncConsoleLogged;
replServer.close();
if (useGlobal) {
globalConsole._stdout = originalStdout;
}
}
}

(async () => {
await test(false);
await test(true);
})().then(common.mustCall());
Loading