nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
251 lines (234 loc) • 8.7 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/source_map/prepare_stack_trace.js
import { getStringWidth } from "nstdlib/lib/internal/util/inspect";
import { readFileSync } from "nstdlib/lib/fs";
import { findSourceMap } from "nstdlib/lib/internal/source_map/source_map_cache";
import { kIsNodeError } from "nstdlib/lib/internal/errors";
import { fileURLToPath } from "nstdlib/lib/internal/url";
import { setGetSourceMapErrorSource } from "nstdlib/stub/binding/errors";
const kStackLineAt = "\n at ";
// Create a prettified stacktrace, inserting context from source maps
// if possible.
function prepareStackTraceWithSourceMaps(error, trace) {
let errorString;
if (kIsNodeError in error) {
errorString = `${error.name} [${error.code}]: ${error.message}`;
} else {
errorString = Error.prototype.toString.call(error);
}
if (trace.length === 0) {
return errorString;
}
let lastSourceMap;
let lastFileName;
const preparedTrace = Array.prototype.join.call(
Array.prototype.map.call(trace, (callSite, i) => {
try {
// A stack trace will often have several call sites in a row within the
// same file, cache the source map and file content accordingly:
let fileName = callSite.getFileName();
if (fileName === undefined) {
fileName = callSite.getEvalOrigin();
}
const sm =
fileName === lastFileName ? lastSourceMap : findSourceMap(fileName);
lastSourceMap = sm;
lastFileName = fileName;
if (sm) {
return `${kStackLineAt}${serializeJSStackFrame(sm, callSite, trace[i + 1])}`;
}
} catch (err) {
{
/* debug */
}
}
return `${kStackLineAt}${callSite}`;
}),
"",
);
return `${errorString}${preparedTrace}`;
}
/**
* Serialize a single call site in the stack trace.
* Refer to SerializeJSStackFrame in deps/v8/src/objects/call-site-info.cc for
* more details about the default ToString(CallSite).
* The CallSite API is documented at https://v8.dev/docs/stack-trace-api.
* @param {import('internal/source_map/source_map').SourceMap} sm
* @param {CallSite} callSite - the CallSite object to be serialized
* @param {CallSite} callerCallSite - caller site info
* @returns {string} - the serialized call site
*/
function serializeJSStackFrame(sm, callSite, callerCallSite) {
// Source Map V3 lines/columns start at 0/0 whereas stack traces
// start at 1/1:
const { originalLine, originalColumn, originalSource } = sm.findEntry(
callSite.getLineNumber() - 1,
callSite.getColumnNumber() - 1,
);
if (
originalSource === undefined ||
originalLine === undefined ||
originalColumn === undefined
) {
return `${callSite}`;
}
const name = getOriginalSymbolName(sm, callSite, callerCallSite);
const originalSourceNoScheme = String.prototype.startsWith.call(
originalSource,
"file://",
)
? fileURLToPath(originalSource)
: originalSource;
// Construct call site name based on: v8.dev/docs/stack-trace-api:
const fnName = callSite.getFunctionName() ?? callSite.getMethodName();
let prefix = "";
if (callSite.isAsync()) {
// Promise aggregation operation frame has no locations. This must be an
// async stack frame.
prefix = "async ";
} else if (callSite.isConstructor()) {
prefix = "new ";
}
const typeName = callSite.getTypeName();
const namePrefix =
typeName !== null && typeName !== "global" ? `${typeName}.` : "";
const originalName = `${namePrefix}${fnName || "<anonymous>"}`;
// The original call site may have a different symbol name
// associated with it, use it:
const mappedName =
name && name !== originalName ? `${name}` : `${originalName}`;
const hasName = !!(name || originalName);
// Replace the transpiled call site with the original:
return (
`${prefix}${mappedName}${hasName ? " (" : ""}` +
`${originalSourceNoScheme}:${originalLine + 1}:` +
`${originalColumn + 1}${hasName ? ")" : ""}`
);
}
// Transpilers may have removed the original symbol name used in the stack
// trace, if possible restore it from the names field of the source map:
function getOriginalSymbolName(sourceMap, callSite, callerCallSite) {
// First check for a symbol name associated with the enclosing function:
const enclosingEntry = sourceMap.findEntry(
callSite.getEnclosingLineNumber() - 1,
callSite.getEnclosingColumnNumber() - 1,
);
if (enclosingEntry.name) return enclosingEntry.name;
// Fallback to using the symbol name attached to the caller site:
const currentFileName = callSite.getFileName();
if (callerCallSite && currentFileName === callerCallSite.getFileName()) {
const { name } = sourceMap.findEntry(
callerCallSite.getLineNumber() - 1,
callerCallSite.getColumnNumber() - 1,
);
return name;
}
}
/**
* Return a snippet of code from where the exception was originally thrown
* above the stack trace. This called from GetErrorSource in node_errors.cc.
* @param {import('internal/source_map/source_map').SourceMap} sourceMap - the source map to be used
* @param {string} originalSourcePath - path or url of the original source
* @param {number} originalLine - line number in the original source
* @param {number} originalColumn - column number in the original source
* @returns {string | undefined} - the exact line in the source content or undefined if file not found
*/
function getErrorSource(
sourceMap,
originalSourcePath,
originalLine,
originalColumn,
) {
const originalSourcePathNoScheme = String.prototype.startsWith.call(
originalSourcePath,
"file://",
)
? fileURLToPath(originalSourcePath)
: originalSourcePath;
const source = getOriginalSource(sourceMap.payload, originalSourcePath);
if (typeof source !== "string") {
return;
}
const lines = RegExp.prototype[Symbol.split].call(
/\r?\n/,
source,
originalLine + 1,
);
const line = lines[originalLine];
if (!line) {
return;
}
// Display ^ in appropriate position, regardless of whether tabs or
// spaces are used:
let prefix = "";
for (const character of new SafeStringIterator(
String.prototype.slice.call(line, 0, originalColumn + 1),
)) {
prefix +=
character === "\t"
? "\t"
: String.prototype.repeat.call(" ", getStringWidth(character));
}
prefix = String.prototype.slice.call(prefix, 0, -1); // The last character is '^'.
const exceptionLine = `${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`;
return exceptionLine;
}
/**
* Retrieve the original source code from the source map's `sources` list or disk.
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
* @param {string} originalSourcePath - path or url of the original source
* @returns {string | undefined} - the source content or undefined if file not found
*/
function getOriginalSource(payload, originalSourcePath) {
let source;
// payload.sources has been normalized to be an array of absolute urls.
const sourceContentIndex = Array.prototype.indexOf.call(
payload.sources,
originalSourcePath,
);
if (payload.sourcesContent?.[sourceContentIndex]) {
// First we check if the original source content was provided in the
// source map itself:
source = payload.sourcesContent[sourceContentIndex];
} else if (String.prototype.startsWith.call(originalSourcePath, "file://")) {
// If no sourcesContent was found, attempt to load the original source
// from disk:
{
/* debug */
}
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
try {
source = readFileSync(originalSourcePathNoScheme, "utf8");
} catch (err) {
{
/* debug */
}
}
}
return source;
}
/**
* Retrieve exact line in the original source code from the source map's `sources` list or disk.
* @param {string} fileName - actual file name
* @param {number} lineNumber - actual line number
* @param {number} columnNumber - actual column number
* @returns {string | undefined} - the source content or undefined if file not found
*/
function getSourceMapErrorSource(fileName, lineNumber, columnNumber) {
const sm = findSourceMap(fileName);
if (sm === undefined) {
return;
}
const { originalLine, originalColumn, originalSource } = sm.findEntry(
lineNumber - 1,
columnNumber,
);
const errorSource = getErrorSource(
sm,
originalSource,
originalLine,
originalColumn,
);
return errorSource;
}
setGetSourceMapErrorSource(getSourceMapErrorSource);
export { prepareStackTraceWithSourceMaps };