diff --git a/lib/log/stack.ts b/lib/log/stack.ts index b68e830c2ec2b9f54049447f553a31d766702722..b16b98aca8eb8f2cefe72d67d62cbdda0d2ae0ab 100644 --- a/lib/log/stack.ts +++ b/lib/log/stack.ts @@ -1,48 +1,75 @@ -// the default stack trace formatters in node.js, deno, and bun are all -// extremely low quality. they don't highlight the most important -// information, and spend too much space on showing source code. -// -// this printer puts the error metadata at the top. each stack frame gets -// two lines: the absolute path or url, and then one line of source code. -// having five lines of context on just the top frame is useless, since -// five lines is often not enough context to understand the entire block of -// code. one line is all you need to remind yourself what the code on that -// line was doing. and if you can see every frame's source code, you can -// much more easily figure out what the bug is before opening the editor. -// -// to make understanding runtime code easier, `node:*` and `ext:deno` paths -// are resolved into GitHub source code URLs, and node builtins will also -// display their source code inline (via `process.binding("natives")`). -// -// with zero required imports, it can be safely included as a part of the -// headless log and widget host without worrying about if the module can -// import successfully. because of this, 'lib/log' uses this by default. -// -// parsing code is derived from clover's work on bun's DevServer, which she -// derived from stacktracejs. this file is more refined than those two: -// https://github.com/oven-sh/bun/blob/b5f31a6ee2f52ea67eabeb61f6e6e71215d55b26/src/bake/client/stack-trace.ts -// https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js +/** + * the default stack trace formatters in node.js, deno, and bun are all + * extremely low quality. they don't highlight the most important information + * and spend too much space on showing source code. this module provides tools + * to `parse` and `format` errors. see `format`'s comment for design decisions. + * + * to get a taste of the formatter: + * ```ts + * try { + * processFile(undefined); + * } catch (err) { + * console.error(stack.parse(err)); + * console.error(stack.format(err, true)); + * } + * + * function processFile(file) { + * return fs.createReadStream(file); + * } + * + * import * as fs from "node:fs"; + * import * as stack from "lib/log/stack.ts"; + * ``` + * + * @module + */ + +/** Retrieved from `parse` */ export interface Frame { + /** `null` if top-level code or unnamed function. */ fn: string | null; + /** `null` if from a built in without a module name. */ file: string | null; + /** `null` if the source provider doesn't have a source location. */ line: number | null; + /** almost certainly defined if `line` is, but the runtime may omit it */ col: number | null; } -const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m; -const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/; - +/** + * parse a error stack trace from either v8 (node, deno, chrome), + * jsc (safari, bun), or spidermonkey (firefox). + * + * this function is derived from clover's work on bun's DevServer, which she + * derived from stacktracejs. this file is more refined than those two: + * https://github.com/oven-sh/bun/blob/b5f31a6ee2f52ea67eabeb61f6e6e71215d55b26/src/bake/client/stack-trace.ts + * https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js + */ export function parse(error: Error): Frame[] | null { const stack = error?.stack; - if (typeof stack === "string") { - if (stack.match(regexV8Stack)) { - return parseV8OrIe(stack); - } - return parseJscOrSpidermonkey(stack); - } - return null; + if (typeof stack !== "string") return null; + return stack.match(regexV8Stack) ? parseV8OrIe(stack) : parseJscOrSpidermonkey(stack); } +/** + * convert an error into a human readable and debuggable string. + * + * this printer puts the error metadata at the top. each stack frame gets two + * lines: the absolute path or url, and then one line of source code. having + * five lines of context on just the top frame is useless, since five lines is + * often not enough context to understand the entire block of code. one line is + * all you need to remind yourself what the code on that line was doing. and if + * you can see every frame's source code, you can much more easily figure out + * what the bug is before opening the editor. + * + * to make understanding runtime code easier, `node:*` and `ext:deno` paths are + * resolved into GitHub source code URLs, and node builtins will also display + * their source code inline (via `process.binding("natives")`). + * + * with zero external imports, it is safely included as a default error + * formatter. it is used not only in `lib/log.ts`, but also in the headless + * logger. + */ export function format(error: Error, colors = false): string { const { name, message, stack: _, ...payload } = error; @@ -76,10 +103,9 @@ export function format(error: Error, colors = false): string { return headingLine + payloadLines + frameLines + "\n"; } -export function formatFrame( - { fn, file, line, col }: Frame, - colors: boolean, -): string { +/** format a single stack trace frame. see `format` for more details. */ +export function formatFrame(frame: Frame, colors: boolean): string { + let { fn, file, line, col } = frame; let out = ""; if (file === "native") file = null; const internal = !file || file.startsWith("node:") || @@ -373,4 +399,7 @@ function nodeBuiltin(name: string) { return globalThis.process?.getBuiltinModule(name) ?? null; } +const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m; +const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/; + import * as ansi from "lib/log/ansi.ts";