UNPKG

@opstarts/chimp

Version:

Develop acceptance tests & end-to-end tests with realtime feedback.

304 lines (276 loc) 12.6 kB
(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) { 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*/) { // function {name}({args}) m[1]=name m[2]=args var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; // {name} = function ({args}) TODO args capture var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; // {name} = eval() var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; 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); var m; 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; m = reFunctionExpression.exec(code); if (m && m[1]) { return m[1]; } m = reFunctionDeclaration.exec(code); if (m && m[1]) { return m[1]; } m = reFunctionEvaluation.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 m = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/.exec(source); if (m && m[1]) { return m[1]; } else { throw new Error('sourceMappingURL not found'); } } function _extractLocationInfoFromSourceMap(stackframe, rawSourceMap, sourceCache) { return new Promise(function(resolve, reject) { var mapConsumer = new SourceMap.SourceMapConsumer(rawSourceMap); var loc = mapConsumer.originalPositionFor({ line: stackframe.lineNumber, column: stackframe.columnNumber }); if (loc.source) { var mappedSource = mapConsumer.sourceContentFor(loc.source); if (mappedSource) { sourceCache[loc.source] = mappedSource; } resolve( new StackFrame( loc.name || stackframe.functionName, stackframe.args, loc.source, loc.line, 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.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.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)); }; /** * 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) ['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(guessedFunctionName, stackframe.args, stackframe.fileName, lineNumber, 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 base = fileName.substring(0, fileName.lastIndexOf('/') + 1); if (sourceMappingURL[0] !== '/' && !isDataUrl && !(/^https?:\/\/|^\/\//i).test(sourceMappingURL)) { sourceMappingURL = base + sourceMappingURL; } this._get(sourceMappingURL).then(function(sourceMap) { if (typeof sourceMap === 'string') { sourceMap = _parseJson(sourceMap.replace(/^\)\]\}'/, '')); } if (typeof sourceMap.sourceRoot === 'undefined') { sourceMap.sourceRoot = base; } _extractLocationInfoFromSourceMap(stackframe, sourceMap, sourceCache) .then(resolve)['catch'](function() { resolve(stackframe); }); }, reject)['catch'](reject); }.bind(this), reject)['catch'](reject); }.bind(this)); }; }; }));