tl;dr github.com/shiyangzhao…
TS Server
TS Server 指的是 TypeScript 独立服务器,本质上也是一个 tsserver.js 可执行的文件,可以当作一个独立的进程启动。VSCode 是通过 LSP 向 TS Server 发送消息,然后根据返回的数据做类型提示和代码补全。
const number: 123
就是 TS Server 返回的信息。
和 TS Server 之间的通信
TS Server 早期的通信方式标准的 I/O 的形式, child.stdin.write 向子进程的 stdin 发送消息,然后去监听,获取子进程 stdout 的数据:
child.stdout.on('data', data => {
// ...
}}
在 TypeScript 的 issue 下有人建议使用 ipc 作为通信方式,目前 TypeScript 对两种通信方式都支持。
因为 ipc 的通信方式是后面支持的,所以早期的通信只能通过 io 的形式,vscode 内部对 TypeScript 的版本做了判断,版本大于 v4.60 的才使用 ipc 通信:
调试
简单来说,就是把 TypeScript 项目 clone 下来,build 以后执行 TSS_DEBUG=5667 code --user-data-dir ~/.vscode-debug/
,选择一个项目。
这里需要打开一个 TS 文件,因为在你不打开文件的情况下,是不知道你是 ts 项目的,这个时候通信的话,会报错
打开 log 文件
记录了通信的信息
也可以通过给 tsserver.js 打断点来调试,但我觉得不是很好用,不如 log 的信息清晰。
过程
VSCode 会对代码进行 format,如果代码量比较大,会采用了分批处理数据的形式,通信的 buffer 数据包含 ContentLength 字段:
Client 端(VSCode)处理返回的数据,会把每次收到的 buffer 缓存起来,通过给定消息的长度和收到信息的长度比较,判断消息是否完整:
封装的用处
- 给自己的编辑器添加类型提示和代码补全
- 依赖分析,比如通过脚本查找某个变量被哪些文件引用,配合 git diff + ast 解析,查询某个 commit 的影响范围和类型说明。
- ...
如何使用
-
新建一个测试项目
test
(npm init + tsc --init)// a.ts export const num = 123; // b.ts import { num } from './a'; export const b = num + 1; // c.ts import { num } from './a'; export const c = num + 1;
-
使用
tsserver-lite
// index.mjs import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { client } from 'tsserver-lite'; const updateTSFile = async (info) => { const fileInfo = Object.assign( { changedFiles: [], closedFiles: [], openFiles: [], }, info, ); await client.execute('updateOpen', fileInfo); }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const filePath = path.resolve(__dirname, './a.ts'); const main = async () => { client.startService(); const openFile = { file: filePath, fileContent: fs.readFileSync(filePath, 'utf-8'), projectRootPath: path.resolve(__dirname, '../'), scriptKindName: 'ts', }; try { // 这个不能省略 await updateTSFile({ openFiles: [openFile] }); const result = await client.execute( 'quickinfo', { file: filePath, line: 1, offset: 15, }, ); if (result.type === 'response') { console.log('result---', result); } } catch (err) { console.log(`tsserver error: ${err.message}`); } client.closeService(); }; main().catch(err => { console.error(err); process.exit(1); });
node index.mjs
类型提示信息:
依赖查找:
const result = await client.execute( 'references', { file: filePath, line: 1, offset: 15, }, ); if (result.type === 'response') { console.log('result---', result.body.refs); }
总结
很久以前写的项目,过程细节记得不太清楚。当时是用来做依赖分析的,感觉还是可以做很多有意思的事情,更多的功能有待发掘。感谢阅读 ==