diff --git a/packages/backend/src/common/constants/env.constants.ts b/packages/backend/src/common/constants/env.constants.ts index 2edea214..dd1424e1 100644 --- a/packages/backend/src/common/constants/env.constants.ts +++ b/packages/backend/src/common/constants/env.constants.ts @@ -10,7 +10,7 @@ if (!Object.values(NodeEnv).includes(_nodeEnv)) { throw new Error(`Invalid NODE_ENV value: '${_nodeEnv}'`); } -const IS_DEV = isDev(_nodeEnv); +export const IS_DEV = isDev(_nodeEnv); const EnvSchema = z .object({ diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index c6a77cf3..c342ba88 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -8,66 +8,91 @@ import { Command } from "commander"; import { runBuild } from "./commands/build"; import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants"; import { startDeleteFlow } from "./commands/delete"; -import { log } from "./common/cli.utils"; +import { Options_Cli } from "./common/cli.types"; +import { + log, + mergeOptions, + validateOptions, + validatePackages, +} from "./common/cli.utils"; -const runScript = async () => { - const exitHelpfully = (msg?: string) => { - msg && log.error(msg); - console.log(program.helpInformation()); - process.exit(1); - }; +class CompassCli { + private program: Command; + private options: Options_Cli; - 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" - ); + constructor(args: string[]) { + this.program = this.createProgram(); + this.program.parse(args); + this.options = this.getCliOptions(); + } - 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"); + 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( + "--user [id | email]", + "specify which user to run script for" + ); - program - .command("delete") - .description("deletes users data from compass database"); + program + .command("build") + .description("build compass package") + .argument( + `[${ALL_PACKAGES.join(" | ")}]`, + "package to build (only provde 1 at a time)" + ) + .option( + "-c, --clientId ", + "google client id to inject into build" + ); - program.parse(process.argv); + program + .command("delete") + .description("delete user data from compass database"); + return program; + } - const options = program.opts(); - const cmd = program.args[0]; + private getCliOptions(): Options_Cli { + const options = mergeOptions(this.program); + const validOptions = validateOptions(options); - switch (true) { - case cmd === "build": { - await runBuild(options); - break; - } - case cmd === "delete": { - const force = options["force"] as boolean; - const user = options["user"] as string; + return validOptions; + } - if (!user || typeof user !== "string") { - exitHelpfully("You must supply a user"); - } + public async run() { + const { user, force } = this.options; + const cmd = this.program.args[0]; - await startDeleteFlow(user, force); - break; + switch (true) { + case cmd === "build": { + await runBuild(this.options); + 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("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 adb014a8..d3a3aaca 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,37 +9,32 @@ import { PCKG, } from "@scripts/common/cli.constants"; import { - getVmInfo, getPckgsTo, _confirm, log, fileExists, getClientId, + getApiBaseUrl, + getEnvironmentAnswer, + validatePackages, } from "@scripts/common/cli.utils"; export const runBuild = async (options: Options_Cli) => { - const env = options["environment"]; - const vmInfo = await getVmInfo(env); - - const pckgs = - options["packages"] === undefined - ? await getPckgsTo("build") - : options["packages"]; + const pckgs = options.packages ? options.packages : await getPckgsTo("build"); + validatePackages(pckgs); if (pckgs.includes(PCKG.NODE)) { - await buildNodePckgs(vmInfo, options["skipEnv"]); + await buildNodePckgs(options); } - if (pckgs.includes(PCKG.WEB)) { - await buildWeb(vmInfo); + await buildWeb(options); } }; -// eslint-disable-next-line @typescript-eslint/require-await -const buildNodePckgs = async (vmInfo: Info_VM, skipEnv?: boolean) => { +const buildNodePckgs = async (options: Options_Cli) => { removeOldBuildFor(PCKG.NODE); createNodeDirs(); - await copyNodeConfigsToBuild(vmInfo, skipEnv); + await copyNodeConfigsToBuild(options); log.info("Compiling node packages ..."); shell.exec( @@ -58,11 +53,17 @@ const buildNodePckgs = async (vmInfo: Info_VM, skipEnv?: boolean) => { ); }; -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 = options.clientId + ? options.clientId + : await getClientId(environment); const envPath = path.join(__dirname, "..", "..", "..", "backend", envFile); dotenv.config({ path: envPath }); @@ -78,15 +79,16 @@ const buildWeb = async (vmInfo: Info_VM) => { log.success(`Done building web files.`); log.tip(` Now you'll probably want to: - - zip the build/web dir - - copy it to your ${destination} server - - unzip it - - run it`); + - zip the build dir + - 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) => { - 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}`; @@ -99,7 +101,7 @@ const copyNodeConfigsToBuild = async (vmInfo: Info_VM, skipEnv?: boolean) => { log.warning(`Env file does not exist: ${envPath}`); const keepGoing = - skipEnv === 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..297690db 100644 --- a/packages/scripts/src/common/cli.utils.ts +++ b/packages/scripts/src/common/cli.utils.ts @@ -2,20 +2,30 @@ import pkg from "inquirer"; import chalk from "chalk"; const { prompt } = pkg; import shell from "shelljs"; +import { Command } from "commander"; import { ALL_PACKAGES, CLI_ENV } from "./cli.constants"; -import { Category_VM } from "./cli.types"; +import { Environment_Cli, Options_Cli, Schema_Options_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 +68,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", @@ -124,6 +129,24 @@ export const log = { tip: (msg: string) => console.log(chalk.hex("#f5c150")(msg)), }; +export const mergeOptions = (program: Command): Options_Cli => { + const _options = program.opts(); + const packages = program.args[1]?.split(","); + const options: Options_Cli = { + ..._options, + force: _options["force"] === true, + packages, + }; + + const build = program.commands.find((cmd) => cmd.name() === "build"); + const clientId = build?.opts()["clientId"] as string; + if (build && clientId) { + options.clientId = clientId; + } + + return options; +}; + export const _confirm = async (question: string, _default = true) => { const q = [ { @@ -140,3 +163,29 @@ export const _confirm = async (question: string, _default = true) => { process.exit(1); }); }; + +export const validateOptions = (options: Options_Cli): Options_Cli => { + const { data, error } = Schema_Options_Cli.safeParse(options); + if (error) { + log.error(`Invalid CLI options: ${JSON.stringify(error.format())}`); + process.exit(1); + } + + return data; +}; + +export const validatePackages = (packages: string[] | undefined) => { + if (!packages) { + log.error("Package 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); + } +};