Skip to content

Commit 2d1e2e3

Browse files
committed
Allow for multiple install calls
This allows for multiple libraries to utilize this without stepping on each other’s toes. This makes the retrieve helpers additive, giving priority to the last retrieve method registered. In the event of a miss, the next method is called up to and including the default behavior. The `overrideRetriveFile` and `overrideRetrieveSourceMap` flags are added if this behavior is not desired but this is something that most libraries should avoid to be good citizens. Flagged behaviors will now be on if any of the install calls dictate that they would be. Fix for #91
1 parent 6296f61 commit 2d1e2e3

File tree

2 files changed

+93
-21
lines changed

2 files changed

+93
-21
lines changed

source-map-support.js

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ var path = require('path');
33
var fs = require('fs');
44

55
// Only install once if called multiple times
6-
var alreadyInstalled = false;
6+
var errorFormatterInstalled = false;
7+
var uncaughtShimInstalled = false;
78

89
// If true, the caches are reset before a stack trace formatting operation
910
var emptyCacheBetweenOperations = false;
@@ -17,6 +18,10 @@ var sourceMapCache = {};
1718
// Regex for detecting source maps
1819
var reSourceMap = /^data:application\/json[^,]+base64,/;
1920

21+
// Priority list of retrieve handlers
22+
var retrieveFileHandlers = [];
23+
var retrieveMapHandlers = [];
24+
2025
function isInBrowser() {
2126
return ((typeof window !== 'undefined') && (typeof XMLHttpRequest === 'function'));
2227
}
@@ -25,7 +30,21 @@ function hasGlobalProcessEventEmitter() {
2530
return ((typeof process === 'object') && (process !== null) && (typeof process.on === 'function'));
2631
}
2732

28-
function retrieveFile(path) {
33+
function handlerExec(list) {
34+
return function(arg) {
35+
for (var i = 0; i < list.length; i++) {
36+
var ret = list[i](arg);
37+
if (ret) {
38+
return ret;
39+
}
40+
}
41+
return null;
42+
};
43+
}
44+
45+
var retrieveFile = handlerExec(retrieveFileHandlers);
46+
47+
retrieveFileHandlers.push(function(path) {
2948
// Trim the path to make sure there is no extra whitespace.
3049
path = path.trim();
3150
if (path in fileContentsCache) {
@@ -53,7 +72,7 @@ function retrieveFile(path) {
5372
}
5473

5574
return fileContentsCache[path] = contents;
56-
}
75+
});
5776

5877
// Support URLs relative to a directory, but be careful about a protocol prefix
5978
// in case we are in the browser (i.e. directories may start with "http://")
@@ -99,7 +118,8 @@ function retrieveSourceMapURL(source) {
99118
// there is no source map. The map field may be either a string or the parsed
100119
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
101120
// constructor).
102-
function retrieveSourceMap(source) {
121+
var retrieveSourceMap = handlerExec(retrieveMapHandlers);
122+
retrieveMapHandlers.push(function(source) {
103123
var sourceMappingURL = retrieveSourceMapURL(source);
104124
if (!sourceMappingURL) return null;
105125

@@ -124,7 +144,7 @@ function retrieveSourceMap(source) {
124144
url: sourceMappingURL,
125145
map: sourceMapData
126146
};
127-
}
147+
});
128148

129149
function mapSourcePosition(position) {
130150
var sourceMap = sourceMapCache[position.source];
@@ -386,7 +406,7 @@ function shimEmitUncaughtException () {
386406
}
387407

388408
return origEmit.apply(this, arguments);
389-
}
409+
};
390410
}
391411

392412
exports.wrapCallSite = wrapCallSite;
@@ -395,26 +415,43 @@ exports.mapSourcePosition = mapSourcePosition;
395415
exports.retrieveSourceMap = retrieveSourceMap;
396416

397417
exports.install = function(options) {
398-
if (!alreadyInstalled) {
399-
alreadyInstalled = true;
400-
Error.prepareStackTrace = prepareStackTrace;
418+
options = options || {};
401419

402-
// Configure options
403-
options = options || {};
404-
var installHandler = 'handleUncaughtExceptions' in options ?
405-
options.handleUncaughtExceptions : true;
420+
// Allow sources to be found by methods other than reading the files
421+
// directly from disk.
422+
if (options.retrieveFile) {
423+
if (options.overrideRetrieveFile) {
424+
retrieveFileHandlers.length = 0;
425+
}
426+
427+
retrieveFileHandlers.unshift(options.retrieveFile);
428+
}
429+
430+
// Allow source maps to be found by methods other than reading the files
431+
// directly from disk.
432+
if (options.retrieveSourceMap) {
433+
if (options.overrideRetrieveSourceMap) {
434+
retrieveMapHandlers.length = 0;
435+
}
436+
437+
retrieveMapHandlers.unshift(options.retrieveSourceMap);
438+
}
439+
440+
// Configure options
441+
if (!emptyCacheBetweenOperations) {
406442
emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
407443
options.emptyCacheBetweenOperations : false;
444+
}
408445

409-
// Allow sources to be found by methods other than reading the files
410-
// directly from disk.
411-
if (options.retrieveFile)
412-
retrieveFile = options.retrieveFile;
446+
// Install the error reformatter
447+
if (!errorFormatterInstalled) {
448+
errorFormatterInstalled = true;
449+
Error.prepareStackTrace = prepareStackTrace;
450+
}
413451

414-
// Allow source maps to be found by methods other than reading the files
415-
// directly from disk.
416-
if (options.retrieveSourceMap)
417-
retrieveSourceMap = options.retrieveSourceMap;
452+
if (!uncaughtShimInstalled) {
453+
var installHandler = 'handleUncaughtExceptions' in options ?
454+
options.handleUncaughtExceptions : true;
418455

419456
// Provide the option to not install the uncaught exception handler. This is
420457
// to support other uncaught exception handlers (in test frameworks, for
@@ -424,6 +461,7 @@ exports.install = function(options) {
424461
// generated JavaScript code will be shown above the stack trace instead of
425462
// the original source code.
426463
if (installHandler && hasGlobalProcessEventEmitter()) {
464+
uncaughtShimInstalled = true;
427465
shimEmitUncaughtException();
428466
}
429467
}

test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ it('missing source maps should also be cached', function(done) {
375375
' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
376376
'}',
377377
'require("./source-map-support").install({',
378+
' overrideRetrieveSourceMap: true,',
378379
' retrieveSourceMap: function(name) {',
379380
' if (/\\.generated.js$/.test(name)) count++;',
380381
' return null;',
@@ -392,6 +393,39 @@ it('missing source maps should also be cached', function(done) {
392393
]);
393394
});
394395

396+
it('should consult all retrieve source map providers', function(done) {
397+
compareStdout(done, createSingleLineSourceMap(), [
398+
'',
399+
'var count = 0;',
400+
'function foo() {',
401+
' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
402+
'}',
403+
'require("./source-map-support").install({',
404+
' retrieveSourceMap: function(name) {',
405+
' if (/\\.generated.js$/.test(name)) count++;',
406+
' return undefined;',
407+
' }',
408+
'});',
409+
'require("./source-map-support").install({',
410+
' retrieveSourceMap: function(name) {',
411+
' if (/\\.generated.js$/.test(name)) {',
412+
' count++;',
413+
' return ' + JSON.stringify({url: '.original.js', map: createMultiLineSourceMapWithSourcesContent().toJSON()}) + ';',
414+
' }',
415+
' }',
416+
'});',
417+
'process.nextTick(foo);',
418+
'process.nextTick(foo);',
419+
'process.nextTick(function() { console.log(count); });',
420+
], [
421+
'Error: this is the error',
422+
/^ at foo \(.*\/original.js:1004:5\)$/,
423+
'Error: this is the error',
424+
/^ at foo \(.*\/original.js:1004:5\)$/,
425+
'1', // The retrieval should only be attempted once
426+
]);
427+
});
428+
395429
/* The following test duplicates some of the code in
396430
* `compareStackTrace` but appends a charset to the
397431
* source mapping url.

0 commit comments

Comments
 (0)