This is the Bun repository - an all-in-one JavaScript runtime & toolkit designed for speed, with a bundler, test runner, and Node.js-compatible package manager. It's written primarily in Zig with C++ for JavaScriptCore integration, powered by WebKit's JavaScriptCore engine.
- Build Bun:
bun bd- Creates a debug build at
./build/debug/bun-debug - CRITICAL: do not set a timeout when running
bun bd
- Creates a debug build at
- Run tests with your debug build:
bun bd test <test-file>- CRITICAL: Never use
bun testdirectly - it won't include your changes
- CRITICAL: Never use
- Run any command with debug build:
bun bd <command> - Run with JavaScript exception scope verification:
BUN_JSC_validateExceptionChecks=1 BUN_JSC_dumpSimulatedThrows=1 bun bd <command>
Tip: Bun is already installed and in $PATH. The bd subcommand is a package.json script.
All build scripts support build-then-exec. Any bun run build* command (and bun bd, and bun scripts/build.ts directly) accepts trailing args which are passed to the built executable after building. This is the recommended way to run your build — you never invoke ./build/debug/bun-debug directly.
bun bd test foo.test.ts # debug build + quiet debug logs
bun run build test foo.test.ts # debug build
bun run build:release -p 'Bun.version' # release build
bun run build:local run script.ts # debug build with local WebKitWhen exec args are present, build output is suppressed unless the build fails — you see only the binary's output. Build flags (e.g. --asan=off) go before the exec args; see scripts/build.ts header for the full arg routing rules.
Comparing builds: normally use the default build/<profile>/ dir. If you need to preserve a build as a comparison point (rare — e.g. benchmarking before/after a change), --build-dir parks it somewhere the next build won't overwrite:
bun run build:release --build-dir=build/baselineEdits to TypeScript type declarations (packages/bun-types/**/*.d.ts) do not touch any compiled code, so bun bd is unnecessary. The types test just packs the .d.ts files and runs tsc against fixtures — it never executes your build. Run it directly with the system Bun:
bun test test/integration/bun-types/bun-types.test.tsThis is an explicit exception to the "never use bun test directly" rule. There are no native changes for a debug build to pick up, so don't wait on one.
- Single test file:
bun bd test test/js/bun/http/serve.test.ts - Fuzzy match test file:
bun bd test http/serve.test.ts - With filter:
bun bd test test/js/bun/http/serve.test.ts -t "should handle"
If a test is for a specific numbered GitHub Issue, it should be placed in test/regression/issue/${issueNumber}.test.ts. Ensure the issue number is REAL and not a placeholder!
If no valid issue number is provided, find the best existing file to modify instead, such as;
test/js/bun/- Bun-specific API tests (http, crypto, ffi, shell, etc.)test/js/node/- Node.js compatibility teststest/js/web/- Web API tests (fetch, WebSocket, streams, etc.)test/cli/- CLI command tests (install, run, test, etc.)test/bundler/- Bundler and transpiler tests. UseitBundledhelper.test/integration/- End-to-end integration teststest/napi/- N-API compatibility teststest/v8/- V8 C++ API compatibility tests
Tests use Bun's Jest-compatible test runner with proper test fixtures.
- For single-file tests, prefer
-eovertempDir. - For multi-file tests, prefer
tempDirandBun.spawn.
import { test, expect } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
test("(single-file test) my feature", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "console.log('Hello, world!')"],
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`"Hello, world!"`);
expect(exitCode).toBe(0);
});
test("(multi-file test) my feature", async () => {
// Create temp directory with test files
using dir = tempDir("test-prefix", {
"index.js": `import { foo } from "./foo.ts"; foo();`,
"foo.ts": `export function foo() { console.log("foo"); }`,
});
// Spawn Bun process
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
// Prefer snapshot tests over expect(stdout).toBe("hello\n");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"hello"`);
// Assert the exit code last. This gives you a more useful error message on test failure.
expect(exitCode).toBe(0);
});- Always use
port: 0. Do not hardcode ports. Do not use your own random port number function. - Use
normalizeBunSnapshotto normalize snapshot output of the test. - NEVER write tests that check for no "panic" or "uncaught exception" or similar in the test output. These tests will never fail in CI.
- Use
tempDirfrom"harness"to create a temporary directory. Do not usetmpdirSyncorfs.mkdtempSyncto create temporary directories. - When spawning processes, tests should expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0). This gives you a more useful error message on test failure.
- CRITICAL: Do not write flaky tests. Do not use
setTimeoutin tests. Instead,awaitthe condition to be met. You are not testing the TIME PASSING, you are testing the CONDITION. - CRITICAL: Verify your test fails with
USE_SYSTEM_BUN=1 bun test <file>and passes withbun bd test <file>. Your test is NOT VALID if it passes withUSE_SYSTEM_BUN=1.
- Zig code (
src/*.zig): Core runtime, JavaScript bindings, package manager - C++ code (
src/bun.js/bindings/*.cpp): JavaScriptCore bindings, Web APIs - TypeScript (
src/js/): Built-in JavaScript modules with special syntax (see JavaScript Modules section) - Generated code: Many files are auto-generated from
.classes.tsand other sources. Bun will automatically rebuild these files when you make changes to them.
bun.zig- Main entry pointcli.zig- CLI command orchestrationjs_parser.zig,js_lexer.zig,js_printer.zig- JavaScript parsing/printingtranspiler.zig- Wrapper around js_parser with sourcemap supportresolver/- Module resolution systemallocators/- Custom memory allocators for performance
bindings/- C++ JavaScriptCore bindings- Generated classes from
.classes.tsfiles - Manual bindings for complex APIs
- Generated classes from
api/- Bun-specific APIsserver.zig- HTTP server implementationFFI.zig- Foreign Function Interfacecrypto.zig- Cryptographic operationsglob.zig- File pattern matching
node/- Node.js compatibility layer- Module implementations (fs, path, crypto, etc.)
- Process and Buffer APIs
webcore/- Web API implementationsfetch.zig- Fetch APIstreams.zig- Web StreamsBlob.zig,Response.zig,Request.zig
event_loop/- Event loop and task management
src/bundler/- JavaScript bundler- Advanced tree-shaking
- CSS processing
- HTML handling
src/install/- Package managerlockfile/- Lockfile handlingnpm.zig- npm registry clientlifecycle_script_runner.zig- Package scripts
src/shell/- Cross-platform shell implementationsrc/css/- CSS parser and processorsrc/http/- HTTP client implementationwebsocket_client/- WebSocket client (including deflate support)
src/sql/- SQL database integrationssrc/bake/- Server-side rendering framework
Third-party C/C++ libraries are vendored locally and can be read from disk (these are not git submodules):
vendor/boringssl/- BoringSSL (TLS/crypto)vendor/brotli/- Brotli compressionvendor/cares/- c-ares (async DNS)vendor/hdrhistogram/- HdrHistogram (latency tracking)vendor/highway/- Google Highway (SIMD)vendor/libarchive/- libarchive (tar/zip)vendor/libdeflate/- libdeflate (fast deflate)vendor/libuv/- libuv (Windows event loop)vendor/lolhtml/- lol-html (HTML rewriter)vendor/lshpack/- ls-hpack (HTTP/2 HPACK)vendor/mimalloc/- mimalloc (memory allocator)vendor/nodejs/- Node.js headers (compatibility)vendor/picohttpparser/- PicoHTTPParser (HTTP parsing)vendor/tinycc/- TinyCC (FFI JIT compiler, fork: oven-sh/tinycc)vendor/WebKit/- WebKit/JavaScriptCore (JS engine)vendor/zig/- Zig compiler/stdlibvendor/zlib/- zlib (compression, cloudflare fork)vendor/zstd/- Zstandard (compression)
Build configuration for these is in scripts/build/deps/*.ts.
When implementing JavaScript classes in C++:
-
Create three classes if there's a public constructor:
class Foo : public JSC::JSDestructibleObject(if has C++ fields)class FooPrototype : public JSC::JSNonFinalObjectclass FooConstructor : public JSC::InternalFunction
-
Define properties using HashTableValue arrays
-
Add iso subspaces for classes with C++ fields
-
Cache structures in ZigGlobalObject
Code generation happens automatically as part of the build process. The main scripts are:
src/codegen/generate-classes.ts- Generates Zig & C++ bindings from*.classes.tsfilessrc/codegen/generate-jssink.ts- Generates stream-related classessrc/codegen/bundle-modules.ts- Bundles built-in modules likenode:fssrc/codegen/bundle-functions.ts- Bundles global functions likeReadableStream
In development, bundled modules can be reloaded without rebuilding Zig by running bun run build.
Built-in JavaScript modules use special syntax and are organized as:
node/- Node.js compatibility modules (node:fs,node:path, etc.)bun/- Bun-specific modules (bun:ffi,bun:sqlite, etc.)thirdparty/- NPM modules we replace (likews)internal/- Internal modules not exposed to usersbuiltins/- Core JavaScript builtins (streams, console, etc.)
- Before writing code that makes a non-obvious choice, pre-emptively ask "why this and not the alternative?" If you can't answer, research until you can — don't write first and justify later.
- Don't take a bug report's suggested fix at face value; verify it's the right layer.
- If neighboring code does something differently than you're about to, find out why before deviating — its choices are often load-bearing, not stylistic.
- Never use
bun testorbun <file>directly - always usebun bd testorbun bd <command>.bun bdcompiles & runs the debug build. - All changes must be tested - if you're not testing your changes, you're not done.
- Get your tests to pass. If you didn't run the tests, your code does not work.
- Follow existing code style - check neighboring files for patterns
- Create tests in the right folder in
test/and the test must end in.test.tsor.test.tsx - Use absolute paths - Always use absolute paths in file operations
- Avoid shell commands - Don't use
findorgrepin tests; use Bun's Glob and built-in tools - Memory management - In Zig code, be careful with allocators and use defer for cleanup
- Cross-platform - Run
bun run zig:check-allto compile the Zig code on all platforms when making platform-specific changes - Debug builds - Use
BUN_DEBUG_QUIET_LOGS=1to disable debug logging, orBUN_DEBUG_<scopeName>=1to enable specificOutput.scoped(.${scopeName}, .visible)s - Be humble & honest - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user.
- Branch names must start with
claude/- This is a requirement for the CI to work.
ONLY push up changes after running bun bd test <file> and ensuring your tests pass.
Requires the BuildKite CLI (brew install buildkite/buildkite/bk) and a read-scoped token in BUILDKITE_API_TOKEN. The repo's .bk.yaml sets the org/pipeline so -p bun is not needed.
# Show rendered test-failure output for the current branch's latest build,
# tagged [new] vs [also on main]
bun run ci:errors
bun run ci:errors '#26173' # or a PR number / URL / branch / build number
# One-screen progress summary (job counts, failed jobs, failing tests so far)
bun run ci:status
# Save full logs for every failed job to ./tmp/ci-<build>/
bun run ci:logs
# Just the build number, for composing with raw `bk`
bun run ci:find
bk job log <job-uuid> -b $(bun run ci:find)
# Watch the current branch's build until it finishes
bun run ci:watchFor anything else, use bk directly — bk build list, bk api, bk artifacts, etc.
If output from these commands looks wrong — mis-parsed annotation HTML, confusing wording, a field BuildKite changed shape on — fix scripts/find-build.ts directly rather than working around it. It's a thin presenter over bk; keep it accurate.
gh pr view --comments is fine for a quick look at the Conversation tab, but it has a footgun worth knowing about: it only returns issue-stream comments and silently omits review summaries and line-level review comments. If a reviewer leaves an inline comment on a specific file line, it will not show up — no error, no hint that anything is missing.
When you want the complete picture — especially when responding to a review or checking whether anyone requested changes — use bun run pr:comments. It fetches all three GitHub endpoints (/issues/N/comments, /pulls/N/reviews, /pulls/N/comments) and prints them in one chronological listing, each labelled with its actual type (issue comment, review verdict, line comment, reply, suggestion block).
bun run pr:comments # current branch's PR — XML, resolved threads hidden
bun run pr:comments 28838 # by PR number
bun run pr:comments '#28838' # also works
bun run pr:comments https://github.com/oven-sh/bun/pull/28838
bun run pr:comments --include-resolved # also show threads already marked resolved
# Machine-readable output for jq pipelines — one object per entry with
# { when, user, tag, state?, suggestion?, location?, body, url?, resolved?, outdated? }.
# Resolved threads and bot noise (robobun's CI status comment, CodeRabbit
# body-level summaries) are filtered out; --include-resolved restores the former.
bun run pr:comments --json | jq '.[] | select(.user == "Jarred-Sumner")'