You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
7.6 KiB
338 lines
7.6 KiB
'use strict'; |
|
|
|
const escapeStringRegexp = require('escape-string-regexp'); |
|
|
|
const cwd = typeof process === 'object' && process && typeof process.cwd === 'function' |
|
? process.cwd() |
|
: '.' |
|
|
|
const natives = [].concat( |
|
require('module').builtinModules, |
|
'bootstrap_node', |
|
'node', |
|
).map(n => new RegExp(`(?:\\((?:node:)?${n}(?:\\.js)?:\\d+:\\d+\\)$|^\\s*at (?:node:)?${n}(?:\\.js)?:\\d+:\\d+$)`)); |
|
|
|
natives.push( |
|
/\((?:node:)?internal\/[^:]+:\d+:\d+\)$/, |
|
/\s*at (?:node:)?internal\/[^:]+:\d+:\d+$/, |
|
/\/\.node-spawn-wrap-\w+-\w+\/node:\d+:\d+\)?$/ |
|
); |
|
|
|
class StackUtils { |
|
constructor (opts) { |
|
opts = { |
|
ignoredPackages: [], |
|
...opts |
|
}; |
|
|
|
if ('internals' in opts === false) { |
|
opts.internals = StackUtils.nodeInternals(); |
|
} |
|
|
|
if ('cwd' in opts === false) { |
|
opts.cwd = cwd |
|
} |
|
|
|
this._cwd = opts.cwd.replace(/\\/g, '/'); |
|
this._internals = [].concat( |
|
opts.internals, |
|
ignoredPackagesRegExp(opts.ignoredPackages) |
|
); |
|
|
|
this._wrapCallSite = opts.wrapCallSite || false; |
|
} |
|
|
|
static nodeInternals () { |
|
return [...natives]; |
|
} |
|
|
|
clean (stack, indent = 0) { |
|
indent = ' '.repeat(indent); |
|
|
|
if (!Array.isArray(stack)) { |
|
stack = stack.split('\n'); |
|
} |
|
|
|
if (!(/^\s*at /.test(stack[0])) && (/^\s*at /.test(stack[1]))) { |
|
stack = stack.slice(1); |
|
} |
|
|
|
let outdent = false; |
|
let lastNonAtLine = null; |
|
const result = []; |
|
|
|
stack.forEach(st => { |
|
st = st.replace(/\\/g, '/'); |
|
|
|
if (this._internals.some(internal => internal.test(st))) { |
|
return; |
|
} |
|
|
|
const isAtLine = /^\s*at /.test(st); |
|
|
|
if (outdent) { |
|
st = st.trimEnd().replace(/^(\s+)at /, '$1'); |
|
} else { |
|
st = st.trim(); |
|
if (isAtLine) { |
|
st = st.slice(3); |
|
} |
|
} |
|
|
|
st = st.replace(`${this._cwd}/`, ''); |
|
|
|
if (st) { |
|
if (isAtLine) { |
|
if (lastNonAtLine) { |
|
result.push(lastNonAtLine); |
|
lastNonAtLine = null; |
|
} |
|
|
|
result.push(st); |
|
} else { |
|
outdent = true; |
|
lastNonAtLine = st; |
|
} |
|
} |
|
}); |
|
|
|
return result.map(line => `${indent}${line}\n`).join(''); |
|
} |
|
|
|
captureString (limit, fn = this.captureString) { |
|
if (typeof limit === 'function') { |
|
fn = limit; |
|
limit = Infinity; |
|
} |
|
|
|
const {stackTraceLimit} = Error; |
|
if (limit) { |
|
Error.stackTraceLimit = limit; |
|
} |
|
|
|
const obj = {}; |
|
|
|
Error.captureStackTrace(obj, fn); |
|
const {stack} = obj; |
|
Error.stackTraceLimit = stackTraceLimit; |
|
|
|
return this.clean(stack); |
|
} |
|
|
|
capture (limit, fn = this.capture) { |
|
if (typeof limit === 'function') { |
|
fn = limit; |
|
limit = Infinity; |
|
} |
|
|
|
const {prepareStackTrace, stackTraceLimit} = Error; |
|
Error.prepareStackTrace = (obj, site) => { |
|
if (this._wrapCallSite) { |
|
return site.map(this._wrapCallSite); |
|
} |
|
|
|
return site; |
|
}; |
|
|
|
if (limit) { |
|
Error.stackTraceLimit = limit; |
|
} |
|
|
|
const obj = {}; |
|
Error.captureStackTrace(obj, fn); |
|
const { stack } = obj; |
|
Object.assign(Error, {prepareStackTrace, stackTraceLimit}); |
|
|
|
return stack; |
|
} |
|
|
|
at (fn = this.at) { |
|
const [site] = this.capture(1, fn); |
|
|
|
if (!site) { |
|
return {}; |
|
} |
|
|
|
const res = { |
|
line: site.getLineNumber(), |
|
column: site.getColumnNumber() |
|
}; |
|
|
|
setFile(res, site.getFileName(), this._cwd); |
|
|
|
if (site.isConstructor()) { |
|
res.constructor = true; |
|
} |
|
|
|
if (site.isEval()) { |
|
res.evalOrigin = site.getEvalOrigin(); |
|
} |
|
|
|
// Node v10 stopped with the isNative() on callsites, apparently |
|
/* istanbul ignore next */ |
|
if (site.isNative()) { |
|
res.native = true; |
|
} |
|
|
|
let typename; |
|
try { |
|
typename = site.getTypeName(); |
|
} catch (_) { |
|
} |
|
|
|
if (typename && typename !== 'Object' && typename !== '[object Object]') { |
|
res.type = typename; |
|
} |
|
|
|
const fname = site.getFunctionName(); |
|
if (fname) { |
|
res.function = fname; |
|
} |
|
|
|
const meth = site.getMethodName(); |
|
if (meth && fname !== meth) { |
|
res.method = meth; |
|
} |
|
|
|
return res; |
|
} |
|
|
|
parseLine (line) { |
|
const match = line && line.match(re); |
|
if (!match) { |
|
return null; |
|
} |
|
|
|
const ctor = match[1] === 'new'; |
|
let fname = match[2]; |
|
const evalOrigin = match[3]; |
|
const evalFile = match[4]; |
|
const evalLine = Number(match[5]); |
|
const evalCol = Number(match[6]); |
|
let file = match[7]; |
|
const lnum = match[8]; |
|
const col = match[9]; |
|
const native = match[10] === 'native'; |
|
const closeParen = match[11] === ')'; |
|
let method; |
|
|
|
const res = {}; |
|
|
|
if (lnum) { |
|
res.line = Number(lnum); |
|
} |
|
|
|
if (col) { |
|
res.column = Number(col); |
|
} |
|
|
|
if (closeParen && file) { |
|
// make sure parens are balanced |
|
// if we have a file like "asdf) [as foo] (xyz.js", then odds are |
|
// that the fname should be += " (asdf) [as foo]" and the file |
|
// should be just "xyz.js" |
|
// walk backwards from the end to find the last unbalanced ( |
|
let closes = 0; |
|
for (let i = file.length - 1; i > 0; i--) { |
|
if (file.charAt(i) === ')') { |
|
closes++; |
|
} else if (file.charAt(i) === '(' && file.charAt(i - 1) === ' ') { |
|
closes--; |
|
if (closes === -1 && file.charAt(i - 1) === ' ') { |
|
const before = file.slice(0, i - 1); |
|
const after = file.slice(i + 1); |
|
file = after; |
|
fname += ` (${before}`; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (fname) { |
|
const methodMatch = fname.match(methodRe); |
|
if (methodMatch) { |
|
fname = methodMatch[1]; |
|
method = methodMatch[2]; |
|
} |
|
} |
|
|
|
setFile(res, file, this._cwd); |
|
|
|
if (ctor) { |
|
res.constructor = true; |
|
} |
|
|
|
if (evalOrigin) { |
|
res.evalOrigin = evalOrigin; |
|
res.evalLine = evalLine; |
|
res.evalColumn = evalCol; |
|
res.evalFile = evalFile && evalFile.replace(/\\/g, '/'); |
|
} |
|
|
|
if (native) { |
|
res.native = true; |
|
} |
|
|
|
if (fname) { |
|
res.function = fname; |
|
} |
|
|
|
if (method && fname !== method) { |
|
res.method = method; |
|
} |
|
|
|
return res; |
|
} |
|
} |
|
|
|
function setFile (result, filename, cwd) { |
|
if (filename) { |
|
filename = filename.replace(/\\/g, '/'); |
|
if (filename.startsWith(`${cwd}/`)) { |
|
filename = filename.slice(cwd.length + 1); |
|
} |
|
|
|
result.file = filename; |
|
} |
|
} |
|
|
|
function ignoredPackagesRegExp(ignoredPackages) { |
|
if (ignoredPackages.length === 0) { |
|
return []; |
|
} |
|
|
|
const packages = ignoredPackages.map(mod => escapeStringRegexp(mod)); |
|
|
|
return new RegExp(`[\/\\\\]node_modules[\/\\\\](?:${packages.join('|')})[\/\\\\][^:]+:\\d+:\\d+`) |
|
} |
|
|
|
const re = new RegExp( |
|
'^' + |
|
// Sometimes we strip out the ' at' because it's noisy |
|
'(?:\\s*at )?' + |
|
// $1 = ctor if 'new' |
|
'(?:(new) )?' + |
|
// $2 = function name (can be literally anything) |
|
// May contain method at the end as [as xyz] |
|
'(?:(.*?) \\()?' + |
|
// (eval at <anonymous> (file.js:1:1), |
|
// $3 = eval origin |
|
// $4:$5:$6 are eval file/line/col, but not normally reported |
|
'(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?' + |
|
// file:line:col |
|
// $7:$8:$9 |
|
// $10 = 'native' if native |
|
'(?:(.+?):(\\d+):(\\d+)|(native))' + |
|
// maybe close the paren, then end |
|
// if $11 is ), then we only allow balanced parens in the filename |
|
// any imbalance is placed on the fname. This is a heuristic, and |
|
// bound to be incorrect in some edge cases. The bet is that |
|
// having weird characters in method names is more common than |
|
// having weird characters in filenames, which seems reasonable. |
|
'(\\)?)$' |
|
); |
|
|
|
const methodRe = /^(.*?) \[as (.*?)\]$/; |
|
|
|
module.exports = StackUtils;
|
|
|