Open whxaxes opened 6 years ago
强迫症表示这可不行啊,这必须得解决。
更重要的原因是不解决就没法在企业开发中使用
因为 ts-node 的特殊性( 不会生成包含 sourceMap 的 js )
那是不是 ts-node 应该判断那个 inlinesourcemap 的配置?
转一份到知乎专栏吧
@atian25 跟 inlinesourcemap 没关系,因为 ts-node 生成的 js 是保存在内存中的,所以 source-map-support 是拿不到的,只能通过 retrieveFile 方法传给 source-map-support
背景
此前 egg 需要支持 ts,所以我们在 egg-bin 中集成了 ts-node ( 详见 当 Egg 遇到 TypeScript,收获茶叶蛋一枚 ),从而能够让开发者直接跑用 ts 写的 egg 应用,当然也包括单元测试。
但是实际在跑单测的时候却发现,
power-assert
( power-assert 是个很酷的模块,也集成在了 egg-bin 中 ) 却在 ts-node 下失效了,查阅了一下文档发现要引入 espower-typescript 才能让power-assert
在 ts-node 下生效,引入后发现power-assert
正常了,但是却又有了另一个问题:可以看到,当单测出错的时候,错误堆栈中的出错的行数应该是 5,但是实际上却成了 30 ,列数也是一样不对的,可是
ts-node
有内置 source-map-support ,应该是会自动纠正错误堆栈的行数才对的,为啥还会导致堆栈错误?强迫症表示这可不行啊,这必须得解决,于是开始了对源码的折腾...
分析
espower-typescript ?
由于一旦引入
espower-typescript
之后就导致堆栈错误,移除又正常,再加上堆栈错误的原因一般都是 source-map 哪里出问题了,所以首先觉得应该是espower-typescript
里的 source map 处理的问题。看了一下源码,发现里面引入了个espower-source
的模块来处理 source-map 。所以我看了一下espower-source
的源码。源码不复杂,可以看到
espower-source
中会先分析 compile 后的代码,然后从代码中提取出 sourcemap( 比如 ts 编译成 js 后的 inlineSourceMap ),这个 sourcemap 是从 ts 到 js 的 sourcemap,然后再将编译后的代码做 power-assert 的封装( 要实现 power-assert 的那种展示效果,是需要对代码做额外包装的 ),同时会生成一个新的 sourcemap ,这个就是从 js 到 封装后的 js 的 sourcemap。然后将两个 source map 合并成一个新的 sourcemap 并且返回。这咋看之下,逻辑没问题呀,按道理这个新的 sourcemap 应该是可以映射出封装后的 js 到 ts 的位置的。紧接着我将
instrumented.code
加了行号之后打印了出来可以看到,前面截图中出错的行号正是这个封装后的 js 代码堆栈行号,也就是 sourcemap 是没有映射到 ts 上的。
那是不是合并生成的 sourcemap 是有问题的?抱着这个疑问我又看了一下用来合并 sourcemap 的模块 multi-stage-sourcemap 的代码逻辑,也没看出来问题,那只能直接自己手动使用 source-map 库来算来一下这个位置,看一下对不对了。
于是在
espowerSource
的源码中手动加上了以下这段代码想通过使用
source-map
模块的Consumer
来根据新的 sourcemap ,以及传入上面报错截图中的行数及列数,看下能否算出来正确的 ts 中的行数及列数。结果如下嗯...结果是对的,锅貌似不在 espower-typescript 呀?
source-map-support ?
那既然锅不是 espower-typescript 的,难道是
source-map-support
的?毕竟实际上做 sourcemap 映射的,是我们引入的source-map-support
的模块。然后又浏览了一下 source-map 的源码,发现 source-map-support 是通过 hook 掉
Error.prepareStackTrace
方法来实现在每次出错的时候,能够拿到错误堆栈,并且根据出错代码的 sourcemap 做行数及列数的矫正,于是根据这个代码找到了 source-map-support 中的mapSourcePosition
方法,就是用于错误行数及列数矫正的。根据上面的测试,我们知道
originalPositionFor
方法是用来计算原始位置的,然后我将计算出来的 originalPosition 打印了一下,发现映射出来的 source、line、column 的值全是 null,为啥会是 null ?那只能说明,这里拿到的 sourcemap 是错误的。于是我就将在source-map-support
中拿到的 sourcemap,跟espower-typescript
中最后返回的 sourcemap 做了对比,发现.... 完!全!不!一!样!但是这个 sourcemap 却跟 js => ts 的那个 sourcemap 一毛一样。也就是说,在 source-map-support 中拿到的 sourcemap 其实是 ts 生成的 sourcemap,而不是 espower-typescript 生成的那串,难怪会导致行数算不出来,都不是同个 sourcemap。
ts-node !
因为 source-map-support 是 ts-node 引入的,既然 source-map-support 里拿到的是错误的 sourcemap,那肯定就是 ts-node 导致的了,于是又去看 ts-node 的源码,然后就发现了导致该问题的代码。
可以看到,在 ts-node 中缓存了编译后的代码,并且在
source-map-support
的 retrieveFile 方法中返回缓存值。而source-map-support
的retrieveFile
是用来接收包含 sourcemap 信息的代码文件的。因为 ts-node 在source-map-support
获取 sourcemap 的时候稳定返回了缓存值,所以就导致 espower-typescript 中生成的 sourcemap 没有生效。解决方案
既然知道了原因,要解决就很简单了,直接复写
source-map-support
的retrieveFile
方法,返回正确的缓存值:经过验证,在引入 espower-typescript 之后再引入上面的代码,就可以解决这个问题了。
最后
最后这么来看,其实也不是 ts-node 的锅,因为 ts-node 的特殊性( 不会生成包含 sourceMap 的 js ),所以必须得在
source-map-support
的retrieveFile
方法返回缓存在内存中的 js 代码,否则source-map-support
自己去读 ts 文件的话也是拿不到 sourcemap ,一样会导致堆栈行数错误。主要原因还是在于多个模块都是基于修改
module._compile
来实现,大家都生成了 sourcemap,但是没有考虑如何能被source-map-support
正确消费而已。当查出这个原因之后,发现导致这个的原因并不复杂,只是从出现问题,到解决问题这个过程还是比较折腾的( 也有可能是我学艺不精,绕了个圈子[摊手] ),各种看源码....正所谓一言不合就看源码。
写这篇文章,也是方便之后,如果有其他类似的通过修改
module._compile
来实现的模块出现堆栈问题的时候,提供一种这样的解决思路。