Skip to content

Commit 52689a1

Browse files
committed
refactor(cli): convert CLI to class and use zod for validation
1 parent 7e4e01b commit 52689a1

File tree

4 files changed

+141
-130
lines changed

4 files changed

+141
-130
lines changed

packages/scripts/src/cli.ts

Lines changed: 88 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,101 +6,113 @@ dotenv.config({
66
import { Command } from "commander";
77

88
import { runBuild } from "./commands/build";
9-
import { ALL_PACKAGES, CATEGORY_VM, PCKG } from "./common/cli.constants";
9+
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
1010
import { startDeleteFlow } from "./commands/delete";
11+
import { Options_Cli, Schema_Options_Cli } from "./common/cli.types";
1112
import { log } from "./common/cli.utils";
12-
import { Options_Cli } from "./common/cli.types";
1313

14-
const createProgram = () => {
15-
const program = new Command();
16-
program.option(
17-
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
18-
"specify environment"
19-
);
20-
program.option("-f, --force", "forces operation, no cautionary prompts");
21-
program.option(
22-
"-u, --user [id | email]",
23-
"specifies which user to run script for"
24-
);
14+
class CompassCli {
15+
private program: Command;
16+
private options: Options_Cli;
2517

26-
program
27-
.command("build")
28-
.description("build compass package(s)")
29-
.argument(
30-
`[${ALL_PACKAGES.join(" | ")}]`,
31-
"package(s) to build, separated by comma"
32-
)
33-
.option("--skip-env", "skips copying env files to build");
18+
constructor(args: string[]) {
19+
this.program = this.createProgram();
20+
this.program.parse(args);
21+
this.options = this.getCliOptions();
22+
}
3423

35-
program
36-
.command("delete")
37-
.description("delete user data from compass database");
38-
return program;
39-
};
24+
private createProgram(): Command {
25+
const program = new Command();
26+
program.option(
27+
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
28+
"specify environment"
29+
);
30+
program.option("-f, --force", "force operation, no cautionary prompts");
31+
program.option(
32+
"-u, --user [id | email]",
33+
"specify which user to run script for"
34+
);
4035

41-
const exitHelpfully = (program: Command, msg?: string) => {
42-
msg && log.error(msg);
43-
console.log(program.helpInformation());
44-
process.exit(1);
45-
};
36+
program
37+
.command("build")
38+
.description("build compass package(s)")
39+
.argument(
40+
`[${ALL_PACKAGES.join(" | ")}]`,
41+
"package(s) to build, separated by comma"
42+
)
43+
.option("--skip-env", "skip copying env files to build");
4644

47-
const getCliOptions = (program: Command): Options_Cli => {
48-
const _options = program.opts();
49-
const packages = program.args[1]?.split(",");
45+
program
46+
.command("delete")
47+
.description("delete user data from compass database");
48+
return program;
49+
}
5050

51-
const options = {
52-
..._options,
53-
packages,
54-
force: _options["force"] === true,
55-
user: _options["user"] as string,
56-
};
51+
private getCliOptions(): Options_Cli {
52+
const _options = this.program.opts();
53+
const packages = this.program.args[1]?.split(",");
54+
const options: Options_Cli = {
55+
..._options,
56+
force: _options["force"] === true,
57+
packages,
58+
};
5759

58-
return options;
59-
};
60+
const { data, error } = Schema_Options_Cli.safeParse(options);
61+
if (error) {
62+
log.error(`Invalid CLI options: ${JSON.stringify(error.format())}`);
63+
process.exit(1);
64+
}
6065

61-
const validatePackages = (packages: string[] | undefined) => {
62-
if (!packages) {
63-
log.error("Packages must be defined");
66+
return data;
6467
}
65-
if (!packages?.includes(PCKG.NODE) && !packages?.includes(PCKG.WEB)) {
66-
log.error(
67-
`One or more of these pckgs isn't supported: ${(
68-
packages as string[]
69-
)?.toString()}`
70-
);
7168

72-
process.exit(1);
69+
private validatePackages(packages: string[] | undefined) {
70+
if (!packages) {
71+
log.error("Packages must be defined");
72+
process.exit(1);
73+
}
74+
const unsupportedPackages = packages.filter(
75+
(pkg) => !ALL_PACKAGES.includes(pkg)
76+
);
77+
if (unsupportedPackages.length > 0) {
78+
log.error(
79+
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
80+
);
81+
process.exit(1);
82+
}
7383
}
74-
};
75-
76-
const runScript = async () => {
77-
const program = createProgram();
78-
program.parse(process.argv);
7984

80-
const options = getCliOptions(program);
81-
const { user, force } = options;
85+
public async run() {
86+
const { user, force, packages } = this.options;
87+
const cmd = this.program.args[0];
8288

83-
const cmd = program.args[0];
84-
switch (true) {
85-
case cmd === "build": {
86-
validatePackages(options.packages);
87-
await runBuild(options);
88-
break;
89-
}
90-
case cmd === "delete": {
91-
if (!user || typeof user !== "string") {
92-
exitHelpfully(program, "You must supply a user");
89+
switch (true) {
90+
case cmd === "build": {
91+
this.validatePackages(packages);
92+
await runBuild(this.options);
93+
break;
9394
}
94-
95-
await startDeleteFlow(user as string, force);
96-
break;
95+
case cmd === "delete": {
96+
if (!user || typeof user !== "string") {
97+
this.exitHelpfully("You must supply a user");
98+
}
99+
await startDeleteFlow(user as string, force);
100+
break;
101+
}
102+
default:
103+
this.exitHelpfully("Unsupported cmd");
97104
}
98-
default:
99-
exitHelpfully(program, "Unsupported cmd");
100105
}
101-
};
102106

103-
runScript().catch((err) => {
107+
private exitHelpfully(msg?: string) {
108+
msg && log.error(msg);
109+
console.log(this.program.helpInformation());
110+
process.exit(1);
111+
}
112+
}
113+
114+
const cli = new CompassCli(process.argv);
115+
cli.run().catch((err) => {
104116
console.log(err);
105117
process.exit(1);
106118
});

packages/scripts/src/commands/build.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
import dotenv from "dotenv";
22
import path from "path";
33
import shell from "shelljs";
4-
import { Options_Cli, Info_VM } from "@scripts/common/cli.types";
4+
import { Options_Cli } from "@scripts/common/cli.types";
55
import {
66
COMPASS_BUILD_DEV,
77
COMPASS_ROOT_DEV,
88
NODE_BUILD,
99
PCKG,
1010
} from "@scripts/common/cli.constants";
1111
import {
12-
getVmInfo,
1312
getPckgsTo,
1413
_confirm,
1514
log,
1615
fileExists,
1716
getClientId,
17+
getApiBaseUrl,
18+
getEnvironmentAnswer,
1819
} from "@scripts/common/cli.utils";
1920

2021
export const runBuild = async (options: Options_Cli) => {
21-
const vmInfo = await getVmInfo(options.environment);
22-
2322
const pckgs =
2423
options?.packages?.length === 0
2524
? await getPckgsTo("build")
2625
: (options.packages as string[]);
2726

2827
if (pckgs.includes(PCKG.NODE)) {
29-
await buildNodePckgs(vmInfo, options);
28+
await buildNodePckgs(options);
3029
}
3130
if (pckgs.includes(PCKG.WEB)) {
32-
await buildWeb(vmInfo);
31+
await buildWeb(options);
3332
}
3433
};
3534

36-
const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => {
35+
const buildNodePckgs = async (options: Options_Cli) => {
3736
removeOldBuildFor(PCKG.NODE);
3837
createNodeDirs();
39-
await copyNodeConfigsToBuild(vmInfo, options.skipEnv, options.force);
38+
await copyNodeConfigsToBuild(options);
4039

4140
log.info("Compiling node packages ...");
4241
shell.exec(
@@ -55,11 +54,15 @@ const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => {
5554
);
5655
};
5756

58-
const buildWeb = async (vmInfo: Info_VM) => {
59-
const { baseUrl, destination } = vmInfo;
60-
const envFile = destination === "staging" ? ".env" : ".env.prod";
57+
const buildWeb = async (options: Options_Cli) => {
58+
const environment =
59+
options.environment !== undefined
60+
? options.environment
61+
: await getEnvironmentAnswer();
6162

62-
const gClientId = await getClientId(destination);
63+
const envFile = environment === "staging" ? ".env" : ".env.prod";
64+
const baseUrl = await getApiBaseUrl(environment);
65+
const gClientId = await getClientId(environment);
6366

6467
const envPath = path.join(__dirname, "..", "..", "..", "backend", envFile);
6568
dotenv.config({ path: envPath });
@@ -76,18 +79,15 @@ const buildWeb = async (vmInfo: Info_VM) => {
7679
log.tip(`
7780
Now you'll probably want to:
7881
- zip the build dir
79-
- copy it to your ${destination} server
80-
- unzip it and serve as the static assets
82+
- copy it to your ${environment} environment
83+
- unzip it to expose the static assets
84+
- serve assets
8185
`);
8286
process.exit(0);
8387
};
8488

85-
const copyNodeConfigsToBuild = async (
86-
vmInfo: Info_VM,
87-
skipEnv?: boolean,
88-
force?: boolean
89-
) => {
90-
const envName = vmInfo.destination === "production" ? ".prod.env" : ".env";
89+
const copyNodeConfigsToBuild = async (options: Options_Cli) => {
90+
const envName = options.environment === "production" ? ".prod.env" : ".env";
9191

9292
const envPath = `${COMPASS_ROOT_DEV}/packages/backend/${envName}`;
9393

@@ -100,9 +100,7 @@ const copyNodeConfigsToBuild = async (
100100
log.warning(`Env file does not exist: ${envPath}`);
101101

102102
const keepGoing =
103-
skipEnv === true || force === true
104-
? true
105-
: await _confirm("Continue anyway?");
103+
options.force === true ? true : await _confirm("Continue anyway?");
106104

107105
if (!keepGoing) {
108106
log.error("Exiting due to missing env file");
Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
export type Category_VM = "staging" | "production";
1+
import { z } from "zod";
22

3-
export interface Info_VM {
4-
baseUrl: string;
5-
destination: Category_VM;
6-
}
3+
export type Environment_Cli = "staging" | "production";
74

8-
export interface Options_Cli {
9-
build?: boolean;
10-
delete?: boolean;
11-
environment?: Category_VM;
12-
force?: boolean;
13-
packages?: string[];
14-
skipEnv?: boolean;
15-
user?: string;
16-
}
5+
export const Schema_Options_Cli = z.object({
6+
clientId: z.string().optional(),
7+
environment: z.enum(["staging", "production"]).optional(),
8+
force: z.boolean().optional(),
9+
packages: z.array(z.string()).optional(),
10+
user: z.string().optional(),
11+
});
12+
13+
export type Options_Cli = z.infer<typeof Schema_Options_Cli>;

packages/scripts/src/common/cli.utils.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@ const { prompt } = pkg;
44
import shell from "shelljs";
55

66
import { ALL_PACKAGES, CLI_ENV } from "./cli.constants";
7-
import { Category_VM } from "./cli.types";
7+
import { Environment_Cli } from "./cli.types";
88

99
export const fileExists = (file: string) => {
1010
return shell.test("-e", file);
1111
};
1212

13-
export const getClientId = async (destination: Category_VM) => {
14-
if (destination === "staging") {
13+
export const getApiBaseUrl = async (environment: Environment_Cli) => {
14+
const category = environment ? environment : await getEnvironmentAnswer();
15+
const isStaging = category === "staging";
16+
const domain = await getDomainAnswer(isStaging);
17+
const baseUrl = `https://${domain}/api`;
18+
19+
return baseUrl;
20+
};
21+
22+
export const getClientId = async (environment: Environment_Cli) => {
23+
if (environment === "staging") {
1524
return process.env["CLIENT_ID"] as string;
1625
}
1726

18-
if (destination === "production") {
27+
if (environment === "production") {
1928
const q = `Enter the googleClientId for the production environment:`;
2029

2130
return prompt([{ type: "input", name: "answer", message: q }])
@@ -58,22 +67,17 @@ const getDomainAnswer = async (isStaging: boolean) => {
5867
process.exit(1);
5968
});
6069
};
61-
export const getVmInfo = async (environment?: Category_VM) => {
62-
const destination = environment
63-
? environment
64-
: ((await getListAnswer("Select environment to use:", [
65-
"staging",
66-
"production",
67-
])) as Category_VM);
68-
69-
const isStaging = destination === "staging";
70-
const domain = await getDomainAnswer(isStaging);
71-
const baseUrl = `https://${domain}/api`;
7270

73-
return { baseUrl, destination };
71+
export const getEnvironmentAnswer = async (): Promise<Environment_Cli> => {
72+
const environment = (await getListAnswer("Select environment to use:", [
73+
"staging",
74+
"production",
75+
])) as Environment_Cli;
76+
77+
return environment;
7478
};
7579

76-
const getListAnswer = async (question: string, choices: string[]) => {
80+
export const getListAnswer = async (question: string, choices: string[]) => {
7781
const q = [
7882
{
7983
type: "list",

0 commit comments

Comments
 (0)