diff options
Diffstat (limited to 'packages/bun-error/index.tsx')
-rw-r--r-- | packages/bun-error/index.tsx | 605 |
1 files changed, 197 insertions, 408 deletions
diff --git a/packages/bun-error/index.tsx b/packages/bun-error/index.tsx index de9484648..9b367c2ef 100644 --- a/packages/bun-error/index.tsx +++ b/packages/bun-error/index.tsx @@ -12,8 +12,14 @@ import type { StackFrame, WebsocketMessageBuildFailure, } from "../../src/api/schema"; - -enum StackFrameScope { +import { + messagesToMarkdown, + problemsToMarkdown, + withBunInfo, +} from "./markdown"; +import { fetchMappings, remapPosition, sourceMappings } from "./sourcemap"; + +export enum StackFrameScope { Eval = 1, Module = 2, Function = 3, @@ -22,7 +28,7 @@ enum StackFrameScope { Constructor = 6, } -enum JSErrorCode { +export enum JSErrorCode { Error = 0, EvalError = 1, RangeError = 2, @@ -90,20 +96,55 @@ const errorTags = [ <ErrorTag type={ErrorTagType.hmr}></ErrorTag>, ]; -const normalizedFilename = (filename: string, cwd: string): string => { +function getAssetPrefixPath() { + return globalThis["__BUN_HMR"]?.assetPrefixPath || ""; +} + +export const normalizedFilename = (filename: string, cwd: string): string => { + if (filename.startsWith("http://") || filename.startsWith("https://")) { + const url = new URL(filename, globalThis.location.href); + if (url.origin === globalThis.location.origin) { + filename = url.pathname; + } + } + + var blobI = filename.indexOf("/blob:"); + if (blobI > -1) { + filename = filename.substring(blobI + "/blob:".length); + } + + const assetPrefixPath = getAssetPrefixPath(); + if (cwd && filename.startsWith(cwd)) { - return filename.substring(cwd.length); + filename = filename.substring(cwd.length); + + if (assetPrefixPath.length > 0 && filename.startsWith(assetPrefixPath)) { + return filename.substring(assetPrefixPath.length); + } + } + + if (assetPrefixPath.length > 0 && filename.startsWith(assetPrefixPath)) { + return filename.substring(assetPrefixPath.length); } return filename; }; -const blobFileURL = ( - filename: string, +function hasColumnOrLine(filename: string) { + return /:\d+/.test(filename); +} + +function appendLineColumnIfNeeded( + base: string, line?: number, column?: number -): string => { - var base = `/blob:${filename}`; +) { + if (hasColumnOrLine(base)) return base; + + return appendLineColumn(base, line, column); +} + +function appendLineColumn(base: string, line?: number, column?: number) { if (Number.isFinite(line)) { base += `:${line}`; @@ -112,7 +153,35 @@ const blobFileURL = ( } } - return new URL(base, location.href).href; + return base; +} + +const blobFileURL = ( + filename: string, + line?: number, + column?: number +): string => { + var base = `/blob:${filename}`; + + base = appendLineColumnIfNeeded(base, line, column); + + return new URL(base, globalThis.location.href).href; +}; + +const maybeBlobFileURL = ( + filename: string, + line?: number, + column?: number +): string => { + if (filename.includes(".bun")) { + return blobFileURL(filename, line, column); + } + + if (filename.includes("blob:")) { + return appendLineColumnIfNeeded(filename, line, column); + } + + return srcFileURL(filename, line, column); }; const openWithoutFlashOfNewTab = (event: MouseEvent) => { @@ -151,21 +220,22 @@ const openWithoutFlashOfNewTab = (event: MouseEvent) => { return false; }; -const srcFileURL = (filename: string, line: number, column: number): string => { +const srcFileURL = ( + filename: string, + line?: number, + column?: number +): string => { + if (filename.startsWith("http://") || filename.startsWith("https://")) + return appendLineColumnIfNeeded(filename); + if (filename.endsWith(".bun")) { - return new URL("/" + filename, location.href).href; + return new URL("/" + filename, globalThis.location.href).href; } var base = `/src:${filename}`; - if (Number.isFinite(line) && line > -1) { - base = base + `:${line}`; + base = appendLineColumnIfNeeded(base, line, column); - if (Number.isFinite(column) && column > -1) { - base = base + `:${column}`; - } - } - - return new URL(base, location.href).href; + return new URL(base, globalThis.location.href).href; }; class FancyTypeError { @@ -240,121 +310,14 @@ class FancyTypeError { var onClose = dismissError; -const clientURL = (filename) => { - return `/${filename.replace(/^(\/)?/g, "")}`; -}; - -function bunInfoToMarkdown(_info) { - if (!_info) return; - const info = { ..._info, platform: { ..._info.platform } }; - - var operatingSystemVersion = info.platform.version; - - if (info.platform.os.toLowerCase() === "macos") { - const [major, minor, patch] = operatingSystemVersion.split("."); - switch (major) { - case "22": { - operatingSystemVersion = `13.${minor}.${patch}`; - break; - } - case "21": { - operatingSystemVersion = `12.${minor}.${patch}`; - break; - } - case "20": { - operatingSystemVersion = `11.${minor}.${patch}`; - break; - } - - case "19": { - operatingSystemVersion = `10.15.${patch}`; - break; - } - - case "18": { - operatingSystemVersion = `10.14.${patch}`; - break; - } - - case "17": { - operatingSystemVersion = `10.13.${patch}`; - break; - } - - case "16": { - operatingSystemVersion = `10.12.${patch}`; - break; - } - - case "15": { - operatingSystemVersion = `10.11.${patch}`; - break; - } - } - info.platform.os = "macOS"; - } - - if (info.platform.arch === "arm" && info.platform.os === "macOS") { - info.platform.arch = "Apple Silicon"; - } else if (info.platform.arch === "arm") { - info.platform.arch = "aarch64"; - } - - var base = `Info: -> bun v${info.bun_version} -`; - - if (info.framework && info.framework_version) { - base += `> framework: ${info.framework}@${info.framework_version}`; - } else if (info.framework) { - base += `> framework: ${info.framework}`; - } - - base = - base.trim() + - ` -> ${info.platform.os} ${operatingSystemVersion} (${info.platform.arch}) -> User-Agent: ${navigator.userAgent} -> Pathname: ${location.pathname} -`; - - return base; -} - -var bunInfoMemoized; -function getBunInfo() { - if (bunInfoMemoized) return bunInfoMemoized; - if ("sessionStorage" in globalThis) { - try { - const bunInfoMemoizedString = sessionStorage.getItem("__bunInfo"); - if (bunInfoMemoizedString) { - bunInfoMemoized = JSON.parse(bunInfoMemoized); - return bunInfoMemoized; - } - } catch (exception) {} +export const clientURL = (filename) => { + if (filename.includes(".bun")) { + return `/${filename.replace(/^(\/)?/g, "")}`; } - const controller = new AbortController(); - const timeout = 1000; - const id = setTimeout(() => controller.abort(), timeout); - return fetch("/bun:info", { - signal: controller.signal, - headers: { - Accept: "application/json", - }, - }) - .then((resp) => resp.json()) - .then((bunInfo) => { - clearTimeout(id); - bunInfoMemoized = bunInfo; - if ("sessionStorage" in globalThis) { - try { - sessionStorage.setItem("__bunInfo", JSON.stringify(bunInfo)); - } catch (exception) {} - } - return bunInfo; - }); -} + // Since bun has source maps now, we assume that it will we are dealing with a src url + return srcFileURL(filename); +}; const IndentationContext = createContext(0); @@ -539,7 +502,7 @@ const SourceLines = ({ const { line, text } = _sourceLines[i]; const classes = { empty: text.trim().length === 0, - highlight: highlight + +!isClient === line || _sourceLines.length === 1, + highlight: highlight === line, }; if (classes.highlight) highlightI = i; const _text = classes.empty ? "" : text.substring(dedent); @@ -569,20 +532,7 @@ const SourceLines = ({ classes.empty ? "BunError-SourceLine-text--empty" : "" } ${classes.highlight ? "BunError-SourceLine-text--highlight" : ""}`} > - {classes.highlight ? ( - <> - {_text.substring(0, highlightColumnStart - dedent)} - <span id="BunError-SourceLine-text-highlightExpression"> - {_text.substring( - highlightColumnStart - dedent, - highlightColumnEnd - dedent - )} - </span> - {_text.substring(highlightColumnEnd - dedent)} - </> - ) : ( - _text - )} + {_text} </div> </div> ); @@ -644,7 +594,7 @@ const BuildErrorStackTrace = ({ location }: { location: Location }) => { ); }; -const StackFrameIdentifier = ({ +export const StackFrameIdentifier = ({ functionName, scope, markdown = false, @@ -800,7 +750,7 @@ const NativeStackTrace = ({ const { file = "", position } = frames[0]; const { cwd } = useContext(ErrorGroupContext); const filename = normalizedFilename(file, cwd); - const urlBuilder = isClient ? clientURL : blobFileURL; + const urlBuilder = isClient ? clientURL : maybeBlobFileURL; // const [isFocused, setFocused] = React.useState(false); const ref = React.useRef<HTMLDivElement>(); const buildURL = React.useCallback( @@ -867,7 +817,7 @@ const NativeStackTrace = ({ {children} </SourceLines> )} - {sourceLines.length === 0 && isClient && ( + {sourceLines.length === 0 && ( <AsyncSourceLines highlight={position.line} sourceLines={sourceLines} @@ -875,7 +825,7 @@ const NativeStackTrace = ({ highlightColumnStart={position.column_start} buildURL={buildURL} highlightColumnEnd={position.column_stop} - isClient + isClient={isClient} > {children} </AsyncSourceLines> @@ -887,18 +837,6 @@ const NativeStackTrace = ({ ); }; -const divet = <span className="BunError-divet">^</span>; -const DivetRange = ({ start, stop }) => { - const length = Math.max(stop - start, 0); - if (length === 0) return null; - return ( - <span - className="BunError-DivetRange" - style={{ width: `${length - 1}ch` }} - ></span> - ); -}; - const Indent = ({ by, children }) => { const amount = useContext(IndentationContext); return ( @@ -1175,19 +1113,6 @@ const OverlayMessageContainer = ({ ); }; -function problemsToMarkdown(problems: Problems) { - var markdown = ""; - if (problems?.build?.msgs?.length) { - markdown += messagesToMarkdown(problems.build.msgs); - } - - if (problems?.exceptions?.length) { - markdown += exceptionsToMarkdown(problems.exceptions); - } - - return markdown; -} - // we can ignore the synchronous copy to clipboard API...I think function copyToClipboard(input: string | Promise<string>) { if (!input) return; @@ -1199,231 +1124,6 @@ function copyToClipboard(input: string | Promise<string>) { return navigator.clipboard.writeText(input).then(() => {}); } -function messagesToMarkdown(messages: Message[]): string { - return messages - .map(messageToMarkdown) - .map((a) => a.trim()) - .join("\n"); -} - -function exceptionsToMarkdown(exceptions: JSExceptionType[]): string { - return exceptions - .map(exceptionToMarkdown) - .map((a) => a.trim()) - .join("\n"); -} - -function exceptionToMarkdown(exception: JSException): string { - const { name, message, stack } = exception; - - let markdown = ""; - - if (name === "Error" || name === "RangeError" || name === "TypeError") { - markdown += `**${message}**\n`; - } else { - markdown += `**${name}**\n ${message}\n`; - } - - if (stack.frames.length > 0) { - var frames = stack.frames; - if (stack.source_lines.length > 0) { - const { - file = "", - function_name = "", - position: { - line = -1, - column_start: column = -1, - column_stop: columnEnd = column, - } = { - line: -1, - column_start: -1, - column_stop: -1, - }, - scope = 0, - } = stack.frames[0]; - - if (file) { - if (function_name.length > 0) { - markdown += `In \`${function_name}\` – ${file}`; - } else if (scope > 0 && scope < StackFrameScope.Constructor + 1) { - markdown += `${StackFrameIdentifier({ - functionName: function_name, - scope, - markdown: true, - })} ${file}`; - } else { - markdown += `In ${file}`; - } - - if (line > -1) { - markdown += `:${line}`; - if (column > -1) { - markdown += `:${column}`; - } - } - - if (stack.source_lines.length > 0) { - // TODO: include loader - const extnameI = file.lastIndexOf("."); - const extname = extnameI > -1 ? file.slice(extnameI + 1) : ""; - - markdown += "\n```"; - markdown += extname; - markdown += "\n"; - stack.source_lines.forEach((sourceLine) => { - markdown += sourceLine.text + "\n"; - if (sourceLine.line === line && stack.source_lines.length > 1) { - markdown += - ("/* " + "^".repeat(Math.max(columnEnd - column, 1))).padStart( - Math.max(column + 2, 0) - ) + " happened here */\n"; - } - }); - markdown += "\n```"; - } - } - } - - if (frames.length > 0) { - markdown += "\nStack trace:\n"; - var padding = 0; - for (let frame of frames) { - const { - function_name = "", - position: { line = -1, column_start: column = -1 } = { - line: -1, - column_start: -1, - }, - scope = 0, - } = frame; - padding = Math.max( - padding, - StackFrameIdentifier({ - scope, - functionName: function_name, - markdown: true, - }).length - ); - } - - markdown += "```js\n"; - - for (let frame of frames) { - const { - file = "", - function_name = "", - position: { line = -1, column_start: column = -1 } = { - line: -1, - column_start: -1, - }, - scope = 0, - } = frame; - - markdown += ` -${StackFrameIdentifier({ - scope, - functionName: function_name, - markdown: true, -}).padEnd(padding, " ")}`; - - if (file) { - markdown += ` ${file}`; - if (line > -1) { - markdown += `:${line}`; - if (column > -1) { - markdown += `:${column}`; - } - } - } - } - - markdown += "\n```\n"; - } - } - - return markdown; -} - -function messageToMarkdown(message: Message): string { - var tag = "Error"; - if (message.on.build) { - tag = "BuildError"; - } - var lines = (message.data.text ?? "").split("\n"); - - var markdown = ""; - if (message?.on?.resolve) { - markdown += `**ResolveError**: "${message.on.resolve}" failed to resolve\n`; - } else { - var firstLine = lines[0]; - lines = lines.slice(1); - if (firstLine.length > 120) { - const words = firstLine.split(" "); - var end = 0; - for (let i = 0; i < words.length; i++) { - if (end + words[i].length >= 120) { - firstLine = words.slice(0, i).join(" "); - lines.unshift(words.slice(i).join(" ")); - break; - } - } - } - - markdown += `**${tag}**${firstLine.length > 0 ? ": " + firstLine : ""}\n`; - } - - if (message.data?.location?.file) { - markdown += `In ${normalizedFilename(message.data.location.file, thisCwd)}`; - if (message.data.location.line > -1) { - markdown += `:${message.data.location.line}`; - if (message.data.location.column > -1) { - markdown += `:${message.data.location.column}`; - } - } - - if (message.data.location.line_text.length) { - const extnameI = message.data.location.file.lastIndexOf("."); - const extname = - extnameI > -1 ? message.data.location.file.slice(extnameI + 1) : ""; - - markdown += - "\n```" + extname + "\n" + message.data.location.line_text + "\n```\n"; - } else { - markdown += "\n"; - } - - if (lines.length > 0) { - markdown += lines.join("\n"); - } - } - - return markdown; -} - -const withBunInfo = (text) => { - const bunInfo = getBunInfo(); - - const trimmed = text.trim(); - - if (bunInfo && "then" in bunInfo) { - return bunInfo.then( - (info) => { - const markdown = bunInfoToMarkdown(info).trim(); - return trimmed + "\n" + markdown + "\n"; - }, - () => trimmed + "\n" - ); - } - - if (bunInfo) { - const markdown = bunInfoToMarkdown(bunInfo).trim(); - - return trimmed + "\n" + markdown + "\n"; - } - - return trimmed + "\n"; -}; - const Footer = ({ toMarkdown, data }) => ( <div className="BunError-footer"> <div @@ -1468,7 +1168,7 @@ const BuildFailureMessageContainer = ({ </div> ); }; -var thisCwd = ""; +export var thisCwd = ""; const ErrorGroupContext = createContext<{ cwd: string }>(null); var reactRoot; @@ -1513,8 +1213,15 @@ export function renderFallbackError(fallback: FallbackMessageContainer) { } import { parse as getStackTrace } from "./stack-trace-parser"; +var runtimeErrorController: AbortController; +var pending = []; +var onIdle = globalThis.requestIdleCallback || ((cb) => setTimeout(cb, 32)); +function clearSourceMappings() { + sourceMappings.clear(); +} export function renderRuntimeError(error: Error) { + runtimeErrorController = new AbortController(); if (typeof error === "string") { error = { name: "Error", @@ -1580,6 +1287,7 @@ export function renderRuntimeError(error: Error) { } } } + const signal = runtimeErrorController.signal; const fallback: FallbackMessageContainer = { message: error.message, @@ -1595,11 +1303,86 @@ export function renderRuntimeError(error: Error) { exceptions: [exception], }, }; - return renderWithFunc(() => ( - <ErrorGroupContext.Provider value={fallback}> - <OverlayMessageContainer isClient {...fallback} /> - </ErrorGroupContext.Provider> - )); + + var stopThis = { stopped: false }; + pending.push(stopThis); + + const BunError = () => { + return ( + <ErrorGroupContext.Provider value={fallback}> + <OverlayMessageContainer isClient {...fallback} /> + </ErrorGroupContext.Provider> + ); + }; + + // Remap the sourcemaps + // But! If we've already fetched the source mappings in this page load before + // Rely on the cached ones + // and don't fetch them again + const framePromises = exception.stack.frames + .map((frame, i) => { + if (stopThis.stopped) return null; + return [ + fetchMappings(normalizedFilename(frame.file, thisCwd), signal), + i, + ]; + }) + .map((result) => { + if (!result) return; + const [mappings, frameIndex] = result; + if (mappings?.then) { + return mappings.then((mappings) => { + if (!mappings || stopThis.stopped) { + return null; + } + var frame = exception.stack.frames[frameIndex]; + + const { line, column_start } = frame.position; + const remapped = remapPosition(mappings, line, column_start); + if (!remapped) return null; + frame.position.line_start = frame.position.line = remapped[0]; + frame.position.column_stop = + frame.position.expression_stop = + frame.position.expression_start = + frame.position.column_start = + remapped[1]; + }, console.error); + } else { + var frame = exception.stack.frames[frameIndex]; + const { line, column_start } = frame.position; + const remapped = remapPosition(mappings, line, column_start); + if (!remapped) return null; + frame.position.line_start = frame.position.line = remapped[0]; + frame.position.column_stop = + frame.position.expression_stop = + frame.position.expression_start = + frame.position.column_start = + remapped[1]; + } + }); + + var anyPromises = false; + for (let i = 0; i < framePromises.length; i++) { + if (framePromises[i] && framePromises[i].then) { + anyPromises = true; + break; + } + } + + if (anyPromises) { + Promise.allSettled(framePromises).finally(() => { + if (stopThis.stopped || signal.aborted) return; + onIdle(clearSourceMappings); + return renderWithFunc(() => { + return <BunError />; + }); + }); + } else { + onIdle(clearSourceMappings); + renderWithFunc(() => { + return <BunError />; + }); + } } export function dismissError() { @@ -1608,6 +1391,12 @@ export function dismissError() { const root = document.getElementById("__bun__error-root"); if (root) root.remove(); reactRoot = null; + if (runtimeErrorController) { + runtimeErrorController.abort(); + runtimeErrorController = null; + } + + while (pending.length > 0) pending.shift().stopThis = true; } } |