Skip to content

Support multiple instances of source-map-support #28

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
Oct 4, 2021
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
152 changes: 99 additions & 53 deletions source-map-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -58,21 +104,27 @@ 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);
if (ret) {
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)) {
Expand All @@ -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 = '';
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
};
Expand All @@ -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
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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);
};

Expand All @@ -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;

Expand All @@ -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;
}
42 changes: 42 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,45 @@ it('normal console.trace', function(done) {
/^ at Object\.<anonymous> \((?:.*[/\\])?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\)$/
]);
});