UNPKG

@tsed/cli-core

Version:
263 lines (262 loc) 9.86 kB
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 };