Skip to content

Commit a1abb8c

Browse files
authored
Merge pull request #29 from cspotcode/uninstall
Add `uninstall()` which will remove hooks from the environment
2 parents c98a759 + 50e4017 commit a1abb8c

File tree

3 files changed

+203
-49
lines changed

3 files changed

+203
-49
lines changed

source-map-support.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ export function resetRetrieveHandlers(): void;
5050
* @param options Can be used to e.g. disable uncaughtException handler.
5151
*/
5252
export function install(options?: Options): void;
53+
54+
/**
55+
* Uninstall SourceMap support.
56+
*/
57+
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
};
@@ -650,13 +676,19 @@ exports.install = function(options) {
650676
options.emptyCacheBetweenOperations : false;
651677
}
652678

679+
653680
// Install the error reformatter
654-
if (!sharedData.errorFormatterInstalled) {
655-
sharedData.errorFormatterInstalled = true;
656-
Error.prepareStackTrace = prepareStackTrace;
681+
if (!sharedData.errorPrepareStackTraceHook) {
682+
const originalValue = Error.prepareStackTrace;
683+
sharedData.errorPrepareStackTraceHook = {
684+
enabled: true,
685+
originalValue,
686+
installedValue: undefined
687+
};
688+
Error.prepareStackTrace = sharedData.errorPrepareStackTraceHook.installedValue = createPrepareStackTrace(sharedData.errorPrepareStackTraceHook);
657689
}
658690

659-
if (!sharedData.uncaughtShimInstalled) {
691+
if (!sharedData.processEmitHook) {
660692
var installHandler = 'handleUncaughtExceptions' in options ?
661693
options.handleUncaughtExceptions : true;
662694

@@ -679,12 +711,35 @@ exports.install = function(options) {
679711
// generated JavaScript code will be shown above the stack trace instead of
680712
// the original source code.
681713
if (installHandler && hasGlobalProcessEventEmitter()) {
682-
sharedData.uncaughtShimInstalled = true;
683714
shimEmitUncaughtException();
684715
}
685716
}
686717
};
687718

719+
exports.uninstall = function() {
720+
if(sharedData.processEmitHook) {
721+
// Disable behavior
722+
sharedData.processEmitHook.enabled = false;
723+
// If possible, remove our hook function. May not be possible if subsequent third-party hooks have wrapped around us.
724+
if(process.emit === sharedData.processEmitHook.installedValue) {
725+
process.emit = sharedData.processEmitHook.originalValue;
726+
}
727+
sharedData.processEmitHook = undefined;
728+
}
729+
if(sharedData.errorPrepareStackTraceHook) {
730+
// Disable behavior
731+
sharedData.errorPrepareStackTraceHook.enabled = false;
732+
// If possible or necessary, remove our hook function.
733+
// In vanilla environments, prepareStackTrace is `undefined`.
734+
// We cannot delegate to `undefined` the way we can to a function w/`.apply()`; our only option is to remove the function.
735+
// If we are the *first* hook installed, and another was installed on top of us, we have no choice but to remove both.
736+
if(Error.prepareStackTrace === sharedData.errorPrepareStackTraceHook.installedValue || typeof sharedData.errorPrepareStackTraceHook.originalValue !== 'function') {
737+
Error.prepareStackTrace = sharedData.errorPrepareStackTraceHook.originalValue;
738+
}
739+
sharedData.errorPrepareStackTraceHook = undefined;
740+
}
741+
}
742+
688743
exports.resetRetrieveHandlers = function() {
689744
sharedData.retrieveFileHandlers.length = 0;
690745
sharedData.retrieveMapHandlers.length = 0;

test.js

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

4+
const priorErrorPrepareStackTrace = Error.prepareStackTrace;
5+
const priorProcessEmit = process.emit;
6+
const underTest = require('./source-map-support');
57
var SourceMapGenerator = require('source-map').SourceMapGenerator;
68
var child_process = require('child_process');
79
var assert = require('assert');
@@ -136,14 +138,35 @@ function compareStdout(done, sourceMap, source, expected) {
136138
});
137139
}
138140

141+
it('normal throw without source-map-support installed', normalThrowWithoutSourceMapSupportInstalled);
142+
139143
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() {
140155
compareStackTrace(createMultiLineSourceMap(), [
141156
'throw new Error("test");'
142157
], [
143158
'Error: test',
144159
/^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
145160
]);
146-
});
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+
}
147170

148171
/* The following test duplicates some of the code in
149172
* `normal throw` but triggers file read failure.
@@ -680,3 +703,74 @@ it('supports multiple instances', function(done) {
680703
/^ at foo \((?:.*[/\\])?.original2\.js:1:1\)$/
681704
]);
682705
});
706+
707+
describe('uninstall', function() {
708+
this.beforeEach(function() {
709+
underTest.uninstall();
710+
process.emit = priorProcessEmit;
711+
Error.prepareStackTrace = priorErrorPrepareStackTrace;
712+
});
713+
714+
it('uninstall removes hooks and source-mapping behavior', function() {
715+
assert.strictEqual(Error.prepareStackTrace, priorErrorPrepareStackTrace);
716+
assert.strictEqual(process.emit, priorProcessEmit);
717+
normalThrowWithoutSourceMapSupportInstalled();
718+
});
719+
720+
it('install re-adds hooks', function() {
721+
installSms();
722+
normalThrow();
723+
});
724+
725+
it('uninstall removes prepareStackTrace even in presence of third-party hooks if none were installed before us', function() {
726+
installSms();
727+
const wrappedPrepareStackTrace = Error.prepareStackTrace;
728+
let pstInvocations = 0;
729+
function thirdPartyPrepareStackTraceHook() {
730+
pstInvocations++;
731+
return wrappedPrepareStackTrace.apply(this, arguments);
732+
}
733+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHook;
734+
underTest.uninstall();
735+
assert.strictEqual(Error.prepareStackTrace, undefined);
736+
assert(pstInvocations === 0);
737+
});
738+
739+
it('uninstall preserves third-party prepareStackTrace hooks if one was installed before us', function() {
740+
let beforeInvocations = 0;
741+
function thirdPartyPrepareStackTraceHookInstalledBefore() {
742+
beforeInvocations++;
743+
return 'foo';
744+
}
745+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHookInstalledBefore;
746+
installSms();
747+
const wrappedPrepareStackTrace = Error.prepareStackTrace;
748+
let afterInvocations = 0;
749+
function thirdPartyPrepareStackTraceHookInstalledAfter() {
750+
afterInvocations++;
751+
return wrappedPrepareStackTrace.apply(this, arguments);
752+
}
753+
Error.prepareStackTrace = thirdPartyPrepareStackTraceHookInstalledAfter;
754+
underTest.uninstall();
755+
assert.strictEqual(Error.prepareStackTrace, thirdPartyPrepareStackTraceHookInstalledAfter);
756+
assert.strictEqual(new Error().stack, 'foo');
757+
assert.strictEqual(beforeInvocations, 1);
758+
assert.strictEqual(afterInvocations, 1);
759+
});
760+
761+
it('uninstall preserves third-party process.emit hooks installed after us', function() {
762+
installSms();
763+
const wrappedProcessEmit = process.emit;
764+
let peInvocations = 0;
765+
function thirdPartyProcessEmit() {
766+
peInvocations++;
767+
return wrappedProcessEmit.apply(this, arguments);
768+
}
769+
process.emit = thirdPartyProcessEmit;
770+
underTest.uninstall();
771+
assert.strictEqual(process.emit, thirdPartyProcessEmit);
772+
normalThrowWithoutSourceMapSupportInstalled();
773+
process.emit('foo');
774+
assert(peInvocations >= 1);
775+
});
776+
});

0 commit comments

Comments
 (0)