@tsed/cli-core
Version:
Build your CLI with TypeScript and Decorators
263 lines (262 loc) • 9.86 kB
JavaScript
import { __decorate } from "tslib";
import { classOf } from "@tsed/core";
import { configuration, constant, destroyInjector, DIContext, getContext, inject, Injectable, injector, logger, Provider, runInContext } from "@tsed/di";
import { $asyncEmit } from "@tsed/hooks";
import { Argument, Command } from "commander";
import Inquirer from "inquirer";
// @ts-ignore
import inquirer_autocomplete_prompt from "inquirer-autocomplete-prompt";
import { v4 } from "uuid";
import { CommandStoreKeys } from "../domains/CommandStoreKeys.js";
import { PackageManagersModule } from "../packageManagers/index.js";
import { createSubTasks, createTasksRunner } from "../utils/createTasksRunner.js";
import { getCommandMetadata } from "../utils/getCommandMetadata.js";
import { mapCommanderOptions } from "../utils/index.js";
import { mapCommanderArgs } from "../utils/mapCommanderArgs.js";
import { parseOption } from "../utils/parseOption.js";
import { CliHooks } from "./CliHooks.js";
import { ProjectPackageJson } from "./ProjectPackageJson.js";
Inquirer.registerPrompt("autocomplete", inquirer_autocomplete_prompt);
let CliService = class CliService {
constructor() {
this.reinstallAfterRun = constant("project.reinstallAfterRun", false);
this.program = new Command();
this.pkg = constant("pkg", { version: "1.0.0" });
this.hooks = inject(CliHooks);
this.projectPkg = inject(ProjectPackageJson);
this.packageManagers = inject(PackageManagersModule);
this.commands = new Map();
}
/**
* Parse process.argv and runLifecycle action
* @param argv
*/
async parseArgs(argv) {
const { program } = this;
program.version(this.pkg.version);
this.load();
await program.parseAsync(argv);
}
/**
* Run lifecycle
* @param cmdName
* @param data
* @param $ctx
*/
runLifecycle(cmdName, data = {}, $ctx) {
return runInContext($ctx, async () => {
await $asyncEmit("$loadPackageJson");
data = await this.beforePrompt(cmdName, data);
$ctx.set("data", data);
data = await this.prompt(cmdName, data);
await this.dispatch(cmdName, data, $ctx);
});
}
async dispatch(cmdName, data, $ctx) {
try {
$ctx.set("dispatchCmd", cmdName);
$ctx.set("data", data);
await this.exec(cmdName, data, $ctx);
}
catch (er) {
await $asyncEmit("$onFinish", er);
await destroyInjector();
throw er;
}
await $asyncEmit("$onFinish");
await destroyInjector();
}
async exec(cmdName, data, $ctx) {
const initialTasks = await this.getTasks(cmdName, data);
if (initialTasks.length) {
const tasks = [
...initialTasks,
{
title: "Install dependencies",
enabled: () => this.reinstallAfterRun && (this.projectPkg.rewrite || this.projectPkg.reinstall),
task: createSubTasks(() => this.packageManagers.install(data), { ...data, concurrent: false })
},
...(await this.getPostInstallTasks(cmdName, data))
];
return createTasksRunner(tasks, this.mapData(cmdName, data, $ctx));
}
}
/**
* Run prompt for a given command
* @param cmdName
* @param ctx Initial data
*/
async beforePrompt(cmdName, ctx = {}) {
const provider = this.commands.get(cmdName);
const instance = inject(provider.useClass);
const verbose = ctx.verbose;
if (instance.$beforePrompt) {
ctx = await instance.$beforePrompt(JSON.parse(JSON.stringify(ctx)));
ctx.verbose = verbose;
}
return ctx;
}
/**
* Run prompt for a given command
* @param cmdName
* @param ctx Initial data
*/
async prompt(cmdName, ctx = {}) {
const provider = this.commands.get(cmdName);
const instance = inject(provider.useClass);
if (instance.$prompt) {
const questions = [
...(await instance.$prompt(ctx)),
...(await this.hooks.emit(CommandStoreKeys.PROMPT_HOOKS, cmdName, ctx))
];
if (questions.length) {
ctx = {
...ctx,
...(await Inquirer.prompt(questions))
};
}
}
return ctx;
}
/**
* Run lifecycle
* @param cmdName
* @param data
*/
async getTasks(cmdName, data) {
const $ctx = getContext();
const provider = this.commands.get(cmdName);
const instance = inject(provider.token);
data = this.mapData(cmdName, data, $ctx);
if (instance.$beforeExec) {
await instance.$beforeExec(data);
}
return [...(await instance.$exec(data)), ...(await this.hooks.emit(CommandStoreKeys.EXEC_HOOKS, cmdName, data))];
}
async getPostInstallTasks(cmdName, data) {
const provider = this.commands.get(cmdName);
const instance = inject(provider.useClass);
data = this.mapData(cmdName, data, getContext());
return [
...(instance.$postInstall ? await instance.$postInstall(data) : []),
...(await this.hooks.emit(CommandStoreKeys.POST_INSTALL_HOOKS, cmdName, data)),
...(instance.$afterPostInstall ? await instance.$afterPostInstall(data) : [])
];
}
createCommand(metadata) {
const { args, name, options, description, alias, allowUnknownOption } = metadata;
if (this.commands.has(name)) {
return this.commands.get(name).command;
}
let cmd = this.program.command(name);
const onAction = (commandName) => {
const [, ...rawArgs] = cmd.args;
const mappedArgs = mapCommanderArgs(args, this.program.args.filter((arg) => commandName === arg));
const allOpts = mapCommanderOptions(commandName, this.program.commands);
const data = {
...allOpts,
verbose: !!this.program.opts().verbose,
...mappedArgs,
...cmd.opts(),
rawArgs
};
const $ctx = new DIContext({
id: v4(),
injector: injector(),
logger: logger(),
level: logger().level,
maxStackSize: 0,
platform: "CLI"
});
$ctx.set("data", data);
$ctx.set("command", metadata);
configuration().set("command.metadata", metadata);
return this.runLifecycle(name, data, $ctx);
};
if (alias) {
cmd = cmd.alias(alias);
}
cmd = cmd.description(description);
cmd = this.buildArguments(cmd, args);
cmd = cmd.action(onAction);
if (options) {
cmd = this.buildOption(cmd, options, !!allowUnknownOption);
}
return cmd;
}
load() {
injector()
.getProviders("command")
.forEach((provider) => this.build(provider));
}
mapData(cmdName, data, $ctx) {
const provider = this.commands.get(cmdName);
const instance = inject(provider.useClass);
const verbose = data.verbose;
if (instance.$mapContext) {
data = instance.$mapContext(JSON.parse(JSON.stringify(data)));
data.verbose = verbose;
}
if (data.verbose) {
logger().level = "debug";
}
else {
logger().level = "info";
}
data.bindLogger = $ctx.get("command").bindLogger;
$ctx.set("data", data);
return data;
}
/**
* Build command and sub-commands
* @param provider
*/
build(provider) {
const metadata = getCommandMetadata(provider.useClass);
if (metadata.name) {
if (this.commands.has(metadata.name)) {
throw Error(`The ${metadata.name} command is already registered. Change your command name used by the class ${classOf(provider.useClass)}`);
}
provider.command = this.createCommand(metadata);
this.commands.set(metadata.name, provider);
}
}
/**
* Build sub-command options
* @param subCommand
* @param options
* @param allowUnknownOptions
*/
buildOption(subCommand, options, allowUnknownOptions) {
Object.entries(options).reduce((subCommand, [flags, { description, required, customParser, defaultValue, ...options }]) => {
const fn = (v) => {
return parseOption(v, options);
};
if (options.type === Boolean) {
defaultValue = false;
}
return required
? subCommand.requiredOption(flags, description, fn, defaultValue)
: subCommand.option(flags, description, fn, defaultValue);
}, subCommand);
subCommand.option("-r, --root-dir <path>", "Project root directory");
subCommand.option("--verbose", "Verbose mode", () => true);
if (allowUnknownOptions) {
subCommand.allowUnknownOption(true);
}
return subCommand;
}
buildArguments(cmd, args) {
return Object.entries(args).reduce((cmd, [key, { description, required, defaultValue }]) => {
const argument = new Argument(required ? `<${key}>` : `[${key}]`, description);
if (defaultValue !== undefined) {
argument.default(defaultValue);
}
return cmd.addArgument(argument);
}, cmd);
}
};
CliService = __decorate([
Injectable()
], CliService);
export { CliService };