Technology

从 Unbound Breakpoint 到源码断点:Mastra / Node.js 调试里的 sourcemap 问题

VS Code attach 成功不等于源码断点能命中。以 Mastra 为例,源码断点 unbound 时,关键是判断 Node Inspector、运行时代码和 sourcemap 映射哪一层断了。

这次想解决的问题很具体:

在 VS Code 里调试 Mastra 这类 Node.js / TypeScript 框架时,为什么断点已经打上了,却一直显示 Unbound breakpoint

最后发现,问题不在 VS Code 是否 attach 成功,也不在 Node Inspector 是否启动,而在源码和运行时代码之间缺少 sourcemap 映射。

先区分两个问题

Node.js 程序要能被 VS Code 调试,至少要过两关:

  1. Node 进程要以 Inspector 模式启动。
  2. VS Code 要能把源码位置映射到当前运行的 JavaScript 代码。

第一关解决的是“调试器能不能连上进程”。第二关解决的是“源码里的断点能不能落到运行时代码上”。

这两个问题很容易混在一起。看到 Unbound breakpoint 时,直觉上会怀疑 VS Code 没连上 Node 进程。但这次的现象说明,连接本身可能是正常的,真正断掉的是 sourcemap。

Mastra 的普通启动和 Inspector 启动

Mastra 普通开发启动一般是:

npm run mastra dev

如果要开启 Node Inspector,可以让 Mastra dev server 以 inspect mode 启动。比如直接使用 Mastra CLI:

npx mastra dev --inspect=9229

Inspector 默认端口通常是 9229。启动后,VS Code 可以 attach 到这个 Node 进程,Chrome 浏览器也可以通过 DevTools 连接。

如果项目里是通过 npm script 包了一层 Mastra CLI,需要注意把参数传给底层命令。可以根据项目脚本选择类似下面的形式:

npm run mastra -- dev --inspect=9229

关键不是命令表面长什么样,而是最终运行的 Mastra dev server 必须带上 --inspect

VS Code attach 配置

VS Code 侧需要在 .vscode/launch.json 里增加一个 attach 配置:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach Mastra",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true,
      "sourceMaps": true,
      "resolveSourceMapLocations": [
        "${workspaceFolder}/**",
        "!**/node_modules/**"
      ],
      "skipFiles": [
        "<node_internals>/**"
      ]
    }
  ]
}

这里有两个配置值得特别注意:

  • sourceMaps: true:告诉 VS Code 使用 sourcemap。
  • resolveSourceMapLocations:限制 sourcemap 解析范围,避免进入 node_modules 造成混乱。

配置好以后,如果 Node 进程确实以 Inspector 模式启动,VS Code 就可以 attach 到端口 9229

为什么还会出现 Unbound breakpoint

这次真正卡住的地方是:VS Code 已经 attach 上了,但源码断点仍然显示 Unbound breakpoint

Unbound breakpoint 的意思是:

VS Code 知道你想在这个源码位置打断点,但当前正在运行的 Node.js 进程里,没有找到它能对应上的那份代码。

这在 Node.js / TypeScript 项目里很常见,因为源码通常不是被 Node.js 直接执行的。中间可能经过很多层:

  • TypeScript 编译
  • ESM 转换
  • tsx / ts-node
  • Vite / tsup / esbuild
  • Mastra dev server
  • 动态加载
  • 热更新

只要中间任何一层 sourcemap 没有接上,断点就可能飘走。

所以看到 Unbound breakpoint 时,不要只问“VS Code 为什么没连上”,还要问:

当前运行的 JavaScript 代码,能不能反向映射回我正在看的 TypeScript 源码?

关键判断:运行时代码能断,源码不能断

这次排查里最关键的观察是:

Mastra 会生成 .mastra/output 目录,里面有实际运行的 .mjs 文件。直接在 .mastra/output 里的 .mjs 文件上打断点,是可以命中的。

这说明:

  1. Node Inspector 已经启动。
  2. VS Code attach 是有效的。
  3. 当前 Node 进程确实可以被调试。
  4. 问题不在“调试器没连上”,而在“源码和编译后代码没有映射上”。

于是问题就收窄了:

TypeScript 源码断点不能命中
-> .mastra/output/*.mjs 断点可以命中
-> 调试链路本身正常
-> 源码到运行时代码的 sourcemap 映射缺失

查看 .mastra/output 后发现,当前没有 sourcemap。也就是说,VS Code 只能看到运行中的 .mjs,但不知道它对应到源码里的哪一行。

两种临时解决方式

第一种方式,是直接在源码里写 debugger

debugger

只要代码实际执行到这里,Node Inspector 就会停住。这种方式不依赖你在 VS Code 里手动绑定源码断点,适合快速确认某段代码有没有被执行。

但它不是最理想的长期方案。因为它解决的是“强行停住”,不是“让源码断点正常工作”。

第二种方式,是开启 Mastra 的 bundler sourcemap。

根据 Mastra 官方配置,可以在 Mastra 初始化时打开:

import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
  bundler: {
    sourcemap: true,
  },
})

bundler.sourcemap 的作用是让 Mastra 在打包运行时代码时生成 sourcemap。这样 VS Code attach 到 Node 进程后,才能把 .mastra/output 里的运行时代码映射回原始源码。

这次经验真正沉淀的判断

以后在 Node.js / TypeScript 项目里遇到源码断点不生效,可以按这个顺序排查:

  1. 先确认 Node 进程是否以 Inspector 模式启动。
  2. 再确认 VS Code 是否成功 attach 到对应端口。
  3. 然后去运行时代码位置打断点,看能不能命中。
  4. 如果运行时代码能断、源码不能断,优先怀疑 sourcemap。
  5. 检查框架或 bundler 是否真的生成了 sourcemap。
  6. 再检查 VS Code 的 sourceMapsresolveSourceMapLocations 配置。

这比一开始就反复调整 VS Code 配置更稳。

真正的判断点是:

如果编译后的运行时代码可以断,但源码断点一直 unbound,那么调试器链路大概率是通的,问题更可能在 sourcemap 映射。

用一句话总结

Mastra 这类 Node.js / TypeScript 框架里,VS Code attach 成功不等于源码断点就一定能命中。源码断点能不能命中,取决于运行时代码和源码之间是否有可用的 sourcemap。

这次的解决路径是:

npx mastra dev --inspect=9229
-> VS Code attach 9229
-> 发现源码断点 Unbound breakpoint
-> 验证 .mastra/output/*.mjs 可以断
-> 判断 Inspector 正常,sourcemap 缺失
-> 开启 Mastra bundler.sourcemap
-> 源码断点可以映射到运行时代码

调试 Node.js 程序时,不要只看“有没有连上调试器”。更重要的是确认三件事是否闭合:

源码
-> sourcemap
-> 当前 Node 进程正在运行的 JavaScript

这三者闭合以后,VS Code 里的源码断点才真正有地方落下去。

Back to Blog

Related Posts

View All Posts »

VS Code 越用越慢?别忘了定期审计过时插件

项目打开缓慢不一定是文件太多或缓存膨胀,也可能是一个早已被内置能力替代的插件正在阻塞 Extension Host。记录一次插件性能排查,以及一套值得定期执行的审计方法。