1'use strict'; 2 3const { 4 ArrayPrototypeIndexOf, 5 ArrayPrototypeJoin, 6 ArrayPrototypeMap, 7 ErrorPrototypeToString, 8 StringPrototypeRepeat, 9 StringPrototypeSlice, 10 StringPrototypeSplit, 11 StringPrototypeStartsWith, 12 SafeStringIterator, 13} = primordials; 14 15let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => { 16 debug = fn; 17}); 18const { getStringWidth } = require('internal/util/inspect'); 19const { readFileSync } = require('fs'); 20const { findSourceMap } = require('internal/source_map/source_map_cache'); 21const { 22 kNoOverride, 23 overrideStackTrace, 24 maybeOverridePrepareStackTrace 25} = require('internal/errors'); 26const { fileURLToPath } = require('internal/url'); 27 28// Create a prettified stacktrace, inserting context from source maps 29// if possible. 30const prepareStackTrace = (globalThis, error, trace) => { 31 // API for node internals to override error stack formatting 32 // without interfering with userland code. 33 // TODO(bcoe): add support for source-maps to repl. 34 if (overrideStackTrace.has(error)) { 35 const f = overrideStackTrace.get(error); 36 overrideStackTrace.delete(error); 37 return f(error, trace); 38 } 39 40 const globalOverride = 41 maybeOverridePrepareStackTrace(globalThis, error, trace); 42 if (globalOverride !== kNoOverride) return globalOverride; 43 44 const errorString = ErrorPrototypeToString(error); 45 46 if (trace.length === 0) { 47 return errorString; 48 } 49 50 let errorSource = ''; 51 let lastSourceMap; 52 let lastFileName; 53 const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => { 54 const str = i !== 0 ? '\n at ' : ''; 55 try { 56 // A stack trace will often have several call sites in a row within the 57 // same file, cache the source map and file content accordingly: 58 const fileName = t.getFileName(); 59 const sm = fileName === lastFileName ? 60 lastSourceMap : 61 findSourceMap(fileName); 62 lastSourceMap = sm; 63 lastFileName = fileName; 64 if (sm) { 65 // Source Map V3 lines/columns start at 0/0 whereas stack traces 66 // start at 1/1: 67 const { 68 originalLine, 69 originalColumn, 70 originalSource, 71 } = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); 72 if (originalSource && originalLine !== undefined && 73 originalColumn !== undefined) { 74 const name = getOriginalSymbolName(sm, trace, i); 75 if (i === 0) { 76 errorSource = getErrorSource( 77 sm, 78 originalSource, 79 originalLine, 80 originalColumn 81 ); 82 } 83 // Construct call site name based on: v8.dev/docs/stack-trace-api: 84 const fnName = t.getFunctionName() ?? t.getMethodName(); 85 const originalName = `${t.getTypeName() !== 'global' ? 86 `${t.getTypeName()}.` : ''}${fnName ? fnName : '<anonymous>'}`; 87 // The original call site may have a different symbol name 88 // associated with it, use it: 89 const prefix = (name && name !== originalName) ? 90 `${name}` : 91 `${originalName ? originalName : ''}`; 92 const hasName = !!(name || originalName); 93 const originalSourceNoScheme = 94 StringPrototypeStartsWith(originalSource, 'file://') ? 95 fileURLToPath(originalSource) : originalSource; 96 // Replace the transpiled call site with the original: 97 return `${str}${prefix}${hasName ? ' (' : ''}` + 98 `${originalSourceNoScheme}:${originalLine + 1}:` + 99 `${originalColumn + 1}${hasName ? ')' : ''}`; 100 } 101 } 102 } catch (err) { 103 debug(err.stack); 104 } 105 return `${str}${t}`; 106 }), ''); 107 return `${errorSource}${errorString}\n at ${preparedTrace}`; 108}; 109 110// Transpilers may have removed the original symbol name used in the stack 111// trace, if possible restore it from the names field of the source map: 112function getOriginalSymbolName(sourceMap, trace, curIndex) { 113 // First check for a symbol name associated with the enclosing function: 114 const enclosingEntry = sourceMap.findEntry( 115 trace[curIndex].getEnclosingLineNumber() - 1, 116 trace[curIndex].getEnclosingColumnNumber() - 1 117 ); 118 if (enclosingEntry.name) return enclosingEntry.name; 119 // Fallback to using the symbol name attached to the next stack frame: 120 const currentFileName = trace[curIndex].getFileName(); 121 const nextCallSite = trace[curIndex + 1]; 122 if (nextCallSite && currentFileName === nextCallSite.getFileName()) { 123 const { name } = sourceMap.findEntry( 124 nextCallSite.getLineNumber() - 1, 125 nextCallSite.getColumnNumber() - 1 126 ); 127 return name; 128 } 129} 130 131// Places a snippet of code from where the exception was originally thrown 132// above the stack trace. This logic is modeled after GetErrorSource in 133// node_errors.cc. 134function getErrorSource( 135 sourceMap, 136 originalSourcePath, 137 originalLine, 138 originalColumn 139) { 140 let exceptionLine = ''; 141 const originalSourcePathNoScheme = 142 StringPrototypeStartsWith(originalSourcePath, 'file://') ? 143 fileURLToPath(originalSourcePath) : originalSourcePath; 144 const source = getOriginalSource( 145 sourceMap.payload, 146 originalSourcePathNoScheme 147 ); 148 const lines = StringPrototypeSplit(source, /\r?\n/, originalLine + 1); 149 const line = lines[originalLine]; 150 if (!line) return exceptionLine; 151 152 // Display ^ in appropriate position, regardless of whether tabs or 153 // spaces are used: 154 let prefix = ''; 155 for (const character of new SafeStringIterator( 156 StringPrototypeSlice(line, 0, originalColumn + 1))) { 157 prefix += (character === '\t') ? '\t' : 158 StringPrototypeRepeat(' ', getStringWidth(character)); 159 } 160 prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'. 161 162 exceptionLine = 163 `${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`; 164 return exceptionLine; 165} 166 167function getOriginalSource(payload, originalSourcePath) { 168 let source; 169 const originalSourcePathNoScheme = 170 StringPrototypeStartsWith(originalSourcePath, 'file://') ? 171 fileURLToPath(originalSourcePath) : originalSourcePath; 172 const sourceContentIndex = 173 ArrayPrototypeIndexOf(payload.sources, originalSourcePath); 174 if (payload.sourcesContent?.[sourceContentIndex]) { 175 // First we check if the original source content was provided in the 176 // source map itself: 177 source = payload.sourcesContent[sourceContentIndex]; 178 } else { 179 // If no sourcesContent was found, attempt to load the original source 180 // from disk: 181 try { 182 source = readFileSync(originalSourcePathNoScheme, 'utf8'); 183 } catch (err) { 184 debug(err); 185 source = ''; 186 } 187 } 188 return source; 189} 190 191module.exports = { 192 prepareStackTrace, 193}; 194