diff --git a/source-map-support.js b/source-map-support.js index e2568d7..2368e75 100644 --- a/source-map-support.js +++ b/source-map-support.js @@ -23,29 +23,75 @@ function dynamicRequire(mod, request) { return mod.require(request); } -// Only install once if called multiple times -var errorFormatterInstalled = false; -var uncaughtShimInstalled = false; +// Increment this if the format of sharedData changes in a breaking way. +var sharedDataVersion = 1; + +function initializeSharedData(defaults) { + var sharedDataKey = 'source-map-support/sharedData'; + if (typeof Symbol !== 'undefined') { + sharedDataKey = Symbol.for(sharedDataKey); + } + var sharedData = this[sharedDataKey]; + if (!sharedData) { + sharedData = { version: sharedDataVersion }; + if (Object.defineProperty) { + Object.defineProperty(this, sharedDataKey, { value: sharedData }); + } else { + this[sharedDataKey] = sharedData; + } + } + if (sharedDataVersion !== sharedData.version) { + throw new Error("Multiple incompatible instances of source-map-support were loaded"); + } + for (var key in defaults) { + if (!(key in sharedData)) { + sharedData[key] = defaults[key]; + } + } + return sharedData; +} -// If true, the caches are reset before a stack trace formatting operation -var emptyCacheBetweenOperations = false; +// If multiple instances of source-map-support are loaded into the same +// context, they shouldn't overwrite each other. By storing handlers, caches, +// and other state on a shared object, different instances of +// source-map-support can work together in a limited way. This does require +// that future versions of source-map-support continue to support the fields on +// this object. If this internal contract ever needs to be broken, increment +// sharedDataVersion. (This version number is not the same as any of the +// package's version numbers, which should reflect the *external* API of +// source-map-support.) +var sharedData = initializeSharedData({ -// Supports {browser, node, auto} -var environment = "auto"; + // Only install once if called multiple times + errorFormatterInstalled: false, + uncaughtShimInstalled: false, + + // If true, the caches are reset before a stack trace formatting operation + emptyCacheBetweenOperations: false, + + // Maps a file path to a string containing the file contents + fileContentsCache: {}, + + // Maps a file path to a source map for that file + sourceMapCache: {}, -// Maps a file path to a string containing the file contents -var fileContentsCache = {}; + // Priority list of retrieve handlers + retrieveFileHandlers: [], + retrieveMapHandlers: [], -// Maps a file path to a source map for that file -var sourceMapCache = {}; + // Priority list of internally-implemented handlers. + // When resetting state, we must keep these. + internalRetrieveFileHandlers: [], + internalRetrieveMapHandlers: [], + +}); + +// Supports {browser, node, auto} +var environment = "auto"; // 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; @@ -58,7 +104,7 @@ function hasGlobalProcessEventEmitter() { return ((typeof process === 'object') && (process !== null) && (typeof process.on === 'function')); } -function handlerExec(list) { +function handlerExec(list, internalList) { return function(arg) { for (var i = 0; i < list.length; i++) { var ret = list[i](arg); @@ -66,13 +112,19 @@ function handlerExec(list) { return ret; } } + for (var i = 0; i < internalList.length; i++) { + var ret = internalList[i](arg); + if (ret) { + return ret; + } + } return null; }; } -var retrieveFile = handlerExec(retrieveFileHandlers); +var retrieveFile = handlerExec(sharedData.retrieveFileHandlers, sharedData.internalRetrieveFileHandlers); -retrieveFileHandlers.push(function(path) { +sharedData.internalRetrieveFileHandlers.push(function(path) { // Trim the path to make sure there is no extra whitespace. path = path.trim(); if (/^file:/.test(path)) { @@ -83,8 +135,8 @@ retrieveFileHandlers.push(function(path) { '/'; // file:///root-dir/file -> /root-dir/file }); } - if (path in fileContentsCache) { - return fileContentsCache[path]; + if (path in sharedData.fileContentsCache) { + return sharedData.fileContentsCache[path]; } var contents = ''; @@ -105,7 +157,7 @@ retrieveFileHandlers.push(function(path) { /* ignore any errors */ } - return fileContentsCache[path] = contents; + return sharedData.fileContentsCache[path] = contents; }); // Support URLs relative to a directory, but be careful about a protocol prefix @@ -160,8 +212,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). -var retrieveSourceMap = handlerExec(retrieveMapHandlers); -retrieveMapHandlers.push(function(source) { +var retrieveSourceMap = handlerExec(sharedData.retrieveMapHandlers, sharedData.internalRetrieveMapHandlers); +sharedData.internalRetrieveMapHandlers.push(function(source) { var sourceMappingURL = retrieveSourceMapURL(source); if (!sourceMappingURL) return null; @@ -189,12 +241,12 @@ retrieveMapHandlers.push(function(source) { }); function mapSourcePosition(position) { - var sourceMap = sourceMapCache[position.source]; + var sourceMap = sharedData.sourceMapCache[position.source]; if (!sourceMap) { // Call the (overrideable) retrieveSourceMap function to get the source map. var urlAndMap = retrieveSourceMap(position.source); if (urlAndMap) { - sourceMap = sourceMapCache[position.source] = { + sourceMap = sharedData.sourceMapCache[position.source] = { url: urlAndMap.url, map: new SourceMapConsumer(urlAndMap.map) }; @@ -206,12 +258,12 @@ function mapSourcePosition(position) { var contents = sourceMap.map.sourcesContent[i]; if (contents) { var url = supportRelativeURL(sourceMap.url, source); - fileContentsCache[url] = contents; + sharedData.fileContentsCache[url] = contents; } }); } } else { - sourceMap = sourceMapCache[position.source] = { + sourceMap = sharedData.sourceMapCache[position.source] = { url: null, map: null }; @@ -434,9 +486,9 @@ const ErrorPrototypeToString = (err) =>Error.prototype.toString.call(err); // This function is part of the V8 stack trace API, for more info see: // https://v8.dev/docs/stack-trace-api function prepareStackTrace(error, stack) { - if (emptyCacheBetweenOperations) { - fileContentsCache = {}; - sourceMapCache = {}; + if (sharedData.emptyCacheBetweenOperations) { + sharedData.fileContentsCache = {}; + sharedData.sourceMapCache = {}; } // node gives its own errors special treatment. Mimic that behavior @@ -474,7 +526,7 @@ function getErrorSource(error) { var column = +match[3]; // Support the inline sourceContents inside the source map - var contents = fileContentsCache[source]; + var contents = sharedData.fileContentsCache[source]; // Support files on disk if (!contents && fs && fs.existsSync(source)) { @@ -537,8 +589,8 @@ function shimEmitUncaughtException () { }; } -var originalRetrieveFileHandlers = retrieveFileHandlers.slice(0); -var originalRetrieveMapHandlers = retrieveMapHandlers.slice(0); +var originalRetrieveFileHandlers = sharedData.retrieveFileHandlers.slice(0); +var originalRetrieveMapHandlers = sharedData.retrieveMapHandlers.slice(0); exports.wrapCallSite = wrapCallSite; exports.getErrorSource = getErrorSource; @@ -559,20 +611,20 @@ exports.install = function(options) { // directly from disk. if (options.retrieveFile) { if (options.overrideRetrieveFile) { - retrieveFileHandlers.length = 0; + sharedData.retrieveFileHandlers.length = 0; } - retrieveFileHandlers.unshift(options.retrieveFile); + sharedData.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; + sharedData.retrieveMapHandlers.length = 0; } - retrieveMapHandlers.unshift(options.retrieveSourceMap); + sharedData.retrieveMapHandlers.unshift(options.retrieveSourceMap); } // Support runtime transpilers that include inline source maps @@ -583,8 +635,8 @@ exports.install = function(options) { if (!$compile.__sourceMapSupport) { Module.prototype._compile = function(content, filename) { - fileContentsCache[filename] = content; - sourceMapCache[filename] = undefined; + sharedData.fileContentsCache[filename] = content; + sharedData.sourceMapCache[filename] = undefined; return $compile.call(this, content, filename); }; @@ -593,18 +645,18 @@ exports.install = function(options) { } // Configure options - if (!emptyCacheBetweenOperations) { - emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ? + if (!sharedData.emptyCacheBetweenOperations) { + sharedData.emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ? options.emptyCacheBetweenOperations : false; } // Install the error reformatter - if (!errorFormatterInstalled) { - errorFormatterInstalled = true; + if (!sharedData.errorFormatterInstalled) { + sharedData.errorFormatterInstalled = true; Error.prepareStackTrace = prepareStackTrace; } - if (!uncaughtShimInstalled) { + if (!sharedData.uncaughtShimInstalled) { var installHandler = 'handleUncaughtExceptions' in options ? options.handleUncaughtExceptions : true; @@ -627,19 +679,13 @@ 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; + sharedData.uncaughtShimInstalled = true; shimEmitUncaughtException(); } } }; exports.resetRetrieveHandlers = function() { - retrieveFileHandlers.length = 0; - retrieveMapHandlers.length = 0; - - retrieveFileHandlers = originalRetrieveFileHandlers.slice(0); - retrieveMapHandlers = originalRetrieveMapHandlers.slice(0); - - retrieveSourceMap = handlerExec(retrieveMapHandlers); - retrieveFile = handlerExec(retrieveFileHandlers); + sharedData.retrieveFileHandlers.length = 0; + sharedData.retrieveMapHandlers.length = 0; } diff --git a/test.js b/test.js index ff4a6d7..1de210f 100644 --- a/test.js +++ b/test.js @@ -638,3 +638,45 @@ it('normal console.trace', function(done) { /^ at Object\. \((?:.*[/\\])?line2\.js:1002:102\)$/ ]); }); + +it('supports multiple instances', function(done) { + function finish(err) { + fs.unlinkSync('.original2.js'); + fs.unlinkSync('.generated2.js'); + fs.unlinkSync('.generated2.js.map.extra') + done(err); + } + var sourceMap = createEmptySourceMap(); + sourceMap.addMapping({ + generated: { line: 1, column: 0 }, + original: { line: 1, column: 0 }, + source: '.original2.js' + }); + fs.writeFileSync('.generated2.js.map.extra', sourceMap.toString()); + fs.writeFileSync('.generated2.js', [ + 'module.exports = function foo() { throw new Error("this is the error"); }', + '//@ sourceMappingURL=.generated2.js.map' + ].join('\n')); + fs.writeFileSync('.original2.js', 'this is some other original code'); + compareStdout(finish, createEmptySourceMap(), [ + 'require("./source-map-support").install({', + ' retrieveFile: function(path) {', + ' var fs = require("fs");', + ' if (fs.existsSync(path + ".extra")) {', + ' return fs.readFileSync(path + ".extra", "utf8");', + ' }', + ' }', + '});', + 'var foo = require("./.generated2.js");', + 'delete require.cache[require.resolve("./source-map-support")];', + 'require("./source-map-support").install();', + 'process.nextTick(foo);', + 'process.nextTick(function() { process.exit(1); });' + ], [ + /[/\\].original2\.js:1$/, + 'this is some other original code', + '^', + 'Error: this is the error', + /^ at foo \((?:.*[/\\])?.original2\.js:1:1\)$/ + ]); +});