stacktracey.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. "use strict";
  2. /* ------------------------------------------------------------------------ */
  3. const O = Object,
  4. isBrowser = (typeof window !== 'undefined') && (window.window === window) && window.navigator,
  5. nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
  6. lastOf = x => x[x.length - 1],
  7. getSource = require ('get-source'),
  8. partition = require ('./impl/partition'),
  9. asTable = require ('as-table'),
  10. nixSlashes = x => x.replace (/\\/g, '/'),
  11. pathRoot = isBrowser ? window.location.href : (nixSlashes (process.cwd ()) + '/')
  12. /* ------------------------------------------------------------------------ */
  13. class StackTracey {
  14. constructor (input, offset) {
  15. const originalInput = input
  16. , isParseableSyntaxError = input && (input instanceof SyntaxError && !isBrowser)
  17. /* new StackTracey () */
  18. if (!input) {
  19. input = new Error ()
  20. offset = (offset === undefined) ? 1 : offset
  21. }
  22. /* new StackTracey (Error) */
  23. if (input instanceof Error) {
  24. input = input.stack || ''
  25. }
  26. /* new StackTracey (string) */
  27. if (typeof input === 'string') {
  28. input = this.rawParse (input).slice (offset).map (x => this.extractEntryMetadata (x))
  29. }
  30. /* new StackTracey (array) */
  31. if (Array.isArray (input)) {
  32. if (isParseableSyntaxError) {
  33. const rawLines = nodeRequire ('util').inspect (originalInput).split ('\n')
  34. , fileLine = rawLines[0].split (':')
  35. , line = fileLine.pop ()
  36. , file = fileLine.join (':')
  37. if (file) {
  38. input.unshift ({
  39. file: nixSlashes (file),
  40. line: line,
  41. column: (rawLines[2] || '').indexOf ('^') + 1,
  42. sourceLine: rawLines[1],
  43. callee: '(syntax error)',
  44. syntaxError: true
  45. })
  46. }
  47. }
  48. this.items = input
  49. } else {
  50. this.items = []
  51. }
  52. }
  53. extractEntryMetadata (e) {
  54. const decomposedPath = this.decomposePath (e.file || '')
  55. const fileRelative = decomposedPath[0]
  56. const externalDomain = decomposedPath[1]
  57. return O.assign (e, {
  58. calleeShort: e.calleeShort || lastOf ((e.callee || '').split ('.')),
  59. fileRelative: fileRelative,
  60. fileShort: this.shortenPath (fileRelative),
  61. fileName: lastOf ((e.file || '').split ('/')),
  62. thirdParty: this.isThirdParty (fileRelative, externalDomain) && !e.index,
  63. externalDomain: externalDomain
  64. })
  65. }
  66. shortenPath (relativePath) {
  67. return relativePath.replace (/^node_modules\//, '')
  68. .replace (/^webpack\/bootstrap\//, '')
  69. .replace (/^__parcel_source_root\//, '')
  70. }
  71. decomposePath (fullPath) {
  72. let result = fullPath
  73. if (isBrowser) result = result.replace (pathRoot, '')
  74. const externalDomainMatch = result.match (/^(http|https)\:\/\/?([^\/]+)\/(.*)/)
  75. const externalDomain = externalDomainMatch ? externalDomainMatch[2] : undefined
  76. result = externalDomainMatch ? externalDomainMatch[3] : result
  77. if (!isBrowser) result = nodeRequire ('path').relative (pathRoot, result)
  78. return [
  79. nixSlashes(result).replace (/^.*\:\/\/?\/?/, ''), // cut webpack:/// and webpack:/ things
  80. externalDomain
  81. ]
  82. }
  83. isThirdParty (relativePath, externalDomain) {
  84. return externalDomain ||
  85. (relativePath[0] === '~') || // webpack-specific heuristic
  86. (relativePath[0] === '/') || // external source
  87. (relativePath.indexOf ('node_modules') === 0) ||
  88. (relativePath.indexOf ('webpack/bootstrap') === 0)
  89. }
  90. rawParse (str) {
  91. const lines = (str || '').split ('\n')
  92. const entries = lines.map (line => {
  93. line = line.trim ()
  94. let callee, fileLineColumn = [], native, planA, planB
  95. if ((planA = line.match (/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
  96. (planA = line.match (/at (.+) \((.+)\)/)) ||
  97. ((line.slice (0, 3) !== 'at ') && (planA = line.match (/(.*)@(.*)/)))) {
  98. callee = planA[1]
  99. native = (planA[2] === 'native')
  100. fileLineColumn = (planA[2].match (/(.*):(\d+):(\d+)/) ||
  101. planA[2].match (/(.*):(\d+)/) || []).slice (1)
  102. } else if ((planB = line.match (/^(at\s+)*(.+):(\d+):(\d+)/) )) {
  103. fileLineColumn = (planB).slice (2)
  104. } else {
  105. return undefined
  106. }
  107. /* Detect things like Array.reduce
  108. TODO: detect more built-in types */
  109. if (callee && !fileLineColumn[0]) {
  110. const type = callee.split ('.')[0]
  111. if (type === 'Array') {
  112. native = true
  113. }
  114. }
  115. return {
  116. beforeParse: line,
  117. callee: callee || '',
  118. index: isBrowser && (fileLineColumn[0] === window.location.href),
  119. native: native || false,
  120. file: nixSlashes (fileLineColumn[0] || ''),
  121. line: parseInt (fileLineColumn[1] || '', 10) || undefined,
  122. column: parseInt (fileLineColumn[2] || '', 10) || undefined
  123. }
  124. })
  125. return entries.filter (x => (x !== undefined))
  126. }
  127. withSourceAt (i) {
  128. return this.items[i] && this.withSource (this.items[i])
  129. }
  130. withSourceAsyncAt (i) {
  131. return this.items[i] && this.withSourceAsync (this.items[i])
  132. }
  133. withSource (loc) {
  134. if (this.shouldSkipResolving (loc)) {
  135. return loc
  136. } else {
  137. let resolved = getSource (loc.file || '').resolve (loc)
  138. if (!resolved.sourceFile) {
  139. return loc
  140. }
  141. return this.withSourceResolved (loc, resolved)
  142. }
  143. }
  144. withSourceAsync (loc) {
  145. if (this.shouldSkipResolving (loc)) {
  146. return Promise.resolve (loc)
  147. } else {
  148. return getSource.async (loc.file || '')
  149. .then (x => x.resolve (loc))
  150. .then (resolved => this.withSourceResolved (loc, resolved))
  151. .catch (e => this.withSourceResolved (loc, { error: e, sourceLine: '' }))
  152. }
  153. }
  154. shouldSkipResolving (loc) {
  155. return loc.sourceFile || loc.error || (loc.file && loc.file.indexOf ('<') >= 0) // skip things like <anonymous> and stuff that was already fetched
  156. }
  157. withSourceResolved (loc, resolved) {
  158. if (resolved.sourceFile && !resolved.sourceFile.error) {
  159. resolved.file = nixSlashes (resolved.sourceFile.path)
  160. resolved = this.extractEntryMetadata (resolved)
  161. }
  162. if (resolved.sourceLine.includes ('// @hide')) {
  163. resolved.sourceLine = resolved.sourceLine.replace ('// @hide', '')
  164. resolved.hide = true
  165. }
  166. if (resolved.sourceLine.includes ('__webpack_require__') || // webpack-specific heuristics
  167. resolved.sourceLine.includes ('/******/ ({')) {
  168. resolved.thirdParty = true
  169. }
  170. return O.assign ({ sourceLine: '' }, loc, resolved)
  171. }
  172. withSources () {
  173. return this.map (x => this.withSource (x))
  174. }
  175. withSourcesAsync () {
  176. return Promise.all (this.items.map (x => this.withSourceAsync (x)))
  177. .then (items => new StackTracey (items))
  178. }
  179. mergeRepeatedLines () {
  180. return new StackTracey (
  181. partition (this.items, e => e.file + e.line).map (
  182. group => {
  183. return group.items.slice (1).reduce ((memo, entry) => {
  184. memo.callee = (memo.callee || '<anonymous>') + ' → ' + (entry.callee || '<anonymous>')
  185. memo.calleeShort = (memo.calleeShort || '<anonymous>') + ' → ' + (entry.calleeShort || '<anonymous>')
  186. return memo
  187. }, O.assign ({}, group.items[0]))
  188. }
  189. )
  190. )
  191. }
  192. clean () {
  193. const s = this.withSources ().mergeRepeatedLines ()
  194. return s.filter (s.isClean.bind (s))
  195. }
  196. cleanAsync () {
  197. return this.withSourcesAsync ().then (s => {
  198. s = s.mergeRepeatedLines ()
  199. return s.filter (s.isClean.bind (s))
  200. })
  201. }
  202. isClean (entry, index) {
  203. return (index === 0) || !(entry.thirdParty || entry.hide || entry.native)
  204. }
  205. at (i) {
  206. return O.assign ({
  207. beforeParse: '',
  208. callee: '<???>',
  209. index: false,
  210. native: false,
  211. file: '<???>',
  212. line: 0,
  213. column: 0
  214. }, this.items[i])
  215. }
  216. asTable (opts) {
  217. const maxColumnWidths = (opts && opts.maxColumnWidths) || this.maxColumnWidths ()
  218. const trimEnd = (s, n) => s && ((s.length > n) ? (s.slice (0, n-1) + '…') : s)
  219. const trimStart = (s, n) => s && ((s.length > n) ? ('…' + s.slice (-(n-1))) : s)
  220. const trimmed = this.map (
  221. e => [
  222. ('at ' + trimEnd (e.calleeShort, maxColumnWidths.callee)),
  223. trimStart ((e.fileShort && (e.fileShort + ':' + e.line)) || '', maxColumnWidths.file),
  224. trimEnd (((e.sourceLine || '').trim () || ''), maxColumnWidths.sourceLine)
  225. ]
  226. )
  227. return asTable (trimmed.items)
  228. }
  229. maxColumnWidths () {
  230. return {
  231. callee: 30,
  232. file: 60,
  233. sourceLine: 80
  234. }
  235. }
  236. static resetCache () {
  237. getSource.resetCache ()
  238. getSource.async.resetCache ()
  239. }
  240. static locationsEqual (a, b) {
  241. return (a.file === b.file) &&
  242. (a.line === b.line) &&
  243. (a.column === b.column)
  244. }
  245. }
  246. /* Array methods
  247. ------------------------------------------------------------------------ */
  248. ;['map', 'filter', 'slice', 'concat'].forEach (method => {
  249. StackTracey.prototype[method] = function (/*...args */) { // no support for ...args in Node v4 :(
  250. return new StackTracey (this.items[method].apply (this.items, arguments))
  251. }
  252. })
  253. /* ------------------------------------------------------------------------ */
  254. module.exports = StackTracey