| ... | ... | @@ -1,48 +1,75 @@ |
| 1 | | // the default stack trace formatters in node.js, deno, and bun are all |
| 2 | | // extremely low quality. they don't highlight the most important |
| 3 | | // information, and spend too much space on showing source code. |
| 4 | | // |
| 5 | | // this printer puts the error metadata at the top. each stack frame gets |
| 6 | | // two lines: the absolute path or url, and then one line of source code. |
| 7 | | // having five lines of context on just the top frame is useless, since |
| 8 | | // five lines is often not enough context to understand the entire block of |
| 9 | | // code. one line is all you need to remind yourself what the code on that |
| 10 | | // line was doing. and if you can see every frame's source code, you can |
| 11 | | // much more easily figure out what the bug is before opening the editor. |
| 12 | | // |
| 13 | | // to make understanding runtime code easier, `node:*` and `ext:deno` paths |
| 14 | | // are resolved into GitHub source code URLs, and node builtins will also |
| 15 | | // display their source code inline (via `process.binding("natives")`). |
| 16 | | // |
| 17 | | // with zero required imports, it can be safely included as a part of the |
| 18 | | // headless log and widget host without worrying about if the module can |
| 19 | | // import successfully. because of this, 'lib/log' uses this by default. |
| 20 | | // |
| 21 | | // parsing code is derived from clover's work on bun's DevServer, which she |
| 22 | | // derived from stacktracejs. this file is more refined than those two: |
| 23 | | // https://github.com/oven-sh/bun/blob/b5f31a6ee2f52ea67eabeb61f6e6e71215d55b26/src/bake/client/stack-trace.ts |
| 24 | | // https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js |
| 1 | /** |
| 2 | * the default stack trace formatters in node.js, deno, and bun are all |
| 3 | * extremely low quality. they don't highlight the most important information |
| 4 | * and spend too much space on showing source code. this module provides tools |
| 5 | * to `parse` and `format` errors. see `format`'s comment for design decisions. |
| 6 | * |
| 7 | * to get a taste of the formatter: |
| 8 | * ```ts |
| 9 | * try { |
| 10 | * processFile(undefined); |
| 11 | * } catch (err) { |
| 12 | * console.error(stack.parse(err)); |
| 13 | * console.error(stack.format(err, true)); |
| 14 | * } |
| 15 | * |
| 16 | * function processFile(file) { |
| 17 | * return fs.createReadStream(file); |
| 18 | * } |
| 19 | * |
| 20 | * import * as fs from "node:fs"; |
| 21 | * import * as stack from "lib/log/stack.ts"; |
| 22 | * ``` |
| 23 | * |
| 24 | * @module |
| 25 | */ |
| 26 | |
| 27 | /** Retrieved from `parse` */ |
| 25 | 28 | export interface Frame { |
| 29 | /** `null` if top-level code or unnamed function. */ |
| 26 | 30 | fn: string | null; |
| 31 | /** `null` if from a built in without a module name. */ |
| 27 | 32 | file: string | null; |
| 33 | /** `null` if the source provider doesn't have a source location. */ |
| 28 | 34 | line: number | null; |
| 35 | /** almost certainly defined if `line` is, but the runtime may omit it */ |
| 29 | 36 | col: number | null; |
| 30 | 37 | } |
| 31 | 38 | |
| 32 | | const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m; |
| 33 | | const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/; |
| 34 | | |
| 39 | /** |
| 40 | * parse a error stack trace from either v8 (node, deno, chrome), |
| 41 | * jsc (safari, bun), or spidermonkey (firefox). |
| 42 | * |
| 43 | * this function is derived from clover's work on bun's DevServer, which she |
| 44 | * derived from stacktracejs. this file is more refined than those two: |
| 45 | * https://github.com/oven-sh/bun/blob/b5f31a6ee2f52ea67eabeb61f6e6e71215d55b26/src/bake/client/stack-trace.ts |
| 46 | * https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js |
| 47 | */ |
| 35 | 48 | export function parse(error: Error): Frame[] | null { |
| 36 | 49 | const stack = error?.stack; |
| 37 | | if (typeof stack === "string") { |
| 38 | | if (stack.match(regexV8Stack)) { |
| 39 | | return parseV8OrIe(stack); |
| 40 | | } |
| 41 | | return parseJscOrSpidermonkey(stack); |
| 42 | | } |
| 43 | | return null; |
| 50 | if (typeof stack !== "string") return null; |
| 51 | return stack.match(regexV8Stack) ? parseV8OrIe(stack) : parseJscOrSpidermonkey(stack); |
| 44 | 52 | } |
| 45 | 53 | |
| 54 | /** |
| 55 | * convert an error into a human readable and debuggable string. |
| 56 | * |
| 57 | * this printer puts the error metadata at the top. each stack frame gets two |
| 58 | * lines: the absolute path or url, and then one line of source code. having |
| 59 | * five lines of context on just the top frame is useless, since five lines is |
| 60 | * often not enough context to understand the entire block of code. one line is |
| 61 | * all you need to remind yourself what the code on that line was doing. and if |
| 62 | * you can see every frame's source code, you can much more easily figure out |
| 63 | * what the bug is before opening the editor. |
| 64 | * |
| 65 | * to make understanding runtime code easier, `node:*` and `ext:deno` paths are |
| 66 | * resolved into GitHub source code URLs, and node builtins will also display |
| 67 | * their source code inline (via `process.binding("natives")`). |
| 68 | * |
| 69 | * with zero external imports, it is safely included as a default error |
| 70 | * formatter. it is used not only in `lib/log.ts`, but also in the headless |
| 71 | * logger. |
| 72 | */ |
| 46 | 73 | export function format(error: Error, colors = false): string { |
| 47 | 74 | const { name, message, stack: _, ...payload } = error; |
| 48 | 75 | |
| ... | ... | @@ -76,10 +103,9 @@ export function format(error: Error, colors = false): string { |
| 76 | 103 | return headingLine + payloadLines + frameLines + "\n"; |
| 77 | 104 | } |
| 78 | 105 | |
| 79 | | export function formatFrame( |
| 80 | | { fn, file, line, col }: Frame, |
| 81 | | colors: boolean, |
| 82 | | ): string { |
| 106 | /** format a single stack trace frame. see `format` for more details. */ |
| 107 | export function formatFrame(frame: Frame, colors: boolean): string { |
| 108 | let { fn, file, line, col } = frame; |
| 83 | 109 | let out = ""; |
| 84 | 110 | if (file === "native") file = null; |
| 85 | 111 | const internal = !file || file.startsWith("node:") || |
| ... | ... | @@ -373,4 +399,7 @@ function nodeBuiltin(name: string) { |
| 373 | 399 | return globalThis.process?.getBuiltinModule(name) ?? null; |
| 374 | 400 | } |
| 375 | 401 | |
| 402 | const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m; |
| 403 | const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/; |
| 404 | |
| 376 | 405 | import * as ansi from "lib/log/ansi.ts"; |