Last active
November 28, 2024 19:54
-
-
Save Caellian/58278ba00c2c98234e98a4dc6fdf0355 to your computer and use it in GitHub Desktop.
JS stack trace parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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