Skip to content

Commit c98a759

Browse files
authored
Merge pull request #28 from cspotcode/support-multiple-instances
Support multiple instances of source-map-support
2 parents 2e086aa + 79e0c1b commit c98a759

File tree

2 files changed

+141
-53
lines changed

2 files changed

+141
-53
lines changed

source-map-support.js

Lines changed: 99 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,75 @@ function dynamicRequire(mod, request) {
2323
return mod.require(request);
2424
}
2525

26-
// Only install once if called multiple times
27-
var errorFormatterInstalled = false;
28-
var uncaughtShimInstalled = false;
26+
// Increment this if the format of sharedData changes in a breaking way.
27+
var sharedDataVersion = 1;
28+
29+
function initializeSharedData(defaults) {
30+
var sharedDataKey = 'source-map-support/sharedData';
31+
if (typeof Symbol !== 'undefined') {
32+
sharedDataKey = Symbol.for(sharedDataKey);
33+
}
34+
var sharedData = this[sharedDataKey];
35+
if (!sharedData) {
36+
sharedData = { version: sharedDataVersion };
37+
if (Object.defineProperty) {
38+
Object.defineProperty(this, sharedDataKey, { value: sharedData });
39+
} else {
40+
this[sharedDataKey] = sharedData;
41+
}
42+
}
43+
if (sharedDataVersion !== sharedData.version) {
44+
throw new Error("Multiple incompatible instances of source-map-support were loaded");
45+
}
46+
for (var key in defaults) {
47+
if (!(key in sharedData)) {
48+
sharedData[key] = defaults[key];
49+
}
50+
}
51+
return sharedData;
52+
}
2953

30-
// If true, the caches are reset before a stack trace formatting operation
31-
var emptyCacheBetweenOperations = false;
54+
// If multiple instances of source-map-support are loaded into the same
55+
// context, they shouldn't overwrite each other. By storing handlers, caches,
56+
// and other state on a shared object, different instances of
57+
// source-map-support can work together in a limited way. This does require
58+
// that future versions of source-map-support continue to support the fields on
59+
// this object. If this internal contract ever needs to be broken, increment
60+
// sharedDataVersion. (This version number is not the same as any of the
61+
// package's version numbers, which should reflect the *external* API of
62+
// source-map-support.)
63+
var sharedData = initializeSharedData({
3264

33-
// Supports {browser, node, auto}
34-
var environment = "auto";
65+
// Only install once if called multiple times
66+
errorFormatterInstalled: false,
67+
uncaughtShimInstalled: false,
68+
69+
// If true, the caches are reset before a stack trace formatting operation
70+
emptyCacheBetweenOperations: false,
71+
72+
// Maps a file path to a string containing the file contents
73+
fileContentsCache: {},
74+
75+
// Maps a file path to a source map for that file
76+
sourceMapCache: {},
3577

36-
// Maps a file path to a string containing the file contents
37-
var fileContentsCache = {};
78+
// Priority list of retrieve handlers
79+
retrieveFileHandlers: [],
80+
retrieveMapHandlers: [],
3881

39-
// Maps a file path to a source map for that file
40-
var sourceMapCache = {};
82+
// Priority list of internally-implemented handlers.
83+
// When resetting state, we must keep these.
84+
internalRetrieveFileHandlers: [],
85+
internalRetrieveMapHandlers: [],
86+
87+
});
88+
89+
// Supports {browser, node, auto}
90+
var environment = "auto";
4191

4292
// Regex for detecting source maps
4393
var reSourceMap = /^data:application\/json[^,]+base64,/;
4494

45-
// Priority list of retrieve handlers
46-
var retrieveFileHandlers = [];
47-
var retrieveMapHandlers = [];
48-
4995
function isInBrowser() {
5096
if (environment === "browser")
5197
return true;
@@ -58,21 +104,27 @@ function hasGlobalProcessEventEmitter() {
58104
return ((typeof process === 'object') && (process !== null) && (typeof process.on === 'function'));
59105
}
60106

61-
function handlerExec(list) {
107+
function handlerExec(list, internalList) {
62108
return function(arg) {
63109
for (var i = 0; i < list.length; i++) {
64110
var ret = list[i](arg);
65111
if (ret) {
66112
return ret;
67113
}
68114
}
115+
for (var i = 0; i < internalList.length; i++) {
116+
var ret = internalList[i](arg);
117+
if (ret) {
118+
return ret;
119+
}
120+
}
69121
return null;
70122
};
71123
}
72124

73-
var retrieveFile = handlerExec(retrieveFileHandlers);
125+
var retrieveFile = handlerExec(sharedData.retrieveFileHandlers, sharedData.internalRetrieveFileHandlers);
74126

75-
retrieveFileHandlers.push(function(path) {
127+
sharedData.internalRetrieveFileHandlers.push(function(path) {
76128
// Trim the path to make sure there is no extra whitespace.
77129
path = path.trim();
78130
if (/^file:/.test(path)) {
@@ -83,8 +135,8 @@ retrieveFileHandlers.push(function(path) {
83135
'/'; // file:///root-dir/file -> /root-dir/file
84136
});
85137
}
86-
if (path in fileContentsCache) {
87-
return fileContentsCache[path];
138+
if (path in sharedData.fileContentsCache) {
139+
return sharedData.fileContentsCache[path];
88140
}
89141

90142
var contents = '';
@@ -105,7 +157,7 @@ retrieveFileHandlers.push(function(path) {
105157
/* ignore any errors */
106158
}
107159

108-
return fileContentsCache[path] = contents;
160+
return sharedData.fileContentsCache[path] = contents;
109161
});
110162

111163
// Support URLs relative to a directory, but be careful about a protocol prefix
@@ -160,8 +212,8 @@ function retrieveSourceMapURL(source) {
160212
// there is no source map. The map field may be either a string or the parsed
161213
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
162214
// constructor).
163-
var retrieveSourceMap = handlerExec(retrieveMapHandlers);
164-
retrieveMapHandlers.push(function(source) {
215+
var retrieveSourceMap = handlerExec(sharedData.retrieveMapHandlers, sharedData.internalRetrieveMapHandlers);
216+
sharedData.internalRetrieveMapHandlers.push(function(source) {
165217
var sourceMappingURL = retrieveSourceMapURL(source);
166218
if (!sourceMappingURL) return null;
167219

@@ -189,12 +241,12 @@ retrieveMapHandlers.push(function(source) {
189241
});
190242

191243
function mapSourcePosition(position) {
192-
var sourceMap = sourceMapCache[position.source];
244+
var sourceMap = sharedData.sourceMapCache[position.source];
193245
if (!sourceMap) {
194246
// Call the (overrideable) retrieveSourceMap function to get the source map.
195247
var urlAndMap = retrieveSourceMap(position.source);
196248
if (urlAndMap) {
197-
sourceMap = sourceMapCache[position.source] = {
249+
sourceMap = sharedData.sourceMapCache[position.source] = {
198250
url: urlAndMap.url,
199251
map: new SourceMapConsumer(urlAndMap.map)
200252
};
@@ -206,12 +258,12 @@ function mapSourcePosition(position) {
206258
var contents = sourceMap.map.sourcesContent[i];
207259
if (contents) {
208260
var url = supportRelativeURL(sourceMap.url, source);
209-
fileContentsCache[url] = contents;
261+
sharedData.fileContentsCache[url] = contents;
210262
}
211263
});
212264
}
213265
} else {
214-
sourceMap = sourceMapCache[position.source] = {
266+
sourceMap = sharedData.sourceMapCache[position.source] = {
215267
url: null,
216268
map: null
217269
};
@@ -434,9 +486,9 @@ const ErrorPrototypeToString = (err) =>Error.prototype.toString.call(err);
434486
// This function is part of the V8 stack trace API, for more info see:
435487
// https://v8.dev/docs/stack-trace-api
436488
function prepareStackTrace(error, stack) {
437-
if (emptyCacheBetweenOperations) {
438-
fileContentsCache = {};
439-
sourceMapCache = {};
489+
if (sharedData.emptyCacheBetweenOperations) {
490+
sharedData.fileContentsCache = {};
491+
sharedData.sourceMapCache = {};
440492
}
441493

442494
// node gives its own errors special treatment. Mimic that behavior
@@ -474,7 +526,7 @@ function getErrorSource(error) {
474526
var column = +match[3];
475527

476528
// Support the inline sourceContents inside the source map
477-
var contents = fileContentsCache[source];
529+
var contents = sharedData.fileContentsCache[source];
478530

479531
// Support files on disk
480532
if (!contents && fs && fs.existsSync(source)) {
@@ -537,8 +589,8 @@ function shimEmitUncaughtException () {
537589
};
538590
}
539591

540-
var originalRetrieveFileHandlers = retrieveFileHandlers.slice(0);
541-
var originalRetrieveMapHandlers = retrieveMapHandlers.slice(0);
592+
var originalRetrieveFileHandlers = sharedData.retrieveFileHandlers.slice(0);
593+
var originalRetrieveMapHandlers = sharedData.retrieveMapHandlers.slice(0);
542594

543595
exports.wrapCallSite = wrapCallSite;
544596
exports.getErrorSource = getErrorSource;
@@ -559,20 +611,20 @@ exports.install = function(options) {
559611
// directly from disk.
560612
if (options.retrieveFile) {
561613
if (options.overrideRetrieveFile) {
562-
retrieveFileHandlers.length = 0;
614+
sharedData.retrieveFileHandlers.length = 0;
563615
}
564616

565-
retrieveFileHandlers.unshift(options.retrieveFile);
617+
sharedData.retrieveFileHandlers.unshift(options.retrieveFile);
566618
}
567619

568620
// Allow source maps to be found by methods other than reading the files
569621
// directly from disk.
570622
if (options.retrieveSourceMap) {
571623
if (options.overrideRetrieveSourceMap) {
572-
retrieveMapHandlers.length = 0;
624+
sharedData.retrieveMapHandlers.length = 0;
573625
}
574626

575-
retrieveMapHandlers.unshift(options.retrieveSourceMap);
627+
sharedData.retrieveMapHandlers.unshift(options.retrieveSourceMap);
576628
}
577629

578630
// Support runtime transpilers that include inline source maps
@@ -583,8 +635,8 @@ exports.install = function(options) {
583635

584636
if (!$compile.__sourceMapSupport) {
585637
Module.prototype._compile = function(content, filename) {
586-
fileContentsCache[filename] = content;
587-
sourceMapCache[filename] = undefined;
638+
sharedData.fileContentsCache[filename] = content;
639+
sharedData.sourceMapCache[filename] = undefined;
588640
return $compile.call(this, content, filename);
589641
};
590642

@@ -593,18 +645,18 @@ exports.install = function(options) {
593645
}
594646

595647
// Configure options
596-
if (!emptyCacheBetweenOperations) {
597-
emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
648+
if (!sharedData.emptyCacheBetweenOperations) {
649+
sharedData.emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
598650
options.emptyCacheBetweenOperations : false;
599651
}
600652

601653
// Install the error reformatter
602-
if (!errorFormatterInstalled) {
603-
errorFormatterInstalled = true;
654+
if (!sharedData.errorFormatterInstalled) {
655+
sharedData.errorFormatterInstalled = true;
604656
Error.prepareStackTrace = prepareStackTrace;
605657
}
606658

607-
if (!uncaughtShimInstalled) {
659+
if (!sharedData.uncaughtShimInstalled) {
608660
var installHandler = 'handleUncaughtExceptions' in options ?
609661
options.handleUncaughtExceptions : true;
610662

@@ -627,19 +679,13 @@ exports.install = function(options) {
627679
// generated JavaScript code will be shown above the stack trace instead of
628680
// the original source code.
629681
if (installHandler && hasGlobalProcessEventEmitter()) {
630-
uncaughtShimInstalled = true;
682+
sharedData.uncaughtShimInstalled = true;
631683
shimEmitUncaughtException();
632684
}
633685
}
634686
};
635687

636688
exports.resetRetrieveHandlers = function() {
637-
retrieveFileHandlers.length = 0;
638-
retrieveMapHandlers.length = 0;
639-
640-
retrieveFileHandlers = originalRetrieveFileHandlers.slice(0);
641-
retrieveMapHandlers = originalRetrieveMapHandlers.slice(0);
642-
643-
retrieveSourceMap = handlerExec(retrieveMapHandlers);
644-
retrieveFile = handlerExec(retrieveFileHandlers);
689+
sharedData.retrieveFileHandlers.length = 0;
690+
sharedData.retrieveMapHandlers.length = 0;
645691
}

test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,3 +638,45 @@ it('normal console.trace', function(done) {
638638
/^ at Object\.<anonymous> \((?:.*[/\\])?line2\.js:1002:102\)$/
639639
]);
640640
});
641+
642+
it('supports multiple instances', function(done) {
643+
function finish(err) {
644+
fs.unlinkSync('.original2.js');
645+
fs.unlinkSync('.generated2.js');
646+
fs.unlinkSync('.generated2.js.map.extra')
647+
done(err);
648+
}
649+
var sourceMap = createEmptySourceMap();
650+
sourceMap.addMapping({
651+
generated: { line: 1, column: 0 },
652+
original: { line: 1, column: 0 },
653+
source: '.original2.js'
654+
});
655+
fs.writeFileSync('.generated2.js.map.extra', sourceMap.toString());
656+
fs.writeFileSync('.generated2.js', [
657+
'module.exports = function foo() { throw new Error("this is the error"); }',
658+
'//@ sourceMappingURL=.generated2.js.map'
659+
].join('\n'));
660+
fs.writeFileSync('.original2.js', 'this is some other original code');
661+
compareStdout(finish, createEmptySourceMap(), [
662+
'require("./source-map-support").install({',
663+
' retrieveFile: function(path) {',
664+
' var fs = require("fs");',
665+
' if (fs.existsSync(path + ".extra")) {',
666+
' return fs.readFileSync(path + ".extra", "utf8");',
667+
' }',
668+
' }',
669+
'});',
670+
'var foo = require("./.generated2.js");',
671+
'delete require.cache[require.resolve("./source-map-support")];',
672+
'require("./source-map-support").install();',
673+
'process.nextTick(foo);',
674+
'process.nextTick(function() { process.exit(1); });'
675+
], [
676+
/[/\\].original2\.js:1$/,
677+
'this is some other original code',
678+
'^',
679+
'Error: this is the error',
680+
/^ at foo \((?:.*[/\\])?.original2\.js:1:1\)$/
681+
]);
682+
});

0 commit comments

Comments
 (0)