Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions lib/internal/repl/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,31 @@
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSort,
Boolean,
FunctionPrototypeBind,
FunctionPrototypeToString,
MathMin,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyNames,
ObjectGetPrototypeOf,
RegExpPrototypeExec,
SafeSet,
SafeStringIterator,
StringPrototypeIndexOf,
StringPrototypeLastIndexOf,
StringPrototypePadEnd,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeToLowerCase,
StringPrototypeTrim,
Symbol,
SymbolFor,
} = primordials;

const { sendInspectorCommand } = require('internal/util/inspector');
Expand Down Expand Up @@ -841,6 +851,224 @@ function setReplBuiltinLibs(value) {
_builtinLibs = value;
}

/**
* Creates a `help()` function for use in the REPL context.
* - `help()` -- prints REPL usage guide (commands, shortcuts)
* - `help(value)` -- introspects a value, showing type, properties, methods,
* prototype chain, and function signature info.
*
* Uses `util.styleText` to dogfood core coloring. Output goes to
* `repl.output` so it respects the terminal's color capabilities.
* @param {object} repl The REPLServer instance.
* @returns {Function} The `help` function to be injected into the context.
*/
function createREPLHelp(repl) {
const { styleText } = require('util');
const kStyleOpts = { __proto__: null, validateStream: false };

function style(color, text) {
if (!repl.useColors) return `${text}`;
return styleText(color, `${text}`, kStyleOpts);
}

function help(value) {
const lines = [];

if (arguments.length === 0) {
// No argument: show REPL guide.
ArrayPrototypePush(lines,
style('bold', 'Node.js REPL Help'),
'',
style('yellow', 'Commands:'),
);

const cmds = repl.commands;
const names = ArrayPrototypeSort(ObjectGetOwnPropertyNames(cmds));
for (let i = 0; i < names.length; i++) {
const name = names[i];
const cmd = cmds[name];
const desc = cmd.help || '';
ArrayPrototypePush(lines,
` ${style('green', `.${StringPrototypePadEnd(name, 12)}`)}${desc}`);
}

ArrayPrototypePush(lines,
'',
style('yellow', 'Shortcuts:'),
` ${style('green', 'Ctrl+C ')}Abort current expression`,
` ${style('green', 'Ctrl+D ')}Exit the REPL`,
` ${style('green', 'Ctrl+L ')}Clear the screen`,
` ${style('green', 'Tab ')}Autocomplete`,
` ${style('green', 'Up / Down ')}Navigate history`,
` ${style('green', '_ ')}Last evaluated result`,
` ${style('green', '_error ')}Last thrown error`,
'',
style('yellow', 'Usage:'),
` ${style('cyan', 'help(value)')} Inspect any value`,
` ${style('cyan', 'help(fn)')} Show function signature`,
` ${style('cyan', 'help(obj)')} List object properties`,
);

repl.output.write(ArrayPrototypeJoin(lines, '\n') + '\n');
return undefined;
}

// --- Introspect a value ---

const type = value === null ? 'null' : typeof value;

// Header: type and constructor info.
if (type === 'function') {
const fnStr = FunctionPrototypeToString(value);
const isAsync = RegExpPrototypeExec(/^async\s/, fnStr) !== null;
const isGenerator = RegExpPrototypeExec(/^(async\s+)?function\s*\*/, fnStr) !== null;
const isClass = RegExpPrototypeExec(/^class[\s{]/, fnStr) !== null;

let kind = 'Function';
if (isClass) kind = 'Class';
else if (isAsync && isGenerator) kind = 'AsyncGeneratorFunction';
else if (isAsync) kind = 'AsyncFunction';
else if (isGenerator) kind = 'GeneratorFunction';

ArrayPrototypePush(lines,
`${style('yellow', kind)}: ${style('bold', value.name || '(anonymous)')}`);

// Show parameter info from function source.
const match = RegExpPrototypeExec(
/(?:^(?:async\s+)?(?:function\s*\*?\s*)?(\w*)\s*\(([^)]*)\)|^(\w*)\s*=>\s*)/,
fnStr,
);
if (match) {
const params = (match[2] || '').trim();
if (params) {
ArrayPrototypePush(lines,
` ${style('gray', 'Parameters:')} ${style('cyan', params)}`);
}
}

ArrayPrototypePush(lines,
` ${style('gray', 'Length:')} ${value.length}`);

if (value.prototype !== undefined) {
const protoMethods = [];
try {
const protoNames = ObjectGetOwnPropertyNames(value.prototype);
for (let i = 0; i < protoNames.length; i++) {
const pn = protoNames[i];
if (pn === 'constructor') continue;
const desc = ObjectGetOwnPropertyDescriptor(value.prototype, pn);
if (desc && typeof desc.value === 'function') {
ArrayPrototypePush(protoMethods, pn);
}
}
} catch {
// Some prototypes throw on access; skip.
}
if (protoMethods.length > 0) {
ArrayPrototypePush(lines,
` ${style('gray', 'Prototype methods:')}`,
` ${style('magenta', ArrayPrototypeJoin(ArrayPrototypeSort(protoMethods), ', '))}`);
}
}
} else if (type === 'object' || type === 'null' ||
type === 'undefined') {
if (value === null) {
ArrayPrototypePush(lines, style('yellow', 'null'));
} else if (value === undefined) {
ArrayPrototypePush(lines, style('yellow', 'undefined'));
} else {
const ctor = value.constructor?.name || 'Object';
ArrayPrototypePush(lines,
`${style('yellow', ctor)} {`);

// Categorize own properties.
const methods = [];
const gettersSetters = [];
const properties = [];

try {
const ownNames = ObjectGetOwnPropertyNames(value);
for (let i = 0; i < ownNames.length; i++) {
const name = ownNames[i];
const desc = ObjectGetOwnPropertyDescriptor(value, name);
if (!desc) continue;
if (desc.get || desc.set) {
const accessors = [];
if (desc.get) ArrayPrototypePush(accessors, 'get');
if (desc.set) ArrayPrototypePush(accessors, 'set');
ArrayPrototypePush(gettersSetters,
` ${style('green', name)} [${ArrayPrototypeJoin(accessors, '/')}]`);
} else if (typeof desc.value === 'function') {
ArrayPrototypePush(methods,
` ${style('magenta', name)}()`);
} else {
const valStr = inspect(desc.value, {
depth: 0,
colors: repl.useColors,
maxStringLength: 40,
});
ArrayPrototypePush(properties,
` ${style('cyan', name)}: ${valStr}`);
}
}
} catch {
ArrayPrototypePush(lines,
` ${style('gray', '(properties not enumerable)')}`);
}

if (methods.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Methods:'));
for (let i = 0; i < methods.length; i++) {
ArrayPrototypePush(lines, methods[i]);
}
}
if (gettersSetters.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Accessors:'));
for (let i = 0; i < gettersSetters.length; i++) {
ArrayPrototypePush(lines, gettersSetters[i]);
}
}
if (properties.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Properties:'));
for (let i = 0; i < properties.length; i++) {
ArrayPrototypePush(lines, properties[i]);
}
}

ArrayPrototypePush(lines, '}');

// Show prototype chain.
const chain = [];
let proto = ObjectGetPrototypeOf(value);
while (proto && proto !== ObjectGetPrototypeOf({})) {
const protoName = proto.constructor?.name || '(anonymous)';
ArrayPrototypePush(chain, protoName);
proto = ObjectGetPrototypeOf(proto);
}
if (chain.length > 0) {
ArrayPrototypePush(lines,
`${style('gray', 'Prototype chain:')} ${ArrayPrototypeJoin(chain, ' -> ')}`);
}
}
} else {
// Primitive: string, number, boolean, bigint, symbol.
ArrayPrototypePush(lines,
`${style('yellow', type)}: ${inspect(value, { colors: repl.useColors })}`);
}

repl.output.write(ArrayPrototypeJoin(lines, '\n') + '\n');
return undefined;
}

// Custom inspect so typing `help` alone (without parens) shows a hint.
ObjectDefineProperty(help, SymbolFor('nodejs.util.inspect.custom'), {
__proto__: null,
value: () => '[Function: help] -- call help() or help(value)',
});

return help;
}

module.exports = {
REPL_MODE_SLOPPY: Symbol('repl-sloppy'),
REPL_MODE_STRICT,
Expand All @@ -856,4 +1084,5 @@ module.exports = {
getReplBuiltinLibs,
setReplBuiltinLibs,
fixReplRequire,
createREPLHelp,
};
10 changes: 10 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const {
getReplBuiltinLibs,
setReplBuiltinLibs,
fixReplRequire,
createREPLHelp,
} = require('internal/repl/utils');
const {
complete,
Expand Down Expand Up @@ -1212,6 +1213,15 @@ class REPLServer extends Interface {

// Allow REPL extensions to extend the new context
this.emit('reset', this.context);

// Provide a `help()` function for interactive introspection.
ObjectDefineProperty(this.context, 'help', {
__proto__: null,
configurable: true,
writable: true,
enumerable: false,
value: createREPLHelp(this),
});
}
displayPrompt(preserveCursor) {
let prompt = this._initialPrompt;
Expand Down
Loading
Loading