UNPKG

20.2 kBJavaScriptView Raw
1"use strict";
2/*
3 * @adonisjs/ace
4 *
5 * (c) Harminder Virk <virk@adonisjs.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10Object.defineProperty(exports, "__esModule", { value: true });
11exports.Kernel = void 0;
12const Hooks_1 = require("../Hooks");
13const Parser_1 = require("../Parser");
14const HelpCommand_1 = require("../HelpCommand");
15const Exceptions_1 = require("../Exceptions");
16const help_1 = require("../utils/help");
17const validateCommand_1 = require("../utils/validateCommand");
18const cliui_1 = require("@poppinss/cliui");
19/**
20 * Ace kernel class is used to register, find and invoke commands by
21 * parsing `process.argv.splice(2)` value.
22 */
23class Kernel {
24 constructor(application) {
25 this.application = application;
26 /**
27 * Reference to hooks class to execute lifecycle
28 * hooks
29 */
30 this.hooks = new Hooks_1.Hooks();
31 /**
32 * The state of the kernel
33 */
34 this.state = 'idle';
35 /**
36 * Exit handler for gracefully exiting the process
37 */
38 this.exitHandler = (kernel) => {
39 if (kernel.error && typeof kernel.error.handle === 'function') {
40 kernel.error.handle(kernel.error);
41 }
42 else if (kernel.error) {
43 cliui_1.logger.fatal(kernel.error);
44 }
45 process.exit(kernel.exitCode === undefined ? 0 : kernel.exitCode);
46 };
47 /**
48 * Find if CLI process is interactive. This flag can be
49 * toggled programmatically
50 */
51 this.isInteractive = cliui_1.isInteractive;
52 /**
53 * Find if console output is mocked
54 */
55 this.isMockingConsoleOutput = false;
56 /**
57 * The default command that will be invoked when no command is
58 * defined
59 */
60 this.defaultCommand = HelpCommand_1.HelpCommand;
61 /**
62 * List of registered commands
63 */
64 this.commands = {};
65 this.aliases = this.application.rcFile.commandsAliases;
66 /**
67 * List of registered flags
68 */
69 this.flags = {};
70 }
71 /**
72 * Executing global flag handlers. The global flag handlers are
73 * not async as of now, but later we can look into making them
74 * async.
75 */
76 executeGlobalFlagsHandlers(argv, command) {
77 const globalFlags = Object.keys(this.flags);
78 const parsedOptions = new Parser_1.Parser(this.flags).parse(argv, command);
79 globalFlags.forEach((name) => {
80 const value = parsedOptions[name];
81 /**
82 * Flag was not specified
83 */
84 if (value === undefined) {
85 return;
86 }
87 /**
88 * Calling the handler
89 */
90 this.flags[name].handler(parsedOptions[name], parsedOptions, command);
91 });
92 }
93 /**
94 * Returns an array of all registered commands
95 */
96 getAllCommandsAndAliases() {
97 let commands = Object.keys(this.commands).map((name) => this.commands[name]);
98 let aliases = {};
99 /**
100 * Concat manifest commands when they exists
101 */
102 if (this.manifestLoader && this.manifestLoader.booted) {
103 const { commands: manifestCommands, aliases: manifestAliases } = this.manifestLoader.getCommands();
104 commands = commands.concat(manifestCommands);
105 aliases = Object.assign(aliases, manifestAliases);
106 }
107 return {
108 commands,
109 aliases: Object.assign(aliases, this.aliases),
110 };
111 }
112 /**
113 * Processes the args and sets values on the command instance
114 */
115 async processCommandArgsAndFlags(commandInstance, args) {
116 const parser = new Parser_1.Parser(this.flags);
117 const command = commandInstance.constructor;
118 /**
119 * Parse the command arguments. The `parse` method will raise exception if flag
120 * or arg is not
121 */
122 const parsedOptions = parser.parse(args, command);
123 /**
124 * We validate the command arguments after the global flags have been
125 * executed. It is required, since flags may have nothing to do
126 * with the validaty of command itself
127 */
128 command.args.forEach((arg, index) => {
129 parser.validateArg(arg, index, parsedOptions, command);
130 });
131 /**
132 * Creating a new command instance and setting
133 * parsed options on it.
134 */
135 commandInstance.parsed = parsedOptions;
136 /**
137 * Setup command instance argument and flag
138 * properties.
139 */
140 for (let i = 0; i < command.args.length; i++) {
141 const arg = command.args[i];
142 if (arg.type === 'spread') {
143 commandInstance[arg.propertyName] = parsedOptions._.slice(i);
144 break;
145 }
146 else {
147 commandInstance[arg.propertyName] = parsedOptions._[i];
148 }
149 }
150 /**
151 * Set flag value on the command instance
152 */
153 for (let flag of command.flags) {
154 const flagValue = parsedOptions[flag.name];
155 if (flagValue !== undefined) {
156 commandInstance[flag.propertyName] = flagValue;
157 }
158 }
159 }
160 /**
161 * Execute the main command. For calling commands within commands,
162 * one must call "kernel.exec".
163 */
164 async execMain(commandName, args) {
165 const command = await this.find([commandName]);
166 /**
167 * Command not found. So execute global flags handlers and
168 * raise an exception
169 */
170 if (!command) {
171 this.executeGlobalFlagsHandlers(args);
172 throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName));
173 }
174 /**
175 * Make an instance of the command
176 */
177 const commandInstance = await this.application.container.makeAsync(command, [
178 this.application,
179 this,
180 ]);
181 /**
182 * Execute global flags
183 */
184 this.executeGlobalFlagsHandlers(args, command);
185 /**
186 * Process the arguments and flags for the command
187 */
188 await this.processCommandArgsAndFlags(commandInstance, args);
189 /**
190 * Keep a reference to the entry command. So that we know if we
191 * want to entertain `.exit` or not
192 */
193 this.entryCommand = commandInstance;
194 /**
195 * Execute before run hooks
196 */
197 await this.hooks.execute('before', 'run', commandInstance);
198 /**
199 * Execute command
200 */
201 return commandInstance.exec();
202 }
203 /**
204 * Handles exiting the process
205 */
206 async exitProcess(error) {
207 /**
208 * Check for state to avoid exiting the process multiple times
209 */
210 if (this.state === 'completed') {
211 return;
212 }
213 this.state = 'completed';
214 /**
215 * Re-assign error if entry command exists and has error
216 */
217 if (!error && this.entryCommand && this.entryCommand.error) {
218 error = this.entryCommand.error;
219 }
220 /**
221 * Execute the after run hooks. Wrapping inside try/catch since this is the
222 * cleanup handler for the process and must handle all exceptions
223 */
224 try {
225 if (this.entryCommand) {
226 await this.hooks.execute('after', 'run', this.entryCommand);
227 }
228 }
229 catch (hookError) {
230 error = hookError;
231 }
232 /**
233 * Assign error to the kernel instance
234 */
235 if (error) {
236 this.error = error;
237 }
238 /**
239 * Figure out the exit code for the process
240 */
241 const exitCode = error ? 1 : 0;
242 const commandExitCode = this.entryCommand && this.entryCommand.exitCode;
243 this.exitCode = commandExitCode === undefined ? exitCode : commandExitCode;
244 try {
245 await this.exitHandler(this);
246 }
247 catch (exitHandlerError) {
248 cliui_1.logger.warning('Expected exit handler to exit the process. Instead it raised an exception');
249 cliui_1.logger.fatal(exitHandlerError);
250 }
251 }
252 before(action, callback) {
253 this.hooks.add('before', action, callback);
254 return this;
255 }
256 after(action, callback) {
257 this.hooks.add('after', action, callback);
258 return this;
259 }
260 /**
261 * Register an array of command constructors
262 */
263 register(commands) {
264 commands.forEach((command) => {
265 command.boot();
266 (0, validateCommand_1.validateCommand)(command);
267 this.commands[command.commandName] = command;
268 /**
269 * Registering command aliaes
270 */
271 command.aliases.forEach((alias) => (this.aliases[alias] = command.commandName));
272 });
273 return this;
274 }
275 /**
276 * Register a global flag. It can be defined in combination with
277 * any command.
278 */
279 flag(name, handler, options) {
280 this.flags[name] = Object.assign({
281 name,
282 propertyName: name,
283 handler,
284 type: 'boolean',
285 }, options);
286 return this;
287 }
288 /**
289 * Use manifest instance to lazy load commands
290 */
291 useManifest(manifestLoader) {
292 this.manifestLoader = manifestLoader;
293 return this;
294 }
295 /**
296 * Register an exit handler
297 */
298 onExit(callback) {
299 this.exitHandler = callback;
300 return this;
301 }
302 /**
303 * Returns an array of command names suggestions for a given name.
304 */
305 getSuggestions(name, distance = 3) {
306 const leven = require('leven');
307 const { commands, aliases } = this.getAllCommandsAndAliases();
308 const suggestions = commands
309 .filter(({ commandName }) => leven(name, commandName) <= distance)
310 .map(({ commandName }) => commandName);
311 return suggestions.concat(Object.keys(aliases).filter((alias) => leven(name, alias) <= distance));
312 }
313 /**
314 * Preload the manifest file. Re-running this method twice will
315 * result in a noop
316 */
317 async preloadManifest() {
318 /**
319 * Load manifest commands when instance of manifest loader exists.
320 */
321 if (this.manifestLoader) {
322 await this.manifestLoader.boot();
323 }
324 }
325 /**
326 * Finds the command from the command line argv array. If command for
327 * the given name doesn't exists, then it will return `null`.
328 *
329 * Does executes the before and after hooks regardless of whether the
330 * command has been found or not
331 */
332 async find(argv) {
333 /**
334 * ----------------------------------------------------------------------------
335 * Even though in `Unix` the command name may appear in between or at last, with
336 * ace we always want the command name to be the first argument. However, the
337 * arguments to the command itself can appear in any sequence. For example:
338 *
339 * Works
340 * - node ace make:controller foo
341 * - node ace make:controller --http foo
342 *
343 * Doesn't work
344 * - node ace foo make:controller
345 * ----------------------------------------------------------------------------
346 */
347 const [commandName] = argv;
348 /**
349 * Command name from the registered aliases
350 */
351 const aliasCommandName = this.aliases[commandName];
352 /**
353 * Manifest commands gets preference over manually registered commands.
354 *
355 * - We check the manifest loader is register
356 * - The manifest loader has the command
357 * - Or the manifest loader has the alias command
358 */
359 const commandNode = this.manifestLoader
360 ? this.manifestLoader.hasCommand(commandName)
361 ? this.manifestLoader.getCommand(commandName)
362 : this.manifestLoader.hasCommand(aliasCommandName)
363 ? this.manifestLoader.getCommand(aliasCommandName)
364 : undefined
365 : undefined;
366 if (commandNode) {
367 commandNode.command.aliases = commandNode.command.aliases || [];
368 if (aliasCommandName && !commandNode.command.aliases.includes(commandName)) {
369 commandNode.command.aliases.push(commandName);
370 }
371 await this.hooks.execute('before', 'find', commandNode.command);
372 const command = await this.manifestLoader.loadCommand(commandNode.command.commandName);
373 await this.hooks.execute('after', 'find', command);
374 return command;
375 }
376 else {
377 /**
378 * Try to find command inside manually registered command or fallback
379 * to null
380 */
381 const command = this.commands[commandName] || this.commands[aliasCommandName] || null;
382 /**
383 * Share main command name as an alias with the command
384 */
385 if (command) {
386 command.aliases = command.aliases || [];
387 if (aliasCommandName && !command.aliases.includes(commandName)) {
388 command.aliases.push(commandName);
389 }
390 }
391 /**
392 * Executing before and after together to be compatible
393 * with the manifest find before and after hooks
394 */
395 await this.hooks.execute('before', 'find', command);
396 await this.hooks.execute('after', 'find', command);
397 return command;
398 }
399 }
400 /**
401 * Run the default command. The default command doesn't accept
402 * and args or flags.
403 */
404 async runDefaultCommand() {
405 this.defaultCommand.boot();
406 (0, validateCommand_1.validateCommand)(this.defaultCommand);
407 /**
408 * Execute before/after find hooks
409 */
410 await this.hooks.execute('before', 'find', this.defaultCommand);
411 await this.hooks.execute('after', 'find', this.defaultCommand);
412 /**
413 * Make the command instance using the container
414 */
415 const commandInstance = await this.application.container.makeAsync(this.defaultCommand, [
416 this.application,
417 this,
418 ]);
419 /**
420 * Execute before run hook
421 */
422 await this.hooks.execute('before', 'run', commandInstance);
423 /**
424 * Keep a reference to the entry command
425 */
426 this.entryCommand = commandInstance;
427 /**
428 * Execute the command
429 */
430 return commandInstance.exec();
431 }
432 /**
433 * Find if a command is the main command. Main commands are executed
434 * directly from the terminal.
435 */
436 isMain(command) {
437 return !!this.entryCommand && this.entryCommand === command;
438 }
439 /**
440 * Enforce mocking the console output. Command logs, tables, prompts
441 * will be mocked
442 */
443 mockConsoleOutput() {
444 this.isMockingConsoleOutput = true;
445 return this;
446 }
447 /**
448 * Toggle interactive state
449 */
450 interactive(state) {
451 this.isInteractive = state;
452 return this;
453 }
454 /**
455 * Execute a command as a sub-command. Do not call "handle" and
456 * always use this method to invoke command programatically
457 */
458 async exec(commandName, args) {
459 const command = await this.find([commandName]);
460 /**
461 * Command not found.
462 */
463 if (!command) {
464 throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName));
465 }
466 /**
467 * Make an instance of command and keep a reference of it as `this.entryCommand`
468 */
469 const commandInstance = await this.application.container.makeAsync(command, [
470 this.application,
471 this,
472 ]);
473 /**
474 * Process args and flags for the command
475 */
476 await this.processCommandArgsAndFlags(commandInstance, args);
477 let commandError;
478 /**
479 * Wrapping the command execution inside a try/catch, so that
480 * we can run the after hooks regardless of success or
481 * failure
482 */
483 try {
484 await this.hooks.execute('before', 'run', commandInstance);
485 await commandInstance.exec();
486 }
487 catch (error) {
488 commandError = error;
489 }
490 /**
491 * Execute after hooks
492 */
493 await this.hooks.execute('after', 'run', commandInstance);
494 /**
495 * Re-throw error (if any)
496 */
497 if (commandError) {
498 throw commandError;
499 }
500 return commandInstance;
501 }
502 /**
503 * Makes instance of a given command by processing command line arguments
504 * and setting them on the command instance
505 */
506 async handle(argv) {
507 if (this.state !== 'idle') {
508 return;
509 }
510 this.state = 'running';
511 try {
512 /**
513 * Preload the manifest file to load the manifest files
514 */
515 this.preloadManifest();
516 /**
517 * Branch 1
518 * Run default command and invoke the exit handler
519 */
520 if (!argv.length) {
521 await this.runDefaultCommand();
522 await this.exitProcess();
523 return;
524 }
525 /**
526 * Branch 2
527 * No command has been mentioned and hence execute all the global flags
528 * invoke the exit handler
529 */
530 const hasMentionedCommand = !argv[0].startsWith('-');
531 if (!hasMentionedCommand) {
532 this.executeGlobalFlagsHandlers(argv);
533 await this.exitProcess();
534 return;
535 }
536 /**
537 * Branch 3
538 * Execute the given command as the main command
539 */
540 const [commandName, ...args] = argv;
541 await this.execMain(commandName, args);
542 /**
543 * Exit the process if there isn't any entry command
544 */
545 if (!this.entryCommand) {
546 await this.exitProcess();
547 return;
548 }
549 const entryCommandConstructor = this.entryCommand.constructor;
550 /**
551 * Exit the process if entry command isn't a stayalive command. Stayalive
552 * commands should call `this.exit` to exit the process.
553 */
554 if (!entryCommandConstructor.settings.stayAlive) {
555 await this.exitProcess();
556 }
557 }
558 catch (error) {
559 await this.exitProcess(error);
560 }
561 }
562 /**
563 * Print the help screen for a given command or all commands/flags
564 */
565 printHelp(command, commandsToAppend, aliasesToAppend) {
566 let { commands, aliases } = this.getAllCommandsAndAliases();
567 /**
568 * Append additional commands and aliases for help screen only
569 */
570 if (commandsToAppend) {
571 commands = commands.concat(commandsToAppend);
572 }
573 if (aliasesToAppend) {
574 aliases = Object.assign({}, aliases, aliasesToAppend);
575 }
576 if (command) {
577 (0, help_1.printHelpFor)(command, aliases);
578 }
579 else {
580 const flags = Object.keys(this.flags).map((name) => this.flags[name]);
581 (0, help_1.printHelp)(commands, flags, aliases);
582 }
583 }
584 /**
585 * Trigger kernel to exit the process. The call to this method
586 * is ignored when command is not same the `entryCommand`.
587 *
588 * In other words, subcommands cannot trigger exit
589 */
590 async exit(command, error) {
591 if (command !== this.entryCommand) {
592 return;
593 }
594 await this.exitProcess(error);
595 }
596}
597exports.Kernel = Kernel;