Files
scriptc/tests/harness/ts7-client-harness.ts
Chris Tate 806b96d32b feat(compiler): own the TypeScript server from native clients
- Launch and manage TypeScript server processes through a portable native FFI boundary.
- Share the production session API between Node and statically compiled clients.
- Preserve Unicode command-line arguments and release server resources on every exit path.
2026-09-28 09:54:18 -05:00

63 lines
2.8 KiB
TypeScript

import { spawn, type ChildProcess } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { expect } from "vitest";
import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js";
import { TS7_FILE_SYSTEM_CALLBACKS } from "../../packages/compiler/src/frontend/ts7/rpc-filesystem.js";
const root = fileURLToPath(new URL("../..", import.meta.url));
function result(child: ChildProcess): Promise<{ status: number | null; signal: string | null; stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`TypeScript client timed out: ${stderr}`));
}, 45_000);
child.stdout?.setEncoding("utf8").on("data", (chunk: string) => { stdout += chunk; });
child.stderr?.setEncoding("utf8").on("data", (chunk: string) => { stderr += chunk; });
child.once("error", (error) => { clearTimeout(timeout); reject(error); });
child.once("close", (status, signal) => {
clearTimeout(timeout);
resolve({ status, signal, stdout, stderr });
});
});
}
export async function runClient(command: string, args: string[], directory: string, report: string): Promise<string> {
const server = spawn(ts7Executable(), ["--api", "--timing", "--cwd", directory, `--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`], {
stdio: ["pipe", "pipe", "pipe"],
});
let serverStderr = "";
server.stderr.setEncoding("utf8").on("data", (chunk: string) => { serverStderr += chunk; });
const serverDone = new Promise<void>((resolve) => { server.once("close", () => resolve()); });
let serverError: Error | undefined;
server.on("error", (error) => { serverError = error; });
try {
// Node creates POSIX pipes nonblocking. Both consumers below use sync
// reads; change that property before inheriting the underlying handles.
if (process.platform !== "win32") {
for (const stream of [server.stdin, server.stdout]) {
(stream as unknown as { _handle: { setBlocking: (value: boolean) => void } })._handle.setBlocking(true);
}
}
const child = spawn(command, [...args, directory, report], {
cwd: root, stdio: ["ignore", "pipe", "pipe", server.stdout, server.stdin],
});
const actual = await result(child);
expect(serverError).toBeUndefined();
expect(actual.signal, actual.stderr + serverStderr).toBeNull();
expect(actual.status, actual.stderr + serverStderr).toBe(0);
expect(actual.stdout).toBe("");
expect(actual.stderr).toBe("");
expect(serverStderr).toBe("");
return readFileSync(report, "utf8");
} finally {
server.stdin.destroy();
server.stdout.destroy();
server.kill();
await serverDone;
}
}