import { SourceMapConsumer, RawSourceMap } from "source-map"
import { htmlEscape } from "../client-abuse/linkTo"
import { IS_SIM } from "./constants"
/**
* Try-catch wrapper which print original source stack using source map.
* @param fn Function to call
* @param minimumBucket Skip source mapper if bucket is too low (default: 100)
* @returns Wrapper function
*/
export function wrapErrorMapper(fn: () => void, minimumBucket = 100): () => void {
return () => {
try {
fn()
} catch (e) {
if (e instanceof Error) {
if (IS_SIM) {
consoleError(
"Source maps don't work in the simulator - displaying original error\n" +
(e.stack ?? "")
)
} else if (Game.cpu.bucket < minimumBucket) {
consoleError("No enough cpu to map error\n" + (e.stack ?? ""))
} else {
consoleError(getSourceMappedStackTrace(e))
}
} else {
// can't handle it
consoleError("Bad error: " + (e as string))
throw e
}
}
}
}
const consoleError = (s: string) =>
console.log(`${htmlEscape(s).replace("\n", "
")}`)
// Cache source-map to improve performance
let consumer: SourceMapConsumer | undefined
// Cache previously mapped traces to improve performance
const cache: Record = {}
/**
* Generates a stack trace using a source map generate original symbol names.
*
* WARNING - EXTREMELY high CPU cost for first call after reset - >30 CPU! Use sparingly!
* (Consecutive calls after a reset are more reasonable, ~0.1 CPU/ea)
* @param error The error or original stack trace
* @returns The source-mapped stack trace string
*/
export function getSourceMappedStackTrace(error: Error | string): string {
const stack: string = error instanceof Error ? error.stack ?? "" : error
if (Object.prototype.hasOwnProperty.call(cache, stack)) {
return cache[stack]
}
// eslint-disable-next-line no-useless-escape
const re = /^\s+at\s+(.+?\s+)?\(?([0-z._\-\\\/]+):(\d+):(\d+)\)?$/gm
let match: RegExpExecArray | null
let outStack = error.toString()
while ((match = re.exec(stack))) {
if (match[2] !== "main" && match[2] !== "main.js") break // no more parseable lines
// eslint-disable-next-line @typescript-eslint/no-var-requires
consumer ??= new SourceMapConsumer(require("main.js.map") as RawSourceMap)
const pos = consumer.originalPositionFor({
column: parseInt(match[4], 10),
line: parseInt(match[3], 10),
})
if ((pos.line as number | undefined) == undefined) break // no known position
if (pos.name) {
outStack += `\n at ${pos.name} (${pos.source}:${pos.line}:${pos.column})`
} else {
if (match[1]) {
// no original source file name known - use file name from given trace
outStack += `\n at ${match[1]} (${pos.source}:${pos.line}:${pos.column})`
} else {
// no original source file name known or in given trace - omit name
outStack += `\n at ${pos.source}:${pos.line}:${pos.column}`
}
}
}
cache[stack] = outStack
return outStack
}