Files
bun.sh/packages/bun-error/markdown.ts
2025-05-12 17:12:17 -07:00

373 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { JSException, JSException as JSExceptionType, Message, Problems } from "../../src/api/schema";
import { normalizedFilename, StackFrameIdentifier, StackFrameScope, thisCwd } from "./index";
export 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;
}
export function messagesToMarkdown(messages: Message[]): string {
return messages
.map(messageToMarkdown)
.map(a => a.trim())
.join("\n");
}
export function exceptionsToMarkdown(exceptions: JSExceptionType[]): string {
return exceptions
.map(exceptionToMarkdown)
.map(a => a.trim())
.join("\n");
}
function exceptionToMarkdown(exception: JSException): string {
const { name: name_, message: message_, stack } = exception;
var name = String(name_).trim();
var message = String(message_).trim();
// check both so if it turns out one of them was only whitespace, we don't count it
const hasName = name_ && name_.length > 0 && name.length > 0;
const hasMessage = message_ && message_.length > 0 && message.length > 0;
let markdown = "";
if (
(name === "Error" ||
name === "RangeError" ||
name === "TypeError" ||
name === "ReferenceError" ||
name === "DOMException") &&
hasMessage
) {
markdown += `**${message}**\n`;
} else if (hasName && hasMessage) {
markdown += `**${name}**\n${message}\n`;
} else if (hasMessage) {
markdown += `${message}\n`;
} else if (hasName) {
markdown += `**${name}**\n`;
}
if (stack.frames.length > 0) {
var frames = stack.frames;
if (stack.source_lines.length > 0) {
const {
file: _file = "",
function_name = "",
position: { line = -1, column_start: column = -1, column_stop: columnEnd = column } = {
line: -1,
column_start: -1,
column_stop: -1,
},
scope = 0 as any,
} = stack.frames[0];
const file = normalizedFilename(_file, thisCwd);
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 => {
const lineText = sourceLine.text.trimEnd();
markdown += lineText + "\n";
if (sourceLine.line === line && stack.source_lines.length > 1) {
// the comment should start at the first non-whitespace character
// ideally it should be length the original line
// but it may not be
var prefix = "".padStart(lineText.length - lineText.trimStart().length, " ");
prefix += "/* ".padEnd(column - 1 - prefix.length, " ") + "^ happened here ";
markdown += prefix.padEnd(Math.max(lineText.length, 1) - 1, " ") + "*/\n";
}
});
markdown = markdown.trimEnd() + "\n```";
}
}
}
if (frames.length > 0) {
markdown += "\nStack trace:\n";
var padding = 0;
// Limit to 8 frames because it may be a huge stack trace
// and we want to not hit the message limit
const framesToDisplay = frames.slice(0, Math.min(frames.length, 8));
for (let frame of framesToDisplay) {
const {
function_name = "",
position: { line = -1, column_start: column = -1 } = {
line: -1,
column_start: -1,
},
scope = 0 as any,
} = frame;
padding = Math.max(
padding,
StackFrameIdentifier({
scope,
functionName: function_name,
markdown: true,
}).length,
);
}
markdown += "```js\n";
for (let frame of framesToDisplay) {
const {
file = "",
function_name = "",
position: { line = -1, column_start: column = -1 } = {
line: -1,
column_start: -1,
},
scope = 0 as any,
} = frame;
markdown += `
${StackFrameIdentifier({
scope,
functionName: function_name,
markdown: true,
}).padEnd(padding, " ")}`;
if (file) {
markdown += ` ${normalizedFilename(file, thisCwd)}`;
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;
}
export 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";
};
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: ${globalThis.navigator.userAgent}
> Pathname: ${globalThis.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) {}
}
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;
});
}