UNPKG

@wooksjs/event-cli

Version:
348 lines (343 loc) 11.1 kB
import { CliHelpRenderer } from "@prostojs/cli-help"; import minimist from "minimist"; import { WooksAdapterBase } from "wooks"; import { createAsyncEventContext, useAsyncEventContext } from "@wooksjs/event-core"; //#region packages/event-cli/src/event-cli.ts function createCliContext(data, options) { return createAsyncEventContext({ event: { ...data, type: "CLI" }, options }); } /** * Wrapper on top of useEventContext that provides * proper context types for CLI event * @returns set of hooks { getCtx, restoreCtx, clearCtx, hookStore, getStore, setStore } */ function useCliContext() { return useAsyncEventContext("CLI"); } //#endregion //#region packages/event-cli/src/cli-adapter.ts const cliShortcuts = { cli: "CLI" }; var WooksCli = class extends WooksAdapterBase { logger; cliHelp; constructor(opts, wooks) { super(wooks, opts?.logger, opts?.router); this.opts = opts; this.logger = opts?.logger || this.getLogger(`[wooks-cli]`); this.cliHelp = opts?.cliHelp instanceof CliHelpRenderer ? opts.cliHelp : new CliHelpRenderer(opts?.cliHelp); } /** * ### Register CLI Command * Command path segments may be separated by / or space. * * For example the folowing path are interpreted the same: * - "command test use:dev :name" * - "command/test/use:dev/:name" * * Where name will become an argument * * ```js * // example without options * app.cli('command/:arg', () => 'arg = ' + useRouteParams().params.arg ) * * // example with options * app.cli('command/:arg', { * description: 'Description of the command', * options: [{ keys: ['project', 'p'], description: 'Description of the option', value: 'myProject' }], * args: { arg: 'Description of the arg' }, * aliases: ['cmd'], // alias "cmd/:arg" will be registered * examples: [{ * description: 'Example of usage with someProject', * cmd: 'argValue -p=someProject', * // will result in help display: * // "# Example of usage with someProject\n" + * // "$ myCli command argValue -p=someProject\n" * }], * handler: () => 'arg = ' + useRouteParams().params.arg * }) * ``` * * @param path command path * @param _options handler or options * * @returns */ cli(path, _options) { const options = typeof _options === "function" ? { handler: _options } : _options; const handler = typeof _options === "function" ? _options : _options.handler; const makePath = (s) => `/${s.replace(/\s+/gu, "/")}`; const targetPath = makePath(path); const routed = this.on("CLI", targetPath, handler); if (options.onRegister) options.onRegister(targetPath, 0, routed); for (const alias of options.aliases || []) { const vars = routed.getArgs().map((k) => `:${k}`).join("/"); const targetPath$1 = makePath(alias) + (vars ? `/${vars}` : ""); this.on("CLI", targetPath$1, handler); if (options.onRegister) options.onRegister(targetPath$1, 1, routed); } const command = routed.getStaticPart().replace(/\//gu, " ").trim(); const args = { ...options.args }; for (const arg of routed.getArgs()) if (!args[arg]) args[arg] = ""; this.cliHelp.addEntry({ command, aliases: options.aliases?.map((alias) => alias.replace(/\\:/gu, ":")), args, description: options.description, examples: options.examples, options: options.options, custom: { handler: options.handler, cb: options.onRegister } }); return routed; } alreadyComputedAliases = false; computeAliases() { if (!this.alreadyComputedAliases) { this.alreadyComputedAliases = true; const aliases = this.cliHelp.getComputedAliases(); for (const [alias, entry] of Object.entries(aliases)) if (entry.custom) { const vars = Object.keys(entry.args || {}).map((k) => `:${k}`).join("/"); const path = `/${alias.replace(/\s+/gu, "/").replace(/:/gu, "\\:")}${vars ? `/${vars}` : ""}`; this.on("CLI", path, entry.custom.handler); if (entry.custom.cb) entry.custom.cb(path, 3); } } } /** * ## run * ### Start command processing * Triggers command processing * * By default takes `process.argv.slice(2)` as a command * * It's possible to replace the command by passing an argument * * @param _argv optionally overwrite `process.argv.slice(2)` with your `argv` array */ async run(_argv, _opts) { const argv = _argv || process.argv.slice(2); const parsedFlags = minimist(argv, _opts); const pathParams = parsedFlags._; const path = `/${pathParams.map((v) => encodeURI(v).replace(/\//gu, "%2F")).join("/")}`; const runInContext = createCliContext({ opts: _opts, argv, pathParams, cliHelp: this.cliHelp, command: path.replace(/\//gu, " ").trim() }, this.mergeEventOptions(this.opts?.eventOptions)); return runInContext(async () => { const { store } = useCliContext(); store("flags").value = parsedFlags; this.computeAliases(); const { handlers: foundHandlers, firstStatic } = this.wooks.lookup("CLI", path); if (typeof firstStatic === "string") store("event").set("command", firstStatic.replace(/\//gu, " ").trim()); const handlers = foundHandlers || this.opts?.onNotFound && [this.opts.onNotFound] || null; if (handlers) try { for (const handler of handlers) { const response = await handler(); if (typeof response === "string") console.log(response); else if (Array.isArray(response)) response.forEach((r) => { console.log(typeof r === "string" ? r : JSON.stringify(r, null, " ")); }); else if (response instanceof Error) { this.onError(response); return response; } else if (response) console.log(JSON.stringify(response, null, " ")); } } catch (error) { this.onError(error); return error; } else { this.onUnknownCommand(pathParams); return /* @__PURE__ */ new Error("Unknown command"); } }); } onError(e) { if (this.opts?.onError) this.opts.onError(e); else { this.error(e.message); process.exit(1); } } /** * Triggers `unknown command` processing and callbacks * @param pathParams `string[]` containing command */ onUnknownCommand(pathParams) { const raiseError = () => { this.error(`Unknown command: ${pathParams.join(" ")}`); process.exit(1); }; if (this.opts?.onUnknownCommand) this.opts.onUnknownCommand(pathParams, raiseError); else raiseError(); } error(e) { if (typeof e === "string") console.error(`ERROR: ${e}`); else console.error(`ERROR: ${e.message}`); } }; /** * Factory for WooksCli App * @param opts TWooksCliOptions * @param wooks Wooks | WooksAdapterBase * @returns WooksCli */ function createCliApp(opts, wooks) { return new WooksCli(opts, wooks); } //#endregion //#region packages/event-cli/src/composables/options.ts /** * Get CLI Options * * @returns an object with CLI options */ function useCliOptions() { const { store } = useCliContext(); const flags = store("flags"); if (!flags.value) { const event = store("event"); flags.value = minimist(event.get("argv"), event.get("opts")); } return flags.value; } /** * Getter for Cli Option value * * @param name name of the option * @returns value of a CLI option */ function useCliOption(name) { try { const options = useCliHelp().getEntry().options || []; const opt = options.find((o) => o.keys.includes(name)); if (opt) { for (const key of opt.keys) if (useCliOptions()[key]) return useCliOptions()[key]; } } catch (error) {} return useCliOptions()[name]; } //#endregion //#region packages/event-cli/src/composables/cli-help.ts /** * ## useCliHelp * ### Composable * ```js * // example of printing cli instructions * const { print } = useCliHelp() * // print with colors * print(true) * // print with no colors * // print(false) * ``` * @returns */ function useCliHelp() { const event = useCliContext().store("event"); const getCliHelp = () => event.get("cliHelp"); const getEntry = () => getCliHelp().match(event.get("command")).main; return { getCliHelp, getEntry, render: (width, withColors) => getCliHelp().render(event.get("command"), width, withColors), print: (withColors) => { getCliHelp().print(event.get("command"), withColors); } }; } /** * ## useAutoHelp * ### Composable * * Prints help if `--help` option provided. * * ```js * // example of use: print help and exit * app.cli('test', () => { * useAutoHelp() && process.exit(0) * return 'hit test command' * }) * * // add option -h to print help, no colors * app.cli('test/nocolors', () => { * useAutoHelp(['help', 'h'], false) && process.exit(0) * return 'hit test nocolors command' * }) * ``` * @param keys default `['help']` - list of options to trigger help render * @param colors default `true`, prints with colors when true * @returns true when --help was provided. Otherwise returns false */ function useAutoHelp(keys = ["help"], colors = true) { for (const option of keys) if (useCliOption(option) === true) { useCliHelp().print(colors); return true; } } /** * ##useCommandLookupHelp * ### Composable * * Tries to find valid command based on provided command. * * If manages to find a valid command, throws an error * suggesting a list of valid commands * * Best to use in `onUnknownCommand` callback: * * ```js * const app = createCliApp({ * onUnknownCommand: (path, raiseError) => { * // will throw an error suggesting a list * // of valid commands if could find some * useCommandLookupHelp() * // fallback to a regular error handler * raiseError() * }, * }) * ``` * * @param lookupDepth depth of search in backwards * @example * * For provided command `run test:drive dir` * - lookup1: `run test:drive dir` (deep = 0) * - lookup2: `run test:drive` (deep = 1) * - lookup3: `run test` (deep = 2) * - lookup4: `run` (deep = 3) * ... */ function useCommandLookupHelp(lookupDepth = 3) { const parts = useCliContext().store("event").get("pathParams")?.flatMap((p) => `${p} `.split(":").map((s, i) => i ? `:${s}` : s)) || []; const cliHelp = useCliHelp().getCliHelp(); const cmd = cliHelp.getCliName(); let data; for (let i = 0; i < Math.min(parts.length, lookupDepth + 1); i++) { const pathParams = parts.slice(0, i ? -i : parts.length).join("").trim(); try { data = cliHelp.match(pathParams); break; } catch (error) { const variants = cliHelp.lookup(pathParams); if (variants.length > 0) throw new Error(`Wrong command, did you mean:\n${variants.slice(0, 7).map((c) => ` $ ${cmd} ${c.main.command}`).join("\n")}`); } } if (data) { const { main, children } = data; if (main.args && Object.keys(main.args).length > 0) throw new Error(`Arguments expected: ${Object.keys(main.args).map((l) => `<${l}>`).join(", ")}`); else if (children?.length > 0) throw new Error(`Wrong command, did you mean:\n${children.slice(0, 7).map((c) => ` $ ${cmd} ${c.command}`).join("\n")}`); } } //#endregion export { WooksCli, cliShortcuts, createCliApp, createCliContext, useAutoHelp, useCliContext, useCliHelp, useCliOption, useCliOptions, useCommandLookupHelp };