moleculer-repl
Version:
REPL module for Moleculer
227 lines (189 loc) • 6.2 kB
JavaScript
/*
* moleculer-repl
* Copyright (c) 2026 MoleculerJS (https://github.com/moleculerjs/moleculer-repl)
* MIT Licensed
*/
;
require("v8"); // Load first. It won't work in `info.js`
const _ = require("lodash");
const { table, getBorderCharacters } = require("table");
const kleur = require("kleur");
const clui = require("clui");
const { homedir } = require("os");
const { join } = require("path");
const nodeRepl = require("repl");
const { parseArgsStringToArgv } = require("string-argv");
const { parser } = require("./args-parser");
const { autocompleteHandler, getAvailableCommands } = require("./autocomplete");
const registerCommands = require("./commands");
const commander = require("commander");
const program = new commander.Command();
program.exitOverride();
program.allowUnknownOption(true);
program.showHelpAfterError(true);
program.showSuggestionAfterError(true);
/**
* @typedef {import("moleculer").ServiceBroker} ServiceBroker
*/
/**
* Start REPL mode
*
* @param {ServiceBroker} broker
* @param {REPLOptions} opts
*/
/* istanbul ignore next */
function REPL(broker, opts) {
if (Array.isArray(opts)) opts = { customCommands: opts };
opts = _.defaultsDeep(opts || {}, {
customCommands: null,
delimiter: "mol $"
});
// Attach the commands to the program
registerCommands(program, broker);
// Attach user defined commands
if (Array.isArray(opts.customCommands)) {
const availableCommands = getAvailableCommands(program);
opts.customCommands.forEach(def => {
if (availableCommands.includes(def.command)) {
broker.logger.warn(`Command called '${def.command}' already exists. Skipping...`);
return;
}
try {
registerCustomCommands(broker, program, def);
} catch (error) {
broker.logger.error(
`An error ocurred while registering '${def.command}' command`,
error
);
}
});
}
// Start the server
const replServer = nodeRepl.start({
prompt: opts.delimiter.endsWith(" ") ? opts.delimiter : opts.delimiter + " ", // Add empty space
completer: line => autocompleteHandler(line, broker, program),
eval: evaluator
});
// Setup history
replServer.setupHistory(join(homedir(), ".moleculer_repl_history"), (err, repl) => {
if (err) broker.logger.error("Failed to initialize Moleculer REPL history", err);
});
// Caught on "Ctrl+D"
replServer.on("exit", async () => {
await broker.stop();
process.exit(0);
});
// Attach broker to REPL's context
replServer.context.broker = broker;
return replServer;
}
/**
* Node.js custom evaluation function
* More info: https://nodejs.org/api/repl.html#custom-evaluation-functions
*
* @param {String} cmd
* @param {import("vm").Context} context
* @param {String} filename
* @param {Function} callback
*/
async function evaluator(cmd, context, filename, callback) {
/** @type {import('moleculer').ServiceBroker} */
const broker = this.context.broker;
const argv = parseArgsStringToArgv(cmd, "node", "Moleculer REPL");
if (argv.length !== 2) {
try {
await program.parseAsync(argv);
} catch (error) {
if (
error.code !== "commander.helpDisplayed" &&
error.code !== "commander.unknownCommand" &&
error.code !== "commander.unknownOption" &&
error.code !== "commander.help" &&
error.code !== "commander.missingArgument"
) {
broker.logger.error(error);
}
}
}
callback(null);
}
/**
* Registers user defined commands
* @param {import('moleculer').ServiceBroker} broker
* @param {import("commander").Command} program Commander
* @param {Partial<CustomCommand>} def Command definition
*/
function registerCustomCommands(broker, program, def) {
const cmd = program.command(def.command);
if (def.description) cmd.description(def.description);
if (def.alias) {
if (!Array.isArray(def.alias)) def.alias = [def.alias];
def.alias.forEach(alias => cmd.alias(alias));
}
if (def.allowUnknownOptions) {
cmd.allowUnknownOption(def.allowUnknownOptions);
cmd.allowExcessArguments(def.allowUnknownOptions);
}
if (def.parse) {
// Use custom parser
cmd.hook("preAction", thisCommand => {
def.parse.call(parser, thisCommand);
});
} else {
cmd.hook("preAction", /** @param {any} thisCommand */ (thisCommand) => {
const parsedOpts = thisCommand.parseOptions(thisCommand.args);
let values = {};
if (parsedOpts.operands.length > 0) {
for (let i = 0; i < parsedOpts.operands.length; i++) {
const arg = thisCommand._args[i];
if (arg) {
values[arg._name] = parsedOpts.operands[i];
}
}
}
let parsedArgs = {
...parser(parsedOpts.unknown), // Other params
...thisCommand._optionValues // Contains commander.js flag values
};
const rawCommand = thisCommand.parent.rawArgs.slice(2).join(" ");
// Set the params
thisCommand.params = {
options: parsedArgs,
rawCommand,
...values
};
});
}
if (def.options) {
def.options.forEach(opt => {
cmd.option(opt.option, opt.description);
});
}
cmd.action(async function () {
// Clear the parsed values for next execution
/** @type {any} */ (this)._optionValues = {};
const helpers = { cmd, table, kleur, clui, getBorderCharacters };
return def.action(broker, /** @type {any} */ (this).params, helpers);
});
}
module.exports = REPL;
/**
* @typedef CommandOptions
* @property {String} option Command option. More info: https://github.com/tj/commander.js/#options
* @property {String} description Option description
*/
/**
* @typedef CustomCommand Custom command definition
* @property {String} command Command declaration
* @property {String?} description Command description
* @property {Array<String> | String | null} alias Command alias
* @property {Boolean?} allowUnknownOptions Allow unknown command options
* @property {Function?} parse Custom params parser
* @property {Array<CommandOptions>} options Command options
* @property {(broker: ServiceBroker, args: unknown, helpers: unknown) => Promise<void>} action Custom command handler
*/
/**
* @typedef REPLOptions REPL Options
* @property {string|null=} delimiter REPL delimiter
* @property {Array<CustomCommand>|CustomCommand|null=} customCommands Custom commands
*/