Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(cli pretty): Simplify our runner and have nicer output #710

Merged
merged 3 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions bin/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Argv } from "yargs";
import {
checkIfAuthenticated,
LabeledProcessRunner,
runCommand,
project,
region,
writeUiEnvFile,
Expand All @@ -14,8 +14,6 @@ import {
} from "@aws-sdk/client-cloudfront";
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";

const runner = new LabeledProcessRunner();

export const deploy = {
command: "deploy",
describe: "deploy the project",
Expand All @@ -24,19 +22,15 @@ export const deploy = {
},
handler: async (options: { stage: string; stack?: string }) => {
await checkIfAuthenticated();
await runner.run_command_and_output(
"CDK Deploy",
["cdk", "deploy", "-c", `stage=${options.stage}`, "--all"],
await runCommand(
"cdk",
["deploy", "-c", `stage=${options.stage}`, "--all"],
".",
);

await writeUiEnvFile(options.stage);

await runner.run_command_and_output(
"Build",
["bun", "run", "build"],
"react-app",
);
await runCommand("bun", ["run", "build"], "react-app");

const { s3BucketName, cloudfrontDistributionId } = JSON.parse(
(
Expand All @@ -63,14 +57,14 @@ export const deploy = {
// There's a mime type issue when aws s3 syncing files up
// Empirically, this issue never presents itself if the bucket is cleared just before.
// Until we have a neat way of ensuring correct mime types, we'll remove all files from the bucket.
await runner.run_command_and_output(
"S3 Clean",
["aws", "s3", "rm", `s3://${s3BucketName}/`, "--recursive"],
await runCommand(
"aws",
["s3", "rm", `s3://${s3BucketName}/`, "--recursive"],
".",
);
await runner.run_command_and_output(
"S3 Sync",
["aws", "s3", "sync", buildDir, `s3://${s3BucketName}/`],
await runCommand(
"aws",
["s3", "sync", buildDir, `s3://${s3BucketName}/`],
".",
);

Expand Down
15 changes: 4 additions & 11 deletions bin/cli/src/commands/docs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Argv } from "yargs";
import { LabeledProcessRunner } from "../lib";

const runner = new LabeledProcessRunner();
import { runCommand } from "../lib";

export const docs = {
command: "docs",
Expand All @@ -14,17 +12,12 @@ export const docs = {
default: false,
}),
handler: async ({ stop }: { stop: boolean }) => {
await runner.run_command_and_output(
"Stop any existing container.",
["docker", "rm", "-f", "jekyll"],
"docs",
);
await runCommand("docker", ["rm", "-f", "jekyll"], "docs");

if (!stop) {
await runner.run_command_and_output(
"Run docs at http://localhost:4000",
await runCommand(
"docker",
[
"docker",
"run",
"--rm",
"-i",
Expand Down
16 changes: 3 additions & 13 deletions bin/cli/src/commands/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Argv } from "yargs";
import { checkIfAuthenticated, LabeledProcessRunner } from "../lib";

const runner = new LabeledProcessRunner();
import { checkIfAuthenticated, runCommand } from "../lib";

export const e2e = {
command: "e2e",
Expand All @@ -14,16 +12,8 @@ export const e2e = {
}),
handler: async ({ ui }: { ui: boolean }) => {
await checkIfAuthenticated();
await runner.run_command_and_output(
"Install playwright",
["bun", "playwright", "install", "--with-deps"],
".",
);
await runCommand("bun", ["playwright", "install", "--with-deps"], ".");

await runner.run_command_and_output(
ui ? "e2e:ui tests" : "e2e tests",
["bun", ui ? "e2e:ui" : "e2e"],
".",
);
await runCommand("bun", [ui ? "e2e:ui" : "e2e"], ".");
},
};
11 changes: 2 additions & 9 deletions bin/cli/src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { LabeledProcessRunner } from "../lib";

const runner = new LabeledProcessRunner();
import { runCommand } from "../lib";

export const install = {
command: "install",
describe: "install all project dependencies",
handler: async () => {
await runner.run_command_and_output(
"Install",
["bun", "install"],
".",
true,
);
await runCommand("bun", ["install"], ".");
},
};
10 changes: 4 additions & 6 deletions bin/cli/src/commands/logs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Argv } from "yargs";
import {
checkIfAuthenticated,
LabeledProcessRunner,
runCommand,
project,
region,
setStageFromBranch,
Expand All @@ -20,8 +20,6 @@ import prompts from "prompts";

const lambdaClient = new LambdaClient({ region });

const runner = new LabeledProcessRunner();

export const logs = {
command: "logs",
describe: "Stream a lambda's cloudwatch logs.",
Expand Down Expand Up @@ -79,9 +77,9 @@ export const logs = {
const lambdaLogGroup = await getLambdaLogGroup(lambda);

// Stream the logs
await runner.run_command_and_output(
"stream awslogs",
["awslogs", "get", lambdaLogGroup, "-s10m", "--watch"],
await runCommand(
"awslogs",
["get", lambdaLogGroup, "-s10m", "--watch"],
".",
);
},
Expand Down
10 changes: 2 additions & 8 deletions bin/cli/src/commands/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Argv } from "yargs";
import { LabeledProcessRunner } from "../lib";

const runner = new LabeledProcessRunner();
import { runCommand } from "../lib";

export const test = {
command: "test",
Expand Down Expand Up @@ -33,10 +31,6 @@ export const test = {
if (argv.ui) {
testCommand = "test:ui";
}
await runner.run_command_and_output(
"Unit Tests",
["bun", "run", testCommand],
".",
);
await runCommand("bun", ["run", testCommand], ".");
},
};
16 changes: 3 additions & 13 deletions bin/cli/src/commands/ui.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Argv } from "yargs";
import {
checkIfAuthenticated,
LabeledProcessRunner,
runCommand,
setStageFromBranch,
writeUiEnvFile,
} from "../lib";

const runner = new LabeledProcessRunner();

export const ui = {
command: "ui",
describe: "Run react-server locally against an aws backend",
Expand All @@ -18,15 +16,7 @@ export const ui = {
await checkIfAuthenticated();
const stage = options.stage || (await setStageFromBranch());
await writeUiEnvFile(stage, true);
await runner.run_command_and_output(
`Build`,
["bun", "run", "build"],
"react-app",
);
await runner.run_command_and_output(
`Run`,
["bun", "run", "dev"],
`react-app`,
);
await runCommand("bun", ["run", "build"], "react-app");
await runCommand("bun", ["run", "dev"], `react-app`);
},
};
148 changes: 30 additions & 118 deletions bin/cli/src/lib/runner.ts
Original file line number Diff line number Diff line change
@@ -1,126 +1,38 @@
import { spawn } from "child_process";

// LabeledProcessRunner is a command runner that interleaves the output from different
// calls to run_command_and_output each with their own prefix
export class LabeledProcessRunner {
private prefixColors: Record<string, string> = {};
private colors = [
"1",
"2",
"3",
"4",
"5",
"6",
"9",
"10",
"11",
"12",
"13",
"14",
];

// formattedPrefix pads the prefix for a given process so that all prefixes are
// right aligned in your terminal.
private formattedPrefix(prefix: string): string {
let color: string;

if (prefix! in this.prefixColors) {
color = this.prefixColors[prefix];
} else {
const frontColor = this.colors.shift();
if (frontColor != undefined) {
color = frontColor;
this.colors.push(color);
this.prefixColors[prefix] = color;
} else {
throw "dev.ts programming error";
}
}

let maxLength = 0;
for (const pre in this.prefixColors) {
if (pre.length > maxLength) {
maxLength = pre.length;
}
}

return `\x1b[38;5;${color}m ${prefix.padStart(maxLength)}|\x1b[0m`;
import { spawn, SpawnOptions } from "child_process";
import path from "path";

export async function runCommand(
command: string,
args: string[],
cwd: string | null,
): Promise<void> {
// Resolve the full path of the working directory
const fullPath = cwd ? path.resolve(cwd) : null;

// Print the command and arguments
console.log(`Executing command: ${command} ${args.join(" ")}`);
if (fullPath) {
console.log(`Working directory: ${fullPath}`);
}

private sanitizeInput(input) {
// A basic pattern that allows letters, numbers, dashes, underscores, and periods
// Adjust the pattern to fit the expected input format
const sanitizedInput = input.replace(/[^a-zA-Z0-9-_.]/g, "");
return sanitizedInput;
}
return new Promise((resolve, reject) => {
const options: SpawnOptions = fullPath
? { cwd: fullPath, stdio: ["inherit", "inherit", "inherit"] }
: { stdio: ["inherit", "inherit", "inherit"] };

// run_command_and_output runs the given shell command and interleaves its output with all
// other commands run via this method.
//
// prefix: the prefix to display at the start of every line printed by this command
// cmd: an array containing the command and all arguments to the command to be run
// cwd: optional directory to change into before running the command
// returns a promise that errors if the command exits error and resolves on success
async run_command_and_output(
prefix: string,
cmd: string[],
cwd: string | null,
catchAll = false,
silenced: {
open?: boolean;
stdout?: boolean;
stderr?: boolean;
close?: boolean;
} = {},
) {
silenced = {
...{ open: false, stdout: false, stderr: false, close: false },
...silenced,
};
const proc_opts = cwd ? { cwd } : {};
const proc = spawn(command, args, options);

const command = this.sanitizeInput(cmd[0]);
const args = cmd.slice(1);

const proc = spawn(command, args, proc_opts);
const paddedPrefix = `[${prefix}]`;
if (!silenced.open)
process.stdout.write(`${paddedPrefix} Running: ${cmd.join(" ")}\n`);

const handleOutput = (data: Buffer, prefix: string, silenced: boolean) => {
const paddedPrefix = this.formattedPrefix(prefix);
if (!silenced)
for (const line of data.toString().split("\n")) {
process.stdout.write(`${paddedPrefix} ${line}\n`);
}
};

proc.stdout.on("data", (data) =>
handleOutput(data, prefix, silenced.stdout!),
);
proc.stderr.on("data", (data) =>
handleOutput(data, prefix, silenced.stderr!),
);

return new Promise<void>((resolve, reject) => {
proc.on("error", (error) => {
if (!silenced.stderr)
process.stdout.write(`${paddedPrefix} A PROCESS ERROR: ${error}\n`);
reject(error);
});
proc.on("error", (error) => {
console.error(`Error: ${error.message}`);
reject(error);
});

proc.on("close", (code) => {
if (!silenced.close)
process.stdout.write(`${paddedPrefix} Exit: ${code}\n`);
// If there's a failure and we haven't asked to catch all...
if (code != 0 && !catchAll) {
// This is not my area.
// Deploy failures don't get handled and show up here with non zero exit codes
// Here we throw an error. Not sure what's best.
throw `Exit ${code}`;
}
proc.on("close", (code) => {
if (code !== 0) {
reject(new Error(`Command failed with exit code ${code}`));
} else {
resolve();
});
}
});
}
});
}
Loading