| 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 {@linkcode parse} and {@linkcode format} errors. see {@linkcode format}'s |
| 6 | * comment for design decisions. |
| 7 | * |
| 8 | * to get a taste of the formatter: |
| 9 | * ```ts |
| 10 | * try { |
| 11 | * processFile(undefined); |
| 12 | * } catch (err) { |
| 13 | * console.error(stack.parse(err)); |
| 14 | * console.error(stack.format(err, true)); |
| 15 | * } |
| 16 | * |
| 17 | * function processFile(file) { |
| 18 | * return fs.createReadStream(file); |
| 19 | * } |
| 20 | * |
| 21 | * import * as fs from "node:fs"; |
| 22 | * import * as stack from "@clo/lib/log/stack"; |
| 23 | * ``` |
| 24 | * |
| 25 | * @module |
| 26 | */ |
| 27 | |
| 28 | /** retrieved from `parse` */ |
| 29 | export interface Frame { |
| 30 | /** `null` if top-level code or unnamed function. */ |
| 31 | fn: string | null; |
| 32 | /** `null` if from a built in without a module name. */ |
| 33 | file: string | null; |
| 34 | /** `null` if the source provider doesn't have a source location. */ |
| 35 | line: number | null; |
| 36 | /** almost certainly defined if `line` is, but the runtime may omit it */ |
| 37 | col: number | null; |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * parse a error stack trace from either v8 (node, deno, chrome), |
| 42 | * jsc (safari, bun), or spidermonkey (firefox). |
| 43 | * |
| 44 | * this function is derived from clover's work on bun's DevServer, which she |
| 45 | * derived from stacktracejs. this file is more refined than those two: |
| 46 | * https://github.com/oven-sh/bun/blob/b5f31a6ee2f52ea67eabeb61f6e6e71215d55b26/src/bake/client/stack-trace.ts |
| 47 | * https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js |
| 48 | * |
| 49 | * supports parsing v8, JavaScriptCore, SpiderMonkey, and IE error stack |
| 50 | * frames, effectively working in every JavaScript environment. leaves |
| 51 | * filesystem urls and paths intact (won't convert 'file:///' to or from a file |
| 52 | * path) |
| 53 | */ |
| 54 | export function parse(error: Error | string): Frame[] | null { |
| 55 | const stack = (error as Error)?.stack ?? error; |
| 56 | if (typeof stack !== "string") return null; |
| 57 | // a v8 trace begins with the message, which is all there is without frames |
| 58 | if (typeof error !== "string" && stack === String(error)) return []; |
| 59 | return stack.match(regexV8Stack) |
| 60 | ? parseV8OrIe(stack) |
| 61 | : parseJscOrSpidermonkey(stack); |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * capture and store for later, or use to trace the caller. with the |
| 66 | * default `trimStart` of `2`, the caller file is `capture()[0]?.file`. |
| 67 | */ |
| 68 | export function capture(trimStart = 2): Frame[] { |
| 69 | return parse(new Error())?.slice(trimStart) ?? []; |
| 70 | } |
| 71 | |
| 72 | /** an error with a method by this key prints itself, given whether to use colors */ |
| 73 | export const formatSymbol: unique symbol = Symbol.for("@clo/lib/log/stack.format"); |
| 74 | |
| 75 | /** an error stops what it happened in, and a warning only reports something */ |
| 76 | export type Severity = "error" | "warning"; |
| 77 | |
| 78 | /** |
| 79 | * an error as {@linkcode format} lays it out, from {@linkcode describe}. it is |
| 80 | * plain data, so it can be drawn elsewhere, like on a page. |
| 81 | */ |
| 82 | export interface Diagnostic { |
| 83 | /** |
| 84 | * what is said before the colon: the severity, or else the error's name, |
| 85 | * which says more than `Error` or `TypeError` would |
| 86 | */ |
| 87 | label: string; |
| 88 | severity: Severity; |
| 89 | message: string; |
| 90 | /** |
| 91 | * what it is about: the place its `file`, `line`, and `col` fields name, |
| 92 | * or else the first frame outside of a runtime or a package |
| 93 | */ |
| 94 | location: Excerpt | null; |
| 95 | /** its other fields, each as json */ |
| 96 | fields: Array<[name: string, json: string]>; |
| 97 | /** its `notes` field, and then the frames of its stack */ |
| 98 | notes: Note[]; |
| 99 | cause: Diagnostic | null; |
| 100 | /** an `AggregateError`'s errors */ |
| 101 | errors: Diagnostic[]; |
| 102 | /** more errors like it, of which only their message and where they came from are said */ |
| 103 | similar: Array<{ message: string; from: string }>; |
| 104 | } |
| 105 | |
| 106 | export interface Note { |
| 107 | text: string; |
| 108 | /** the place it points at */ |
| 109 | location: Excerpt | null; |
| 110 | /** frames of a stack, which a page shows folded */ |
| 111 | frames: Excerpt[]; |
| 112 | } |
| 113 | |
| 114 | /** a place in source code, with the code there */ |
| 115 | export interface Excerpt { |
| 116 | /** as shown: relative to the root inside of it, or else a path or url. `null` for native code */ |
| 117 | file: string | null; |
| 118 | line: number | null; |
| 119 | col: number | null; |
| 120 | /** the function, for a frame of a stack */ |
| 121 | fn: string | null; |
| 122 | /** the lines of the file from `first`, around `line`. none when the source can't be read */ |
| 123 | code: string[]; |
| 124 | first: number; |
| 125 | /** what on `line` it points at, as offsets into the line */ |
| 126 | span: [start: number, end: number] | null; |
| 127 | /** a runtime's or a package's code, which matters less */ |
| 128 | internal: boolean; |
| 129 | } |
| 130 | |
| 131 | export interface DescribeOptions { |
| 132 | /** @default "error" */ |
| 133 | severity?: Severity; |
| 134 | /** |
| 135 | * paths inside of it are shown relative to it, and a relative `file` field |
| 136 | * is read from it. defaults to the working directory. |
| 137 | */ |
| 138 | root?: string; |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * lay out an error as {@linkcode format} prints it. the source code of each |
| 143 | * place is read as it is now, and the place an error is about shows the lines |
| 144 | * around it as well. |
| 145 | */ |
| 146 | export function describe(error: unknown, options: DescribeOptions = {}): Diagnostic { |
| 147 | const severity = options.severity ?? "error"; |
| 148 | const root = options.root ?? node.process?.cwd() ?? null; |
| 149 | const sources = new Map<string, string[] | null>(); |
| 150 | const seen = new Set<unknown>(); |
| 151 | const excerpt = (frame: Frame, context: number) => toExcerpt(frame, context, root, sources); |
| 152 | // a `width` says how much of the line is meant, where a column alone points at a word |
| 153 | const place = ({ file, line, col, width }: Record<string, unknown>, context: number) => { |
| 154 | const at = excerpt({ fn: null, file: file as string, line: num(line), col: num(col) }, context); |
| 155 | if (at.span && at.col && num(width)) at.span = [at.col - 1, at.col - 1 + num(width)!]; |
| 156 | return at; |
| 157 | }; |
| 158 | |
| 159 | const visit = (value: unknown): Diagnostic => { |
| 160 | seen.add(value); |
| 161 | const object = typeof value === "object" && value !== null ? value as Record<string, unknown> : null; |
| 162 | const name = typeof object?.name === "string" ? object.name : ""; |
| 163 | const message = value instanceof Error ? value.message : typeof value === "string" ? value : json(value); |
| 164 | // a `file` field says what the error is about, which can be other than where it was thrown |
| 165 | const about = object && typeof object.file === "string" ? object : null; |
| 166 | |
| 167 | const fields: Diagnostic["fields"] = []; |
| 168 | for (const [key, field] of Object.entries(object ?? {})) { |
| 169 | if (["name", "message", "stack", "cause", "errors"].includes(key)) continue; |
| 170 | if (about && ["file", "line", "col", "width"].includes(key)) continue; |
| 171 | if (key === "notes" && Array.isArray(field)) continue; |
| 172 | fields.push([key, json(field)]); |
| 173 | } |
| 174 | |
| 175 | const notes: Note[] = []; |
| 176 | for (const note of Array.isArray(object?.notes) ? object.notes as unknown[] : []) { |
| 177 | const fields = (typeof note === "object" && note !== null ? note : {}) as Record<string, unknown>; |
| 178 | if (typeof fields.text !== "string") continue; |
| 179 | notes.push({ |
| 180 | text: fields.text, |
| 181 | location: typeof fields.file === "string" ? place(fields, 0) : null, |
| 182 | frames: [], |
| 183 | }); |
| 184 | } |
| 185 | |
| 186 | let frames = value instanceof Error ? parse(value) ?? [] : []; |
| 187 | if (node.isServer && ("Bun" in globalThis)) { |
| 188 | frames = frames.filter((frame) => |
| 189 | frame.file !== "native" |
| 190 | || !["moduleEvaluation", "loadAndEvaluateModule", "processTicksAndRejections"].includes(frame.fn ?? "") |
| 191 | ); |
| 192 | } |
| 193 | const stack = frames.map((frame) => excerpt(frame, 0)); |
| 194 | let location: Excerpt | null = null; |
| 195 | if (about) { |
| 196 | location = place(about, 1); |
| 197 | if (stack.length > 0) notes.push({ text: "thrown here", location: null, frames: stack }); |
| 198 | } else if (frames.length > 0) { |
| 199 | const at = Math.max(0, stack.findIndex((frame) => !frame.internal)); |
| 200 | location = excerpt(frames[at]!, 1); |
| 201 | if (at > 0) notes.push({ text: "thrown inside", location: null, frames: stack.slice(0, at) }); |
| 202 | if (at + 1 < stack.length) notes.push({ text: "called from", location: null, frames: stack.slice(at + 1) }); |
| 203 | } |
| 204 | |
| 205 | const cause = object && object.cause !== undefined && !seen.has(object.cause) ? visit(object.cause) : null; |
| 206 | return { |
| 207 | label: name && name !== "Error" && name !== "TypeError" ? name : severity, |
| 208 | severity, |
| 209 | message, |
| 210 | location, |
| 211 | fields, |
| 212 | notes, |
| 213 | cause, |
| 214 | errors: value instanceof AggregateError ? value.errors.filter((inner) => !seen.has(inner)).map(visit) : [], |
| 215 | similar: [], |
| 216 | }; |
| 217 | }; |
| 218 | return visit(error); |
| 219 | } |
| 220 | |
| 221 | /** |
| 222 | * convert an error into a human readable and debuggable string. |
| 223 | * |
| 224 | * this printer starts with what went wrong and where: a line saying whether it |
| 225 | * is an error or a warning and its message, then the place it is about. each |
| 226 | * place gets two lines: the path, relative to the working directory when it |
| 227 | * is inside of it, and then one line of source code. having five lines of |
| 228 | * context is useless, since five lines is often not enough context to |
| 229 | * understand the entire block of code. one line is all you need to remind |
| 230 | * yourself what the code on that line was doing. and if you can see every |
| 231 | * frame's source code, you can much more easily figure out what the bug is |
| 232 | * before opening the editor. the stack comes after, as a note. |
| 233 | * |
| 234 | * to make understanding runtime code easier, `node:*` and `ext:deno` paths are |
| 235 | * resolved into GitHub source code URLs, and node builtins will also display |
| 236 | * their source code inline (via `node:fs` and `process.binding("natives")`). |
| 237 | * |
| 238 | * with zero external imports, it is safely included as a default error |
| 239 | * formatter. it is used not only in `lib/log.ts`, but also in the headless |
| 240 | * logger. |
| 241 | */ |
| 242 | export function format(error: Error, colors = false, options?: DescribeOptions): string { |
| 243 | // what an AggregateError holds can be any value |
| 244 | if (typeof error === "object" && error !== null && formatSymbol in error) { |
| 245 | return (error as Error & { [formatSymbol](colors: boolean): string })[formatSymbol](colors); |
| 246 | } |
| 247 | return formatDiagnostic(describe(error, options), colors); |
| 248 | } |
| 249 | |
| 250 | /** print what {@linkcode describe} laid out, like {@linkcode format} */ |
| 251 | export function formatDiagnostic(diagnostic: Diagnostic, colors = false): string { |
| 252 | const paint = (code: string, text: string) => colors && code && text ? ansi.style(code, text) : text; |
| 253 | const tone = diagnostic.severity === "error" ? ansi.fgRed : ansi.fgYellow; |
| 254 | const head = (label: string, text: string, code: string) => |
| 255 | paint(code + ansi.bold, label) + paint(ansi.dim, ":") + " " + text + "\n"; |
| 256 | |
| 257 | let out = head(diagnostic.label, diagnostic.message, tone); |
| 258 | if (diagnostic.location) out += formatExcerpt(diagnostic.location, colors, tone) + "\n"; |
| 259 | for (const [name, json] of diagnostic.fields) out += ` ${name}${paint(ansi.dim, ":")} ${json}\n`; |
| 260 | for (const note of diagnostic.notes) { |
| 261 | out += head("note", note.text, ""); |
| 262 | if (note.location) out += formatExcerpt(note.location, colors, tone) + "\n"; |
| 263 | for (const frame of note.frames) out += formatExcerpt(frame, colors, tone) + "\n"; |
| 264 | } |
| 265 | if (diagnostic.cause) out += head("note", "caused by", "") + formatDiagnostic(diagnostic.cause, colors); |
| 266 | for (const error of diagnostic.errors) out += formatDiagnostic(error, colors); |
| 267 | if (diagnostic.similar.length > 0) { |
| 268 | out += paint(ansi.dim, `and ${diagnostic.similar.length} more like it`) + "\n"; |
| 269 | for (const { message, from } of diagnostic.similar) out += ` ${message} ${paint(ansi.dim, from)}\n`; |
| 270 | } |
| 271 | return out; |
| 272 | } |
| 273 | |
| 274 | /** format a single stack trace frame. see `format` for more details. */ |
| 275 | export function formatFrame(frame: Frame, colors: boolean): string { |
| 276 | return formatExcerpt(toExcerpt(frame, 0, node.process?.cwd() ?? null, new Map()), colors, ansi.fgRed); |
| 277 | } |
| 278 | |
| 279 | /** a place, and its line of code with what it points at in `tone` */ |
| 280 | function formatExcerpt( |
| 281 | { file, line, col, fn, code, first, span, internal }: Excerpt, |
| 282 | colors: boolean, |
| 283 | tone: string, |
| 284 | ): string { |
| 285 | const paint = (style: string, text: string) => colors && style && text ? ansi.style(style, text) : text; |
| 286 | let out = file === null ? paint(ansi.dim, "[unknown source]") : paint(internal ? ansi.dim : "", file); |
| 287 | if (file !== null && line) { |
| 288 | const position = file.startsWith("https://") ? `#L${line}` : `:${line}${col ? `:${col}` : ""}`; |
| 289 | out += paint(ansi.dim + ansi.fgYellow, position); |
| 290 | } |
| 291 | if (fn) out += " at " + paint(internal ? ansi.dim : ansi.fgCyan, fn); |
| 292 | |
| 293 | const text = line === null ? undefined : code[line - first]; |
| 294 | if (text === undefined) return out; |
| 295 | if (!colors || !span) return out + "\n" + text; |
| 296 | // the line is dim, and what it points at is bold |
| 297 | const [start, end] = span; |
| 298 | const color = internal ? "" : tone; |
| 299 | return out + "\n" + paint(color + ansi.dim, text.slice(0, start)) |
| 300 | + paint(color + (internal ? ansi.dim : ansi.bold), text.slice(start, end)) |
| 301 | + paint(color + ansi.dim, text.slice(end)); |
| 302 | } |
| 303 | |
| 304 | /** a frame as a place, with `context` lines of its source above and below */ |
| 305 | function toExcerpt( |
| 306 | { fn, file, line, col }: Frame, |
| 307 | context: number, |
| 308 | root: string | null, |
| 309 | sources: Map<string, string[] | null>, |
| 310 | ): Excerpt { |
| 311 | const path = node.builtin("path"); |
| 312 | // a frame of evaluated code has no file |
| 313 | if (!file || file === "native") file = null; |
| 314 | if (file?.startsWith("file://")) file = decodeURIComponent(new URL(file).pathname); |
| 315 | // a relative path is from the root, like a `file` field |
| 316 | if (file && path && root && !file.includes(":") && !path.isAbsolute(file)) file = path.resolve(root, file); |
| 317 | const runtime = !file || (node.isServer && /^(node|bun|internal|ext):/.test(file)); |
| 318 | const internal = runtime || /[\\/]node_modules[\\/]/.test(file!); |
| 319 | // the code of bun's runtime, and the code deno transpiles, has no lines that map |
| 320 | if (runtime && ("Bun" in globalThis || ("Deno" in globalThis && file?.endsWith(".ts.js") === false))) line = null; |
| 321 | |
| 322 | const lines = file && line ? memo(sources, file, () => readSource(file!)) : null; |
| 323 | const text = line ? lines?.[line - 1] : undefined; |
| 324 | const first = line ? Math.max(1, line - context) : 1; |
| 325 | return { |
| 326 | file: file && displayPath(file, root), |
| 327 | line: line || null, |
| 328 | col: col || null, |
| 329 | fn, |
| 330 | code: text === undefined ? [] : lines!.slice(first - 1, line! + context), |
| 331 | first, |
| 332 | span: text !== undefined && col ? spanAt(text, col) : null, |
| 333 | internal, |
| 334 | }; |
| 335 | } |
| 336 | |
| 337 | /** what a column points at: the word there, or the word before the symbols there */ |
| 338 | function spanAt(text: string, col: number): [number, number] | null { |
| 339 | if (col > text.length) return null; |
| 340 | const word = /[\p{L}\p{N}_$]/u; |
| 341 | let start = col - 1; |
| 342 | let end = col; |
| 343 | while (start > 0 && !word.test(text[start]!) && !/\s/.test(text[start]!)) start -= 1; |
| 344 | while (start > 0 && word.test(text[start - 1]!)) start -= 1; |
| 345 | while (end < text.length && word.test(text[end]!)) end += 1; |
| 346 | // `new` is about what it constructs |
| 347 | if (text.slice(start, end) === "new") end += text.slice(end).match(/^\s*[\p{L}\p{N}_$]+/u)?.[0]?.length ?? 0; |
| 348 | return [start, end]; |
| 349 | } |
| 350 | |
| 351 | /** a path relative to `root` when inside of it, or a url to the source of a runtime's own code */ |
| 352 | function displayPath(file: string, root: string | null): string { |
| 353 | const path = node.builtin("path"); |
| 354 | const process = node.process; |
| 355 | if (process && "Bun" in globalThis && /^(node|internal):/.test(file)) { |
| 356 | return `https://github.com/oven-sh/bun/blob/bun-v${process.versions.bun}/src/js/${file.replace(":", "/")}.ts`; |
| 357 | } |
| 358 | if (process && file.startsWith("node:")) { |
| 359 | if ("Deno" in globalThis) { |
| 360 | return `https://github.com/denoland/deno/blob/v${process.versions.deno}/ext/node/polyfills/${file.slice(5)}.js`; |
| 361 | } |
| 362 | return `https://github.com/nodejs/node/blob/v${process.versions.node}/lib/${file.slice(5)}.js`; |
| 363 | } |
| 364 | // deno's transpiled internals have no source maps |
| 365 | if (process?.versions.deno && file.startsWith("ext:deno_") && !file.endsWith(".ts.js")) { |
| 366 | const polyfill = file.startsWith("ext:deno_node"); |
| 367 | const name = file.slice(polyfill ? "ext:deno_node/".length : "ext:deno_".length); |
| 368 | return `https://github.com/denoland/deno/blob/v${process.versions.deno}/ext/` |
| 369 | + (polyfill ? `node/polyfills/${name.endsWith(".ts.js") ? name.slice(0, -3) : name}` : name); |
| 370 | } |
| 371 | if (!path || !root || !path.isAbsolute(file)) return file; |
| 372 | const relative = path.relative(root, file); |
| 373 | return relative.startsWith("..") || path.isAbsolute(relative) ? file : relative; |
| 374 | } |
| 375 | |
| 376 | /** the lines of a file, or of a node.js builtin */ |
| 377 | function readSource(file: string): string[] | null { |
| 378 | // node.js has a secret binding for all the internal source codes. bun and |
| 379 | // deno do not offer a way to get the source of their internal modules. |
| 380 | if (file.startsWith("node:")) return node.binding("natives")?.[file.slice(5)]?.split("\n") ?? null; |
| 381 | const fs = node.builtin("fs"); |
| 382 | const path = node.builtin("path"); |
| 383 | if (!fs || !path || !path.isAbsolute(file)) return null; |
| 384 | try { |
| 385 | return fs.readFileSync(file, "utf-8").split("\n"); |
| 386 | } catch { |
| 387 | return null; |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | function json(value: unknown): string { |
| 392 | const bigint = (inner: unknown) => typeof inner === "bigint" ? `${inner}n` : inner; |
| 393 | if (typeof value === "bigint") return `${value}n`; |
| 394 | try { |
| 395 | return JSON.stringify(value, (_, inner) => bigint(inner)) ?? String(value); |
| 396 | } catch { |
| 397 | return String(value); |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | function num(value: unknown): number | null { |
| 402 | return typeof value === "number" && value > 0 ? value : null; |
| 403 | } |
| 404 | |
| 405 | function memo<K, V>(map: Map<K, V>, key: K, create: () => V): V { |
| 406 | if (!map.has(key)) map.set(key, create()); |
| 407 | return map.get(key)!; |
| 408 | } |
| 409 | |
| 410 | function parseV8OrIe(stack: string): Frame[] { |
| 411 | return stack |
| 412 | .split("\n") |
| 413 | .filter((line) => !!line.match(regexV8Stack)) |
| 414 | .map(function(line) { |
| 415 | let sanitizedLine = line |
| 416 | .replace(/^\s+/, "") |
| 417 | .replace(/\(eval code/g, "(") |
| 418 | .replace(/^.*?\s+/, ""); |
| 419 | |
| 420 | // capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in |
| 421 | // case it has spaces in it, as the string is split on \s+ later on |
| 422 | const loc = sanitizedLine.match(/ (\(.+\)$)/); |
| 423 | |
| 424 | // remove the parenthesized location from the line, if it was matched |
| 425 | sanitizedLine = loc ? sanitizedLine.replace(loc[0], "") : sanitizedLine; |
| 426 | |
| 427 | // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine |
| 428 | // because this line doesn't have function name |
| 429 | // an anonymous async frame is "async <location>" |
| 430 | const locationParts = extractLocation(loc?.[1] ?? sanitizedLine.replace(/^async /, "")); |
| 431 | const functionName = (loc && sanitizedLine) || undefined; |
| 432 | const fileName = ["eval", "<anonymous>"].indexOf(locationParts[0]) > -1 |
| 433 | ? undefined |
| 434 | : locationParts[0]; |
| 435 | |
| 436 | return { |
| 437 | fn: functionName ?? null, |
| 438 | file: fileName, |
| 439 | line: 0 | locationParts[1], |
| 440 | col: 0 | locationParts[2], |
| 441 | } satisfies Frame; |
| 442 | }); |
| 443 | } |
| 444 | |
| 445 | function parseJscOrSpidermonkey(stack: string): Frame[] { |
| 446 | // Using string literal "\n" does not work in Safari. |
| 447 | return stack.split(/\n/g).map((source) => { |
| 448 | let fn: string | null = ""; |
| 449 | let file: string | null = null; |
| 450 | let line: number | null = null; |
| 451 | let col: number | null = null; |
| 452 | if (source.endsWith("@")) { |
| 453 | // Safari eval frames only have function names and nothing else |
| 454 | fn = source.slice(0, -1); |
| 455 | } else if (source.indexOf("@") === -1 && source.indexOf(":") === -1) { |
| 456 | // Safari eval frames only have function names and nothing else |
| 457 | fn = source.endsWith("@") ? source.slice(0, -1) : source; |
| 458 | } else { |
| 459 | const matches = source.match(regexFunctionName); |
| 460 | const functionName = matches && matches[1] ? matches[1] : undefined; |
| 461 | const locationParts = extractLocation( |
| 462 | source.replace(regexFunctionName, ""), |
| 463 | ); |
| 464 | fn = functionName!; |
| 465 | file = locationParts[0]; |
| 466 | line = 0 | locationParts[1]; |
| 467 | col = 0 | locationParts[2]; |
| 468 | } |
| 469 | if (fn === "module code") fn = null; |
| 470 | return { |
| 471 | fn, |
| 472 | file, |
| 473 | line, |
| 474 | col, |
| 475 | }; |
| 476 | }); |
| 477 | } |
| 478 | |
| 479 | function extractLocation(urlLike: string) { |
| 480 | // Fail-fast but return locations like "(native)" |
| 481 | if (urlLike.indexOf(":") === -1) { |
| 482 | return [urlLike]; |
| 483 | } |
| 484 | |
| 485 | const parts: any = regexLocation.exec(urlLike.replace(/[()]/g, "")); |
| 486 | return [parts[1], parts[2] || undefined, parts[3] || undefined]; |
| 487 | } |
| 488 | |
| 489 | const regexFunctionName = /((.*".+"[^@]*)?[^@]*)(?:@)/; |
| 490 | const regexV8Stack = /^\s*at .*(\S+:\d+|\(native\))/m; |
| 491 | const regexLocation = /(.+?)(?::(\d+))?(?::(\d+))?$/; |
| 492 | |
| 493 | import * as node from "../node.ts"; |
| 494 | import * as ansi from "../string/ansi.ts"; |