From 0fee857492069d09951a09fc94ed452b2634797b Mon Sep 17 00:00:00 2001 From: CatLover <152669316+catloversg@users.noreply.github.com> Date: Thu, 6 Feb 2025 23:44:12 +0700 Subject: [PATCH 1/3] MISC: Improve stack trace of errors in transformed scripts --- package-lock.json | 8 +-- package.json | 2 + src/Netscript/ErrorMessages.ts | 55 ++-------------- src/NetscriptJSEvaluator.ts | 3 +- src/NetscriptWorker.ts | 2 +- src/Script/LoadedModule.ts | 4 +- src/utils/ErrorHandler.ts | 12 ++-- src/utils/ErrorHelper.ts | 24 +++++-- src/utils/StackTraceUtils.ts | 111 +++++++++++++++++++++++++++++++++ 9 files changed, 151 insertions(+), 70 deletions(-) create mode 100644 src/utils/StackTraceUtils.ts diff --git a/package-lock.json b/package-lock.json index d6ebf5a841..c19b65ec6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "clsx": "^1.2.1", "convert-source-map": "^2.0.0", "date-fns": "^2.30.0", + "error-stack-parser": "^2.1.4", "escodegen": "^2.1.0", "jszip": "^3.10.1", "material-ui-color": "^1.2.0", @@ -47,6 +48,7 @@ "react-resizable": "^3.0.5", "react-syntax-highlighter": "^15.5.0", "remark-gfm": "^3.0.1", + "source-map-js": "^1.2.1", "sprintf-js": "^1.1.3", "tss-react": "^4.9.10" }, @@ -8356,7 +8358,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dev": true, + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } @@ -17241,7 +17243,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -17401,8 +17402,7 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "dev": true + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "node_modules/statuses": { "version": "2.0.1", diff --git a/package.json b/package.json index d31276c549..515334483d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "clsx": "^1.2.1", "convert-source-map": "^2.0.0", "date-fns": "^2.30.0", + "error-stack-parser": "^2.1.4", "escodegen": "^2.1.0", "jszip": "^3.10.1", "material-ui-color": "^1.2.0", @@ -47,6 +48,7 @@ "react-resizable": "^3.0.5", "react-syntax-highlighter": "^15.5.0", "remark-gfm": "^3.0.1", + "source-map-js": "^1.2.1", "sprintf-js": "^1.1.3", "tss-react": "^4.9.10" }, diff --git a/src/Netscript/ErrorMessages.ts b/src/Netscript/ErrorMessages.ts index fc5fe31ea0..1f622a1da6 100644 --- a/src/Netscript/ErrorMessages.ts +++ b/src/Netscript/ErrorMessages.ts @@ -1,6 +1,7 @@ import type { WorkerScript } from "./WorkerScript"; import { ScriptDeath } from "./ScriptDeath"; import type { NetscriptContext } from "./APIWrapper"; +import { parseStackTrace } from "../utils/StackTraceUtils"; /** Log a message to a script's logs */ export function log(ctx: NetscriptContext, message: () => string) { @@ -25,57 +26,13 @@ export function basicErrorMessage(ws: WorkerScript | ScriptDeath, msg: string, t * instance, then remove "unrelated" traces (code in our codebase) and leave only traces of the player's code. */ export function errorMessage(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string { - const errstack = new Error().stack; - if (errstack === undefined) throw new Error("how did we not throw an error?"); - const stack = errstack.split("\n").slice(1); const ws = ctx.workerScript; - const caller = ctx.functionPath; - const userstack = []; - for (const stackline of stack) { - const filename = (() => { - // Check urls for dependencies - for (const [url, script] of ws.scriptRef.dependencies) if (stackline.includes(url)) return script.filename; - // Check for filenames directly if no URL found - if (stackline.includes(ws.scriptRef.filename)) return ws.scriptRef.filename; - for (const script of ws.scriptRef.dependencies.values()) { - if (stackline.includes(script.filename)) return script.filename; - } - })(); - if (!filename) continue; - - let call = { line: "-1", func: "unknown" }; - const chromeCall = parseChromeStackline(stackline); - if (chromeCall) { - call = chromeCall; - } - - const firefoxCall = parseFirefoxStackline(stackline); - if (firefoxCall) { - call = firefoxCall; - } - - userstack.push(`${filename}:L${call.line}@${call.func}`); - } - + const stackLines = parseStackTrace(new Error(), ws); log(ctx, () => msg); + const caller = ctx.functionPath; let rejectMsg = `${caller}: ${msg}`; - if (userstack.length !== 0) rejectMsg += `\n\nStack:\n${userstack.join("\n")}`; - return basicErrorMessage(ws, rejectMsg, type); - - interface ILine { - line: string; - func: string; - } - function parseChromeStackline(line: string): ILine | null { - const lineMatch = line.match(/.*:(\d+):\d+.*/); - const funcMatch = line.match(/.*at (.+) \(.*/); - if (lineMatch && funcMatch) return { line: lineMatch[1], func: funcMatch[1] }; - return null; - } - function parseFirefoxStackline(line: string): ILine | null { - const lineMatch = line.match(/.*:(\d+):\d+$/); - const lio = line.lastIndexOf("@"); - if (lineMatch && lio !== -1) return { line: lineMatch[1], func: line.slice(0, lio) }; - return null; + if (stackLines.length !== 0) { + rejectMsg += `\n\nStack: ${stackLines}`; } + return basicErrorMessage(ws, rejectMsg, type); } diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index e343f8f17b..adc7b3d8cc 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -181,6 +181,7 @@ function generateLoadedModule(script: Script, scripts: Map error instanceof ScriptDeath ? "main() terminated." - : getErrorMessageWithStackAndCause(error, "Script crashed due to an error: "), + : getErrorMessageWithStackAndCause(error, "Script crashed due to an error:\n", workerScript), ); }) .finally(() => { diff --git a/src/Script/LoadedModule.ts b/src/Script/LoadedModule.ts index 6c39806300..51f0d4a00c 100644 --- a/src/Script/LoadedModule.ts +++ b/src/Script/LoadedModule.ts @@ -13,9 +13,11 @@ export interface ScriptModule { export class LoadedModule { url: ScriptURL; module: Promise; + sourceMap?: string; - constructor(url: ScriptURL, module: Promise) { + constructor(url: ScriptURL, module: Promise, sourceMap?: string) { this.url = url; this.module = module; + this.sourceMap = sourceMap; } } diff --git a/src/utils/ErrorHandler.ts b/src/utils/ErrorHandler.ts index 540a5953fb..b59d0cbbcc 100644 --- a/src/utils/ErrorHandler.ts +++ b/src/utils/ErrorHandler.ts @@ -12,7 +12,9 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, i } if (ws && typeof e === "string") { const headerText = basicErrorMessage(ws, "", ""); - if (!e.includes(headerText)) e = basicErrorMessage(ws, e); + if (!e.includes(headerText)) { + e = basicErrorMessage(ws, e); + } } else if (e instanceof SyntaxError) { const msg = `${e.message} (sorry we can't be more helpful)`; e = ws ? basicErrorMessage(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`; @@ -24,14 +26,8 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, i if (ws) { console.error(`An error was thrown in your script. Hostname: ${ws.hostname}, script name: ${ws.name}.`); } - /** - * If e is an instance of Error, we print it to the console. This is especially useful when debugging a TypeScript - * script. The stack trace in the error popup contains only the trace of the transpiled code. Even with a source - * map, parsing it to get the relevant info from the original TypeScript file is complicated. The built-in developer - * tool of browsers will do that for us if we print the error to the console. - */ console.error(e); - const msg = getErrorMessageWithStackAndCause(e); + const msg = getErrorMessageWithStackAndCause(e, "", ws); e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`; } if (typeof e !== "string") { diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts index a92ed66175..6bcbd50e19 100644 --- a/src/utils/ErrorHelper.ts +++ b/src/utils/ErrorHelper.ts @@ -3,6 +3,7 @@ import type React from "react"; import type { Page } from "../ui/Router"; import { commitHash } from "./helpers/commitHash"; import { CONSTANTS } from "../Constants"; +import { type LiteWorkerScript, parseStackTrace } from "./StackTraceUtils"; enum GameEnv { Production, @@ -54,7 +55,10 @@ export interface IErrorData { export const newIssueUrl = `https://github.com/bitburner-official/bitburner-src/issues/new`; -export function parseUnknownError(error: unknown): { +export function parseUnknownError( + error: unknown, + ws: LiteWorkerScript | null, +): { errorAsString: string; stack?: string; causeAsString?: string; @@ -65,11 +69,19 @@ export function parseUnknownError(error: unknown): { let causeAsString: string | undefined = undefined; let causeStack: string | undefined = undefined; if (error instanceof Error) { - stack = error.stack; + if (ws) { + stack = parseStackTrace(error, ws); + } else { + stack = error.stack; + } if (error.cause != null) { causeAsString = String(error.cause); if (error.cause instanceof Error) { - causeStack = error.cause.stack; + if (ws) { + causeStack = parseStackTrace(error.cause, ws); + } else { + causeStack = error.cause.stack; + } } } } @@ -81,8 +93,8 @@ export function parseUnknownError(error: unknown): { }; } -export function getErrorMessageWithStackAndCause(error: unknown, prefix = ""): string { - const errorData = parseUnknownError(error); +export function getErrorMessageWithStackAndCause(error: unknown, prefix = "", ws: LiteWorkerScript | null): string { + const errorData = parseUnknownError(error, ws); let errorMessage = `${prefix}${errorData.errorAsString}`; if (errorData.stack) { errorMessage += `\nStack: ${errorData.stack}`; @@ -127,7 +139,7 @@ export function getErrorMetadata(error: unknown, errorInfo?: React.ErrorInfo, pa export function getErrorForDisplay(error: unknown, errorInfo?: React.ErrorInfo, page?: Page): IErrorData { const metadata = getErrorMetadata(error, errorInfo, page); - const errorData = parseUnknownError(error); + const errorData = parseUnknownError(error, null); const fileName = String(metadata.error.fileName); const features = `lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` + diff --git a/src/utils/StackTraceUtils.ts b/src/utils/StackTraceUtils.ts new file mode 100644 index 0000000000..62abea2778 --- /dev/null +++ b/src/utils/StackTraceUtils.ts @@ -0,0 +1,111 @@ +import ErrorStackParser from "error-stack-parser"; +import { type RawSourceMap, SourceMapConsumer } from "source-map-js"; + +/** + * parseStackTrace uses some properties of workerScript, but the dependency chain of WorkerScript is long. In order to + * avoid worsening the dependency chain of parseStackTrace's callers, we create this minimal version of WorkerScript's + * type. It violates the DRY principle, but it's worth the trouble: + * - We rarely change WorkerScript. + * - The entire codebase is riddled with massive dependency chains. We should avoid worsening the situation. + */ +export type LiteWorkerScript = { + hostname: string; + scriptRef: { + dependencies: Map< + unknown, + { + filename: string; + mod: { + sourceMap?: string; + } | null; + } + >; + }; +}; + +/** + * This function parses the stack trace of the error and returns only stack lines in the player's scripts. With + * transformed scripts, it also parses the source map to show the original lines/columns of the original scripts. + * + * For example: This stack: + * + * at errorMessage (webpack://bitburner/./src/Netscript/ErrorMessages.ts?:35:97) + * at Object.getServer (webpack://bitburner/./src/Netscript/NetscriptHelpers.tsx?:420:72) + * at eval (webpack://bitburner/./src/NetscriptFunctions.ts?:889:86) + * at Proxy.wrappedFunction (webpack://bitburner/./src/Netscript/APIWrapper.ts?:67:16) + * at test1 (home/a.ts:10:8) + * at main (home/a.ts:23:5) + * at startNetscript2Script (webpack://bitburner/./src/NetscriptWorker.ts?:91:9) + * + * Becomes: + * + * at test1 (home/a.ts:11:5) + * at main (home/a.ts:26:2) + * + * There are 2 changes: + * - All stack lines pointing to our codebase are stripped. + * - Stack lines show original lines/columns (e.g., a.ts:23:5 -> home/a.ts:26:2). + */ +export function parseStackTrace(error: Error, workerScript: LiteWorkerScript): string { + const stackFrames = ErrorStackParser.parse(error); + const stackLines = [error.message]; + // Cache of found source maps. + const sourceMaps = new Map(); + for (const stackFrame of stackFrames) { + if (!stackFrame.fileName) { + continue; + } + /** + * Filename in the stack line is actually `${hostname}/${filename}`. Check sourceURL in generateLoadedModule + * (src\NetscriptJSEvaluator.ts). + */ + if (!stackFrame.fileName.startsWith(workerScript.hostname)) { + continue; + } + const fileName = stackFrame.fileName.replace(`${workerScript.hostname}/`, ""); + if (!fileName) { + continue; + } + let line = stackFrame.lineNumber; + let column = stackFrame.columnNumber; + if (line !== undefined && column !== undefined) { + let sourceMap = sourceMaps.get(fileName); + // If the source map is not in the cache, we try to find it. + if (!sourceMap) { + /** + * workerScript.scriptRef.dependencies contains directly or indirectly import, including the script itself. + * Check Script in src\Script\Script.ts. + */ + for (const script of workerScript.scriptRef.dependencies.values()) { + // Find the current script in workerScript.scriptRef.dependencies. + if (script.filename !== fileName) { + continue; + } + // Check if sourceMap exists. + if (script.mod?.sourceMap) { + sourceMap = script.mod.sourceMap; + // Put it in the cache. + sourceMaps.set(fileName, sourceMap); + } + break; + } + } + // Parse the source map if it exists. + if (sourceMap) { + /** + * SourceMap is generated by SWC, so we assume that it's valid. Validating it with ajv is unnecessary. If there + * are bugs in SWC or source-map-js, the try-catch block will ensure that the game won't crash. + */ + try { + const sourceMapConsumer = new SourceMapConsumer(JSON.parse(sourceMap) as RawSourceMap); + ({ line, column } = sourceMapConsumer.originalPositionFor({ line, column })); + } catch (errorParsingSourceMap) { + console.error(errorParsingSourceMap); + console.error(`Cannot parse map of ${fileName} in ${workerScript.hostname}. Source map: ${sourceMap}`); + } + } + } + stackLines.push(` at ${stackFrame.functionName} (${workerScript.hostname}/${fileName}:${line}:${column})`); + } + return stackLines.join("\n"); +} From dc5c95e7efdb4921068418c5d56c521b8cd0d1f4 Mon Sep 17 00:00:00 2001 From: CatLover <152669316+catloversg@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:06:20 +0700 Subject: [PATCH 2/3] Fix build error --- src/utils/helpers/exceptionAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers/exceptionAlert.tsx b/src/utils/helpers/exceptionAlert.tsx index 77c7dd2e19..e67ad25dcf 100644 --- a/src/utils/helpers/exceptionAlert.tsx +++ b/src/utils/helpers/exceptionAlert.tsx @@ -18,7 +18,7 @@ const errorSet = new Set(); */ export function exceptionAlert(error: unknown, showOnlyOnce = false): void { console.error(error); - const errorData = parseUnknownError(error); + const errorData = parseUnknownError(error, null); if (showOnlyOnce) { // Calculate the "id" of the error. const errorId = cyrb53(errorData.errorAsString + errorData.stack); From 8312441d9ccd0a1427cfac6bd519b4f9e0987ebb Mon Sep 17 00:00:00 2001 From: CatLover <152669316+catloversg@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:21:52 +0700 Subject: [PATCH 3/3] Rework parseStackTrace --- src/NetscriptJSEvaluator.ts | 3 +- src/Script/LoadedModule.ts | 13 ++- src/utils/ErrorHandler.ts | 29 ++++++- src/utils/StackTraceUtils.ts | 156 +++++++++++++++++++++++++---------- 4 files changed, 152 insertions(+), 49 deletions(-) diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index adc7b3d8cc..f5914af38b 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -181,7 +181,6 @@ function generateLoadedModule(script: Script, scripts: Map; + /** + * URL of the first script containing the exact code of the module. For example, let's say we have test.ts and + * test-clone.ts on "home", and they have the same code. If we run test.ts first, sourceUrl is "home/test.ts". When we + * run test-clone.ts after that, instead of generating another module, we reuse the cache module of test.ts. + * + * Each script instance (src\Script\Script.ts) has a property called "mod". That property is a LoadedModule instance. + * In the previous example, sourceUrl of mod of both test.ts and test-clone.ts are "home/test.ts". + */ + sourceUrl: string; sourceMap?: string; - constructor(url: ScriptURL, module: Promise, sourceMap?: string) { + constructor(url: ScriptURL, module: Promise, sourceUrl: string, sourceMap?: string) { this.url = url; this.module = module; + this.sourceUrl = sourceUrl; this.sourceMap = sourceMap; } } diff --git a/src/utils/ErrorHandler.ts b/src/utils/ErrorHandler.ts index b59d0cbbcc..6b4f15e5d3 100644 --- a/src/utils/ErrorHandler.ts +++ b/src/utils/ErrorHandler.ts @@ -23,11 +23,36 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, i if (e.name === "Canceled" && e.message === "Canceled") { return; } + const msg = getErrorMessageWithStackAndCause(e, "", ws); + /** + * Print the error to console. This is useful when the player wants to check the stack trace after closing the error + * dialog and relevant tail windows. + */ if (ws) { console.error(`An error was thrown in your script. Hostname: ${ws.hostname}, script name: ${ws.name}.`); + /** + * With scripts contain inline source map (e.g., transformed TypeScript/JSX scripts), the built-in developer tool + * of browsers uses the inline source map and show the original lines/columns in the stack trace. However, due to + * how we cache module, filenames in the stack trace may be wrong. For more information, please check + * parseStackTrace in src\utils\StackTraceUtils.ts. + * + * getErrorMessageWithStackAndCause uses parseStackTrace to recover correct debug information (file name, lines, + * columns). + */ + console.error(msg); + } else { + /** + * This happens when there is: + * - Uncaught async error in the player's script. [1] + * - Uncaught async error in our codebase or our dependencies. [2] + * + * In both cases, we don't have access to a WorkerScript instance, so printing the error as-is is fine. Doing that + * is also useful for [2]. Occasionally, our dependencies have bugs (especially monaco), so having the stack trace + * is good for debugging. + */ + console.error(e); + console.error("check this", msg); } - console.error(e); - const msg = getErrorMessageWithStackAndCause(e, "", ws); e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`; } if (typeof e !== "string") { diff --git a/src/utils/StackTraceUtils.ts b/src/utils/StackTraceUtils.ts index 62abea2778..21699b5a2f 100644 --- a/src/utils/StackTraceUtils.ts +++ b/src/utils/StackTraceUtils.ts @@ -3,8 +3,8 @@ import { type RawSourceMap, SourceMapConsumer } from "source-map-js"; /** * parseStackTrace uses some properties of workerScript, but the dependency chain of WorkerScript is long. In order to - * avoid worsening the dependency chain of parseStackTrace's callers, we create this minimal version of WorkerScript's - * type. It violates the DRY principle, but it's worth the trouble: + * avoid worsening the dependency chain of parseStackTrace's callers, we use this minimal version of WorkerScript's type + * instead of importing WorkerScript. It violates the DRY principle, but it's worth the trouble: * - We rarely change WorkerScript. * - The entire codebase is riddled with massive dependency chains. We should avoid worsening the situation. */ @@ -16,6 +16,7 @@ export type LiteWorkerScript = { { filename: string; mod: { + sourceUrl: string; sourceMap?: string; } | null; } @@ -49,60 +50,127 @@ export type LiteWorkerScript = { export function parseStackTrace(error: Error, workerScript: LiteWorkerScript): string { const stackFrames = ErrorStackParser.parse(error); const stackLines = [error.message]; - // Cache of found source maps. - const sourceMaps = new Map(); + const cache = new Map(); for (const stackFrame of stackFrames) { if (!stackFrame.fileName) { continue; } /** - * Filename in the stack line is actually `${hostname}/${filename}`. Check sourceURL in generateLoadedModule - * (src\NetscriptJSEvaluator.ts). + * Due to how we cache modules, fileName in stackFrame may be wrong. Let's say that we have test.ts and + * test-clone.ts. They have the same code: + * + * export async function main(ns: NS) { + * throw new Error("test error"); + * } + * + * If we run test.ts first, then run test-clone.ts, they generate the same error stack trace: + * + * Error: test error + * at main (test.ts:2:9) + * at startNetscript2Script (NetscriptWorker.ts:91:9) + * + * Even when we run test-clone.ts, fileName in stackFrame still points to test.ts. In order to solve this problem, + * we loop through workerScript.scriptRef.dependencies and find the correct script. This property contains directly + * or indirectly imports, including the script itself. In the previous example: + * + * test.ts: stackFrame.fileName is test.ts. workerScript.scriptRef.dependencies contains a Script instance: + * - filename: "test.ts" + * - mod: { + * sourceUrl: "home/test.ts" + * } + * + * test-clone.ts: stackFrame.fileName is test.ts. workerScript.scriptRef.dependencies contains a Script instance: + * - filename: "test-clone.ts" + * - mod.sourceUrl: "home/test.ts" + * + * Both sourceUrl point to "home/test.ts", but filename is the correct value. + * + * Note that this solution still fails to find the correct filename in edge cases. For example: + * + * lib-edge-1.ts and lib-edge-2.ts with same code: + * + * export function test1(): void { + * } + * export function test2(): void { + * throw new Error("test error test2"); + * } + * export async function main(ns: NS) { + * ns.print("lib-edge"); + * } + * + * b.ts: + * + * import { test1 } from "./lib-edge-1"; + * import { test2 } from "./lib-edge-2"; + * export async function main(ns: NS) { + * test1(); + * test2(); + * } + * + * Run b.ts: + * at test2 (home/lib-edge-1.ts:4:8) -> Wrong filename + * at main (home/b.ts:5:2) + * + * When we run b.ts, "dependencies" contains: + * - Script 1: + * - filename: "lib-edge-1.ts" + * - mod.sourceUrl: "home/lib-edge-1.ts" + * - Script 2: + * - filename: "b.ts" + * - mod.sourceUrl: "home/b.ts" + * + * b.ts imports both lib-edge-1.ts and lib-edge-2.ts, but they have the same code, so "dependencies" only contains + * the module of lib-edge-1.ts and b.ts. + * + * Without changing how we cache modules, we don't have enough information to perfectly deduce the correct filename + * in all cases, unless we perform AST analysis in this function. It only affects edge cases, so we can accept it as + * a known limitation. */ - if (!stackFrame.fileName.startsWith(workerScript.hostname)) { - continue; + let fileName; + let sourceMap; + const cachedValue = cache.get(stackFrame.fileName); + if (!cachedValue) { + // Find correct fileName. + for (const script of workerScript.scriptRef.dependencies.values()) { + if (script.mod === null) { + continue; + } + // console.log("scrip", script); + if (script.mod.sourceUrl !== stackFrame.fileName) { + continue; + } + fileName = script.filename; + sourceMap = script.mod.sourceMap; + // Put it in the cache. + cache.set(stackFrame.fileName, { fileName, sourceMap }); + break; + } + } else { + // Reuse cached value. + fileName = cachedValue.fileName; + sourceMap = cachedValue.sourceMap; } - const fileName = stackFrame.fileName.replace(`${workerScript.hostname}/`, ""); + + // This only happens when the current stackFrame points to our codebase. if (!fileName) { + console.warn(stackFrame.fileName); continue; } + let line = stackFrame.lineNumber; let column = stackFrame.columnNumber; - if (line !== undefined && column !== undefined) { - let sourceMap = sourceMaps.get(fileName); - // If the source map is not in the cache, we try to find it. - if (!sourceMap) { - /** - * workerScript.scriptRef.dependencies contains directly or indirectly import, including the script itself. - * Check Script in src\Script\Script.ts. - */ - for (const script of workerScript.scriptRef.dependencies.values()) { - // Find the current script in workerScript.scriptRef.dependencies. - if (script.filename !== fileName) { - continue; - } - // Check if sourceMap exists. - if (script.mod?.sourceMap) { - sourceMap = script.mod.sourceMap; - // Put it in the cache. - sourceMaps.set(fileName, sourceMap); - } - break; - } - } - // Parse the source map if it exists. - if (sourceMap) { - /** - * SourceMap is generated by SWC, so we assume that it's valid. Validating it with ajv is unnecessary. If there - * are bugs in SWC or source-map-js, the try-catch block will ensure that the game won't crash. - */ - try { - const sourceMapConsumer = new SourceMapConsumer(JSON.parse(sourceMap) as RawSourceMap); - ({ line, column } = sourceMapConsumer.originalPositionFor({ line, column })); - } catch (errorParsingSourceMap) { - console.error(errorParsingSourceMap); - console.error(`Cannot parse map of ${fileName} in ${workerScript.hostname}. Source map: ${sourceMap}`); - } + if (line !== undefined && column !== undefined && sourceMap !== undefined) { + // console.log("stackFrame", stackFrame); + /** + * SourceMap is generated by SWC, so we assume that it's valid. Validating it with ajv is unnecessary. If there + * are bugs in SWC or source-map-js, the try-catch block will ensure that the game won't crash. + */ + try { + const sourceMapConsumer = new SourceMapConsumer(JSON.parse(sourceMap) as RawSourceMap); + ({ line, column } = sourceMapConsumer.originalPositionFor({ line, column })); + } catch (errorParsingSourceMap) { + console.error(errorParsingSourceMap); + console.error(`Cannot parse map of ${fileName} in ${workerScript.hostname}. Source map: ${sourceMap}`); } } stackLines.push(` at ${stackFrame.functionName} (${workerScript.hostname}/${fileName}:${line}:${column})`);