import React from "react"; import { useContext, createContext } from "react"; import { render, unmountComponentAtNode } from "react-dom"; import type { FallbackMessageContainer, JSException, JSException as JSExceptionType, Location, Message, SourceLine, StackFrame, WebsocketMessageBuildFailure, } from "../../src/api/schema"; import { messagesToMarkdown, problemsToMarkdown, withBunInfo, } from "./markdown"; import { fetchAllMappings, remapPosition, sourceMappings } from "./sourcemap"; export enum StackFrameScope { Eval = 1, Module = 2, Function = 3, Global = 4, Wasm = 5, Constructor = 6, } export enum JSErrorCode { Error = 0, EvalError = 1, RangeError = 2, ReferenceError = 3, SyntaxError = 4, TypeError = 5, URIError = 6, AggregateError = 7, // StackOverflow & OutOfMemoryError is not an ErrorType in within JSC, so the number here is just totally made up OutOfMemoryError = 8, BundlerError = 252, StackOverflow = 253, UserErrorCode = 254, } const JSErrorCodeLabel = { 0: "Error", 1: "EvalError", 2: "RangeError", 3: "ReferenceError", 4: "SyntaxError", 5: "TypeError", 6: "URIError", 7: "AggregateError", 253: "StackOverflow", 8: "OutOfMemory", }; const BUN_ERROR_CONTAINER_ID = "__bun-error__"; enum RuntimeType { Nothing = 0x0, Function = 0x1, Undefined = 0x2, Null = 0x4, Boolean = 0x8, AnyInt = 0x10, Number = 0x20, String = 0x40, Object = 0x80, Symbol = 0x100, BigInt = 0x200, } enum ErrorTagType { build, resolve, server, client, hmr, } const ErrorTag = ({ type }: { type: ErrorTagType }) => (
{ErrorTagType[type]}
); const errorTags = [ , , , , , ]; 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)) { 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; }; function hasColumnOrLine(filename: string) { return /:\d+/.test(filename); } function appendLineColumnIfNeeded( base: string, line?: number, column?: number, ) { if (hasColumnOrLine(base)) return base; return appendLineColumn(base, line, column); } function appendLineColumn(base: string, line?: number, column?: number) { if (Number.isFinite(line)) { base += `:${line}`; if (Number.isFinite(column)) { base += `:${column}`; } } 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: React.MouseEventHandler = ( event, ) => { const target = event.currentTarget; const href = target.getAttribute("href"); if (!href || event.button !== 0) { return true; } event.preventDefault(); event.nativeEvent.preventDefault(); event.nativeEvent.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); const headers = new Headers(); headers.set("Accept", "text/plain"); if (target.dataset.line) { headers.set("Editor-Line", target.dataset.line); } if (target.dataset.column) { headers.set("Editor-Column", target.dataset.column); } headers.set("Open-In-Editor", "1"); globalThis .fetch(href.split("?")[0], { headers: headers, }) .then( () => {}, (er) => {}, ); return false; }; 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, globalThis.location.href).href; } if (!filename.startsWith("/") && thisCwd) { var orig = filename; filename = thisCwd; if (thisCwd.endsWith("/")) { filename += orig; } else { filename += "/" + orig; } } var base = `/src:${filename}`; base = appendLineColumnIfNeeded(base, line, column); return new URL(base, globalThis.location.href).href; }; class FancyTypeError { constructor(exception: JSException) { this.runtimeType = exception.runtime_type || 0; this.runtimeTypeName = RuntimeType[this.runtimeType] || "undefined"; this.message = exception.message || ""; this.explain = ""; this.normalize(exception); } runtimeType: RuntimeType; explain: string; runtimeTypeName: string; message: string; normalize(exception: JSException) { if (!exception.message) return; const i = exception.message.lastIndexOf(" is "); if (i === -1) return; const partial = exception.message.substring(i + " is ".length); const nextWord = /(["a-zA-Z0-9_\.]+)\)$/.exec(partial); if (nextWord && nextWord[0]) { this.runtimeTypeName = nextWord[0]; this.runtimeTypeName = this.runtimeTypeName.substring( 0, this.runtimeTypeName.length - 1, ); switch (this.runtimeTypeName.toLowerCase()) { case "undefined": { this.runtimeType = RuntimeType.Undefined; break; } case "null": { this.runtimeType = RuntimeType.Null; break; } case "string": { this.runtimeType = RuntimeType.String; break; } case "true": case "false": { this.runtimeType = RuntimeType.Boolean; break; } case "number": this.runtimeType = RuntimeType.Number; break; case "bigint": this.runtimeType = RuntimeType.BigInt; break; case "symbol": this.runtimeType = RuntimeType.Symbol; break; default: { this.runtimeType = RuntimeType.Object; break; } } this.message = exception.message.substring(0, i); this.message = this.message.substring( 0, this.message.lastIndexOf("(In "), ); } } } export const clientURL = (filename) => { if (filename.includes(".bun")) { return `/${filename.replace(/^(\/)?/g, "")}`; } // 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); enum LoadState { pending, loaded, failed, } const AsyncSourceLines = ({ highlight = -1, highlightColumnStart = 0, highlightColumnEnd = Infinity, children, buildURL, sourceLines, setSourceLines, }: { highlight: number; highlightColumnStart: number; highlightColumnEnd: number; children?: React.ReactNode; buildURL: (line?: number, column?: number) => string; sourceLines: SourceLine[]; setSourceLines: (lines: SourceLine[]) => void; }) => { const [loadState, setLoadState] = React.useState(LoadState.pending); const controller = React.useRef(null); const url = React.useRef(buildURL(0, 0)); React.useEffect(() => { controller.current = new AbortController(); var cancelled = false; fetch(url.current, { signal: controller.current.signal, headers: { Accept: "text/plain", }, }) .then((resp) => { return resp.text(); }) .then((text) => { if (cancelled) return; // TODO: make this faster const lines = text.split("\n"); const startLineNumber = Math.max( Math.min(Math.max(highlight - 4, 0), lines.length - 1), 0, ); const endLineNumber = Math.min(startLineNumber + 8, lines.length); const sourceLines: SourceLine[] = new Array( endLineNumber - startLineNumber, ); var index = 0; for (let i = startLineNumber; i < endLineNumber; i++) { const currentLine = lines[i - 1]; if (typeof currentLine === "undefined") break; sourceLines[index++] = { line: i, text: currentLine, }; } setSourceLines( index !== sourceLines.length ? sourceLines.slice(0, index) : sourceLines, ); setLoadState(LoadState.loaded); }) .catch((err) => { if (!cancelled) { console.error(err); setLoadState(LoadState.failed); } }); return () => { cancelled = true; if (controller.current) { controller.current.abort(); controller.current = null; } }; }, [controller, setLoadState, setSourceLines, url, highlight]); switch (loadState) { case LoadState.pending: { return (
); } case LoadState.failed: { return null; } case LoadState.loaded: { return ( {children} ); } default: { throw new Error("Invalid state"); } } }; const SourceLines = ({ sourceLines, highlight = -1, highlightColumnStart = 0, highlightColumnEnd = Infinity, children, buildURL, }: { sourceLines: SourceLine[]; highlight: number; highlightColumnStart: number; highlightColumnEnd: number; children?: React.ReactNode; buildURL: (line?: number, column?: number) => string; }) => { let start = sourceLines.length; let end = 0; let dedent = Infinity; let _i = 0; var minLineNumber = sourceLines.length + highlight + 1; var maxLineNumber = 0; for (let i = 0; i < sourceLines.length; i++) { // bun only prints \n, no \r\n, so this should work fine sourceLines[i].text = sourceLines[i].text.replaceAll("\n", ""); // This will now only trim spaces (and vertical tab character which never prints) const left = sourceLines[i].text.trimStart(); minLineNumber = Math.min(sourceLines[i].line, minLineNumber); maxLineNumber = Math.max(sourceLines[i].line, maxLineNumber); if (left.length > 0) { start = Math.min(start, i); end = Math.max(end, i + 1); dedent = Math.min(sourceLines[i].text.length - left.length, dedent); } } const leftPad = maxLineNumber.toString(10).length - minLineNumber.toString(10).length; const _sourceLines = sourceLines.slice(start, end); const lines = new Array(_sourceLines.length + React.Children.count(children)); let highlightI = 0; for (let i = 0; i < _sourceLines.length; i++) { const { line, text } = _sourceLines[i]; const classes = { empty: text.trim().length === 0, highlight: highlight === line, }; if (classes.highlight) highlightI = i; const _text = classes.empty ? "" : text.substring(dedent); lines[i] = (
{line.toString(10).padStart(leftPad, " ")}
{_text}
); } return (
{lines}
); }; const BuildErrorSourceLines = ({ location, filename, }: { location: Location; filename: string; }) => { const { line, line_text, column } = location; const sourceLines: SourceLine[] = [{ line, text: line_text }]; const buildURL = React.useCallback( (line, column) => srcFileURL(filename, line, column), [srcFileURL, filename], ); return ( ); }; const BuildErrorStackTrace = ({ location }: { location: Location }) => { const { cwd } = useContext(ErrorGroupContext); const filename = normalizedFilename(location.file, cwd); const { line, column } = location; return ( ); }; export const StackFrameIdentifier = ({ functionName, scope, markdown = false, }: { functionName?: string; markdown: boolean; scope: StackFrameScope; }) => { switch (scope) { case StackFrameScope.Constructor: { functionName = markdown && functionName ? "`" + functionName + "`" : functionName; return functionName ? `new ${functionName}` : "new (anonymous)"; } case StackFrameScope.Eval: { return "eval"; } case StackFrameScope.Module: { return "(esm)"; } case StackFrameScope.Global: { return "(global)"; } case StackFrameScope.Wasm: { return "(wasm)"; } case StackFrameScope.Function: default: { return functionName ? markdown ? "`" + functionName + "`" : functionName : "λ()"; } } }; const getNativeStackFrameIdentifier = (frame) => { const { file, function_name: functionName, scope } = frame; return StackFrameIdentifier({ functionName, scope, markdown: false, }); }; const NativeStackFrame = ({ frame, maxLength, urlBuilder, }: { frame: StackFrame; maxLength: number; urlBuilder: typeof maybeBlobFileURL; }) => { const { cwd } = useContext(ErrorGroupContext); const { file, function_name: functionName, position: { line, column_start: column }, scope, } = frame; const fileName = normalizedFilename(file, cwd); return ( ); }; const NativeStackFrames = ({ frames, urlBuilder }) => { const items = new Array(frames.length); var maxLength = 0; for (let i = 0; i < frames.length; i++) { maxLength = Math.max( getNativeStackFrameIdentifier(frames[i]).length, maxLength, ); } for (let i = 0; i < frames.length; i++) { items[i] = ( ); } return (
{items}
); }; const NativeStackTrace = ({ frames, sourceLines, setSourceLines, children, isClient = false, }: { frames: StackFrame[]; sourceLines: SourceLine[]; setSourceLines: (sourceLines: SourceLine[]) => void; children?: React.ReactNode; isClient: boolean; }) => { const { file = "", position } = frames[0]; const { cwd } = useContext(ErrorGroupContext); const filename = normalizedFilename(file, cwd); const urlBuilder = isClient ? clientURL : maybeBlobFileURL; const ref = React.useRef(null); const buildURL = React.useCallback( (line, column) => urlBuilder(file, line, column), [file, urlBuilder], ); return (
{filename}:{position.line}:{position.column_start} {sourceLines.length > 0 && ( {children} )} {sourceLines.length === 0 && ( {children} )} {frames.length > 1 && ( )}
); }; const Indent = ({ by, children }) => { const amount = useContext(IndentationContext); return ( <> {` `.repeat(by - amount)} {children} ); }; const JSException = ({ value, isClient = false, }: { value: JSExceptionType; isClient: boolean; }) => { const tag = isClient ? ErrorTagType.client : ErrorTagType.server; const [sourceLines, _setSourceLines] = React.useState( value?.stack?.source_lines ?? [], ); var message = value.message || ""; var name = value.name || ""; if (!name && !message) { name = `Unknown error`; } // mutating a prop is sacrilege function setSourceLines(sourceLines: SourceLine[]) { _setSourceLines(sourceLines); if (!value.stack) { value.stack = { frames: [], source_lines: sourceLines, }; } else { value.stack.source_lines = sourceLines; } } switch (value.code) { case JSErrorCode.TypeError: { const fancyTypeError = new FancyTypeError(value); if (fancyTypeError.runtimeType !== RuntimeType.Nothing) { return (
TypeError
{errorTags[tag]}
{fancyTypeError.message}
{fancyTypeError.runtimeTypeName.length && (
It's{" "} {fancyTypeError.runtimeTypeName} .
)} {value.stack && ( {fancyTypeError.runtimeTypeName} )}
); } } default: { const newline = message.indexOf("\n"); if (newline > -1) { const subtitle = message.substring(newline + 1).trim(); message = message.substring(0, newline).trim(); if (subtitle.length) { return (
{name}
{errorTags[tag]}
{message}
{subtitle}
{value.stack && ( )}
); } } return (
{name}
{errorTags[tag]}
{message}
{value.stack && ( )}
); } } }; const Summary = ({ errorCount, onClose, }: { errorCount: number; onClose: () => void; }) => { return (
{errorCount} error{errorCount > 1 ? "s" : ""} on this page
Want help?
); }; const BuildError = ({ message }: { message: Message }) => { let title = (message.data.text || "").trim(); const newline = title.indexOf("\n"); let subtitle = ""; if (newline > -1) { subtitle = title.slice(newline + 1).trim(); title = title.slice(0, newline); } return (
BuildError
{title}
{subtitle.length > 0 && (
{subtitle}
)} {message.data.location && ( )}
); }; const ResolveError = ({ message }: { message: Message }) => { const { cwd } = useContext(ErrorGroupContext); let title = (message.data.text || "").trim(); const newline = title.indexOf("\n"); let subtitle: string | null = null; if (newline > -1) { subtitle = title.slice(newline + 1).trim(); title = title.slice(0, newline); } return (
ResolveError
Can't import{" "} "{message.on.resolve}"
{subtitle &&
{subtitle}
} {message.data.location && ( )}
); }; const OverlayMessageContainer = ({ problems, reason, isClient = false, }: FallbackMessageContainer & { isClient: boolean }) => { const errorCount = problems ? problems.exceptions.length + problems.build.errors : 0; return (
{problems?.exceptions.map((problem, index) => ( ))} {problems?.build.msgs.map((buildMessage, index) => { if (buildMessage.on.build) { return ; } else if (buildMessage.on.resolve) { return ; } else { throw new Error("Unknown build message type"); } })}
); }; // we can ignore the synchronous copy to clipboard API...I think function copyToClipboard(input: string | Promise) { if (!input) return; if (typeof input === "object" && "then" in input) { return input.then((str) => copyToClipboard(str)); } return navigator.clipboard.writeText(input).then(() => {}); } const Footer = ({ toMarkdown, data }) => (
copyToClipboard(withBunInfo(String(toMarkdown(data))))} > {" "} Copy as markdown
); const BuildFailureMessageContainer = ({ messages, }: { messages: Message[]; }) => { return (
{messages.map((buildMessage, index) => { if (buildMessage.on.build) { return ; } else if (buildMessage.on.resolve) { return ; } else { throw new Error("Unknown build message type"); } })}
); }; export var thisCwd = ""; const ErrorGroupContext = createContext<{ cwd?: string }>({ cwd: undefined }); var reactRoot; function renderWithFunc(func) { if (!reactRoot) { const root = document.createElement("div"); root.id = "__bun__error-root"; reactRoot = document.createElement("div"); reactRoot.id = BUN_ERROR_CONTAINER_ID; const fallbackStyleSheet = document.querySelector( "style[data-has-bun-fallback-style]", ); if (!fallbackStyleSheet) { reactRoot.style.visibility = "hidden"; } const shadowRoot = root.attachShadow({ mode: "closed" }); if (!fallbackStyleSheet) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = new URL("/bun:erro.css", document.baseURI).href; link.onload = () => { reactRoot.style.visibility = "visible"; }; shadowRoot.appendChild(link); } else { fallbackStyleSheet.remove(); shadowRoot.appendChild(fallbackStyleSheet); reactRoot.classList.add("BunErrorRoot--FullPage"); const page = document.querySelector("style[data-bun-error-page-style]"); if (page) { page.remove(); shadowRoot.appendChild(page); } } shadowRoot.appendChild(reactRoot); document.body.appendChild(root); render(func(), reactRoot); } else { render(func(), reactRoot); } } export function renderFallbackError(fallback: FallbackMessageContainer) { if (fallback && fallback.cwd) { thisCwd = fallback.cwd; } // Not an error if (fallback?.problems?.name === "JSDisabled") return; return renderWithFunc(() => ( )); } globalThis[Symbol.for("Bun__renderFallbackError")] = renderFallbackError; import { parse as getStackTrace } from "./stack-trace-parser"; var runtimeErrorController: AbortController | null = null; var pending: { stopped: boolean }[] = []; 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", message: error, }; } const exception = { name: String(error.name), message: String(error.message), runtime_type: 0, stack: { frames: error.stack ? getStackTrace(error.stack) : [], source_lines: [], }, }; var lineNumberProperty = ""; var columnNumberProperty = ""; var fileNameProperty = ""; if (error && typeof error === "object") { // safari if ("line" in error) { lineNumberProperty = "line"; // firefox } else if ("lineNumber" in error) { lineNumberProperty = "lineNumber"; } // safari if ("column" in error) { columnNumberProperty = "column"; // firefox } else if ("columnNumber" in error) { columnNumberProperty = "columnNumber"; } // safari if ("sourceURL" in error) { fileNameProperty = "sourceURL"; // firefox } else if ("fileName" in error) { fileNameProperty = "fileName"; } } if (Number.isFinite(error[lineNumberProperty])) { if (exception.stack?.frames.length == 0) { exception.stack.frames.push({ file: error[fileNameProperty] || "", position: { line: +error[lineNumberProperty] || 1, column_start: +error[columnNumberProperty] || 1, }, } as StackFrame); } else if (exception.stack && exception.stack.frames.length > 0) { exception.stack.frames[0].position.line = error[lineNumberProperty]; if (Number.isFinite(error[columnNumberProperty])) { exception.stack.frames[0].position.column_start = error[columnNumberProperty]; } } } const signal = runtimeErrorController.signal; const fallback: FallbackMessageContainer = { message: error.message, problems: { build: { warnings: 0, errors: 0, msgs: [], }, code: 0, name: error.name, exceptions: [exception], }, }; var stopThis = { stopped: false }; pending.push(stopThis); const BunError = () => { return ( ); }; // 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 = fetchAllMappings( exception.stack.frames.map((frame) => normalizedFilename(frame.file, thisCwd), ), signal, ) .map((frame, i) => { if (stopThis.stopped) return null; return [frame, 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 { if (!mappings) 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]; } }); 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 ; }); }); } else { onIdle(clearSourceMappings); renderWithFunc(() => { return ; }); } } export function dismissError() { if (reactRoot) { unmountComponentAtNode(reactRoot); 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().stopped = true; } } export const renderBuildFailure = ( failure: WebsocketMessageBuildFailure, cwd: string, ) => { thisCwd = cwd; renderWithFunc(() => ( )); }; export const clearBuildFailure = dismissError; globalThis.__BunClearBuildFailure = dismissError;