Skip to content

Allow for multiple install calls #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 66 additions & 28 deletions source-map-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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://")
Expand Down Expand Up @@ -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;

Expand All @@ -131,7 +151,7 @@ function retrieveSourceMap(source) {
url: sourceMappingURL,
map: sourceMapData
};
}
});

function mapSourcePosition(position) {
var sourceMap = sourceMapCache[position.source];
Expand Down Expand Up @@ -393,7 +413,7 @@ function shimEmitUncaughtException () {
}

return origEmit.apply(this, arguments);
}
};
}

exports.wrapCallSite = wrapCallSite;
Expand All @@ -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
Expand All @@ -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();
}
}
Expand Down
34 changes: 34 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;',
Expand All @@ -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.
Expand Down