diff --git a/source-map-support.d.ts b/source-map-support.d.ts index d8cb9d8..21c1055 100755 --- a/source-map-support.d.ts +++ b/source-map-support.d.ts @@ -50,6 +50,13 @@ export interface Options { * @param options options object internally passed to node's `_resolveFilename` hook */ onConflictingLibraryRedirect?: (request: string, parent: any, isMain: boolean, options: any, redirectedRequest: string) => void; + + /** + * If enabled, will fallback to getting mapped function name from callsite (from the following stack frame) when neither + * enclosing position nor runtime have a function name. + * Default: true + */ + callsiteFallback?: boolean; } export interface Position { diff --git a/source-map-support.js b/source-map-support.js index ad830b6..46f91e0 100644 --- a/source-map-support.js +++ b/source-map-support.js @@ -92,6 +92,10 @@ var sharedData = initializeSharedData({ // If true, the caches are reset before a stack trace formatting operation emptyCacheBetweenOperations: false, + // If true, will fallback to getting mapped function name from callsite (from the following stack frame) when neither + // enclosing position nor runtime have a function name. + callsiteFallback: true, + // Maps a file path to a string containing the file contents fileContentsCache: Object.create(null), @@ -554,6 +558,14 @@ function cloneCallSite(frame) { return object; } +// Fix position in Node where some (internal) code is prepended. +// See https://github.com/evanw/node-source-map-support/issues/36 +// Header removed in node at ^10.16 || >=11.11.0 +// v11 is not an LTS candidate, we can just test the one version with it. +// Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11 +const noHeader = /^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/; +const headerLength = isInBrowser() ? 0 : (noHeader.test(process.version) ? 0 : 62); + function wrapCallSite(frame, state) { // provides interface backward compatibility if (state === undefined) { @@ -577,31 +589,45 @@ function wrapCallSite(frame, state) { var line = frame.getLineNumber(); var column = frame.getColumnNumber() - 1; - - // Fix position in Node where some (internal) code is prepended. - // See https://github.com/evanw/node-source-map-support/issues/36 - // Header removed in node at ^10.16 || >=11.11.0 - // v11 is not an LTS candidate, we can just test the one version with it. - // Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11 - var noHeader = /^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/; - var headerLength = noHeader.test(process.version) ? 0 : 62; - if (line === 1 && column > headerLength && !isInBrowser() && !frame.isEval()) { + if (line === 1 && column > headerLength && !frame.isEval()) { column -= headerLength; } - var position = mapSourcePosition({ source: source, line: line, column: column }); + + let enclosingName = null; + // Was added in node 14.17 + if(frame.getEnclosingLineNumber) { + const enclosingLine = frame.getEnclosingLineNumber(); + const enclosingColumn = frame.getEnclosingColumnNumber() - 1; + const enclosingPosition = mapSourcePosition({ + source: source, + line: enclosingLine, + column: enclosingColumn + }); + if(enclosingPosition) enclosingName = enclosingPosition.name; + } + state.curPosition = position; + const nextPosition = state.nextPosition; + frame = cloneCallSite(frame); + + // Check for function name in this order: + // enclosing position, then runtime function name, finally fallback to callsite (subsequent stack frame's position) var originalFunctionName = frame.getFunctionName; frame.getFunctionName = function() { - if (state.nextPosition == null) { - return originalFunctionName(); + if(enclosingName != null) return enclosingName; + const originalName = originalFunctionName(); + if(originalName != null) return originalName; + if (sharedData.callsiteFallback && nextPosition != null) { + const nextPositionName = nextPosition.name; + if(nextPositionName != null) return nextPositionName; } - return state.nextPosition.name || originalFunctionName(); + return null; }; frame.getFileName = function() { return position.source; }; frame.getLineNumber = function() { return position.line; }; @@ -895,6 +921,10 @@ exports.install = function(options) { shimEmitUncaughtException(); } } + + if (typeof options.callsiteFallback === 'boolean') { + sharedData.callsiteFallback = options.callsiteFallback; + } }; exports.uninstall = function() { diff --git a/test.js b/test.js index 7dabc6c..6aef456 100644 --- a/test.js +++ b/test.js @@ -597,26 +597,43 @@ it('finds the last sourceMappingURL', async function() { it('maps original name from source', async function() { var sourceMap = createEmptySourceMap(); + + // Note: related to discussion here: https://github.com/facebook/jest/pull/12786#issuecomment-1116380918 + // Mapping is at start of the `function` keyword because that is the "enclosing" position of the first stack frame. + // This is technically wrong; enclosing names should not use the sourcemap "names"; they should use the new type of + // "names" mapping being proposed here: https://github.com/source-map/source-map-rfc/issues/12 + + // Position of `function foo` + sourceMap.addMapping({ + generated: { line: 2, column: 0 }, + original: { line: 1002, column: 1 }, + source: `.original-${id}.js`, + name: "myOriginalName" + }); + // Position of `new Error` sourceMap.addMapping({ - generated: { line: 2, column: 8 }, + generated: { line: 3, column: 8 }, original: { line: 1000, column: 10 }, source: `.original-${id}.js`, }); + // Position of `foo();` sourceMap.addMapping({ - generated: { line: 4, column: 0 }, - original: { line: 1002, column: 1 }, + generated: { line: 5, column: 0 }, + original: { line: 1003, column: 1 }, source: `.original-${id}.js`, - name: "myOriginalName" + name: "myOriginalCallsiteName" }); + const expectedFunctionName = semver.gte(process.version, '14.17.0') ? 'myOriginalName' : 'foo'; await compareStackTrace(sourceMap, [ + '', 'function foo() {', ' throw new Error("test");', '}', 'foo();' ], [ 'Error: test', - re`^ at myOriginalName \(${stackFramePathStartsWith()}(?:.*[/\\])?\.original-${id}.js:1000:11\)$`, - re`^ at ${stackFrameAtTest()} \(${stackFramePathStartsWith()}(?:.*[/\\])?\.original-${id}.js:1002:2\)$` + re`^ at ${expectedFunctionName} \(${stackFramePathStartsWith()}(?:.*[/\\])?\.original-${id}.js:1000:11\)$`, + re`^ at ${stackFrameAtTest()} \(${stackFramePathStartsWith()}(?:.*[/\\])?\.original-${id}.js:1003:2\)$` ]); });