1import { useEffect, useRef, useState } from "react";
2import { Line, sleep, TermChrome, useSpinner } from "./_terminal";
3
4interface Step {
5 kind: "boot" | "ready" | "edit" | "reload" | "diff" | "wire" | "request";
6 label?: string;
7 detail?: string;
8 ms: number;
9}
10
11const STEPS: Step[] = [
12 { kind: "boot", label: "Photos", detail: "Cloudflare.R2.Bucket", ms: 600 },
13 {
14 kind: "boot",
15 label: "Sessions",
16 detail: "Cloudflare.KV.Namespace",
17 ms: 500,
18 },
19 {
20 kind: "boot",
21 label: "Api",
22 detail: "Cloudflare.Worker · local → workerd",
23 ms: 700,
24 },
25 { kind: "ready", ms: 700 },
26 { kind: "edit", label: "src/Api.ts", ms: 900 },
27 { kind: "reload", detail: "38ms", ms: 600 },
28 { kind: "request", label: "GET /object/hello.txt", detail: "200", ms: 900 },
29 { kind: "edit", label: "alchemy.run.ts", ms: 1000 },
30 { kind: "diff", label: "Queue", detail: "Cloudflare.Queues.Queue", ms: 700 },
31 { kind: "wire", label: "Api → Queue.bind", ms: 700 },
32 { kind: "ready", ms: 1100 },
33];
34
35interface LogLine {
36 text: React.ReactNode;
37 key: string;
38}
39
40let _id = 0;
41
42export default function DevTerminal({
43 title = "~/my-app",
44}: {
45 title?: string;
46}) {
47 const [lines, setLines] = useState<LogLine[]>([]);
48 const [busy, setBusy] = useState(false);
49 const cancelRef = useRef(false);
50
51 useEffect(() => {
52 cancelRef.current = false;
53 const aborted = () => cancelRef.current;
54
55 const push = (text: React.ReactNode) =>
56 setLines((ls) => [...ls, { text, key: `${++_id}` }].slice(-14));
57
58 const reset = () => setLines([]);
59
60 const run = async () => {
61 while (!aborted()) {
62 reset();
63 push(
64 <>
65 <span style={{ color: "var(--alc-code-comment)" }}>$ </span>
66 <span>alchemy dev</span>
67 </>,
68 );
69 await sleep(300);
70 if (aborted()) return;
71
72 for (const step of STEPS) {
73 if (aborted()) return;
74 if (step.kind === "boot") {
75 setBusy(true);
76 await sleep(step.ms);
77 setBusy(false);
78 push(
79 <>
80 <span style={{ color: "var(--alc-success)" }}>✓ </span>
81 <span
82 style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}
83 >
84 {step.label}
85 </span>
86 <span
87 style={{ color: "var(--alc-code-comment)" }}
88 >{` (${step.detail})`}</span>
89 <span style={{ color: "var(--alc-success)" }}> created</span>
90 </>,
91 );
92 } else if (step.kind === "ready") {
93 await sleep(step.ms);
94 push(
95 <>
96 <span style={{ color: "var(--alc-code-comment)" }}> → </span>
97 <span style={{ color: "var(--alc-accent-bright)" }}>
98 http://localhost:1337
99 </span>
100 </>,
101 );
102 push(
103 <>
104 <span style={{ color: "var(--alc-code-comment)" }}>
105 Watching for changes…
106 </span>
107 </>,
108 );
109 } else if (step.kind === "edit") {
110 await sleep(step.ms);
111 push(
112 <>
113 <span style={{ color: "var(--alc-warn)" }}>↻ </span>
114 <span style={{ color: "var(--alc-code-comment)" }}>
115 {step.label} changed
116 </span>
117 </>,
118 );
119 } else if (step.kind === "reload") {
120 setBusy(true);
121 await sleep(step.ms);
122 setBusy(false);
123 push(
124 <>
125 <span style={{ color: "var(--alc-success)" }}>✓ </span>
126 <span
127 style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}
128 >
129 Api
130 </span>
131 <span
132 style={{ color: "var(--alc-code-comment)" }}
133 >{` reloaded in `}</span>
134 <span
135 style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}
136 >
137 {step.detail}
138 </span>
139 </>,
140 );
141 } else if (step.kind === "diff") {
142 await sleep(step.ms);
143 push(
144 <>
145 <span style={{ color: "var(--alc-success)" }}>+ </span>
146 <span
147 style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}
148 >
149 {step.label}
150 </span>
151 <span
152 style={{ color: "var(--alc-code-comment)" }}
153 >{` (${step.detail})`}</span>
154 <span style={{ color: "var(--alc-success)" }}> created</span>
155 </>,
156 );
157 } else if (step.kind === "wire") {
158 await sleep(step.ms);
159 push(
160 <>
161 <span style={{ color: "var(--alc-warn)" }}>~ </span>
162 <span style={{ color: "var(--alc-code-comment)" }}>wired </span>
163 <span style={{ color: "var(--alc-code-type)" }}>
164 {step.label}
165 </span>
166 </>,
167 );
168 } else if (step.kind === "request") {
169 await sleep(step.ms);
170 push(
171 <>
172 <span
173 style={{ color: "var(--alc-code-comment)" }}
174 >{`[${new Date().toLocaleTimeString().slice(0, 8)}] `}</span>
175 <span style={{ color: "var(--alc-fg-invert)" }}>
176 {step.label}
177 </span>
178 <span
179 style={{ color: "var(--alc-success)" }}
180 >{` ${step.detail}`}</span>
181 </>,
182 );
183 }
184 }
185 await sleep(2400);
186 }
187 };
188
189 run();
190 return () => {
191 cancelRef.current = true;
192 };
193 }, []);
194
195 const spinner = useSpinner(busy);
196
197 return (
198 <TermChrome
199 title={title}
200 badge="DEV"
201 badgeColor="var(--alc-accent-bright)"
202 maxLines={15}
203 >
204 {lines.map((l) => (
205 <Line key={l.key}>{l.text}</Line>
206 ))}
207 {busy && (
208 <Line>
209 <span style={{ color: "var(--alc-code-comment)" }}>{spinner} </span>
210 </Line>
211 )}
212 </TermChrome>
213 );
214}