Skip to content

Commit df935be

Browse files
committed
Support multiple instances of source-map-support
This covers the use cases that b40cc25 doesn't; namely, if several libraries reference different versions of source-map-support, then they would previously all get their own source map caches, handler arrays, etc., and not work with each other. This commit moves such data from module-local variables into a versioned global object, enabling multiple instances of source-map-support to cooperate.
1 parent d173d90 commit df935be

File tree

2 files changed

+125
-42
lines changed

2 files changed

+125
-42
lines changed

source-map-support.js

Lines changed: 83 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,70 @@ try {
1212
/* nop */
1313
}
1414

15-
// Only install once if called multiple times
16-
var errorFormatterInstalled = false;
17-
var uncaughtShimInstalled = false;
15+
// Increment this if the format of sharedData changes in a breaking way.
16+
var sharedDataVersion = 1;
1817

19-
// If true, the caches are reset before a stack trace formatting operation
20-
var emptyCacheBetweenOperations = false;
18+
function initializeSharedData(defaults) {
19+
var sharedDataKey = 'source-map-support/sharedData';
20+
if (typeof Symbol !== 'undefined') {
21+
sharedDataKey = Symbol.for(sharedDataKey);
22+
}
23+
var sharedData = this[sharedDataKey];
24+
if (!sharedData) {
25+
sharedData = { version: sharedDataVersion };
26+
if (Object.defineProperty) {
27+
Object.defineProperty(this, sharedDataKey, { value: sharedData });
28+
} else {
29+
this[sharedDataKey] = sharedData;
30+
}
31+
}
32+
if (sharedDataVersion !== sharedData.version) {
33+
throw new Error("Multiple incompatible instances of source-map-support were loaded");
34+
}
35+
for (var key in defaults) {
36+
if (!(key in sharedData)) {
37+
sharedData[key] = defaults[key];
38+
}
39+
}
40+
return sharedData;
41+
}
2142

22-
// Supports {browser, node, auto}
23-
var environment = "auto";
43+
// If multiple instances of source-map-support are loaded into the same
44+
// context, they shouldn't overwrite each other. By storing handlers, caches,
45+
// and other state on a shared object, different instances of
46+
// source-map-support can work together in a limited way. This does require
47+
// that future versions of source-map-support continue to support the fields on
48+
// this object. If this internal contract ever needs to be broken, increment
49+
// sharedDataVersion. (This version number is not the same as any of the
50+
// package's version numbers, which should reflect the *external* API of
51+
// source-map-support.)
52+
var sharedData = initializeSharedData({
2453

25-
// Maps a file path to a string containing the file contents
26-
var fileContentsCache = {};
54+
// Only install once if called multiple times
55+
errorFormatterInstalled: false,
56+
uncaughtShimInstalled: false,
2757

28-
// Maps a file path to a source map for that file
29-
var sourceMapCache = {};
58+
// If true, the caches are reset before a stack trace formatting operation
59+
emptyCacheBetweenOperations: false,
60+
61+
// Maps a file path to a string containing the file contents
62+
fileContentsCache: {},
63+
64+
// Maps a file path to a source map for that file
65+
sourceMapCache: {},
66+
67+
// Priority list of retrieve handlers
68+
retrieveFileHandlers: [],
69+
retrieveMapHandlers: [],
70+
71+
});
72+
73+
// Supports {browser, node, auto}
74+
var environment = "auto";
3075

3176
// Regex for detecting source maps
3277
var reSourceMap = /^data:application\/json[^,]+base64,/;
3378

34-
// Priority list of retrieve handlers
35-
var retrieveFileHandlers = [];
36-
var retrieveMapHandlers = [];
37-
3879
function isInBrowser() {
3980
if (environment === "browser")
4081
return true;
@@ -59,9 +100,9 @@ function handlerExec(list) {
59100
};
60101
}
61102

62-
var retrieveFile = handlerExec(retrieveFileHandlers);
103+
var retrieveFile = handlerExec(sharedData.retrieveFileHandlers);
63104

64-
retrieveFileHandlers.push(function(path) {
105+
sharedData.retrieveFileHandlers.push(function(path) {
65106
// Trim the path to make sure there is no extra whitespace.
66107
path = path.trim();
67108
if (/^file:/.test(path)) {
@@ -72,8 +113,8 @@ retrieveFileHandlers.push(function(path) {
72113
'/'; // file:///root-dir/file -> /root-dir/file
73114
});
74115
}
75-
if (path in fileContentsCache) {
76-
return fileContentsCache[path];
116+
if (path in sharedData.fileContentsCache) {
117+
return sharedData.fileContentsCache[path];
77118
}
78119

79120
var contents = null;
@@ -95,7 +136,7 @@ retrieveFileHandlers.push(function(path) {
95136
}
96137
}
97138

98-
return fileContentsCache[path] = contents;
139+
return sharedData.fileContentsCache[path] = contents;
99140
});
100141

101142
// Support URLs relative to a directory, but be careful about a protocol prefix
@@ -150,8 +191,8 @@ function retrieveSourceMapURL(source) {
150191
// there is no source map. The map field may be either a string or the parsed
151192
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
152193
// constructor).
153-
var retrieveSourceMap = handlerExec(retrieveMapHandlers);
154-
retrieveMapHandlers.push(function(source) {
194+
var retrieveSourceMap = handlerExec(sharedData.retrieveMapHandlers);
195+
sharedData.retrieveMapHandlers.push(function(source) {
155196
var sourceMappingURL = retrieveSourceMapURL(source);
156197
if (!sourceMappingURL) return null;
157198

@@ -179,12 +220,12 @@ retrieveMapHandlers.push(function(source) {
179220
});
180221

181222
function mapSourcePosition(position) {
182-
var sourceMap = sourceMapCache[position.source];
223+
var sourceMap = sharedData.sourceMapCache[position.source];
183224
if (!sourceMap) {
184225
// Call the (overrideable) retrieveSourceMap function to get the source map.
185226
var urlAndMap = retrieveSourceMap(position.source);
186227
if (urlAndMap) {
187-
sourceMap = sourceMapCache[position.source] = {
228+
sourceMap = sharedData.sourceMapCache[position.source] = {
188229
url: urlAndMap.url,
189230
map: new SourceMapConsumer(urlAndMap.map)
190231
};
@@ -196,12 +237,12 @@ function mapSourcePosition(position) {
196237
var contents = sourceMap.map.sourcesContent[i];
197238
if (contents) {
198239
var url = supportRelativeURL(sourceMap.url, source);
199-
fileContentsCache[url] = contents;
240+
sharedData.fileContentsCache[url] = contents;
200241
}
201242
});
202243
}
203244
} else {
204-
sourceMap = sourceMapCache[position.source] = {
245+
sourceMap = sharedData.sourceMapCache[position.source] = {
205246
url: null,
206247
map: null
207248
};
@@ -383,9 +424,9 @@ function wrapCallSite(frame) {
383424
// This function is part of the V8 stack trace API, for more info see:
384425
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
385426
function prepareStackTrace(error, stack) {
386-
if (emptyCacheBetweenOperations) {
387-
fileContentsCache = {};
388-
sourceMapCache = {};
427+
if (sharedData.emptyCacheBetweenOperations) {
428+
sharedData.fileContentsCache = {};
429+
sharedData.sourceMapCache = {};
389430
}
390431

391432
return error + stack.map(function(frame) {
@@ -402,7 +443,7 @@ function getErrorSource(error) {
402443
var column = +match[3];
403444

404445
// Support the inline sourceContents inside the source map
405-
var contents = fileContentsCache[source];
446+
var contents = sharedData.fileContentsCache[source];
406447

407448
// Support files on disk
408449
if (!contents && fs && fs.existsSync(source)) {
@@ -472,20 +513,20 @@ exports.install = function(options) {
472513
// directly from disk.
473514
if (options.retrieveFile) {
474515
if (options.overrideRetrieveFile) {
475-
retrieveFileHandlers.length = 0;
516+
sharedData.retrieveFileHandlers.length = 0;
476517
}
477518

478-
retrieveFileHandlers.unshift(options.retrieveFile);
519+
sharedData.retrieveFileHandlers.unshift(options.retrieveFile);
479520
}
480521

481522
// Allow source maps to be found by methods other than reading the files
482523
// directly from disk.
483524
if (options.retrieveSourceMap) {
484525
if (options.overrideRetrieveSourceMap) {
485-
retrieveMapHandlers.length = 0;
526+
sharedData.retrieveMapHandlers.length = 0;
486527
}
487528

488-
retrieveMapHandlers.unshift(options.retrieveSourceMap);
529+
sharedData.retrieveMapHandlers.unshift(options.retrieveSourceMap);
489530
}
490531

491532
// Support runtime transpilers that include inline source maps
@@ -500,8 +541,8 @@ exports.install = function(options) {
500541

501542
if (!$compile.__sourceMapSupport) {
502543
Module.prototype._compile = function(content, filename) {
503-
fileContentsCache[filename] = content;
504-
sourceMapCache[filename] = undefined;
544+
sharedData.fileContentsCache[filename] = content;
545+
sharedData.sourceMapCache[filename] = undefined;
505546
return $compile.call(this, content, filename);
506547
};
507548

@@ -510,18 +551,18 @@ exports.install = function(options) {
510551
}
511552

512553
// Configure options
513-
if (!emptyCacheBetweenOperations) {
514-
emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
554+
if (!sharedData.emptyCacheBetweenOperations) {
555+
sharedData.emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
515556
options.emptyCacheBetweenOperations : false;
516557
}
517558

518559
// Install the error reformatter
519-
if (!errorFormatterInstalled) {
520-
errorFormatterInstalled = true;
560+
if (!sharedData.errorFormatterInstalled) {
561+
sharedData.errorFormatterInstalled = true;
521562
Error.prepareStackTrace = prepareStackTrace;
522563
}
523564

524-
if (!uncaughtShimInstalled) {
565+
if (!sharedData.uncaughtShimInstalled) {
525566
var installHandler = 'handleUncaughtExceptions' in options ?
526567
options.handleUncaughtExceptions : true;
527568

@@ -533,7 +574,7 @@ exports.install = function(options) {
533574
// generated JavaScript code will be shown above the stack trace instead of
534575
// the original source code.
535576
if (installHandler && hasGlobalProcessEventEmitter()) {
536-
uncaughtShimInstalled = true;
577+
sharedData.uncaughtShimInstalled = true;
537578
shimEmitUncaughtException();
538579
}
539580
}

test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,3 +601,45 @@ it('handleUncaughtExceptions is true with existing listener', function(done) {
601601
done();
602602
});
603603
});
604+
605+
it('supports multiple instances', function(done) {
606+
function finish(err) {
607+
fs.unlinkSync('.original2.js');
608+
fs.unlinkSync('.generated2.js');
609+
fs.unlinkSync('.generated2.js.map.extra')
610+
done(err);
611+
}
612+
var sourceMap = createEmptySourceMap();
613+
sourceMap.addMapping({
614+
generated: { line: 1, column: 0 },
615+
original: { line: 1, column: 0 },
616+
source: '.original2.js'
617+
});
618+
fs.writeFileSync('.generated2.js.map.extra', sourceMap);
619+
fs.writeFileSync('.generated2.js', [
620+
'module.exports = function foo() { throw new Error("this is the error"); }',
621+
'//@ sourceMappingURL=.generated2.js.map'
622+
].join('\n'));
623+
fs.writeFileSync('.original2.js', 'this is some other original code');
624+
compareStdout(finish, createEmptySourceMap(), [
625+
'require("./source-map-support").install({',
626+
' retrieveFile: function(path) {',
627+
' var fs = require("fs");',
628+
' if (fs.existsSync(path + ".extra")) {',
629+
' return fs.readFileSync(path + ".extra", "utf8");',
630+
' }',
631+
' }',
632+
'});',
633+
'var foo = require("./.generated2.js");',
634+
'delete require.cache[require.resolve("./source-map-support")];',
635+
'require("./source-map-support").install();',
636+
'process.nextTick(foo);',
637+
'process.nextTick(function() { process.exit(1); });'
638+
], [
639+
/[/\\].original2\.js:1$/,
640+
'this is some other original code',
641+
'^',
642+
'Error: this is the error',
643+
/^ at foo \((?:.*[/\\])?.original2\.js:1:1\)$/
644+
]);
645+
});

0 commit comments

Comments
 (0)