Skip to content

Commit 8ab8b7a

Browse files
committed
Merge remote-tracking branch 'origin/master' into ej/sourceMapSupportRedirect
2 parents e865d19 + a1abb8c commit 8ab8b7a

File tree

3 files changed

+204
-51
lines changed

3 files changed

+204
-51
lines changed

source-map-support.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ export function resetRetrieveHandlers(): void;
6060
* @param options Can be used to e.g. disable uncaughtException handler.
6161
*/
6262
export function install(options?: Options): void;
63+
64+
/**
65+
* Uninstall SourceMap support.
66+
*/
67+
export function uninstall(): void;

source-map-support.js

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ function dynamicRequire(mod, request) {
2323
return mod.require(request);
2424
}
2525

26+
/**
27+
* @typedef {{
28+
* enabled: boolean;
29+
* originalValue: any;
30+
* installedValue: any;
31+
* }} HookState
32+
* Used for installing and uninstalling hooks
33+
*/
34+
2635
// Increment this if the format of sharedData changes in a breaking way.
2736
var sharedDataVersion = 1;
2837

@@ -63,8 +72,11 @@ function initializeSharedData(defaults) {
6372
var sharedData = initializeSharedData({
6473

6574
// Only install once if called multiple times
66-
errorFormatterInstalled: false,
67-
uncaughtShimInstalled: false,
75+
// Remember how the environment looked before installation so we can restore if able
76+
/** @type {HookState} */
77+
errorPrepareStackTraceHook: undefined,
78+
/** @type {HookState} */
79+
processEmitHook: undefined,
6880

6981
// If true, the caches are reset before a stack trace formatting operation
7082
emptyCacheBetweenOperations: false,
@@ -483,38 +495,45 @@ try {
483495

484496
const ErrorPrototypeToString = (err) =>Error.prototype.toString.call(err);
485497

486-
// This function is part of the V8 stack trace API, for more info see:
487-
// https://v8.dev/docs/stack-trace-api
488-
function prepareStackTrace(error, stack) {
489-
if (sharedData.emptyCacheBetweenOperations) {
490-
sharedData.fileContentsCache = {};
491-
sharedData.sourceMapCache = {};
492-
}
493-
494-
// node gives its own errors special treatment. Mimic that behavior
495-
// https://github.com/nodejs/node/blob/3cbaabc4622df1b4009b9d026a1a970bdbae6e89/lib/internal/errors.js#L118-L128
496-
// https://github.com/nodejs/node/pull/39182
497-
var errorString;
498-
if (kIsNodeError) {
499-
if(kIsNodeError in error) {
500-
errorString = `${error.name} [${error.code}]: ${error.message}`;
498+
/** @param {HookState} hookState */
499+
function createPrepareStackTrace(hookState) {
500+
return prepareStackTrace;
501+
502+
// This function is part of the V8 stack trace API, for more info see:
503+
// https://v8.dev/docs/stack-trace-api
504+
function prepareStackTrace(error, stack) {
505+
if(!hookState.enabled) return hookState.originalValue.apply(this, arguments);
506+
507+
if (sharedData.emptyCacheBetweenOperations) {
508+
sharedData.fileContentsCache = {};
509+
sharedData.sourceMapCache = {};
510+
}
511+
512+
// node gives its own errors special treatment. Mimic that behavior
513+
// https://github.com/nodejs/node/blob/3cbaabc4622df1b4009b9d026a1a970bdbae6e89/lib/internal/errors.js#L118-L128
514+
// https://github.com/nodejs/node/pull/39182
515+
var errorString;
516+
if (kIsNodeError) {
517+
if(kIsNodeError in error) {
518+
errorString = `${error.name} [${error.code}]: ${error.message}`;
519+
} else {
520+
errorString = ErrorPrototypeToString(error);
521+
}
501522
} else {
502-
errorString = ErrorPrototypeToString(error);
523+
var name = error.name || 'Error';
524+
var message = error.message || '';
525+
errorString = name + ": " + message;
503526
}
504-
} else {
505-
var name = error.name || 'Error';
506-
var message = error.message || '';
507-
errorString = name + ": " + message;
508-
}
509527

510-
var state = { nextPosition: null, curPosition: null };
511-
var processedStack = [];
512-
for (var i = stack.length - 1; i >= 0; i--) {
513-
processedStack.push('\n at ' + wrapCallSite(stack[i], state));
514-
state.nextPosition = state.curPosition;
528+
var state = { nextPosition: null, curPosition: null };
529+
var processedStack = [];
530+
for (var i = stack.length - 1; i >= 0; i--) {
531+
processedStack.push('\n at ' + wrapCallSite(stack[i], state));
532+
state.nextPosition = state.curPosition;
533+
}
534+
state.curPosition = state.nextPosition = null;
535+
return errorString + processedStack.reverse().join('');
515536
}
516-
state.curPosition = state.nextPosition = null;
517-
return errorString + processedStack.reverse().join('');
518537
}
519538

520539
// Generate position and snippet of original source with pointer
@@ -571,19 +590,26 @@ function printFatalErrorUponExit (error) {
571590
}
572591

573592
function shimEmitUncaughtException () {
574-
var origEmit = process.emit;
593+
const originalValue = process.emit;
594+
var hook = sharedData.processEmitHook = {
595+
enabled: true,
596+
originalValue,
597+
installedValue: undefined
598+
};
575599
var isTerminatingDueToFatalException = false;
576600
var fatalException;
577601

578-
process.emit = function (type) {
579-
const hadListeners = origEmit.apply(this, arguments);
580-
if (type === 'uncaughtException' && !hadListeners) {
581-
isTerminatingDueToFatalException = true;
582-
fatalException = arguments[1];
583-
process.exit(1);
584-
}
585-
if (type === 'exit' && isTerminatingDueToFatalException) {
586-
printFatalErrorUponExit(fatalException);
602+
process.emit = sharedData.processEmitHook.installedValue = function (type) {
603+
const hadListeners = originalValue.apply(this, arguments);
604+
if(hook.enabled) {
605+
if (type === 'uncaughtException' && !hadListeners) {
606+
isTerminatingDueToFatalException = true;
607+
fatalException = arguments[1];
608+
process.exit(1);
609+
}
610+
if (type === 'exit' && isTerminatingDueToFatalException) {
611+
printFatalErrorUponExit(fatalException);
612+
}
587613
}
588614
return hadListeners;
589615
};
@@ -671,13 +697,19 @@ exports.install = function(options) {
671697
options.emptyCacheBetweenOperations : false;
672698
}
673699

700+
674701
// Install the error reformatter
675-
if (!sharedData.errorFormatterInstalled) {
676-
sharedData.errorFormatterInstalled = true;
677-
Error.prepareStackTrace = prepareStackTrace;
702+
if (!sharedData.errorPrepareStackTraceHook) {
703+
const originalValue = Error.prepareStackTrace;
704+
sharedData.errorPrepareStackTraceHook = {
705+
enabled: true,
706+
originalValue,
707+
installedValue: undefined
708+
};
709+
Error.prepareStackTrace = sharedData.errorPrepareStackTraceHook.installedValue = createPrepareStackTrace(sharedData.errorPrepareStackTraceHook);
678710
}
679711

680-
if (!sharedData.uncaughtShimInstalled) {
712+
if (!sharedData.processEmitHook) {
681713
var installHandler = 'handleUncaughtExceptions' in options ?
682714
options.handleUncaughtExceptions : true;
683715

@@ -700,12 +732,35 @@ exports.install = function(options) {
700732
// generated JavaScript code will be shown above the stack trace instead of
701733
// the original source code.
702734
if (installHandler && hasGlobalProcessEventEmitter()) {
703-
sharedData.uncaughtShimInstalled = true;
704735
shimEmitUncaughtException();
705736
}
706737
}
707738
};
708739

740+
exports.uninstall = function() {
741+
if(sharedData.processEmitHook) {
742+
// Disable behavior
743+
sharedData.processEmitHook.enabled = false;
744+
// If possible, remove our hook function. May not be possible if subsequent third-party hooks have wrapped around us.
745+
if(process.emit === sharedData.processEmitHook.installedValue) {
746+
process.emit = sharedData.processEmitHook.originalValue;
747+
}
748+
sharedData.processEmitHook = undefined;
749+
}
750+
if(sharedData.errorPrepareStackTraceHook) {
751+
// Disable behavior
752+
sharedData.errorPrepareStackTraceHook.enabled = false;
753+
// If possible or necessary, remove our hook function.
754+
// In vanilla environments, prepareStackTrace is `undefined`.
755+
// We cannot delegate to `undefined` the way we can to a function w/`.apply()`; our only option is to remove the function.
756+
// If we are the *first* hook installed, and another was installed on top of us, we have no choice but to remove both.
757+
if(Error.prepareStackTrace === sharedData.errorPrepareStackTraceHook.installedValue || typeof sharedData.errorPrepareStackTraceHook.originalValue !== 'function') {
758+
Error.prepareStackTrace = sharedData.errorPrepareStackTraceHook.originalValue;
759+
}
760+
sharedData.errorPrepareStackTraceHook = undefined;
761+
}
762+
}
763+
709764
exports.resetRetrieveHandlers = function() {
710765
sharedData.retrieveFileHandlers.length = 0;
711766
sharedData.retrieveMapHandlers.length = 0;

test.js

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
const underTest = require('./source-map-support');
2-
underTest.install({
3-
emptyCacheBetweenOperations: true // Needed to be able to test for failure
4-
});
1+
// Note: some tests rely on side-effects from prior tests.
2+
// You may not get meaningful results running a subset of tests.
53

4+
const priorErrorPrepareStackTrace = Error.prepareStackTrace;
5+
const priorProcessEmit = process.emit;
6+
const underTest = require('./source-map-support');
67
var SourceMapGenerator = require('source-map').SourceMapGenerator;
78
var child_process = require('child_process');
89
var assert = require('assert');
@@ -137,14 +138,35 @@ function compareStdout(done, sourceMap, source, expected) {
137138
});
138139
}
139140

141+
it('normal throw without source-map-support installed', normalThrowWithoutSourceMapSupportInstalled);
142+
140143
it('normal throw', function() {
144+
installSms();
145+
normalThrow();
146+
});
147+
148+
function installSms() {
149+
underTest.install({
150+
emptyCacheBetweenOperations: true // Needed to be able to test for failure
151+
});
152+
}
153+
154+
function normalThrow() {
141155
compareStackTrace(createMultiLineSourceMap(), [
142156
'throw new Error("test");'
143157
], [
144158
'Error: test',
145159
/^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
146160
]);
147-
});
161+
}
162+
function normalThrowWithoutSourceMapSupportInstalled() {
163+
compareStackTrace(createMultiLineSourceMap(), [
164+
'throw new Error("test");'
165+
], [
166+
'Error: test',
167+
/^ at Object\.exports\.test \((?:.*[/\\])?\.generated\.js:1:34\)$/
168+
]);
169+
}
148170

149171
/* The following test duplicates some of the code in
150172
* `normal throw` but triggers file read failure.
@@ -713,4 +735,75 @@ describe('redirects require() of "source-map-support" to this module', function(
713735
assert.strictEqual(redirectedRequest, require.resolve('.'));
714736
}
715737
});
716-
});
738+
});
739+
740+
describe('uninstall', function() {
741+
this.beforeEach(function() {
742+
underTest.uninstall();
743+
process.emit = priorProcessEmit;
744+
Error.prepareStackTrace = priorErrorPrepareStackTrace;
745+
});
746+
747+
it('uninstall removes hooks and source-mapping behavior', function() {
748+
assert.strictEqual(Error.prepareStackTrace, priorErrorPrepareStackTrace);
749+
assert.strictEqual(process.emit, priorProcessEmit);
750+
normalThrowWithoutSourceMapSupportInstalled();
751+
});
752+
753+
it('install re-adds hooks', function() {
754+
installSms();
755+
normalThrow();
756+
});
757+
758+
it('uninstall removes prepareStackTrace even in presence of third-party hooks if none were installed before us', function() {
759+
installSms();
760+
const wrappedPrepareStackTrace = Error.prepareStackTrace;
761+
let pstInvocations = 0;
762+
function thirdPartyPrepareStackTraceHook() {
763+
pstInvocations++;
764+
return wrappedPrepareStackTrace.apply(this, arguments);
765+
}
766+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHook;
767+
underTest.uninstall();
768+
assert.strictEqual(Error.prepareStackTrace, undefined);
769+
assert(pstInvocations === 0);
770+
});
771+
772+
it('uninstall preserves third-party prepareStackTrace hooks if one was installed before us', function() {
773+
let beforeInvocations = 0;
774+
function thirdPartyPrepareStackTraceHookInstalledBefore() {
775+
beforeInvocations++;
776+
return 'foo';
777+
}
778+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHookInstalledBefore;
779+
installSms();
780+
const wrappedPrepareStackTrace = Error.prepareStackTrace;
781+
let afterInvocations = 0;
782+
function thirdPartyPrepareStackTraceHookInstalledAfter() {
783+
afterInvocations++;
784+
return wrappedPrepareStackTrace.apply(this, arguments);
785+
}
786+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHookInstalledAfter;
787+
underTest.uninstall();
788+
assert.strictEqual(Error.prepareStackTrace, thirdPartyPrepareStackTraceHookInstalledAfter);
789+
assert.strictEqual(new Error().stack, 'foo');
790+
assert.strictEqual(beforeInvocations, 1);
791+
assert.strictEqual(afterInvocations, 1);
792+
});
793+
794+
it('uninstall preserves third-party process.emit hooks installed after us', function() {
795+
installSms();
796+
const wrappedProcessEmit = process.emit;
797+
let peInvocations = 0;
798+
function thirdPartyProcessEmit() {
799+
peInvocations++;
800+
return wrappedProcessEmit.apply(this, arguments);
801+
}
802+
process.emit = thirdPartyProcessEmit;
803+
underTest.uninstall();
804+
assert.strictEqual(process.emit, thirdPartyProcessEmit);
805+
normalThrowWithoutSourceMapSupportInstalled();
806+
process.emit('foo');
807+
assert(peInvocations >= 1);
808+
});
809+
});

0 commit comments

Comments
 (0)