UNPKG

@adonisjs/ace

Version:

Commandline apps framework used by AdonisJs

610 lines (609 loc) 20.9 kB
"use strict"; /* * @adonisjs/ace * * (c) Harminder Virk <virk@adonisjs.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Kernel = void 0; const Hooks_1 = require("../Hooks"); const Parser_1 = require("../Parser"); const HelpCommand_1 = require("../HelpCommand"); const Exceptions_1 = require("../Exceptions"); const help_1 = require("../utils/help"); const validateCommand_1 = require("../utils/validateCommand"); const cliui_1 = require("@poppinss/cliui"); /** * Ace kernel class is used to register, find and invoke commands by * parsing `process.argv.splice(2)` value. */ class Kernel { constructor(application) { this.application = application; /** * Reference to hooks class to execute lifecycle * hooks */ this.hooks = new Hooks_1.Hooks(); /** * The state of the kernel */ this.state = 'idle'; /** * Exit handler for gracefully exiting the process */ this.exitHandler = (kernel) => { if (kernel.error && typeof kernel.error.handle === 'function') { kernel.error.handle(kernel.error); } else if (kernel.error) { cliui_1.logger.fatal(kernel.error); } process.exit(kernel.exitCode === undefined ? 0 : kernel.exitCode); }; /** * Find if CLI process is interactive. This flag can be * toggled programmatically */ this.isInteractive = cliui_1.isInteractive; /** * Find if console output is mocked */ this.isMockingConsoleOutput = false; /** * The default command that will be invoked when no command is * defined */ this.defaultCommand = HelpCommand_1.HelpCommand; /** * List of registered commands */ this.commands = {}; this.aliases = this.application.rcFile.commandsAliases; /** * List of registered flags */ this.flags = {}; } /** * Executing global flag handlers. The global flag handlers are * not async as of now, but later we can look into making them * async. */ executeGlobalFlagsHandlers(argv, command) { const globalFlags = Object.keys(this.flags); const parsedOptions = new Parser_1.Parser(this.flags).parse(argv, command); globalFlags.forEach((name) => { const value = parsedOptions[name]; /** * Flag was not specified */ if (value === undefined) { return; } /** * Calling the handler */ this.flags[name].handler(parsedOptions[name], parsedOptions, command); }); } /** * Returns an array of all registered commands */ getAllCommandsAndAliases() { let commands = Object.keys(this.commands).map((name) => this.commands[name]); let aliases = {}; /** * Concat manifest commands when they exists */ if (this.manifestLoader && this.manifestLoader.booted) { const { commands: manifestCommands, aliases: manifestAliases } = this.manifestLoader.getCommands(); commands = commands.concat(manifestCommands); aliases = Object.assign(aliases, manifestAliases); } return { commands, aliases: Object.assign(aliases, this.aliases), }; } /** * Processes the args and sets values on the command instance */ async processCommandArgsAndFlags(commandInstance, args) { const parser = new Parser_1.Parser(this.flags); const command = commandInstance.constructor; /** * Parse the command arguments. The `parse` method will raise exception if flag * or arg is not */ const parsedOptions = parser.parse(args, command); /** * We validate the command arguments after the global flags have been * executed. It is required, since flags may have nothing to do * with the validaty of command itself */ command.args.forEach((arg, index) => { parser.validateArg(arg, index, parsedOptions, command); }); /** * Creating a new command instance and setting * parsed options on it. */ commandInstance.parsed = parsedOptions; /** * Setup command instance argument and flag * properties. */ for (let i = 0; i < command.args.length; i++) { const arg = command.args[i]; const defaultValue = commandInstance[arg.propertyName]; if (arg.type === 'spread') { const value = parsedOptions._.slice(i); /** * Set the property value to arguments defined via the CLI * If no arguments are supplied, then use the default value assigned to the class property * If the default value is undefined, then assign an empty array */ commandInstance[arg.propertyName] = value.length ? value : defaultValue !== undefined ? defaultValue : []; break; } else { const value = parsedOptions._[i]; commandInstance[arg.propertyName] = value !== undefined ? value : defaultValue; } } /** * Set flag value on the command instance */ for (let flag of command.flags) { const flagValue = parsedOptions[flag.name]; if (flagValue !== undefined) { commandInstance[flag.propertyName] = flagValue; } } } /** * Execute the main command. For calling commands within commands, * one must call "kernel.exec". */ async execMain(commandName, args) { const command = await this.find([commandName]); /** * Command not found. So execute global flags handlers and * raise an exception */ if (!command) { this.executeGlobalFlagsHandlers(args); throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName)); } /** * Make an instance of the command */ const commandInstance = await this.application.container.makeAsync(command, [ this.application, this, ]); /** * Execute global flags */ this.executeGlobalFlagsHandlers(args, command); /** * Process the arguments and flags for the command */ await this.processCommandArgsAndFlags(commandInstance, args); /** * Keep a reference to the entry command. So that we know if we * want to entertain `.exit` or not */ this.entryCommand = commandInstance; /** * Execute before run hooks */ await this.hooks.execute('before', 'run', commandInstance); /** * Execute command */ return commandInstance.exec(); } /** * Handles exiting the process */ async exitProcess(error) { /** * Check for state to avoid exiting the process multiple times */ if (this.state === 'completed') { return; } this.state = 'completed'; /** * Re-assign error if entry command exists and has error */ if (!error && this.entryCommand && this.entryCommand.error) { error = this.entryCommand.error; } /** * Execute the after run hooks. Wrapping inside try/catch since this is the * cleanup handler for the process and must handle all exceptions */ try { if (this.entryCommand) { await this.hooks.execute('after', 'run', this.entryCommand); } } catch (hookError) { error = hookError; } /** * Assign error to the kernel instance */ if (error) { this.error = error; } /** * Figure out the exit code for the process */ const exitCode = error ? 1 : 0; const commandExitCode = this.entryCommand && this.entryCommand.exitCode; this.exitCode = commandExitCode === undefined ? exitCode : commandExitCode; try { await this.exitHandler(this); } catch (exitHandlerError) { cliui_1.logger.warning('Expected exit handler to exit the process. Instead it raised an exception'); cliui_1.logger.fatal(exitHandlerError); } } before(action, callback) { this.hooks.add('before', action, callback); return this; } after(action, callback) { this.hooks.add('after', action, callback); return this; } /** * Register an array of command constructors */ register(commands) { commands.forEach((command) => { command.boot(); (0, validateCommand_1.validateCommand)(command); this.commands[command.commandName] = command; /** * Registering command aliaes */ command.aliases.forEach((alias) => (this.aliases[alias] = command.commandName)); }); return this; } /** * Register a global flag. It can be defined in combination with * any command. */ flag(name, handler, options) { this.flags[name] = Object.assign({ name, propertyName: name, handler, type: 'boolean', }, options); return this; } /** * Use manifest instance to lazy load commands */ useManifest(manifestLoader) { this.manifestLoader = manifestLoader; return this; } /** * Register an exit handler */ onExit(callback) { this.exitHandler = callback; return this; } /** * Returns an array of command names suggestions for a given name. */ getSuggestions(name, distance = 3) { const leven = require('leven'); const { commands, aliases } = this.getAllCommandsAndAliases(); const suggestions = commands .filter(({ commandName }) => leven(name, commandName) <= distance) .map(({ commandName }) => commandName); return suggestions.concat(Object.keys(aliases).filter((alias) => leven(name, alias) <= distance)); } /** * Preload the manifest file. Re-running this method twice will * result in a noop */ async preloadManifest() { /** * Load manifest commands when instance of manifest loader exists. */ if (this.manifestLoader) { await this.manifestLoader.boot(); } } /** * Finds the command from the command line argv array. If command for * the given name doesn't exists, then it will return `null`. * * Does executes the before and after hooks regardless of whether the * command has been found or not */ async find(argv) { /** * ---------------------------------------------------------------------------- * Even though in `Unix` the command name may appear in between or at last, with * ace we always want the command name to be the first argument. However, the * arguments to the command itself can appear in any sequence. For example: * * Works * - node ace make:controller foo * - node ace make:controller --http foo * * Doesn't work * - node ace foo make:controller * ---------------------------------------------------------------------------- */ const [commandName] = argv; /** * Command name from the registered aliases */ const aliasCommandName = this.aliases[commandName]; /** * Manifest commands gets preference over manually registered commands. * * - We check the manifest loader is register * - The manifest loader has the command * - Or the manifest loader has the alias command */ const commandNode = this.manifestLoader ? this.manifestLoader.hasCommand(commandName) ? this.manifestLoader.getCommand(commandName) : this.manifestLoader.hasCommand(aliasCommandName) ? this.manifestLoader.getCommand(aliasCommandName) : undefined : undefined; if (commandNode) { commandNode.command.aliases = commandNode.command.aliases || []; if (aliasCommandName && !commandNode.command.aliases.includes(commandName)) { commandNode.command.aliases.push(commandName); } await this.hooks.execute('before', 'find', commandNode.command); const command = await this.manifestLoader.loadCommand(commandNode.command.commandName); await this.hooks.execute('after', 'find', command); return command; } else { /** * Try to find command inside manually registered command or fallback * to null */ const command = this.commands[commandName] || this.commands[aliasCommandName] || null; /** * Share main command name as an alias with the command */ if (command) { command.aliases = command.aliases || []; if (aliasCommandName && !command.aliases.includes(commandName)) { command.aliases.push(commandName); } } /** * Executing before and after together to be compatible * with the manifest find before and after hooks */ await this.hooks.execute('before', 'find', command); await this.hooks.execute('after', 'find', command); return command; } } /** * Run the default command. The default command doesn't accept * and args or flags. */ async runDefaultCommand() { this.defaultCommand.boot(); (0, validateCommand_1.validateCommand)(this.defaultCommand); /** * Execute before/after find hooks */ await this.hooks.execute('before', 'find', this.defaultCommand); await this.hooks.execute('after', 'find', this.defaultCommand); /** * Make the command instance using the container */ const commandInstance = await this.application.container.makeAsync(this.defaultCommand, [ this.application, this, ]); /** * Execute before run hook */ await this.hooks.execute('before', 'run', commandInstance); /** * Keep a reference to the entry command */ this.entryCommand = commandInstance; /** * Execute the command */ return commandInstance.exec(); } /** * Find if a command is the main command. Main commands are executed * directly from the terminal. */ isMain(command) { return !!this.entryCommand && this.entryCommand === command; } /** * Enforce mocking the console output. Command logs, tables, prompts * will be mocked */ mockConsoleOutput() { this.isMockingConsoleOutput = true; return this; } /** * Toggle interactive state */ interactive(state) { this.isInteractive = state; return this; } /** * Execute a command as a sub-command. Do not call "handle" and * always use this method to invoke command programatically */ async exec(commandName, args) { const command = await this.find([commandName]); /** * Command not found. */ if (!command) { throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName)); } /** * Make an instance of command and keep a reference of it as `this.entryCommand` */ const commandInstance = await this.application.container.makeAsync(command, [ this.application, this, ]); /** * Process args and flags for the command */ await this.processCommandArgsAndFlags(commandInstance, args); let commandError; /** * Wrapping the command execution inside a try/catch, so that * we can run the after hooks regardless of success or * failure */ try { await this.hooks.execute('before', 'run', commandInstance); await commandInstance.exec(); } catch (error) { commandError = error; } /** * Execute after hooks */ await this.hooks.execute('after', 'run', commandInstance); /** * Re-throw error (if any) */ if (commandError) { throw commandError; } return commandInstance; } /** * Makes instance of a given command by processing command line arguments * and setting them on the command instance */ async handle(argv) { if (this.state !== 'idle') { return; } this.state = 'running'; try { /** * Preload the manifest file to load the manifest files */ this.preloadManifest(); /** * Branch 1 * Run default command and invoke the exit handler */ if (!argv.length) { await this.runDefaultCommand(); await this.exitProcess(); return; } /** * Branch 2 * No command has been mentioned and hence execute all the global flags * invoke the exit handler */ const hasMentionedCommand = !argv[0].startsWith('-'); if (!hasMentionedCommand) { this.executeGlobalFlagsHandlers(argv); await this.exitProcess(); return; } /** * Branch 3 * Execute the given command as the main command */ const [commandName, ...args] = argv; await this.execMain(commandName, args); /** * Exit the process if there isn't any entry command */ if (!this.entryCommand) { await this.exitProcess(); return; } const entryCommandConstructor = this.entryCommand.constructor; /** * Exit the process if entry command isn't a stayalive command. Stayalive * commands should call `this.exit` to exit the process. */ if (!entryCommandConstructor.settings.stayAlive) { await this.exitProcess(); } } catch (error) { await this.exitProcess(error); } } /** * Print the help screen for a given command or all commands/flags */ printHelp(command, commandsToAppend, aliasesToAppend) { let { commands, aliases } = this.getAllCommandsAndAliases(); /** * Append additional commands and aliases for help screen only */ if (commandsToAppend) { commands = commands.concat(commandsToAppend); } if (aliasesToAppend) { aliases = Object.assign({}, aliases, aliasesToAppend); } if (command) { (0, help_1.printHelpFor)(command, aliases); } else { const flags = Object.keys(this.flags).map((name) => this.flags[name]); (0, help_1.printHelp)(commands, flags, aliases); } } /** * Trigger kernel to exit the process. The call to this method * is ignored when command is not same the `entryCommand`. * * In other words, subcommands cannot trigger exit */ async exit(command, error) { if (command !== this.entryCommand) { return; } await this.exitProcess(error); } } exports.Kernel = Kernel;