@adonisjs/ace
Version:
Commandline apps framework used by AdonisJs
610 lines (609 loc) • 20.9 kB
JavaScript
"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;