Skip to content

Commit ef67413

Browse files
authored
feat: Implement build UI (#55)
* chore: Install ink * chore: build/index.ts -> build/index.tsx * chore: Install react * chore: Create Spinner component * feat: Implement build ui * fix: Fix ui
1 parent 8ccd8f0 commit ef67413

File tree

6 files changed

+247
-62
lines changed

6 files changed

+247
-62
lines changed

bun.lock

Lines changed: 73 additions & 7 deletions
Large diffs are not rendered by default.

lib/cli/build/index.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

lib/cli/build/index.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as fs from "node:fs";
2+
import { register } from "node:module";
3+
import * as path from "node:path";
4+
import { pathToFileURL } from "node:url";
5+
import { render } from "ink";
6+
import BuildUI from "./ui";
7+
8+
export async function build(args: string[]) {
9+
const ghatsDir = path.resolve(process.cwd(), "node_modules/.ghats");
10+
const actionJsPath = path.join(ghatsDir, "action.js");
11+
if (!fs.existsSync(actionJsPath)) {
12+
fs.mkdirSync(ghatsDir, { recursive: true });
13+
fs.writeFileSync(actionJsPath, "export {}");
14+
}
15+
16+
register("@swc-node/register/esm", pathToFileURL("./"));
17+
18+
const workflowPaths: string[] = [];
19+
if (args.length > 0) {
20+
workflowPaths.push(...args);
21+
} else {
22+
const githubWorkflowsPath = ".github/workflows";
23+
workflowPaths.push(
24+
...fs
25+
.readdirSync(githubWorkflowsPath)
26+
.filter((file) => {
27+
if (!file.endsWith(".ts")) return false;
28+
if (path.basename(file).startsWith("_")) return false;
29+
30+
return true;
31+
})
32+
.map((file) => path.join(githubWorkflowsPath, file))
33+
);
34+
}
35+
36+
const { unmount, rerender } = render(
37+
<BuildUI workflowPaths={workflowPaths} currentIndex={0} />
38+
);
39+
try {
40+
for (
41+
let currentIndex = 0;
42+
currentIndex < workflowPaths.length;
43+
currentIndex++
44+
) {
45+
const workflowPath = workflowPaths[currentIndex];
46+
await _buildWorkflow(workflowPath!);
47+
rerender(
48+
<BuildUI
49+
workflowPaths={workflowPaths}
50+
currentIndex={currentIndex + 1}
51+
/>
52+
);
53+
}
54+
} finally {
55+
unmount();
56+
}
57+
}
58+
59+
async function _buildWorkflow(workflowPath: string) {
60+
const module = await import(path.resolve(process.cwd(), workflowPath));
61+
const workflowYml = JSON.stringify(module.default);
62+
63+
const dirname = path.dirname(workflowPath);
64+
const filenameWithoutExtension = path.basename(
65+
workflowPath,
66+
path.extname(workflowPath)
67+
);
68+
69+
fs.mkdirSync(dirname, { recursive: true });
70+
fs.writeFileSync(
71+
getBuildTargetPath(workflowPath),
72+
[
73+
"# DO NOT EDIT THIS FILE",
74+
"# This file is automatically generated by ghats (https://www.npmjs.com/package/ghats)",
75+
`# Edit the workflow in .github/workflows/${filenameWithoutExtension}.ts instead, and run \`ghats build\` to update this file.`,
76+
workflowYml,
77+
].join("\n")
78+
);
79+
}
80+
81+
export function getBuildTargetPath(workflowPath: string) {
82+
const dirname = path.dirname(workflowPath);
83+
const filenameWithoutExtension = path.basename(
84+
workflowPath,
85+
path.extname(workflowPath)
86+
);
87+
88+
return path.join(dirname, `${filenameWithoutExtension}.yml`);
89+
}

lib/cli/build/ui.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Box, Text } from "ink";
2+
import Spinner from "../ui/Spinner";
3+
import { getBuildTargetPath } from ".";
4+
5+
type BuildUIProps = {
6+
workflowPaths: string[];
7+
currentIndex: number;
8+
};
9+
10+
type Status = "pending" | "building" | "done";
11+
12+
function getStatus(index: number, currentIndex: number): Status {
13+
if (index < currentIndex) return "done";
14+
if (index === currentIndex) return "building";
15+
return "pending";
16+
}
17+
18+
export default function BuildUI({ workflowPaths, currentIndex }: BuildUIProps) {
19+
return (
20+
<Box flexDirection="column">
21+
{workflowPaths.map((workflowPath, index) => (
22+
<WorkflowPath
23+
key={workflowPath}
24+
status={getStatus(index, currentIndex)}
25+
workflowPath={workflowPath}
26+
/>
27+
))}
28+
</Box>
29+
);
30+
}
31+
32+
type WorkflowPathProps = {
33+
status: Status;
34+
workflowPath: string;
35+
};
36+
37+
function WorkflowPath({ status, workflowPath }: WorkflowPathProps) {
38+
switch (status) {
39+
case "pending":
40+
return (
41+
<Text dimColor>
42+
{" "}
43+
{workflowPath}
44+
</Text>
45+
);
46+
case "building":
47+
return (
48+
<Text>
49+
<Spinner />
50+
<Text dimColor> {workflowPath}</Text>
51+
</Text>
52+
);
53+
case "done":
54+
return (
55+
<Text>
56+
<Text color="green" bold>
57+
58+
</Text>
59+
<Text bold> {getBuildTargetPath(workflowPath)}</Text>
60+
</Text>
61+
);
62+
}
63+
}

lib/cli/ui/Spinner.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useEffect, useState } from "react";
2+
import { Text } from "ink";
3+
4+
const spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5+
6+
export default function Spinner() {
7+
const [spinnerIndex, setSpinnerIndex] = useState(0);
8+
9+
useEffect(() => {
10+
const interval = setInterval(() => {
11+
setSpinnerIndex((spinnerIndex) => (spinnerIndex + 1) % spinners.length);
12+
}, 50);
13+
14+
return () => clearInterval(interval);
15+
}, []);
16+
17+
return <Text color="cyan">{spinners[spinnerIndex]}</Text>;
18+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929
"dependencies": {
3030
"@swc-node/register": "1.10.10",
3131
"commander": "13.1.0",
32+
"ink": "5.2.0",
3233
"octokit": "4.1.2",
34+
"react": "18.3.1",
3335
"yaml": "2.7.1"
3436
},
3537
"devDependencies": {
3638
"@biomejs/biome": "2.0.0-beta.1",
3739
"@types/bun": "1.2.8",
40+
"@types/ink": "2.0.3",
41+
"@types/react": "18.3.20",
3842
"ghats": "0.7.0",
3943
"husky": "9.1.7",
4044
"lint-staged": "15.5.0",

0 commit comments

Comments
 (0)