Skip to content

Instantly share code, notes, and snippets.

@acutmore
Last active October 18, 2022 15:34
Show Gist options
  • Save acutmore/e01a70838d689013d447114b5c15b4b9 to your computer and use it in GitHub Desktop.
Save acutmore/e01a70838d689013d447114b5c15b4b9 to your computer and use it in GitHub Desktop.
A small wrapper around Typescript's tsserver for easier scripting
// @ts-check
const childProcess = require('child_process');
const path = require('path');
const TTL_MS = 5000;
/**
* @description Start a connection to tsserver
* @param {{debug?: boolean}} options
* @example
const chat = tsserverAPI.start();
await chat.read(msg => msg.event === 'typingsInstallerPid');
chat.send({
type: 'request',
command: 'open',
arguments: {
file: '/path/to/file.ts'
}
});
await chat.read(msg => msg.event === 'projectLoadingFinish');
const reply = await chat.send({
type: 'request',
command: 'implementation',
arguments: {
file: '/path/to/file.ts',
line: 6,
offset: 24
}
});
console.log(reply);
*/
function start({debug} = {}) {
const tsserverPath = path.join(path.dirname(require.resolve('typescript')), 'tsserver.js');
const proc = childProcess.fork(tsserverPath, [], {silent: true});
const unreadMessages = new Set();
const waitingReaders = new Set();
let readBuffer = '';
proc.stdout.on('data', buffer => {
readBuffer += buffer.toString('utf8');
const lines = readBuffer.split('\n');
readBuffer = '';
for (const line of lines) {
if (line.startsWith('{')) {
try {
const msg = JSON.parse(line);
if (debug) {
console.debug('>>', msg);
}
unreadMessages.add(msg);
setTimeout(() => {
unreadMessages.delete(msg);
}, TTL_MS);
waitingReaders.forEach(asleepReader => {
asleepReader();
});
} catch {
readBuffer = line;
continue;
}
}
}
});
let seq = 0;
/**
* @description Send 'msg' to the tsserver process
* @param {object} msg
* @returns {Promise<object>} The reponse from tsserver
*/
async function send(msg) {
const msgSeq = (msg.seq = ++seq);
proc.stdin.write(JSON.stringify(msg) + '\n');
return read(msg => msg.request_seq === msgSeq);
}
/**
* @description read the next message that matches 'condition'
* @param {Function} condition
* @returns {Promise<object>} the matched message
*/
async function read(condition) {
return new Promise(resolve => {
function reader() {
for (const msg of unreadMessages.values()) {
if (condition(msg)) {
unreadMessages.delete(msg);
waitingReaders.delete(reader);
resolve(msg);
break;
}
}
}
reader();
waitingReaders.add(reader);
});
}
return {
send,
read
};
}
module.exports = exports = {
start
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment