[d565449] | 1 | (function(root, factory) {
|
---|
| 2 | 'use strict';
|
---|
| 3 | // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers.
|
---|
| 4 |
|
---|
| 5 | /* istanbul ignore next */
|
---|
| 6 | if (typeof define === 'function' && define.amd) {
|
---|
| 7 | define('stacktrace-gps', ['source-map', 'stackframe'], factory);
|
---|
| 8 | } else if (typeof exports === 'object') {
|
---|
| 9 | module.exports = factory(require('source-map/lib/source-map-consumer'), require('stackframe'));
|
---|
| 10 | } else {
|
---|
| 11 | root.StackTraceGPS = factory(root.SourceMap || root.sourceMap, root.StackFrame);
|
---|
| 12 | }
|
---|
| 13 | }(this, function(SourceMap, StackFrame) {
|
---|
| 14 | 'use strict';
|
---|
| 15 |
|
---|
| 16 | /**
|
---|
| 17 | * Make a X-Domain request to url and callback.
|
---|
| 18 | *
|
---|
| 19 | * @param {String} url
|
---|
| 20 | * @returns {Promise} with response text if fulfilled
|
---|
| 21 | */
|
---|
| 22 | function _xdr(url) {
|
---|
| 23 | return new Promise(function(resolve, reject) {
|
---|
| 24 | var req = new XMLHttpRequest();
|
---|
| 25 | req.open('get', url);
|
---|
| 26 | req.onerror = reject;
|
---|
| 27 | req.onreadystatechange = function onreadystatechange() {
|
---|
| 28 | if (req.readyState === 4) {
|
---|
| 29 | if ((req.status >= 200 && req.status < 300) ||
|
---|
| 30 | (url.substr(0, 7) === 'file://' && req.responseText)) {
|
---|
| 31 | resolve(req.responseText);
|
---|
| 32 | } else {
|
---|
| 33 | reject(new Error('HTTP status: ' + req.status + ' retrieving ' + url));
|
---|
| 34 | }
|
---|
| 35 | }
|
---|
| 36 | };
|
---|
| 37 | req.send();
|
---|
| 38 | });
|
---|
| 39 |
|
---|
| 40 | }
|
---|
| 41 |
|
---|
| 42 | /**
|
---|
| 43 | * Convert a Base64-encoded string into its original representation.
|
---|
| 44 | * Used for inline sourcemaps.
|
---|
| 45 | *
|
---|
| 46 | * @param {String} b64str Base-64 encoded string
|
---|
| 47 | * @returns {String} original representation of the base64-encoded string.
|
---|
| 48 | */
|
---|
| 49 | function _atob(b64str) {
|
---|
| 50 | if (typeof window !== 'undefined' && window.atob) {
|
---|
| 51 | return window.atob(b64str);
|
---|
| 52 | } else {
|
---|
| 53 | throw new Error('You must supply a polyfill for window.atob in this environment');
|
---|
| 54 | }
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | function _parseJson(string) {
|
---|
| 58 | if (typeof JSON !== 'undefined' && JSON.parse) {
|
---|
| 59 | return JSON.parse(string);
|
---|
| 60 | } else {
|
---|
| 61 | throw new Error('You must supply a polyfill for JSON.parse in this environment');
|
---|
| 62 | }
|
---|
| 63 | }
|
---|
| 64 |
|
---|
| 65 | function _findFunctionName(source, lineNumber/*, columnNumber*/) {
|
---|
| 66 | var syntaxes = [
|
---|
| 67 | // {name} = function ({args}) TODO args capture
|
---|
| 68 | /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/,
|
---|
| 69 | // function {name}({args}) m[1]=name m[2]=args
|
---|
| 70 | /function\s+([^('"`]*?)\s*\(([^)]*)\)/,
|
---|
| 71 | // {name} = eval()
|
---|
| 72 | /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/,
|
---|
| 73 | // fn_name() {
|
---|
| 74 | /\b(?!(?:if|for|switch|while|with|catch)\b)(?:(?:static)\s+)?(\S+)\s*\(.*?\)\s*\{/,
|
---|
| 75 | // {name} = () => {
|
---|
| 76 | /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*\(.*?\)\s*=>/
|
---|
| 77 | ];
|
---|
| 78 | var lines = source.split('\n');
|
---|
| 79 |
|
---|
| 80 | // Walk backwards in the source lines until we find the line which matches one of the patterns above
|
---|
| 81 | var code = '';
|
---|
| 82 | var maxLines = Math.min(lineNumber, 20);
|
---|
| 83 | for (var i = 0; i < maxLines; ++i) {
|
---|
| 84 | // lineNo is 1-based, source[] is 0-based
|
---|
| 85 | var line = lines[lineNumber - i - 1];
|
---|
| 86 | var commentPos = line.indexOf('//');
|
---|
| 87 | if (commentPos >= 0) {
|
---|
| 88 | line = line.substr(0, commentPos);
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | if (line) {
|
---|
| 92 | code = line + code;
|
---|
| 93 | var len = syntaxes.length;
|
---|
| 94 | for (var index = 0; index < len; index++) {
|
---|
| 95 | var m = syntaxes[index].exec(code);
|
---|
| 96 | if (m && m[1]) {
|
---|
| 97 | return m[1];
|
---|
| 98 | }
|
---|
| 99 | }
|
---|
| 100 | }
|
---|
| 101 | }
|
---|
| 102 | return undefined;
|
---|
| 103 | }
|
---|
| 104 |
|
---|
| 105 | function _ensureSupportedEnvironment() {
|
---|
| 106 | if (typeof Object.defineProperty !== 'function' || typeof Object.create !== 'function') {
|
---|
| 107 | throw new Error('Unable to consume source maps in older browsers');
|
---|
| 108 | }
|
---|
| 109 | }
|
---|
| 110 |
|
---|
| 111 | function _ensureStackFrameIsLegit(stackframe) {
|
---|
| 112 | if (typeof stackframe !== 'object') {
|
---|
| 113 | throw new TypeError('Given StackFrame is not an object');
|
---|
| 114 | } else if (typeof stackframe.fileName !== 'string') {
|
---|
| 115 | throw new TypeError('Given file name is not a String');
|
---|
| 116 | } else if (typeof stackframe.lineNumber !== 'number' ||
|
---|
| 117 | stackframe.lineNumber % 1 !== 0 ||
|
---|
| 118 | stackframe.lineNumber < 1) {
|
---|
| 119 | throw new TypeError('Given line number must be a positive integer');
|
---|
| 120 | } else if (typeof stackframe.columnNumber !== 'number' ||
|
---|
| 121 | stackframe.columnNumber % 1 !== 0 ||
|
---|
| 122 | stackframe.columnNumber < 0) {
|
---|
| 123 | throw new TypeError('Given column number must be a non-negative integer');
|
---|
| 124 | }
|
---|
| 125 | return true;
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | function _findSourceMappingURL(source) {
|
---|
| 129 | var sourceMappingUrlRegExp = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg;
|
---|
| 130 | var lastSourceMappingUrl;
|
---|
| 131 | var matchSourceMappingUrl;
|
---|
| 132 | // eslint-disable-next-line no-cond-assign
|
---|
| 133 | while (matchSourceMappingUrl = sourceMappingUrlRegExp.exec(source)) {
|
---|
| 134 | lastSourceMappingUrl = matchSourceMappingUrl[1];
|
---|
| 135 | }
|
---|
| 136 | if (lastSourceMappingUrl) {
|
---|
| 137 | return lastSourceMappingUrl;
|
---|
| 138 | } else {
|
---|
| 139 | throw new Error('sourceMappingURL not found');
|
---|
| 140 | }
|
---|
| 141 | }
|
---|
| 142 |
|
---|
| 143 | function _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache) {
|
---|
| 144 | return new Promise(function(resolve, reject) {
|
---|
| 145 | var loc = sourceMapConsumer.originalPositionFor({
|
---|
| 146 | line: stackframe.lineNumber,
|
---|
| 147 | column: stackframe.columnNumber
|
---|
| 148 | });
|
---|
| 149 |
|
---|
| 150 | if (loc.source) {
|
---|
| 151 | // cache mapped sources
|
---|
| 152 | var mappedSource = sourceMapConsumer.sourceContentFor(loc.source);
|
---|
| 153 | if (mappedSource) {
|
---|
| 154 | sourceCache[loc.source] = mappedSource;
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | resolve(
|
---|
| 158 | // given stackframe and source location, update stackframe
|
---|
| 159 | new StackFrame({
|
---|
| 160 | functionName: loc.name || stackframe.functionName,
|
---|
| 161 | args: stackframe.args,
|
---|
| 162 | fileName: loc.source,
|
---|
| 163 | lineNumber: loc.line,
|
---|
| 164 | columnNumber: loc.column
|
---|
| 165 | }));
|
---|
| 166 | } else {
|
---|
| 167 | reject(new Error('Could not get original source for given stackframe and source map'));
|
---|
| 168 | }
|
---|
| 169 | });
|
---|
| 170 | }
|
---|
| 171 |
|
---|
| 172 | /**
|
---|
| 173 | * @constructor
|
---|
| 174 | * @param {Object} opts
|
---|
| 175 | * opts.sourceCache = {url: "Source String"} => preload source cache
|
---|
| 176 | * opts.sourceMapConsumerCache = {/path/file.js.map: SourceMapConsumer}
|
---|
| 177 | * opts.offline = True to prevent network requests.
|
---|
| 178 | * Best effort without sources or source maps.
|
---|
| 179 | * opts.ajax = Promise returning function to make X-Domain requests
|
---|
| 180 | */
|
---|
| 181 | return function StackTraceGPS(opts) {
|
---|
| 182 | if (!(this instanceof StackTraceGPS)) {
|
---|
| 183 | return new StackTraceGPS(opts);
|
---|
| 184 | }
|
---|
| 185 | opts = opts || {};
|
---|
| 186 |
|
---|
| 187 | this.sourceCache = opts.sourceCache || {};
|
---|
| 188 | this.sourceMapConsumerCache = opts.sourceMapConsumerCache || {};
|
---|
| 189 |
|
---|
| 190 | this.ajax = opts.ajax || _xdr;
|
---|
| 191 |
|
---|
| 192 | this._atob = opts.atob || _atob;
|
---|
| 193 |
|
---|
| 194 | this._get = function _get(location) {
|
---|
| 195 | return new Promise(function(resolve, reject) {
|
---|
| 196 | var isDataUrl = location.substr(0, 5) === 'data:';
|
---|
| 197 | if (this.sourceCache[location]) {
|
---|
| 198 | resolve(this.sourceCache[location]);
|
---|
| 199 | } else if (opts.offline && !isDataUrl) {
|
---|
| 200 | reject(new Error('Cannot make network requests in offline mode'));
|
---|
| 201 | } else {
|
---|
| 202 | if (isDataUrl) {
|
---|
| 203 | // data URLs can have parameters.
|
---|
| 204 | // see http://tools.ietf.org/html/rfc2397
|
---|
| 205 | var supportedEncodingRegexp =
|
---|
| 206 | /^data:application\/json;([\w=:"-]+;)*base64,/;
|
---|
| 207 | var match = location.match(supportedEncodingRegexp);
|
---|
| 208 | if (match) {
|
---|
| 209 | var sourceMapStart = match[0].length;
|
---|
| 210 | var encodedSource = location.substr(sourceMapStart);
|
---|
| 211 | var source = this._atob(encodedSource);
|
---|
| 212 | this.sourceCache[location] = source;
|
---|
| 213 | resolve(source);
|
---|
| 214 | } else {
|
---|
| 215 | reject(new Error('The encoding of the inline sourcemap is not supported'));
|
---|
| 216 | }
|
---|
| 217 | } else {
|
---|
| 218 | var xhrPromise = this.ajax(location, {method: 'get'});
|
---|
| 219 | // Cache the Promise to prevent duplicate in-flight requests
|
---|
| 220 | this.sourceCache[location] = xhrPromise;
|
---|
| 221 | xhrPromise.then(resolve, reject);
|
---|
| 222 | }
|
---|
| 223 | }
|
---|
| 224 | }.bind(this));
|
---|
| 225 | };
|
---|
| 226 |
|
---|
| 227 | /**
|
---|
| 228 | * Creating SourceMapConsumers is expensive, so this wraps the creation of a
|
---|
| 229 | * SourceMapConsumer in a per-instance cache.
|
---|
| 230 | *
|
---|
| 231 | * @param {String} sourceMappingURL = URL to fetch source map from
|
---|
| 232 | * @param {String} defaultSourceRoot = Default source root for source map if undefined
|
---|
| 233 | * @returns {Promise} that resolves a SourceMapConsumer
|
---|
| 234 | */
|
---|
| 235 | this._getSourceMapConsumer = function _getSourceMapConsumer(sourceMappingURL, defaultSourceRoot) {
|
---|
| 236 | return new Promise(function(resolve) {
|
---|
| 237 | if (this.sourceMapConsumerCache[sourceMappingURL]) {
|
---|
| 238 | resolve(this.sourceMapConsumerCache[sourceMappingURL]);
|
---|
| 239 | } else {
|
---|
| 240 | var sourceMapConsumerPromise = new Promise(function(resolve, reject) {
|
---|
| 241 | return this._get(sourceMappingURL).then(function(sourceMapSource) {
|
---|
| 242 | if (typeof sourceMapSource === 'string') {
|
---|
| 243 | sourceMapSource = _parseJson(sourceMapSource.replace(/^\)\]\}'/, ''));
|
---|
| 244 | }
|
---|
| 245 | if (typeof sourceMapSource.sourceRoot === 'undefined') {
|
---|
| 246 | sourceMapSource.sourceRoot = defaultSourceRoot;
|
---|
| 247 | }
|
---|
| 248 |
|
---|
| 249 | resolve(new SourceMap.SourceMapConsumer(sourceMapSource));
|
---|
| 250 | }).catch(reject);
|
---|
| 251 | }.bind(this));
|
---|
| 252 | this.sourceMapConsumerCache[sourceMappingURL] = sourceMapConsumerPromise;
|
---|
| 253 | resolve(sourceMapConsumerPromise);
|
---|
| 254 | }
|
---|
| 255 | }.bind(this));
|
---|
| 256 | };
|
---|
| 257 |
|
---|
| 258 | /**
|
---|
| 259 | * Given a StackFrame, enhance function name and use source maps for a
|
---|
| 260 | * better StackFrame.
|
---|
| 261 | *
|
---|
| 262 | * @param {StackFrame} stackframe object
|
---|
| 263 | * @returns {Promise} that resolves with with source-mapped StackFrame
|
---|
| 264 | */
|
---|
| 265 | this.pinpoint = function StackTraceGPS$$pinpoint(stackframe) {
|
---|
| 266 | return new Promise(function(resolve, reject) {
|
---|
| 267 | this.getMappedLocation(stackframe).then(function(mappedStackFrame) {
|
---|
| 268 | function resolveMappedStackFrame() {
|
---|
| 269 | resolve(mappedStackFrame);
|
---|
| 270 | }
|
---|
| 271 |
|
---|
| 272 | this.findFunctionName(mappedStackFrame)
|
---|
| 273 | .then(resolve, resolveMappedStackFrame)
|
---|
| 274 | // eslint-disable-next-line no-unexpected-multiline
|
---|
| 275 | ['catch'](resolveMappedStackFrame);
|
---|
| 276 | }.bind(this), reject);
|
---|
| 277 | }.bind(this));
|
---|
| 278 | };
|
---|
| 279 |
|
---|
| 280 | /**
|
---|
| 281 | * Given a StackFrame, guess function name from location information.
|
---|
| 282 | *
|
---|
| 283 | * @param {StackFrame} stackframe
|
---|
| 284 | * @returns {Promise} that resolves with enhanced StackFrame.
|
---|
| 285 | */
|
---|
| 286 | this.findFunctionName = function StackTraceGPS$$findFunctionName(stackframe) {
|
---|
| 287 | return new Promise(function(resolve, reject) {
|
---|
| 288 | _ensureStackFrameIsLegit(stackframe);
|
---|
| 289 | this._get(stackframe.fileName).then(function getSourceCallback(source) {
|
---|
| 290 | var lineNumber = stackframe.lineNumber;
|
---|
| 291 | var columnNumber = stackframe.columnNumber;
|
---|
| 292 | var guessedFunctionName = _findFunctionName(source, lineNumber, columnNumber);
|
---|
| 293 | // Only replace functionName if we found something
|
---|
| 294 | if (guessedFunctionName) {
|
---|
| 295 | resolve(new StackFrame({
|
---|
| 296 | functionName: guessedFunctionName,
|
---|
| 297 | args: stackframe.args,
|
---|
| 298 | fileName: stackframe.fileName,
|
---|
| 299 | lineNumber: lineNumber,
|
---|
| 300 | columnNumber: columnNumber
|
---|
| 301 | }));
|
---|
| 302 | } else {
|
---|
| 303 | resolve(stackframe);
|
---|
| 304 | }
|
---|
| 305 | }, reject)['catch'](reject);
|
---|
| 306 | }.bind(this));
|
---|
| 307 | };
|
---|
| 308 |
|
---|
| 309 | /**
|
---|
| 310 | * Given a StackFrame, seek source-mapped location and return new enhanced StackFrame.
|
---|
| 311 | *
|
---|
| 312 | * @param {StackFrame} stackframe
|
---|
| 313 | * @returns {Promise} that resolves with enhanced StackFrame.
|
---|
| 314 | */
|
---|
| 315 | this.getMappedLocation = function StackTraceGPS$$getMappedLocation(stackframe) {
|
---|
| 316 | return new Promise(function(resolve, reject) {
|
---|
| 317 | _ensureSupportedEnvironment();
|
---|
| 318 | _ensureStackFrameIsLegit(stackframe);
|
---|
| 319 |
|
---|
| 320 | var sourceCache = this.sourceCache;
|
---|
| 321 | var fileName = stackframe.fileName;
|
---|
| 322 | this._get(fileName).then(function(source) {
|
---|
| 323 | var sourceMappingURL = _findSourceMappingURL(source);
|
---|
| 324 | var isDataUrl = sourceMappingURL.substr(0, 5) === 'data:';
|
---|
| 325 | var defaultSourceRoot = fileName.substring(0, fileName.lastIndexOf('/') + 1);
|
---|
| 326 |
|
---|
| 327 | if (sourceMappingURL[0] !== '/' && !isDataUrl && !(/^https?:\/\/|^\/\//i).test(sourceMappingURL)) {
|
---|
| 328 | sourceMappingURL = defaultSourceRoot + sourceMappingURL;
|
---|
| 329 | }
|
---|
| 330 |
|
---|
| 331 | return this._getSourceMapConsumer(sourceMappingURL, defaultSourceRoot)
|
---|
| 332 | .then(function(sourceMapConsumer) {
|
---|
| 333 | return _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache)
|
---|
| 334 | .then(resolve)['catch'](function() {
|
---|
| 335 | resolve(stackframe);
|
---|
| 336 | });
|
---|
| 337 | });
|
---|
| 338 | }.bind(this), reject)['catch'](reject);
|
---|
| 339 | }.bind(this));
|
---|
| 340 | };
|
---|
| 341 | };
|
---|
| 342 | }));
|
---|