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