美文网首页Node.js
[Node] 随遇而安 TypeScript(七):Debug

[Node] 随遇而安 TypeScript(七):Debug

作者: 何幻 | 来源:发表于2020-06-01 18:12 被阅读0次

背景

有很多优秀的代码编辑器,具有自动重构选中代码的功能,VSCode 也能执行这样的操作。
我们用 VSCode 打开一个 .ts 文件,

const f = x => {
  x, y
};

鼠标选中函数体 x, y,按快捷键 ⌘ + .,就会弹出下图这样的选择框,

我们选第二个 Extract to function in global scope,选中的代码就会被提取到一个全局函数中,

const f = x => {
  newFunction(x);
};
function newFunction(x: any) {
  x, y;
}

VSCode 到底是怎么做到的呢?
简单说,它是通过 Language Server Protocol 调用了 tsserver。
由 tsserver 返回了重构后的结果。

本文我们先来研究这条链路是怎么跑通的,下一篇文章再来探讨 tsserver 的业务逻辑。

1. 调试 vscode 和 tsserver

为了看清整条链路,我们需要同时对 vscode 和 tsserver 进行调试。

1.1 源码准备

vscode 源码,
我们选用了当前 release 最新的版本 v1.45.1

$ git clone https://github.com/microsoft/vscode.git
$ cd vscode
$ git checkout 1.45.1

为了描述方便,我们将这里的 vscode 目录,记为 {VSCodeRoot}

tsserver 源码在 typescript 中,当前已经更新到 v3.9.3 了,
但为了保持与前几篇文章一致,我们仍然使用 v3.7.3

$ git clone https://github.com/microsoft/TypeScript.git
$ cd TypeScript
$ git checkout v3.7.3

为了描述方便,我们将这里的 TypeScript 目录,记为 {TypeScriptRoot}

1.2 编译

$ cd {VSCodeRoot}
$ yarn
$ yarn compile

有些 node 版本中 yarn 会失败,我本机的 node 版本是 v10.17.0
yarn 版本是 1.22.4

$ cd {TypeScriptRoot}
$ npm i
$ node node_modules/.bin/gulp LKG

gulp LKG,会将 src/ 中的源码编译到 built/local/ 文件夹中。

1.3 一些必要的软链接

为了能让 vscode 源码调用我们下载的 typescript 源码,需要添加一些软链接。

# 进入 vscode 源码根目录
$ cd {VSCodeRoot}

# 内置插件依赖的 typescript 目录改个名,不用这个目录了
$ mv extensions/node_modules/typescript extensions/node_modules/_typescript

# 软链到 typescript 源码根目录
$ ln -s {TypeScriptRoot} extensions/node_modules/typescript

vscode 源码中的内置插件,依赖的 TypeScript 默认位于 {VSCodeRoot}/extensions/node_modules 中。
为了对 TypeScript(tsserver)源码进行调试(固定 v3.7.3 版本,且用上 source map),
这里创建了一个软链接,让 vscode 直接依赖我们之前下载的 TypeScript 源码。

# 进入 typescript 源码根目录
$ cd {TypeScriptRoot}

# lib/ 目录改个名,不用这个目录了
$ mv lib _lib

# 将 lib/ 软链到 typescript 构建产物 built/local/ 目录
$ ln -s built/local lib

vscode 启动 tsserver 时,硬编码了 tsserver 的路径(即,lib 这个名字不能修改),
而这个路径下的 tsserver.js 是没有 source map 的。
为了能调试源码,我们将原来的 lib/ 目录删掉,并建立软链接,指向 TypeScript 项目编译产物目录 built/local/

1.4 调试配置

打开 vscode 源码根目录 {VSCodeRoot} 中的 .vscode/launch.json
添加这样一个调试配置,名字记为 Debug TypeScript Extension

{
    ...,
  "configurations": [
    {
      "type": "extensionHost",
      "request": "launch",
      "name": "Debug TypeScript Extension",
      "runtimeExecutable": "${execPath}",
      "args": [
        "${workspaceFolder}",
        "--extensionDevelopmentPath=${workspaceFolder}/extensions/typescript-language-features",
      ],
      "outFiles": [
        "${workspaceFolder}/extensions/typescript-language-features/out/**/*.js"
      ],
      "env": {
        "TSS_DEBUG": "9003",
      }
    },
        ...,
  ]
}

env.TSS_DEBUG 设置为了 9003,这是 vscode 内置 TypeScript 插件(typescript-language-features)启动 tsserver 的调试端口号。
位于 extensions/typescript-language-features/src/tsServer/spawner.ts#L98

const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions(kind, configuration));
getForkOptions(kind: ServerKind, configuration: TypeScriptServiceConfiguration) {
  const debugPort = TypeScriptServerSpawner.getDebugPort(kind);
  const tsServerForkOptions: electron.ForkOptions = {
    execArgv: [
      ...(debugPort ? [`--inspect=${debugPort}`] : []),
      ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : [])
    ]
  };
  return tsServerForkOptions;
}

注意这里用了 --inspect 而不是 --inspect-brk
这说明 tsserver 启动后并不会停在第一行等待 attach,而是直接继续运行。

源码中支持 --inspect-brkcommit 已经 merge 到 master 了。
只是在写这篇文章的时候,还没有 release,
下一个 release 应该就可以使用 env.TSS_DEBUG_BRK 来配置 --inspect-brk 形式的调试端口号了。

typescript 源码目录 {TypeScript} 下是没有 .vscode/launch.json 的,
我们新建这样一个文件(或点击菜单:Run - Add configuration 也行),并添加如下配置,

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "attach to tsserver",
      "port": 9003,
      "skipFiles": [
        "<node_internals>/**"
      ]
    }
  ]
}

注意到这里的 port 端口号,与 vscode 那边的 env.TSS_DEBUG 应保持一致。

1.5 启动调试

用 VSCode 打开 vscode 源码目录 {VSCodeRoot}
提前在 extensions/typescript-language-features/src/extension.ts#L27
typescript 插件(typescript-language-features)的激活函数 activate 中第一行打个断点。

在调试面板中选择刚才创建的配置 Debug TypeScript Extension,按 F5 启动调试。
它会打开一个新的 VSCode 窗口,名为 [Extension Development Host]

我们在这个窗口中打开一个 .ts 文件,以激活 typescript 插件(typescript-language-features)。


vscode 那边已激活 typescript 插件(typescript-language-features)之后(按 F5 运行下去),
用 VSCode 打开 typescript 源码目录 {TypeScriptRoot}
直接按 F5,启动调试(因为就一个配置,默认选中了 attach to tsserver)。


看起来好像没有反应,其实已经 attach 到 tsserver 了。
上文我们提到了,这是因为当前版本的 vscode(1.45.1)采用了 --inspect 方式启动 tsserver,而不是 --inspect-brk

2. 业务逻辑

按照上文的介绍,我们已经启动了 vscode 源码仓库中的 typescript 插件(typescript-language-features),
这个插件启动的 tsserver 我们也已经 attach 上了。

下面我们来看一下整体的代码重构逻辑。

2.1 tsserver

先到 typescript 源码仓库 getEditsForRefactor 函数中打个断点,
src/services/refactorProvider.ts#L36

export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {
  const refactor = refactors.get(refactorName);
  return refactor && refactor.getEditsForAction(context, actionName);
}

然后在 vscode 起来的 [Extension Development Host] 窗口中(已打开了一个 .ts 文件),重复本文开篇背景中介绍的操作步骤。

const f = x => {
  x, y
};

选中 x, y,按 ⌘ + .,选择 Extract to function in global scope

就发现代码跑到了 getEditsForRefactor 函数的断点处了。

这说明重构操作确实用到了 tsserver,跑到了 tsserver 的代码中。
查看调用栈,tsserver 是通过监听 message 的方式,来执行代码重构操作的。


2.2 typescript-language-features

vscode 这边是怎么发送消息的呢?
通过查看 typescript 插件(typescript-language-features)的激活逻辑,或者搜索 getEditsForRefactor 关键字,
我们发现,消息是在 extensions/typescript-language-features/src/features/refactor.ts#L77 execute 函数中发送的,

class ApplyRefactoringCommand implements Command {
  public async execute(
    ...,
  ): Promise<boolean> {
    ...,
    const response = await this.client.execute('getEditsForRefactor', args, nulToken);
    ...,
    const workspaceEdit = await this.toWorkspaceEdit(response.body);
    ...,
  }
}

execute 函数,先是给 tsserver 发消息,得到了重构结果,
然后再调用 this.toWorkspaceEdit 将改动结果应用到编辑器中。

查看调用栈,可以粗略的识别出这是一个响应前端快捷键,然后再向 tsserver 发送消息的过程。


总结

本文花了较大篇幅介绍 vscode + typescript(tsserver)的联合调试过程。
这个过程看起来很简单,但其实跑通它也花费了不少的精力。

一图胜千言,我们借助软链接,让 vscode 启动了我们 typescript 源码中的 tsserver。


链路通了以后,再研究重构相关的代码逻辑就事半功倍了。
下文开始探讨 tsserver getEditsForRefactor,看它是怎样得到重构结果的。


参考

Language Server Protocol
vscode v1.45.1
typescript v3.7.3

相关文章

网友评论

    本文标题:[Node] 随遇而安 TypeScript(七):Debug

    本文链接:https://www.haomeiwen.com/subject/cbqszhtx.html