• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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