Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/readline.md467
-rw-r--r--lib/internal/readline/promises.js131
-rw-r--r--lib/readline.js4
-rw-r--r--lib/readline/promises.js51
-rw-r--r--test/parallel/test-readline-promises-csi.mjs163
-rw-r--r--test/parallel/test-readline-promises-interface.js1076
-rw-r--r--test/parallel/test-readline-promises-tab-complete.js116
-rw-r--r--tools/doc/type-parser.mjs7
8 files changed, 1963 insertions, 52 deletions
diff --git a/doc/api/readline.md b/doc/api/readline.md
index 8f056c942e3..b886c5b2bb6 100644
--- a/doc/api/readline.md
+++ b/doc/api/readline.md
@@ -7,22 +7,48 @@
<!-- source_link=lib/readline.js -->
The `readline` module provides an interface for reading data from a [Readable][]
-stream (such as [`process.stdin`][]) one line at a time. It can be accessed
-using:
+stream (such as [`process.stdin`][]) one line at a time.
-```js
+To use the promise-based APIs:
+
+```mjs
+import * as readline from 'node:readline/promises';
+```
+
+```cjs
+const readline = require('readline/promises');
+```
+
+To use the callback and sync APIs:
+
+```mjs
+import * as readline from 'node:readline';
+```
+
+```cjs
const readline = require('readline');
```
The following simple example illustrates the basic use of the `readline` module.
-```js
+```mjs
+import * as readline from 'node:readline/promises';
+import { stdin as input, stdout as output } from 'process';
+
+const rl = readline.createInterface({ input, output });
+
+const answer = await rl.question('What do you think of Node.js? ');
+
+console.log(`Thank you for your valuable feedback: ${answer}`);
+
+rl.close();
+```
+
+```cjs
const readline = require('readline');
+const { stdin: input, stdout: output } = require('process');
-const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
-});
+const rl = readline.createInterface({ input, output });
rl.question('What do you think of Node.js? ', (answer) => {
// TODO: Log the answer in a database
@@ -36,16 +62,18 @@ Once this code is invoked, the Node.js application will not terminate until the
`readline.Interface` is closed because the interface waits for data to be
received on the `input` stream.
-## Class: `Interface`
+<a id='readline_class_interface'></a>
+## Class: `InterfaceConstructor`
<!-- YAML
added: v0.1.104
-->
* Extends: {EventEmitter}
-Instances of the `readline.Interface` class are constructed using the
-`readline.createInterface()` method. Every instance is associated with a
-single `input` [Readable][] stream and a single `output` [Writable][] stream.
+Instances of the `InterfaceConstructor` class are constructed using the
+`readlinePromises.createInterface()` or `readline.createInterface()` method.
+Every instance is associated with a single `input` [Readable][] stream and a
+single `output` [Writable][] stream.
The `output` stream is used to print prompts for user input that arrives on,
and is read from, the `input` stream.
@@ -56,18 +84,18 @@ added: v0.1.98
The `'close'` event is emitted when one of the following occur:
-* The `rl.close()` method is called and the `readline.Interface` instance has
+* The `rl.close()` method is called and the `InterfaceConstructor` instance has
relinquished control over the `input` and `output` streams;
* The `input` stream receives its `'end'` event;
* The `input` stream receives <kbd>Ctrl</kbd>+<kbd>D</kbd> to signal
end-of-transmission (EOT);
* The `input` stream receives <kbd>Ctrl</kbd>+<kbd>C</kbd> to signal `SIGINT`
and there is no `'SIGINT'` event listener registered on the
- `readline.Interface` instance.
+ `InterfaceConstructor` instance.
The listener function is called without passing any arguments.
-The `readline.Interface` instance is finished once the `'close'` event is
+The `InterfaceConstructor` instance is finished once the `'close'` event is
emitted.
### Event: `'line'`
@@ -220,12 +248,12 @@ The `'SIGTSTP'` event is _not_ supported on Windows.
added: v0.1.98
-->
-The `rl.close()` method closes the `readline.Interface` instance and
+The `rl.close()` method closes the `InterfaceConstructor` instance and
relinquishes control over the `input` and `output` streams. When called,
the `'close'` event will be emitted.
Calling `rl.close()` does not immediately stop other events (including `'line'`)
-from being emitted by the `readline.Interface` instance.
+from being emitted by the `InterfaceConstructor` instance.
### `rl.pause()`
<!-- YAML
@@ -236,7 +264,7 @@ The `rl.pause()` method pauses the `input` stream, allowing it to be resumed
later if necessary.
Calling `rl.pause()` does not immediately pause other events (including
-`'line'`) from being emitted by the `readline.Interface` instance.
+`'line'`) from being emitted by the `InterfaceConstructor` instance.
### `rl.prompt([preserveCursor])`
<!-- YAML
@@ -246,14 +274,14 @@ added: v0.1.98
* `preserveCursor` {boolean} If `true`, prevents the cursor placement from
being reset to `0`.
-The `rl.prompt()` method writes the `readline.Interface` instances configured
+The `rl.prompt()` method writes the `InterfaceConstructor` instances configured
`prompt` to a new line in `output` in order to provide a user with a new
location at which to provide input.
When called, `rl.prompt()` will resume the `input` stream if it has been
paused.
-If the `readline.Interface` was created with `output` set to `null` or
+If the `InterfaceConstructor` was created with `output` set to `null` or
`undefined` the prompt is not written.
### `rl.question(query[, options], callback)`
@@ -276,7 +304,7 @@ function passing the provided input as the first argument.
When called, `rl.question()` will resume the `input` stream if it has been
paused.
-If the `readline.Interface` was created with `output` set to `null` or
+If the `InterfaceConstructor` was created with `output` set to `null` or
`undefined` the `query` is not written.
The `callback` function passed to `rl.question()` does not follow the typical
@@ -377,7 +405,7 @@ If `key` is specified, `data` is ignored.
When called, `rl.write()` will resume the `input` stream if it has been
paused.
-If the `readline.Interface` was created with `output` set to `null` or
+If the `InterfaceConstructor` was created with `output` set to `null` or
`undefined` the `data` and `key` are not written.
```js
@@ -406,13 +434,13 @@ changes:
Create an `AsyncIterator` object that iterates through each line in the input
stream as a string. This method allows asynchronous iteration of
-`readline.Interface` objects through `for await...of` loops.
+`InterfaceConstructor` objects through `for await...of` loops.
Errors in the input stream are not forwarded.
If the loop is terminated with `break`, `throw`, or `return`,
[`rl.close()`][] will be called. In other words, iterating over a
-`readline.Interface` will always consume the input stream fully.
+`InterfaceConstructor` will always consume the input stream fully.
Performance is not on par with the traditional `'line'` event API. Use `'line'`
instead for performance-sensitive applications.
@@ -502,7 +530,346 @@ Returns the real position of the cursor in relation to the input
prompt + string. Long input (wrapping) strings, as well as multiple
line prompts are included in the calculations.
-## `readline.clearLine(stream, dir[, callback])`
+## Promises API
+<!-- YAML
+added: REPLACEME
+-->
+
+### Class: `readlinePromises.Interface`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Extends: {readline.InterfaceConstructor}
+
+Instances of the `readlinePromises.Interface` class are constructed using the
+`readlinePromises.createInterface()` method. Every instance is associated with a
+single `input` [Readable][] stream and a single `output` [Writable][] stream.
+The `output` stream is used to print prompts for user input that arrives on,
+and is read from, the `input` stream.
+
+#### `rl.question(query[, options])`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `query` {string} A statement or query to write to `output`, prepended to the
+ prompt.
+* `options` {Object}
+ * `signal` {AbortSignal} Optionally allows the `question()` to be canceled
+ using an `AbortController`.
+* Returns: {Promise} A promise that is fulfilled with the user's
+ input in response to the `query`.
+
+The `rl.question()` method displays the `query` by writing it to the `output`,
+waits for user input to be provided on `input`, then invokes the `callback`
+function passing the provided input as the first argument.
+
+When called, `rl.question()` will resume the `input` stream if it has been
+paused.
+
+If the `readlinePromises.Interface` was created with `output` set to `null` or
+`undefined` the `query` is not written.
+
+Example usage:
+
+```mjs
+const answer = await rl.question('What is your favorite food? ');
+console.log(`Oh, so your favorite food is ${answer}`);
+```
+
+Using an `AbortController` to cancel a question.
+
+```mjs
+const ac = new AbortController();
+const signal = ac.signal;
+
+const answer = await rl.question('What is your favorite food? ', { signal });
+console.log(`Oh, so your favorite food is ${answer}`);
+
+signal.addEventListener('abort', () => {
+ console.log('The food question timed out');
+}, { once: true });
+
+setTimeout(() => ac.abort(), 10000);
+```
+
+### Class: `readlinePromises.Readline`
+<!-- YAML
+added: REPLACEME
+-->
+
+#### `new readlinePromises.Readline(stream)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `stream` {stream.Writable} A [TTY][] stream.
+
+#### `rl.clearLine(dir)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `dir` {integer}
+ * `-1`: to the left from cursor
+ * `1`: to the right from cursor
+ * `0`: the entire line
+* Returns: this
+
+The `rl.clearLine()` method adds to the internal list of pending action an
+action that clears current line of the associated `stream` in a specified
+direction identified by `dir`.
+You need to call `rl.commit()` to see the effect of this method.
+
+#### `rl.clearScreenDown()`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: this
+
+The `rl.clearScreenDown()` method adds to the internal list of pending action an
+action that clears the associated stream from the current position of the
+cursor down.
+You need to call `rl.commit()` to see the effect of this method.
+
+#### `rl.commit()`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {Promise}
+
+The `rl.commit()` method sends all the pending actions to the associated
+`stream` and clears the internal list of pending actions.
+
+#### `rl.cursorTo(x[, y])`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `x` {integer}
+* `y` {integer}
+* Returns: this
+
+The `rl.cursorTo()` method adds to the internal list of pending action an action
+that moves cursor to the specified position in the associated `stream`.
+You need to call `rl.commit()` to see the effect of this method.
+
+#### `rl.moveCursor(dx, dy)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `dx` {integer}
+* `dy` {integer}
+* Returns: this
+
+The `rl.moveCursor()` method adds to the internal list of pending action an
+action that moves the cursor *relative* to its current position in the
+associated `stream`.
+You need to call `rl.commit()` to see the effect of this method.
+
+#### `rl.rollback()`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: this
+
+The `rl.rollback` methods clears the internal list of pending actions without
+sending it to the associated `stream`.
+
+### `readlinePromises.createInterface(options)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `options` {Object}
+ * `input` {stream.Readable} The [Readable][] stream to listen to. This option
+ is *required*.
+ * `output` {stream.Writable} The [Writable][] stream to write readline data
+ to.
+ * `completer` {Function} An optional function used for Tab autocompletion.
+ * `terminal` {boolean} `true` if the `input` and `output` streams should be
+ treated like a TTY, and have ANSI/VT100 escape codes written to it.
+ **Default:** checking `isTTY` on the `output` stream upon instantiation.
+ * `history` {string[]} Initial list of history lines. This option makes sense
+ only if `terminal` is set to `true` by the user or by an internal `output`
+ check, otherwise the history caching mechanism is not initialized at all.
+ **Default:** `[]`.
+ * `historySize` {number} Maximum number of history lines retained. To disable
+ the history set this value to `0`. This option makes sense only if
+ `terminal` is set to `true` by the user or by an internal `output` check,
+ otherwise the history caching mechanism is not initialized at all.
+ **Default:** `30`.
+ * `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
+ to the history list duplicates an older one, this removes the older line
+ from the list. **Default:** `false`.
+ * `prompt` {string} The prompt string to use. **Default:** `'> '`.
+ * `crlfDelay` {number} If the delay between `\r` and `\n` exceeds
+ `crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
+ end-of-line input. `crlfDelay` will be coerced to a number no less than
+ `100`. It can be set to `Infinity`, in which case `\r` followed by `\n`
+ will always be considered a single newline (which may be reasonable for
+ [reading files][] with `\r\n` line delimiter). **Default:** `100`.
+ * `escapeCodeTimeout` {number} The duration `readlinePromises` will wait for a
+ character (when reading an ambiguous key sequence in milliseconds one that
+ can both form a complete key sequence using the input read so far and can
+ take additional input to complete a longer key sequence).
+ **Default:** `500`.
+ * `tabSize` {integer} The number of spaces a tab is equal to (minimum 1).
+ **Default:** `8`.
+* Returns: {readlinePromises.Interface}
+
+The `readlinePromises.createInterface()` method creates a new `readlinePromises.Interface`
+instance.
+
+```js
+const readlinePromises = require('readline/promises');
+const rl = readlinePromises.createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+```
+
+Once the `readlinePromises.Interface` instance is created, the most common case
+is to listen for the `'line'` event:
+
+```js
+rl.on('line', (line) => {
+ console.log(`Received: ${line}`);
+});
+```
+
+If `terminal` is `true` for this instance then the `output` stream will get
+the best compatibility if it defines an `output.columns` property and emits
+a `'resize'` event on the `output` if or when the columns ever change
+([`process.stdout`][] does this automatically when it is a TTY).
+
+#### Use of the `completer` function
+
+The `completer` function takes the current line entered by the user
+as an argument, and returns an `Array` with 2 entries:
+
+* An `Array` with matching entries for the completion.
+* The substring that was used for the matching.
+
+For instance: `[[substr1, substr2, ...], originalsubstring]`.
+
+```js
+function completer(line) {
+ const completions = '.help .error .exit .quit .q'.split(' ');
+ const hits = completions.filter((c) => c.startsWith(line));
+ // Show all completions if none found
+ return [hits.length ? hits : completions, line];
+}
+```
+
+The `completer` function can also returns a {Promise}, or be asynchronous:
+
+```js
+async function completer(linePartial) {
+ await someAsyncWork();
+ return [['123'], linePartial];
+}
+```
+
+## Callback API
+<!-- YAML
+added: v0.1.104
+-->
+
+### Class: `readline.Interface`
+<!-- YAML
+added: v0.1.104
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/37947
+ description: The class `readline.Interface` now inherits from `Interface`.
+-->
+
+* Extends: {readline.InterfaceConstructor}
+
+Instances of the `readline.Interface` class are constructed using the
+`readline.createInterface()` method. Every instance is associated with a
+single `input` [Readable][] stream and a single `output` [Writable][] stream.
+The `output` stream is used to print prompts for user input that arrives on,
+and is read from, the `input` stream.
+
+#### `rl.question(query[, options], callback)`
+<!-- YAML
+added: v0.3.3
+-->
+
+* `query` {string} A statement or query to write to `output`, prepended to the
+ prompt.
+* `options` {Object}
+ * `signal` {AbortSignal} Optionally allows the `question()` to be canceled
+ using an `AbortController`.
+* `callback` {Function} A callback function that is invoked with the user's
+ input in response to the `query`.
+
+The `rl.question()` method displays the `query` by writing it to the `output`,
+waits for user input to be provided on `input`, then invokes the `callback`
+function passing the provided input as the first argument.
+
+When called, `rl.question()` will resume the `input` stream if it has been
+paused.
+
+If the `readline.Interface` was created with `output` set to `null` or
+`undefined` the `query` is not written.
+
+The `callback` function passed to `rl.question()` does not follow the typical
+pattern of accepting an `Error` object or `null` as the first argument.
+The `callback` is called with the provided answer as the only argument.
+
+Example usage:
+
+```js
+rl.question('What is your favorite food? ', (answer) => {
+ console.log(`Oh, so your favorite food is ${answer}`);
+});
+```
+
+Using an `AbortController` to cancel a question.
+
+```js
+const ac = new AbortController();
+const signal = ac.signal;
+
+rl.question('What is your favorite food? ', { signal }, (answer) => {
+ console.log(`Oh, so your favorite food is ${answer}`);
+});
+
+signal.addEventListener('abort', () => {
+ console.log('The food question timed out');
+}, { once: true });
+
+setTimeout(() => ac.abort(), 10000);
+```
+
+If this method is invoked as it's util.promisify()ed version, it returns a
+Promise that fulfills with the answer. If the question is canceled using
+an `AbortController` it will reject with an `AbortError`.
+
+```js
+const util = require('util');
+const question = util.promisify(rl.question).bind(rl);
+
+async function questionExample() {
+ try {
+ const answer = await question('What is you favorite food? ');
+ console.log(`Oh, so your favorite food is ${answer}`);
+ } catch (err) {
+ console.error('Question rejected', err);
+ }
+}
+questionExample();
+```
+
+### `readline.clearLine(stream, dir[, callback])`
<!-- YAML
added: v0.7.7
changes:
@@ -524,7 +891,7 @@ changes:
The `readline.clearLine()` method clears current line of given [TTY][] stream
in a specified direction identified by `dir`.
-## `readline.clearScreenDown(stream[, callback])`
+### `readline.clearScreenDown(stream[, callback])`
<!-- YAML
added: v0.7.7
changes:
@@ -542,7 +909,7 @@ changes:
The `readline.clearScreenDown()` method clears the given [TTY][] stream from
the current position of the cursor down.
-## `readline.createInterface(options)`
+### `readline.createInterface(options)`
<!-- YAML
added: v0.1.98
changes:
@@ -646,7 +1013,7 @@ If you want your application to exit without waiting for user input, you can
process.stdin.unref();
```
-### Use of the `completer` function
+#### Use of the `completer` function
The `completer` function takes the current line entered by the user
as an argument, and returns an `Array` with 2 entries:
@@ -674,7 +1041,7 @@ function completer(linePartial, callback) {
}
```
-## `readline.cursorTo(stream, x[, y][, callback])`
+### `readline.cursorTo(stream, x[, y][, callback])`
<!-- YAML
added: v0.7.7
changes:
@@ -694,13 +1061,33 @@ changes:
The `readline.cursorTo()` method moves cursor to the specified position in a
given [TTY][] `stream`.
+### `readline.moveCursor(stream, dx, dy[, callback])`
+<!-- YAML
+added: v0.7.7
+changes:
+ - version: v12.7.0
+ pr-url: https://github.com/nodejs/node/pull/28674
+ description: The stream's write() callback and return value are exposed.
+-->
+
+* `stream` {stream.Writable}
+* `dx` {number}
+* `dy` {number}
+* `callback` {Function} Invoked once the operation completes.
+* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for
+ the `'drain'` event to be emitted before continuing to write additional data;
+ otherwise `true`.
+
+The `readline.moveCursor()` method moves the cursor *relative* to its current
+position in a given [TTY][] `stream`.
+
## `readline.emitKeypressEvents(stream[, interface])`
<!-- YAML
added: v0.7.7
-->
* `stream` {stream.Readable}
-* `interface` {readline.Interface}
+* `interface` {readline.InterfaceConstructor}
The `readline.emitKeypressEvents()` method causes the given [Readable][]
stream to begin emitting `'keypress'` events corresponding to received input.
@@ -720,26 +1107,6 @@ if (process.stdin.isTTY)
process.stdin.setRawMode(true);
```
-## `readline.moveCursor(stream, dx, dy[, callback])`
-<!-- YAML
-added: v0.7.7
-changes:
- - version: v12.7.0
- pr-url: https://github.com/nodejs/node/pull/28674
- description: The stream's write() callback and return value are exposed.
--->
-
-* `stream` {stream.Writable}
-* `dx` {number}
-* `dy` {number}
-* `callback` {Function} Invoked once the operation completes.
-* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for
- the `'drain'` event to be emitted before continuing to write additional data;
- otherwise `true`.
-
-The `readline.moveCursor()` method moves the cursor *relative* to its current
-position in a given [TTY][] `stream`.
-
## Example: Tiny CLI
The following example illustrates the use of `readline.Interface` class to
diff --git a/lib/internal/readline/promises.js b/lib/internal/readline/promises.js
new file mode 100644
index 00000000000..3e20085b281
--- /dev/null
+++ b/lib/internal/readline/promises.js
@@ -0,0 +1,131 @@
+'use strict';
+
+const {
+ ArrayPrototypeJoin,
+ ArrayPrototypePush,
+ Promise,
+} = primordials;
+
+const { CSI } = require('internal/readline/utils');
+const { validateInteger } = require('internal/validators');
+const { isWritable } = require('internal/streams/utils');
+const { codes: { ERR_INVALID_ARG_TYPE } } = require('internal/errors');
+
+const {
+ kClearToLineBeginning,
+ kClearToLineEnd,
+ kClearLine,
+ kClearScreenDown,
+} = CSI;
+
+class Readline {
+ #stream;
+ #todo = [];
+
+ constructor(stream) {
+ if (!isWritable(stream))
+ throw new ERR_INVALID_ARG_TYPE('stream', 'Writable', stream);
+ this.#stream = stream;
+ }
+
+ /**
+ * Moves the cursor to the x and y coordinate on the given stream.
+ * @param {integer} x
+ * @param {integer} [y]
+ * @returns {Readline} this
+ */
+ cursorTo(x, y = undefined) {
+ validateInteger(x, 'x');
+ if (y != null) validateInteger(y, 'y');
+
+ ArrayPrototypePush(
+ this.#todo,
+ y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`
+ );
+
+ return this;
+ }
+
+ /**
+ * Moves the cursor relative to its current location.
+ * @param {integer} dx
+ * @param {integer} dy
+ * @returns {Readline} this
+ */
+ moveCursor(dx, dy) {
+ if (dx || dy) {
+ validateInteger(dx, 'dx');
+ validateInteger(dy, 'dy');
+
+ let data = '';
+
+ if (dx < 0) {
+ data += CSI`${-dx}D`;
+ } else if (dx > 0) {
+ data += CSI`${dx}C`;
+ }
+
+ if (dy < 0) {
+ data += CSI`${-dy}A`;
+ } else if (dy > 0) {
+ data += CSI`${dy}B`;
+ }
+ ArrayPrototypePush(this.#todo, data);
+ }
+ return this;
+ }
+
+ /**
+ * Clears the current line the cursor is on.
+ * @param {-1|0|1} dir Direction to clear:
+ * -1 for left of the cursor
+ * +1 for right of the cursor
+ * 0 for the entire line
+ * @returns {Readline} this
+ */
+ clearLine(dir) {
+ validateInteger(dir, 'dir', -1, 1);
+
+ ArrayPrototypePush(
+ this.#todo,
+ dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine
+ );
+ return this;
+ }
+
+ /**
+ * Clears the screen from the current position of the cursor down.
+ * @returns {Readline} this
+ */
+ clearScreenDown() {
+ ArrayPrototypePush(this.#todo, kClearScreenDown);
+ return this;
+ }
+
+ /**
+ * Sends all the pending actions to the associated `stream` and clears the
+ * internal list of pending actions.
+ * @returns {Promise<void>} Resolves when all pending actions have been
+ * flushed to the associated `stream`.
+ */
+ commit() {
+ return new Promise((resolve) => {
+ this.#stream.write(ArrayPrototypeJoin(this.#todo, ''), resolve);
+ this.#todo = [];
+ });
+ }
+
+ /**
+ * Clears the internal list of pending actions without sending it to the
+ * associated `stream`.
+ * @returns {Readline} this
+ */
+ rollback() {
+ this.#todo = [];
+ return this;
+ }
+}
+
+module.exports = {
+ Readline,
+};
diff --git a/lib/readline.js b/lib/readline.js
index 0b17039ff17..1d9e839f2c2 100644
--- a/lib/readline.js
+++ b/lib/readline.js
@@ -39,6 +39,7 @@ const {
moveCursor,
} = require('internal/readline/callbacks');
const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
+const promises = require('readline/promises');
const {
AbortError,
@@ -462,5 +463,6 @@ module.exports = {
createInterface,
cursorTo,
emitKeypressEvents,
- moveCursor
+ moveCursor,
+ promises,
};
diff --git a/lib/readline/promises.js b/lib/readline/promises.js
new file mode 100644
index 00000000000..90658e5a5e9
--- /dev/null
+++ b/lib/readline/promises.js
@@ -0,0 +1,51 @@
+'use strict';
+
+const {
+ Promise,
+} = primordials;
+
+const {
+ Readline,
+} = require('internal/readline/promises');
+
+const {
+ Interface: _Interface,
+ kQuestionCancel,
+} = require('internal/readline/interface');
+
+const {
+ AbortError,
+} = require('internal/errors');
+
+class Interface extends _Interface {
+ // eslint-disable-next-line no-useless-constructor
+ constructor(input, output, completer, terminal) {
+ super(input, output, completer, terminal);
+ }
+ question(query, options = {}) {
+ return new Promise((resolve, reject) => {
+ if (options.signal) {
+ if (options.signal.aborted) {
+ return reject(new AbortError());
+ }
+
+ options.signal.addEventListener('abort', () => {
+ this[kQuestionCancel]();
+ reject(new AbortError());
+ }, { once: true });
+ }
+
+ super.question(query, resolve);
+ });
+ }
+}
+
+function createInterface(input, output, completer, terminal) {
+ return new Interface(input, output, completer, terminal);
+}
+
+module.exports = {
+ Interface,
+ Readline,
+ createInterface,
+};
diff --git a/test/parallel/test-readline-promises-csi.mjs b/test/parallel/test-readline-promises-csi.mjs
new file mode 100644
index 00000000000..1ba105fc198
--- /dev/null
+++ b/test/parallel/test-readline-promises-csi.mjs
@@ -0,0 +1,163 @@
+// Flags: --expose-internals
+
+
+import '../common/index.mjs';
+import assert from 'assert';
+import { Readline } from 'readline/promises';
+import { Writable } from 'stream';
+
+import utils from 'internal/readline/utils';
+const { CSI } = utils;
+
+const INVALID_ARG = {
+ name: 'TypeError',
+ code: 'ERR_INVALID_ARG_TYPE',
+};
+
+class TestWritable extends Writable {
+ data = '';
+ _write(chunk, encoding, callback) {
+ this.data += chunk.toString();
+ callback();
+ }
+}
+
+[
+ undefined, null,
+ 0, 1, 1n, 1.1, NaN, Infinity,
+ true, false,
+ Symbol(),
+ '', '1',
+ [], {}, () => {},
+].forEach((arg) =>
+ assert.throws(() => new Readline(arg), INVALID_ARG)
+);
+
+{
+ const writable = new TestWritable();
+ const readline = new Readline(writable);
+
+ await readline.clearScreenDown().commit();
+ assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
+ await readline.clearScreenDown().commit();
+
+ writable.data = '';
+ await readline.clearScreenDown().rollback();
+ assert.deepStrictEqual(writable.data, '');
+
+ writable.data = '';
+ await readline.clearLine(-1).commit();
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
+
+ writable.data = '';
+ await readline.clearLine(1).commit();
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);
+
+ writable.data = '';
+ await readline.clearLine(0).commit();
+ assert.deepStrictEqual(writable.data, CSI.kClearLine);
+
+ writable.data = '';
+ await readline.clearLine(-1).commit();
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
+
+ await readline.clearLine(0, null).commit();
+
+ // Nothing is written when moveCursor 0, 0
+ for (const set of
+ [
+ [0, 0, ''],
+ [1, 0, '\x1b[1C'],
+ [-1, 0, '\x1b[1D'],
+ [0, 1, '\x1b[1B'],
+ [0, -1, '\x1b[1A'],
+ [1, 1, '\x1b[1C\x1b[1B'],
+ [-1, 1, '\x1b[1D\x1b[1B'],
+ [-1, -1, '\x1b[1D\x1b[1A'],
+ [1, -1, '\x1b[1C\x1b[1A'],
+ ]) {
+ writable.data = '';
+ await readline.moveCursor(set[0], set[1]).commit();
+ assert.deepStrictEqual(writable.data, set[2]);
+ writable.data = '';
+ await readline.moveCursor(set[0], set[1]).commit();
+ assert.deepStrictEqual(writable.data, set[2]);
+ }
+
+
+ await readline.moveCursor(1, 1, null).commit();
+
+ writable.data = '';
+ [
+ undefined, null,
+ true, false,
+ Symbol(),
+ '', '1',
+ [], {}, () => {},
+ ].forEach((arg) =>
+ assert.throws(() => readline.cursorTo(arg), INVALID_ARG)
+ );
+ assert.strictEqual(writable.data, '');
+
+ writable.data = '';
+ assert.throws(() => readline.cursorTo('a', 'b'), INVALID_ARG);
+ assert.strictEqual(writable.data, '');
+
+ writable.data = '';
+ assert.throws(() => readline.cursorTo('a', 1), INVALID_ARG);
+ assert.strictEqual(writable.data, '');
+
+ writable.data = '';
+ assert.throws(() => readline.cursorTo(1, 'a'), INVALID_ARG);
+ assert.strictEqual(writable.data, '');
+
+ writable.data = '';
+ await readline.cursorTo(1).commit();
+ assert.strictEqual(writable.data, '\x1b[2G');
+
+ writable.data = '';
+ await readline.cursorTo(1, 2).commit();
+ assert.strictEqual(writable.data, '\x1b[3;2H');
+
+ writable.data = '';
+ await readline.cursorTo(1, 2).commit();
+ assert.strictEqual(writable.data, '\x1b[3;2H');
+
+ writable.data = '';
+ await readline.cursorTo(1).cursorTo(1, 2).commit();
+ assert.strictEqual(writable.data, '\x1b[2G\x1b[3;2H');
+
+ writable.data = '';
+ await readline.cursorTo(1).commit();
+ assert.strictEqual(writable.data, '\x1b[2G');
+
+ // Verify that cursorTo() rejects if x or y is NaN.
+ [1.1, NaN, Infinity].forEach((arg) => {
+ assert.throws(() => readline.cursorTo(arg), {
+ code: 'ERR_OUT_OF_RANGE',
+ name: 'RangeError',
+ });
+ });
+
+ [1.1, NaN, Infinity].forEach((arg) => {
+ assert.throws(() => readline.cursorTo(1, arg), {
+ code: 'ERR_OUT_OF_RANGE',
+ name: 'RangeError',
+ });
+ });
+
+ assert.throws(() => readline.cursorTo(NaN, NaN), {
+ code: 'ERR_OUT_OF_RANGE',
+ name: 'RangeError',
+ });
+}
+
+{
+ const error = new Error();
+ const writable = new class extends Writable {
+ _write() { throw error; }
+ }();
+ const readline = new Readline(writable);
+
+ await assert.rejects(readline.cursorTo(1).commit(), error);
+}
diff --git a/test/parallel/test-readline-promises-interface.js b/test/parallel/test-readline-promises-interface.js
new file mode 100644
index 00000000000..79803f99c19
--- /dev/null
+++ b/test/parallel/test-readline-promises-interface.js
@@ -0,0 +1,1076 @@
+// Flags: --expose-internals
+'use strict';
+const common = require('../common');
+common.skipIfDumbTerminal();
+
+const assert = require('assert');
+const readline = require('readline/promises');
+const {
+ getStringWidth,
+ stripVTControlCharacters
+} = require('internal/util/inspect');
+const EventEmitter = require('events').EventEmitter;
+const { Writable, Readable } = require('stream');
+
+class FakeInput extends EventEmitter {
+ resume() {}
+ pause() {}
+ write() {}
+ end() {}
+}
+
+function isWarned(emitter) {
+ for (const name in emitter) {
+ const listeners = emitter[name];
+ if (listeners.warned) return true;
+ }
+ return false;
+}
+
+function getInterface(options) {
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ ...options,
+ });
+ return [rli, fi];
+}
+
+function assertCursorRowsAndCols(rli, rows, cols) {
+ const cursorPos = rli.getCursorPos();
+ assert.strictEqual(cursorPos.rows, rows);
+ assert.strictEqual(cursorPos.cols, cols);
+}
+
+[
+ undefined,
+ 50,
+ 0,
+ 100.5,
+ 5000,
+].forEach((crlfDelay) => {
+ const [rli] = getInterface({ crlfDelay });
+ assert.strictEqual(rli.crlfDelay, Math.max(crlfDelay || 100, 100));
+ rli.close();
+});
+
+{
+ const input = new FakeInput();
+
+ // Constructor throws if completer is not a function or undefined
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ completer: 'string is not valid'
+ });
+ }, {
+ name: 'TypeError',
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ completer: ''
+ });
+ }, {
+ name: 'TypeError',
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ completer: false
+ });
+ }, {
+ name: 'TypeError',
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+
+ // Constructor throws if history is not an array
+ ['not an array', 123, 123n, {}, true, Symbol(), null].forEach((history) => {
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ history,
+ });
+ }, {
+ name: 'TypeError',
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ });
+
+ // Constructor throws if historySize is not a positive number
+ ['not a number', -1, NaN, {}, true, Symbol(), null].forEach((historySize) => {
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ historySize,
+ });
+ }, {
+ name: 'RangeError',
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+ });
+
+ // Check for invalid tab sizes.
+ assert.throws(
+ () => new readline.Interface({
+ input,
+ tabSize: 0
+ }),
+ {
+ message: 'The value of "tabSize" is out of range. ' +
+ 'It must be >= 1 && < 4294967296. Received 0',
+ code: 'ERR_OUT_OF_RANGE'
+ }
+ );
+
+ assert.throws(
+ () => new readline.Interface({
+ input,
+ tabSize: '4'
+ }),
+ { code: 'ERR_INVALID_ARG_TYPE' }
+ );
+
+ assert.throws(
+ () => new readline.Interface({
+ input,
+ tabSize: 4.5
+ }),
+ {
+ code: 'ERR_OUT_OF_RANGE',
+ message: 'The value of "tabSize" is out of range. ' +
+ 'It must be an integer. Received 4.5'
+ }
+ );
+}
+
+// Sending a single character with no newline
+{
+ const fi = new FakeInput();
+ const rli = new readline.Interface(fi, {});
+ rli.on('line', common.mustNotCall());
+ fi.emit('data', 'a');
+ rli.close();
+}
+
+// Sending multiple newlines at once that does not end with a new line and a
+// `end` event(last line is). \r should behave like \n when alone.
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length - 1));
+ fi.emit('data', expectedLines.join('\r'));
+ rli.close();
+}
+
+// \r at start of input should output blank line
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ['', 'foo' ];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length));
+ fi.emit('data', '\rfoo\r');
+ rli.close();
+}
+
+// \t does not become part of the input when there is a completer function
+{
+ const completer = (line) => [[], line];
+ const [rli, fi] = getInterface({ terminal: true, completer });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'foo');
+ }));
+ for (const character of '\tfo\to\t') {
+ fi.emit('data', character);
+ }
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// \t when there is no completer function should behave like an ordinary
+// character
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '\t');
+ }));
+ fi.emit('data', '\t');
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// Adding history lines should emit the history event with
+// the history array
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ rli.on('history', common.mustCall((history) => {
+ const expectedHistory = expectedLines.slice(0, history.length).reverse();
+ assert.deepStrictEqual(history, expectedHistory);
+ }, expectedLines.length));
+ for (const line of expectedLines) {
+ fi.emit('data', `${line}\n`);
+ }
+ rli.close();
+}
+
+// Altering the history array in the listener should not alter
+// the line being processed
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLine = 'foo';
+ rli.on('history', common.mustCall((history) => {
+ assert.strictEqual(history[0], expectedLine);
+ history.shift();
+ }));
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLine);
+ assert.strictEqual(rli.history.length, 0);
+ }));
+ fi.emit('data', `${expectedLine}\n`);
+ rli.close();
+}
+
+// Duplicate lines are removed from history when
+// `options.removeHistoryDuplicates` is `true`
+{
+ const [rli, fi] = getInterface({
+ terminal: true,
+ removeHistoryDuplicates: true
+ });
+ const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
+ // ['foo', 'baz', 'bar', bat'];
+ let callCount = 0;
+ rli.on('line', function(line) {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit('data', `${expectedLines.join('\n')}\n`);
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'foo'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ fi.emit('keypress', '.', { name: 'down' }); // 'baz'
+ assert.strictEqual(rli.line, 'baz');
+ assert.strictEqual(rli.historyIndex, 2);
+ fi.emit('keypress', '.', { name: 'n', ctrl: true }); // 'bar'
+ assert.strictEqual(rli.line, 'bar');
+ assert.strictEqual(rli.historyIndex, 1);
+ fi.emit('keypress', '.', { name: 'n', ctrl: true });
+ assert.strictEqual(rli.line, 'bat');
+ assert.strictEqual(rli.historyIndex, 0);
+ // Activate the substring history search.
+ fi.emit('keypress', '.', { name: 'down' }); // 'bat'
+ assert.strictEqual(rli.line, 'bat');
+ assert.strictEqual(rli.historyIndex, -1);
+ // Deactivate substring history search.
+ fi.emit('keypress', '.', { name: 'backspace' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, 'ba');
+ // Activate the substring history search.
+ fi.emit('keypress', '.', { name: 'down' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, 'ba');
+ fi.emit('keypress', '.', { name: 'down' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, 'ba');
+ fi.emit('keypress', '.', { name: 'up' }); // 'bat'
+ assert.strictEqual(rli.historyIndex, 0);
+ assert.strictEqual(rli.line, 'bat');
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.strictEqual(rli.historyIndex, 1);
+ assert.strictEqual(rli.line, 'bar');
+ fi.emit('keypress', '.', { name: 'up' }); // 'baz'
+ assert.strictEqual(rli.historyIndex, 2);
+ assert.strictEqual(rli.line, 'baz');
+ fi.emit('keypress', '.', { name: 'up' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 4);
+ assert.strictEqual(rli.line, 'ba');
+ fi.emit('keypress', '.', { name: 'up' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 4);
+ assert.strictEqual(rli.line, 'ba');
+ // Deactivate substring history search and reset history index.
+ fi.emit('keypress', '.', { name: 'right' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, 'ba');
+ // Substring history search activated.
+ fi.emit('keypress', '.', { name: 'up' }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 0);
+ assert.strictEqual(rli.line, 'bat');
+ rli.close();
+}
+
+// Duplicate lines are not removed from history when
+// `options.removeHistoryDuplicates` is `false`
+{
+ const [rli, fi] = getInterface({
+ terminal: true,
+ removeHistoryDuplicates: false
+ });
+ const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
+ let callCount = 0;
+ rli.on('line', function(line) {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit('data', `${expectedLines.join('\n')}\n`);
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'foo'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ rli.close();
+}
+
+// Regression test for repl freeze, #1968:
+// check that nothing fails if 'keypress' event throws.
+{
+ const [rli, fi] = getInterface({ terminal: true });
+ const keys = [];
+ const err = new Error('bad thing happened');
+ fi.on('keypress', function(key) {
+ keys.push(key);
+ if (key === 'X') {
+ throw err;
+ }
+ });
+ assert.throws(
+ () => fi.emit('data', 'fooX'),
+ (e) => {
+ assert.strictEqual(e, err);
+ return true;
+ }
+ );
+ fi.emit('data', 'bar');
+ assert.strictEqual(keys.join(''), 'fooXbar');
+ rli.close();
+}
+
+// History is bound
+{
+ const [rli, fi] = getInterface({ terminal: true, historySize: 2 });
+ const lines = ['line 1', 'line 2', 'line 3'];
+ fi.emit('data', lines.join('\n') + '\n');
+ assert.strictEqual(rli.history.length, 2);
+ assert.strictEqual(rli.history[0], 'line 3');
+ assert.strictEqual(rli.history[1], 'line 2');
+}
+
+// Question
+{
+ const [rli] = getInterface({ terminal: true });
+ const expectedLines = ['foo'];
+ rli.question(expectedLines[0]).then(() => rli.close());
+ assertCursorRowsAndCols(rli, 0, expectedLines[0].length);
+ rli.close();
+}
+
+// Sending a multi-line question
+{
+ const [rli] = getInterface({ terminal: true });
+ const expectedLines = ['foo', 'bar'];
+ rli.question(expectedLines.join('\n')).then(() => rli.close());
+ assertCursorRowsAndCols(
+ rli, expectedLines.length - 1, expectedLines.slice(-1)[0].length);
+ rli.close();
+}
+
+{
+ // Beginning and end of line
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ fi.emit('keypress', '.', { ctrl: true, name: 'a' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ fi.emit('keypress', '.', { ctrl: true, name: 'e' });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+}
+
+{
+ // Back and Forward one character
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Back one character
+ fi.emit('keypress', '.', { ctrl: true, name: 'b' });
+ assertCursorRowsAndCols(rli, 0, 18);
+ // Back one character
+ fi.emit('keypress', '.', { ctrl: true, name: 'b' });
+ assertCursorRowsAndCols(rli, 0, 17);
+ // Forward one character
+ fi.emit('keypress', '.', { ctrl: true, name: 'f' });
+ assertCursorRowsAndCols(rli, 0, 18);
+ // Forward one character
+ fi.emit('keypress', '.', { ctrl: true, name: 'f' });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+}
+
+// Back and Forward one astral character
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', '💻');
+
+ // Move left one character/code point
+ fi.emit('keypress', '.', { name: 'left' });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Move right one character/code point
+ fi.emit('keypress', '.', { name: 'right' });
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '💻');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// Two astral characters left
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', '💻');
+
+ // Move left one character/code point
+ fi.emit('keypress', '.', { name: 'left' });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ fi.emit('data', '🐕');
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '🐕💻');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// Two astral characters right
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', '💻');
+
+ // Move left one character/code point
+ fi.emit('keypress', '.', { name: 'right' });
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ fi.emit('data', '🐕');
+ assertCursorRowsAndCols(rli, 0, 4);
+
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '💻🐕');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+{
+ // `wordLeft` and `wordRight`
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ fi.emit('keypress', '.', { ctrl: true, name: 'left' });
+ assertCursorRowsAndCols(rli, 0, 16);
+ fi.emit('keypress', '.', { meta: true, name: 'b' });
+ assertCursorRowsAndCols(rli, 0, 10);
+ fi.emit('keypress', '.', { ctrl: true, name: 'right' });
+ assertCursorRowsAndCols(rli, 0, 16);
+ fi.emit('keypress', '.', { meta: true, name: 'f' });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+}
+
+// `deleteWordLeft`
+[
+ { ctrl: true, name: 'w' },
+ { ctrl: true, name: 'backspace' },
+ { meta: true, name: 'backspace' },
+].forEach((deleteWordLeftKey) => {
+ let [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ fi.emit('keypress', '.', { ctrl: true, name: 'left' });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'the quick fox');
+ }));
+ fi.emit('keypress', '.', deleteWordLeftKey);
+ fi.emit('data', '\n');
+ rli.close();
+
+ // No effect if pressed at beginning of line
+ [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ fi.emit('keypress', '.', { ctrl: true, name: 'a' });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'the quick brown fox');
+ }));
+ fi.emit('keypress', '.', deleteWordLeftKey);
+ fi.emit('data', '\n');
+ rli.close();
+});
+
+// `deleteWordRight`
+[
+ { ctrl: true, name: 'delete' },
+ { meta: true, name: 'delete' },
+ { meta: true, name: 'd' },
+].forEach((deleteWordRightKey) => {
+ let [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ fi.emit('keypress', '.', { ctrl: true, name: 'left' });
+ fi.emit('keypress', '.', { ctrl: true, name: 'left' });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'the quick fox');
+ }));
+ fi.emit('keypress', '.', deleteWordRightKey);
+ fi.emit('data', '\n');
+ rli.close();
+
+ // No effect if pressed at end of line
+ [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'the quick brown fox');
+ }));
+ fi.emit('keypress', '.', deleteWordRightKey);
+ fi.emit('data', '\n');
+ rli.close();
+});
+
+// deleteLeft
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Delete left character
+ fi.emit('keypress', '.', { ctrl: true, name: 'h' });
+ assertCursorRowsAndCols(rli, 0, 18);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'the quick brown fo');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// deleteLeft astral character
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', '💻');
+ assertCursorRowsAndCols(rli, 0, 2);
+ // Delete left character
+ fi.emit('keypress', '.', { ctrl: true, name: 'h' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// deleteRight
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+
+ // Go to the start of the line
+ fi.emit('keypress', '.', { ctrl: true, name: 'a' });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete right character
+ fi.emit('keypress', '.', { ctrl: true, name: 'd' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'he quick brown fox');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// deleteRight astral character
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', '💻');
+
+ // Go to the start of the line
+ fi.emit('keypress', '.', { ctrl: true, name: 'a' });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete right character
+ fi.emit('keypress', '.', { ctrl: true, name: 'd' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// deleteLineLeft
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Delete from current to start of line
+ fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'backspace' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// deleteLineRight
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick brown fox');
+
+ // Go to the start of the line
+ fi.emit('keypress', '.', { ctrl: true, name: 'a' });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete from current to end of line
+ fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// Close readline interface
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('keypress', '.', { ctrl: true, name: 'c' });
+ assert(rli.closed);
+}
+
+// Multi-line input cursor position
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.columns = 10;
+ fi.emit('data', 'multi-line text');
+ assertCursorRowsAndCols(rli, 1, 5);
+ rli.close();
+}
+
+// Multi-line input cursor position and long tabs
+{
+ const [rli, fi] = getInterface({ tabSize: 16, terminal: true, prompt: '' });
+ fi.columns = 10;
+ fi.emit('data', 'multi-line\ttext \t');
+ assert.strictEqual(rli.cursor, 17);
+ assertCursorRowsAndCols(rli, 3, 2);
+ rli.close();
+}
+
+// Check for the default tab size.
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ fi.emit('data', 'the quick\tbrown\tfox');
+ assert.strictEqual(rli.cursor, 19);
+ // The first tab is 7 spaces long, the second one 3 spaces.
+ assertCursorRowsAndCols(rli, 0, 27);
+}
+
+// Multi-line prompt cursor position
+{
+ const [rli, fi] = getInterface({
+ terminal: true,
+ prompt: '\nfilledline\nwraping text\n> '
+ });
+ fi.columns = 10;
+ fi.emit('data', 't');
+ assertCursorRowsAndCols(rli, 4, 3);
+ rli.close();
+}
+
+// Clear the whole screen
+{
+ const [rli, fi] = getInterface({ terminal: true, prompt: '' });
+ const lines = ['line 1', 'line 2', 'line 3'];
+ fi.emit('data', lines.join('\n'));
+ fi.emit('keypress', '.', { ctrl: true, name: 'l' });
+ assertCursorRowsAndCols(rli, 0, 6);
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'line 3');
+ }));
+ fi.emit('data', '\n');
+ rli.close();
+}
+
+// Wide characters should be treated as two columns.
+assert.strictEqual(getStringWidth('a'), 1);
+assert.strictEqual(getStringWidth('あ'), 2);
+assert.strictEqual(getStringWidth('č°˘'), 2);
+assert.strictEqual(getStringWidth('ęł '), 2);
+assert.strictEqual(getStringWidth(String.fromCodePoint(0x1f251)), 2);
+assert.strictEqual(getStringWidth('abcde'), 5);
+assert.strictEqual(getStringWidth('古池や'), 6);
+assert.strictEqual(getStringWidth('ノード.js'), 9);
+assert.strictEqual(getStringWidth('你弽'), 4);
+assert.strictEqual(getStringWidth('안녕하세요'), 10);
+assert.strictEqual(getStringWidth('A\ud83c\ude00BC'), 5);
+assert.strictEqual(getStringWidth('👨‍👩‍👦‍👦'), 8);
+assert.strictEqual(getStringWidth('🐕𐐷あ💻😀'), 9);
+// TODO(BridgeAR): This should have a width of 4.
+assert.strictEqual(getStringWidth('⓬⓪'), 2);
+assert.strictEqual(getStringWidth('\u0301\u200D\u200E'), 0);
+
+// Check if vt control chars are stripped
+assert.strictEqual(stripVTControlCharacters('\u001b[31m> \u001b[39m'), '> ');
+assert.strictEqual(
+ stripVTControlCharacters('\u001b[31m> \u001b[39m> '),
+ '> > '
+);
+assert.strictEqual(stripVTControlCharacters('\u001b[31m\u001b[39m'), '');
+assert.strictEqual(stripVTControlCharacters('> '), '> ');
+assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m'), 2);
+assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m> '), 4);
+assert.strictEqual(getStringWidth('\u001b[31m\u001b[39m'), 0);
+assert.strictEqual(getStringWidth('> '), 2);
+
+// Check EventEmitter memory leak
+for (let i = 0; i < 12; i++) {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+ rl.close();
+ assert.strictEqual(isWarned(process.stdin._events), false);
+ assert.strictEqual(isWarned(process.stdout._events), false);
+}
+
+[true, false].forEach(function(terminal) {
+ // Disable history
+ {
+ const [rli, fi] = getInterface({ terminal, historySize: 0 });
+ assert.strictEqual(rli.historySize, 0);
+
+ fi.emit('data', 'asdf\n');
+ assert.deepStrictEqual(rli.history, []);
+ rli.close();
+ }
+
+ // Default history size 30
+ {
+ const [rli, fi] = getInterface({ terminal });
+ assert.strictEqual(rli.historySize, 30);
+
+ fi.emit('data', 'asdf\n');
+ assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []);
+ rli.close();
+ }
+
+ // Sending a full line
+ {
+ const [rli, fi] = getInterface({ terminal });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'asdf');
+ }));
+ fi.emit('data', 'asdf\n');
+ }
+
+ // Sending a blank line
+ {
+ const [rli, fi] = getInterface({ terminal });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, '');
+ }));
+ fi.emit('data', '\n');
+ }
+
+ // Sending a single character with no newline and then a newline
+ {
+ const [rli, fi] = getInterface({ terminal });
+ let called = false;
+ rli.on('line', (line) => {
+ called = true;
+ assert.strictEqual(line, 'a');
+ });
+ fi.emit('data', 'a');
+ assert.ok(!called);
+ fi.emit('data', '\n');
+ assert.ok(called);
+ rli.close();
+ }
+
+ // Sending multiple newlines at once
+ {
+ const [rli, fi] = getInterface({ terminal });
+ const expectedLines = ['foo', 'bar', 'baz'];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length));
+ fi.emit('data', `${expectedLines.join('\n')}\n`);
+ rli.close();
+ }
+
+ // Sending multiple newlines at once that does not end with a new line
+ {
+ const [rli, fi] = getInterface({ terminal });
+ const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length - 1));
+ fi.emit('data', expectedLines.join('\n'));
+ rli.close();
+ }
+
+ // Sending multiple newlines at once that does not end with a new(empty)
+ // line and a `end` event
+ {
+ const [rli, fi] = getInterface({ terminal });
+ const expectedLines = ['foo', 'bar', 'baz', ''];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length - 1));
+ rli.on('close', common.mustCall());
+ fi.emit('data', expectedLines.join('\n'));
+ fi.emit('end');
+ rli.close();
+ }
+
+ // Sending a multi-byte utf8 char over multiple writes
+ {
+ const buf = Buffer.from('☎', 'utf8');
+ const [rli, fi] = getInterface({ terminal });
+ let callCount = 0;
+ rli.on('line', function(line) {
+ callCount++;
+ assert.strictEqual(line, buf.toString('utf8'));
+ });
+ for (const i of buf) {
+ fi.emit('data', Buffer.from([i]));
+ }
+ assert.strictEqual(callCount, 0);
+ fi.emit('data', '\n');
+ assert.strictEqual(callCount, 1);
+ rli.close();
+ }
+
+ // Calling readline without `new`
+ {
+ const [rli, fi] = getInterface({ terminal });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'asdf');
+ }));
+ fi.emit('data', 'asdf\n');
+ rli.close();
+ }
+
+ // Calling the question callback
+ {
+ const [rli] = getInterface({ terminal });
+ rli.question('foo?').then(common.mustCall((answer) => {
+ assert.strictEqual(answer, 'bar');
+ }));
+ rli.write('bar\n');
+ rli.close();
+ }
+
+ // Aborting a question
+ {
+ const ac = new AbortController();
+ const signal = ac.signal;
+ const [rli] = getInterface({ terminal });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'bar');
+ }));
+ assert.rejects(rli.question('hello?', { signal }), { name: 'AbortError' })
+ .then(common.mustCall());
+ ac.abort();
+ rli.write('bar\n');
+ rli.close();
+ }
+
+ // Can create a new readline Interface with a null output argument
+ {
+ const [rli, fi] = getInterface({ output: null, terminal });
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, 'asdf');
+ }));
+ fi.emit('data', 'asdf\n');
+
+ rli.setPrompt('ddd> ');
+ rli.prompt();
+ rli.write("really shouldn't be seeing this");
+ rli.question('What do you think of node.js? ', function(answer) {
+ console.log('Thank you for your valuable feedback:', answer);
+ rli.close();
+ });
+ }
+
+ // Calling the getPrompt method
+ {
+ const expectedPrompts = ['$ ', '> '];
+ const [rli] = getInterface({ terminal });
+ for (const prompt of expectedPrompts) {
+ rli.setPrompt(prompt);
+ assert.strictEqual(rli.getPrompt(), prompt);
+ }
+ }
+
+ {
+ const expected = terminal ?
+ ['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G'] :
+ ['$ '];
+
+ const output = new Writable({
+ write: common.mustCall((chunk, enc, cb) => {
+ assert.strictEqual(chunk.toString(), expected.shift());
+ cb();
+ rl.close();
+ }, expected.length)
+ });
+
+ const rl = readline.createInterface({
+ input: new Readable({ read: common.mustCall() }),
+ output,
+ prompt: '$ ',
+ terminal
+ });
+
+ rl.prompt();
+
+ assert.strictEqual(rl.getPrompt(), '$ ');
+ }
+
+ {
+ const fi = new FakeInput();
+ assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []);
+ }
+
+ // Emit two line events when the delay
+ // between \r and \n exceeds crlfDelay
+ {
+ const crlfDelay = 200;
+ const [rli, fi] = getInterface({ terminal, crlfDelay });
+ let callCount = 0;
+ rli.on('line', function(line) {
+ callCount++;
+ });
+ fi.emit('data', '\r');
+ setTimeout(common.mustCall(() => {
+ fi.emit('data', '\n');
+ assert.strictEqual(callCount, 2);
+ rli.close();
+ }), crlfDelay + 10);
+ }
+
+ // For the purposes of the following tests, we do not care about the exact
+ // value of crlfDelay, only that the behaviour conforms to what's expected.
+ // Setting it to Infinity allows the test to succeed even under extreme
+ // CPU stress.
+ const crlfDelay = Infinity;
+
+ // Set crlfDelay to `Infinity` is allowed
+ {
+ const delay = 200;
+ const [rli, fi] = getInterface({ terminal, crlfDelay });
+ let callCount = 0;
+ rli.on('line', function(line) {
+ callCount++;
+ });
+ fi.emit('data', '\r');
+ setTimeout(common.mustCall(() => {
+ fi.emit('data', '\n');
+ assert.strictEqual(callCount, 1);
+ rli.close();
+ }), delay);
+ }
+
+ // Sending multiple newlines at once that does not end with a new line
+ // and a `end` event(last line is)
+
+ // \r\n should emit one line event, not two
+ {
+ const [rli, fi] = getInterface({ terminal, crlfDelay });
+ const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length - 1));
+ fi.emit('data', expectedLines.join('\r\n'));
+ rli.close();
+ }
+
+ // \r\n should emit one line event when split across multiple writes.
+ {
+ const [rli, fi] = getInterface({ terminal, crlfDelay });
+ const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ let callCount = 0;
+ rli.on('line', common.mustCall((line) => {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ }, expectedLines.length));
+ expectedLines.forEach((line) => {
+ fi.emit('data', `${line}\r`);
+ fi.emit('data', '\n');
+ });
+ rli.close();
+ }
+
+ // Emit one line event when the delay between \r and \n is
+ // over the default crlfDelay but within the setting value.
+ {
+ const delay = 125;
+ const [rli, fi] = getInterface({ terminal, crlfDelay });
+ let callCount = 0;
+ rli.on('line', () => callCount++);
+ fi.emit('data', '\r');
+ setTimeout(common.mustCall(() => {
+ fi.emit('data', '\n');
+ assert.strictEqual(callCount, 1);
+ rli.close();
+ }), delay);
+ }
+});
+
+// Ensure that the _wordLeft method works even for large input
+{
+ const input = new Readable({
+ read() {
+ this.push('\x1B[1;5D'); // CTRL + Left
+ this.push(null);
+ },
+ });
+ const output = new Writable({
+ write: common.mustCall((data, encoding, cb) => {
+ assert.strictEqual(rl.cursor, rl.line.length - 1);
+ cb();
+ }),
+ });
+ const rl = new readline.createInterface({
+ input,
+ output,
+ terminal: true,
+ });
+ rl.line = `a${' '.repeat(1e6)}a`;
+ rl.cursor = rl.line.length;
+}
diff --git a/test/parallel/test-readline-promises-tab-complete.js b/test/parallel/test-readline-promises-tab-complete.js
new file mode 100644
index 00000000000..45a4be35977
--- /dev/null
+++ b/test/parallel/test-readline-promises-tab-complete.js
@@ -0,0 +1,116 @@
+'use strict';
+
+// Flags: --expose-internals
+
+const common = require('../common');
+const readline = require('readline/promises');
+const assert = require('assert');
+const { EventEmitter } = require('events');
+const { getStringWidth } = require('internal/util/inspect');
+
+common.skipIfDumbTerminal();
+
+// This test verifies that the tab completion supports unicode and the writes
+// are limited to the minimum.
+[
+ 'あ',
+ '𐐡',
+ '🐕',
+].forEach((char) => {
+ [true, false].forEach((lineBreak) => {
+ [
+ (line) => [
+ ['First group', '',
+ `${char}${'a'.repeat(10)}`,
+ `${char}${'b'.repeat(10)}`,
+ char.repeat(11),
+ ],
+ line,
+ ],
+
+ async (line) => [
+ ['First group', '',
+ `${char}${'a'.repeat(10)}`,
+ `${char}${'b'.repeat(10)}`,
+ char.repeat(11),
+ ],
+ line,
+ ],
+ ].forEach((completer) => {
+
+ let output = '';
+ const width = getStringWidth(char) - 1;
+
+ class FakeInput extends EventEmitter {
+ columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3
+
+ write = common.mustCall((data) => {
+ output += data;
+ }, 6)
+
+ resume() {}
+ pause() {}
+ end() {}
+ }
+
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ terminal: true,
+ completer: common.mustCallAtLeast(completer),
+ });
+
+ const last = '\r\nFirst group\r\n\r\n' +
+ `${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` +
+ `${char}${'b'.repeat(10)}` +
+ (lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) +
+ `${char.repeat(11)}\r\n` +
+ `\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`;
+
+ const expectations = [char, '', last];
+
+ rli.on('line', common.mustNotCall());
+ for (const character of `${char}\t\t`) {
+ fi.emit('data', character);
+ queueMicrotask(() => {
+ assert.strictEqual(output, expectations.shift());
+ output = '';
+ });
+ }
+ rli.close();
+ });
+ });
+});
+
+{
+ let output = '';
+ class FakeInput extends EventEmitter {
+ columns = 80
+
+ write = common.mustCall((data) => {
+ output += data;
+ }, 1)
+
+ resume() {}
+ pause() {}
+ end() {}
+ }
+
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ terminal: true,
+ completer:
+ common.mustCallAtLeast(() => Promise.reject(new Error('message'))),
+ });
+
+ rli.on('line', common.mustNotCall());
+ fi.emit('data', '\t');
+ queueMicrotask(() => {
+ assert.match(output, /^Tab completion error: Error: message/);
+ output = '';
+ });
+ rli.close();
+}
diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs
index bcd95360ee6..3cdee188c01 100644
--- a/tools/doc/type-parser.mjs
+++ b/tools/doc/type-parser.mjs
@@ -197,7 +197,12 @@ const customTypesMap = {
'PerformanceObserverEntryList':
'perf_hooks.html#class-performanceobserverentrylist',
- 'readline.Interface': 'readline.html#class-interface',
+ 'readline.Interface':
+ 'readline.html#class-readlineinterface',
+ 'readline.InterfaceConstructor':
+ 'readline.html#class-interfaceconstructor',
+ 'readlinePromises.Interface':
+ 'readline.html#class-readlinepromisesinterface',
'repl.REPLServer': 'repl.html#class-replserver',