initial commit

This commit is contained in:
i2p
2026-08-27 21:09:14 +00:00
commit a5b6d59437
12681 changed files with 3253832 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+3
View File
@@ -0,0 +1,3 @@
# @types/bun alias
This is an internal alias of the DefinitelyTyped `@types/bun` package. This alias exists to skip downloading the @types/bun package in Bun's own CI.
+1
View File
@@ -0,0 +1 @@
/// <reference types="bun-types" />
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@types/bun",
"version": "1.2.2",
"description": "TypeScript definitions for bun",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/bun",
"license": "MIT",
"contributors": [
{
"name": "Jarred Sumner",
"githubUsername": "Jarred-Sumner",
"url": "https://github.com/Jarred-Sumner"
},
{
"name": "Ashcon Partovi",
"githubUsername": "electroid",
"url": "https://github.com/electroid"
},
{
"name": "Robobun",
"githubUsername": "robobun",
"url": "https://github.com/robobun"
},
{
"name": "Dylan Conway",
"githubUsername": "dylan-conway",
"url": "https://github.com/dylan-conway"
},
{
"name": "Meghan Denny",
"githubUsername": "nektro",
"url": "https://github.com/nektro"
},
{
"name": "Michael H",
"githubUsername": "RiskyMH",
"url": "https://github.com/RiskyMH"
}
],
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/bun"
},
"scripts": {},
"dependencies": {
"bun-types": "workspace:"
},
"peerDependencies": {},
"typesPublisherContentHash": "caeb56f2d4753af08a8e57856ec5e9d86ad3542da2171a68e19698ec4539995a",
"typeScriptVersion": "5.0"
}
+202
View File
@@ -0,0 +1,202 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# End of https://www.toptal.com/developers/gitignore/api/node
# Created by https://www.toptal.com/developers/gitignore/api/macos
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
# End of https://www.toptal.com/developers/gitignore/api/macos
# Created by https://www.toptal.com/developers/gitignore/api/windows
# Edit at https://www.toptal.com/developers/gitignore?templates=windows
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows
#Added by cargo
/target
Cargo.lock
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
*.node
dist/
index.js
index.d.ts
+13
View File
@@ -0,0 +1,13 @@
target
Cargo.lock
.cargo
.github
npm
.eslintrc
.prettierignore
rustfmt.toml
yarn.lock
*.node
.yarn
__test__
renovate.json
+21
View File
@@ -0,0 +1,21 @@
[package]
edition = "2021"
name = "bun-mdx-rs"
version = "0.0.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
napi-derive = "2.12.2"
mdxjs = "0.2.11"
bun-native-plugin = { path = "../bun-native-plugin-rs" }
[build-dependencies]
napi-build = "2.0.1"
[profile.release]
lto = true
strip = "symbols"
+34
View File
@@ -0,0 +1,34 @@
# bun-build-mdx-rs
This is a proof of concept for using a third-party native addon in `Bun.build()`.
This uses `mdxjs-rs` to convert MDX to JSX.
TODO: **This needs to be built & published to npm.**
## Building locally:
```sh
cargo build --release
```
```js
import { build } from "bun";
import mdx from "./index.js";
// TODO: This needs to be prebuilt for the current platform
// Probably use a napi-rs template for this
import addon from "./target/release/libmdx_bun.dylib" with { type: "file" };
const results = await build({
entrypoints: ["./hello.jsx"],
plugins: [mdx({ addon })],
minify: true,
outdir: "./dist",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});
console.log(results);
```
@@ -0,0 +1,7 @@
import test from "ava";
import { sum } from "../index.js";
test("sum from native", t => {
t.is(sum(1, 2), 3);
});
+5
View File
@@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}
+6
View File
@@ -0,0 +1,6 @@
import page1 from "./page1.mdx";
import page2 from "./page2.mdx";
import page3 from "./page3.mdx";
import page4 from "./page4.mdx";
console.log(page1, page2, page3, page4);
+11
View File
@@ -0,0 +1,11 @@
# Hello World
This is a sample MDX file that demonstrates various MDX features.
## Components
You can use JSX components directly in MDX:
<Button onClick={() => alert("Hello!")}>Click me</Button>
## Code Blocks
+11
View File
@@ -0,0 +1,11 @@
# Hello World
This is a sample MDX file that demonstrates various MDX features.
## Components
You can use JSX components directly in MDX:
<Button onClick={() => alert("Hello!")}>Click me</Button>
## Code Blocks
+11
View File
@@ -0,0 +1,11 @@
# Hello World
This is a sample MDX file that demonstrates various MDX features.
## Components
You can use JSX components directly in MDX:
<Button onClick={() => alert("Hello!")}>Click me</Button>
## Code Blocks
+11
View File
@@ -0,0 +1,11 @@
# Hello World
This is a sample MDX file that demonstrates various MDX features.
## Components
You can use JSX components directly in MDX:
<Button onClick={() => alert("Hello!")}>Click me</Button>
## Code Blocks
@@ -0,0 +1,3 @@
# `bun-mdx-rs-darwin-arm64`
This is the **aarch64-apple-darwin** binary for `bun-mdx-rs`
@@ -0,0 +1,18 @@
{
"name": "bun-mdx-rs-darwin-arm64",
"version": "0.0.0",
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"main": "bun-mdx-rs.darwin-arm64.node",
"files": [
"bun-mdx-rs.darwin-arm64.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
}
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-darwin-x64`
This is the **x86_64-apple-darwin** binary for `bun-mdx-rs`
@@ -0,0 +1,18 @@
{
"name": "bun-mdx-rs-darwin-x64",
"version": "0.0.0",
"os": [
"darwin"
],
"cpu": [
"x64"
],
"main": "bun-mdx-rs.darwin-x64.node",
"files": [
"bun-mdx-rs.darwin-x64.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
}
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-linux-arm64-gnu`
This is the **aarch64-unknown-linux-gnu** binary for `bun-mdx-rs`
@@ -0,0 +1,21 @@
{
"name": "bun-mdx-rs-linux-arm64-gnu",
"version": "0.0.0",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"main": "bun-mdx-rs.linux-arm64-gnu.node",
"files": [
"bun-mdx-rs.linux-arm64-gnu.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"glibc"
]
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-linux-arm64-musl`
This is the **aarch64-unknown-linux-musl** binary for `bun-mdx-rs`
@@ -0,0 +1,21 @@
{
"name": "bun-mdx-rs-linux-arm64-musl",
"version": "0.0.0",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"main": "bun-mdx-rs.linux-arm64-musl.node",
"files": [
"bun-mdx-rs.linux-arm64-musl.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"musl"
]
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-linux-x64-gnu`
This is the **x86_64-unknown-linux-gnu** binary for `bun-mdx-rs`
@@ -0,0 +1,21 @@
{
"name": "bun-mdx-rs-linux-x64-gnu",
"version": "0.0.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"main": "bun-mdx-rs.linux-x64-gnu.node",
"files": [
"bun-mdx-rs.linux-x64-gnu.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"glibc"
]
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-linux-x64-musl`
This is the **x86_64-unknown-linux-musl** binary for `bun-mdx-rs`
@@ -0,0 +1,21 @@
{
"name": "bun-mdx-rs-linux-x64-musl",
"version": "0.0.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"main": "bun-mdx-rs.linux-x64-musl.node",
"files": [
"bun-mdx-rs.linux-x64-musl.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"musl"
]
}
@@ -0,0 +1,3 @@
# `bun-mdx-rs-win32-x64-msvc`
This is the **x86_64-pc-windows-msvc** binary for `bun-mdx-rs`
@@ -0,0 +1,18 @@
{
"name": "bun-mdx-rs-win32-x64-msvc",
"version": "0.0.0",
"os": [
"win32"
],
"cpu": [
"x64"
],
"main": "bun-mdx-rs.win32-x64-msvc.node",
"files": [
"bun-mdx-rs.win32-x64-msvc.node"
],
"license": "MIT",
"engines": {
"node": ">= 10"
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"name": "bun-mdx-rs",
"version": "0.0.0",
"main": "index.js",
"types": "index.d.ts",
"napi": {
"name": "bun-mdx-rs",
"triples": {
"additional": [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"x86_64-unknown-linux-musl"
]
}
},
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^2.18.4",
"ava": "^6.0.1"
},
"ava": {
"timeout": "3m"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"artifacts": "napi artifacts",
"build": "napi build --platform --release",
"build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"test": "ava",
"universal": "napi universal",
"version": "napi version"
}
}
+2
View File
@@ -0,0 +1,2 @@
tab_spaces = 2
edition = "2021"
+25
View File
@@ -0,0 +1,25 @@
use bun_native_plugin::{anyhow, bun, define_bun_plugin, BunLoader, Result};
use mdxjs::{compile, Options as CompileOptions};
use napi_derive::napi;
define_bun_plugin!("bun-mdx-rs");
#[bun]
pub fn bun_mdx_rs(handle: &mut OnBeforeParse) -> Result<()> {
let source_str = handle.input_source_code()?;
let mut options = CompileOptions::gfm();
// Leave it as JSX for Bun to handle
options.jsx = true;
let path = handle.path()?;
options.filepath = Some(path.to_string());
let jsx = compile(&source_str, &options)
.map_err(|e| anyhow::anyhow!("Failed to compile MDX: {:?}", e))?;
handle.set_output_source_code(jsx, BunLoader::BUN_LOADER_JSX);
Ok(())
}
@@ -0,0 +1,2 @@
protocol/*/protocol.json linguist-generated=true
protocol/*/index.d.ts linguist-generated=true
@@ -0,0 +1 @@
protocol/*.json
@@ -0,0 +1,3 @@
# bun-debug-adapter-protocol
https://microsoft.github.io/debug-adapter-protocol/overview
@@ -0,0 +1,21 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-debug-adapter-protocol",
"dependencies": {
"semver": "^7.5.4",
"source-map-js": "^1.0.2",
},
},
},
"packages": {
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"semver": ["[email protected]", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
"source-map-js": ["[email protected]", "", {}, "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="],
"yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
}
}
@@ -0,0 +1,3 @@
export * from "./src/debugger/adapter";
export * from "./src/debugger/signal";
export type * from "./src/protocol";
@@ -0,0 +1,9 @@
{
"name": "bun-debug-adapter-protocol",
"version": "0.0.1",
"type": "module",
"dependencies": {
"semver": "^7.5.4",
"source-map-js": "^1.0.2"
}
}
@@ -0,0 +1,176 @@
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import type { Protocol, Type } from "../src/protocol/schema";
run().catch(console.error);
async function run() {
const cwd = new URL("../src/protocol/", import.meta.url);
const runner = "Bun" in globalThis ? "bunx" : "npx";
const write = (name: string, data: string) => {
const path = new URL(name, cwd);
writeFileSync(path, data);
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
};
const schema: Protocol = await download(
"https://microsoft.github.io/debug-adapter-protocol/debugAdapterProtocol.json",
);
write("protocol.json", JSON.stringify(schema));
const types = formatProtocol(schema);
write("index.d.ts", `// GENERATED - DO NOT EDIT\n${types}`);
}
function formatProtocol(protocol: Protocol, extraTs?: string): string {
const { definitions } = protocol;
const requestMap = new Map();
const responseMap = new Map();
const eventMap = new Map();
let body = `export namespace DAP {`;
loop: for (const [key, definition] of Object.entries(definitions)) {
if (/[a-z]+Request$/i.test(key)) {
continue;
}
if (/[a-z]+Arguments$/i.test(key)) {
const name = key.replace(/(Request)?Arguments$/, "");
const requestName = `${name}Request`;
requestMap.set(toMethod(name), requestName);
body += formatType(definition, requestName);
continue;
}
if ("allOf" in definition) {
const { allOf } = definition;
for (const type of allOf) {
if (type.type !== "object") {
continue;
}
const { description, properties = {} } = type;
if (/[a-z]+Event$/i.test(key)) {
const { event, body: type = {} } = properties;
if (!event || !("enum" in event)) {
continue;
}
const [eventKey] = event.enum ?? [];
eventMap.set(eventKey, key);
const eventType: Type = {
type: "object",
description,
...type,
};
body += formatType(eventType, key);
continue loop;
}
if (/[a-z]+Response$/i.test(key)) {
const { body: type = {} } = properties;
const bodyType: Type = {
type: "object",
description,
...type,
};
const name = key.replace(/Response$/, "");
responseMap.set(toMethod(name), key);
body += formatType(bodyType, key);
continue loop;
}
}
}
body += formatType(definition, key);
}
for (const [key, name] of responseMap) {
if (requestMap.has(key)) {
continue;
}
const requestName = `${name.replace(/Response$/, "")}Request`;
requestMap.set(key, requestName);
body += formatType({ type: "object", properties: {} }, requestName);
}
body += formatMapType("RequestMap", requestMap);
body += formatMapType("ResponseMap", responseMap);
body += formatMapType("EventMap", eventMap);
if (extraTs) {
body += extraTs;
}
return body + "};";
}
function formatMapType(key: string, typeMap: Map<string, string>): string {
const type: Type = {
type: "object",
required: [...typeMap.keys()],
properties: Object.fromEntries([...typeMap.entries()].map(([key, value]) => [key, { $ref: value }])),
};
return formatType(type, key);
}
function formatType(type: Type, key?: string): string {
const { description, type: kind } = type;
let body = "";
if (key) {
if (description) {
body += `\n${toComment(description)}\n`;
}
body += `export type ${key}=`;
}
if (kind === "boolean") {
body += "boolean";
} else if (kind === "number" || kind === "integer") {
body += "number";
} else if (kind === "string") {
const { enum: choices } = type;
if (choices) {
body += choices.map(value => `"${value}"`).join("|");
} else {
body += "string";
}
} else if (kind === "array") {
const { items } = type;
const itemType = items ? formatType(items) : "unknown";
body += `${itemType}[]`;
} else if (kind === "object") {
const { properties, required } = type;
if (!properties || Object.keys(properties).length === 0) {
body += "{}";
} else {
body += "{";
for (const [key, { description, ...type }] of Object.entries(properties)) {
if (description) {
body += `\n${toComment(description)}`;
}
const delimit = required?.includes(key) ? ":" : "?:";
body += `\n${key}${delimit}${formatType(type)};`;
}
body += "}";
}
} else if ("$ref" in type) {
const { $ref: ref } = type;
body += ref.split("/").pop() || "unknown";
} else if ("allOf" in type) {
const { allOf } = type;
body += allOf.map(type => formatType(type)).join("&");
} else {
body += "unknown";
}
if (key) {
body += ";";
}
return body;
}
function toMethod(name: string): string {
return `${name.substring(0, 1).toLowerCase()}${name.substring(1)}`;
}
function toComment(description?: string): string {
if (!description) {
return "";
}
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
return lines.join("\n");
}
async function download<T>(url: string | URL): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.statusText}`);
}
return response.json();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
"use strict";
export default {
fetch(request) {
const animal = getAnimal(request.url);
const voice = animal.talk();
return new Response(voice);
},
};
function getAnimal(query) {
switch (query.split("/").pop()) {
case "dog":
return new Dog();
case "cat":
return new Cat();
}
return new Bird();
}
class Dog {
name = "dog";
talk() {
return "woof";
}
}
class Cat {
name = "cat";
talk() {
return "meow";
}
}
class Bird {
name = "bird";
talk() {
return "chirp";
}
}
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvYnVuLWRlYnVnLWFkYXB0ZXItcHJvdG9jb2wvZGVidWdnZXIvZml4dHVyZXMvd2l0aC1zb3VyY2VtYXAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IHtcbiAgZmV0Y2gocmVxdWVzdDogUmVxdWVzdCk6IFJlc3BvbnNlIHtcbiAgICBjb25zdCBhbmltYWwgPSBnZXRBbmltYWwocmVxdWVzdC51cmwpO1xuICAgIGNvbnN0IHZvaWNlID0gYW5pbWFsLnRhbGsoKTtcbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKHZvaWNlKTtcbiAgfSxcbn07XG5cbmZ1bmN0aW9uIGdldEFuaW1hbChxdWVyeTogc3RyaW5nKTogQW5pbWFsIHtcbiAgc3dpdGNoIChxdWVyeS5zcGxpdChcIi9cIikucG9wKCkpIHtcbiAgICBjYXNlIFwiZG9nXCI6XG4gICAgICByZXR1cm4gbmV3IERvZygpO1xuICAgIGNhc2UgXCJjYXRcIjpcbiAgICAgIHJldHVybiBuZXcgQ2F0KCk7XG4gIH1cbiAgcmV0dXJuIG5ldyBCaXJkKCk7XG59XG5cbmludGVyZmFjZSBBbmltYWwge1xuICByZWFkb25seSBuYW1lOiBzdHJpbmc7XG4gIHRhbGsoKTogc3RyaW5nO1xufVxuXG5jbGFzcyBEb2cgaW1wbGVtZW50cyBBbmltYWwge1xuICBuYW1lID0gXCJkb2dcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwid29vZlwiO1xuICB9XG59XG5cbmNsYXNzIENhdCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImNhdFwiO1xuXG4gIHRhbGsoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gXCJtZW93XCI7XG4gIH1cbn1cblxuY2xhc3MgQmlyZCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImJpcmRcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwiY2hpcnBcIjtcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLGVBQWU7QUFBQSxFQUNiLE1BQU0sU0FBNEI7QUFDaEMsVUFBTSxTQUFTLFVBQVUsUUFBUSxHQUFHO0FBQ3BDLFVBQU0sUUFBUSxPQUFPLEtBQUs7QUFDMUIsV0FBTyxJQUFJLFNBQVMsS0FBSztBQUFBLEVBQzNCO0FBQ0Y7QUFFQSxTQUFTLFVBQVUsT0FBdUI7QUFDeEMsVUFBUSxNQUFNLE1BQU0sR0FBRyxFQUFFLElBQUksR0FBRztBQUFBLElBQzlCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLElBQ2pCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLEVBQ25CO0FBQ0EsU0FBTyxJQUFJLEtBQUs7QUFDbEI7QUFPQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLEtBQXVCO0FBQUEsRUFDM0IsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7IiwKICAibmFtZXMiOiBbXQp9Cg==
@@ -0,0 +1,46 @@
export default {
fetch(request: Request): Response {
const animal = getAnimal(request.url);
const voice = animal.talk();
return new Response(voice);
},
};
function getAnimal(query: string): Animal {
switch (query.split("/").pop()) {
case "dog":
return new Dog();
case "cat":
return new Cat();
}
return new Bird();
}
interface Animal {
readonly name: string;
talk(): string;
}
class Dog implements Animal {
name = "dog";
talk(): string {
return "woof";
}
}
class Cat implements Animal {
name = "cat";
talk(): string {
return "meow";
}
}
class Bird implements Animal {
name = "bird";
talk(): string {
return "chirp";
}
}
@@ -0,0 +1,20 @@
export default {
fetch(request) {
return new Response(a());
},
};
function a() {
return b();
}
function b() {
return c();
}
function c() {
function d() {
return "hello";
}
return d();
}
@@ -0,0 +1,117 @@
import type { Socket } from "node:net";
const enum FramerState {
WaitingForLength,
WaitingForMessage,
}
let socketFramerMessageLengthBuffer: Buffer;
export class SocketFramer {
state: FramerState = FramerState.WaitingForLength;
pendingLength: number = 0;
sizeBuffer: Buffer = Buffer.alloc(4);
sizeBufferIndex: number = 0;
bufferedData: Buffer = Buffer.alloc(0);
socket: Socket;
private onMessage: (message: string | string[]) => void;
constructor(socket: Socket, onMessage: (message: string | string[]) => void) {
this.socket = socket;
this.onMessage = onMessage;
if (!socketFramerMessageLengthBuffer) {
socketFramerMessageLengthBuffer = Buffer.alloc(4);
}
this.reset();
}
reset(): void {
this.state = FramerState.WaitingForLength;
this.bufferedData = Buffer.alloc(0);
this.sizeBufferIndex = 0;
this.sizeBuffer = Buffer.alloc(4);
}
send(data: string): void {
socketFramerMessageLengthBuffer.writeUInt32BE(Buffer.byteLength(data), 0);
this.socket.write(socketFramerMessageLengthBuffer);
this.socket.write(data);
}
onData(data: Buffer): void {
this.bufferedData = this.bufferedData.length > 0 ? Buffer.concat([this.bufferedData, data]) : data;
let messagesToDeliver: string[] = [];
let position = 0;
while (position < this.bufferedData.length) {
// Need 4 bytes for the length
if (this.bufferedData.length - position < 4) {
break;
}
// Read the length prefix
const messageLength = this.bufferedData.readUInt32BE(position);
// Validate message length
if (messageLength <= 0 || messageLength > 1024 * 1024) {
// 1MB max
// Try to resync by looking for the next valid message
let newPosition = position + 1;
let found = false;
while (newPosition < this.bufferedData.length - 4) {
const testLength = this.bufferedData.readUInt32BE(newPosition);
if (testLength > 0 && testLength <= 1024 * 1024) {
// Verify we can read the full message
if (this.bufferedData.length - newPosition - 4 >= testLength) {
const testMessage = this.bufferedData.toString("utf-8", newPosition + 4, newPosition + 4 + testLength);
if (testMessage.startsWith('{"')) {
position = newPosition;
found = true;
break;
}
}
}
newPosition++;
}
if (!found) {
// Couldn't find a valid message, discard buffer up to this point
this.bufferedData = this.bufferedData.slice(position + 4);
return;
}
continue;
}
// Check if we have the complete message
if (this.bufferedData.length - position - 4 < messageLength) {
break;
}
const message = this.bufferedData.toString("utf-8", position + 4, position + 4 + messageLength);
if (message.startsWith('{"')) {
messagesToDeliver.push(message);
}
position += 4 + messageLength;
}
if (position > 0) {
this.bufferedData =
position < this.bufferedData.length ? this.bufferedData.slice(position) : SocketFramer.emptyBuffer;
}
if (messagesToDeliver.length === 1) {
this.onMessage(messagesToDeliver[0]);
} else if (messagesToDeliver.length > 1) {
this.onMessage(messagesToDeliver);
}
}
private static emptyBuffer = Buffer.from([]);
}
@@ -0,0 +1,178 @@
import { randomBytes } from "node:crypto";
import { EventEmitter } from "node:events";
import type { Server, Socket } from "node:net";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
const isDebug = process.env.NODE_ENV === "development";
export type UnixSignalEventMap = {
"Signal.listening": [string];
"Signal.error": [Error];
"Signal.received": [string];
"Signal.closed": [];
"Signal.Socket.closed": [socket: Socket];
"Signal.Socket.connect": [socket: Socket];
};
/**
* Starts a server that listens for signals on a UNIX domain socket.
*/
export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
#path: string;
#server: Server;
#ready: Promise<void>;
constructor(path?: string | URL | undefined) {
super();
this.#path = path ? parseUnixPath(path) : randomUnixPath();
this.#server = createServer();
this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
this.#server.on("error", error => this.emit("Signal.error", error));
this.#server.on("close", () => this.emit("Signal.closed"));
this.#server.on("connection", socket => {
this.emit("Signal.Socket.connect", socket);
socket.on("data", data => {
this.emit("Signal.received", data.toString());
});
socket.on("close", () => {
this.emit("Signal.Socket.closed", socket);
});
});
this.#ready = new Promise((resolve, reject) => {
this.#server.on("listening", resolve);
this.#server.on("error", reject);
});
this.#server.listen(this.#path);
}
emit<E extends keyof UnixSignalEventMap>(event: E, ...args: UnixSignalEventMap[E]): boolean {
if (isDebug) {
console.log(event, ...args);
}
return super.emit(event, ...(args as never));
}
/**
* The path to the UNIX domain socket.
*/
get url(): string {
return `unix://${this.#path}`;
}
/**
* Resolves when the server is listening or rejects if an error occurs.
*/
get ready(): Promise<void> {
return this.#ready;
}
/**
* Closes the server.
*/
close(): void {
this.#server.close();
}
}
export function randomUnixPath(): string {
return join(tmpdir(), `${randomBytes(16).toString("hex")}.sock`);
}
function parseUnixPath(path: string | URL): string {
if (typeof path === "string" && path.startsWith("/")) {
return path;
}
try {
const { pathname } = new URL(path);
return pathname;
} catch {
throw new Error(`Invalid UNIX path: ${path}`);
}
}
export type TCPSocketSignalEventMap = {
"Signal.listening": [];
"Signal.error": [Error];
"Signal.closed": [];
"Signal.received": [string];
"Signal.Socket.closed": [socket: Socket];
"Signal.Socket.connect": [socket: Socket];
};
export class TCPSocketSignal extends EventEmitter {
#port: number;
#server: ReturnType<typeof createServer>;
#ready: Promise<void>;
constructor(port: number) {
super();
this.#port = port;
this.#server = createServer((socket: Socket) => {
this.emit("Signal.Socket.connect", socket);
socket.on("data", data => {
this.emit("Signal.received", data.toString());
});
socket.on("error", error => {
this.emit("Signal.error", error);
});
socket.on("close", () => {
this.emit("Signal.Socket.closed", socket);
});
});
this.#server.on("close", () => {
this.emit("Signal.closed");
});
this.#ready = new Promise((resolve, reject) => {
this.#server.listen(this.#port, "127.0.0.1", () => {
const address = this.#server.address();
if (address && typeof address === "object") {
this.#port = address.port;
}
this.emit("Signal.listening");
resolve();
});
this.#server.on("error", reject);
});
}
emit<E extends keyof TCPSocketSignalEventMap>(event: E, ...args: TCPSocketSignalEventMap[E]): boolean {
if (isDebug) {
console.log(event, ...args);
}
return super.emit(event, ...args);
}
/**
* The TCP port.
*/
get port(): number {
return this.#port;
}
get url(): string {
return `tcp://127.0.0.1:${this.#port}`;
}
/**
* Resolves when the server is listening or rejects if an error occurs.
*/
get ready(): Promise<void> {
return this.#ready;
}
/**
* Closes the server.
*/
close(): void {
this.#server.close();
}
}
@@ -0,0 +1,124 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { connect } from "node:net";
import { networkInterfaces } from "node:os";
import { getRandomId, WebSocketDebugAdapter } from "./adapter.js";
import { TCPSocketSignal } from "./signal.js";
import { SourceMap } from "./sourcemap.js";
test("works without source map", () => {
const sourceMap = getSourceMap("without-sourcemap.js");
expect(sourceMap.generatedLocation({ line: 7 })).toEqual({ line: 7, column: 0, verified: true });
expect(sourceMap.generatedLocation({ line: 7, column: 2 })).toEqual({ line: 7, column: 2, verified: true });
expect(sourceMap.originalLocation({ line: 11 })).toEqual({ line: 11, column: 0, verified: true });
expect(sourceMap.originalLocation({ line: 11, column: 2 })).toEqual({ line: 11, column: 2, verified: true });
});
test("works with source map", () => {
const sourceMap = getSourceMap("with-sourcemap.js");
// FIXME: Columns don't appear to be accurate for `generatedLocation`
expect(sourceMap.generatedLocation({ line: 3 })).toMatchObject({ line: 4, verified: true });
expect(sourceMap.generatedLocation({ line: 27 })).toMatchObject({ line: 20, verified: true });
expect(sourceMap.originalLocation({ line: 32 })).toEqual({ line: 43, column: 4, verified: true });
expect(sourceMap.originalLocation({ line: 13 })).toEqual({ line: 13, column: 6, verified: true });
});
function getSourceMap(filename: string): SourceMap {
const { pathname } = new URL(`./fixtures/${filename}`, import.meta.url);
const source = readFileSync(pathname, "utf-8");
const match = source.match(/\/\/# sourceMappingURL=(.*)$/m);
if (match) {
const [, url] = match;
return SourceMap(url);
}
return SourceMap();
}
test("only forwards inspector events from known protocol domains to the adapter", () => {
const adapter = new WebSocketDebugAdapter();
// Replace the launch request handler so a (wrongly) forwarded event is observable
// without spawning any process.
const launchCalls: unknown[][] = [];
(adapter as any).launch = (...args: unknown[]) => {
launchCalls.push(args);
};
const inspector = adapter.getInspector();
// The WebSocket inspector re-emits any message without an "id" using the method name
// chosen by the remote peer. An event named after a DAP request must not be forwarded
// to the adapter, where it would be dispatched to the matching request handler.
(inspector as any).emit("launch", {
runtime: "/bin/sh",
runtimeArgs: ["-c", "echo unexpected"],
program: "example.js",
});
expect(launchCalls).toHaveLength(0);
// A genuine inspector-domain event still reaches listeners registered on the adapter.
const heapEvents: unknown[] = [];
adapter.on("Heap.garbageCollected", event => {
heapEvents.push(event);
});
(inspector as any).emit("Heap.garbageCollected", {
collection: { type: "full", startTime: 0, endTime: 1 },
});
expect(heapEvents).toEqual([{ collection: { type: "full", startTime: 0, endTime: 1 } }]);
});
test("getRandomId returns a distinct 32-character lowercase hex string on every call", () => {
const ids = new Set<string>();
for (let i = 0; i < 256; i++) {
const id = getRandomId();
expect(id).toMatch(/^[0-9a-f]{32}$/);
ids.add(id);
}
expect(ids.size).toBe(256);
});
test("TCPSocketSignal accepts connections only on the loopback interface", async () => {
// Same construction the VS Code extension uses (diagnostics.ts createSignal).
const signal = new TCPSocketSignal(0);
await signal.ready;
const port = signal.port;
try {
// The legitimate local client connects over loopback and its payload is delivered.
const received = new Promise<string>(resolve => signal.once("Signal.received", resolve));
await new Promise<void>((resolve, reject) => {
const client = connect({ host: "127.0.0.1", port }, () => {
client.end("hello");
resolve();
});
client.on("error", reject);
});
expect(await received).toBe("hello");
// The same port is not reachable through a non-loopback interface address.
let external: string | undefined;
for (const addresses of Object.values(networkInterfaces())) {
for (const { family, internal, address } of addresses ?? []) {
if (family === "IPv4" && !internal) {
external = address;
break;
}
}
if (external) break;
}
if (external) {
const externalHost = external;
const connectError = await new Promise<Error | null>(resolve => {
const client = connect({ host: externalHost, port }, () => {
client.end();
resolve(null);
});
client.on("error", error => resolve(error));
});
expect(connectError).not.toBeNull();
}
} finally {
signal.close();
}
});
@@ -0,0 +1,209 @@
import type { LineRange, MappedPosition } from "source-map-js";
import { SourceMapConsumer } from "source-map-js";
export type LocationRequest = {
line?: number;
column?: number;
url?: string;
};
export type Location = {
line: number; // 0-based
column: number; // 0-based
} & (
| {
verified: true;
}
| {
verified?: false;
message?: string;
}
);
export interface SourceMap {
/**
* Converts a location in the original source to a location in the generated source.
* @param request A request
*/
generatedLocation(request: LocationRequest): Location;
/**
* Converts a location in the generated source to a location in the original source.
* @param request A request
*/
originalLocation(request: LocationRequest): Location;
}
class ActualSourceMap implements SourceMap {
#sourceMap: SourceMapConsumer;
#sources: string[];
constructor(sourceMap: SourceMapConsumer) {
this.#sourceMap = sourceMap;
this.#sources = (sourceMap as any)._absoluteSources;
}
#getSource(url?: string): string {
const sources = this.#sources;
if (!sources.length) {
return "";
}
if (sources.length === 1 || !url) {
return sources[0];
}
for (const source of sources) {
if (url.endsWith(source)) {
return source;
}
}
return "";
}
generatedLocation(request: LocationRequest): Location {
const { line, column, url } = request;
let lineRange: LineRange;
try {
const source = this.#getSource(url);
lineRange = this.#sourceMap.generatedPositionFor({
line: lineTo1BasedLine(line),
column: columnToColumn(column),
source,
});
} catch (error) {
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: false,
message: unknownToError(error),
};
}
if (!locationIsValid(lineRange)) {
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: false,
};
}
const { line: gline, column: gcolumn } = lineRange;
return {
line: lineTo0BasedLine(gline),
column: columnToColumn(gcolumn),
verified: true,
};
}
originalLocation(request: LocationRequest): Location {
const { line, column } = request;
let mappedPosition: MappedPosition;
try {
mappedPosition = this.#sourceMap.originalPositionFor({
line: lineTo1BasedLine(line),
column: columnToColumn(column),
});
} catch (error) {
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: false,
message: unknownToError(error),
};
}
if (!locationIsValid(mappedPosition)) {
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: false,
};
}
const { line: oline, column: ocolumn } = mappedPosition;
return {
line: lineTo0BasedLine(oline),
column: columnToColumn(ocolumn),
verified: true,
};
}
}
class NoopSourceMap implements SourceMap {
generatedLocation(request: LocationRequest): Location {
const { line, column } = request;
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: true,
};
}
originalLocation(request: LocationRequest): Location {
const { line, column } = request;
return {
line: lineToLine(line),
column: columnToColumn(column),
verified: true,
};
}
}
const defaultSourceMap = new NoopSourceMap();
export function SourceMap(url?: string): SourceMap {
if (!url) {
return defaultSourceMap;
}
if (!url.startsWith("data:")) {
const match = url.match(/\/\/[#@]\s*sourceMappingURL=(.*)$/m);
if (!match) {
return defaultSourceMap;
}
const [_, sourceMapUrl] = match;
url = sourceMapUrl;
}
try {
const [_, base64] = url.split(",", 2);
const decoded = Buffer.from(base64, "base64url").toString("utf8");
const schema = JSON.parse(decoded);
const sourceMap = new SourceMapConsumer(schema);
return new ActualSourceMap(sourceMap);
} catch (error) {
console.warn("Failed to parse source map URL", url);
}
return defaultSourceMap;
}
function lineTo1BasedLine(line?: number): number {
return numberIsValid(line) ? line + 1 : 1;
}
function lineTo0BasedLine(line?: number): number {
return numberIsValid(line) ? line - 1 : 0;
}
function lineToLine(line?: number): number {
return numberIsValid(line) ? line : 0;
}
function columnToColumn(column?: number): number {
return numberIsValid(column) ? column : 0;
}
function locationIsValid(location: Location): location is Location {
const { line, column } = location;
return numberIsValid(line) && numberIsValid(column);
}
function numberIsValid(number?: number): number is number {
return typeof number === "number" && isFinite(number) && number >= 0;
}
function unknownToError(error: unknown): string {
if (error instanceof Error) {
const { message } = error;
return message;
}
return String(error);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
export type Protocol = {
$schema: string;
title: string;
description: string;
type: "object";
definitions: Record<string, Type>;
};
export type Type = {
description?: string;
} & (
| {
type: "number" | "integer" | "boolean";
}
| {
type: "string";
enum?: string[];
enumDescriptions?: string[];
}
| {
type: "object";
properties?: Record<string, Type>;
required?: string[];
}
| {
type: "array";
items?: Type;
}
| {
type?: undefined;
$ref: string;
}
| {
type?: undefined;
allOf: Type[];
}
);
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "NodeNext",
"target": "esnext",
"moduleResolution": "nodenext",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
// "composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/**/*.ts"]
}
+446
View File
@@ -0,0 +1,446 @@
:host {
--bun-error-color: #e33737;
--bun-error-monospace:
ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace",
"Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--bun-error-width: 512px;
}
:host a {
color: inherit;
text-decoration: none;
}
:host a:hover {
text-decoration: underline;
}
#BunErrorOverlay-container {
box-shadow:
0px 16px 24px rgba(0, 0, 0, 0.06),
0px 2px 6px rgba(0, 0, 0, 0.1),
0px 0px 1px rgba(0, 0, 0, 0.04);
backdrop-filter: blur(42px);
backface-visibility: visible;
border: inset 1px solid rgba(0, 0, 0, 0.2);
border-radius: 17px;
background-color: rgba(255, 255, 255, 0.92);
width: var(--bun-error-width);
position: fixed;
top: 120px;
right: 48px;
z-index: 999999;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
}
.BunErrorRoot--FullPage #BunErrorOverlay-container {
position: static;
top: unset;
right: unset;
margin: 60px auto;
}
.BunError-error-message--quoted {
color: rgb(25, 46, 9);
}
:host a {
color: inherit;
}
.BunError-Summary-ErrorIcon {
content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAgCAYAAAB3j6rJAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJiSURBVHgBzZdLUttAEIa7RwpVeKUD4Ip8hFSB7V3kG8AJgBPgG+CcAG4QcwPfIFpiigU5gRWcqmy1AVwVaTo9Y+yyiEeakc3jX7j0GEmfe7r/6QH4IELYUJNuN/LyPMwRU2g04lYcp1BDtUEYIPSJvvNhtHI5QcTB3vX1FThKQE0xxI8XEEohEQ2n+/sROKoWyP3BwYn6qHGA553DW4CgEFUfin6328fwmiDTbldBhFXjiHNlEkUBWMoJRCUoSNm3HB6Kx0fbsW4gnpQDQLT/l4hntlGxBlHR4NJ0mndW4D08WCWuNYgv5YXhVsJmNASi0bqbnNj9P2pKtwGiyxXxcM2tJNvd/bI3Hp82b26OJOK3dc9nc+PbHMRUrhyJgqV7QowMr4iqTK4SpNK8VvRXSnNiVphcKYhO0BLzIkvAZ5WaXCmID6AeDGFLYpO7NJWzEUSbF9EAHKTagYohgcnkjCDavKoVgqNMJrcWpKZ5AQlh46LBp6enCysQ36LuFy8tnBFZ2Tkn+cnLcv6vQ1PlypViC6I+PsiEUB3ZomMLLZ+Mm+Nxzwgy7XQmsMVKKVWe95q3t7E6LEyNi3ktRZSWrTWl8rxl5JcgVeZlUJI1Gq3FWsM+cQpuCn91Ov0CiEd0Bo7RkERXq2tNPps5R4UBzlU5axAdDQDrbmr5EsSvhQs7OxG4S5ucTtb7dntYxzeeNWLzi3MhWpwnxy4d3IpSX/0yxGeor0OGmPcqWHu/FixypNY2cZvSINxZ/YT3VTIHmc0u1Qm8k9R+WYO07u7SDLHH+1bnzfNGYjPkhfJIbdr/AfAS02LsKGiuAAAAAElFTkSuQmCC");
width: 20px;
height: 19px;
margin-right: 6px;
display: block;
}
.BunError-Summary-CloseIcon {
content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKKSURBVHgBxZjpTsMwEISnaTmLKLf4AX94/7fiFlRcJaUKl6exK8e1k7XTY6QVUbDjj/V4bdNBe2Uquio2nPdfKn6RqA7StKuir2JHxXZNO4LlKkb65zeWBLavYxdpelMxhABQCkaQI6QDuRrqCEoCdqjiFItXoeIagex1Gzqfo8zUMsSxByo+Vfz4fokaqH0sVx09xhxcCOwY5RSuQgbuHVZ58YGx0RlWK8LtoYT74wsXjEXyHM3eW4Y4JgFZ76ZV2xancAPrE+3T44MNRiCp2bnd5MK29A0rfyFsP7VRz3pxLOxoF8ctFZeYz7wRPfOgn/mHX6B5Rui1zP7gDmQaWc8TlEXytwGKKpy+dRoYMG41Um+52fHBuVBG0jH6ZpAtyMVV20MYLgRFq+xBpm1TFrjtbAo7dfUAI1SzxMr9ocF8UFIPUx2TsQxx4pTQ9G7migVAVYBiwagQnK0kKBsotdITblDz7eRDgAH7QZrop9CBj/67gbywVmTAxGdxVKEeGtoQKgkusz4QoxDUbK+zZOCibkzGW/yYtMawTNx73tPkJ/CXEj7zMEjPSY7zRWYNJtUwAGVWX2i1sgi/QabcgJn7X4p8JSEEJ139467TqS/oxAvup34+QrhOmR1ijHLV89uS2xY9+WjPN7N3hbRiu0hNj1U2BKfzGesVs/XCBzc7r0j32iLEbE1Xs2/aHtHivzQtxEzNTia+VUIoGnaA1Yl3iEptDC1fblGcb2nRbSNC3cGZpbq6MtGdCJf6f7Qm0c+38BwiJANKbzexeoJegT7FZOIA5SbdFjDXUJO6RrFTRChe82Jv7PQPbcFyMJZ0aOMd3qx47SMoyw5B6VnjF2aECyjXMFEl6B+GYop4atKAigAAAABJRU5ErkJggg==");
width: 22px;
height: 22px;
border-radius: 50%;
cursor: pointer;
}
.BunError-Summary-CloseIcon:hover {
transform: scale(1.2);
background-color: rgb(255, 255, 255);
}
.BunError-Summary {
display: grid;
grid-template-columns: min-content auto min-content min-content;
grid-template-rows: 46px;
align-items: center;
padding: 0 18px;
border-bottom: 1px solid rgb(220, 220, 220);
}
.BunError-footer {
display: flex;
border-top: 1px solid rgb(220, 220, 220);
align-items: center;
grid-template-columns: auto auto;
justify-content: space-between;
}
.BunError-footerItem {
padding: 12px 18px;
}
.BunError-Summary-Title {
font-weight: 500;
letter-spacing: 0.36px;
}
.BunError-ErrorTag,
.BunError-error-code {
color: rgb(165, 165, 165);
font-weight: 500;
font-size: 12pt;
}
.BunError-ErrorTag {
font-size: 14px;
text-transform: uppercase;
font-weight: 300;
}
.BunError-error-header {
display: flex;
align-items: center;
gap: 0.5ch;
}
.BunError-error-message {
color: var(--bun-error-color);
font-size: 16pt;
font-weight: bold;
}
.BunError-list {
margin-top: 14px;
gap: 14px;
}
.BunError-error-subtitle,
.BunError-error-header,
.BunError-error-message {
padding-left: 18px;
padding-right: 18px;
}
.BunError-error-subtitle {
font-size: 500;
}
.BunError-NativeStackTrace {
margin-top: 0;
width: 100%;
}
.BunError-NativeStackTrace-filename {
padding: 8px 18px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-weight: 500;
letter-spacing: 0.36px;
margin-bottom: 8px;
display: block;
}
.BunError-NativeStackTrace-filename:hover {
text-decoration: underline;
}
.BunError-SourceLines-lines a {
text-decoration: none;
}
.BunError-SourceLine-text::selection,
.BunError-SourceLine-text *::selection {
background-color: #6437e3;
color: white;
}
.BunError-SourceLine-text:active:before {
width: 61px;
z-index: -1;
pointer-events: none;
}
.BunError-SourceLines-lines {
}
.BunError-SourceLine {
display: grid;
grid-template-columns: min-content auto;
}
.BunError-SourceLines {
font-size: 14px;
align-items: center;
overflow-x: auto;
}
.BunError-CopyButton {
font-weight: 500;
font-size: 1em;
vertical-align: middle;
display: flex;
align-content: center;
align-items: center;
border-right: 1px solid #ccc;
background: #fcfcfc;
border-bottom-left-radius: 12px;
color: #333;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
transition: transform 0.1s linear;
}
.BunError-CopyButton,
#BunError-poweredBy {
padding: 12px 12px;
}
.BunError-Summary-help {
display: flex;
white-space: nowrap;
gap: 9px;
margin-right: 18px;
border-right: 1px solid rgb(220, 220, 220);
height: 100%;
align-items: center;
padding-right: 18px;
text-decoration: none;
cursor: pointer;
}
.BunError-Summary-help svg {
opacity: 0.5;
}
#BunErrorOverlay-container .BunError-Summary-help {
color: #999;
}
#BunErrorOverlay-container .BunError-Summary-help:hover {
color: #5865f2;
}
#BunErrorOverlay-container .BunError-Summary-help:hover svg {
opacity: 1;
}
.BunError-CopyButton svg {
width: 18px;
margin-right: 0.7em;
opacity: 0.75;
margin-left: 6px;
fill: currentColor;
stroke: currentColor;
}
.BunError-CopyButton:hover {
background: #6437e3;
color: white;
border-right-color: #6437e3;
}
.BunError-CopyButton:active {
transform: scale(1.03, 1.03);
transform-origin: center center;
}
.BunError-SourceLine-text {
white-space: pre;
cursor: text;
display: block;
position: relative;
font-family: var(--bun-error-monospace);
}
.BunError-SourceLine-number {
font-variant: tabular-nums;
display: block;
border-left: 4px solid transparent;
margin-left: -1px;
cursor: pointer;
padding-left: 14px;
padding-right: 12px;
text-align: right;
text-decoration: none;
position: relative;
}
.BunError-SourceLine-number,
.BunError-SourceLine-number * {
user-select: none;
-webkit-user-select: none;
}
.BunError-SourceLine-number:active + .BunError-SourceLine-text {
background-color: #6437e3;
color: white;
}
.BunError-SourceLine-number:hover {
background-color: #7443fa;
color: white;
}
.BunError-SourceLine:hover .BunError-SourceLine-number {
border-left-color: #7443fa;
}
.BunError-SourceLine-number--empty {
color: rgb(165, 165, 165);
}
.BunError-SourceLine-number,
.BunError-SourceLine-text {
height: 18px;
}
.BunError-SourceLines-highlighter--0,
.BunError-SourceLines-highlighter--1,
.BunError-SourceLines-highlighter--2,
.BunError-SourceLines-highlighter--3,
.BunError-SourceLines-highlighter--4,
.BunError-SourceLines-highlighter--5 {
position: absolute;
}
#BunErrorOverlay-container .BunError-SourceLine-text--highlight {
color: #e33737;
}
#BunError-poweredBy {
height: 16px;
content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPMAAAAgCAYAAADHXE6+AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AYht+mSqVUHOwg4pChCooFURFHqWIRLJS2QqsOJpf+QZOGJMXFUXAtOPizWHVwcdbVwVUQBH9AHJ2cFF2kxO+SQosY7zju4b3vfbn7DhAaFaaaXROAqllGKh4Ts7lVMfCKAII0RzEmMVNPpBcz8Bxf9/Dx/S7Ks7zr/hy9St5kgE8knmO6YRFvEM9sWjrnfeIwK0kK8TnxuEEXJH7kuuzyG+eiwwLPDBuZ1DxxmFgsdrDcwaxkqMTTxBFF1ShfyLqscN7irFZqrHVP/sJQXltJc53WEOJYQgJJiJBRQxkVWIjSrpFiIkXnMQ//oONPkksmVxmMHAuoQoXk+MH/4HdvzcLUpJsUigHdL7b9MQwEdoFm3ba/j227eQL4n4Erre2vNoDZT9LrbS1yBPRtAxfXbU3eAy53gIEnXTIkR/LTEgoF4P2MvikH9N8CwTW3b61znD4AGerV8g1wcAiMFCl73ePdPZ19+7em1b8foeJyunuPxOcAAAAGYktHRAC0ABcAF4a+JZkAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmBwoKLQocPFhsAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAACkNJREFUeNrtXT1sG0cW/rg2yMLcFKYAk2pMQDq6kXDCsQkNA3ETxYXTJC7uKl+TNGnuBzinuKuuSopclavjFEmKOI1VOK4UwLaaCLHBNBIkgGpIBhBd3PKKJWAyxb4Jn55mdmd3uTJp7wMWS3Jn3ryZfd+8n5ldFpBTThmT4ziF8XjsAigDqAK4BGBC38tUrAzggqb6L3Q+ANCnc04aKuRDkFNGdAvAKh1VBlpJk5h8hwCeAngM4EE+zDmYc8qeti3LTVK00QdwNwd1QOfyIcgpI/rzGRiUMoBr9PnZax/O5DqXUwZUfgkTx63XfdDP53qX0ysAZgC4Te72UMhRDYnZh3T06TBRlR0mHgeatsuUMyhrwgNo6nBv4/eivSF5H49MsuYxc05ZUBXANzHKT2bU7ucAvmXfPwbwjmXs/aeQ618bgMzpAYBP2PcbAO5E1HkM4J/s+yqAf1u29bmcCHLLPD9UArAGwAOwb1mnAcAF0AYwWsA+TxBkph8COBTKeYkAsRkj7q45juMAmIzH4zhxeiGibMFQhk9CruM4DmvXpTphE9WKmAD/Y+nV3ACwAeADPmY5mOeH/gBgnT53LMBZAXCdPo8I0IsG5GcA/h5iLZ8B+B7AZyGg4CC7QGAqhIBUC1wC4okZ4Ny5c4UXL14ULCeSsqheFtcnBg+mTIC0BTKvewfAv37rQ46hubLMus8mKho+LwqQbUO8p3TYAPMXS+CdovF4DLLqv5WNAHKcPoS1XaUYuZpgHK+Rhc7BnNPcA1mCNKre4xAQRbbJrfpJXMebGGLSKqbLa0nodu5m55QlzTqbvWJR5gFmu9VzllaZ85Tu9i3KDySlDeWqn2fB+jqALsVrALAMoEYu3/8AHFFyJspVrACok+vnARgwnpKaVOeJgVcTgA9gN+T6MfQJoxKA3wF4g2Q5juhDnfrcpjLL9JuOfwnAZQBLrJ89Gr+o8akRbxDvnsW4xnHVVZ/9kLGXfUWC8U0K5rgguE3WSwKiIOLrL2c5e8RMoKWZwFZEP8oJeG0AeKTAvEZHA0Ea/m2mcIquEqh2DQyXAbxFE4MkD8COUKwaKQsMCtMieUwJngbJrEsYrVNCqSTKX6V2nmgSTC2SXYGzya5x/jreinoItjF6hoTVpmZ81NikpSsA3tTI5QHYEjJtMpnuG/hdZuM7oGPWVIV56aiKYK11I4LHISWB+nPmjcQB5F0ES2pDFgt/FCOOrnI3u8QSKS0Cpg/gOQnlMkta1ChfHSeXENTNr9Dh0vVtBtqBmAgkmGuCf1vTplLWkbD2Cog+WeMik6PBlJjXc9m5wX7vCiC3xLUhyerS+SaAexreHMg+1RtRnespweKyPg8YX3XtpgB0j67XaGxGhsmBTwjICMx3EtYd0jjfXfBQQ66NA8HGEJXhtrbuupi5QcDZEZauxT4fCZeSK/gPAPYMda8yKzcSSiWV0xVWTSrdRaaYOqXWWaQWyVOhs87LqBEgnmh4r4XwVpOIq+HdZP0ZUF1fgK2WQnEaGpnUBFJhsu+wSajGQNvWjL+6vo/5W8MuMCu2gSDbvaj0yPD7U8oBrMa1zNJVlJa3TWBqMhesyxTJZTd+L6RuUShPh1k1lylijclykTyHJdZmjXkTHQEaRVsai7JD8pbobAoZHmrq1lg/dzXXd1meYR3TjRxFZuk94u0Lq7cN4N2USiH7q9p6j/p7hWRUIUszxOupiRAjKzoA8F+WVJLx6AqA9w3uZhXTTG4fwKcLDmpTCGENZl3+3bT5oC3cYunuhtXdN9TthrjVSpGO2ASiaMnAo8Ksn8k1PDJYfz6ZeSFu5ygkIdRl4YorZArj3UvpZpv4eqy/XKYR8zqUqw3hTan6WYJZPZtsOu4B+BtO71/WKfNnFvH1K03nDQqgoxFZlJK4+SURKyOC50VR3iMl43FzhYHDJ8u2pAF7T7iAFXY27bVNusGiyM42vEsiFkcEYAcC+HHoOOSaJ8ZHyfAj8wa4t+QyOXoJ5ZllMqqPYBfY+xZl/wHgQwb+oWUb5VcBzI4BtAgBtARw2oHoCMtcYS73QFwvigmhE5EU0h0lpuReAjCn4e1ndB+TxLQ9Jk/d4Gntzome2k4OVWGd44B54a36+ZiWy9Uo5ZD9XrRQrKEGzOsMEMvCKvBEWZ1AUtK42GCew4BctCzIQ7B8l4RKlpNFXHITgv5nip15VrsR4bq/DFqNUXaFJZXieAjvGWJuU8y+EJZ52VC2ZgDksSGWNdX1NO6lz8rVNVa3y6z2EuMzMEwUUcpdjwEA6SK7EcBbDnGt6yH1KinuYz1CHtPYd0ROgLvYe2egfzZe3Tuwe4xRx/OQzjYbVa6Rm76B6X7pOwjWe5N4D2e+7q2zzGt0I0eGpIi80R12rQn9LqgrBgVS1uI5pjujahqr2yXedQbYnsFlV8tY69An5NQSUlzr3WOgMS1rKd4jAF+IyYrv/upq6rkp7qOpv3w8dZPfQHg9CLlPWVncrxDsqVZv31TAu0Qg1rm/YdspOYjUw/+2oeANOpJu15w7N9sl1+IHlpxaw8lNGj2h5Hy9eJMUfcDq8qWZjgGENeHijTTxHc9A6/i0qT21FRGUVPOZLOsWySgd7VF9V/BWbj9/hNEzuLNAsEtOjY9ya5sM8EmpRaDuML5rFvFvh927MuvXWa0tVy2TW7aAlu8CO0gZD98Fe5hh0cDcJqW8aaizo1HWbSrvEujrhlhzK0ShWhq3mtMRTu/M0sWE2zSjq91sLYMscZM7IwTrtpsM0E0D74fiN74G7WL6HDLEBNlIeB/bNOYmmbowL6ftYbo9le8XWERSGy04fZkCzAfkvS0EmHUx88/Q7y/uItgC2QkB6j5OZ2x94vldSEKFW3vfoEx7QnlNluOIElQ6N1w9tKGTpafJAeji5i2DfD6NzZahn/epbV8zrlvMU/Bgl/XmuYaeQS7V362ISWpf3Ituhjo3yaj+EMHGER3A+wlluIfpO74W0jKrmXmfxZ9DRGc21U4m4OT2y6Glct4ny/XcUL5HIC1auMge8Sthuow1iqh3HyfXYaP6uc3iUdt+qgdVKkwBfTZBHVP/bVzcEU1KfDy2yXO6GHPsh+Lep6WzTv4MEWwuMbX7KYJNJVHxtuzD9xau+mTewYyEcWXaej0LIMUhH/E2PgxmLG/cduLy82bQZ4jYOmsXu5AiwTTRfH9GYO1HuN+fwO6hjglNDn9lvx1auOovPZudv2nk9SX1vPZNESvPam35QYRCT1IC+SkB7i+O4/Qt5fmjJp+hA/4HAHqO40yYZT6IAPKBeDvJ/yPce/WK32EE377FJHIANjteZ8mXL7CYb3rMyZ4u4/TarRcS76ehNxAsQa1g+qdxZXZ2LYHcJ6AdQv/uaO0bNB3HwXg8nigg0Yv7qkwmDoqfLPlWAYx18bR4Q6eiCzj5p3hJ4nDdSwv6UlBgmmHtYPHe8phTfOKPR6qHMdrIdsdXIcIiVwVoJgkVv2Bp/W3L2cqftnxOOeWUU0C/AoLy/OlRotLzAAAAAElFTkSuQmCC");
}
#BunError-SourceLine-text-highlightExpression {
font-weight: bold;
text-decoration-style: wavy;
}
.BunError-JSException--TypeError #BunError-SourceLine-text-highlightExpression {
border: 1px solid rgba(0, 0, 0, 0.2);
}
.BunError-Indented {
display: inline-block;
user-select: none;
-webkit-user-select: none;
}
.BunError-divet {
vertical-align: bottom;
user-select: none;
-webkit-user-select: none;
}
.BunError-error-typename {
font-family: var(--bun-error-monospace);
color: #e39437;
font-weight: bold;
}
.BunError-error-muted {
font-weight: normal;
user-select: none;
-webkit-user-select: none;
}
.BunError-error-muted,
.BunError-StackFrame--muted {
color: rgb(165, 165, 165);
}
.BunError-NativeStackTrace .BunError-error-typename {
user-select: none;
-webkit-user-select: none;
}
.BunError-StackFrame-link {
text-align: right;
}
.BunError-StackFrame-link-content {
display: flex;
justify-content: flex-end;
text-align: right;
gap: 0.25ch;
white-space: nowrap;
}
.BunError-StackFrame {
display: table-row;
}
.BunError-StackFrame:hover {
border-left-color: #5865f2;
}
.BunError-StackFrame-identifier {
padding-right: 18px;
font-size: 0.8em;
font-family: var(--bun-error-monospace);
letter-spacing: 0.49px;
width: var(--max-length, auto);
max-width: 48ch;
white-space: nowrap;
text-overflow: clip;
overflow: hidden;
}
.BunError-error-message--mono {
font-family: var(--bun-error-monospace);
}
.BunError-StackFrame-identifier,
.BunError-StackFrame-link {
display: table-cell;
font-weight: 500;
}
.BunError-BuildError {
padding-bottom: 18px;
}
.BunError-StackFrame-link-content {
font-size: 0.8em;
}
.BunError-StackFrames {
display: table;
table-layout: auto;
width: 100%;
box-sizing: border-box;
margin: 0 auto;
border-radius: 4px;
line-height: 1.2;
border-collapse: separate;
border-spacing: 12px 6px;
background-color: rgb(244, 244, 244);
}
.BunError-StackFrames-container {
overflow-x: auto;
max-width: var(--bun-error-width);
}
+14
View File
@@ -0,0 +1,14 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-error",
"dependencies": {
"preact": "^10.27.2",
},
},
},
"packages": {
"preact": ["[email protected]", "", {}, "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg=="],
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff
+362
View File
@@ -0,0 +1,362 @@
import { normalizedFilename, StackFrameIdentifier, thisCwd } from "./index";
import { StackFrameScope, type JSException, type Message, type Problems } from "./schema";
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: JSException[]): 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 && stack.frames.length > 0) {
var frames = stack.frames;
if (stack.source_lines.length > 0) {
const {
file: _file = "",
function_name = "",
position: { line = -1, column = -1 } = { line: -1, column: -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 = -1 } = { line: -1, column: -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 = -1 } = { line: -1, column: -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;
});
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "bun-error",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"scripts": {
"build": "bun build --production --define:process.env.NODE_ENV=\"'production'\" --minify index.tsx bun-error.css --outdir=dist --target=browser --format=esm"
},
"dependencies": {
"preact": "^10.27.2"
}
}
+10
View File
@@ -0,0 +1,10 @@
# bun-error
<img width="687" alt="image" src="https://user-images.githubusercontent.com/709451/162382958-23614e8f-239c-4ba6-be75-b76ceef8227c.png">
`bun-error` is a frontend component for rendering error messages and stack traces from Bun. It's used in:
- `bun dev`
- `Bun.serve`
It is heavily tied to the implementation details of Bun, and is not intended to be used outside of Bun.
+148
View File
@@ -0,0 +1,148 @@
// Based on https://github.com/stacktracejs/error-stack-parser/blob/master/error-stack-parser.js
import type { StackFramePosition, StackFrameScope, StackFrame as StackFrameType } from "./schema";
export class StackFrame implements StackFrameType {
function_name: string;
file: string;
position: StackFramePosition;
scope: StackFrameScope;
lineText: string = "";
remapped: boolean = false;
constructor({
functionName: function_name = "",
fileName: file = "",
lineNumber: line = -1,
columnNumber: column = -1,
source = "",
}) {
this.function_name = function_name;
this.file = file;
if (source) this.lineText = source;
this.scope = 3;
this.position = {
line: line,
column: column,
source_offset: -1,
line_start: -1,
line_stop: -1,
column_start: column,
column_stop: -1,
expression_start: -1,
expression_stop: -1,
};
}
}
const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/;
const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/;
export default class RuntimeError {
original: Error;
stack: StackFrame[];
static from(error: Error): RuntimeError {
const runtime = new RuntimeError();
runtime.original = error;
runtime.stack = this.parseStack(error);
return runtime;
}
/**
* Given an Error object, extract the most information from it.
*
* @param {Error} error object
* @return {Array} of StackFrames
*/
static parseStack(error) {
if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) {
return this.parseV8OrIE(error);
} else if (error.stack) {
return this.parseFFOrSafari(error);
} else {
return [];
}
}
// Separate line and column numbers from a string of the form: (URI:Line:Column)
static extractLocation(urlLike) {
// Fail-fast but return locations like "(native)"
if (urlLike.indexOf(":") === -1) {
return [urlLike];
}
var regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
var parts = regExp.exec(urlLike.replace(/[()]/g, ""));
return [parts[1], parts[2] || undefined, parts[3] || undefined];
}
static parseV8OrIE(error) {
var filtered = error.stack.split("\n").filter(function (line) {
return !!line.match(CHROME_IE_STACK_REGEXP);
}, this);
return filtered.map(function (line) {
if (line.indexOf("(eval ") > -1) {
// Throw away eval information until we implement stacktrace.js/stackframe#8
line = line.replace(/eval code/g, "eval").replace(/(\(eval at [^()]*)|(\),.*$)/g, "");
}
var sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "(");
// capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in
// case it has spaces in it, as the string is split on \s+ later on
var location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/);
// remove the parenthesized location from the line, if it was matched
sanitizedLine = location ? sanitizedLine.replace(location[0], "") : sanitizedLine;
var tokens = sanitizedLine.split(/\s+/).slice(1);
// if a location was matched, pass it to extractLocation() otherwise pop the last token
var locationParts = this.extractLocation(location ? location[1] : tokens.pop());
var functionName = tokens.join(" ") || undefined;
var fileName = ["eval", "<anonymous>"].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0];
return new StackFrame({
functionName: functionName,
fileName: fileName,
lineNumber: locationParts[1],
columnNumber: locationParts[2],
source: line,
});
}, this);
}
static parseFFOrSafari(error) {
var filtered = error.stack.split("\n").filter(function (line) {
return !line.match(SAFARI_NATIVE_CODE_REGEXP);
}, this);
return filtered.map(function (line) {
// Throw away eval information until we implement stacktrace.js/stackframe#8
if (line.indexOf(" > eval") > -1) {
line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");
}
if (line.indexOf("@") === -1 && line.indexOf(":") === -1) {
// Safari eval frames only have function names and nothing else
return new StackFrame({
functionName: line,
});
} else {
var functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/;
var matches = line.match(functionNameRegex);
var functionName = matches && matches[1] ? matches[1] : undefined;
var locationParts = this.extractLocation(line.replace(functionNameRegex, ""));
return new StackFrame({
functionName: functionName,
fileName: locationParts[0],
lineNumber: locationParts[1],
columnNumber: locationParts[2],
source: line,
});
}
}, this);
}
}
+108
View File
@@ -0,0 +1,108 @@
// Shape of the JSON payload the `Bun.serve({ development: true })` error page
// embeds in `<script id="__bunfallback" type="application/json">`.
// Produced by src/runtime/server/DevErrorPage.rs.
export enum StackFrameScope {
Eval = 1,
Module = 2,
Function = 3,
Global = 4,
Wasm = 5,
Constructor = 6,
}
export interface StackFramePosition {
/** 1-based; -1 when the frame has no source position */
line: number;
/** 1-based; -1 when the frame has no source position */
column: number;
// Only set on frames built client-side (runtime-error.ts, source-map remapping in index.tsx).
source_offset?: number;
line_start?: number;
line_stop?: number;
column_start?: number;
column_stop?: number;
expression_start?: number;
expression_stop?: number;
}
export interface StackFrame {
function_name: string;
file: string;
position: StackFramePosition;
scope: StackFrameScope;
}
export interface SourceLine {
/** 1-based */
line: number;
text: string;
}
export interface StackTrace {
source_lines: SourceLine[];
frames: StackFrame[];
}
export interface JSException {
name?: string;
message?: string;
/** `JSRuntimeType` flags */
runtime_type?: number;
/** `JSErrorCode` */
code?: number;
stack?: StackTrace;
}
export const enum MessageLevel {
err = 1,
warn = 2,
note = 3,
info = 4,
debug = 5,
}
export interface Location {
file: string;
namespace: string;
line: number;
column: number;
line_text: string;
offset: number;
}
export interface MessageData {
text?: string;
location?: Location;
}
export interface MessageMeta {
/** The specifier that failed to resolve, for resolve errors. */
resolve?: string;
build?: boolean;
}
export interface Message {
level: MessageLevel;
data: MessageData;
notes: MessageData[];
on: MessageMeta;
}
export interface Log {
warnings: number;
errors: number;
msgs: Message[];
}
export interface Problems {
exceptions: JSException[];
build: Log;
}
export interface FallbackMessageContainer {
/** One-line summary of what failed, e.g. "GET /foo failed" */
message?: string;
problems?: Problems;
cwd?: string;
}
+331
View File
@@ -0,0 +1,331 @@
// Accelerate VLQ decoding with a lookup table
const vlqTable = new Uint8Array(128);
const vlqChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
vlqTable.fill(0xff);
for (let i = 0; i < vlqChars.length; i++) vlqTable[vlqChars.charCodeAt(i)] = i;
export function parseSourceMap(json) {
if (json.version !== 3) {
throw new Error("Invalid source map");
}
if (!(json.sources instanceof Array) || json.sources.some(x => typeof x !== "string")) {
throw new Error("Invalid source map");
}
if (typeof json.mappings !== "string") {
throw new Error("Invalid source map");
}
const { sources, sourcesContent, names, mappings } = json;
const emptyData = new Int32Array(0);
for (let i = 0; i < sources.length; i++) {
sources[i] = {
name: sources[i],
content: (sourcesContent && sourcesContent[i]) || "",
data: emptyData,
dataLength: 0,
};
}
const data = decodeMappings(mappings, sources.length);
return { sources, names, data };
}
// ripped from https://github.com/evanw/source-map-visualization/blob/gh-pages/code.js#L179
export function decodeMappings(mappings, sourcesCount) {
const n = mappings.length;
let data = new Int32Array(1024);
let dataLength = 0;
let generatedLine = 0;
let generatedLineStart = 0;
let generatedColumn = 0;
let originalSource = 0;
let originalLine = 0;
let originalColumn = 0;
let originalName = 0;
let needToSortGeneratedColumns = false;
let i = 0;
function decodeError(text) {
const error = `Invalid VLQ data at index ${i}: ${text}`;
throw new Error(error);
}
function decodeVLQ() {
let shift = 0;
let vlq = 0;
// Scan over the input
while (true) {
// Read a byte
if (i >= mappings.length) decodeError("Expected extra data");
const c = mappings.charCodeAt(i);
if ((c & 0x7f) !== c) decodeError("Invalid character");
const index = vlqTable[c & 0x7f];
if (index === 0xff) decodeError("Invalid character");
i++;
// Decode the byte
vlq |= (index & 31) << shift;
shift += 5;
// Stop if there's no continuation bit
if ((index & 32) === 0) break;
}
// Recover the signed value
return vlq & 1 ? -(vlq >> 1) : vlq >> 1;
}
while (i < n) {
let c = mappings.charCodeAt(i);
// Handle a line break
if (c === 59 /* ; */) {
// The generated columns are very rarely out of order. In that case,
// sort them with insertion since they are very likely almost ordered.
if (needToSortGeneratedColumns) {
for (let j = generatedLineStart + 6; j < dataLength; j += 6) {
const genL = data[j];
const genC = data[j + 1];
const origS = data[j + 2];
const origL = data[j + 3];
const origC = data[j + 4];
const origN = data[j + 5];
let k = j - 6;
for (; k >= generatedLineStart && data[k + 1] > genC; k -= 6) {
data[k + 6] = data[k];
data[k + 7] = data[k + 1];
data[k + 8] = data[k + 2];
data[k + 9] = data[k + 3];
data[k + 10] = data[k + 4];
data[k + 11] = data[k + 5];
}
data[k + 6] = genL;
data[k + 7] = genC;
data[k + 8] = origS;
data[k + 9] = origL;
data[k + 10] = origC;
data[k + 11] = origN;
}
}
generatedLine++;
generatedColumn = 0;
generatedLineStart = dataLength;
needToSortGeneratedColumns = false;
i++;
continue;
}
// Ignore stray commas
if (c === 44 /* , */) {
i++;
continue;
}
// Read the generated column
const generatedColumnDelta = decodeVLQ();
if (generatedColumnDelta < 0) needToSortGeneratedColumns = true;
generatedColumn += generatedColumnDelta;
if (generatedColumn < 0) decodeError("Invalid generated column");
// It's valid for a mapping to have 1, 4, or 5 variable-length fields
let isOriginalSourceMissing = true;
let isOriginalNameMissing = true;
if (i < n) {
c = mappings.charCodeAt(i);
if (c === 44 /* , */) {
i++;
} else if (c !== 59 /* ; */) {
isOriginalSourceMissing = false;
// Read the original source
const originalSourceDelta = decodeVLQ();
originalSource += originalSourceDelta;
if (originalSource < 0 || originalSource >= sourcesCount) decodeError("Invalid original source");
// Read the original line
const originalLineDelta = decodeVLQ();
originalLine += originalLineDelta;
if (originalLine < 0) decodeError("Invalid original line");
// Read the original column
const originalColumnDelta = decodeVLQ();
originalColumn += originalColumnDelta;
if (originalColumn < 0) decodeError("Invalid original column");
// Check for the optional name index
if (i < n) {
c = mappings.charCodeAt(i);
if (c === 44 /* , */) {
i++;
} else if (c !== 59 /* ; */) {
isOriginalNameMissing = false;
// Read the optional name index
const originalNameDelta = decodeVLQ();
originalName += originalNameDelta;
if (originalName < 0) decodeError("Invalid original name");
// Handle the next character
if (i < n) {
c = mappings.charCodeAt(i);
if (c === 44 /* , */) {
i++;
} else if (c !== 59 /* ; */) {
decodeError("Invalid character after mapping");
}
}
}
}
}
}
// Append the mapping to the typed array
if (dataLength + 6 > data.length) {
const newData = new Int32Array(data.length << 1);
newData.set(data);
data = newData;
}
data[dataLength] = generatedLine;
data[dataLength + 1] = generatedColumn;
if (isOriginalSourceMissing) {
data[dataLength + 2] = -1;
data[dataLength + 3] = -1;
data[dataLength + 4] = -1;
} else {
data[dataLength + 2] = originalSource;
data[dataLength + 3] = originalLine;
data[dataLength + 4] = originalColumn;
}
data[dataLength + 5] = isOriginalNameMissing ? -1 : originalName;
dataLength += 6;
}
return data.subarray(0, dataLength);
}
export function remapPosition(decodedMappings: Int32Array, line: number, column: number) {
if (!(decodedMappings instanceof Int32Array)) {
throw new Error("decodedMappings must be an Int32Array");
}
if (!Number.isFinite(line)) {
throw new Error("line must be a finite number");
}
if (!Number.isFinite(column)) {
throw new Error("column must be a finite number");
}
if (decodedMappings.length === 0 || line < 0 || column < 0) return null;
const index = indexOfMapping(decodedMappings, line, column);
if (index === -1) return null;
return [decodedMappings[index + 3] + 1, decodedMappings[index + 4]];
}
async function fetchRemoteSourceMap(file: string, signal) {
const response = await globalThis.fetch(file + ".map", {
signal,
headers: {
Accept: "application/json",
"Mappings-Only": "1",
},
});
if (response.ok) {
return await response.json();
}
return null;
}
export var sourceMappings = new Map();
export function fetchMappings(file, signal) {
if (file.includes(".bun")) return null;
if (sourceMappings.has(file)) {
return sourceMappings.get(file);
}
return fetchRemoteSourceMap(file, signal).then(json => {
if (!json) return null;
const { data } = parseSourceMap(json);
sourceMappings.set(file, data);
return data;
});
}
// this batches duplicate requests
export function fetchAllMappings(files, signal) {
var results = new Array(files.length);
var map = new Map();
for (var i = 0; i < files.length; i++) {
const existing = map.get(files[i]);
if (existing) {
existing.push(i);
} else map.set(files[i], [i]);
}
for (const [file, indices] of [...map.entries()]) {
const mapped = fetchMappings(file, signal);
if (mapped?.then) {
var resolvers = [];
for (let i = 0; i < indices.length; i++) {
results[indices[i]] = new Promise((resolve, reject) => {
resolvers[i] = res => resolve(res ? [res, i] : null);
});
}
mapped.finally(a => {
for (let resolve of resolvers) {
try {
resolve(a);
} catch {
} finally {
}
}
resolvers.length = 0;
resolvers = null;
});
} else {
for (let i = 0; i < indices.length; i++) {
results[indices[i]] = mapped ? [mapped, indices[i]] : null;
}
}
}
return results;
}
function indexOfMapping(mappings: Int32Array, line: number, column: number) {
// the array is [generatedLine, generatedColumn, sourceIndex, sourceLine, sourceColumn, nameIndex]
// 0 - generated line
var count = mappings.length / 6;
var index = 0;
while (count > 0) {
var step = (count / 2) | 0;
var i = index + step;
// this multiply is slow but it's okay for now
var j = i * 6;
if (mappings[j] < line || (mappings[j] == line && mappings[j + 1] <= column)) {
index = i + 1;
count -= step + 1;
} else {
count = step;
}
}
index = index | 0;
if (index > 0) {
if (mappings[(index - 1) * 6] == line) {
return (index - 1) * 6;
}
}
return null;
}
+171
View File
@@ -0,0 +1,171 @@
const UNKNOWN_FUNCTION = "<unknown>";
import type { StackFrame } from "./schema";
/**
* This parses the different stack traces and puts them into one format
* This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)
*/
export function parse(stackString): StackFrame[] {
const lines = stackString.split("\n");
return lines.reduce((stack, line) => {
const parseResult = parseChrome(line) || parseWinjs(line) || parseGecko(line) || parseNode(line) || parseJSC(line);
if (parseResult) {
stack.push(parseResult);
}
return stack;
}, []);
}
const formatFile = file => {
if (!file) {
return "";
}
if (file.startsWith("blob:")) {
if (globalThis["__BUN"]?.client) {
const replacement = globalThis["__BUN"]?.client.dependencies.getFilePathFromBlob(file);
if (replacement) {
file = replacement;
}
}
}
var _file = String(file);
if (_file.startsWith(globalThis.location?.origin)) {
_file = _file.substring(globalThis.location?.origin.length);
}
while (_file.startsWith("/")) {
_file = _file.substring(1);
}
if (_file.endsWith(".bun")) {
_file = "node_modules.bun";
}
return _file;
};
const chromeRe =
/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
const chromeEvalRe = /\((\S*)(?::(\d+))(?::(\d+))\)/;
function parseChrome(line) {
const parts = chromeRe.exec(line);
if (!parts) {
return null;
}
const isNative = parts[2] && parts[2].indexOf("native") === 0; // start of line
const isEval = parts[2] && parts[2].indexOf("eval") === 0; // start of line
const submatch = chromeEvalRe.exec(parts[2]);
if (isEval && submatch != null) {
// throw out eval line/column and use top-most line/column number
parts[2] = submatch[1]; // url
parts[3] = submatch[2]; // line
parts[4] = submatch[3]; // column
}
return {
file: formatFile(!isNative ? parts[2] : null),
function_name: parts[1] || "",
position: {
line: parts[3] ? +parts[3] : null,
column_start: parts[4] ? +parts[4] : null,
},
};
}
const winjsRe =
/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseWinjs(line) {
const parts = winjsRe.exec(line);
if (!parts) {
return null;
}
return {
file: formatFile(parts[2]),
function_name: parts[1],
position: {
line: +parts[3],
column_start: parts[4] ? +parts[4] : null,
},
};
}
const geckoRe =
/^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i;
const geckoEvalRe = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
function parseGecko(line) {
const parts = geckoRe.exec(line);
if (!parts) {
return null;
}
const isEval = parts[3] && parts[3].indexOf(" > eval") > -1;
const submatch = geckoEvalRe.exec(parts[3]);
if (isEval && submatch != null) {
// throw out eval line/column and use top-most line number
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = null; // no column when eval
}
return {
file: formatFile(parts[3]),
function_name: parts[1] || "",
position: {
line: parts[4] ? +parts[4] : null,
column_start: parts[5] ? +parts[5] : null,
},
};
}
const javaScriptCoreRe = /^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
function parseJSC(line) {
const parts = javaScriptCoreRe.exec(line);
if (!parts) {
return null;
}
return {
file: formatFile(parts[3]),
function_name: parts[1] || "",
position: {
line: +parts[4],
column_start: parts[5] ? +parts[5] : null,
},
};
}
const nodeRe = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseNode(line) {
const parts = nodeRe.exec(line);
if (!parts) {
return null;
}
return {
file: formatFile(parts[2]),
function_name: parts[1] || "",
position: {
line: +parts[3],
column_start: parts[4] ? +parts[4] : null,
},
};
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
+171
View File
@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
out
@@ -0,0 +1,8 @@
# bun-devtools-frontend
This is the WebKit Web Inspector bundled as standalone assets.
Two ways to use this:
- Set `?ws=hostname:port` to connect to a `bun --inspect`'d process.
- Navigate to `/inspect/hostname:port`
+77
View File
@@ -0,0 +1,77 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "web-inspector-bun",
"dependencies": {
"esbuild": "^0.19.2",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^6.0.0",
},
},
},
"packages": {
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.0.29" } }, "sha512-E6UWZuN4ymAxzUBWVIGDHJ3Zey7I8cMzDZ+cB1BqhZsmd1uPb9iAQzpWMruY1mKzsuD3R+dZPoBkZz8QL1KhSA=="],
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long=="],
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A=="],
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "~20.11.3", "@types/ws": "~8.5.10" } }, "sha512-Z+U1ORr/2UCwxelIZxE83pyPLclviYL9UewQCNEUmGeLObY8ao+3WF3D8N1+NMv2+S+hUWsdBJam+4GoPEz35g=="],
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
"undici-types": ["[email protected]", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
}
}
@@ -0,0 +1,14 @@
{
"name": "web-inspector-bun",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^6.0.0"
},
"dependencies": {
"esbuild": "^0.19.2"
}
}
@@ -0,0 +1,171 @@
import { copyFileSync, mkdirSync, readdirSync, rmSync, statSync } from "fs";
import { join } from "path";
try {
const basePath = join(import.meta.dir, "../../../src/bun.js/WebKit/Source/WebInspectorUI/UserInterface");
const htmlPath = join(basePath, "Main.html");
const backendCommands = join(
import.meta.dir,
"../../../src/bun.js/WebKit/WebKitBuild/Release/JavaScriptCore/DerivedSources/inspector/InspectorBackendCommands.js",
);
const scriptsToBundle = [];
const stylesToBundle = [];
const jsReplacementId = crypto.randomUUID() + ".js";
const cssReplacementId = crypto.randomUUID() + ".css";
const html = new HTMLRewriter()
.on("script", {
element(element) {
const src = element.getAttribute("src");
if (
src &&
!src?.includes("External") &&
!src?.includes("WebKitAdditions") &&
!src.includes("DOMUtilities.js")
) {
if (scriptsToBundle.length === 0) {
element.replace("<script>var WI = {};\n</script>", { html: true });
} else {
element.remove();
}
scriptsToBundle.push(src);
}
},
})
.on("script:not([src])", {
element(element) {
element.remove();
},
})
.on("head", {
element(element) {
element.prepend(
`
<script type="text/javascript">
if (!Element.prototype.scrollIntoViewIfNeeded) {
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
var parent = this.parentNode,
parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
}
</script>
<base href="/" /> `,
{ html: true },
);
element.append(
`
<style>
body {
--undocked-title-area-height: 0px !important;
}
</style>
<script src="${jsReplacementId}"></script>
<script type="text/javascript">
WI.sharedApp = new WI.AppController;
WI.sharedApp.initialize();
</script>`,
{ html: true },
);
},
})
// .on("link[rel=stylesheet]", {
// element(element) {
// const href = element.getAttribute("href");
// if (href && !href?.includes("External") && !href?.includes("WebKitAdditions")) {
// element.remove();
// stylesToBundle.push(href);
// }
// },
// })
.transform(new Response(Bun.file(htmlPath)));
let htmlText = await html.text();
rmSync(join(import.meta.dir, "out"), { recursive: true, force: true });
mkdirSync(join(import.meta.dir, "out", "Protocol"), { recursive: true });
const javascript = scriptsToBundle.map(a => `import '${join(basePath, a)}';`).join("\n") + "\n";
// const css = stylesToBundle.map(a => `@import "${join(basePath, a)}";`).join("\n") + "\n";
await Bun.write(join(import.meta.dir, "out/manifest.js"), javascript);
mkdirSync("out/WebKitAdditions/WebInspectorUI/", { recursive: true });
await Bun.write(join(import.meta.dir, "out/WebKitAdditions/WebInspectorUI/WebInspectorUIAdditions.js"), "");
await Bun.write(join(import.meta.dir, "out/WebKitAdditions/WebInspectorUI/WebInspectorUIAdditions.css"), "");
// await Bun.write(join(import.meta.dir, "manifest.css"), css);
const jsBundle = await Bun.build({
entrypoints: [join(import.meta.dir, "out/manifest.js")],
outdir: "out",
minify: true,
});
const jsFilename = "manifest-" + jsBundle.outputs[0].hash + ".js";
// const cssBundle = await build({
// bundle: true,
// minify: true,
// write: false,
// entryPoints: [join(import.meta.dir, "manifest.css")],
// outdir: "out",
// loader: {
// ".css": "css",
// ".svg": "dataurl",
// },
// external: ["*.png"],
// plugins: [
// {
// name: "css",
// setup(build) {
// build.onResolve({ filter: new RegExp("/Images/Warning.svg") }, args => ({
// path: join(basePath, "Images/Warning.svg"),
// }));
// },
// },
// ],
// });
// const cssFilename = "manifest-" + cssBundle.outputFiles[0].hash.replaceAll("/", "_") + ".css";
htmlText = htmlText.replace(jsReplacementId, jsFilename);
// htmlText = htmlText.replace(cssReplacementId, cssFilename);
await Bun.write(join(import.meta.dir, "out", jsFilename), jsBundle.outputs[0]);
// await Bun.write(join(import.meta.dir, "out", cssFilename), cssBundle.outputFiles[0].text);
await Bun.write(join(import.meta.dir, "out", "index.html"), htmlText);
await Bun.write(join(import.meta.dir, "out", "index.html"), htmlText);
await Bun.write(join(import.meta.dir, "out", "Protocol", "InspectorBackendCommands.js"), Bun.file(backendCommands));
function recursiveCopy(src, dest) {
readdirSync(src).forEach(file => {
const srcPath = join(src, file);
const destPath = join(dest, file);
if (statSync(srcPath).isDirectory()) {
mkdirSync(destPath, { recursive: true });
recursiveCopy(srcPath, destPath);
} else {
rmSync(destPath, { force: true });
copyFileSync(srcPath, destPath);
}
});
}
recursiveCopy(basePath, join(import.meta.dir, "out"));
} catch (e) {
console.error("Failed to build. Please make sure you've ran `make jsc` locally.");
throw e;
}
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "nodenext",
"moduleDetection": "force",
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true,
"noImplicitAny": false,
"outDir": "dist",
"types": ["node"]
},
"include": [".", "../bun-types/index.d.ts"]
}
@@ -0,0 +1,2 @@
src/protocol/*/protocol.json linguist-generated=true
src/protocol/*/index.d.ts linguist-generated=true
@@ -0,0 +1,3 @@
index.js
protocol/*.json
protocol/v8
+354
View File
@@ -0,0 +1,354 @@
# bun-inspector-protocol
`bun-inspector-protocol` is a TypeScript library that provides a comprehensive interface for interacting with the WebKit Inspector Protocol. This package makes it easy to build debugging tools, IDE integrations, and other developer tools that communicate with Bun's JavaScript runtime.
You can use this library with Node.js or Bun.
## Overview
The WebKit Inspector Protocol is a JSON-based protocol similar to the Chrome DevTools Protocol. It allows external tools to interact with Bun's JavaScript runtime for debugging, profiling, and instrumentation purposes.
## Features
- 🌐 **WebSocket communication**: Connect to Bun's debugging endpoint via WebSockets
- 🔌 **Socket communication**: Connect via Unix/TCP sockets for local debugging
- 🔄 **Full API typing**: Complete TypeScript definitions for the protocol
- 📊 **Object preview utilities**: Format runtime objects for display
- 🔄 **Event-driven architecture**: Subscribe to specific debugging events
- 🧩 **Promise-based API**: Clean, modern async interface
## Installation
```bash
bun add bun-inspector-protocol
# npm install bun-inspector-protocol
# yarn add bun-inspector-protocol
# pnpm add bun-inspector-protocol
```
## Basic Usage
The first step is to spawn a Bun process with the inspector attached. There are a few different ways to do this.
The `--inspect-wait` flag is the easiest way to spawn a Bun process with the inspector attached.
```bash
bun --inspect-wait my-script.ts
```
From there, it will start a WebSocket server defaulting to port 9229 and you will need to read the output from stdout to get the URL of the inspector:
```bash
bun --inspect-wait my-script.ts 2>&1 | grep -o '\sws://.*$'
```
From there, you can connect to the inspector using the `WebSocketInspector` class:
```typescript
import { WebSocketInspector } from "bun-inspector-protocol";
// Create a new inspector client
const inspector = new WebSocketInspector("ws://localhost:9229/ws");
```
### Connecting via WebSocket
```typescript
import { WebSocketInspector } from "bun-inspector-protocol";
// Create a new inspector client
const inspector = new WebSocketInspector("ws://localhost:9229/ws");
// Listen for connection events
inspector.on("Inspector.connected", () => {
console.log("Connected to debugger!");
});
inspector.on("Inspector.error", error => {
console.error("Inspector error:", error);
});
// Connect to the debugger
await inspector.start();
// Enable the Runtime domain
await inspector.send("Runtime.enable");
// Execute some code in the target context
const result = await inspector.send("Runtime.evaluate", {
expression: "2 + 2",
returnByValue: true,
});
console.log("Evaluation result:", result.result.value); // 4
// Close the connection
inspector.close();
```
### Connecting via Socket (for Local Debugging)
```typescript
import { NodeSocketInspector } from "bun-inspector-protocol";
import { Socket } from "node:net";
// Create a socket connection
const socket = new Socket();
socket.connect("/path/to/debug/socket");
// Create a new inspector client
const inspector = new NodeSocketInspector(socket);
// Set up event listeners and use the API as with WebSocketInspector
inspector.on("Inspector.connected", () => {
console.log("Connected to debugger via socket!");
});
await inspector.start();
// Use the same API as WebSocketInspector from here...
```
## Event Handling
The inspector emits various events you can listen for:
```typescript
// Listen for specific protocol events
inspector.on("Debugger.scriptParsed", params => {
console.log("Script parsed:", params.url);
});
// Listen for breakpoint hits
inspector.on("Debugger.paused", params => {
console.log("Execution paused at:", params.callFrames[0].location);
});
// Listen for console messages
inspector.on("Runtime.consoleAPICalled", params => {
console.log(
"Console message:",
params.args
.map(arg =>
// Use the included utility to format objects
remoteObjectToString(arg, true),
)
.join(" "),
);
});
```
## Protocol Domains
The WebKit Inspector Protocol is organized into domains that group related functionality. Based on the JavaScriptCore protocol implementation, the following domains are available:
### Console Domain
- Console message capturing and monitoring
- Support for different logging channels and levels (xml, javascript, network, etc.)
- Methods: `enable`, `disable`, `clearMessages`, `setLoggingChannelLevel`, etc.
- Events: `messageAdded`, `messageRepeatCountUpdated`, `messagesCleared`
### Debugger Domain
- Comprehensive debugging capabilities
- Setting and managing breakpoints (conditional, URL-based, symbolic)
- Execution control (pause, resume, step, etc.)
- Stack frame inspection and manipulation
- Methods: `enable`, `setBreakpoint`, `resume`, `stepInto`, `evaluateOnCallFrame`, etc.
- Events: `scriptParsed`, `breakpointResolved`, `paused`, `resumed`
### Heap Domain
- Memory management and garbage collection monitoring
- Heap snapshot creation and analysis
- Memory leak detection with tracking
- Methods: `enable`, `gc`, `snapshot`, `startTracking`, `stopTracking`
- Events: `garbageCollected`, `trackingStart`, `trackingComplete`
### Inspector Domain
- Core inspector functionality
- Methods: `enable`, `disable`, `initialized`
- Events: `evaluateForTestInFrontend`, `inspect`
### LifecycleReporter Domain
- Process lifecycle management
- Error reporting
- Methods: `enable`, `preventExit`, `stopPreventingExit`
- Events: `reload`, `error`
### Runtime Domain
- JavaScript runtime interaction
- Expression evaluation
- Object property inspection
- Promise handling
- Type profiling and control flow analysis
- Methods: `evaluate`, `callFunctionOn`, `getProperties`, `awaitPromise`, etc.
- Events: `executionContextCreated`
### ScriptProfiler Domain
- Script execution profiling
- Performance tracking
- Methods: `startTracking`, `stopTracking`
- Events: `trackingStart`, `trackingUpdate`, `trackingComplete`
### TestReporter Domain
- Test execution monitoring
- Test status reporting (pass, fail, timeout, skip, todo)
- Methods: `enable`, `disable`
- Events: `found`, `start`, `end`
Each domain has its own set of commands, events, and data types. Refer to the TypeScript definitions in this package for complete API details.
## Working with Remote Objects
When evaluating expressions, you'll often receive remote object references. Use the `remoteObjectToString` utility to convert these to string representations:
```typescript
import { remoteObjectToString } from "bun-inspector-protocol";
const result = await inspector.send("Runtime.evaluate", {
expression: "{ a: 1, b: { c: 'hello' } }",
});
console.log(remoteObjectToString(result.result, true));
// Output: {a: 1, b: {c: "hello"}}
```
## Message Structure
The protocol uses a simple JSON-based message format:
### Requests
```typescript
interface Request<T> {
id: number; // Unique request identifier
method: string; // Domain.method name format
params: T; // Method-specific parameters
}
```
### Responses
```typescript
interface Response<T> {
id: number; // Matching request identifier
result?: T; // Method-specific result (on success)
error?: {
// Error information (on failure)
code?: string;
message: string;
};
}
```
### Events
```typescript
interface Event<T> {
method: string; // Domain.event name format
params: T; // Event-specific parameters
}
```
### Setting Breakpoints
```typescript
// Set a breakpoint by URL
const { breakpointId } = await inspector.send("Debugger.setBreakpointByUrl", {
lineNumber: 42,
url: "/app/foo.ts",
condition: "x > 5", // Optional condition
});
// Set a breakpoint with custom actions
await inspector.send("Debugger.setBreakpoint", {
location: { scriptId: "123", lineNumber: 10 },
options: {
condition: "count > 5",
actions: [
{ type: "log", data: "Breakpoint hit!" },
{ type: "evaluate", data: "console.log('Custom breakpoint action')" },
],
autoContinue: true,
},
});
// Remove a breakpoint
await inspector.send("Debugger.removeBreakpoint", { breakpointId });
```
### Memory Profiling
```typescript
// Start heap tracking
await inspector.send("Heap.enable");
await inspector.send("Heap.startTracking");
// Listen for GC events
inspector.on("Heap.garbageCollected", ({ collection }) => {
console.log(
`GC completed: ${collection.type} (${collection.endTime - collection.startTime}ms)`,
);
});
// ... perform operations to analyze ...
// Get heap snapshot
const { snapshotData } = await inspector.send("Heap.stopTracking");
// Process snapshotData to find memory leaks
```
### Script Profiling
```typescript
// Start script profiling with sampling
await inspector.send("ScriptProfiler.startTracking", { includeSamples: true });
// Listen for profiling updates
inspector.on("ScriptProfiler.trackingUpdate", event => {
console.log("Profiling event:", event);
});
// Stop profiling to get complete data
inspector.on("ScriptProfiler.trackingComplete", data => {
if (data.samples) {
// Process stack traces
console.log(`Collected ${data.samples.stackTraces.length} stack traces`);
}
});
await inspector.send("ScriptProfiler.stopTracking");
```
## Protocol Differences from Upstream WebKit
Notable Bun-specific additions include:
- `LifecycleReporter` domain for process lifecycle management
- Enhanced `TestReporter` domain for test framework integration
- Additional utilities for script and heap profiling
## Building Tools with the Protocol
This library is ideal for building:
- IDE extensions and debuggers
- Performance monitoring tools
- Testing frameworks with runtime instrumentation
- Hot module reloading systems
- Custom REPL environments
- Profiling and optimization tools
## Full API Reference
For complete API documentation, please refer to the TypeScript definitions included in this package. The definitions provide comprehensive information about all available commands, events, and their parameters.
## License
MIT
+15
View File
@@ -0,0 +1,15 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "bun-inspector-protocol",
"dependencies": {
"ws": "^8.18.3",
},
},
},
"packages": {
"ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
}
}
+5
View File
@@ -0,0 +1,5 @@
export type * from "./src/inspector/index";
export * from "./src/inspector/node-socket";
export * from "./src/inspector/websocket";
export type * from "./src/protocol/index";
export * from "./src/util/preview";
@@ -0,0 +1,19 @@
{
"name": "bun-inspector-protocol",
"version": "0.0.2",
"scripts": {
"build": "bun build --target=node --outfile=index.js --minify-syntax ./index.ts --external=ws"
},
"dependencies": {
"ws": "^8.18.3"
},
"main": "./index.js",
"type": "module",
"files": [
"index.js",
"index.ts",
"src",
"README.md",
"LICENSE"
]
}
@@ -0,0 +1,250 @@
// Regenerates src/protocol/jsc/{protocol.json,index.d.ts} from the inspector
// protocol of the WebKit build Bun links against.
//
// bun scripts/generate-protocol.ts [path/to/CombinedDomains.json] [--v8]
//
// CombinedDomains.json is what JavaScriptCore's build produces from
// Source/JavaScriptCore/inspector/protocol/*.json. The bun-webkit prebuilt
// tarball ships it at its top level, so after `bun bd` it can be found in the
// build cache for the WEBKIT_VERSION pinned in scripts/build/deps/webkit.ts;
// that is what is used when no path is given. A local WebKit build writes it
// to <build>/JavaScriptCore/DerivedSources/CombinedDomains.json.
//
// Pass --v8 to also refresh src/protocol/v8 from the Chrome DevTools protocol
// repository (requires network access).
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
import type { Domain, Property, Protocol } from "../src/protocol/schema";
function formatProtocol(protocol: Protocol, extraTs?: string): string {
const { name, domains } = protocol;
const eventMap = new Map();
const commandMap = new Map();
let body = `export namespace ${name} {`;
for (const { domain, types = [], events = [], commands = [] } of domains) {
body += `export namespace ${domain} {`;
for (const type of types) {
body += formatProperty(type);
}
for (const { name, description, parameters = [] } of events) {
const symbol = `${domain}.${name}`;
const title = toTitle(name);
eventMap.set(symbol, `${domain}.${title}`);
body += formatProperty({
id: `${title}Event`,
type: "object",
description: `${description}\n@event \`${symbol}\``,
properties: parameters,
});
}
for (const { name, description, parameters = [], returns = [] } of commands) {
const symbol = `${domain}.${name}`;
const title = toTitle(name);
commandMap.set(symbol, `${domain}.${title}`);
body += formatProperty({
id: `${title}Request`,
type: "object",
description: `${description}\n@request \`${symbol}\``,
properties: parameters,
});
body += formatProperty({
id: `${title}Response`,
type: "object",
description: `${description}\n@response \`${symbol}\``,
properties: returns,
});
}
body += "};";
}
for (const type of ["Event", "Request", "Response"]) {
const sourceMap = type === "Event" ? eventMap : commandMap;
body += formatProperty({
id: `${type}Map`,
type: "object",
properties: [...sourceMap.entries()].map(([name, title]) => ({
name: `"${name}"`,
type: undefined,
$ref: `${title}${type}`,
})),
});
}
if (extraTs) {
body += extraTs;
}
return body + "};";
}
function formatProperty(property: Property): string {
const { id, description, type, optional } = property;
let body = "";
if (id) {
if (description) {
body += `\n${toComment(description)}\n`;
}
body += `export type ${id}=`;
}
if (type === "boolean") {
body += "boolean";
} else if (type === "number" || type === "integer") {
body += "number";
} else if (type === "string") {
const { enum: choices } = property;
if (choices) {
body += choices.map(value => `"${value}"`).join("|");
} else {
body += "string";
}
} else if (type === "array") {
const { items } = property;
const itemType = items ? formatProperty(items) : "unknown";
body += `${itemType}[]`;
} else if (type === "object") {
const { properties } = property;
if (!properties) {
body += "Record<string, unknown>";
} else if (properties.length === 0) {
body += "{}";
} else {
body += "{";
for (const { name, description, ...property } of properties) {
if (description) {
body += `\n${toComment(description)}`;
}
const delimit = property.optional ? "?:" : ":";
body += `\n${name}${delimit}${formatProperty({ ...property, id: undefined })};`;
}
body += "}";
}
} else if ("$ref" in property) {
body += property.$ref;
} else {
body += "unknown";
}
if (optional) {
body += "|undefined";
}
if (id) {
body += ";";
}
return body;
}
/**
* @link https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
*/
async function downloadV8(): Promise<Protocol> {
const baseUrl = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
const domains = ["Runtime", "Console", "Debugger", "Memory", "HeapProfiler", "Profiler", "Network", "Inspector"];
return Promise.all([
download<Protocol>(`${baseUrl}/js_protocol.json`),
download<Protocol>(`${baseUrl}/browser_protocol.json`),
]).then(([js, browser]) => ({
name: "V8",
version: js.version,
domains: [...js.domains, ...browser.domains]
.filter(domain => !domains.includes(domain.domain))
.sort((a, b) => a.domain.localeCompare(b.domain)),
}));
}
async function download<V>(url: string): Promise<V> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status}: ${url}`);
}
return response.json();
}
function toTitle(name: string): string {
return name.charAt(0).toUpperCase() + name.slice(1);
}
function toComment(description?: string): string {
if (!description) {
return "";
}
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
return lines.join("\n");
}
const repoRoot = path.resolve(import.meta.dir, "..", "..", "..");
/** The CombinedDomains.json of the prebuilt WebKit pinned by scripts/build/deps/webkit.ts, if it has been downloaded. */
function findPinnedCombinedDomains(): string | undefined {
const webkitTs = readFileSync(path.join(repoRoot, "scripts", "build", "deps", "webkit.ts"), "utf-8");
const version = /^export const WEBKIT_VERSION = "([^"]+)";/m.exec(webkitTs)?.[1];
if (!version) {
throw new Error("Could not find WEBKIT_VERSION in scripts/build/deps/webkit.ts");
}
// Mirrors prebuiltDestDir() in scripts/build/deps/webkit.ts:
// <cache>/webkit-<version>[-<os>][-<arch>][-debug|-lto][-asan]/
const dirVersion = version.startsWith("autobuild-") ? version.slice("autobuild-".length) : version.slice(0, 16);
const bunInstall = process.env.BUN_INSTALL
? path.resolve(repoRoot, process.env.BUN_INSTALL)
: path.join(homedir(), ".bun");
const cacheDir = path.join(bunInstall, "build-cache");
if (!existsSync(cacheDir)) {
return undefined;
}
const glob = new Bun.Glob(`webkit-${dirVersion}*/CombinedDomains.json`);
const [match] = [...glob.scanSync({ cwd: cacheDir })].sort();
return match && path.join(cacheDir, match);
}
/**
* Domains that Bun's WebKit fork declares as debuggable from JavaScript, but that bun registers no
* agent for (see src/jsc/bindings/BunDebugger.cpp): bun answers every command in them with
* "'<domain>' domain was not found". test/cli/inspect/bun-inspector-protocol.test.ts checks that
* this is still true, so remove a domain from here once bun implements it.
*/
const domainsWithoutAgent = new Set(["File", "Process"]);
const args = process.argv.slice(2);
const includeV8 = args.includes("--v8");
const combinedDomainsPath = args.find(arg => !arg.startsWith("--")) ?? findPinnedCombinedDomains();
if (!combinedDomainsPath) {
console.error(
"Could not find CombinedDomains.json for the pinned WebKit version. " +
"Run `bun bd` first or pass the path to a WebKit build's CombinedDomains.json.",
);
process.exit(1);
}
console.log(`Reading ${combinedDomainsPath}`);
const combinedDomains: { domains: Domain[] } = await Bun.file(combinedDomainsPath).json();
const protocolDir = path.resolve(import.meta.dir, "..", "src", "protocol");
const written: string[] = [];
const write = (name: string, data: string) => {
const filePath = path.join(protocolDir, name);
writeFileSync(filePath, data);
written.push(filePath);
};
const base = readFileSync(path.join(protocolDir, "protocol.d.ts"), "utf-8");
const baseNoComments = base.replace(/\/\/.*/g, "");
const jsc: Protocol = {
name: "JSC",
version: {
major: 1,
minor: 4,
},
domains: combinedDomains.domains
.filter(domain => domain.debuggableTypes?.includes("javascript") && !domainsWithoutAgent.has(domain.domain))
.sort((a, b) => a.domain.localeCompare(b.domain)),
};
write("jsc/protocol.json", JSON.stringify(jsc, null, 2));
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
if (includeV8) {
const v8 = await downloadV8();
write("v8/protocol.json", JSON.stringify(v8));
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
}
const { status } = spawnSync("bunx", ["prettier", "--write", ...written], { cwd: repoRoot, stdio: "inherit" });
if (status !== 0) {
process.exit(status ?? 1);
}
@@ -0,0 +1,40 @@
import type { EventEmitter } from "node:events";
import type { JSC } from "../protocol";
export type InspectorEventMap = {
[E in keyof JSC.EventMap]: [JSC.EventMap[E]];
} & {
"Inspector.connecting": [string];
"Inspector.connected": [];
"Inspector.disconnected": [Error | undefined];
"Inspector.error": [Error];
"Inspector.pendingRequest": [JSC.Request];
"Inspector.request": [JSC.Request];
"Inspector.response": [JSC.Response];
"Inspector.event": [JSC.Event];
};
/**
* A client that can send and receive messages to/from a debugger.
*/
export interface Inspector extends EventEmitter<InspectorEventMap> {
/**
* Starts the inspector.
*/
start(...args: unknown[]): Promise<boolean>;
/**
* Sends a request to the debugger.
*/
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M],
): Promise<JSC.ResponseMap[M]>;
/**
* If the inspector is closed.
*/
get closed(): boolean;
/**
* Closes the inspector.
*/
close(...args: unknown[]): void;
}
@@ -0,0 +1,235 @@
import { EventEmitter } from "node:events";
import { Socket } from "node:net";
import { SocketFramer } from "../../../bun-debug-adapter-protocol/src/debugger/node-socket-framer.js";
import type { JSC } from "../protocol";
import type { Inspector, InspectorEventMap } from "./index";
/**
* An inspector that communicates with a debugger over a (unix) socket.
* This is used in the extension as follows:
*
* 1. Extension sets environment variable `BUN_INSPECT_NOTIFY` inside of all vscode terminals.
* This is a path to a unix socket that the extension will listen on.
* 2. Bun reads it and connects to the socket, setting up a reverse connection for sending DAP
* messages.
*/
export class NodeSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
#ready: Promise<boolean> | undefined;
#socket: Socket;
#requestId: number;
#pendingRequests: JSC.Request[];
#pendingResponses: Map<
number,
{
request: JSC.Request;
done: (result: unknown) => void;
}
>;
#framer: SocketFramer;
constructor(socket: Socket) {
super();
this.#socket = socket;
this.#requestId = 1;
this.#pendingRequests = [];
this.#pendingResponses = new Map();
this.#framer = new SocketFramer(socket, message => {
if (Array.isArray(message)) {
for (const m of message) {
this.#accept(m);
}
} else {
this.#accept(message);
}
});
}
private onConnectOrImmediately(cb: () => void) {
const isAlreadyConnected = this.#socket.connecting === false;
if (isAlreadyConnected) {
cb();
} else {
this.#socket.once("connect", cb);
}
}
async start(): Promise<boolean> {
if (this.#ready) {
return this.#ready;
}
if (this.closed) {
this.close();
const addressWithPort = this.#socket.remoteAddress + ":" + this.#socket.remotePort;
this.emit("Inspector.connecting", addressWithPort);
}
const socket = this.#socket;
this.onConnectOrImmediately(() => {
this.emit("Inspector.connected");
for (let i = 0; i < this.#pendingRequests.length; i++) {
const request = this.#pendingRequests[i];
if (this.#send(request)) {
this.emit("Inspector.request", request);
} else {
this.#pendingRequests = this.#pendingRequests.slice(i);
break;
}
}
});
socket.on("data", data => this.#framer.onData(data));
socket.on("error", error => {
this.#close(unknownToError(error));
});
socket.on("close", hadError => {
if (hadError) {
this.#close(new Error("Socket closed due to a transmission error"));
} else {
this.#close();
}
});
const ready = new Promise<boolean>(resolve => {
if (socket.connecting) {
socket.on("connect", () => resolve(true));
} else {
resolve(true);
}
socket.on("close", () => resolve(false));
socket.on("error", () => resolve(false));
}).finally(() => {
this.#ready = undefined;
});
this.#ready = ready;
return ready;
}
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M] | undefined,
): Promise<JSC.ResponseMap[M]> {
const id = this.#requestId++;
const request = {
id,
method,
params: params ?? {},
};
return new Promise((resolve, reject) => {
let timerId: number | undefined;
const done = (result: any) => {
this.#pendingResponses.delete(id);
if (timerId) {
clearTimeout(timerId);
}
if (result instanceof Error) {
reject(result);
} else {
resolve(result);
}
};
this.#pendingResponses.set(id, {
request: request,
done: done,
});
if (this.#send(request)) {
timerId = +setTimeout(() => done(new Error(`Timed out: ${method}`)), 10_000);
this.emit("Inspector.request", request);
} else {
this.emit("Inspector.pendingRequest", request);
}
});
}
#send(request: JSC.Request): boolean {
this.#framer.send(JSON.stringify(request));
if (!this.#pendingRequests.includes(request)) {
this.#pendingRequests.push(request);
}
return false;
}
#accept(message: string): void {
let data: JSC.Event | JSC.Response;
try {
data = JSON.parse(message);
} catch (cause) {
this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
return;
}
if (!("id" in data)) {
this.emit("Inspector.event", data);
const { method, params } = data;
this.emit(method, params);
return;
}
this.emit("Inspector.response", data);
const { id } = data;
const handle = this.#pendingResponses.get(id);
if (!handle) {
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
return;
}
if ("error" in data) {
const { error } = data;
const { message } = error;
handle.done(new Error(message));
} else {
const { result } = data;
handle.done(result);
}
}
get closed(): boolean {
return !this.#socket.writable;
}
close(): void {
this.#socket?.end();
}
#close(error?: Error): void {
for (const handle of this.#pendingResponses.values()) {
handle.done(error ?? new Error("Socket closed while waiting for: " + handle.request.method));
}
this.#pendingResponses.clear();
if (error) {
this.emit("Inspector.error", error);
}
this.emit("Inspector.disconnected", error);
}
}
function unknownToError(input: unknown): Error {
if (input instanceof Error) {
return input;
}
if (typeof input === "object" && input !== null && "message" in input) {
const { message } = input;
return new Error(`${message}`);
}
return new Error(`${input}`);
}
@@ -0,0 +1,250 @@
import { EventEmitter } from "node:events";
import { WebSocket } from "ws";
import type { JSC } from "../protocol";
import type { Inspector, InspectorEventMap } from "./index";
/**
* An inspector that communicates with a debugger over a WebSocket.
*/
export class WebSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
#url?: string;
#webSocket?: WebSocket;
#ready: Promise<boolean> | undefined;
#requestId: number;
#pendingRequests: JSC.Request[];
#pendingResponses: Map<number, (result: unknown) => void>;
constructor(url?: string | URL) {
super();
this.#url = url ? String(url) : undefined;
this.#requestId = 1;
this.#pendingRequests = [];
this.#pendingResponses = new Map();
}
get url(): string {
return this.#url!;
}
async start(url?: string | URL): Promise<boolean> {
if (url) {
this.#url = String(url);
}
if (!this.#url) {
this.emit("Inspector.error", new Error("Inspector needs a URL, but none was provided"));
return false;
}
return this.#connect(this.#url);
}
async #connect(url: string): Promise<boolean> {
if (this.#ready) {
return this.#ready;
}
this.close(1001, "Restarting...");
this.emit("Inspector.connecting", url);
let webSocket: WebSocket;
try {
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
webSocket = new WebSocket(url, {
headers: {
"Ref-Event-Loop": "0",
},
finishRequest: (request: import("http").ClientRequest) => {
request.setHeader("Ref-Event-Loop", "0");
request.end();
},
});
} catch (cause) {
this.#close(unknownToError(cause));
return false;
}
webSocket.addEventListener("open", () => {
this.emit("Inspector.connected");
for (let i = 0; i < this.#pendingRequests.length; i++) {
const request = this.#pendingRequests[i];
if (this.#send(request)) {
this.emit("Inspector.request", request);
} else {
this.#pendingRequests = this.#pendingRequests.slice(i);
break;
}
}
});
webSocket.addEventListener("message", ({ data }) => {
if (typeof data === "string") {
this.#accept(data);
} else {
this.emit("Inspector.error", new Error(`WebSocket received unexpected binary message: ${data.toString()}`));
}
});
webSocket.addEventListener("error", event => {
this.#close(unknownToError(event));
});
webSocket.addEventListener("unexpected-response", () => {
this.#close(new Error("WebSocket upgrade failed"));
});
webSocket.addEventListener("close", ({ code, reason }) => {
if (code === 1001 || code === 1006) {
this.#close();
return;
}
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
});
this.#webSocket = webSocket;
const ready = new Promise<boolean>(resolve => {
webSocket.addEventListener("open", () => resolve(true));
webSocket.addEventListener("close", () => resolve(false));
webSocket.addEventListener("error", () => resolve(false));
}).finally(() => {
this.#ready = undefined;
});
this.#ready = ready;
return ready;
}
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M] | undefined,
): Promise<JSC.ResponseMap[M]> {
const id = this.#requestId++;
const request = {
id,
method,
params: params ?? {},
};
return new Promise((resolve, reject) => {
let timerId: number | undefined;
const done = (result: any) => {
this.#pendingResponses.delete(id);
if (timerId) {
clearTimeout(timerId);
}
if (result instanceof Error) {
reject(result);
} else {
resolve(result);
}
};
this.#pendingResponses.set(id, done);
if (this.#send(request)) {
timerId = +setTimeout(() => done(new Error(`Timed out: ${method}`)), 10_000);
this.emit("Inspector.request", request);
} else {
this.emit("Inspector.pendingRequest", request);
}
});
}
#send(request: JSC.Request): boolean {
if (this.#webSocket) {
const { readyState } = this.#webSocket!;
if (readyState === WebSocket.OPEN) {
this.#webSocket.send(JSON.stringify(request));
return true;
}
}
if (!this.#pendingRequests.includes(request)) {
this.#pendingRequests.push(request);
}
return false;
}
#accept(message: string): void {
let data: JSC.Event | JSC.Response;
try {
data = JSON.parse(message);
} catch (cause) {
this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
return;
}
if (!("id" in data)) {
this.emit("Inspector.event", data);
const { method, params } = data;
this.emit(method, params);
return;
}
this.emit("Inspector.response", data);
const { id } = data;
const resolve = this.#pendingResponses.get(id);
if (!resolve) {
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
return;
}
if ("error" in data) {
const { error } = data;
const { message } = error;
resolve(new Error(message));
} else {
const { result } = data;
resolve(result);
}
}
get closed(): boolean {
if (!this.#webSocket) {
return true;
}
const { readyState } = this.#webSocket;
switch (readyState) {
case WebSocket.CLOSED:
case WebSocket.CLOSING:
return true;
}
return false;
}
close(code?: number, reason?: string): void {
this.#webSocket?.close(code ?? 1001, reason);
}
#close(error?: Error): void {
for (const resolve of this.#pendingResponses.values()) {
resolve(error ?? new Error("WebSocket closed"));
}
this.#pendingResponses.clear();
if (error) {
this.emit("Inspector.error", error);
}
this.emit("Inspector.disconnected", error);
}
}
function unknownToError(input: unknown): Error {
if (input instanceof Error) {
return input;
}
if (typeof input === "object" && input !== null && "message" in input) {
const { message } = input;
return new Error(`${message}`);
}
return new Error(`${input}`);
}
@@ -0,0 +1 @@
export type { JSC } from "./jsc";
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
// @ts-nocheck
// The content of this file is included in each generated protocol file.
export type Event<T extends keyof EventMap = keyof EventMap> = {
readonly method: T;
readonly params: EventMap[T];
};
export type Request<T extends keyof RequestMap = keyof RequestMap> = {
readonly id: number;
readonly method: T;
readonly params: RequestMap[T];
};
export type Response<T extends keyof ResponseMap = keyof ResponseMap> = {
readonly id: number;
} & (
| {
readonly method?: T;
readonly result: ResponseMap[T];
}
| {
readonly error: {
readonly code?: string;
readonly message: string;
};
}
);
@@ -0,0 +1,59 @@
// Represents the schema of the protocol.json file.
export type Protocol = {
readonly name: string;
readonly version: {
readonly major: number;
readonly minor: number;
};
readonly domains: readonly Domain[];
};
export type Domain = {
readonly domain: string;
readonly debuggableTypes?: readonly string[];
readonly dependencies?: readonly string[];
readonly types?: readonly Property[];
readonly commands?: readonly Command[];
readonly events?: readonly Event[];
};
export type Command = {
readonly name: string;
readonly description?: string;
readonly parameters?: readonly Property[];
readonly returns?: readonly Property[];
};
export type Event = {
readonly name: string;
readonly description?: string;
readonly parameters?: readonly Property[];
};
export type Property = {
readonly id?: string;
readonly name?: string;
readonly description?: string;
readonly optional?: boolean;
} & (
| {
readonly type: "array";
readonly items?: Property;
}
| {
readonly type: "object";
readonly properties?: readonly Property[];
}
| {
readonly type: "string";
readonly enum?: readonly string[];
}
| {
readonly type: "boolean" | "number" | "integer";
}
| {
readonly type: undefined;
readonly $ref: string;
}
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
import type { JSC } from "../protocol";
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject, topLevel?: boolean): string {
const { type, subtype, value, description, className, preview } = remoteObject;
switch (type) {
case "undefined":
return "undefined";
case "boolean":
case "number":
return description ?? JSON.stringify(value);
case "string":
if (topLevel) {
return String(value ?? description);
}
return JSON.stringify(value ?? description);
case "symbol":
case "bigint":
return description!;
case "function":
return description!.replace("function", "ƒ") || "ƒ";
}
switch (subtype) {
case "null":
return "null";
case "regexp":
case "date":
case "error":
return description!;
}
if (preview) {
return objectPreviewToString(preview);
}
if (className) {
return className;
}
return description || "Object";
}
export function objectPreviewToString(objectPreview: JSC.Runtime.ObjectPreview): string {
const { type, subtype, entries, properties, overflow, description, size } = objectPreview;
if (type !== "object") {
return remoteObjectToString(objectPreview);
}
let items: string[];
if (entries) {
items = entries.map(entryPreviewToString).sort();
} else if (properties) {
if (isIndexed(subtype)) {
items = properties.map(indexedPropertyPreviewToString);
if (subtype !== "array") {
items.sort();
}
} else {
items = properties.map(namedPropertyPreviewToString).sort();
}
} else {
items = ["…"];
}
if (overflow) {
items.push("…");
}
let label: string;
if (description === "Object") {
label = "";
} else if (size === undefined) {
label = description!;
} else {
label = `${description}(${size})`;
}
if (!items.length) {
return label || "{}";
}
if (label) {
label += " ";
}
if (isIndexed(subtype)) {
return `${label}[${items.join(", ")}]`;
}
return `${label}{${items.join(", ")}}`;
}
function propertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
const { type, value, ...preview } = propertyPreview;
if (type === "accessor") {
return "ƒ";
}
return remoteObjectToString({ ...preview, type, description: value });
}
function entryPreviewToString(entryPreview: JSC.Runtime.EntryPreview): string {
const { key, value } = entryPreview;
if (key) {
return `${objectPreviewToString(key)} => ${objectPreviewToString(value)}`;
}
return objectPreviewToString(value);
}
function namedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
const { name, valuePreview } = propertyPreview;
if (valuePreview) {
return `${name}: ${objectPreviewToString(valuePreview)}`;
}
return `${name}: ${propertyPreviewToString(propertyPreview)}`;
}
function indexedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
const { valuePreview } = propertyPreview;
if (valuePreview) {
return objectPreviewToString(valuePreview);
}
return propertyPreviewToString(propertyPreview);
}
function isIndexed(type?: JSC.Runtime.RemoteObject["subtype"]): boolean {
return type === "array" || type === "set" || type === "weakset";
}
@@ -0,0 +1,190 @@
import type { Server } from "bun";
import { serve } from "bun";
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
import { WebSocketInspector } from "../../src/inspector/websocket";
let server: Server;
let url: URL;
describe("WebSocketInspector", () => {
test("fails without a URL", () => {
const ws = new WebSocketInspector();
const fn = mock(error => {
expect(error).toBeInstanceOf(Error);
});
ws.on("Inspector.error", fn);
expect(ws.start()).resolves.toBeFalse();
expect(fn).toHaveBeenCalled();
});
test("fails with invalid URL", () => {
const ws = new WebSocketInspector("notaurl");
const fn = mock(error => {
expect(error).toBeInstanceOf(Error);
});
ws.on("Inspector.error", fn);
expect(ws.start()).resolves.toBeFalse();
expect(fn).toHaveBeenCalled();
});
test("fails with valid URL but no server", () => {
const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/");
const fn = mock(error => {
expect(error).toBeInstanceOf(Error);
});
ws.on("Inspector.error", fn);
expect(ws.start()).resolves.toBeFalse();
expect(fn).toHaveBeenCalled();
});
test("fails with invalid upgrade response", () => {
const ws = new WebSocketInspector(new URL("/", url));
const fn = mock(error => {
expect(error).toBeInstanceOf(Error);
});
ws.on("Inspector.error", fn);
expect(ws.start()).resolves.toBeFalse();
expect(fn).toHaveBeenCalled();
});
test("can connect to a server", () => {
const ws = new WebSocketInspector(url);
const fn = mock(() => {
expect(ws.closed).toBe(false);
});
ws.on("Inspector.connected", fn);
expect(ws.start()).resolves.toBeTrue();
expect(fn).toHaveBeenCalled();
ws.close();
});
test("can disconnect from a server", () => {
const ws = new WebSocketInspector(url);
const fn = mock(() => {
expect(ws.closed).toBeTrue();
});
ws.on("Inspector.disconnected", fn);
expect(ws.start()).resolves.toBeTrue();
ws.close();
expect(fn).toHaveBeenCalled();
});
test("can connect to a server multiple times", () => {
const ws = new WebSocketInspector(url);
const fn0 = mock(() => {
expect(ws.closed).toBeFalse();
});
ws.on("Inspector.connected", fn0);
const fn1 = mock(() => {
expect(ws.closed).toBeTrue();
});
ws.on("Inspector.disconnected", fn1);
for (let i = 0; i < 3; i++) {
expect(ws.start()).resolves.toBeTrue();
ws.close();
}
expect(fn0).toHaveBeenCalledTimes(3);
expect(fn1).toHaveBeenCalledTimes(3);
});
test("can send a request", () => {
const ws = new WebSocketInspector(url);
const fn0 = mock(request => {
expect(request).toStrictEqual({
id: 1,
method: "Debugger.setPauseOnAssertions",
params: {
enabled: true,
},
});
});
ws.on("Inspector.request", fn0);
const fn1 = mock(response => {
expect(response).toStrictEqual({
id: 1,
result: {
ok: true,
},
});
});
ws.on("Inspector.response", fn1);
expect(ws.start()).resolves.toBeTrue();
expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true });
expect(fn0).toHaveBeenCalled();
expect(fn1).toHaveBeenCalled();
ws.close();
});
test("can send a request before connecting", () => {
const ws = new WebSocketInspector(url);
const fn0 = mock(request => {
expect(request).toStrictEqual({
id: 1,
method: "Runtime.enable",
params: {},
});
});
ws.on("Inspector.pendingRequest", fn0);
ws.on("Inspector.request", fn0);
const fn1 = mock(response => {
expect(response).toStrictEqual({
id: 1,
result: {
ok: true,
},
});
});
ws.on("Inspector.response", fn1);
const request = ws.send("Runtime.enable");
expect(ws.start()).resolves.toBe(true);
expect(request).resolves.toMatchObject({ ok: true });
expect(fn0).toHaveBeenCalledTimes(2);
expect(fn1).toHaveBeenCalled();
ws.close();
});
test("can receive an event", () => {
const ws = new WebSocketInspector(url);
const fn = mock(event => {
expect(event).toStrictEqual({
method: "Debugger.scriptParsed",
params: {
scriptId: "1",
},
});
});
ws.on("Inspector.event", fn);
expect(ws.start()).resolves.toBeTrue();
expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true });
expect(fn).toHaveBeenCalled();
ws.close();
});
});
beforeAll(() => {
server = serve({
port: 0,
fetch(request, server) {
if (request.url.endsWith("/ws") && server.upgrade(request)) {
return;
}
return new Response();
},
websocket: {
message(ws, message) {
const { id, method } = JSON.parse(String(message));
ws.send(JSON.stringify({ id, result: { ok: true } }));
if (method === "Debugger.enable") {
ws.send(JSON.stringify({ method: "Debugger.scriptParsed", params: { scriptId: "1" } }));
}
},
},
});
const { hostname, port } = server;
url = new URL(`ws://${hostname}:${port}/ws`);
});
afterAll(() => {
server?.stop(true);
});
@@ -0,0 +1 @@
console.log(process.execArgv);
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Bundler",
"moduleDetection": "force",
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true,
"outDir": "dist"
},
"include": [".", "../bun-types/index.d.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
.serverless/
node_modules/
bun-lambda-layer/
*.zip
+115
View File
@@ -0,0 +1,115 @@
# bun-lambda
A custom runtime layer that runs Bun on AWS Lambda.
## Setup
First, you will need to deploy the layer to your AWS account. Clone this repository and run the `publish-layer` script to get started. Note: the `publish-layer` script also builds the layer.
```sh
git clone --filter=blob:none --sparse https://github.com/oven-sh/bun.git
git -C bun sparse-checkout set packages/bun-lambda
cd bun/packages/bun-lambda
bun install
bun run publish-layer
```
## Usage
Once you publish the layer to your AWS account, you can create a Lambda function that uses the layer.
### Step 1: Create a Bun Lambda handler function
In addition to providing the Bun runtime itself, the Bun Lambda Layer also provides an event transformation so you can write your Bun function in a classic Bun server format. This allows you to also run your Lambda function as a local Bun server with `bun run <handler-name>.ts`. Here are some examples of how to write a Bun Lambda function:
#### HTTP Event Example
When an event is triggered from [API Gateway](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html), the layer transforms the event payload into a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). This means you can test your Lambda function locally using `bun run`, without any code changes.
```ts
export default {
async fetch(request: Request): Promise<Response> {
console.log(request.headers.get("x-amzn-function-arn"));
// ...
return new Response("Hello from Lambda!", {
status: 200,
headers: {
"Content-Type": "text/plain",
},
});
},
};
```
#### Non-HTTP Event Example
For non-HTTP events — S3, SQS, EventBridge, etc. — the event payload is the body of the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request).
```ts
export default {
async fetch(request: Request): Promise<Response> {
const event = await request.json();
// ...
return new Response();
},
};
```
### Step 2: Build the Bun handler
The final step is to upload your Bun handler. You can technically write the handler directly in the console if you wish, but if you want a full development environment, use the Bun toolkit. There are several ways you can choose to build and manage your artifacts, but follow these steps for a simple approach:
1. Run `bun build <handler-entry>.[ts|js] --outfile ./dist/handler.js`
2. Zip the `/dist` folder
### Step 3: Create the Lambda function on AWS
Once you've written your Lambda function, you need to configure a new Lambda function to use Bun. The following steps apply to configuring in the console, CloudFormation, CDK, Terraform, or any other configuration management option for AWS:
1. Create the Lambda function
2. Set the Runtime to custom with Amazon Linux 2
3. Set the handler to <handler-file-name>.fetch (e.g. if your bundled Bun handler is at `handler.js`, set the handler as `handler.fetch`)
4. Set the architecture to whichever architecture you configured when you built/deployed the Lambda Layer
5. Attach the Lambda Layer to your new function
6. Upload the zip file from step 2. You can do this in the console directly, upload to S3 and set that as the location for the handler file in Lambda, or use something like CDK to manage this for you.
## API
### `bun run build-layer`
Builds a Lambda layer for Bun and saves it to a `.zip` file.
| Flag | Description | Default |
| ----------- | -------------------------------------------------------------------- | ---------------------- |
| `--arch` | The architecture, either: "x64" or "aarch64" | aarch64 |
| `--release` | The release of Bun, either: "latest", "canary", or a release "x.y.z" | latest |
| `--output` | The path to write the layer as a `.zip`. | ./bun-lambda-layer.zip |
Example:
```sh
bun run build-layer -- \
--arch x64 \
--release canary \
--output /path/to/layer.zip
```
### `bun run publish-layer`
Builds a Lambda layer for Bun then publishes it to your AWS account.
| Flag | Description | Default |
| ---------- | ----------------------------------------- | ------- |
| `--layer` | The layer name. | bun |
| `--region` | The region name, or "\*" for all regions. | |
| `--public` | If the layer should be public. | false |
Example:
```sh
bun run publish-layer -- \
--arch aarch64 \
--release latest \
--output /path/to/layer.zip \
--region us-east-1
```
+3
View File
@@ -0,0 +1,3 @@
#! /bin/sh
export BUN_INSTALL_CACHE_DIR=/tmp/bun/cache
exec /opt/bun --cwd $LAMBDA_TASK_ROOT /opt/runtime.ts
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
import type { Server, ServerWebSocket } from "bun";
export default {
async fetch(request: Request, server: Server): Promise<Response | undefined> {
console.log("Request", {
url: request.url,
method: request.method,
headers: request.headers.toJSON(),
body: request.body ? await request.text() : null,
});
if (server.upgrade(request)) {
console.log("WebSocket upgraded");
return;
}
return new Response("Hello from Bun on Lambda!", {
status: 200,
headers: {
"Content-Type": "text/plain;charset=utf-8",
},
});
},
websocket: {
async open(ws: ServerWebSocket): Promise<void> {
console.log("WebSocket opened");
},
async message(ws: ServerWebSocket, message: string): Promise<void> {
console.log("WebSocket message", message);
},
async close(ws: ServerWebSocket, code: number, reason?: string): Promise<void> {
console.log("WebSocket closed", { code, reason });
},
},
};
+22
View File
@@ -0,0 +1,22 @@
{
"name": "bun-lambda",
"private": true,
"devDependencies": {
"@oclif/plugin-plugins": "^3.5.0",
"bun-types": "^0.7.0",
"jszip": "^3.10.1",
"oclif": "^3.6.5",
"prettier": "^2.8.2"
},
"dependencies": {
"aws4fetch": "^1.0.17"
},
"scripts": {
"build-layer": "bun scripts/build-layer.ts",
"publish-layer": "bun scripts/publish-layer.ts",
"format": "prettier --write ."
},
"overrides": {
"braces": "^3.0.3"
}
}
+826
View File
@@ -0,0 +1,826 @@
import { AwsClient } from "aws4fetch";
import type { Server, ServerWebSocket } from "bun";
type Lambda = {
fetch: (request: Request, server: Server) => Promise<Response | undefined>;
error?: (error: unknown) => Promise<Response>;
websocket?: {
open?: (ws: ServerWebSocket) => Promise<void>;
message?: (ws: ServerWebSocket, message: string) => Promise<void>;
close?: (ws: ServerWebSocket, code: number, reason: string) => Promise<void>;
};
};
let requestId: string | undefined;
let traceId: string | undefined;
let functionArn: string | undefined;
let aws: AwsClient | undefined;
let logger = console.log;
function log(level: string, ...args: any[]): void {
if (!args.length) {
return;
}
const messages = args.map(arg => Bun.inspect(arg).replace(/\n/g, "\r"));
if (requestId === undefined) {
logger(level, ...messages);
} else {
logger(level, `RequestId: ${requestId}`, ...messages);
}
}
console.log = (...args: any[]) => log("INFO", ...args);
console.info = (...args: any[]) => log("INFO", ...args);
console.warn = (...args: any[]) => log("WARN", ...args);
console.error = (...args: any[]) => log("ERROR", ...args);
console.debug = (...args: any[]) => log("DEBUG", ...args);
console.trace = (...args: any[]) => log("TRACE", ...args);
let warnings: Set<string> | undefined;
function warnOnce(message: string, ...args: any[]): void {
if (warnings === undefined) {
warnings = new Set();
}
if (warnings.has(message)) {
return;
}
warnings.add(message);
console.warn(message, ...args);
}
function reset(): void {
requestId = undefined;
traceId = undefined;
warnings = undefined;
}
function exit(...cause: any[]): never {
console.error(...cause);
process.exit(1);
}
function env(name: string, fallback?: string): string {
const value = process.env[name] ?? fallback ?? null;
if (value === null) {
exit(`Runtime failed to find the '${name}' environment variable`);
}
return value;
}
const runtimeUrl = new URL(`http://${env("AWS_LAMBDA_RUNTIME_API")}/2018-06-01/`);
async function fetch(url: string, options?: RequestInit): Promise<Response> {
const { href } = new URL(url, runtimeUrl);
const response = await globalThis.fetch(href, {
...options,
timeout: false,
});
if (!response.ok) {
exit(`Runtime failed to send request to Lambda [status: ${response.status}]`);
}
return response;
}
async function fetchAws(url: string, options?: RequestInit): Promise<Response> {
if (aws === undefined) {
aws = new AwsClient({
accessKeyId: env("AWS_ACCESS_KEY_ID"),
secretAccessKey: env("AWS_SECRET_ACCESS_KEY"),
sessionToken: env("AWS_SESSION_TOKEN"),
region: env("AWS_REGION"),
});
}
return aws.fetch(url, options);
}
type LambdaError = {
readonly errorType: string;
readonly errorMessage: string;
readonly stackTrace?: string[];
};
function formatError(error: unknown): LambdaError {
if (error instanceof Error) {
return {
errorType: error.name,
errorMessage: error.message,
stackTrace: error.stack?.split("\n").filter(line => !line.includes(" /opt/runtime.ts")),
};
}
return {
errorType: "Error",
errorMessage: Bun.inspect(error),
};
}
async function sendError(type: string, cause: unknown): Promise<void> {
console.error(cause);
await fetch(requestId === undefined ? "runtime/init/error" : `runtime/invocation/${requestId}/error`, {
method: "POST",
headers: {
"Content-Type": "application/vnd.aws.lambda.error+json",
"Lambda-Runtime-Function-Error-Type": `Bun.${type}`,
},
body: JSON.stringify(formatError(cause)),
});
}
async function throwError(type: string, cause: unknown): Promise<never> {
await sendError(type, cause);
exit();
}
async function init(): Promise<Lambda> {
const handlerName = env("_HANDLER");
const index = handlerName.lastIndexOf(".");
const fileName = handlerName.substring(0, index);
const filePath = `${env("LAMBDA_TASK_ROOT")}/${fileName}`;
let file;
try {
file = await import(filePath);
} catch (cause) {
if (cause instanceof Error && cause.message.startsWith("Cannot find module")) {
return throwError("FileDoesNotExist", `Did not find a file named '${fileName}'`);
}
return throwError("InitError", cause);
}
const moduleName = handlerName.substring(index + 1) || "fetch";
let module = file["default"] ?? file[moduleName] ?? {};
if (typeof module === "function") {
module = {
fetch: module,
};
} else if (typeof module === "object" && moduleName !== "fetch") {
module = {
...module,
fetch: module[moduleName],
};
}
const { fetch, websocket } = module;
if (typeof fetch !== "function") {
return throwError(
fetch === undefined ? "MethodDoesNotExist" : "MethodIsNotAFunction",
`${fileName} does not have a default export with a function named '${moduleName}'`,
);
}
if (websocket === undefined) {
return module;
}
for (const name of ["open", "message", "close"]) {
const method = websocket[name];
if (method === undefined) {
continue;
}
if (typeof method !== "function") {
return throwError(
"MethodIsNotAFunction",
`${fileName} does not have a function named '${name}' on the default 'websocket' property`,
);
}
}
return module;
}
type LambdaRequest<E = any> = {
readonly requestId: string;
readonly traceId: string;
readonly functionArn: string;
readonly deadlineMs: number | null;
readonly event: E;
};
async function receiveRequest(): Promise<LambdaRequest> {
const response = await fetch("runtime/invocation/next");
requestId = response.headers.get("Lambda-Runtime-Aws-Request-Id") ?? undefined;
if (requestId === undefined) {
exit("Runtime received a request without a request ID");
}
traceId = response.headers.get("Lambda-Runtime-Trace-Id") ?? undefined;
if (traceId === undefined) {
exit("Runtime received a request without a trace ID");
}
process.env["_X_AMZN_TRACE_ID"] = traceId;
functionArn = response.headers.get("Lambda-Runtime-Invoked-Function-Arn") ?? undefined;
if (functionArn === undefined) {
exit("Runtime received a request without a function ARN");
}
const deadlineMs = parseInt(response.headers.get("Lambda-Runtime-Deadline-Ms") ?? "0") || null;
let event;
try {
event = await response.json();
} catch (cause) {
exit("Runtime received a request with invalid JSON", cause);
}
return {
requestId,
traceId,
functionArn,
deadlineMs,
event,
};
}
type LambdaResponse = {
readonly statusCode: number;
readonly headers?: Record<string, string>;
readonly isBase64Encoded?: boolean;
readonly body?: string;
readonly multiValueHeaders?: Record<string, string[]>;
readonly cookies?: string[];
};
async function formatResponse(response: Response): Promise<LambdaResponse> {
const statusCode = response.status;
const headers = response.headers.toJSON();
if (statusCode === 101) {
const protocol = headers["sec-websocket-protocol"];
if (protocol === undefined) {
return {
statusCode: 200,
};
}
return {
statusCode: 200,
headers: {
"Sec-WebSocket-Protocol": protocol,
},
};
}
const mime = headers["content-type"];
const isBase64Encoded = !mime || (!mime.startsWith("text/") && !mime.startsWith("application/json"));
const body = isBase64Encoded ? Buffer.from(await response.arrayBuffer()).toString("base64") : await response.text();
delete headers["set-cookie"];
const cookies = response.headers.getAll("Set-Cookie");
if (cookies.length === 0) {
return {
statusCode,
headers,
isBase64Encoded,
body,
};
}
return {
statusCode,
headers,
cookies,
multiValueHeaders: {
"Set-Cookie": cookies,
},
isBase64Encoded,
body,
};
}
async function sendResponse(response: unknown): Promise<void> {
if (requestId === undefined) {
exit("Runtime attempted to send a response without a request ID");
}
await fetch(`runtime/invocation/${requestId}/response`, {
method: "POST",
body: response === null ? null : typeof response === "string" ? response : JSON.stringify(response),
});
}
function formatBody(body?: string, isBase64Encoded?: boolean): string | null {
if (body === undefined) {
return null;
}
if (!isBase64Encoded) {
return body;
}
return Buffer.from(body, "base64").toString("utf8");
}
type HttpEventV1 = {
readonly requestContext: {
readonly requestId: string;
readonly domainName: string;
readonly httpMethod: string;
readonly path: string;
};
readonly headers: Record<string, string>;
readonly multiValueHeaders?: Record<string, string[]>;
readonly queryStringParameters?: Record<string, string>;
readonly multiValueQueryStringParameters?: Record<string, string[]>;
readonly isBase64Encoded: boolean;
readonly body?: string;
};
function isHttpEventV1(event: any): event is HttpEventV1 {
return !event.Records && event.version !== "2.0" && event.version !== "0" && typeof event.requestContext === "object";
}
function formatHttpEventV1(event: HttpEventV1): Request {
const request = event.requestContext;
const headers = new Headers();
for (const [name, values] of Object.entries(event.multiValueHeaders ?? {})) {
for (const value of values) {
headers.append(name, value);
}
}
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(`${proto}://${hostname}/`);
url.pathname = request.path;
for (const [name, values] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
for (const value of values ?? []) {
url.searchParams.append(name, value);
}
}
return new Request(url.toString(), {
method: request.httpMethod,
headers,
body: formatBody(event.body, event.isBase64Encoded),
});
}
type HttpEventV2 = {
readonly version: "2.0";
readonly requestContext: {
readonly requestId: string;
readonly domainName: string;
readonly http: {
readonly method: string;
readonly path: string;
};
};
readonly headers: Record<string, string>;
readonly queryStringParameters?: Record<string, string>;
readonly cookies?: string[];
readonly isBase64Encoded: boolean;
readonly body?: string;
};
function isHttpEventV2(event: any): event is HttpEventV2 {
return !event.Records && event.version === "2.0" && typeof event.requestContext === "object";
}
function formatHttpEventV2(event: HttpEventV2): Request {
const request = event.requestContext;
const headers = new Headers();
for (const [name, values] of Object.entries(event.headers)) {
for (const value of values.split(",")) {
headers.append(name, value);
}
}
for (const cookie of event.cookies ?? []) {
headers.append("Set-Cookie", cookie);
}
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(`${proto}://${hostname}/`);
url.pathname = request.http.path;
for (const [name, values] of Object.entries(event.queryStringParameters ?? {})) {
url.searchParams.append(name, values);
}
return new Request(url.toString(), {
method: request.http.method,
headers,
body: formatBody(event.body, event.isBase64Encoded),
});
}
function isHttpEvent(event: any): boolean {
return isHttpEventV1(event) || isHttpEventV2(event);
}
type WebSocketEvent = {
readonly headers: Record<string, string>;
readonly multiValueHeaders: Record<string, string[]>;
readonly isBase64Encoded: boolean;
readonly body?: string;
readonly requestContext: {
readonly apiId: string;
readonly requestId: string;
readonly connectionId: string;
readonly domainName: string;
readonly stage: string;
readonly identity: {
readonly sourceIp: string;
};
} & (
| {
readonly eventType: "CONNECT";
}
| {
readonly eventType: "MESSAGE";
}
| {
readonly eventType: "DISCONNECT";
readonly disconnectStatusCode: number;
readonly disconnectReason: string;
}
);
};
function isWebSocketEvent(event: any): event is WebSocketEvent {
return typeof event.requestContext === "object" && typeof event.requestContext.connectionId === "string";
}
function isWebSocketUpgrade(event: any): event is WebSocketEvent {
return isWebSocketEvent(event) && event.requestContext.eventType === "CONNECT";
}
function formatWebSocketUpgrade(event: WebSocketEvent): Request {
const request = event.requestContext;
const headers = new Headers();
headers.set("Upgrade", "websocket");
headers.set("x-amzn-connection-id", request.connectionId);
for (const [name, values] of Object.entries(event.multiValueHeaders as any)) {
for (const value of (values as any) ?? []) {
headers.append(name, value);
}
}
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(`${proto}://${hostname}/${request.stage}`);
return new Request(url.toString(), {
headers,
body: formatBody(event.body, event.isBase64Encoded),
});
}
function formatUnknownEvent(event: unknown): Request {
return new Request("https://lambda/", {
method: "POST",
body: JSON.stringify(event),
headers: {
"Content-Type": "application/json;charset=utf-8",
},
});
}
function formatRequest(input: LambdaRequest): Request | undefined {
const { event, requestId, traceId, functionArn, deadlineMs } = input;
let request: Request;
if (isHttpEventV2(event)) {
request = formatHttpEventV2(event);
} else if (isHttpEventV1(event)) {
request = formatHttpEventV1(event);
} else if (isWebSocketEvent(event)) {
if (!isWebSocketUpgrade(event)) {
return undefined;
}
request = formatWebSocketUpgrade(event);
} else {
request = formatUnknownEvent(input);
}
request.headers.set("x-amzn-requestid", requestId);
request.headers.set("x-amzn-trace-id", traceId);
request.headers.set("x-amzn-function-arn", functionArn);
if (deadlineMs !== null) {
request.headers.set("x-amzn-deadline-ms", `${deadlineMs}`);
}
// @ts-ignore: Attach the original event to the Request
request.aws = event;
return request;
}
class LambdaServer implements Server {
#lambda: Lambda;
#webSockets: Map<string, LambdaWebSocket>;
#upgrade: Response | null;
pendingRequests: number;
pendingWebSockets: number;
port: number;
hostname: string;
development: boolean;
constructor(lambda: Lambda) {
this.#lambda = lambda;
this.#webSockets = new Map();
this.#upgrade = null;
this.pendingRequests = 0;
this.pendingWebSockets = 0;
this.port = 80;
this.hostname = "lambda";
this.development = false;
}
async accept(request: LambdaRequest): Promise<unknown> {
const deadlineMs = request.deadlineMs === null ? Date.now() + 60_000 : request.deadlineMs;
const durationMs = Math.max(1, deadlineMs - Date.now());
let response: unknown;
try {
response = await Promise.race([
new Promise<undefined>(resolve => setTimeout(resolve, durationMs)),
this.#acceptRequest(request),
]);
} catch (cause) {
await sendError("RequestError", cause);
return;
}
if (response === undefined) {
await sendError("TimeoutError", "Function timed out");
return;
}
return response;
}
async #acceptRequest(event: LambdaRequest): Promise<unknown> {
const request = formatRequest(event);
let response: Response | undefined;
if (request === undefined) {
await this.#acceptWebSocket(event.event);
} else {
response = await this.fetch(request);
if (response.status === 101) {
await this.#acceptWebSocket(event.event);
}
}
if (response === undefined) {
return {
statusCode: 200,
};
}
if (!isHttpEvent(event.event)) {
return response.text();
}
return formatResponse(response);
}
async #acceptWebSocket(event: WebSocketEvent): Promise<void> {
const request = event.requestContext;
const { connectionId, eventType } = request;
const webSocket = this.#webSockets.get(connectionId);
if (webSocket === undefined || this.#lambda.websocket === undefined) {
return;
}
const { open, message, close } = this.#lambda.websocket;
switch (eventType) {
case "CONNECT": {
if (open) {
await open(webSocket);
}
break;
}
case "MESSAGE": {
if (message) {
const body = formatBody(event.body, event.isBase64Encoded);
if (body !== null) {
await message(webSocket, body);
}
}
break;
}
case "DISCONNECT": {
try {
if (close) {
const { disconnectStatusCode: code, disconnectReason: reason } = request;
await close(webSocket, code, reason);
}
} finally {
this.#webSockets.delete(connectionId);
this.pendingWebSockets--;
}
break;
}
}
}
stop(): void {
exit("Runtime exited because Server.stop() was called");
}
reload(options: any): void {
this.#lambda = {
fetch: options.fetch ?? this.#lambda.fetch,
error: options.error ?? this.#lambda.error,
websocket: options.websocket ?? this.#lambda.websocket,
};
this.port =
typeof options.port === "number"
? options.port
: typeof options.port === "string"
? parseInt(options.port)
: this.port;
this.hostname = options.hostname ?? this.hostname;
this.development = options.development ?? this.development;
}
async fetch(request: Request): Promise<Response> {
this.pendingRequests++;
try {
let response = await this.#lambda.fetch(request, this);
if (response instanceof Response) {
return response;
}
if (response === undefined && this.#upgrade !== null) {
return this.#upgrade;
}
throw new Error("fetch() did not return a Response");
} catch (cause) {
console.error(cause);
if (this.#lambda.error !== undefined) {
try {
return await this.#lambda.error(cause);
} catch (cause) {
console.error(cause);
}
}
return new Response(null, { status: 500 });
} finally {
this.pendingRequests--;
this.#upgrade = null;
}
}
upgrade<T = undefined>(
request: Request,
options?: {
headers?: HeadersInit;
data?: T;
},
): boolean {
if (request.method === "GET" && request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
this.#upgrade = new Response(null, {
status: 101,
headers: options?.headers,
});
if ("aws" in request && isWebSocketUpgrade(request.aws)) {
const { connectionId } = request.aws.requestContext;
this.#webSockets.set(connectionId, new LambdaWebSocket(request.aws, options?.data));
this.pendingWebSockets++;
}
return true;
}
return false;
}
publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
let count = 0;
for (const webSocket of this.#webSockets.values()) {
count += webSocket.publish(topic, data, compress) ? 1 : 0;
}
return count;
}
}
class LambdaWebSocket implements ServerWebSocket {
#connectionId: string;
#url: string;
#invokeArn: string;
#topics: Set<string> | null;
remoteAddress: string;
readyState: 0 | 2 | 1 | -1 | 3;
binaryType?: "arraybuffer" | "uint8array";
data: any;
constructor(event: WebSocketEvent, data?: any) {
const request = event.requestContext;
this.#connectionId = `${request.connectionId}`;
this.#url = `https://${request.domainName}/${request.stage}/@connections/${this.#connectionId}`;
const [region, accountId] = (functionArn ?? "").split(":").slice(3, 5);
this.#invokeArn = `arn:aws:execute-api:${region}:${accountId}:${request.apiId}/${request.stage}/*`;
this.#topics = null;
this.remoteAddress = request.identity.sourceIp;
this.readyState = 1; // WebSocket.OPEN
this.data = data;
}
send(data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
if (typeof data === "string") {
return this.sendText(data, compress);
}
if (data instanceof ArrayBuffer) {
return this.sendBinary(new Uint8Array(data), compress);
}
const buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return this.sendBinary(buffer, compress);
}
sendText(data: string, compress?: boolean): number {
fetchAws(this.#url, {
method: "POST",
body: data,
})
.then(({ status }) => {
if (status === 403) {
warnOnce(
"Failed to send WebSocket message due to insufficient IAM permissions",
`Assign the following IAM policy to ${functionArn} to fix this issue:`,
{
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["execute-api:Invoke"],
Resource: [this.#invokeArn],
},
],
},
);
} else {
warnOnce(`Failed to send WebSocket message due to a ${status} error`);
}
})
.catch(error => {
warnOnce("Failed to send WebSocket message", error);
});
return data.length;
}
sendBinary(data: Uint8Array, compress?: boolean): number {
warnOnce(
"Lambda does not support binary WebSocket messages",
"https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-binary-media-types.html",
);
const base64 = Buffer.from(data).toString("base64");
return this.sendText(base64, compress);
}
publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
if (this.isSubscribed(topic)) {
return this.send(data, compress);
}
return -1;
}
publishText(topic: string, data: string, compress?: boolean): number {
if (this.isSubscribed(topic)) {
return this.sendText(data, compress);
}
return -1;
}
publishBinary(topic: string, data: Uint8Array, compress?: boolean): number {
if (this.isSubscribed(topic)) {
return this.sendBinary(data, compress);
}
return -1;
}
close(code?: number, reason?: string): void {
// TODO: code? reason?
fetchAws(this.#url, {
method: "DELETE",
})
.then(({ status }) => {
if (status === 403) {
warnOnce(
"Failed to close WebSocket due to insufficient IAM permissions",
`Assign the following IAM policy to ${functionArn} to fix this issue:`,
{
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["execute-api:Invoke"],
Resource: [this.#invokeArn],
},
],
},
);
} else {
warnOnce(`Failed to close WebSocket due to a ${status} error`);
}
})
.catch(error => {
warnOnce("Failed to close WebSocket", error);
});
this.readyState = 3; // WebSocket.CLOSED;
}
subscribe(topic: string): boolean {
if (this.readyState === 3) {
return false;
}
if (this.#topics === null) {
this.#topics = new Set();
}
this.#topics.add(topic);
return true;
}
unsubscribe(topic: string): boolean {
if (this.readyState === 3) {
return false;
}
return this.#topics?.delete(topic) ?? false;
}
isSubscribed(topic: string): boolean {
return this.readyState !== 3 && this.#topics !== null && this.#topics.has(topic);
}
cork(callback: (ws: ServerWebSocket<undefined>) => any): void | Promise<void> {
// Lambda does not support sending multiple messages at a time.
return callback(this);
}
}
const lambda = await init();
const server = new LambdaServer(lambda);
while (true) {
try {
const request = await receiveRequest();
const response = await server.accept(request);
if (response !== undefined) {
await sendResponse(response);
}
} finally {
reset();
}
}

Some files were not shown because too many files have changed in this diff Show More