Files
scriptc/tests/fixtures/self-hosting/ts7-native-process.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

116 lines
5.2 KiB
TypeScript

import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { NativeTs7Process } from "../../../packages/compiler/src/frontend/ts7/native-process.js";
import { createNativeTs7Api } from "../../../packages/compiler/src/frontend/ts7/native-api.js";
import { Ts7RpcClient } from "../../../packages/compiler/src/frontend/ts7/rpc-client.js";
import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js";
import { runTs7Client } from "./ts7-client-cases.js";
function check(value: boolean, label: string): void {
if (!value) throw new Error(label);
}
function checkApi(executable: string, directory: string): void {
const path = join(directory, "native-api.ts").replace(/\\/g, "/");
const config = join(directory, "native-api.tsconfig.json").replace(/\\/g, "/");
let text = "export const answer = 42;\n";
const api = createNativeTs7Api({
executable, cwd: directory, collectTiming: true,
fs: {
readFile: (file) => file === path ? text : file === config ? JSON.stringify({
compilerOptions: { strict: true, noEmit: true, types: [] as string[] }, files: [path],
}) : undefined,
fileExists: (file) => file === path || file === config ? true : undefined,
directoryExists: () => undefined,
realpath: () => undefined,
getAccessibleEntries: () => undefined,
},
});
try {
check(api.parseConfigFile(config).fileNames.includes(path), "native API config callback");
const first = api.updateSnapshot({ openProjects: [config] });
const project = first.getProject(config)!;
check(project.program.getSourceFile(path)!.text === text, "native API source");
check(project.program.getSemanticDiagnostics(path).length === 0, "native API valid program");
text = 'export const answer: number = "bad";\n';
const second = api.updateSnapshot({ fileChanges: { changed: [path] } });
check(second.getProject(config)!.program.getSemanticDiagnostics(path).some((d) => d.code === 2322), "native API updated diagnostics");
check(project.program.getSemanticDiagnostics(path).length === 0, "native API old snapshot");
check(api.getTimingInfo().totals.sourceFilesFetched > 0, "native API timing");
first.dispose();
second.dispose();
} finally {
api.close();
api.close();
}
let refused = false;
try { api.updateSnapshot(); } catch { refused = true; }
check(refused, "closed native API");
}
function checkTransport(executable: string): void {
let failed = false;
try { new NativeTs7Process(executable + ".missing", "echo"); }
catch (error) { failed = error instanceof Error && error.message.startsWith("TypeScript server startup:"); }
check(failed, "native startup error");
failed = false;
try { new NativeTs7Process(executable, "bad\0cwd"); } catch { failed = true; }
check(failed, "embedded NUL refused");
const child = new NativeTs7Process(executable, "echo");
check(child.pid > 0, "native PID");
try {
const storage = new Uint8Array([90, 91, 0, 255, 128, 33, 92, 93]);
const view = storage.subarray(1, 7);
let sent = 0;
while (sent < 4) sent += child.write(view, 1 + sent, 4 - sent);
const output = new Uint8Array(8);
for (let i = 0; i < output.length; i++) output[i] = 77;
const destination = output.subarray(1, 7);
let received = 0;
while (received < 4) received += child.read(destination, 1 + received, 4 - received);
check(output.join(",") === "77,77,0,255,128,33,77,77", "mutable FFI view and offsets");
check(child.read(view, view.length, 0) === 0 && child.write(view, 0, 0) === 0, "zero transfers");
for (const offset of [-1, 0.5, 8, Number.NaN]) {
failed = false;
try { child.read(view, offset, 1); } catch (error) { failed = error instanceof RangeError; }
check(failed, "invalid native read offset");
failed = false;
try { child.write(view, offset, 1); } catch (error) { failed = error instanceof RangeError; }
check(failed, "invalid native write offset");
}
} finally {
child.close();
child.close();
}
failed = false;
try { child.read(new Uint8Array(0), 0, 0); } catch { failed = true; }
check(failed, "closed transport refuses reads");
const exited = new NativeTs7Process(executable, "exit");
const byte = new Uint8Array(1);
check(exited.read(byte, 0, 1) === 0, "native EOF");
failed = false;
try { exited.write(byte, 0, 1); } catch { failed = true; }
check(failed, "native broken pipe");
exited.close();
console.log("native transport passed");
}
const executable = process.argv[2]!;
const directory = process.argv[3]!;
const report = process.argv[4]!;
const mode = process.argv[5] ?? "client";
if (mode === "transport") {
checkTransport(executable);
} else if (mode === "exit" || mode === "normal-exit") {
const child = new NativeTs7Process(executable, "stall");
const byte = new Uint8Array(1);
check(child.read(byte, 0, 1) === 1 && byte[0] === 33, "server started before parent exit");
writeFileSync(report, String(child.pid));
if (mode === "exit") process.exit(0);
} else {
checkApi(executable, directory);
const child = new NativeTs7Process(executable, directory, true);
runTs7Client(new Ts7RpcClient(new Ts7Wire(child)), directory, report);
child.close();
}