UNPKG

@adonisjs/repl

Version:
307 lines (306 loc) 8.21 kB
// src/repl.ts import stringWidth from "string-width"; import useColors from "@poppinss/colors"; import { inspect, promisify as utilPromisify } from "util"; import { Recoverable, start as startRepl } from "repl"; var GLOBAL_NODE_PROPERTIES = [ "performance", "global", "clearInterval", "clearTimeout", "setInterval", "setTimeout", "queueMicrotask", "clearImmediate", "setImmediate", "structuredClone", "atob", "btoa", "fetch", "crypto", "navigator" ]; var TS_UTILS_HELPERS = [ "__extends", "__assign", "__rest", "__decorate", "__param", "__esDecorate", "__runInitializers", "__propKey", "__setFunctionName", "__metadata", "__awaiter", "__generator", "__exportStar", "__createBinding", "__values", "__read", "__spread", "__spreadArrays", "__spreadArray", "__await", "__asyncGenerator", "__asyncDelegator", "__asyncValues", "__makeTemplateObject", "__importStar", "__importDefault", "__classPrivateFieldGet", "__classPrivateFieldSet", "__classPrivateFieldIn" ]; var Repl = class { #replOptions; /** * Length of the longest custom method name. We need to show a * symmetric view of custom methods and their description */ #longestCustomMethodName = 0; /** * Reference to the original `eval` method of the repl server. * Since we are monkey patching it, we need a reference to it * to call it after our custom logic */ #originalEval; /** * Compiler that will transform the user input just * before evaluation */ #compiler; /** * Path to the history file */ #historyFilePath; /** * Set of registered ready callbacks */ #onReadyCallbacks = []; /** * A set of registered custom methods */ #customMethods = {}; /** * Colors reference */ colors = useColors.ansi(); /** * Reference to the repl server. Available after the `start` method * is invoked */ server; constructor(options) { const { compiler, historyFilePath, ...rest } = options || {}; this.#compiler = compiler; this.#historyFilePath = historyFilePath; this.#replOptions = rest; } /** * Registering custom methods with the server context by wrapping * them inside a function and passes the REPL server instance * to the method */ #registerCustomMethodWithContext(name) { const customMethod = this.#customMethods[name]; if (!customMethod) { return; } const handler = (...args) => customMethod.handler(this, ...args); Object.defineProperty(handler, "name", { value: customMethod.handler.name }); this.server.context[name] = handler; } /** * Setup context with default globals */ #setupContext() { this.addMethod( "clear", function clear(repl, key) { if (!key) { console.log(repl.colors.red("Define a property name to remove from the context")); } else { delete repl.server.context[key]; } repl.server.displayPrompt(); }, { description: "Clear a property from the REPL context", usage: `clear ${this.colors.gray("(propertyName)")}` } ); this.addMethod( "p", function promisify(_, fn) { return utilPromisify(fn); }, { description: 'Promisify a function. Similar to Node.js "util.promisify"', usage: `p ${this.colors.gray("(function)")}` } ); Object.keys(this.#customMethods).forEach((name) => { this.#registerCustomMethodWithContext(name); }); } /** * Find if the error is recoverable or not */ #isRecoverableError(error) { return /^(Unexpected end of input|Unexpected token|' expected)/.test(error.message); } /** * Custom eval method to execute the user code * * Basically we are monkey patching the original eval method, because * we want to: * - Compile the user code before executing it * - And also benefit from the original eval method that supports * cool features like top level await */ #eval(code, context, filename, callback) { try { const compiled = this.#compiler ? this.#compiler.compile(code, filename) : code; return this.#originalEval(compiled, context, filename, callback); } catch (error) { if (this.#isRecoverableError(error)) { callback(new Recoverable(error), null); return; } callback(error, null); } } /** * Setup history file */ #setupHistory() { if (!this.#historyFilePath) { return; } this.server.setupHistory(this.#historyFilePath, (error) => { if (!error) { return; } console.log(this.colors.red("Unable to write to the history file. Exiting")); console.error(error); process.exit(1); }); } /** * Prints the help for the context properties */ #printContextHelp() { console.log(""); console.log(this.colors.green("CONTEXT PROPERTIES/METHODS:")); const context = Object.keys(this.server.context).reduce( (result, key) => { if (!this.#customMethods[key] && !GLOBAL_NODE_PROPERTIES.includes(key) && !TS_UTILS_HELPERS.includes(key)) { result[key] = this.server.context[key]; } return result; }, {} ); console.log(inspect(context, false, 1, true)); } /** * Prints the help for the custom methods */ #printCustomMethodsHelp() { console.log(""); console.log(this.colors.green("GLOBAL METHODS:")); Object.keys(this.#customMethods).forEach((method) => { const { options } = this.#customMethods[method]; const usage = this.colors.yellow(options.usage || method); const spaces = " ".repeat(this.#longestCustomMethodName - options.width + 2); const description = this.colors.dim(options.description || ""); console.log(`${usage}${spaces}${description}`); }); } /** * Prints the context to the console */ #ls() { this.#printCustomMethodsHelp(); this.#printContextHelp(); this.server.displayPrompt(); } /** * Notify by writing to the console */ notify(message) { console.log(this.colors.yellow().italic(message)); if (this.server) { this.server.displayPrompt(); } } /** * Register a callback to be invoked once the server is ready */ ready(callback) { this.#onReadyCallbacks.push(callback); return this; } /** * Register a custom loader function to be added to the context */ addMethod(name, handler, options) { const width = stringWidth(options?.usage || name); if (width > this.#longestCustomMethodName) { this.#longestCustomMethodName = width; } this.#customMethods[name] = { handler, options: Object.assign({ width }, options) }; if (this.server) { this.#registerCustomMethodWithContext(name); } return this; } /** * Returns the collection of registered methods */ getMethods() { return this.#customMethods; } /** * Register a compiler. Make sure register the compiler before * calling the start method */ useCompiler(compiler) { this.#compiler = compiler; return this; } /** * Start the REPL server */ start(context) { console.log(""); this.notify('Type ".ls" to a view list of available context methods/properties'); this.server = startRepl({ prompt: `> ${this.#compiler?.supportsTypescript ? "(ts) " : "(js) "}`, input: process.stdin, output: process.stdout, terminal: process.stdout.isTTY && !Number.parseInt(process.env.NODE_NO_READLINE, 10), useGlobal: true, ...this.#replOptions }); if (context) { Object.keys(context).forEach((key) => { this.server.context[key] = context[key]; }); } this.server.defineCommand("ls", { help: "View list of available context methods/properties", action: this.#ls.bind(this) }); this.#setupContext(); this.#setupHistory(); this.#originalEval = this.server.eval; this.server.eval = this.#eval.bind(this); this.server.displayPrompt(); this.#onReadyCallbacks.forEach((callback) => callback(this)); return this; } }; export { Repl };