Skip to content

Instantly share code, notes, and snippets.

@Caellian
Last active November 28, 2024 19:54
Show Gist options
  • Save Caellian/58278ba00c2c98234e98a4dc6fdf0355 to your computer and use it in GitHub Desktop.
Save Caellian/58278ba00c2c98234e98a4dc6fdf0355 to your computer and use it in GitHub Desktop.
JS stack trace parser
/**
* @typedef {object} LCPath
* @property {"eval" | string} file
* @property {number} [line]
* @property {number} [column]
*/
/**
* Parses a line-column path (e.g. "/some/file.txt:2:12"), into a computer
* friendly format.
* @param {string} path - input path to parse.
* @returns {LCPath} path with separated line-column information.
*/
function parseLCPath(path) {
let clean = path;
if (path.startsWith("file://")) {
clean = path.slice(7);
}
if (clean.startsWith("/")) {
let fileParts = clean.split(":");
return {
file: fileParts[0],
line: Number(fileParts[1]) || undefined,
column: Number(fileParts[2]) || undefined,
};
} else if (clean.startsWith("eval")) {
let fileParts = clean.split(":");
return {
file: "eval",
line: Number(fileParts[1]) || undefined,
column: Number(fileParts[2]) || undefined,
};
} else {
// got "href://" or something
let fileParts = clean.split(":");
return {
file: fileParts[0] + ":" + fileParts[1],
line: Number(fileParts[2]) || undefined,
column: Number(fileParts[3]) || undefined,
};
}
}
/**
* Format of stack trace like, excluding "at" prefix.
*
* e.g. async main [as m] (file:///example/main.js)
*/
const LINE = new RegExp(/(async\s+)?([^\s]+\s+)?(\[as\s(\w+)]\s)?(.*)/);
/**
* @typedef {object} StackEntry
* @property {boolean} [async] - whether the entry is asynchronous
* @property {string} [method] - called method
* This can only be `undefined` if entry represents file execution.
* @property {string} [alias] - export alias of method that was called
* @property {boolean} [debugger] - true if entry is part of debugger scope
* Note: not used.
* @property {LCPath} path - path of the entry file
* Note: `path.file` is "eval" for evaluted code.
*/
/**
* Produces structured stack trace information.
*
* Note: tested only in Node environment.
* @param {string | string[] | null} [since] - skip all stack trace entries
* before and **including** one containing this string (or many)
* @param {string | string[] | null} [until] - skip all stack trace entries
* after and **including** one containing this string (or many)
* @returns {StackEntry[]} caller stack trace
*/
function structuredTrace(since = null, until = "structuredTrace") {
let stack = Error().stack.split("\n").slice(1); // Skip first "Error" line
let current;
if (since != null) {
if (typeof since === "string") {
while ((current = stack.pop())?.includes(since) === false) {}
} else if (Array.isArray(since)) {
let current = stack.pop();
let done = false;
while (current && !done) {
for (const item in since) {
if (current.includes(item)) {
done = true;
break;
}
}
if (!done) current = stack.pop();
}
}
}
stack.reverse();
if (until != null) {
if (typeof until === "string") {
while ((current = stack.pop())?.includes(until) === false) {}
} else if (Array.isArray(until)) {
let current = stack.pop();
let done = false;
while (current && !done) {
for (const item in until) {
if (current.includes(item)) {
done = true;
break;
}
}
if (!done) current = stack.pop();
}
}
}
stack.reverse();
stack = stack.map((it) => it.trim().slice(3)); // extract clean trace entries
function parseStackLine(line) {
const matches = LINE.exec(line);
const async = matches[1] ? true : undefined;
let method = matches[2]?.trim();
let alias = undefined;
if (matches[3] !== null) {
alias = matches[4];
}
let path = matches[5];
if (path.startsWith("(") && path.endsWith(")")) {
path = path.slice(1, path.length - 1);
}
path = parseLCPath(path);
return {
async,
method,
alias,
path,
};
}
return stack.map(parseStackLine);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment