# Node.js 中 sourcemap 使用问题总结

## 起源

Node 应用功能越来越复杂，很多业务都开始尝试使用 TypeScript 来开发。现在前端写的 JS 大部分是经过编译过程的，浏览器中通过 source map 的使用，可以很好的解决源码和编译运行时代码差异的问题。

那么，在 Node 服务器环境应该如何使用 source map 呢？最近在重新搭建一个完全基于 ts 的 Node 应用，所有的相关流程看起来都挺好的，唯一的缺陷是报错信息错误信息指向的是 js 文件。我觉得应该探索下如何让 Node 支持 source map 。

## 原理

对于 Node 而言，服务器 source map 最大的价值在于错误信息有正确的错误堆栈，所以只要我们能够实现自定义错误堆栈信息就可以了。

恰好 v8 引擎有提供一个私有的 ([Stack-Trace-API](https://github.com/v8/v8/wiki/Stack-Trace-API))， 这个提供了让开发者自定义错误 stack 信息的能力。具体来说，开发者可以实现 `Error` 对象的 `prepareStackTrace` 方法，如果 `Error` 对象上定义了这个方法，那么每次错误信息都会经过 `Error.prepareStackTrace` 处理后返回。

`Error.prepareStackTrace` 方法可以拿到两个参数，错误基本信息和结构化错误堆栈，第二个参数是一个数组，通过这个数组可以拿到错误文件以及位置信息。最后基于这些信息重新返回一个字符串，这样就可以覆盖 Error 对象的 stack 属性了。

基本代码结构如下：

```javascript
function prepareStackTrace(error, stack) {
  return error + stack.map(function(frame) {
    return '\n    at ' + wrapCallSite(frame);;
  }).join('');
}
Error.prepareStackTrace = prepareStackTrace;
```

在 `wrapCallSite` 方法里面可以通过分析源码，找到 sourceMap 然后返回正确的位置信息。

原理很简单，已经有一个 npm 包 [source-map-support](https://github.com/evanw/node-source-map-support) 封装好了相关功能。

这看起来已经很完美了。source map 读取只在出现错误的时候才执行，所以**这个功能不会有性能问题，在生成环境也可以开启**。

## 问题

Stack Trace API 看起来很美好，但现实场景总是更加复杂。我在引入 [source-map-support](https://github.com/evanw/node-source-map-support) 后，运行起来没什么问题，但在跑测试用例的时候，错误堆栈的位置信息完全不对。

这个问题排查了很久，最终定位到在 `wrapCallSite` 方法中拿到的 frame 对象返回的行号就是错误的，而这个获取行号的方法是 native code ，这个几乎没法调试了。我想，难道是 Node 的问题？要调试到 Node 源码么？

折腾了很久没有什么效果，就在我打算放弃的时候，我换了一个假设，会不会是某个包依赖影响的？然后我尝试依次删除跑用例时 require 的包，终于发现是因为 [egg-bin](https://eggjs.org/zh-cn/core/unittest.html#%E6%B5%8B%E8%AF%95%E8%BF%90%E8%A1%8C%E5%B7%A5%E5%85%B7) 默认引入的 `power-assert` 导致的。

问题定位到后，解决就容易了。但解决这个问题得先讲讲 power-assert 是如何实现的。

### power-assert 与 sourceMap

power-assert 作为一个断言库，最大的特色在于错误信息的报告是非常友好的，一张图可以很清晰看到区别

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8xODg2N2VlNi1kMzI1LTQ1MDEtYmRiYi0xZTFkYWE0NjgyNmMvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.Hl9VP1eH2aiApgpRB_23cvbapIg_X8yLHVwV6go6XkM)

实现这样炫酷的报告是需要做一些特殊的处理，把测试用例的代码进行一次转换，举个例子

```javascript
it('foo', function foo() {
  var a = 'foo';
  var b = 'b';
  assert(a === b);
});
```

经过 [espower-source](https://github.com/power-assert-js/espower-source) 处理后，变成了这样

```javascript
it('foo', function foo() {
  var a = 'foo';
  var b = 'b';
  assert(expr(capture(capture(a, '/0/left') === capture(b, '/0/right'), '/0'), {
      content: 'assert(a === b)',
      filepath: 'bizLogger.test.ts',
      line: 107
  }));
})
```

> 注：上面的代码不是真实运行的代码，经过一些删减

对于 `assert(a === b);` 这样一个表达式，会通过 `capture` 捕获每一个运算过程的位置和值，最终通过 `expr` 运算。这样经过转换后，代码运行逻辑不变，但是异常发生的时候可以返回 assert 表达式中每一步的返回值。

我所遇到的问题也就是因为 power-assert 对代码进行了转换，最终异常抛出时，真实 js 异常位置信息是转换后的位置，这个位置自然是无法正确定位到源码位置了。

但只运行 power-assert ，不引用 source-map-support 的时候，错误行号还是对的。这是因为 [espower-source](https://github.com/power-assert-js/espower-source) 返回重新编译后的源码后，还同时对源码文件的 sourceMap 进行了重新转换。所以大部分情况，我们是无法感知到源码有经过重新编译。

运行简单流程图如下

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9kODMxOWE2ZC1kODM0LTQ3ZmYtOWNiNS0yYTM4NWM0YzE0MjUvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.cakjwmQSE-uRxnOdUEcV5XsiLLyo-kTu2PUGZS6F_G8)

### 解决问题

回到最初的问题，跑用例的时候行号不对了。power-assert 的影响在于两点


1. 测试文件源码会被 power-assert 修改，增加一些信息收集代码
2. power-assert 同时有引入 [source-map-support](https://github.com/evanw/node-source-map-support) 来对错误堆栈进行重新定位

当我在我自己的业务中也引入 [source-map-support](https://github.com/evanw/node-source-map-support) 来重新定位错误堆栈时，我所拿到的源码是被 power-assert 修改过的，所以这时候是无法重新定位到正确的 ts 位置了。

既然 power-assert 有引入 sourceMap ，那么是不是我关闭自己引入的 sourceMap 就可以了呢？理论上是应该如此的，但是因为 power-assert 对 sourceMap 文件不支持(inline sourcemap 是支持的)，所以只能定位到 js 源码，无法定位到 ts 源码。

问题确定了，就可以自己动手解决了，我发了一个 [mr](https://github.com/power-assert-js/espower-source/pull/14) 让 espower-source 支持 sourceMap 文件，最新版的 power-assert 已经可以正确定位到 ts 源码位置了。

另外，还有一个问题，正常情况 source-map-support 同时引入两遍，只要引入的文件路径一直，也是不会有问题的。但 power-assert 使用的还是老版本 source-map-support ，而且老版定位位置信息还是不够准确，这个也很好解决，[升级依赖版本](https://github.com/power-assert-js/espower-loader/pull/6/)既可以。

这两个问题解决后，在自己的业务用引入 source-map-support 也没有问题了，power-assert 返回的错误堆栈也可以正确的指向 sourceMap 位置了。

## 总结

基于 V8 的 [Stack Trace API](https://github.com/v8/v8/wiki/Stack-Trace-API) 的使用，浏览器的 sourceMap 能力也可以应用到 Node 服务器场景下，使用 npm 包 [source-map-support](https://github.com/evanw/node-source-map-support) 就可以了。

有时候可能会遇到一些奇怪的错误行号的问题，这可能是某个依赖包对 js 进行了转换，毕竟这在前端太常见了，动不动就重新编译 js 源码。

---

**Documents**

- [基于实践谈谈软件架构设计](https://iling.fun/s/blog/doc/5z65lqo5a6e6le16lci6lci6l2v5lu25p625p6e6k66k6h-tsGrQnNSNu)
- [rspack 使用体验过程](https://iling.fun/s/blog/doc/rspack-u8S4Mtjkfz)
- [E2E test with Playwright](https://iling.fun/s/blog/doc/e2e-test-with-playwright-JLFKAvWQ0U)
- [Vim everywhere](https://iling.fun/s/blog/doc/vim-everywhere-v6WZyLzGA1)
- [滴答 FaaS 研发平台小结](https://iling.fun/s/blog/doc/faas-XQxhEO3e5r)
- [Cod 服务编排之路](https://iling.fun/s/blog/doc/cod-0CFsOfiX20)
- [Cod 起源和用武之地](https://iling.fun/s/blog/doc/cod-1aAA2rVJZY)
- [2019 年度总结](https://iling.fun/s/blog/doc/2019-RhbTD2SiCn)
- [凤蝶区块历史及其问题排查记录](https://iling.fun/s/blog/doc/5yek6j225yy65z2x5y6g5yy5yk5yw26zeu6aky5o6s5pl6k6w5b2v-WoaaJ9QHpq)
- [如何用单元测试](https://iling.fun/s/blog/doc/5aac5l2v55so5y2v5ywd5rwl6kv-3zwG1cJvTn)
- [小钱袋 Node 应用开发经验总结](https://iling.fun/s/blog/doc/node-QpFfYCuoE0)
- [简单 JS 算数表达式求值算法的实现探索](https://iling.fun/s/blog/doc/js-FXcWkpW2vw)
- [凤蝶可视化编辑实现原理](https://iling.fun/s/blog/doc/5yek6j225yv6keg5yyw57yw6l6r5a6e546w5y6f55cg-ZsBj0CS1ev)
- [凤蝶服务端渲染经验总结二](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut5lqm-a4Kd59d7Td)
- [凤蝶服务端渲染经验总结](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut-GBhTESymbn)
- [Node.js 中 sourcemap 使用问题总结](https://iling.fun/s/blog/doc/nodejs-sourcemap-duex3wDhWt)
- [如何实现一个 mvvm 组件](https://iling.fun/s/blog/doc/mvvm-8TXAVSXe77)
- [webpack 热加载原理探索](https://iling.fun/s/blog/doc/webpack-7Wjn9z1fUB)
- [再谈 vim](https://iling.fun/s/blog/doc/vim-UBf5u57ko7)
- [How to realize velocity template interpreters](https://iling.fun/s/blog/doc/how-to-realize-velocity-template-interpreters-4s1KEznjYR)
- [joycss](https://iling.fun/s/blog/doc/joycss-bGvBvDGF7k)