refactor(readline/promises): re-export readline.promises from readline (#1748)

* refactor(readline/promises): re-export readline.promises from readline/promises

* fix(readline): don't export Readline from `readline`

* perf(readline): return Promise.reject immediately after failed validation
This commit is contained in:
Derrick Farris
2023-01-09 18:27:56 -06:00
committed by GitHub
parent 0e7f69f179
commit 4ef60da8a9
2 changed files with 183 additions and 239 deletions

View File

@@ -28,6 +28,7 @@ var { Array, RegExp, String, Bun } = import.meta.primordials;
var EventEmitter = import.meta.require("node:events");
var { clearTimeout, setTimeout } = import.meta.require("timers");
var { StringDecoder } = import.meta.require("string_decoder");
var isWritable;
var { inspect } = Bun;
var debug = process.env.BUN_JS_DEBUG ? console.log : () => {};
@@ -372,6 +373,14 @@ class ERR_USE_AFTER_CLOSE extends NodeError {
}
}
class AbortError extends Error {
code;
constructor() {
super("The operation was aborted");
this.code = "ABORT_ERR";
}
}
// Validators
/**
@@ -2696,18 +2705,19 @@ Interface.prototype.question = function question(query, options, cb) {
options = kEmptyObject;
}
if (options.signal) {
validateAbortSignal(options.signal, "options.signal");
if (options.signal.aborted) {
var signal = options?.signal;
if (signal) {
validateAbortSignal(signal, "options.signal");
if (signal.aborted) {
return;
}
var onAbort = () => {
this[kQuestionCancel]();
};
options.signal.addEventListener("abort", onAbort, { once: true });
signal.addEventListener("abort", onAbort, { once: true });
var cleanup = () => {
options.signal.removeEventListener("abort", onAbort);
signal.removeEventListener("abort", onAbort);
};
var originalCb = cb;
cb =
@@ -2723,6 +2733,7 @@ Interface.prototype.question = function question(query, options, cb) {
this[kQuestion](query, cb);
}
};
Interface.prototype.question[promisify.custom] = function question(
query,
options,
@@ -2731,26 +2742,24 @@ Interface.prototype.question[promisify.custom] = function question(
options = kEmptyObject;
}
if (options.signal && options.signal.aborted) {
return PromiseReject(
new AbortError(undefined, { cause: options.signal.reason }),
);
var signal = options?.signal;
if (signal && signal.aborted) {
return PromiseReject(new AbortError(undefined, { cause: signal.reason }));
}
return new Promise((resolve, reject) => {
var cb = resolve;
if (options.signal) {
if (signal) {
var onAbort = () => {
reject(new AbortError(undefined, { cause: options.signal.reason }));
reject(new AbortError(undefined, { cause: signal.reason }));
};
options.signal.addEventListener("abort", onAbort, { once: true });
signal.addEventListener("abort", onAbort, { once: true });
cb = (answer) => {
options.signal.removeEventListener("abort", onAbort);
signal.removeEventListener("abort", onAbort);
resolve(answer);
};
}
this.question(query, options, cb);
});
};
@@ -3081,6 +3090,157 @@ function _ttyWriteDumb(s, key) {
}
}
class Readline {
#autoCommit = false;
#stream;
#todo = [];
constructor(stream, options = undefined) {
isWritable ??= import.meta.require("node:stream").isWritable;
if (!isWritable(stream))
throw new ERR_INVALID_ARG_TYPE("stream", "Writable", stream);
this.#stream = stream;
if (options?.autoCommit != null) {
validateBoolean(options.autoCommit, "options.autoCommit");
this.#autoCommit = options.autoCommit;
}
}
/**
* 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");
var data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(this.#todo, data);
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");
var 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`;
}
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(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);
var data =
dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(this.#todo, data);
return this;
}
/**
* Clears the screen from the current position of the cursor down.
* @returns {Readline} this
*/
clearScreenDown() {
if (this.#autoCommit) {
process.nextTick(() => this.#stream.write(kClearScreenDown));
} else {
ArrayPrototypePush.call(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.call(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;
}
}
var PromisesInterface = class Interface extends _Interface {
// eslint-disable-next-line no-useless-constructor
constructor(input, output, completer, terminal) {
super(input, output, completer, terminal);
}
question(query, options = kEmptyObject) {
var signal = options?.signal;
if (signal) {
validateAbortSignal(signal, "options.signal");
if (signal.aborted) {
return PromiseReject(
new AbortError(undefined, { cause: signal.reason }),
);
}
}
return new Promise((resolve, reject) => {
var cb = resolve;
if (options?.signal) {
var onAbort = () => {
this[kQuestionCancel]();
reject(new AbortError(undefined, { cause: signal.reason }));
};
signal.addEventListener("abort", onAbort, { once: true });
cb = (answer) => {
signal.removeEventListener("abort", onAbort);
resolve(answer);
};
}
this[kQuestion](query, cb);
});
}
};
// ----------------------------------------------------------------------------
// Exports
// ----------------------------------------------------------------------------
@@ -3092,7 +3252,11 @@ export var cursorTo = cursorTo;
export var emitKeypressEvents = emitKeypressEvents;
export var moveCursor = moveCursor;
export var promises = {
[SymbolFor("__UNIMPLEMENTED__")]: true,
Readline,
Interface: PromisesInterface,
createInterface(input, output, completer, terminal) {
return new Interface(input, output, completer, terminal);
},
};
export default {
@@ -3107,22 +3271,10 @@ export default {
[SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]: {
CSI,
_Interface,
utils: {
getStringWidth,
stripVTControlCharacters,
},
shared: {
kEmptyObject,
validateBoolean,
validateInteger,
validateAbortSignal,
ERR_INVALID_ARG_TYPE,
},
symbols: {
kQuestion,
kQuestionCancel,
},
},
[SymbolFor("CommonJS")]: 0,
};

View File

@@ -1,218 +1,10 @@
// Attribution: Some parts of of this module are derived from code originating from the Node.js
// readline module which is licensed under an MIT license:
//
// Copyright Node.js contributors. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
var { Promise } = import.meta.primordials;
var readline = import.meta.require("node:readline");
var isWritable;
var ArrayPrototypePush = Array.prototype.push;
var ArrayPrototypeJoin = Array.prototype.join;
var SymbolFor = Symbol.for;
var kInternal = SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__");
var {
CSI,
_Interface,
symbols: { kQuestion, kQuestionCancel },
shared: {
kEmptyObject,
validateAbortSignal,
validateBoolean,
validateInteger,
ERR_INVALID_ARG_TYPE,
},
} = readline[kInternal];
var { kClearToLineBeginning, kClearToLineEnd, kClearLine, kClearScreenDown } =
CSI;
class AbortError extends Error {
code;
constructor() {
super("The operation was aborted");
this.code = "ABORT_ERR";
}
}
export class Readline {
#autoCommit = false;
#stream;
#todo = [];
constructor(stream, options = undefined) {
isWritable ??= import.meta.require("node:stream").isWritable;
if (!isWritable(stream))
throw new ERR_INVALID_ARG_TYPE("stream", "Writable", stream);
this.#stream = stream;
if (options?.autoCommit != null) {
validateBoolean(options.autoCommit, "options.autoCommit");
this.#autoCommit = options.autoCommit;
}
}
/**
* 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");
var data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(this.#todo, data);
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");
var 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`;
}
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(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);
var data =
dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
else ArrayPrototypePush.call(this.#todo, data);
return this;
}
/**
* Clears the screen from the current position of the cursor down.
* @returns {Readline} this
*/
clearScreenDown() {
if (this.#autoCommit) {
process.nextTick(() => this.#stream.write(kClearScreenDown));
} else {
ArrayPrototypePush.call(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.call(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;
}
}
export class Interface extends _Interface {
// eslint-disable-next-line no-useless-constructor
constructor(input, output, completer, terminal) {
super(input, output, completer, terminal);
}
question(query, options = kEmptyObject) {
return new Promise((resolve, reject) => {
var cb = resolve;
if (options?.signal) {
validateAbortSignal(options.signal, "options.signal");
if (options.signal.aborted) {
return reject(
new AbortError(undefined, { cause: options.signal.reason }),
);
}
var onAbort = () => {
this[kQuestionCancel]();
reject(new AbortError(undefined, { cause: options.signal.reason }));
};
options.signal.addEventListener("abort", onAbort, { once: true });
cb = (answer) => {
options.signal.removeEventListener("abort", onAbort);
resolve(answer);
};
}
this[kQuestion](query, cb);
});
}
}
export function createInterface(input, output, completer, terminal) {
return new Interface(input, output, completer, terminal);
}
promises: { Readline, Interface, createInterface },
} = import.meta.require("node:readline");
export default {
Readline,
Interface,
createInterface,
[SymbolFor("CommonJS")]: 0,
[Symbol.for("CommonJS")]: 0,
};