1import { ChildProcess, fork } from 'node:child_process';
2import readline from 'node:readline';
3import ts from 'typescript/lib/tsserverlibrary';
4
5type Command = `${ts.server.protocol.CommandTypes}`;
6
7export class TSServer {
8 #server: ChildProcess;
9 #seq = 0;
10 #resolvePromise: (() => void) | undefined;
11 #waitFor:
12 | ((
13 response: ts.server.protocol.Response | ts.server.protocol.Event
14 ) => boolean)
15 | undefined;
16
17 responses: Array<ts.server.protocol.Response | ts.server.protocol.Event> = [];
18
19 constructor(
20 public projectPath: string,
21 public options: { debugLog?: boolean } = {}
22 ) {
23 const tsserverPath = require.resolve('typescript/lib/tsserver');
24
25 const server = fork(tsserverPath, ['--logVerbosity', 'verbose'], {
26 stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
27 cwd: projectPath,
28 env: {
29 TSS_LOG:
30 '-level verbose -traceToConsole false -logToFile true -file ./tsserver.log',
31 },
32 });
33
34 if (!server?.stdout) {
35 throw new Error('Failed to start tsserver');
36 }
37
38 server.stdout.setEncoding('utf-8');
39 readline.createInterface({ input: server.stdout }).on('line', line => {
40 if (!line.startsWith('{')) return;
41
42 try {
43 const data = JSON.parse(line);
44
45 this.responses.push(data);
46
47 if (this.#resolvePromise && this.#waitFor?.(data)) {
48 this.#resolvePromise();
49 this.#waitFor = undefined;
50 this.#resolvePromise = undefined;
51 }
52
53 if (options.debugLog) {
54 console.log(data);
55 }
56 } catch (e) {
57 console.error(e);
58 }
59 });
60
61 this.#server = server;
62 }
63
64 sendCommand(command: Command, args?: Record<string, unknown>) {
65 this.send({ command, arguments: args });
66 }
67
68 send(data: {}) {
69 const request = JSON.stringify({
70 seq: ++this.#seq,
71 type: 'request',
72 ...data,
73 });
74
75 this.#server.stdin?.write(`${request}\n`);
76 }
77
78 waitForResponse = (
79 cb: (
80 response: ts.server.protocol.Response | ts.server.protocol.Event
81 ) => boolean
82 ) => {
83 this.#waitFor = cb;
84 return new Promise<void>(resolve => {
85 this.#resolvePromise = resolve;
86 });
87 };
88
89 close() {
90 this.#server.kill();
91 }
92}