{"action":"created","issue":{"url":"https://api.github.com/repos/whxaxes/blog/issues/13","repository_url":"https://api.github.com/repos/whxaxes/blog","labels_url":"https://api.github.com/repos/whxaxes/blog/issues/13/labels{/name}","comments_url":"https://api.github.com/repos/whxaxes/blog/issues/13/comments","events_url":"https://api.github.com/repos/whxaxes/blog/issues/13/events","html_url":"https://github.com/whxaxes/blog/issues/13","id":354662338,"node_id":"MDU6SXNzdWUzNTQ2NjIzMzg=","number":13,"title":"ts-node 错误堆栈问题排查小记","user":{"login":"whxaxes","id":5856440,"node_id":"MDQ6VXNlcjU4NTY0NDA=","avatar_url":"https://avatars0.githubusercontent.com/u/5856440?v=4","gravatar_id":"","url":"https://api.github.com/users/whxaxes","html_url":"https://github.com/whxaxes","followers_url":"https://api.github.com/users/whxaxes/followers","following_url":"https://api.github.com/users/whxaxes/following{/other_user}","gists_url":"https://api.github.com/users/whxaxes/gists{/gist_id}","starred_url":"https://api.github.com/users/whxaxes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/whxaxes/subscriptions","organizations_url":"https://api.github.com/users/whxaxes/orgs","repos_url":"https://api.github.com/users/whxaxes/repos","events_url":"https://api.github.com/users/whxaxes/events{/privacy}","received_events_url":"https://api.github.com/users/whxaxes/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2018-08-28T10:12:40Z","updated_at":"2018-08-28T10:28:06Z","closed_at":null,"author_association":"OWNER","body":"\n## 背景\n\n此前在 egg-bin 中集成了 ts-node ，从而能够直接跑用 ts 写的 egg 应用，当然也包括单元测试。\n\n但是实际跑的时候却发现，`power-assert` 在 ts-node 下失效了，查阅了一下文档发现要引入 `espower-typescript` 才能让 `power-assert` 生效，引入后又发现错误堆栈的行数与列数不对了。\n\n![](https://lh3.googleusercontent.com/-TKo0e1xjHjM/W4UWQ2SsI3I/AAAAAAAAAII/OniF3Bu-v2Qhsdt9cPXNTxnRY_B0a38hwCHMYCw/I/15354405535871.jpg)\n\n![](https://lh3.googleusercontent.com/-DlXjxGK0nhc/W4UWQ-mp0qI/AAAAAAAAAIE/Cbfy-dBKx80xE3CALuLkw1NnvOdNbgdnQCHMYCw/I/15354405176504.jpg)\n\n可以看到，出错的行数应该是 5，但是实际上却成了 30 ，强迫症表示这可不行啊，这必须得解决。\n\n## 分析\n\n### espower-typescript ?\n\n由于一旦引入 `espower-typescript` 之后就导致堆栈错误，而堆栈错误的原因一般都是 source-map 哪里出问题了，所以首先觉得应该是 `espower-typescript` 里的 source map 处理的问题。看了一下源码，发现里面引入了个 `espower-source` 的模块来处理 source-map 。所以我看了一下 `espower-source` 的源码。\n\n```js\n// espower-source/index.js\n\nmodule.exports = function espowerSource (originalCode, filepath, options) {\n    ...\n    var espowerOptions = mergeEspowerOptions(options, filepath);\n    // 分析出 originalCode 中的 source map，即 ts => js 的 source map\n    var inMap = handleIncomingSourceMap(originalCode, espowerOptions);\n    ...\n    // 将 originalCode 加上 power-assert 的封装\n    var instrumented = instrument(originalCode, filepath, espowerOptions);\n    // 获取 power-assert 封装后的 source map，即 js => power-asser + js 的 source map\n    var outMap = convert.fromJSON(instrumented.map.toString());\n    if (inMap) {\n        // 合并两个 source map 并且返回\n        var reMap = reconnectSourceMap(inMap, outMap);\n        return instrumented.code + '\\n' + reMap.toComment() + '\\n';\n    } else {\n        return instrumented.code + '\\n' + outMap.toComment() + '\\n';\n    }\n};\n```\n\n源码不复杂，可以看到 `espower-source` 中会先分析 compile 后的代码，然后从代码中提取出 sourcemap（ 比如 ts 编译成 js 后的 inlineSourceMap ），这个 sourcemap 是从 ts 到 js 的 sourcemap，然后再将编译后的代码做 power-assert 的封装（ 要实现 power-assert 的那种展示效果，是需要对代码做额外包装的 ），同时会生成一个新的 sourcemap ，这个就是从 js 到 封装后的 js 的 sourcemap。然后将两个 source map 合并成一个新的 sourcemap 并且返回。\n\n这咋看之下，逻辑没问题呀，按道理这个新的 sourcemap 应该是可以映射出封装后的 js 到 ts 的位置的。\n\n然后我将 `instrumented.code` 加了行号之后打印了出来\n\n![](https://lh3.googleusercontent.com/-dT7xl2KF_vM/W4UWRRcosgI/AAAAAAAAAIQ/1J-sGT_pWK8aU1iNiaJPhWHCgmq16_oJwCHMYCw/I/15354448095573.jpg)\n\n可以看到，前面截图中出错的行号正是这个封装后的 js 代码堆栈行号，也就是 sourcemap 是没有映射到 ts 上的。\n\n然后我就怀疑是不是合并生成的 sourcemap 是有问题的，于是又看了一下合并 sourcemap 的模块 [multi-stage-sourcemap](https://github.com/azu/multi-stage-sourcemap) 的代码逻辑，也没看出来问题，所以就决定，直接自己手动使用 [source-map](https://github.com/mozilla/source-map) 库来算来一下这个位置。\n\n于是在 `espowerSource` 的源码中手动加上了以下这段代码\n\n```js\nconst SourceMapConsumer = require('source-map').SourceMapConsumer;\n// 传入合并后的 sourcemap: reMap.sourcemap\nconst consumer = SourceMapConsumer(reMap.sourcemap);\nconst newPosition = consumer.originalPositionFor({\n  line: 30,\n  column: 15\n});\nconsole.info('>>>', newPosition);\n```\n\n尝试使用 `source-map` 模块的 `Consumer` 来根据新的 sourcemap ，以及传入上面报错截图中的行数及列数，看下能否算出来正确的 ts 中的行数及列数。结果\n\n![](https://lh3.googleusercontent.com/-WRymd18hxAQ/W4UWQ97vZ6I/AAAAAAAAAIM/jHozZjOyDLYtJHKKOvNkZZfZBavQpC37wCHMYCw/I/15354454815892.jpg)\n\n嗯...结果是对的，锅貌似不在 espower-typescript 呀？\n\n### source-map-support ?\n\n然后又想到，既然锅不是 espower-typescript 的，那是不是出在 `source-map-support` 上，因为实际上做 sourcemap 映射的，是我们引入的 `source-map-support` 的模块。\n\n然后又浏览了一下 source-map 的源码，发现 source-map-support 是通过 hook 掉 `Error.prepareStackTrace` 方法来实现在每次出错的时候，能够拿到错误堆栈，并且根据出错代码的 sourcemap 做行数及列数的矫正，于是根据这个代码找到了 source-map-support 中的 `mapSourcePosition` 方法，就是用于错误行数及列数矫正的。\n\n```js\nfunction mapSourcePosition(position) {\n  var sourceMap = sourceMapCache[position.source];\n\n  if (!sourceMap) {\n    ...\n  }\n\n  if (sourceMap && sourceMap.map) {\n    var originalPosition = sourceMap.map.originalPositionFor(position);\n    if (originalPosition.source !== null) {\n      originalPosition.source = supportRelativeURL(\n        sourceMap.url, originalPosition.source);\n      return originalPosition;\n    }\n  }\n\n  return position;\n}\n```\n\n根据上面的测试，我们知道 `originalPositionFor` 方法是用来计算原始位置的，然后我将计算出来的 originalPosition 打印了一下，发现映射出来的 source、line、column 的值全是 null，为啥会是 null ？那只能说明，这里拿到的 sourcemap 是错误的。于是我就将在 `source-map-support` 中拿到的 sourcemap，跟 `espower-typescript` 中最后返回的 sourcemap 做了对比，发现.... 完！全！不！一！样！但是这个 sourcemap 却跟 js => ts 的那个 sourcemap 一毛一样。\n\n也就是说，在 source-map-support 中拿到的 sourcemap 其实是 ts 生成的 sourcemap，而不是 espower-typescript 生成的那串，难怪会导致行数算不出来，都不是同个 sourcemap。\n\n### ts-node !\n\n因为 source-map-support 是 ts-node 引入的，既然 source-map-support 里拿到的是错误的 sourcemap，那肯定就是 ts-node 导致的了，于是又去看 ts-node 的源码，然后就发现了导致该问题的代码。\n\n```js\nvar memoryCache = {\n    contents: Object.create(null),\n    versions: Object.create(null),\n    outputs: Object.create(null)\n};\n...\nsourceMapSupport.install({\n    environment: 'node',\n    retrieveFile: function (path) {\n        return memoryCache.outputs[path];\n    }\n});\n```\n\n可以看到，在 ts-node 中缓存了编译后的代码，并且在 `source-map-support` 的 retrieveFile 方法中返回缓存值。而 `source-map-support` 的 `retrieveFile` 是用来接收包含 sourcemap 信息的代码文件的。因为 ts-node 在 `source-map-support` 获取 sourcemap 的时候稳定返回了缓存值，所以就导致 espower-typescript 中生成的 sourcemap 没有生效。\n\n## 解决方案\n\n既然知道了原因，要解决就很简单了，直接复写 `source-map-support` 的 `retrieveFile` 方法，返回正确的缓存值：\n\n```js\nconst sourceMapSupport = require('source-map-support');\nconst cacheMap = {};\nconst extensions = ['.ts', '.tsx'];\n\nsourceMapSupport.install({\n  environment: 'node',\n  retrieveFile: function (path) {\n    // 根据路径找缓存的编译后的代码\n    return cacheMap[path];\n  }\n});\n\nextensions.forEach(ext => {\n  const originalExtension = require.extensions[ext];\n  require.extensions[ext] = (module, filePath) => {\n    const originalCompile = module._compile;\n    module._compile = function(code, filePath) {\n      // 缓存编译后的代码\n      cacheMap[filePath] = code;\n      return originalCompile.call(this, code, filePath);\n    };\n    return originalExtension(module, filePath);\n  };\n})\n```\n\n经过验证，在引入 espower-typescript 之后再引入上面的代码，就可以解决这个问题了。\n\n## 最后\n\n最后这么来看，其实也不是 ts-node 的锅，因为 ts-node 的特殊性（ 不会生成包含 sourceMap 的 js ），所以必须得在 `source-map-support` 的 `retrieveFile` 方法返回缓存在内存中的 js 代码，否则 `source-map-support` 自己去读 ts 文件的话也是拿不到 sourcemap ，一样会导致堆栈行数错误。\n\n主要原因还是在于多个模块都是基于修改 `module._compile` 来实现，大家都生成了 sourcemap，但是没有考虑如何能被 `source-map-support` 正确消费而已。\n\n当查出这个原因之后，发现导致这个的原因并不复杂，只是从出现问题，到解决问题这个过程还是比较折腾的（ 也有可能是我学艺不精，绕了个圈子[摊手] ），各种看源码....正所谓一言不合就看源码。\n\n写这篇文章，也是方便之后，如果有其他类似的通过修改 `module._compile` 来实现的模块出现堆栈问题的时候，提供一种这样的解决思路。\n\n\n\n\n"},"comment":{"url":"https://api.github.com/repos/whxaxes/blog/issues/comments/416532878","html_url":"https://github.com/whxaxes/blog/issues/13#issuecomment-416532878","issue_url":"https://api.github.com/repos/whxaxes/blog/issues/13","id":416532878,"node_id":"MDEyOklzc3VlQ29tbWVudDQxNjUzMjg3OA==","user":{"login":"atian25","id":227713,"node_id":"MDQ6VXNlcjIyNzcxMw==","avatar_url":"https://avatars2.githubusercontent.com/u/227713?v=4","gravatar_id":"","url":"https://api.github.com/users/atian25","html_url":"https://github.com/atian25","followers_url":"https://api.github.com/users/atian25/followers","following_url":"https://api.github.com/users/atian25/following{/other_user}","gists_url":"https://api.github.com/users/atian25/gists{/gist_id}","starred_url":"https://api.github.com/users/atian25/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/atian25/subscriptions","organizations_url":"https://api.github.com/users/atian25/orgs","repos_url":"https://api.github.com/users/atian25/repos","events_url":"https://api.github.com/users/atian25/events{/privacy}","received_events_url":"https://api.github.com/users/atian25/received_events","type":"User","site_admin":false},"created_at":"2018-08-28T10:28:06Z","updated_at":"2018-08-28T10:28:06Z","author_association":"NONE","body":"> 强迫症表示这可不行啊，这必须得解决。\r\n\r\n更重要的原因是不解决就没法在企业开发中使用\r\n\r\n> 因为 ts-node 的特殊性（ 不会生成包含 sourceMap 的 js ）\r\n\r\n那是不是 ts-node 应该判断那个 inlinesourcemap 的配置？\r\n\r\n"}}
{"actor":{"display_login":"atian25"}}