We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
此前 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 正常了,但是却又有了另一个问题:
power-assert
可以看到,当单测出错的时候,错误堆栈中的出错的行数应该是 5,但是实际上却成了 30 ,列数也是一样不对的,可是 ts-node 有内置 source-map-support ,应该是会自动纠正错误堆栈的行数才对的,为啥还会导致堆栈错误?
ts-node
关于 source-map-support ,可以看一下这篇 Node.js 中 source map 使用问题总结
强迫症表示这可不行啊,这必须得解决,于是开始了对源码的折腾...
由于一旦引入 espower-typescript 之后就导致堆栈错误,移除又正常,再加上堆栈错误的原因一般都是 source-map 哪里出问题了,所以首先觉得应该是 espower-typescript 里的 source map 处理的问题。看了一下源码,发现里面引入了个 espower-source 的模块来处理 source-map 。所以我看了一下 espower-source 的源码。
espower-typescript
espower-source
// espower-source/index.js module.exports = function espowerSource (originalCode, filepath, options) { ... var espowerOptions = mergeEspowerOptions(options, filepath); // 分析出 originalCode 中的 source map,即 ts => js 的 source map var inMap = handleIncomingSourceMap(originalCode, espowerOptions); ... // 将 originalCode 加上 power-assert 的封装 var instrumented = instrument(originalCode, filepath, espowerOptions); // 获取 power-assert 封装后的 source map // 即 js => power-assert + js 的 source map var outMap = convert.fromJSON(instrumented.map.toString()); if (inMap) { // 合并两个 source map 并且返回 var reMap = reconnectSourceMap(inMap, outMap); return instrumented.code + '\n' + reMap.toComment() + '\n'; } else { return instrumented.code + '\n' + outMap.toComment() + '\n'; } };
源码不复杂,可以看到 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 加了行号之后打印了出来
instrumented.code
可以看到,前面截图中出错的行号正是这个封装后的 js 代码堆栈行号,也就是 sourcemap 是没有映射到 ts 上的。
那是不是合并生成的 sourcemap 是有问题的?抱着这个疑问我又看了一下用来合并 sourcemap 的模块 multi-stage-sourcemap 的代码逻辑,也没看出来问题,那只能直接自己手动使用 source-map 库来算来一下这个位置,看一下对不对了。
于是在 espowerSource 的源码中手动加上了以下这段代码
espowerSource
const SourceMapConsumer = require('source-map').SourceMapConsumer; // 传入合并后的 sourcemap: reMap.sourcemap const consumer = new SourceMapConsumer(reMap.sourcemap); const newPosition = consumer.originalPositionFor({ line: 30, column: 15 }); console.info('>>>', newPosition);
想通过使用 source-map 模块的 Consumer 来根据新的 sourcemap ,以及传入上面报错截图中的行数及列数,看下能否算出来正确的 ts 中的行数及列数。结果如下
source-map
Consumer
嗯...结果是对的,锅貌似不在 espower-typescript 呀?
那既然锅不是 espower-typescript 的,难道是 source-map-support 的?毕竟实际上做 sourcemap 映射的,是我们引入的 source-map-support 的模块。
source-map-support
然后又浏览了一下 source-map 的源码,发现 source-map-support 是通过 hook 掉 Error.prepareStackTrace 方法来实现在每次出错的时候,能够拿到错误堆栈,并且根据出错代码的 sourcemap 做行数及列数的矫正,于是根据这个代码找到了 source-map-support 中的 mapSourcePosition 方法,就是用于错误行数及列数矫正的。
Error.prepareStackTrace
mapSourcePosition
function mapSourcePosition(position) { var sourceMap = sourceMapCache[position.source]; if (!sourceMap) { ... } if (sourceMap && sourceMap.map) { var originalPosition = sourceMap.map.originalPositionFor(position); if (originalPosition.source !== null) { originalPosition.source = supportRelativeURL( sourceMap.url, originalPosition.source); return originalPosition; } } return position; }
根据上面的测试,我们知道 originalPositionFor 方法是用来计算原始位置的,然后我将计算出来的 originalPosition 打印了一下,发现映射出来的 source、line、column 的值全是 null,为啥会是 null ?那只能说明,这里拿到的 sourcemap 是错误的。于是我就将在 source-map-support 中拿到的 sourcemap,跟 espower-typescript 中最后返回的 sourcemap 做了对比,发现.... 完!全!不!一!样!但是这个 sourcemap 却跟 js => ts 的那个 sourcemap 一毛一样。
originalPositionFor
也就是说,在 source-map-support 中拿到的 sourcemap 其实是 ts 生成的 sourcemap,而不是 espower-typescript 生成的那串,难怪会导致行数算不出来,都不是同个 sourcemap。
因为 source-map-support 是 ts-node 引入的,既然 source-map-support 里拿到的是错误的 sourcemap,那肯定就是 ts-node 导致的了,于是又去看 ts-node 的源码,然后就发现了导致该问题的代码。
var memoryCache = { contents: Object.create(null), versions: Object.create(null), outputs: Object.create(null) }; ... sourceMapSupport.install({ environment: 'node', retrieveFile: function (path) { return memoryCache.outputs[path]; } });
可以看到,在 ts-node 中缓存了编译后的代码,并且在 source-map-support 的 retrieveFile 方法中返回缓存值。而 source-map-support 的 retrieveFile 是用来接收包含 sourcemap 信息的代码文件的。因为 ts-node 在 source-map-support 获取 sourcemap 的时候稳定返回了缓存值,所以就导致 espower-typescript 中生成的 sourcemap 没有生效。
retrieveFile
既然知道了原因,要解决就很简单了,直接复写 source-map-support 的 retrieveFile 方法,返回正确的缓存值:
const sourceMapSupport = require('source-map-support'); const cacheMap = {}; const extensions = ['.ts', '.tsx']; sourceMapSupport.install({ environment: 'node', retrieveFile: function (path) { // 根据路径找缓存的编译后的代码 return cacheMap[path]; } }); extensions.forEach(ext => { const originalExtension = require.extensions[ext]; require.extensions[ext] = (module, filePath) => { const originalCompile = module._compile; module._compile = function(code, filePath) { // 缓存编译后的代码 cacheMap[filePath] = code; return originalCompile.call(this, code, filePath); }; return originalExtension(module, filePath); }; })
经过验证,在引入 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
当查出这个原因之后,发现导致这个的原因并不复杂,只是从出现问题,到解决问题这个过程还是比较折腾的( 也有可能是我学艺不精,绕了个圈子[摊手] ),各种看源码....正所谓一言不合就看源码。
写这篇文章,也是方便之后,如果有其他类似的通过修改 module._compile 来实现的模块出现堆栈问题的时候,提供一种这样的解决思路。
The text was updated successfully, but these errors were encountered:
强迫症表示这可不行啊,这必须得解决。
更重要的原因是不解决就没法在企业开发中使用
因为 ts-node 的特殊性( 不会生成包含 sourceMap 的 js )
那是不是 ts-node 应该判断那个 inlinesourcemap 的配置?
转一份到知乎专栏吧
Sorry, something went wrong.
@atian25 跟 inlinesourcemap 没关系,因为 ts-node 生成的 js 是保存在内存中的,所以 source-map-support 是拿不到的,只能通过 retrieveFile 方法传给 source-map-support
No branches or pull requests
背景
此前 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
来实现的模块出现堆栈问题的时候,提供一种这样的解决思路。The text was updated successfully, but these errors were encountered: