authorgravatar for git@paperclover.netclo <git@paperclover.net> 2025-09-30 19:08:05-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:47-07:00
log7773a878828806850398f80dfa514df399b2deed
tree2a94a41852e5eaec9ed050d2ec82553d921c9293
parent367f564c813bfada851a56e3d80a2bb51fa9fd6d
signature Commit is signed but in an unrecognized format.

docs(lib/log/stack): document file


1 files changed, 67 insertions(+), 38 deletions(-)

lib/log/stack.ts+67-38
...@@ -1,48 +1,75 @@...@@ -1,48 +1,75 @@
1// the default stack trace formatters in node.js, deno, and bun are all1/**
2// extremely low quality. they don't highlight the most important2 * 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 gets5 * 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, since7 * to get a taste of the formatter:
8// five lines is often not enough context to understand the entire block of8 * ```ts
9// code. one line is all you need to remind yourself what the code on that9 * try {
10// line was doing. and if you can see every frame's source code, you can10 * 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` paths13 * console.error(stack.format(err, true));
14// are resolved into GitHub source code URLs, and node builtins will also14 * }
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 the17 * return fs.createReadStream(file);
18// headless log and widget host without worrying about if the module can18 * }
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 she21 * 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.ts23 *
24// https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js24 * @module
25 */
26
27/** Retrieved from `parse` */
25export interface Frame {28export 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}
3138
32const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m;39/**
33const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/;40 * parse a error stack trace from either v8 (node, deno, chrome),
3441 * 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 */
35export function parse(error: Error): Frame[] | null {48export 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}
4553
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 */
46export function format(error: Error, colors = false): string {73export function format(error: Error, colors = false): string {
47 const { name, message, stack: _, ...payload } = error;74 const { name, message, stack: _, ...payload } = error;
4875
...@@ -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}
78105
79export function formatFrame(106/** format a single stack trace frame. see `format` for more details. */
80 { fn, file, line, col }: Frame,107export 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}
375401
402const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m;
403const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/;
404
376import * as ansi from "lib/log/ansi.ts";405import * as ansi from "lib/log/ansi.ts";