基于 TS Server 的 封装

2,246 阅读2分钟

tl;dr github.com/shiyangzhao…

TS Server

TS Server 指的是 TypeScript 独立服务器,本质上也是一个 tsserver.js 可执行的文件,可以当作一个独立的进程启动。VSCode 是通过 LSP 向 TS Server 发送消息,然后根据返回的数据做类型提示和代码补全。

image.png

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 对两种通信方式都支持。

image.png

因为 ipc 的通信方式是后面支持的,所以早期的通信只能通过 io 的形式,vscode 内部对 TypeScript 的版本做了判断,版本大于 v4.60 的才使用 ipc 通信:

image.png

image.png

调试

文档

简单来说,就是把 TypeScript 项目 clone 下来,build 以后执行 TSS_DEBUG=5667 code --user-data-dir ~/.vscode-debug/,选择一个项目。

这里需要打开一个 TS 文件,因为在你不打开文件的情况下,是不知道你是 ts 项目的,这个时候通信的话,会报错

image.png

打开 log 文件

image.png

记录了通信的信息

image.png

也可以通过给 tsserver.js 打断点来调试,但我觉得不是很好用,不如 log 的信息清晰。

过程

image.png

VSCode 会对代码进行 format,如果代码量比较大,会采用了分批处理数据的形式,通信的 buffer 数据包含 ContentLength 字段:

image.png

Client 端(VSCode)处理返回的数据,会把每次收到的 buffer 缓存起来,通过给定消息的长度和收到信息的长度比较,判断消息是否完整:

image.png

封装的用处

  1. 给自己的编辑器添加类型提示和代码补全
  2. 依赖分析,比如通过脚本查找某个变量被哪些文件引用,配合 git diff + ast 解析,查询某个 commit 的影响范围和类型说明。 image.png image.png
  3. ...

如何使用

  1. 新建一个测试项目 test(npm init + tsc --init) image.png

    // 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;
    
  2. 使用 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
    

    类型提示信息:

    image.png image.png 依赖查找:

    const result = await client.execute(
      'references',
      {
        file: filePath,
        line: 1,
        offset: 15,
      },
    );
    
    if (result.type === 'response') {
      console.log('result---', result.body.refs);
    }
    

    image.png image.png

总结

很久以前写的项目,过程细节记得不太清楚。当时是用来做依赖分析的,感觉还是可以做很多有意思的事情,更多的功能有待发掘。感谢阅读 ==