Skip to content

Commit ae2576c

Browse files
committed
Convert terminal UI to TypeScript
1 parent 0cd2226 commit ae2576c

13 files changed

Lines changed: 438 additions & 257 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ node_modules
33
dist
44
.next
55
out
6-
.vercel
6+
.vercel
7+
tsconfig.tsbuildinfo

eslint.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig([
1010
files: [
1111
"editor/**/*.{js,ts,tsx}",
1212
"runtime/**/*.{js,ts}",
13+
"terminal/**/*.ts",
1314
"test/**/*.{js,ts,tsx}",
1415
"app/**/*.{js,jsx,ts,tsx}",
1516
"lib/**/*.{js,ts}",
@@ -44,7 +45,14 @@ export default defineConfig([
4445
// TypeScript-specific configuration
4546
...tseslint.configs.recommended.map((config) => ({
4647
...config,
47-
files: ["editor/**/*.{ts,tsx}", "runtime/**/*.ts", "test/**/*.{ts,tsx}", "app/**/*.{ts,tsx}", "lib/**/*.ts"],
48+
files: [
49+
"editor/**/*.{ts,tsx}",
50+
"runtime/**/*.ts",
51+
"terminal/**/*.ts",
52+
"test/**/*.{ts,tsx}",
53+
"app/**/*.{ts,tsx}",
54+
"lib/**/*.ts",
55+
],
4856
})),
4957
{
5058
ignores: ["**/*.recho.js", "test/output/**/*"],

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,19 @@
2626
],
2727
"type": "module",
2828
"bin": {
29-
"recho": "./terminal/cli.js"
29+
"recho": "./terminal/cli.ts"
3030
},
3131
"scripts": {
3232
"dev": "vite",
33-
"tui": "node terminal/cli.js",
33+
"tui": "node terminal/cli.ts",
3434
"app:dev": "next dev",
3535
"app:build": "next build",
3636
"app:start": "next start",
37-
"test": "npm run test:lint && npm run test:format && npm run test:js",
37+
"test": "npm run test:lint && npm run test:format && npm run test:typecheck && npm run test:js",
3838
"test:js": "TZ=America/New_York vitest",
39-
"test:format": "prettier --check editor runtime test app",
40-
"test:lint": "eslint"
39+
"test:format": "prettier --check editor runtime terminal test app",
40+
"test:lint": "eslint",
41+
"test:typecheck": "tsc --noEmit"
4142
},
4243
"devDependencies": {
4344
"@tailwindcss/postcss": "^4.1.18",
Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,49 @@
2424
import {parentPort} from "node:worker_threads";
2525

2626
if (!parentPort) {
27-
throw new Error("runtime/worker.js must be loaded as a worker_thread.");
27+
throw new Error("runtime/worker.ts must be loaded as a worker_thread.");
2828
}
2929

30+
const port = parentPort;
31+
32+
type WorkerPostMessage =
33+
| {type: "ready"}
34+
| {type: "changes"; changes: unknown[]}
35+
| {type: "error"; error: SerializedError; source: string}
36+
| {type: "console"; level: ConsoleLevel; text: string}
37+
| {type: "heartbeat"; t: number}
38+
| {type: "online"};
39+
40+
type WorkerCommand =
41+
| {type: "init"; code: string; options?: RuntimeOptions}
42+
| {type: "setCode"; code: string}
43+
| {type: "setIsRunning"; value: boolean}
44+
| {type: "run"}
45+
| {type: "destroy"};
46+
47+
type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
48+
type RuntimeOptions = {cellTimeoutMs?: number};
49+
type SerializedError = {message: string; name: string; stack: string | null; code: unknown};
50+
type RuntimeInstance = {
51+
destroy?: () => void;
52+
onChanges: (callback: (event: {changes: unknown[]}) => void) => void;
53+
onError: (callback: (event: {error: unknown; source?: string}) => void) => void;
54+
setIsRunning: (value: boolean) => void;
55+
setCode: (code: string) => void;
56+
run: () => void;
57+
};
58+
3059
// -- Capture console & stderr BEFORE loading the runtime, so anything the
3160
// stdlib (or its observer.rejected path) writes during boot is forwarded.
32-
function safePost(msg) {
61+
function safePost(msg: WorkerPostMessage) {
3362
try {
34-
parentPort.postMessage(msg);
63+
port.postMessage(msg);
3564
} catch {
3665
/* parent gone */
3766
}
3867
}
3968

40-
function stringifyArgs(args) {
69+
function stringifyArgs(args: unknown[]): string {
4170
return args
4271
.map((a) => {
4372
if (a instanceof Error) return (a.stack ? a.stack : a.message) || String(a);
@@ -51,28 +80,33 @@ function stringifyArgs(args) {
5180
.join(" ");
5281
}
5382

54-
function serializeError(e) {
55-
if (!e) return {message: "(unknown error)"};
83+
function serializeError(e: unknown): SerializedError {
84+
if (!e) return {message: "(unknown error)", name: "Error", stack: null, code: null};
85+
const error = e as {message?: string; name?: string; stack?: string; code?: unknown};
5686
return {
57-
message: e.message || String(e),
58-
name: e.name || "Error",
59-
stack: e.stack || null,
60-
code: e.code || null,
87+
message: error.message || String(e),
88+
name: error.name || "Error",
89+
stack: error.stack || null,
90+
code: error.code || null,
6191
};
6292
}
6393

64-
for (const level of ["log", "info", "warn", "error", "debug"]) {
94+
for (const level of ["log", "info", "warn", "error", "debug"] satisfies ConsoleLevel[]) {
6595
console[level] = (...args) => safePost({type: "console", level, text: stringifyArgs(args)});
6696
}
6797

6898
const origStderrWrite = process.stderr.write.bind(process.stderr);
69-
process.stderr.write = (chunk, encoding, callback) => {
99+
process.stderr.write = ((
100+
chunk: string | Uint8Array,
101+
encoding?: BufferEncoding | ((err?: Error | null) => void),
102+
callback?: (err?: Error | null) => void,
103+
) => {
70104
const text = typeof chunk === "string" ? chunk : (chunk?.toString?.() ?? String(chunk));
71105
if (text.trim()) safePost({type: "console", level: "error", text: text.replace(/\n+$/, "")});
72106
if (typeof encoding === "function") encoding();
73107
else if (typeof callback === "function") callback();
74108
return true;
75-
};
109+
}) as typeof process.stderr.write;
76110

77111
// Process-level catch-alls — async failures in notebook code shouldn't kill
78112
// the worker (and even if they do, the main thread will just respawn).
@@ -90,16 +124,16 @@ process.on("uncaughtException", (e) => {
90124
const HEARTBEAT_MS = 200;
91125
setInterval(() => safePost({type: "heartbeat", t: Date.now()}), HEARTBEAT_MS).unref();
92126

93-
let rt = null;
127+
let rt: RuntimeInstance | null = null;
94128

95-
function bootRuntime(code, options) {
129+
function bootRuntime(code: string, options?: RuntimeOptions) {
96130
rt?.destroy?.();
97131
rt = null;
98132

99133
// Lazy import so the heartbeat above is already running when the runtime
100134
// (and its dependencies) get evaluated.
101135
return import("./index.js").then(({createRuntime}) => {
102-
rt = createRuntime(code, options || {});
136+
rt = createRuntime(code, options || {}) as RuntimeInstance;
103137
rt.onChanges((evt) => safePost({type: "changes", changes: evt.changes}));
104138
rt.onError((evt) => safePost({type: "error", error: serializeError(evt.error), source: evt.source || "runtime"}));
105139
rt.setIsRunning(true);
@@ -112,7 +146,7 @@ function bootRuntime(code, options) {
112146
});
113147
}
114148

115-
parentPort.on("message", (msg) => {
149+
port.on("message", (msg: WorkerCommand) => {
116150
switch (msg?.type) {
117151
case "init":
118152
bootRuntime(msg.code, msg.options).catch((e) =>
@@ -151,7 +185,7 @@ parentPort.on("message", (msg) => {
151185
default:
152186
// Unknown message — log to original stderr (which we replaced above).
153187
try {
154-
origStderrWrite(`recho-worker: unknown message type ${JSON.stringify(msg?.type)}\n`);
188+
origStderrWrite(`recho-worker: unknown message type ${JSON.stringify((msg as {type?: unknown}).type)}\n`);
155189
} catch {
156190
/* ignore */
157191
}

0 commit comments

Comments
 (0)