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
master
kpdecker 9 years ago
parent 793a35a2eb
commit b40cc2539e

@ -3,7 +3,8 @@ var path = require('path');
var fs = require('fs');
// Only install once if called multiple times
var alreadyInstalled = false;
var errorFormatterInstalled = false;
var uncaughtShimInstalled = false;
// If true, the caches are reset before a stack trace formatting operation
var emptyCacheBetweenOperations = false;
@ -20,6 +21,10 @@ var sourceMapCache = {};
// Regex for detecting source maps
var reSourceMap = /^data:application\/json[^,]+base64,/;
// Priority list of retrieve handlers
var retrieveFileHandlers = [];
var retrieveMapHandlers = [];
function isInBrowser() {
if (environment === "browser")
return true;
@ -32,7 +37,21 @@ function hasGlobalProcessEventEmitter() {
return ((typeof process === 'object') && (process !== null) && (typeof process.on === 'function'));
}
function retrieveFile(path) {
function handlerExec(list) {
return function(arg) {
for (var i = 0; i < list.length; i++) {
var ret = list[i](arg);
if (ret) {
return ret;
}
}
return null;
};
}
var retrieveFile = handlerExec(retrieveFileHandlers);
retrieveFileHandlers.push(function(path) {
// Trim the path to make sure there is no extra whitespace.
path = path.trim();
if (path in fileContentsCache) {
@ -60,7 +79,7 @@ function retrieveFile(path) {
}
return fileContentsCache[path] = contents;
}
});
// Support URLs relative to a directory, but be careful about a protocol prefix
// in case we are in the browser (i.e. directories may start with "http://")
@ -106,7 +125,8 @@ function retrieveSourceMapURL(source) {
// there is no source map. The map field may be either a string or the parsed
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
// constructor).
function retrieveSourceMap(source) {
var retrieveSourceMap = handlerExec(retrieveMapHandlers);
retrieveMapHandlers.push(function(source) {
var sourceMappingURL = retrieveSourceMapURL(source);
if (!sourceMappingURL) return null;
@ -131,7 +151,7 @@ function retrieveSourceMap(source) {
url: sourceMappingURL,
map: sourceMapData
};
}
});
function mapSourcePosition(position) {
var sourceMap = sourceMapCache[position.source];
@ -393,7 +413,7 @@ function shimEmitUncaughtException () {
}
return origEmit.apply(this, arguments);
}
};
}
exports.wrapCallSite = wrapCallSite;
@ -402,33 +422,50 @@ exports.mapSourcePosition = mapSourcePosition;
exports.retrieveSourceMap = retrieveSourceMap;
exports.install = function(options) {
if (!alreadyInstalled) {
alreadyInstalled = true;
Error.prepareStackTrace = prepareStackTrace;
options = options || {};
// Configure options
options = options || {};
var installHandler = 'handleUncaughtExceptions' in options ?
options.handleUncaughtExceptions : true;
if (options.environment) {
environment = options.environment;
if (["node", "browser", "auto"].indexOf(environment) === -1) {
throw new Error("environment " + environment + " was unknown. Available options are {auto, browser, node}")
}
}
// Allow sources to be found by methods other than reading the files
// directly from disk.
if (options.retrieveFile) {
if (options.overrideRetrieveFile) {
retrieveFileHandlers.length = 0;
}
retrieveFileHandlers.unshift(options.retrieveFile);
}
// Allow source maps to be found by methods other than reading the files
// directly from disk.
if (options.retrieveSourceMap) {
if (options.overrideRetrieveSourceMap) {
retrieveMapHandlers.length = 0;
}
retrieveMapHandlers.unshift(options.retrieveSourceMap);
}
// Configure options
if (!emptyCacheBetweenOperations) {
emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
options.emptyCacheBetweenOperations : false;
}
if (options.environment) {
environment = options.environment;
if (["node", "browser", "auto"].indexOf(environment) === -1)
throw new Error("environment " + environment + " was unknown. Available options are {auto, browser, node}")
}
// Allow sources to be found by methods other than reading the files
// directly from disk.
if (options.retrieveFile)
retrieveFile = options.retrieveFile;
// Install the error reformatter
if (!errorFormatterInstalled) {
errorFormatterInstalled = true;
Error.prepareStackTrace = prepareStackTrace;
}
// Allow source maps to be found by methods other than reading the files
// directly from disk.
if (options.retrieveSourceMap)
retrieveSourceMap = options.retrieveSourceMap;
if (!uncaughtShimInstalled) {
var installHandler = 'handleUncaughtExceptions' in options ?
options.handleUncaughtExceptions : true;
// Provide the option to not install the uncaught exception handler. This is
// to support other uncaught exception handlers (in test frameworks, for
@ -438,6 +475,7 @@ exports.install = function(options) {
// generated JavaScript code will be shown above the stack trace instead of
// the original source code.
if (installHandler && hasGlobalProcessEventEmitter()) {
uncaughtShimInstalled = true;
shimEmitUncaughtException();
}
}

@ -388,6 +388,7 @@ it('missing source maps should also be cached', function(done) {
' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
'}',
'require("./source-map-support").install({',
' overrideRetrieveSourceMap: true,',
' retrieveSourceMap: function(name) {',
' if (/\\.generated.js$/.test(name)) count++;',
' return null;',
@ -405,6 +406,39 @@ it('missing source maps should also be cached', function(done) {
]);
});
it('should consult all retrieve source map providers', function(done) {
compareStdout(done, createSingleLineSourceMap(), [
'',
'var count = 0;',
'function foo() {',
' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
'}',
'require("./source-map-support").install({',
' retrieveSourceMap: function(name) {',
' if (/\\.generated.js$/.test(name)) count++;',
' return undefined;',
' }',
'});',
'require("./source-map-support").install({',
' retrieveSourceMap: function(name) {',
' if (/\\.generated.js$/.test(name)) {',
' count++;',
' return ' + JSON.stringify({url: '.original.js', map: createMultiLineSourceMapWithSourcesContent().toJSON()}) + ';',
' }',
' }',
'});',
'process.nextTick(foo);',
'process.nextTick(foo);',
'process.nextTick(function() { console.log(count); });',
], [
'Error: this is the error',
/^ at foo \(.*\/original.js:1004:5\)$/,
'Error: this is the error',
/^ at foo \(.*\/original.js:1004:5\)$/,
'1', // The retrieval should only be attempted once
]);
});
/* The following test duplicates some of the code in
* `compareStackTrace` but appends a charset to the
* source mapping url.

Loading…
Cancel
Save