import React from "react"; import { useContext, useState, useCallback, createContext } from "react"; import { render, unmountComponentAtNode } from "react-dom"; import type { FallbackMessageContainer, JSException, JSException as JSExceptionType, Location, Message, Problems, SourceLine, StackFrame, WebsocketMessageBuildFailure, } from "../../src/api/schema"; import { messagesToMarkdown, problemsToMarkdown, withBunInfo, } from "./markdown"; import { fetchAllMappings, fetchMappings, 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 = (event: MouseEvent) => { const target = event.currentTarget as HTMLAnchorElement; 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.line); } 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; } 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) { let 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 ") ); } } } var onClose = dismissError; 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, isClient, sourceLines, setSourceLines, }: { highlightColumnStart: number; highlightColumnEnd: number; highlight: number; buildURL: (line?: number, column?: number) => string; sourceLines: string[]; 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, isClient = false, buildURL, }: { sourceLines: SourceLine[]; highlightColumnStart: number; highlightColumnEnd: number; highlight: number; 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 childrenArray = children || []; const lines = new Array(_sourceLines.length + childrenArray.length); 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 (
{filename}:{line}:{column}
); }; 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)"; break; } case StackFrameScope.Eval: { return "eval"; break; } case StackFrameScope.Module: { return "(esm)"; break; } case StackFrameScope.Global: { return "(global)"; break; } case StackFrameScope.Wasm: { return "(wasm)"; break; } default: { return functionName ? markdown ? "`" + functionName + "`" : functionName : "λ()"; break; } } }; const getNativeStackFrameIdentifier = (frame) => { const { file, function_name: functionName, scope } = frame; return StackFrameIdentifier({ functionName, scope, markdown: false, }); }; const NativeStackFrame = ({ frame, isTop, maxLength, urlBuilder, }: { frame: StackFrame; isTop: boolean; maxLength: number; }) => { const { cwd } = useContext(ErrorGroupContext); const { file, function_name: functionName, position: { line, column_start: column }, scope, } = frame; const fileName = normalizedFilename(file, cwd); return (
{getNativeStackFrameIdentifier(frame)}
{fileName}
{line > -1 &&
:{line}
} {column > -1 && (
:{column}
)}
); }; 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[]; isClient: boolean; setSourceLines: (sourceLines: SourceLine[]) => void; }) => { const { file = "", position } = frames[0]; const { cwd } = useContext(ErrorGroupContext); const filename = normalizedFilename(file, cwd); const urlBuilder = isClient ? clientURL : maybeBlobFileURL; // const [isFocused, setFocused] = React.useState(false); const ref = React.useRef(); const buildURL = React.useCallback( (line, column) => urlBuilder(file, line, column), [file, urlBuilder] ); // React.useLayoutEffect(() => { // var handler1, handler2; // handler1 = document.addEventListener( // "selectionchange", // (event) => { // if (event.target && ref.current && ref.current.contains(event.target)) { // setFocused(true); // } // }, // { passive: true } // ); // handler2 = document.addEventListener( // "selectstart", // (event) => { // console.log(event); // if (event.target && ref.current && ref.current.contains(event.target)) { // setFocused(true); // } // }, // { passive: true } // ); // return () => { // if (handler1) { // document.removeEventListener("selectionchange", handler1); // handler1 = null; // } // if (handler2) { // document.removeEventListener("selectstart", handler2); // handler2 = null; // } // }; // }, [setFocused, ref]); 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(); const 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: Function; }) => { 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 = 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, router, isClient = false, }: FallbackMessageContainer) => { 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 (input && 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 }>(null); 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; reactRoot.style.visibility = "hidden"; const link = document.createElement("link"); link.rel = "stylesheet"; link.href = new URL("/bun:erro.css", document.baseURI).href; link.onload = () => { reactRoot.style.visibility = "visible"; }; const shadowRoot = root.attachShadow({ mode: "closed" }); shadowRoot.appendChild(link); 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(() => ( )); } 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", message: error, }; } const exception: JSException = { 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, }, }); } else if (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 { 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().stopThis = true; } } export const renderBuildFailure = ( failure: WebsocketMessageBuildFailure, cwd: string ) => { thisCwd = cwd; renderWithFunc(() => ( )); }; export const clearBuildFailure = dismissError; globalThis.__BunClearBuildFailure = dismissError;