|
26 | 26 | req.onerror = reject;
|
27 | 27 | req.onreadystatechange = function onreadystatechange() {
|
28 | 28 | if (req.readyState === 4) {
|
29 |
| - if (req.status >= 200 && req.status < 300) { |
| 29 | + if ((req.status >= 200 && req.status < 300) || |
| 30 | + (url.substr(0, 7) === 'file://' && req.responseText)) { |
30 | 31 | resolve(req.responseText);
|
31 | 32 | } else {
|
32 | 33 | reject(new Error('HTTP status: ' + req.status + ' retrieving ' + url));
|
|
62 | 63 | }
|
63 | 64 |
|
64 | 65 | function _findFunctionName(source, lineNumber/*, columnNumber*/) {
|
65 |
| - // function {name}({args}) m[1]=name m[2]=args |
66 |
| - var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; |
67 |
| - // {name} = function ({args}) TODO args capture |
68 |
| - var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; |
69 |
| - // {name} = eval() |
70 |
| - var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; |
| 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 | + ]; |
71 | 78 | var lines = source.split('\n');
|
72 | 79 |
|
73 | 80 | // Walk backwards in the source lines until we find the line which matches one of the patterns above
|
74 | 81 | var code = '';
|
75 | 82 | var maxLines = Math.min(lineNumber, 20);
|
76 |
| - var m; |
77 | 83 | for (var i = 0; i < maxLines; ++i) {
|
78 | 84 | // lineNo is 1-based, source[] is 0-based
|
79 | 85 | var line = lines[lineNumber - i - 1];
|
|
84 | 90 |
|
85 | 91 | if (line) {
|
86 | 92 | code = line + code;
|
87 |
| - m = reFunctionExpression.exec(code); |
88 |
| - if (m && m[1]) { |
89 |
| - return m[1]; |
90 |
| - } |
91 |
| - m = reFunctionDeclaration.exec(code); |
92 |
| - if (m && m[1]) { |
93 |
| - return m[1]; |
94 |
| - } |
95 |
| - m = reFunctionEvaluation.exec(code); |
96 |
| - if (m && m[1]) { |
97 |
| - return m[1]; |
| 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 | + } |
98 | 99 | }
|
99 | 100 | }
|
100 | 101 | }
|
|
125 | 126 | }
|
126 | 127 |
|
127 | 128 | function _findSourceMappingURL(source) {
|
128 |
| - var m = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/.exec(source); |
| 129 | + var m = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/m.exec(source); |
129 | 130 | if (m && m[1]) {
|
130 | 131 | return m[1];
|
131 | 132 | } else {
|
132 | 133 | throw new Error('sourceMappingURL not found');
|
133 | 134 | }
|
134 | 135 | }
|
135 | 136 |
|
136 |
| - function _extractLocationInfoFromSourceMap(stackframe, rawSourceMap, sourceCache) { |
| 137 | + function _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache) { |
137 | 138 | return new Promise(function(resolve, reject) {
|
138 |
| - var mapConsumer = new SourceMap.SourceMapConsumer(rawSourceMap); |
139 |
| - |
140 |
| - var loc = mapConsumer.originalPositionFor({ |
| 139 | + var loc = sourceMapConsumer.originalPositionFor({ |
141 | 140 | line: stackframe.lineNumber,
|
142 | 141 | column: stackframe.columnNumber
|
143 | 142 | });
|
144 | 143 |
|
145 | 144 | if (loc.source) {
|
146 |
| - var mappedSource = mapConsumer.sourceContentFor(loc.source); |
| 145 | + // cache mapped sources |
| 146 | + var mappedSource = sourceMapConsumer.sourceContentFor(loc.source); |
147 | 147 | if (mappedSource) {
|
148 | 148 | sourceCache[loc.source] = mappedSource;
|
149 | 149 | }
|
| 150 | + |
150 | 151 | resolve(
|
151 |
| - new StackFrame( |
152 |
| - loc.name || stackframe.functionName, |
153 |
| - stackframe.args, |
154 |
| - loc.source, |
155 |
| - loc.line, |
156 |
| - loc.column)); |
| 152 | + // given stackframe and source location, update stackframe |
| 153 | + new StackFrame({ |
| 154 | + functionName: loc.name || stackframe.functionName, |
| 155 | + args: stackframe.args, |
| 156 | + fileName: loc.source, |
| 157 | + lineNumber: loc.line, |
| 158 | + columnNumber: loc.column |
| 159 | + })); |
157 | 160 | } else {
|
158 | 161 | reject(new Error('Could not get original source for given stackframe and source map'));
|
159 | 162 | }
|
|
164 | 167 | * @constructor
|
165 | 168 | * @param {Object} opts
|
166 | 169 | * opts.sourceCache = {url: "Source String"} => preload source cache
|
| 170 | + * opts.sourceMapConsumerCache = {/path/file.js.map: SourceMapConsumer} |
167 | 171 | * opts.offline = True to prevent network requests.
|
168 | 172 | * Best effort without sources or source maps.
|
169 | 173 | * opts.ajax = Promise returning function to make X-Domain requests
|
|
175 | 179 | opts = opts || {};
|
176 | 180 |
|
177 | 181 | this.sourceCache = opts.sourceCache || {};
|
| 182 | + this.sourceMapConsumerCache = opts.sourceMapConsumerCache || {}; |
178 | 183 |
|
179 | 184 | this.ajax = opts.ajax || _xdr;
|
180 | 185 |
|
|
213 | 218 | }.bind(this));
|
214 | 219 | };
|
215 | 220 |
|
| 221 | + /** |
| 222 | + * Creating SourceMapConsumers is expensive, so this wraps the creation of a |
| 223 | + * SourceMapConsumer in a per-instance cache. |
| 224 | + * |
| 225 | + * @param sourceMappingURL = {String} URL to fetch source map from |
| 226 | + * @param defaultSourceRoot = Default source root for source map if undefined |
| 227 | + * @returns {Promise} that resolves a SourceMapConsumer |
| 228 | + */ |
| 229 | + this._getSourceMapConsumer = function _getSourceMapConsumer(sourceMappingURL, defaultSourceRoot) { |
| 230 | + return new Promise(function(resolve, reject) { |
| 231 | + if (this.sourceMapConsumerCache[sourceMappingURL]) { |
| 232 | + resolve(this.sourceMapConsumerCache[sourceMappingURL]); |
| 233 | + } else { |
| 234 | + var sourceMapConsumerPromise = new Promise(function(resolve, reject) { |
| 235 | + return this._get(sourceMappingURL).then(function(sourceMapSource) { |
| 236 | + if (typeof sourceMapSource === 'string') { |
| 237 | + sourceMapSource = _parseJson(sourceMapSource.replace(/^\)\]\}'/, '')); |
| 238 | + } |
| 239 | + if (typeof sourceMapSource.sourceRoot === 'undefined') { |
| 240 | + sourceMapSource.sourceRoot = defaultSourceRoot; |
| 241 | + } |
| 242 | + |
| 243 | + resolve(new SourceMap.SourceMapConsumer(sourceMapSource)); |
| 244 | + }, reject); |
| 245 | + }.bind(this)); |
| 246 | + this.sourceMapConsumerCache[sourceMappingURL] = sourceMapConsumerPromise; |
| 247 | + resolve(sourceMapConsumerPromise); |
| 248 | + } |
| 249 | + }.bind(this)); |
| 250 | + }; |
| 251 | + |
216 | 252 | /**
|
217 | 253 | * Given a StackFrame, enhance function name and use source maps for a
|
218 | 254 | * better StackFrame.
|
|
249 | 285 | var guessedFunctionName = _findFunctionName(source, lineNumber, columnNumber);
|
250 | 286 | // Only replace functionName if we found something
|
251 | 287 | if (guessedFunctionName) {
|
252 |
| - resolve(new StackFrame(guessedFunctionName, |
253 |
| - stackframe.args, |
254 |
| - stackframe.fileName, |
255 |
| - lineNumber, |
256 |
| - columnNumber)); |
| 288 | + resolve(new StackFrame({ |
| 289 | + functionName: guessedFunctionName, |
| 290 | + args: stackframe.args, |
| 291 | + fileName: stackframe.fileName, |
| 292 | + lineNumber: lineNumber, |
| 293 | + columnNumber: columnNumber |
| 294 | + })); |
257 | 295 | } else {
|
258 | 296 | resolve(stackframe);
|
259 | 297 | }
|
|
277 | 315 | this._get(fileName).then(function(source) {
|
278 | 316 | var sourceMappingURL = _findSourceMappingURL(source);
|
279 | 317 | var isDataUrl = sourceMappingURL.substr(0, 5) === 'data:';
|
280 |
| - var base = fileName.substring(0, fileName.lastIndexOf('/') + 1); |
| 318 | + var defaultSourceRoot = fileName.substring(0, fileName.lastIndexOf('/') + 1); |
281 | 319 |
|
282 | 320 | if (sourceMappingURL[0] !== '/' && !isDataUrl && !(/^https?:\/\/|^\/\//i).test(sourceMappingURL)) {
|
283 |
| - sourceMappingURL = base + sourceMappingURL; |
| 321 | + sourceMappingURL = defaultSourceRoot + sourceMappingURL; |
284 | 322 | }
|
285 | 323 |
|
286 |
| - this._get(sourceMappingURL).then(function(sourceMap) { |
287 |
| - if (typeof sourceMap === 'string') { |
288 |
| - sourceMap = _parseJson(sourceMap.replace(/^\)\]\}'/, '')); |
289 |
| - } |
290 |
| - if (typeof sourceMap.sourceRoot === 'undefined') { |
291 |
| - sourceMap.sourceRoot = base; |
292 |
| - } |
293 |
| - |
294 |
| - _extractLocationInfoFromSourceMap(stackframe, sourceMap, sourceCache) |
| 324 | + return this._getSourceMapConsumer(sourceMappingURL, defaultSourceRoot).then(function(sourceMapConsumer) { |
| 325 | + return _extractLocationInfoFromSourceMapSource(stackframe, sourceMapConsumer, sourceCache) |
295 | 326 | .then(resolve)['catch'](function() {
|
296 | 327 | resolve(stackframe);
|
297 | 328 | });
|
298 |
| - }, reject)['catch'](reject); |
| 329 | + }); |
299 | 330 | }.bind(this), reject)['catch'](reject);
|
300 | 331 | }.bind(this));
|
301 | 332 | };
|
|
0 commit comments