diff --git a/.gitignore b/.gitignore index 144ec33..02683a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea node_modules *.log +package-lock.json +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc51f25 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Unreleased + +- fix: handle case where Error class is wrapped #10 diff --git a/index.js b/index.js index 7f1ecc9..0f82424 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ 'use strict'; +var assert = require('assert'); /** @@ -8,7 +9,7 @@ * @see https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi */ module.exports = function (depth) { - var pst, stack, file, frame; + var pst, stack, file, frame, startIdx; pst = Error.prepareStackTrace; Error.prepareStackTrace = function (_, stack) { @@ -17,8 +18,13 @@ module.exports = function (depth) { }; stack = (new Error()).stack; + // Handle case where error object is wrapped by say babel. Try to find current file's index first. + startIdx = 0; + while(startIdx < stack.length && stack[startIdx].getFileName() !== __filename) startIdx++; + assert(startIdx < stack.length, 'Unexpected: unable to find caller/index.js in the stack'); + depth = !depth || isNaN(depth) ? 1 : (depth > stack.length - 2 ? stack.length - 2 : depth); - stack = stack.slice(depth + 1); + stack = stack.slice(startIdx + depth + 1); do { frame = stack.shift(); diff --git a/test/caller.js b/test/caller.js index 9a674d5..ac79d9d 100644 --- a/test/caller.js +++ b/test/caller.js @@ -38,6 +38,21 @@ test('caller', function (t) { t.end(); }); + t.test('determine caller when Error is wrapped', function (t) { + var restoreError, actual, expected; + + restoreError = require('./fixtures/wrapped-error')(); + try { + actual = caller(); + } finally { + restoreError(); + } + expected = require.resolve('tape/lib/test'); + + t.equal(actual, expected); + t.end(); + }); + t.test('determine caller with depth cap', function (t) { var callee, actual, expected; diff --git a/test/fixtures/wrapped-error.js b/test/fixtures/wrapped-error.js new file mode 100644 index 0000000..4bc549a --- /dev/null +++ b/test/fixtures/wrapped-error.js @@ -0,0 +1,57 @@ +function copyConstructorProperties(target, source) { + const keys = Object.getOwnPropertyNames(source); + for (let i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!target.hasOwnProperty(key)) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + } + } +} +// inspired from https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/wrap-error-constructor-with-cause.js +function wrapErrorConstructor(ERROR_NAME, wrapper) { + const clearErrorStack = (function () { + const TEST = (function (arg) { return String(Error(arg).stack); })('zxcasd'); + const V8_OR_CHAKRA_STACK_ENTRY = /\n\s*at [^:]*:[^\n]*/; + const IS_V8_OR_CHAKRA_STACK = V8_OR_CHAKRA_STACK_ENTRY.test(TEST); + return function clearErrorStackInner(stack, dropEntries) { + if (IS_V8_OR_CHAKRA_STACK && typeof stack == 'string') { + while (dropEntries--) stack = stack.replace(V8_OR_CHAKRA_STACK_ENTRY, ''); + } return stack; + }; + })(); + + const OriginalError = globalThis[ERROR_NAME]; + + const OriginalErrorPrototype = OriginalError.prototype; + + const WrappedError = wrapper(function (a) { + const message = String(a); + const result = new OriginalError(); + if (message !== undefined) Object.defineProperty(result, 'message', { value: message, enumerable: false, configurable: true, writable: true }); + Object.defineProperty(result, 'stack', { value: clearErrorStack(result.stack, 2), enumerable: false, configurable: true, writable: true }); + // if (this && OriginalErrorPrototype.isPrototypeOf(this)) { + // // inheritIfRequired(result, this, WrappedError); + // Object.setPrototypeOf(result, this.constructor); //?? + // } + return result; + }); + + WrappedError.prototype = OriginalErrorPrototype; + + // Copy ownKeys from OriginalError to WrappedError + copyConstructorProperties(WrappedError, OriginalError); + + globalThis[ERROR_NAME] = WrappedError; +}; + +module.exports = function wrapError() { + const ErrorClassName = 'Error'; + const OriginalError = globalThis[ErrorClassName]; + wrapErrorConstructor(ErrorClassName, function (init) { + return function Error(message) { return init.apply(this, arguments); }; + }); + + return function restore() { + globalThis[ErrorClassName] = OriginalError; + }; +};