import type { Server as WebSocketServer, WebSocketHandler, ServerWebSocket, SocketHandler, Socket } from "bun"; export default function ( executionContextId: string, url: string, createBackend: ( executionContextId: string, refEventLoop: boolean, receive: (...messages: string[]) => void, ) => unknown, send: (message: string) => void, close: () => void, ): void { let debug: Debugger | undefined; try { debug = new Debugger(executionContextId, url, createBackend, send, close); } catch (error) { exit("Failed to start inspector:\n", error); } const { protocol, href, host, pathname } = debug.url; if (!protocol.includes("unix")) { console.log(dim("--------------------- Bun Inspector ---------------------"), reset()); console.log(`Listening:\n ${dim(href)}`); if (protocol.includes("ws")) { console.log(`Inspect in browser:\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`); } console.log(dim("--------------------- Bun Inspector ---------------------"), reset()); } const unix = process.env["BUN_INSPECT_NOTIFY"]; if (unix) { const { protocol, pathname } = parseUrl(unix); if (protocol === "unix:") { notify(pathname); } } } class Debugger { #url: URL; #createBackend: (refEventLoop: boolean, receive: (...messages: string[]) => void) => Writer; constructor( executionContextId: string, url: string, createBackend: ( executionContextId: string, refEventLoop: boolean, receive: (...messages: string[]) => void, ) => unknown, send: (message: string) => void, close: () => void, ) { this.#url = parseUrl(url); this.#createBackend = (refEventLoop, receive) => { const backend = createBackend(executionContextId, refEventLoop, receive); return { write: message => { send.$call(backend, message); return true; }, close: () => close.$call(backend), }; }; this.#listen(); } get url(): URL { return this.#url; } #listen(): void { const { protocol, hostname, port, pathname } = this.#url; if (protocol === "ws:" || protocol === "ws+tcp:") { const server = Bun.serve({ hostname, port, fetch: this.#fetch.bind(this), websocket: this.#websocket, }); this.#url.hostname = server.hostname; this.#url.port = `${server.port}`; return; } if (protocol === "ws+unix:") { Bun.serve({ unix: pathname, fetch: this.#fetch.bind(this), websocket: this.#websocket, }); return; } throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`); } get #websocket(): WebSocketHandler { return { idleTimeout: 0, closeOnBackpressureLimit: false, open: ws => this.#open(ws, webSocketWriter(ws)), message: (ws, message) => { if (typeof message === "string") { this.#message(ws, message); } else { this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`)); } }, drain: ws => this.#drain(ws), close: ws => this.#close(ws), }; } #fetch(request: Request, server: WebSocketServer): Response | undefined { const { method, url, headers } = request; const { pathname } = new URL(url); if (method !== "GET") { return new Response(null, { status: 405, // Method Not Allowed }); } switch (pathname) { case "/json/version": return Response.json(versionInfo()); case "/json": case "/json/list": // TODO? } if (!this.#url.protocol.includes("unix") && this.#url.pathname !== pathname) { return new Response(null, { status: 404, // Not Found }); } const data: Connection = { refEventLoop: headers.get("Ref-Event-Loop") === "0", }; if (!server.upgrade(request, { data })) { return new Response(null, { status: 426, // Upgrade Required headers: { "Upgrade": "websocket", }, }); } } get #socket(): SocketHandler { return { open: socket => this.#open(socket, socketWriter(socket)), data: (socket, message) => this.#message(socket, message.toString()), drain: socket => this.#drain(socket), close: socket => this.#close(socket), error: (socket, error) => this.#error(socket, error), connectError: (_, error) => exit("Failed to start inspector:\n", error), }; } #open(connection: ConnectionOwner, writer: Writer): void { const { data } = connection; const { refEventLoop } = data; const client = bufferedWriter(writer); const backend = this.#createBackend(refEventLoop, (...messages: string[]) => { for (const message of messages) { client.write(message); } }); data.client = client; data.backend = backend; } #message(connection: ConnectionOwner, message: string): void { const { data } = connection; const { backend } = data; backend?.write(message); } #drain(connection: ConnectionOwner): void { const { data } = connection; const { client } = data; client?.drain?.(); } #close(connection: ConnectionOwner): void { const { data } = connection; const { backend } = data; backend?.close(); } #error(connection: ConnectionOwner, error: Error): void { const { data } = connection; const { backend } = data; console.error(error); backend?.close(); } } function versionInfo(): unknown { return { "Protocol-Version": "1.3", "Browser": "Bun", // @ts-ignore: Missing types for `navigator` "User-Agent": navigator.userAgent, "WebKit-Version": process.versions.webkit, "Bun-Version": Bun.version, "Bun-Revision": Bun.revision, }; } function webSocketWriter(ws: ServerWebSocket): Writer { return { write: message => !!ws.sendText(message), close: () => ws.close(), }; } function socketWriter(socket: Socket): Writer { return { write: message => !!socket.write(message), close: () => socket.end(), }; } function bufferedWriter(writer: Writer): Writer { let draining = false; let pendingMessages: string[] = []; return { write: message => { if (draining || !writer.write(message)) { pendingMessages.push(message); } return true; }, drain: () => { draining = true; try { for (let i = 0; i < pendingMessages.length; i++) { if (!writer.write(pendingMessages[i])) { pendingMessages = pendingMessages.slice(i); return; } } } finally { draining = false; } }, close: () => { writer.close(); pendingMessages.length = 0; }, }; } const defaultHostname = "localhost"; const defaultPort = 6499; function parseUrl(url: string): URL { try { if (!url) { return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`); } else if (url.startsWith("/")) { return new URL(url, `ws://${defaultHostname}:${defaultPort}/`); } else if (/^[a-z+]+:\/\//i.test(url)) { return new URL(url); } else if (/^\d+$/.test(url)) { return new URL(randomId(), `ws://${defaultHostname}:${url}/`); } else if (!url.includes("/") && url.includes(":")) { return new URL(randomId(), `ws://${url}/`); } else if (!url.includes(":")) { const [hostname, pathname] = url.split("/", 2); return new URL(`ws://${hostname}:${defaultPort}/${pathname}`); } else { return new URL(randomId(), `ws://${url}`); } } catch { throw new TypeError(`Invalid hostname or URL: '${url}'`); } } function randomId() { return Math.random().toString(36).slice(2); } const { enableANSIColors } = Bun; function dim(string: string): string { if (enableANSIColors) { return `\x1b[2m${string}\x1b[22m`; } return string; } function link(url: string): string { if (enableANSIColors) { return `\x1b[1m\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\\x1b[22m`; } return url; } function reset(): string { if (enableANSIColors) { return "\x1b[49m"; } return ""; } function notify(unix: string): void { Bun.connect({ unix, socket: { open: socket => { socket.end("1"); }, data: () => {}, // required or it errors }, }).finally(() => { // Best-effort }); } function exit(...args: unknown[]): never { console.error(...args); process.exit(1); } type ConnectionOwner = { data: Connection; }; type Connection = { refEventLoop: boolean; client?: Writer; backend?: Writer; }; type Writer = { write: (message: string) => boolean; drain?: () => void; close: () => void; };