@@ -24,6 +24,8 @@ const { |
24 | 24 | const { fileURLToPath } = require('internal/url'); |
25 | 25 | const { setGetSourceMapErrorSource } = internalBinding('errors'); |
26 | 26 |
|
| 27 | +const kStackLineAt = '\n at '; |
| 28 | + |
27 | 29 | // Create a prettified stacktrace, inserting context from source maps |
28 | 30 | // if possible. |
29 | 31 | function prepareStackTraceWithSourceMaps(error, trace) { |
@@ -40,75 +42,98 @@ function prepareStackTraceWithSourceMaps(error, trace) { |
40 | 42 |
|
41 | 43 | let lastSourceMap; |
42 | 44 | let lastFileName; |
43 | | - const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => { |
44 | | - const str = '\n at '; |
| 45 | + const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (callSite, i) => { |
45 | 46 | try { |
46 | 47 | // A stack trace will often have several call sites in a row within the |
47 | 48 | // same file, cache the source map and file content accordingly: |
48 | | - let fileName = t.getFileName(); |
| 49 | + let fileName = callSite.getFileName(); |
49 | 50 | if (fileName === undefined) { |
50 | | - fileName = t.getEvalOrigin(); |
| 51 | + fileName = callSite.getEvalOrigin(); |
51 | 52 | } |
52 | 53 | const sm = fileName === lastFileName ? |
53 | 54 | lastSourceMap : |
54 | 55 | findSourceMap(fileName); |
55 | 56 | lastSourceMap = sm; |
56 | 57 | lastFileName = fileName; |
57 | 58 | if (sm) { |
58 | | - // Source Map V3 lines/columns start at 0/0 whereas stack traces |
59 | | - // start at 1/1: |
60 | | - const { |
61 | | - originalLine, |
62 | | - originalColumn, |
63 | | - originalSource, |
64 | | - } = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); |
65 | | - if (originalSource && originalLine !== undefined && |
66 | | - originalColumn !== undefined) { |
67 | | - const name = getOriginalSymbolName(sm, trace, i); |
68 | | - // Construct call site name based on: v8.dev/docs/stack-trace-api: |
69 | | - const fnName = t.getFunctionName() ?? t.getMethodName(); |
70 | | - const typeName = t.getTypeName(); |
71 | | - const namePrefix = typeName !== null && typeName !== 'global' ? `${typeName}.` : ''; |
72 | | - const originalName = `${namePrefix}${fnName || '<anonymous>'}`; |
73 | | - // The original call site may have a different symbol name |
74 | | - // associated with it, use it: |
75 | | - const prefix = (name && name !== originalName) ? |
76 | | - `${name}` : |
77 | | - `${originalName}`; |
78 | | - const hasName = !!(name || originalName); |
79 | | - const originalSourceNoScheme = |
80 | | - StringPrototypeStartsWith(originalSource, 'file://') ? |
81 | | - fileURLToPath(originalSource) : originalSource; |
82 | | - // Replace the transpiled call site with the original: |
83 | | - return `${str}${prefix}${hasName ? ' (' : ''}` + |
84 | | - `${originalSourceNoScheme}:${originalLine + 1}:` + |
85 | | - `${originalColumn + 1}${hasName ? ')' : ''}`; |
86 | | - } |
| 59 | + return `${kStackLineAt}${serializeJSStackFrame(sm, callSite, trace[i + 1])}`; |
87 | 60 | } |
88 | 61 | } catch (err) { |
89 | 62 | debug(err); |
90 | 63 | } |
91 | | - return `${str}${t}`; |
| 64 | + return `${kStackLineAt}${callSite}`; |
92 | 65 | }), ''); |
93 | 66 | return `${errorString}${preparedTrace}`; |
94 | 67 | } |
95 | 68 |
|
| 69 | +/** |
| 70 | + * Serialize a single call site in the stack trace. |
| 71 | + * Refer to SerializeJSStackFrame in deps/v8/src/objects/call-site-info.cc for |
| 72 | + * more details about the default ToString(CallSite). |
| 73 | + * The CallSite API is documented at https://v8.dev/docs/stack-trace-api. |
| 74 | + * @param {import('internal/source_map/source_map').SourceMap} sm |
| 75 | + * @param {CallSite} callSite - the CallSite object to be serialized |
| 76 | + * @param {CallSite} callerCallSite - caller site info |
| 77 | + * @returns {string} - the serialized call site |
| 78 | + */ |
| 79 | +function serializeJSStackFrame(sm, callSite, callerCallSite) { |
| 80 | + // Source Map V3 lines/columns start at 0/0 whereas stack traces |
| 81 | + // start at 1/1: |
| 82 | + const { |
| 83 | + originalLine, |
| 84 | + originalColumn, |
| 85 | + originalSource, |
| 86 | + } = sm.findEntry(callSite.getLineNumber() - 1, callSite.getColumnNumber() - 1); |
| 87 | + if (originalSource === undefined || originalLine === undefined || |
| 88 | + originalColumn === undefined) { |
| 89 | + return `${callSite}`; |
| 90 | + } |
| 91 | + const name = getOriginalSymbolName(sm, callSite, callerCallSite); |
| 92 | + const originalSourceNoScheme = |
| 93 | + StringPrototypeStartsWith(originalSource, 'file://') ? |
| 94 | + fileURLToPath(originalSource) : originalSource; |
| 95 | + // Construct call site name based on: v8.dev/docs/stack-trace-api: |
| 96 | + const fnName = callSite.getFunctionName() ?? callSite.getMethodName(); |
| 97 | + |
| 98 | + let prefix = ''; |
| 99 | + if (callSite.isAsync()) { |
| 100 | + // Promise aggregation operation frame has no locations. This must be an |
| 101 | + // async stack frame. |
| 102 | + prefix = 'async '; |
| 103 | + } else if (callSite.isConstructor()) { |
| 104 | + prefix = 'new '; |
| 105 | + } |
| 106 | + |
| 107 | + const typeName = callSite.getTypeName(); |
| 108 | + const namePrefix = typeName !== null && typeName !== 'global' ? `${typeName}.` : ''; |
| 109 | + const originalName = `${namePrefix}${fnName || '<anonymous>'}`; |
| 110 | + // The original call site may have a different symbol name |
| 111 | + // associated with it, use it: |
| 112 | + const mappedName = (name && name !== originalName) ? |
| 113 | + `${name}` : |
| 114 | + `${originalName}`; |
| 115 | + const hasName = !!(name || originalName); |
| 116 | + // Replace the transpiled call site with the original: |
| 117 | + return `${prefix}${mappedName}${hasName ? ' (' : ''}` + |
| 118 | + `${originalSourceNoScheme}:${originalLine + 1}:` + |
| 119 | + `${originalColumn + 1}${hasName ? ')' : ''}`; |
| 120 | +} |
| 121 | + |
96 | 122 | // Transpilers may have removed the original symbol name used in the stack |
97 | 123 | // trace, if possible restore it from the names field of the source map: |
98 | | -function getOriginalSymbolName(sourceMap, trace, curIndex) { |
| 124 | +function getOriginalSymbolName(sourceMap, callSite, callerCallSite) { |
99 | 125 | // First check for a symbol name associated with the enclosing function: |
100 | 126 | const enclosingEntry = sourceMap.findEntry( |
101 | | - trace[curIndex].getEnclosingLineNumber() - 1, |
102 | | - trace[curIndex].getEnclosingColumnNumber() - 1, |
| 127 | + callSite.getEnclosingLineNumber() - 1, |
| 128 | + callSite.getEnclosingColumnNumber() - 1, |
103 | 129 | ); |
104 | 130 | if (enclosingEntry.name) return enclosingEntry.name; |
105 | | - // Fallback to using the symbol name attached to the next stack frame: |
106 | | - const currentFileName = trace[curIndex].getFileName(); |
107 | | - const nextCallSite = trace[curIndex + 1]; |
108 | | - if (nextCallSite && currentFileName === nextCallSite.getFileName()) { |
| 131 | + // Fallback to using the symbol name attached to the caller site: |
| 132 | + const currentFileName = callSite.getFileName(); |
| 133 | + if (callerCallSite && currentFileName === callerCallSite.getFileName()) { |
109 | 134 | const { name } = sourceMap.findEntry( |
110 | | - nextCallSite.getLineNumber() - 1, |
111 | | - nextCallSite.getColumnNumber() - 1, |
| 135 | + callerCallSite.getLineNumber() - 1, |
| 136 | + callerCallSite.getColumnNumber() - 1, |
112 | 137 | ); |
113 | 138 | return name; |
114 | 139 | } |
|