// tslint:disable /** * Class StackFrame is a fork of https://github.com/stacktracejs/stackframe/blob/master/stackframe.js * I fork it because: * 1. There are some build issues when importing this package. * 2. Rewrites into typescript give us a better type interface. * 3. StackFrame contains some functions we don't need. */ export class StackFrame { private fileName: string; private functionName: string; private lineNumber?: number; private columnNumber?: number; constructor(obj: { fileName?: string; functionName?: string; lineNumber?: number; columnNumber?: number; }) { this.fileName = obj.fileName || ''; this.functionName = obj.functionName || ''; this.lineNumber = obj.lineNumber; this.columnNumber = obj.columnNumber; } toString() { const lineNumber = this.lineNumber || ''; const columnNumber = this.columnNumber || ''; if (this.functionName) { return ( this.functionName + ' (' + this.fileName + ':' + lineNumber + ':' + columnNumber + ')' ); } return this.fileName + ':' + lineNumber + ':' + columnNumber; } } /** * ErrorStackParser is a fork of https://github.com/stacktracejs/error-stack-parser/blob/master/error-stack-parser.js * I fork it because: * 1. There are some build issues when importing this package. * 2. Rewrites into typescript give us a better type interface. */ const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; export const ErrorStackParser = { /** * Given an Error object, extract the most information from it. * * @param {Error} error object * @return {Array} of StackFrames */ parse: function (error: Error): StackFrame[] { if ( // @ts-ignore typeof error.stacktrace !== 'undefined' || // @ts-ignore typeof error['opera#sourceloc'] !== 'undefined' ) { return this.parseOpera( error as { stacktrace?: string; message: string; stack?: string; }, ); } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { return this.parseV8OrIE(error as { stack: string }); } else if (error.stack) { return this.parseFFOrSafari(error as { stack: string }); } else { throw new Error('Cannot parse given Error object'); } }, // Separate line and column numbers from a string of the form: (URI:Line:Column) extractLocation: function (urlLike: string) { // Fail-fast but return locations like "(native)" if (urlLike.indexOf(':') === -1) { return [urlLike]; } const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; const parts = regExp.exec(urlLike.replace(/[()]/g, '')); if (!parts) throw new Error(`Cannot parse given url: ${urlLike}`); return [parts[1], parts[2] || undefined, parts[3] || undefined]; }, parseV8OrIE: function (error: { stack: string }) { const filtered = error.stack.split('\n').filter(function (line) { return !!line.match(CHROME_IE_STACK_REGEXP); }, this); return filtered.map(function (line) { if (line.indexOf('(eval ') > -1) { // Throw away eval information until we implement stacktrace.js/stackframe#8 line = line .replace(/eval code/g, 'eval') .replace(/(\(eval at [^()]*)|(\),.*$)/g, ''); } let sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '('); // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in // case it has spaces in it, as the string is split on \s+ later on const location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); // remove the parenthesized location from the line, if it was matched sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine; const tokens = sanitizedLine.split(/\s+/).slice(1); // if a location was matched, pass it to extractLocation() otherwise pop the last token const locationParts = this.extractLocation( location ? location[1] : tokens.pop(), ); const functionName = tokens.join(' ') || undefined; const fileName = ['eval', ''].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0]; return new StackFrame({ functionName, fileName, lineNumber: locationParts[1], columnNumber: locationParts[2], }); }, this); }, parseFFOrSafari: function (error: { stack: string }) { const filtered = error.stack.split('\n').filter(function (line) { return !line.match(SAFARI_NATIVE_CODE_REGEXP); }, this); return filtered.map(function (line) { // Throw away eval information until we implement stacktrace.js/stackframe#8 if (line.indexOf(' > eval') > -1) { line = line.replace( / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1', ); } if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { // Safari eval frames only have function names and nothing else return new StackFrame({ functionName: line, }); } else { const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; const matches = line.match(functionNameRegex); const functionName = matches && matches[1] ? matches[1] : undefined; const locationParts = this.extractLocation( line.replace(functionNameRegex, ''), ); return new StackFrame({ functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], }); } }, this); }, parseOpera: function (e: { stacktrace?: string; message: string; stack?: string; }): StackFrame[] { if ( !e.stacktrace || (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) ) { return this.parseOpera9(e as { message: string }); } else if (!e.stack) { return this.parseOpera10(e as { stacktrace: string }); } else { return this.parseOpera11(e as { stack: string }); } }, parseOpera9: function (e: { message: string }) { const lineRE = /Line (\d+).*script (?:in )?(\S+)/i; const lines = e.message.split('\n'); const result = []; for (let i = 2, len = lines.length; i < len; i += 2) { const match = lineRE.exec(lines[i]); if (match) { result.push( new StackFrame({ fileName: match[2], lineNumber: parseFloat(match[1]), }), ); } } return result; }, parseOpera10: function (e: { stacktrace: string }) { const lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; const lines = e.stacktrace.split('\n'); const result = []; for (let i = 0, len = lines.length; i < len; i += 2) { const match = lineRE.exec(lines[i]); if (match) { result.push( new StackFrame({ functionName: match[3] || undefined, fileName: match[2], lineNumber: parseFloat(match[1]), }), ); } } return result; }, // Opera 10.65+ Error.stack very similar to FF/Safari parseOpera11: function (error: { stack: string }) { const filtered = error.stack.split('\n').filter(function (line) { return ( !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/) ); }, this); return filtered.map(function (line: string) { const tokens = line.split('@'); const locationParts = this.extractLocation(tokens.pop()); const functionCall = tokens.shift() || ''; const functionName = functionCall .replace(//, '$2') .replace(/\([^)]*\)/g, '') || undefined; return new StackFrame({ functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], }); }, this); }, };