From b40cc2539e8f643d17ebffb0925d38dd2c726968 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Aug 2015 16:25:38 -0500 Subject: [PATCH] Allow for multiple install calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- source-map-support.js | 94 ++++++++++++++++++++++++++++++------------- test.js | 34 ++++++++++++++++ 2 files changed, 100 insertions(+), 28 deletions(-) diff --git a/source-map-support.js b/source-map-support.js index 9668f79..49ed72d 100644 --- a/source-map-support.js +++ b/source-map-support.js @@ -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(); } } diff --git a/test.js b/test.js index 047f537..1315c9f 100644 --- a/test.js +++ b/test.js @@ -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.