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 | }));
|
---|