mirror of
https://github.com/nodejs/node.git
synced 2025-12-28 07:50:41 +00:00
repl: extract and standardize history from both repl and interface
PR-URL: https://github.com/nodejs/node/pull/58225 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Dario Piotrowicz <dario.piotrowicz@gmail.com>
This commit is contained in:
parent
4be147351a
commit
5a23443104
@ -645,14 +645,35 @@ buffered but not yet executed. This method is primarily intended to be
|
||||
called from within the action function for commands registered using the
|
||||
`replServer.defineCommand()` method.
|
||||
|
||||
### `replServer.setupHistory(historyPath, callback)`
|
||||
### `replServer.setupHistory(historyConfig, callback)`
|
||||
|
||||
<!-- YAML
|
||||
added: v11.10.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58225
|
||||
description: Updated the `historyConfig` parameter to accept an object
|
||||
with `filePath`, `size`, `removeHistoryDuplicates` and
|
||||
`onHistoryFileLoaded` properties.
|
||||
-->
|
||||
|
||||
* `historyPath` {string} the path to the history file
|
||||
* `historyConfig` {Object|string} the path to the history file
|
||||
If it is a string, it is the path to the history file.
|
||||
If it is an object, it can have the following properties:
|
||||
* `filePath` {string} the path to the history file
|
||||
* `size` {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`.
|
||||
* `onHistoryFileLoaded` {Function} called when history writes are ready or upon error
|
||||
* `err` {Error}
|
||||
* `repl` {repl.REPLServer}
|
||||
* `callback` {Function} called when history writes are ready or upon error
|
||||
(Optional if provided as `onHistoryFileLoaded` in `historyConfig`)
|
||||
* `err` {Error}
|
||||
* `repl` {repl.REPLServer}
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) {
|
||||
throw err;
|
||||
}
|
||||
repl.on('exit', () => {
|
||||
if (repl._flushing) {
|
||||
if (repl.historyManager.isFlushing) {
|
||||
return repl.once('flushHistory', () => {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
@ -3,14 +3,12 @@
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
ArrayPrototypePop,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeReverse,
|
||||
ArrayPrototypeShift,
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeUnshift,
|
||||
DateNow,
|
||||
FunctionPrototypeCall,
|
||||
@ -19,6 +17,7 @@ const {
|
||||
MathMax,
|
||||
MathMaxApply,
|
||||
NumberIsFinite,
|
||||
ObjectDefineProperty,
|
||||
ObjectSetPrototypeOf,
|
||||
RegExpPrototypeExec,
|
||||
SafeStringIterator,
|
||||
@ -30,7 +29,6 @@ const {
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeStartsWith,
|
||||
StringPrototypeTrim,
|
||||
Symbol,
|
||||
SymbolAsyncIterator,
|
||||
SymbolDispose,
|
||||
@ -46,8 +44,6 @@ const {
|
||||
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateArray,
|
||||
validateNumber,
|
||||
validateString,
|
||||
validateUint32,
|
||||
} = require('internal/validators');
|
||||
@ -67,7 +63,6 @@ const {
|
||||
charLengthLeft,
|
||||
commonPrefix,
|
||||
kSubstringSearch,
|
||||
reverseString,
|
||||
} = require('internal/readline/utils');
|
||||
let emitKeypressEvents;
|
||||
let kFirstEventParam;
|
||||
@ -78,8 +73,8 @@ const {
|
||||
} = require('internal/readline/callbacks');
|
||||
|
||||
const { StringDecoder } = require('string_decoder');
|
||||
const { ReplHistory } = require('internal/repl/history');
|
||||
|
||||
const kHistorySize = 30;
|
||||
const kMaxUndoRedoStackSize = 2048;
|
||||
const kMincrlfDelay = 100;
|
||||
/**
|
||||
@ -153,7 +148,6 @@ const kWriteToOutput = Symbol('_writeToOutput');
|
||||
const kYank = Symbol('_yank');
|
||||
const kYanking = Symbol('_yanking');
|
||||
const kYankPop = Symbol('_yankPop');
|
||||
const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');
|
||||
const kSavePreviousState = Symbol('_savePreviousState');
|
||||
const kRestorePreviousState = Symbol('_restorePreviousState');
|
||||
const kPreviousLine = Symbol('_previousLine');
|
||||
@ -175,9 +169,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
|
||||
FunctionPrototypeCall(EventEmitter, this);
|
||||
|
||||
let history;
|
||||
let historySize;
|
||||
let removeHistoryDuplicates = false;
|
||||
let crlfDelay;
|
||||
let prompt = '> ';
|
||||
let signal;
|
||||
@ -187,14 +178,17 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
output = input.output;
|
||||
completer = input.completer;
|
||||
terminal = input.terminal;
|
||||
history = input.history;
|
||||
historySize = input.historySize;
|
||||
signal = input.signal;
|
||||
|
||||
// It is possible to configure the history through the input object
|
||||
const historySize = input.historySize;
|
||||
const history = input.history;
|
||||
const removeHistoryDuplicates = input.removeHistoryDuplicates;
|
||||
|
||||
if (input.tabSize !== undefined) {
|
||||
validateUint32(input.tabSize, 'tabSize', true);
|
||||
this.tabSize = input.tabSize;
|
||||
}
|
||||
removeHistoryDuplicates = input.removeHistoryDuplicates;
|
||||
if (input.prompt !== undefined) {
|
||||
prompt = input.prompt;
|
||||
}
|
||||
@ -215,24 +209,18 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
|
||||
crlfDelay = input.crlfDelay;
|
||||
input = input.input;
|
||||
|
||||
input.size = historySize;
|
||||
input.history = history;
|
||||
input.removeHistoryDuplicates = removeHistoryDuplicates;
|
||||
}
|
||||
|
||||
this.setupHistoryManager(input);
|
||||
|
||||
if (completer !== undefined && typeof completer !== 'function') {
|
||||
throw new ERR_INVALID_ARG_VALUE('completer', completer);
|
||||
}
|
||||
|
||||
if (history === undefined) {
|
||||
history = [];
|
||||
} else {
|
||||
validateArray(history, 'history');
|
||||
}
|
||||
|
||||
if (historySize === undefined) {
|
||||
historySize = kHistorySize;
|
||||
}
|
||||
|
||||
validateNumber(historySize, 'historySize', 0);
|
||||
|
||||
// Backwards compat; check the isTTY prop of the output stream
|
||||
// when `terminal` was not specified
|
||||
if (terminal === undefined && !(output === null || output === undefined)) {
|
||||
@ -248,8 +236,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
this.input = input;
|
||||
this[kUndoStack] = [];
|
||||
this[kRedoStack] = [];
|
||||
this.history = history;
|
||||
this.historySize = historySize;
|
||||
this[kPreviousCursorCols] = -1;
|
||||
|
||||
// The kill ring is a global list of blocks of text that were previously
|
||||
@ -260,7 +246,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
this[kKillRing] = [];
|
||||
this[kKillRingCursor] = 0;
|
||||
|
||||
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
|
||||
this.crlfDelay = crlfDelay ?
|
||||
MathMax(kMincrlfDelay, crlfDelay) :
|
||||
kMincrlfDelay;
|
||||
@ -270,7 +255,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
|
||||
this.terminal = !!terminal;
|
||||
|
||||
|
||||
function onerror(err) {
|
||||
self.emit('error', err);
|
||||
}
|
||||
@ -349,8 +333,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
// Cursor position on the line.
|
||||
this.cursor = 0;
|
||||
|
||||
this.historyIndex = -1;
|
||||
|
||||
if (output !== null && output !== undefined)
|
||||
output.on('resize', onresize);
|
||||
|
||||
@ -403,6 +385,36 @@ class Interface extends InterfaceConstructor {
|
||||
return this[kPrompt];
|
||||
}
|
||||
|
||||
setupHistoryManager(options) {
|
||||
this.historyManager = new ReplHistory(this, options);
|
||||
|
||||
if (options.onHistoryFileLoaded) {
|
||||
this.historyManager.initialize(options.onHistoryFileLoaded);
|
||||
}
|
||||
|
||||
ObjectDefineProperty(this, 'history', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.history; },
|
||||
set(newHistory) { return this.historyManager.history = newHistory; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'historyIndex', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.index; },
|
||||
set(historyIndex) { return this.historyManager.index = historyIndex; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'historySize', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.size; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'isFlushing', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.isFlushing; },
|
||||
});
|
||||
}
|
||||
|
||||
[kSetRawMode](mode) {
|
||||
const wasInRawMode = this.input.isRaw;
|
||||
|
||||
@ -478,70 +490,8 @@ class Interface extends InterfaceConstructor {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert newlines to a consistent format for history storage
|
||||
[kNormalizeHistoryLineEndings](line, from, to, reverse = true) {
|
||||
// Multiline history entries are saved reversed
|
||||
// History is structured with the newest entries at the top
|
||||
// and the oldest at the bottom. Multiline histories, however, only occupy
|
||||
// one line in the history file. When loading multiline history with
|
||||
// an old node binary, the history will be saved in the old format.
|
||||
// This is why we need to reverse the multilines.
|
||||
// Reversing the multilines is necessary when adding / editing and displaying them
|
||||
if (reverse) {
|
||||
// First reverse the lines for proper order, then convert separators
|
||||
return reverseString(line, from, to);
|
||||
}
|
||||
// For normal cases (saving to history or non-multiline entries)
|
||||
return StringPrototypeReplaceAll(line, from, to);
|
||||
}
|
||||
|
||||
[kAddHistory]() {
|
||||
if (this.line.length === 0) return '';
|
||||
|
||||
// If the history is disabled then return the line
|
||||
if (this.historySize === 0) return this.line;
|
||||
|
||||
// If the trimmed line is empty then return the line
|
||||
if (StringPrototypeTrim(this.line).length === 0) return this.line;
|
||||
|
||||
// This is necessary because each line would be saved in the history while creating
|
||||
// A new multiline, and we don't want that.
|
||||
if (this[kIsMultiline] && this.historyIndex === -1) {
|
||||
ArrayPrototypeShift(this.history);
|
||||
} else if (this[kLastCommandErrored]) {
|
||||
// If the last command errored and we are trying to edit the history to fix it
|
||||
// Remove the broken one from the history
|
||||
ArrayPrototypeShift(this.history);
|
||||
}
|
||||
|
||||
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', true);
|
||||
|
||||
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
|
||||
if (this.removeHistoryDuplicates) {
|
||||
// Remove older history line if identical to new one
|
||||
const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
|
||||
if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1);
|
||||
}
|
||||
|
||||
// Add the new line to the history
|
||||
ArrayPrototypeUnshift(this.history, normalizedLine);
|
||||
|
||||
// Only store so many
|
||||
if (this.history.length > this.historySize)
|
||||
ArrayPrototypePop(this.history);
|
||||
}
|
||||
|
||||
this.historyIndex = -1;
|
||||
|
||||
// The listener could change the history object, possibly
|
||||
// to remove the last added entry if it is sensitive and should
|
||||
// not be persisted in the history, like a password
|
||||
const line = this[kIsMultiline] ? reverseString(this.history[0]) : this.history[0];
|
||||
|
||||
// Emit history event to notify listeners of update
|
||||
this.emit('history', this.history);
|
||||
|
||||
return line;
|
||||
return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]);
|
||||
}
|
||||
|
||||
[kRefreshLine]() {
|
||||
@ -1184,26 +1134,12 @@ class Interface extends InterfaceConstructor {
|
||||
// <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
|
||||
// one.
|
||||
[kHistoryNext]() {
|
||||
if (this.historyIndex >= 0) {
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
const search = this[kSubstringSearch] || '';
|
||||
let index = this.historyIndex - 1;
|
||||
while (
|
||||
index >= 0 &&
|
||||
(!StringPrototypeStartsWith(this.history[index], search) ||
|
||||
this.line === this.history[index])
|
||||
) {
|
||||
index--;
|
||||
}
|
||||
if (index === -1) {
|
||||
this[kSetLine](search);
|
||||
} else {
|
||||
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
|
||||
}
|
||||
this.historyIndex = index;
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
if (!this.historyManager.canNavigateToNext()) { return; }
|
||||
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
this[kSetLine](this.historyManager.navigateToNext(this[kSubstringSearch]));
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
|
||||
[kMoveUpOrHistoryPrev]() {
|
||||
@ -1218,26 +1154,12 @@ class Interface extends InterfaceConstructor {
|
||||
}
|
||||
|
||||
[kHistoryPrev]() {
|
||||
if (this.historyIndex < this.history.length && this.history.length) {
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
const search = this[kSubstringSearch] || '';
|
||||
let index = this.historyIndex + 1;
|
||||
while (
|
||||
index < this.history.length &&
|
||||
(!StringPrototypeStartsWith(this.history[index], search) ||
|
||||
this.line === this.history[index])
|
||||
) {
|
||||
index++;
|
||||
}
|
||||
if (index === this.history.length) {
|
||||
this[kSetLine](search);
|
||||
} else {
|
||||
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
|
||||
}
|
||||
this.historyIndex = index;
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
if (!this.historyManager.canNavigateToPrevious()) { return; }
|
||||
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
this[kSetLine](this.historyManager.navigateToPrevious(this[kSubstringSearch]));
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
|
||||
// Returns the last character's display position of the given string
|
||||
|
||||
@ -40,14 +40,21 @@ function createRepl(env, opts, cb) {
|
||||
opts.replMode = REPL.REPL_MODE_SLOPPY;
|
||||
}
|
||||
|
||||
const historySize = Number(env.NODE_REPL_HISTORY_SIZE);
|
||||
if (!NumberIsNaN(historySize) && historySize > 0) {
|
||||
opts.historySize = historySize;
|
||||
const size = Number(env.NODE_REPL_HISTORY_SIZE);
|
||||
if (!NumberIsNaN(size) && size > 0) {
|
||||
opts.size = size;
|
||||
} else {
|
||||
opts.historySize = 1000;
|
||||
opts.size = 1000;
|
||||
}
|
||||
|
||||
const repl = REPL.start(opts);
|
||||
const term = 'terminal' in opts ? opts.terminal : process.stdout.isTTY;
|
||||
repl.setupHistory(term ? env.NODE_REPL_HISTORY : '', cb);
|
||||
opts.filePath = term ? env.NODE_REPL_HISTORY : '';
|
||||
|
||||
const repl = REPL.start(opts);
|
||||
|
||||
repl.setupHistory({
|
||||
filePath: opts.filePath,
|
||||
size: opts.size,
|
||||
onHistoryFileLoaded: cb,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypePop,
|
||||
ArrayPrototypeShift,
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeUnshift,
|
||||
Boolean,
|
||||
FunctionPrototype,
|
||||
RegExpPrototypeSymbolSplit,
|
||||
StringPrototypeStartsWith,
|
||||
StringPrototypeTrim,
|
||||
Symbol,
|
||||
} = primordials;
|
||||
|
||||
const { Interface } = require('readline');
|
||||
const { validateNumber, validateArray } = require('internal/validators');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@ -17,168 +24,400 @@ let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
|
||||
});
|
||||
const permission = require('internal/process/permission');
|
||||
const { clearTimeout, setTimeout } = require('timers');
|
||||
const {
|
||||
reverseString,
|
||||
} = require('internal/readline/utils');
|
||||
|
||||
const noop = FunctionPrototype;
|
||||
|
||||
// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
|
||||
// The debounce is to guard against code pasted into the REPL.
|
||||
const kDebounceHistoryMS = 15;
|
||||
const kHistorySize = 30;
|
||||
|
||||
module.exports = setupHistory;
|
||||
// Class fields
|
||||
const kTimer = Symbol('_kTimer');
|
||||
const kWriting = Symbol('_kWriting');
|
||||
const kPending = Symbol('_kPending');
|
||||
const kRemoveHistoryDuplicates = Symbol('_kRemoveHistoryDuplicates');
|
||||
const kHistoryHandle = Symbol('_kHistoryHandle');
|
||||
const kHistoryPath = Symbol('_kHistoryPath');
|
||||
const kContext = Symbol('_kContext');
|
||||
const kIsFlushing = Symbol('_kIsFlushing');
|
||||
const kHistory = Symbol('_kHistory');
|
||||
const kSize = Symbol('_kSize');
|
||||
const kIndex = Symbol('_kIndex');
|
||||
|
||||
function _writeToOutput(repl, message) {
|
||||
repl._writeToOutput(message);
|
||||
repl._refreshLine();
|
||||
}
|
||||
// Class methods
|
||||
const kNormalizeLineEndings = Symbol('_kNormalizeLineEndings');
|
||||
const kWriteToOutput = Symbol('_kWriteToOutput');
|
||||
const kOnLine = Symbol('_kOnLine');
|
||||
const kOnExit = Symbol('_kOnExit');
|
||||
const kInitializeHistory = Symbol('_kInitializeHistory');
|
||||
const kHandleHistoryInitError = Symbol('_kHandleHistoryInitError');
|
||||
const kHasWritePermission = Symbol('_kHasWritePermission');
|
||||
const kValidateOptions = Symbol('_kValidateOptions');
|
||||
const kResolveHistoryPath = Symbol('_kResolveHistoryPath');
|
||||
const kReplHistoryMessage = Symbol('_kReplHistoryMessage');
|
||||
const kFlushHistory = Symbol('_kFlushHistory');
|
||||
const kGetHistoryPath = Symbol('_kGetHistoryPath');
|
||||
|
||||
function setupHistory(repl, historyPath, ready) {
|
||||
// Empty string disables persistent history
|
||||
if (typeof historyPath === 'string')
|
||||
historyPath = StringPrototypeTrim(historyPath);
|
||||
class ReplHistory {
|
||||
constructor(context, options) {
|
||||
this[kValidateOptions](options);
|
||||
|
||||
if (historyPath === '') {
|
||||
repl._historyPrev = _replHistoryMessage;
|
||||
return ready(null, repl);
|
||||
this[kHistoryPath] = ReplHistory[kGetHistoryPath](options);
|
||||
this[kContext] = context;
|
||||
this[kTimer] = null;
|
||||
this[kWriting] = false;
|
||||
this[kPending] = false;
|
||||
this[kRemoveHistoryDuplicates] = options.removeHistoryDuplicates || false;
|
||||
this[kHistoryHandle] = null;
|
||||
this[kIsFlushing] = false;
|
||||
this[kSize] = options.size ?? context.historySize ?? kHistorySize;
|
||||
this[kHistory] = options.history ?? [];
|
||||
this[kIndex] = -1;
|
||||
}
|
||||
|
||||
if (!historyPath) {
|
||||
try {
|
||||
historyPath = path.join(os.homedir(), '.node_repl_history');
|
||||
} catch (err) {
|
||||
_writeToOutput(repl, '\nError: Could not get the home directory.\n' +
|
||||
'REPL session history will not be persisted.\n');
|
||||
|
||||
debug(err.stack);
|
||||
repl._historyPrev = _replHistoryMessage;
|
||||
return ready(null, repl);
|
||||
}
|
||||
}
|
||||
|
||||
if (permission.isEnabled() && permission.has('fs.write', historyPath) === false) {
|
||||
_writeToOutput(repl, '\nAccess to FileSystemWrite is restricted.\n' +
|
||||
'REPL session history will not be persisted.\n');
|
||||
return ready(null, repl);
|
||||
}
|
||||
|
||||
let timer = null;
|
||||
let writing = false;
|
||||
let pending = false;
|
||||
repl.pause();
|
||||
// History files are conventionally not readable by others:
|
||||
// https://github.com/nodejs/node/issues/3392
|
||||
// https://github.com/nodejs/node/pull/3394
|
||||
fs.open(historyPath, 'a+', 0o0600, oninit);
|
||||
|
||||
function oninit(err, hnd) {
|
||||
if (err) {
|
||||
// Cannot open history file.
|
||||
// Don't crash, just don't persist history.
|
||||
_writeToOutput(repl, '\nError: Could not open history file.\n' +
|
||||
'REPL session history will not be persisted.\n');
|
||||
debug(err.stack);
|
||||
|
||||
repl._historyPrev = _replHistoryMessage;
|
||||
repl.resume();
|
||||
return ready(null, repl);
|
||||
}
|
||||
fs.close(hnd, onclose);
|
||||
}
|
||||
|
||||
function onclose(err) {
|
||||
if (err) {
|
||||
return ready(err);
|
||||
}
|
||||
fs.readFile(historyPath, 'utf8', onread);
|
||||
}
|
||||
|
||||
function onread(err, data) {
|
||||
if (err) {
|
||||
return ready(err);
|
||||
initialize(onReadyCallback) {
|
||||
// Empty string disables persistent history
|
||||
if (this[kHistoryPath] === '') {
|
||||
// Save a reference to the context's original _historyPrev
|
||||
this.historyPrev = this[kContext]._historyPrev;
|
||||
this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this);
|
||||
return onReadyCallback(null, this[kContext]);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
repl.history = RegExpPrototypeSymbolSplit(/\r?\n+/, data, repl.historySize);
|
||||
} else {
|
||||
repl.history = [];
|
||||
const resolvedPath = this[kResolveHistoryPath]();
|
||||
if (!resolvedPath) {
|
||||
ReplHistory[kWriteToOutput](
|
||||
this[kContext],
|
||||
'\nError: Could not get the home directory.\n' +
|
||||
'REPL session history will not be persisted.\n',
|
||||
);
|
||||
|
||||
// Save a reference to the context's original _historyPrev
|
||||
this.historyPrev = this[kContext]._historyPrev;
|
||||
this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this);
|
||||
return onReadyCallback(null, this[kContext]);
|
||||
}
|
||||
|
||||
fs.open(historyPath, 'r+', onhandle);
|
||||
}
|
||||
|
||||
function onhandle(err, hnd) {
|
||||
if (err) {
|
||||
return ready(err);
|
||||
if (!this[kHasWritePermission]()) {
|
||||
ReplHistory[kWriteToOutput](
|
||||
this[kContext],
|
||||
'\nAccess to FileSystemWrite is restricted.\n' +
|
||||
'REPL session history will not be persisted.\n',
|
||||
);
|
||||
return onReadyCallback(null, this[kContext]);
|
||||
}
|
||||
fs.ftruncate(hnd, 0, (err) => {
|
||||
repl._historyHandle = hnd;
|
||||
repl.on('line', online);
|
||||
repl.once('exit', onexit);
|
||||
|
||||
// Reading the file data out erases it
|
||||
repl.once('flushHistory', function() {
|
||||
if (!repl.closed) {
|
||||
repl.resume();
|
||||
ready(null, repl);
|
||||
}
|
||||
});
|
||||
flushHistory();
|
||||
this[kContext].pause();
|
||||
|
||||
this[kInitializeHistory](onReadyCallback).catch((err) => {
|
||||
this[kHandleHistoryInitError](err, onReadyCallback);
|
||||
});
|
||||
}
|
||||
|
||||
// ------ history listeners ------
|
||||
function online(line) {
|
||||
repl._flushing = true;
|
||||
addHistory(isMultiline, lastCommandErrored) {
|
||||
const line = this[kContext].line;
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
if (line.length === 0) return '';
|
||||
|
||||
// If the history is disabled then return the line
|
||||
if (this[kSize] === 0) return line;
|
||||
|
||||
// If the trimmed line is empty then return the line
|
||||
if (StringPrototypeTrim(line).length === 0) return line;
|
||||
|
||||
// This is necessary because each line would be saved in the history while creating
|
||||
// a new multiline, and we don't want that.
|
||||
if (isMultiline && this[kIndex] === -1) {
|
||||
ArrayPrototypeShift(this[kHistory]);
|
||||
} else if (lastCommandErrored) {
|
||||
// If the last command errored and we are trying to edit the history to fix it
|
||||
// remove the broken one from the history
|
||||
ArrayPrototypeShift(this[kHistory]);
|
||||
}
|
||||
|
||||
timer = setTimeout(flushHistory, kDebounceHistoryMS);
|
||||
}
|
||||
const normalizedLine = ReplHistory[kNormalizeLineEndings](line, '\n', '\r');
|
||||
|
||||
function flushHistory() {
|
||||
timer = null;
|
||||
if (writing) {
|
||||
pending = true;
|
||||
return;
|
||||
if (this[kHistory].length === 0 || this[kHistory][0] !== normalizedLine) {
|
||||
if (this[kRemoveHistoryDuplicates]) {
|
||||
// Remove older history line if identical to new one
|
||||
const dupIndex = ArrayPrototypeIndexOf(this[kHistory], line);
|
||||
if (dupIndex !== -1) ArrayPrototypeSplice(this[kHistory], dupIndex, 1);
|
||||
}
|
||||
|
||||
// Add the new line to the history
|
||||
ArrayPrototypeUnshift(this[kHistory], normalizedLine);
|
||||
|
||||
// Only store so many
|
||||
if (this[kHistory].length > this[kSize])
|
||||
ArrayPrototypePop(this[kHistory]);
|
||||
}
|
||||
writing = true;
|
||||
const historyData = ArrayPrototypeJoin(repl.history, '\n');
|
||||
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
|
||||
|
||||
this[kIndex] = -1;
|
||||
|
||||
const finalLine = isMultiline ? reverseString(this[kHistory][0]) : this[kHistory][0];
|
||||
|
||||
// The listener could change the history object, possibly
|
||||
// to remove the last added entry if it is sensitive and should
|
||||
// not be persisted in the history, like a password
|
||||
// Emit history event to notify listeners of update
|
||||
this[kContext].emit('history', this[kHistory]);
|
||||
|
||||
return finalLine;
|
||||
}
|
||||
|
||||
function onwritten(err, data) {
|
||||
writing = false;
|
||||
if (pending) {
|
||||
pending = false;
|
||||
online();
|
||||
} else {
|
||||
repl._flushing = Boolean(timer);
|
||||
if (!repl._flushing) {
|
||||
repl.emit('flushHistory');
|
||||
canNavigateToNext() {
|
||||
return this[kIndex] > -1 && this[kHistory].length > 0;
|
||||
}
|
||||
|
||||
navigateToNext(substringSearch) {
|
||||
if (!this.canNavigateToNext()) {
|
||||
return null;
|
||||
}
|
||||
const search = substringSearch || '';
|
||||
let index = this[kIndex] - 1;
|
||||
|
||||
while (
|
||||
index >= 0 &&
|
||||
(!StringPrototypeStartsWith(this[kHistory][index], search) ||
|
||||
this[kContext].line === this[kHistory][index])
|
||||
) {
|
||||
index--;
|
||||
}
|
||||
|
||||
this[kIndex] = index;
|
||||
|
||||
if (index === -1) {
|
||||
return search;
|
||||
}
|
||||
|
||||
return ReplHistory[kNormalizeLineEndings](this[kHistory][index], '\r', '\n');
|
||||
}
|
||||
|
||||
canNavigateToPrevious() {
|
||||
return this[kHistory].length !== this[kIndex] && this[kHistory].length > 0;
|
||||
}
|
||||
|
||||
navigateToPrevious(substringSearch = '') {
|
||||
if (!this.canNavigateToPrevious()) {
|
||||
return null;
|
||||
}
|
||||
const search = substringSearch || '';
|
||||
let index = this[kIndex] + 1;
|
||||
|
||||
while (
|
||||
index < this[kHistory].length &&
|
||||
(!StringPrototypeStartsWith(this[kHistory][index], search) ||
|
||||
this[kContext].line === this[kHistory][index])
|
||||
) {
|
||||
index++;
|
||||
}
|
||||
|
||||
this[kIndex] = index;
|
||||
|
||||
if (index === this[kHistory].length) {
|
||||
return search;
|
||||
}
|
||||
|
||||
return ReplHistory[kNormalizeLineEndings](this[kHistory][index], '\r', '\n');
|
||||
}
|
||||
|
||||
get size() { return this[kSize]; }
|
||||
get isFlushing() { return this[kIsFlushing]; }
|
||||
get history() { return this[kHistory]; }
|
||||
set history(value) { this[kHistory] = value; }
|
||||
get index() { return this[kIndex]; }
|
||||
set index(value) { this[kIndex] = value; }
|
||||
|
||||
// Start private methods
|
||||
|
||||
static [kGetHistoryPath](options) {
|
||||
let historyPath = options.filePath;
|
||||
if (typeof historyPath === 'string') {
|
||||
historyPath = StringPrototypeTrim(historyPath);
|
||||
}
|
||||
return historyPath;
|
||||
}
|
||||
|
||||
static [kNormalizeLineEndings](line, from, to) {
|
||||
// Multiline history entries are saved reversed
|
||||
// History is structured with the newest entries at the top
|
||||
// and the oldest at the bottom. Multiline histories, however, only occupy
|
||||
// one line in the history file. When loading multiline history with
|
||||
// an old node binary, the history will be saved in the old format.
|
||||
// This is why we need to reverse the multilines.
|
||||
// Reversing the multilines is necessary when adding / editing and displaying them
|
||||
return reverseString(line, from, to);
|
||||
}
|
||||
|
||||
static [kWriteToOutput](context, message) {
|
||||
if (typeof context._writeToOutput === 'function') {
|
||||
context._writeToOutput(message);
|
||||
if (typeof context._refreshLine === 'function') {
|
||||
context._refreshLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onexit() {
|
||||
if (repl._flushing) {
|
||||
repl.once('flushHistory', onexit);
|
||||
[kResolveHistoryPath]() {
|
||||
if (!this[kHistoryPath]) {
|
||||
try {
|
||||
this[kHistoryPath] = path.join(os.homedir(), '.node_repl_history');
|
||||
return this[kHistoryPath];
|
||||
} catch (err) {
|
||||
debug(err.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return this[kHistoryPath];
|
||||
}
|
||||
|
||||
[kHasWritePermission]() {
|
||||
return !(permission.isEnabled() &&
|
||||
permission.has('fs.write', this[kHistoryPath]) === false);
|
||||
}
|
||||
|
||||
[kValidateOptions](options) {
|
||||
if (typeof options.history !== 'undefined') {
|
||||
validateArray(options.history, 'history');
|
||||
}
|
||||
if (typeof options.size !== 'undefined') {
|
||||
validateNumber(options.size, 'size', 0);
|
||||
}
|
||||
}
|
||||
|
||||
async [kInitializeHistory](onReadyCallback) {
|
||||
try {
|
||||
// Open and close file first to ensure it exists
|
||||
// History files are conventionally not readable by others
|
||||
// 0o0600 = read/write for owner only
|
||||
const hnd = await fs.promises.open(this[kHistoryPath], 'a+', 0o0600);
|
||||
await hnd.close();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await fs.promises.readFile(this[kHistoryPath], 'utf8');
|
||||
} catch (err) {
|
||||
return this[kHandleHistoryInitError](err, onReadyCallback);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
this[kHistory] = RegExpPrototypeSymbolSplit(/\r?\n+/, data, this[kSize]);
|
||||
} else {
|
||||
this[kHistory] = [];
|
||||
}
|
||||
|
||||
validateArray(this[kHistory], 'history');
|
||||
|
||||
const handle = await fs.promises.open(this[kHistoryPath], 'r+');
|
||||
this[kHistoryHandle] = handle;
|
||||
|
||||
await handle.truncate(0);
|
||||
|
||||
this[kContext].on('line', this[kOnLine].bind(this));
|
||||
this[kContext].once('exit', this[kOnExit].bind(this));
|
||||
|
||||
this[kContext].once('flushHistory', () => {
|
||||
if (!this[kContext].closed) {
|
||||
this[kContext].resume();
|
||||
onReadyCallback(null, this[kContext]);
|
||||
}
|
||||
});
|
||||
|
||||
await this[kFlushHistory]();
|
||||
} catch (err) {
|
||||
return this[kHandleHistoryInitError](err, onReadyCallback);
|
||||
}
|
||||
}
|
||||
|
||||
[kHandleHistoryInitError](err, onReadyCallback) {
|
||||
// Cannot open history file.
|
||||
// Don't crash, just don't persist history.
|
||||
ReplHistory[kWriteToOutput](
|
||||
this[kContext],
|
||||
'\nError: Could not open history file.\n' +
|
||||
'REPL session history will not be persisted.\n',
|
||||
);
|
||||
debug(err.stack);
|
||||
|
||||
// Save a reference to the context's original _historyPrev
|
||||
this.historyPrev = this[kContext]._historyPrev;
|
||||
this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this);
|
||||
this[kContext].resume();
|
||||
return onReadyCallback(null, this[kContext]);
|
||||
}
|
||||
|
||||
[kOnLine]() {
|
||||
this[kIsFlushing] = true;
|
||||
|
||||
if (this[kTimer]) {
|
||||
clearTimeout(this[kTimer]);
|
||||
}
|
||||
|
||||
this[kTimer] = setTimeout(() => this[kFlushHistory](), kDebounceHistoryMS);
|
||||
}
|
||||
|
||||
async [kFlushHistory]() {
|
||||
this[kTimer] = null;
|
||||
if (this[kWriting]) {
|
||||
this[kPending] = true;
|
||||
return;
|
||||
}
|
||||
repl.off('line', online);
|
||||
fs.close(repl._historyHandle, noop);
|
||||
|
||||
this[kWriting] = true;
|
||||
const historyData = ArrayPrototypeJoin(this[kHistory], '\n');
|
||||
|
||||
try {
|
||||
await this[kHistoryHandle].write(historyData, 0, 'utf8');
|
||||
this[kWriting] = false;
|
||||
|
||||
if (this[kPending]) {
|
||||
this[kPending] = false;
|
||||
this[kOnLine]();
|
||||
} else {
|
||||
this[kIsFlushing] = Boolean(this[kTimer]);
|
||||
if (!this[kIsFlushing]) {
|
||||
this[kContext].emit('flushHistory');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this[kWriting] = false;
|
||||
debug('Error writing history file:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async [kOnExit]() {
|
||||
if (this[kIsFlushing]) {
|
||||
this[kContext].once('flushHistory', this[kOnExit].bind(this));
|
||||
return;
|
||||
}
|
||||
this[kContext].off('line', this[kOnLine].bind(this));
|
||||
|
||||
if (this[kHistoryHandle] !== null) {
|
||||
try {
|
||||
await this[kHistoryHandle].close();
|
||||
} catch (err) {
|
||||
debug('Error closing history file:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[kReplHistoryMessage]() {
|
||||
if (this[kHistory].length === 0) {
|
||||
ReplHistory[kWriteToOutput](
|
||||
this[kContext],
|
||||
'\nPersistent history support disabled. ' +
|
||||
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
|
||||
'a valid, user-writable path to enable.\n',
|
||||
);
|
||||
}
|
||||
// First restore the original method on the context
|
||||
this[kContext]._historyPrev = this.historyPrev;
|
||||
// Then call it with the correct context
|
||||
return this[kContext]._historyPrev();
|
||||
}
|
||||
}
|
||||
|
||||
function _replHistoryMessage() {
|
||||
if (this.history.length === 0) {
|
||||
_writeToOutput(
|
||||
this,
|
||||
'\nPersistent history support disabled. ' +
|
||||
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
|
||||
'a valid, user-writable path to enable.\n',
|
||||
);
|
||||
}
|
||||
this._historyPrev = Interface.prototype._historyPrev;
|
||||
return this._historyPrev();
|
||||
}
|
||||
module.exports = {
|
||||
ReplHistory,
|
||||
};
|
||||
|
||||
18
lib/repl.js
18
lib/repl.js
@ -186,7 +186,6 @@ const {
|
||||
stopSigintWatchdog,
|
||||
} = internalBinding('contextify');
|
||||
|
||||
const history = require('internal/repl/history');
|
||||
const {
|
||||
extensionFormatMap,
|
||||
} = require('internal/modules/esm/formats');
|
||||
@ -787,6 +786,8 @@ function REPLServer(prompt,
|
||||
[text, self.editorMode ? self.completeOnEditorMode(cb) : cb]);
|
||||
}
|
||||
|
||||
// All the parameters in the object are defining the "input" param of the
|
||||
// InterfaceConstructor.
|
||||
ReflectApply(Interface, this, [{
|
||||
input: options.input,
|
||||
output: options.output,
|
||||
@ -1075,8 +1076,17 @@ function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) {
|
||||
prompt, source, eval_, useGlobal, ignoreUndefined, replMode);
|
||||
}
|
||||
|
||||
REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) {
|
||||
history(this, historyFile, cb);
|
||||
REPLServer.prototype.setupHistory = function setupHistory(historyConfig = {}, cb) {
|
||||
// TODO(puskin94): necessary because historyConfig can be a string for backwards compatibility
|
||||
const options = typeof historyConfig === 'string' ?
|
||||
{ filePath: historyConfig } :
|
||||
historyConfig;
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
options.onHistoryFileLoaded = cb;
|
||||
}
|
||||
|
||||
this.setupHistoryManager(options);
|
||||
};
|
||||
|
||||
REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
|
||||
@ -1084,7 +1094,7 @@ REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
|
||||
};
|
||||
|
||||
REPLServer.prototype.close = function close() {
|
||||
if (this.terminal && this._flushing && !this._closingOnFlush) {
|
||||
if (this.terminal && this.historyManager.isFlushing && !this._closingOnFlush) {
|
||||
this._closingOnFlush = true;
|
||||
this.once('flushHistory', () =>
|
||||
ReflectApply(Interface.prototype.close, this, []),
|
||||
|
||||
@ -242,7 +242,7 @@ function runTest(assertCleaned) {
|
||||
}
|
||||
|
||||
repl.once('close', () => {
|
||||
if (repl._flushing) {
|
||||
if (repl.historyManager.isFlushing) {
|
||||
repl.once('flushHistory', onClose);
|
||||
return;
|
||||
}
|
||||
|
||||
281
test/parallel/test-repl-programmatic-history-setup-history.js
Normal file
281
test/parallel/test-repl-programmatic-history-setup-history.js
Normal file
@ -0,0 +1,281 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const stream = require('stream');
|
||||
const REPL = require('repl');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
if (process.env.TERM === 'dumb') {
|
||||
common.skip('skipping - dumb terminal');
|
||||
}
|
||||
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
tmpdir.refresh();
|
||||
|
||||
// Mock os.homedir()
|
||||
os.homedir = function() {
|
||||
return tmpdir.path;
|
||||
};
|
||||
|
||||
// Create an input stream specialized for testing an array of actions
|
||||
class ActionStream extends stream.Stream {
|
||||
run(data) {
|
||||
const _iter = data[Symbol.iterator]();
|
||||
const doAction = () => {
|
||||
const next = _iter.next();
|
||||
if (next.done) {
|
||||
// Close the repl. Note that it must have a clean prompt to do so.
|
||||
setImmediate(() => {
|
||||
this.emit('keypress', '', { ctrl: true, name: 'd' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
const action = next.value;
|
||||
|
||||
if (typeof action === 'object') {
|
||||
this.emit('keypress', '', action);
|
||||
} else {
|
||||
this.emit('data', action);
|
||||
}
|
||||
setImmediate(doAction);
|
||||
};
|
||||
doAction();
|
||||
}
|
||||
resume() {}
|
||||
pause() {}
|
||||
}
|
||||
ActionStream.prototype.readable = true;
|
||||
|
||||
|
||||
// Mock keys
|
||||
const UP = { name: 'up' };
|
||||
const DOWN = { name: 'down' };
|
||||
const ENTER = { name: 'enter' };
|
||||
const CLEAR = { ctrl: true, name: 'u' };
|
||||
|
||||
// File paths
|
||||
const historyFixturePath = fixtures.path('.node_repl_history');
|
||||
const historyPath = tmpdir.resolve('.fixture_copy_repl_history');
|
||||
const historyPathFail = fixtures.path('nonexistent_folder', 'filename');
|
||||
const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
|
||||
const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file');
|
||||
const devNullHistoryPath = tmpdir.resolve('.dev-null-repl-history-file');
|
||||
// Common message bits
|
||||
const prompt = '> ';
|
||||
const replDisabled = '\nPersistent history support disabled. Set the ' +
|
||||
'NODE_REPL_HISTORY environment\nvariable to a valid, ' +
|
||||
'user-writable path to enable.\n';
|
||||
const homedirErr = '\nError: Could not get the home directory.\n' +
|
||||
'REPL session history will not be persisted.\n';
|
||||
const replFailedRead = '\nError: Could not open history file.\n' +
|
||||
'REPL session history will not be persisted.\n';
|
||||
|
||||
const tests = [
|
||||
// Makes sure that, if the history file is empty, the history is disabled
|
||||
{
|
||||
env: { NODE_REPL_HISTORY: '' },
|
||||
test: [UP],
|
||||
expected: [prompt, replDisabled, prompt]
|
||||
},
|
||||
// Makes sure that, if the history file is empty (when trimmed), the history is disabled
|
||||
{
|
||||
env: { NODE_REPL_HISTORY: ' ' },
|
||||
test: [UP],
|
||||
expected: [prompt, replDisabled, prompt]
|
||||
},
|
||||
// Properly loads the history file
|
||||
{
|
||||
env: { NODE_REPL_HISTORY: historyPath },
|
||||
test: [UP, CLEAR],
|
||||
expected: [prompt, `${prompt}'you look fabulous today'`, prompt]
|
||||
},
|
||||
// Properly navigates newly added history items
|
||||
{
|
||||
env: {},
|
||||
test: [UP, '21', ENTER, "'42'", ENTER],
|
||||
expected: [
|
||||
prompt,
|
||||
'2', '1', '21\n', prompt,
|
||||
"'", '4', '2', "'", "'42'\n", prompt,
|
||||
],
|
||||
clean: false
|
||||
},
|
||||
{ // Requires the above test case, because navigating old history
|
||||
env: {},
|
||||
test: [UP, UP, UP, DOWN, ENTER],
|
||||
expected: [
|
||||
prompt,
|
||||
`${prompt}'42'`,
|
||||
`${prompt}21`,
|
||||
prompt,
|
||||
`${prompt}21`,
|
||||
'21\n',
|
||||
prompt,
|
||||
]
|
||||
},
|
||||
// Making sure that only the configured number of history items are kept
|
||||
{
|
||||
env: { NODE_REPL_HISTORY: historyPath,
|
||||
NODE_REPL_HISTORY_SIZE: 1 },
|
||||
test: [UP, UP, DOWN, CLEAR],
|
||||
expected: [
|
||||
prompt,
|
||||
`${prompt}'you look fabulous today'`,
|
||||
prompt,
|
||||
`${prompt}'you look fabulous today'`,
|
||||
prompt,
|
||||
]
|
||||
},
|
||||
// Making sure that the history file is not written to if it is not writable
|
||||
{
|
||||
env: { NODE_REPL_HISTORY: historyPathFail,
|
||||
NODE_REPL_HISTORY_SIZE: 1 },
|
||||
test: [UP],
|
||||
expected: [prompt, replFailedRead, prompt, replDisabled, prompt]
|
||||
},
|
||||
// Checking the history file permissions
|
||||
{
|
||||
before: function before() {
|
||||
if (common.isWindows) {
|
||||
const execSync = require('child_process').execSync;
|
||||
execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`, (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath },
|
||||
test: [UP],
|
||||
expected: [prompt]
|
||||
},
|
||||
// Checking failures when os.homedir() fails
|
||||
{
|
||||
before: function before() {
|
||||
// Mock os.homedir() failure
|
||||
os.homedir = function() {
|
||||
throw new Error('os.homedir() failure');
|
||||
};
|
||||
},
|
||||
env: {},
|
||||
test: [UP],
|
||||
expected: [prompt, homedirErr, prompt, replDisabled, prompt]
|
||||
},
|
||||
// Checking that the history file can be set to /dev/null
|
||||
{
|
||||
before: function before() {
|
||||
if (!common.isWindows)
|
||||
fs.symlinkSync('/dev/null', devNullHistoryPath);
|
||||
},
|
||||
env: { NODE_REPL_HISTORY: devNullHistoryPath },
|
||||
test: [UP],
|
||||
expected: [prompt]
|
||||
},
|
||||
];
|
||||
const numtests = tests.length;
|
||||
|
||||
|
||||
function cleanupTmpFile() {
|
||||
try {
|
||||
// Write over the file, clearing any history
|
||||
fs.writeFileSync(defaultHistoryPath, '');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return true;
|
||||
throw err;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copy our fixture to the tmp directory
|
||||
fs.createReadStream(historyFixturePath)
|
||||
.pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest());
|
||||
|
||||
const runTestWrap = common.mustCall(runTest, numtests);
|
||||
|
||||
function runTest(assertCleaned) {
|
||||
const opts = tests.shift();
|
||||
if (!opts) return; // All done
|
||||
|
||||
if (assertCleaned) {
|
||||
try {
|
||||
assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), '');
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') {
|
||||
console.error(`Failed test # ${numtests - tests.length}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const test = opts.test;
|
||||
const expected = opts.expected;
|
||||
const clean = opts.clean;
|
||||
const before = opts.before;
|
||||
const size = opts.env.NODE_REPL_HISTORY_SIZE;
|
||||
const filePath = opts.env.NODE_REPL_HISTORY;
|
||||
|
||||
if (before) before();
|
||||
|
||||
const repl = REPL.start({
|
||||
input: new ActionStream(),
|
||||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
const output = chunk.toString();
|
||||
|
||||
// Ignore escapes and blank lines
|
||||
if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output))
|
||||
return next();
|
||||
|
||||
try {
|
||||
assert.strictEqual(output, expected.shift());
|
||||
} catch (err) {
|
||||
console.error(`Failed test # ${numtests - tests.length}`);
|
||||
throw err;
|
||||
}
|
||||
next();
|
||||
}
|
||||
}),
|
||||
prompt: prompt,
|
||||
useColors: false,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
repl.setupHistory({
|
||||
size,
|
||||
filePath,
|
||||
onHistoryFileLoaded,
|
||||
removeHistoryDuplicates: false
|
||||
});
|
||||
|
||||
function onHistoryFileLoaded(err, repl) {
|
||||
if (err) {
|
||||
console.error(`Failed test # ${numtests - tests.length}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
repl.once('close', () => {
|
||||
if (repl.historyManager.isFlushing) {
|
||||
repl.once('flushHistory', onClose);
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
});
|
||||
|
||||
function onClose() {
|
||||
const cleaned = clean === false ? false : cleanupTmpFile();
|
||||
|
||||
try {
|
||||
// Ensure everything that we expected was output
|
||||
assert.strictEqual(expected.length, 0);
|
||||
setImmediate(runTestWrap, cleaned);
|
||||
} catch (err) {
|
||||
console.error(`Failed test # ${numtests - tests.length}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
repl.inputStream.run(test);
|
||||
}
|
||||
}
|
||||
@ -204,7 +204,7 @@ function runTest(assertCleaned) {
|
||||
const clean = opts.clean;
|
||||
const before = opts.before;
|
||||
const historySize = opts.env.NODE_REPL_HISTORY_SIZE;
|
||||
const historyFile = opts.env.NODE_REPL_HISTORY;
|
||||
const file = opts.env.NODE_REPL_HISTORY;
|
||||
|
||||
if (before) before();
|
||||
|
||||
@ -230,17 +230,17 @@ function runTest(assertCleaned) {
|
||||
prompt: prompt,
|
||||
useColors: false,
|
||||
terminal: true,
|
||||
historySize: historySize
|
||||
historySize
|
||||
});
|
||||
|
||||
repl.setupHistory(historyFile, function(err, repl) {
|
||||
repl.setupHistory(file, function(err, repl) {
|
||||
if (err) {
|
||||
console.error(`Failed test # ${numtests - tests.length}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
repl.once('close', () => {
|
||||
if (repl._flushing) {
|
||||
if (repl.historyManager.isFlushing) {
|
||||
repl.once('flushHistory', onClose);
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user