(function(root, factory) { 'use strict'; // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers. /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { define('stacktrace-gps', ['source-map', 'stackframe'], factory); } else if (typeof exports === 'object') { module.exports = factory(require('source-map/lib/source-map-consumer'), require('stackframe')); } else { root.StackTraceGPS = factory(root.SourceMap || root.sourceMap, root.StackFrame); } }(this, function(SourceMap, StackFrame) { 'use strict'; /** * Make a X-Domain request to url and callback. * * @param {String} url * @returns {Promise} with response text if fulfilled */ function _xdr(url) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open('get', url); req.onerror = reject; req.onreadystatechange = function onreadystatechange() { if (req.readyState === 4) { if ((req.status >= 200 && req.status < 300) || (url.substr(0, 7) === 'file://' && req.responseText)) { resolve(req.responseText); } else { reject(new Error('HTTP status: ' + req.status + ' retrieving ' + url)); } } }; req.send(); }); } /** * Convert a Base64-encoded string into its original representation. * Used for inline sourcemaps. * * @param {String} b64str Base-64 encoded string * @returns {String} original representation of the base64-encoded string. */ function _atob(b64str) { if (typeof window !== 'undefined' && window.atob) { return window.atob(b64str); } else { throw new Error('You must supply a polyfill for window.atob in this environment'); } } function _parseJson(string) { if (typeof JSON !== 'undefined' && JSON.parse) { return JSON.parse(string); } else { throw new Error('You must supply a polyfill for JSON.parse in this environment'); } } function _findFunctionName(source, lineNumber/*, columnNumber*/) { var syntaxes = [ // {name} = function ({args}) TODO args capture /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/, // function {name}({args}) m[1]=name m[2]=args /function\s+([^('"`]*?)\s*\(([^)]*)\)/, // {name} = eval() /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/, // fn_name() { /\b(?!(?:if|for|switch|while|with|catch)\b)(?:(?:static)\s+)?(\S+)\s*\(.*?\)\s*\{/, // {name} = () => { /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*\(.*?\)\s*=>/ ]; var lines = source.split('\n'); // Walk backwards in the source lines until we find the line which matches one of the patterns above var code = ''; var maxLines = Math.min(lineNumber, 20); for (var i = 0; i < maxLines; ++i) { // lineNo is 1-based, source[] is 0-based var line = lines[lineNumber - i - 1]; var commentPos = line.indexOf('//'); if (commentPos >= 0) { line = line.substr(0, commentPos); } if (line) { code = line + code; var len = syntaxes.length; for (var index = 0; index < len; index++) { var m = syntaxes[index].exec(code); if (m && m[1]) { return m[1]; } } } } return undefined; } function _ensureSupportedEnvironment() { if (typeof Object.defineProperty !== 'function' || typeof Object.create !== 'function') { throw new Error('Unable to consume source maps in older browsers'); } } function _ensureStackFrameIsLegit(stackframe) { if (typeof stackframe !== 'object') { throw new TypeError('Given StackFrame is not an object'); } else if (typeof stackframe.fileName !== 'string') { throw new TypeError('Given file name is not a String'); } else if (typeof stackframe.lineNumber !== 'number' || stackframe.lineNumber % 1 !== 0 || stackframe.lineNumber < 1) { throw new TypeError('Given line number must be a positive integer'); } else if (typeof stackframe.columnNumber !== 'number' || stackframe.columnNumber % 1 !== 0 || stackframe.columnNumber < 0) { throw new TypeError('Given column number must be a non-negative integer'); } return true; } function _findSourceMappingURL(source) { var sourceMappingUrlRegExp = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg; var lastSourceMappingUrl; var matchSourceMappingUrl; // eslint-disable-next-line no-cond-assign while (matchSourceMappingUrl = sourceMappingUrlRegExp.exec(source)) { lastSourceMappingUrl = matchSourceMappingUrl[1]; } if (lastSourceMappingUrl) { return lastSourceMappingUrl; } else { throw new Error('sourceMappingURL not found'); } } function _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache) { return new Promise(function(resolve, reject) { var loc = sourceMapConsumer.originalPositionFor({ line: stackframe.lineNumber, column: stackframe.columnNumber }); if (loc.source) { // cache mapped sources var mappedSource = sourceMapConsumer.sourceContentFor(loc.source); if (mappedSource) { sourceCache[loc.source] = mappedSource; } resolve( // given stackframe and source location, update stackframe new StackFrame({ functionName: loc.name || stackframe.functionName, args: stackframe.args, fileName: loc.source, lineNumber: loc.line, columnNumber: loc.column })); } else { reject(new Error('Could not get original source for given stackframe and source map')); } }); } /** * @constructor * @param {Object} opts * opts.sourceCache = {url: "Source String"} => preload source cache * opts.sourceMapConsumerCache = {/path/file.js.map: SourceMapConsumer} * opts.offline = True to prevent network requests. * Best effort without sources or source maps. * opts.ajax = Promise returning function to make X-Domain requests */ return function StackTraceGPS(opts) { if (!(this instanceof StackTraceGPS)) { return new StackTraceGPS(opts); } opts = opts || {}; this.sourceCache = opts.sourceCache || {}; this.sourceMapConsumerCache = opts.sourceMapConsumerCache || {}; this.ajax = opts.ajax || _xdr; this._atob = opts.atob || _atob; this._get = function _get(location) { return new Promise(function(resolve, reject) { var isDataUrl = location.substr(0, 5) === 'data:'; if (this.sourceCache[location]) { resolve(this.sourceCache[location]); } else if (opts.offline && !isDataUrl) { reject(new Error('Cannot make network requests in offline mode')); } else { if (isDataUrl) { // data URLs can have parameters. // see http://tools.ietf.org/html/rfc2397 var supportedEncodingRegexp = /^data:application\/json;([\w=:"-]+;)*base64,/; var match = location.match(supportedEncodingRegexp); if (match) { var sourceMapStart = match[0].length; var encodedSource = location.substr(sourceMapStart); var source = this._atob(encodedSource); this.sourceCache[location] = source; resolve(source); } else { reject(new Error('The encoding of the inline sourcemap is not supported')); } } else { var xhrPromise = this.ajax(location, {method: 'get'}); // Cache the Promise to prevent duplicate in-flight requests this.sourceCache[location] = xhrPromise; xhrPromise.then(resolve, reject); } } }.bind(this)); }; /** * Creating SourceMapConsumers is expensive, so this wraps the creation of a * SourceMapConsumer in a per-instance cache. * * @param {String} sourceMappingURL = URL to fetch source map from * @param {String} defaultSourceRoot = Default source root for source map if undefined * @returns {Promise} that resolves a SourceMapConsumer */ this._getSourceMapConsumer = function _getSourceMapConsumer(sourceMappingURL, defaultSourceRoot) { return new Promise(function(resolve) { if (this.sourceMapConsumerCache[sourceMappingURL]) { resolve(this.sourceMapConsumerCache[sourceMappingURL]); } else { var sourceMapConsumerPromise = new Promise(function(resolve, reject) { return this._get(sourceMappingURL).then(function(sourceMapSource) { if (typeof sourceMapSource === 'string') { sourceMapSource = _parseJson(sourceMapSource.replace(/^\)\]\}'/, '')); } if (typeof sourceMapSource.sourceRoot === 'undefined') { sourceMapSource.sourceRoot = defaultSourceRoot; } resolve(new SourceMap.SourceMapConsumer(sourceMapSource)); }).catch(reject); }.bind(this)); this.sourceMapConsumerCache[sourceMappingURL] = sourceMapConsumerPromise; resolve(sourceMapConsumerPromise); } }.bind(this)); }; /** * Given a StackFrame, enhance function name and use source maps for a * better StackFrame. * * @param {StackFrame} stackframe object * @returns {Promise} that resolves with with source-mapped StackFrame */ this.pinpoint = function StackTraceGPS$$pinpoint(stackframe) { return new Promise(function(resolve, reject) { this.getMappedLocation(stackframe).then(function(mappedStackFrame) { function resolveMappedStackFrame() { resolve(mappedStackFrame); } this.findFunctionName(mappedStackFrame) .then(resolve, resolveMappedStackFrame) // eslint-disable-next-line no-unexpected-multiline ['catch'](resolveMappedStackFrame); }.bind(this), reject); }.bind(this)); }; /** * Given a StackFrame, guess function name from location information. * * @param {StackFrame} stackframe * @returns {Promise} that resolves with enhanced StackFrame. */ this.findFunctionName = function StackTraceGPS$$findFunctionName(stackframe) { return new Promise(function(resolve, reject) { _ensureStackFrameIsLegit(stackframe); this._get(stackframe.fileName).then(function getSourceCallback(source) { var lineNumber = stackframe.lineNumber; var columnNumber = stackframe.columnNumber; var guessedFunctionName = _findFunctionName(source, lineNumber, columnNumber); // Only replace functionName if we found something if (guessedFunctionName) { resolve(new StackFrame({ functionName: guessedFunctionName, args: stackframe.args, fileName: stackframe.fileName, lineNumber: lineNumber, columnNumber: columnNumber })); } else { resolve(stackframe); } }, reject)['catch'](reject); }.bind(this)); }; /** * Given a StackFrame, seek source-mapped location and return new enhanced StackFrame. * * @param {StackFrame} stackframe * @returns {Promise} that resolves with enhanced StackFrame. */ this.getMappedLocation = function StackTraceGPS$$getMappedLocation(stackframe) { return new Promise(function(resolve, reject) { _ensureSupportedEnvironment(); _ensureStackFrameIsLegit(stackframe); var sourceCache = this.sourceCache; var fileName = stackframe.fileName; this._get(fileName).then(function(source) { var sourceMappingURL = _findSourceMappingURL(source); var isDataUrl = sourceMappingURL.substr(0, 5) === 'data:'; var defaultSourceRoot = fileName.substring(0, fileName.lastIndexOf('/') + 1); if (sourceMappingURL[0] !== '/' && !isDataUrl && !(/^https?:\/\/|^\/\//i).test(sourceMappingURL)) { sourceMappingURL = defaultSourceRoot + sourceMappingURL; } return this._getSourceMapConsumer(sourceMappingURL, defaultSourceRoot) .then(function(sourceMapConsumer) { return _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache) .then(resolve)['catch'](function() { resolve(stackframe); }); }); }.bind(this), reject)['catch'](reject); }.bind(this)); }; }; }));