|
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