From 60c043b6de315130ddca402b428c0690403c6de6 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Sun, 29 Dec 2024 07:51:05 -0600 Subject: [PATCH] refactor(cli): convert CLI to class and use zod for validation --- packages/scripts/src/cli.ts | 164 ++++++++++++----------- packages/scripts/src/commands/build.ts | 44 +++--- packages/scripts/src/common/cli.types.ts | 25 ++-- packages/scripts/src/common/cli.utils.ts | 38 +++--- 4 files changed, 141 insertions(+), 130 deletions(-) diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index 33275df2..1ced114c 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -6,101 +6,113 @@ dotenv.config({ import { Command } from "commander"; import { runBuild } from "./commands/build"; -import { ALL_PACKAGES, CATEGORY_VM, PCKG } from "./common/cli.constants"; +import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants"; import { startDeleteFlow } from "./commands/delete"; +import { Options_Cli, Schema_Options_Cli } from "./common/cli.types"; import { log } from "./common/cli.utils"; -import { Options_Cli } from "./common/cli.types"; -const createProgram = () => { - const program = new Command(); - program.option( - `-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`, - "specify environment" - ); - program.option("-f, --force", "forces operation, no cautionary prompts"); - program.option( - "-u, --user [id | email]", - "specifies which user to run script for" - ); +class CompassCli { + private program: Command; + private options: Options_Cli; - program - .command("build") - .description("build compass package(s)") - .argument( - `[${ALL_PACKAGES.join(" | ")}]`, - "package(s) to build, separated by comma" - ) - .option("--skip-env", "skips copying env files to build"); + constructor(args: string[]) { + this.program = this.createProgram(); + this.program.parse(args); + this.options = this.getCliOptions(); + } - program - .command("delete") - .description("delete user data from compass database"); - return program; -}; + private createProgram(): Command { + const program = new Command(); + program.option( + `-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`, + "specify environment" + ); + program.option("-f, --force", "force operation, no cautionary prompts"); + program.option( + "-u, --user [id | email]", + "specify which user to run script for" + ); -const exitHelpfully = (program: Command, msg?: string) => { - msg && log.error(msg); - console.log(program.helpInformation()); - process.exit(1); -}; + program + .command("build") + .description("build compass package(s)") + .argument( + `[${ALL_PACKAGES.join(" | ")}]`, + "package(s) to build, separated by comma" + ) + .option("--skip-env", "skip copying env files to build"); -const getCliOptions = (program: Command): Options_Cli => { - const _options = program.opts(); - const packages = program.args[1]?.split(","); + program + .command("delete") + .description("delete user data from compass database"); + return program; + } - const options = { - ..._options, - packages, - force: _options["force"] === true, - user: _options["user"] as string, - }; + private getCliOptions(): Options_Cli { + const _options = this.program.opts(); + const packages = this.program.args[1]?.split(","); + const options: Options_Cli = { + ..._options, + force: _options["force"] === true, + packages, + }; - return options; -}; + const { data, error } = Schema_Options_Cli.safeParse(options); + if (error) { + log.error(`Invalid CLI options: ${JSON.stringify(error.format())}`); + process.exit(1); + } -const validatePackages = (packages: string[] | undefined) => { - if (!packages) { - log.error("Packages must be defined"); + return data; } - if (!packages?.includes(PCKG.NODE) && !packages?.includes(PCKG.WEB)) { - log.error( - `One or more of these pckgs isn't supported: ${( - packages as string[] - )?.toString()}` - ); - process.exit(1); + private validatePackages(packages: string[] | undefined) { + if (!packages) { + log.error("Packages must be defined"); + process.exit(1); + } + const unsupportedPackages = packages.filter( + (pkg) => !ALL_PACKAGES.includes(pkg) + ); + if (unsupportedPackages.length > 0) { + log.error( + `One or more of these packages isn't supported: ${unsupportedPackages.toString()}` + ); + process.exit(1); + } } -}; - -const runScript = async () => { - const program = createProgram(); - program.parse(process.argv); - const options = getCliOptions(program); - const { user, force } = options; + public async run() { + const { user, force, packages } = this.options; + const cmd = this.program.args[0]; - const cmd = program.args[0]; - switch (true) { - case cmd === "build": { - validatePackages(options.packages); - await runBuild(options); - break; - } - case cmd === "delete": { - if (!user || typeof user !== "string") { - exitHelpfully(program, "You must supply a user"); + switch (true) { + case cmd === "build": { + this.validatePackages(packages); + await runBuild(this.options); + break; } - - await startDeleteFlow(user as string, force); - break; + case cmd === "delete": { + if (!user || typeof user !== "string") { + this.exitHelpfully("You must supply a user"); + } + await startDeleteFlow(user as string, force); + break; + } + default: + this.exitHelpfully("Unsupported cmd"); } - default: - exitHelpfully(program, "Unsupported cmd"); } -}; -runScript().catch((err) => { + private exitHelpfully(msg?: string) { + msg && log.error(msg); + console.log(this.program.helpInformation()); + process.exit(1); + } +} + +const cli = new CompassCli(process.argv); +cli.run().catch((err) => { console.log(err); process.exit(1); }); diff --git a/packages/scripts/src/commands/build.ts b/packages/scripts/src/commands/build.ts index 889f9223..3c441294 100644 --- a/packages/scripts/src/commands/build.ts +++ b/packages/scripts/src/commands/build.ts @@ -1,7 +1,7 @@ import dotenv from "dotenv"; import path from "path"; import shell from "shelljs"; -import { Options_Cli, Info_VM } from "@scripts/common/cli.types"; +import { Options_Cli } from "@scripts/common/cli.types"; import { COMPASS_BUILD_DEV, COMPASS_ROOT_DEV, @@ -9,34 +9,33 @@ import { PCKG, } from "@scripts/common/cli.constants"; import { - getVmInfo, getPckgsTo, _confirm, log, fileExists, getClientId, + getApiBaseUrl, + getEnvironmentAnswer, } from "@scripts/common/cli.utils"; export const runBuild = async (options: Options_Cli) => { - const vmInfo = await getVmInfo(options.environment); - const pckgs = options?.packages?.length === 0 ? await getPckgsTo("build") : (options.packages as string[]); if (pckgs.includes(PCKG.NODE)) { - await buildNodePckgs(vmInfo, options); + await buildNodePckgs(options); } if (pckgs.includes(PCKG.WEB)) { - await buildWeb(vmInfo); + await buildWeb(options); } }; -const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => { +const buildNodePckgs = async (options: Options_Cli) => { removeOldBuildFor(PCKG.NODE); createNodeDirs(); - await copyNodeConfigsToBuild(vmInfo, options.skipEnv, options.force); + await copyNodeConfigsToBuild(options); log.info("Compiling node packages ..."); shell.exec( @@ -55,11 +54,15 @@ const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => { ); }; -const buildWeb = async (vmInfo: Info_VM) => { - const { baseUrl, destination } = vmInfo; - const envFile = destination === "staging" ? ".env" : ".env.prod"; +const buildWeb = async (options: Options_Cli) => { + const environment = + options.environment !== undefined + ? options.environment + : await getEnvironmentAnswer(); - const gClientId = await getClientId(destination); + const envFile = environment === "staging" ? ".env" : ".env.prod"; + const baseUrl = await getApiBaseUrl(environment); + const gClientId = await getClientId(environment); const envPath = path.join(__dirname, "..", "..", "..", "backend", envFile); dotenv.config({ path: envPath }); @@ -76,18 +79,15 @@ const buildWeb = async (vmInfo: Info_VM) => { log.tip(` Now you'll probably want to: - zip the build dir - - copy it to your ${destination} server - - unzip it and serve as the static assets + - copy it to your ${environment} environment + - unzip it to expose the static assets + - serve assets `); process.exit(0); }; -const copyNodeConfigsToBuild = async ( - vmInfo: Info_VM, - skipEnv?: boolean, - force?: boolean -) => { - const envName = vmInfo.destination === "production" ? ".prod.env" : ".env"; +const copyNodeConfigsToBuild = async (options: Options_Cli) => { + const envName = options.environment === "production" ? ".prod.env" : ".env"; const envPath = `${COMPASS_ROOT_DEV}/packages/backend/${envName}`; @@ -100,9 +100,7 @@ const copyNodeConfigsToBuild = async ( log.warning(`Env file does not exist: ${envPath}`); const keepGoing = - skipEnv === true || force === true - ? true - : await _confirm("Continue anyway?"); + options.force === true ? true : await _confirm("Continue anyway?"); if (!keepGoing) { log.error("Exiting due to missing env file"); diff --git a/packages/scripts/src/common/cli.types.ts b/packages/scripts/src/common/cli.types.ts index c08c9b89..cc9465fa 100644 --- a/packages/scripts/src/common/cli.types.ts +++ b/packages/scripts/src/common/cli.types.ts @@ -1,16 +1,13 @@ -export type Category_VM = "staging" | "production"; +import { z } from "zod"; -export interface Info_VM { - baseUrl: string; - destination: Category_VM; -} +export type Environment_Cli = "staging" | "production"; -export interface Options_Cli { - build?: boolean; - delete?: boolean; - environment?: Category_VM; - force?: boolean; - packages?: string[]; - skipEnv?: boolean; - user?: string; -} +export const Schema_Options_Cli = z.object({ + clientId: z.string().optional(), + environment: z.enum(["staging", "production"]).optional(), + force: z.boolean().optional(), + packages: z.array(z.string()).optional(), + user: z.string().optional(), +}); + +export type Options_Cli = z.infer; diff --git a/packages/scripts/src/common/cli.utils.ts b/packages/scripts/src/common/cli.utils.ts index 98ac9443..18ed41d4 100644 --- a/packages/scripts/src/common/cli.utils.ts +++ b/packages/scripts/src/common/cli.utils.ts @@ -4,18 +4,27 @@ const { prompt } = pkg; import shell from "shelljs"; import { ALL_PACKAGES, CLI_ENV } from "./cli.constants"; -import { Category_VM } from "./cli.types"; +import { Environment_Cli } from "./cli.types"; export const fileExists = (file: string) => { return shell.test("-e", file); }; -export const getClientId = async (destination: Category_VM) => { - if (destination === "staging") { +export const getApiBaseUrl = async (environment: Environment_Cli) => { + const category = environment ? environment : await getEnvironmentAnswer(); + const isStaging = category === "staging"; + const domain = await getDomainAnswer(isStaging); + const baseUrl = `https://${domain}/api`; + + return baseUrl; +}; + +export const getClientId = async (environment: Environment_Cli) => { + if (environment === "staging") { return process.env["CLIENT_ID"] as string; } - if (destination === "production") { + if (environment === "production") { const q = `Enter the googleClientId for the production environment:`; return prompt([{ type: "input", name: "answer", message: q }]) @@ -58,22 +67,17 @@ const getDomainAnswer = async (isStaging: boolean) => { process.exit(1); }); }; -export const getVmInfo = async (environment?: Category_VM) => { - const destination = environment - ? environment - : ((await getListAnswer("Select environment to use:", [ - "staging", - "production", - ])) as Category_VM); - - const isStaging = destination === "staging"; - const domain = await getDomainAnswer(isStaging); - const baseUrl = `https://${domain}/api`; - return { baseUrl, destination }; +export const getEnvironmentAnswer = async (): Promise => { + const environment = (await getListAnswer("Select environment to use:", [ + "staging", + "production", + ])) as Environment_Cli; + + return environment; }; -const getListAnswer = async (question: string, choices: string[]) => { +export const getListAnswer = async (question: string, choices: string[]) => { const q = [ { type: "list",