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