UNPKG

@tsed/cli-core

Version:
247 lines (246 loc) 9.41 kB
import { PromptRunner } from "@tsed/cli-prompts"; import { concat, tasks as tasksRunner } from "@tsed/cli-tasks"; import { classOf, isArrowFn } from "@tsed/core"; import { configuration, constant, destroyInjector, DIContext, getContext, inject, injectable, injector, logger, Provider, runInContext } from "@tsed/di"; import { $asyncAlter, $asyncEmit } from "@tsed/hooks"; import { pascalCase } from "change-case"; import { Argument, Command } from "commander"; import { v4 } from "uuid"; import { PackageManagersModule } from "../packageManagers/index.js"; import { getCommandMetadata } from "../utils/getCommandMetadata.js"; import { mapCommanderOptions, validate } 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"; export 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.promptRunner = inject(PromptRunner); 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 () => { $ctx.set("dispatchCmd", cmdName); await $asyncEmit("$loadPackageJson"); try { data = await this.prompt(cmdName, data, $ctx); await this.exec(cmdName, data, $ctx); await $asyncEmit("$onFinish", [$ctx.get("data")]); } catch (er) { await $asyncEmit("$onFinish", [$ctx.get("data"), er]); throw er; } finally { await destroyInjector(); } }); } async exec(cmdName, data, $ctx) { const tasks = await this.getTasks(cmdName, data); $ctx.set("data", data); if (tasks.length) { if (this.reinstallAfterRun && (this.projectPkg.rewrite || this.projectPkg.reinstall)) { tasks.push(this.packageManagers.task("Install dependencies", data), ...(await this.getPostInstallTasks(cmdName, data))); } data = this.mapData(cmdName, data, $ctx); $ctx.set("data", data); const result = await tasksRunner(tasks, data); $ctx.set("data", data); return result; } } /** * Run prompt for a given command * @param cmdName * @param data * @param $ctx */ async prompt(cmdName, data = {}, $ctx) { const provider = this.commands.get(cmdName); const instance = inject(provider.token); $ctx.set("data", data); if (instance.$prompt) { const questions = await instance.$prompt(data); if (questions) { const answers = await this.promptRunner.run(questions, data); data = { ...data, ...answers }; } } $ctx.set("data", data); return data; } /** * 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); return concat(await instance.$exec(data), await $asyncAlter(`$alter${pascalCase(cmdName)}Tasks`, [], [data])); } async getPostInstallTasks(cmdName, data) { const provider = this.commands.get(cmdName); const instance = inject(provider.token); data = this.mapData(cmdName, data, getContext()); return concat(await instance.$postInstall?.(data), await $asyncAlter(`$alter${pascalCase(cmdName)}PostInstallTasks`, [], [data]), await instance.$afterPostInstall?.(data)); } createCommand(metadata) { const { name, description, alias, inputSchema } = metadata; if (this.commands.has(name)) { return this.commands.get(name).command; } const { args, options, allowUnknownOption } = metadata.getOptions(); 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); let data = { ...allOpts, verbose: !!this.program.opts().verbose, ...mappedArgs, ...cmd.opts(), rawArgs }; if (inputSchema) { const schema = isArrowFn(inputSchema) ? inputSchema() : inputSchema; const { isValid, errors, value } = validate(data, schema); if (isValid) { data = value; } else { logger().error({ event: "VALIDATION_ERROR", errors }); throw new Error("Validation error"); } } 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); }; let cmd = this.program.command(name); 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.token); const verbose = data.verbose; data.commandName ||= cmdName; 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.renderMode = $ctx.get("command")?.renderMode; data.logger = logger(); $ctx.set("data", data); return data; } /** * Build command and sub-commands * @param provider */ build(provider) { const metadata = getCommandMetadata(provider.token); 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); } } injectable(CliService);