oven-sh/bun
github.com/oven-sh/bun ↗ · 3996 files · 37.9 MB · scan capped
Bun Documentation • Discord • Issues • Roadmap Read the docs → What is Bun? Bun is an all-in-one toolkit for JavaScript and TypeScript apps.
Overview
Bun is an all-in-one JavaScript/TypeScript runtime, bundler, transpiler, test runner, and npm-compatible package manager built on WebKit's JavaScriptCore and shipped as a single native binary. The codebase at this commit is a ~200-crate Rust Cargo workspace (`src/`) linked against a large C++ JSC bindings layer (`src/jsc/bindings/`, 407 files), ~15 vendored native dependencies, and a TypeScript layer (`src/js/`) that is bundled at build time into the binary as built-in modules like `node:fs` and `bun:ffi`. Everything routes through one process entry (`src/runtime/bin_entry/mod.rs::main`) into a single subcommand dispatch table (`src/runtime/cli/mod.rs`) covering 34 command tags — `run`, `install`, `test`, `build`, `pm`, `x`, `publish`, `audit`, and more. What's distinctive is the obsessive cold-start engineering (`#[cold]` on every non-`bun run` subcommand body, symbol ordering, mimalloc wired into ICU/libuv/BoringSSL before anything allocates) and a review culture encoded as machine-checked policy: `clippy.toml` bans `std::fs`, `mordant.toml` ratchets Rust lints against a baseline, and `REVIEW.md` distills ~2,500 merged PRs into merge-blocking rules.
System map
One native executable with a Rust core, a C++ JSC binding layer, and a build-time-bundled TypeScript module layer. `src/runtime/bin_entry/mod.rs` is the C-ABI `main`; it initializes the crash handler, allocator, stdio, and stack limits, then hands off to `cli::Cli::start()` which decodes a `Tag` and dispatches to one of 34 subcommand bodies. Storage is not a database — it's the lockfile (`bun.lock` text / `bun.lockb` binary under `src/install/lockfile/`), the global package cache, and the standalone-executable graph appended to the binary itself. Builds go through `scripts/build.ts` → CMake/ninja + `cargo` per-crate rlibs; CI is a generated Buildkite pipeline (`.buildkite/ci.ts`) plus 25 GitHub Actions workflows. Deployment shape is a downloadable binary plus npm packages, Docker images (`dockerhub/`), and a VS Code extension (`packages/bun-vscode`).
- Process entry is `extern "C" fn main(argc, argv)` in `src/runtime/bin_entry/mod.rs`, which captures argv first (because musl static builds never pass argv to `.init_array` hooks), then initializes the crash handler, points ICU/libuv/BoringSSL at mimalloc, ignores SIGPIPE/SIGXFSZ, calls `output::stdio::init()`, `StackCheck::configure_thread()`, and finally `cli::Cli::start()` before `Global::exit(0)`.
- The allocator is selected at compile time: mimalloc is the `#[global_allocator]` normally, but under `bun_asan` the build swaps to `std::alloc::System` so the ASAN interceptor sees every allocation; the binary also strong-defines `__asan_default_options` and `__lsan_default_suppressions` from the binary crate (not an rlib) so the link actually picks them up.
- CLI dispatch is a flat `match tag { … }` over `bun_options_types::command_tag::Tag`, with every arm out-lined into its own `#[cold] #[inline(never)]` fn so `Cli::start` compiles to a jump table and cold subcommand bodies (`install`, `create`, bundler, test runner) don't share `.text` pages with the `bun run` startup chain.
- `Tag` enumerates 34 subcommands and each carries a single-byte `char()` code used in crash-report URLs, explicitly documented as needing to stay in sync with the external `oven-sh/bun.report` decoder.
- The Rust side is a Cargo workspace of ~200 member crates declared in the root `Cargo.toml`, tiered so lower crates (`bun_core`, `bun_sys`, `bun_paths`) never depend on `bun_jsc` or `bun_runtime`; `bun_runtime` is the graph root and every crate's rlib is an input to the final link.
- The JS engine boundary lives in two halves: Rust-side glue in `src/jsc/` (`VirtualMachine.rs` at 7,700 lines, `ModuleLoader.rs`, `event_loop.rs`) and 407 files of hand-written + generated C++ in `src/jsc/bindings/` (`BunObject.cpp`, `ZigGlobalObject`, `BunPlugin.h`, per-class `JS*` files).
- Built-in JS modules are authored as TypeScript under `src/js/{node,bun,internal,thirdparty,builtins}` and bundled into the binary by `src/codegen/bundle-modules.ts`; they are not ES modules — only string-literal `require()` and `$`-prefixed intrinsics are allowed.
- Code generation is build-integrated: `generate-classes.ts` turns `*.classes.ts` definitions into paired Rust + C++ bindings, alongside `generate-jssink.ts`, `generate-host-exports.ts`, `generate-js2native.ts`, `bundle-functions.ts` and `generate-node-errors.ts`.
- The build orchestrator is `scripts/build.ts`: it configures and runs ninja, supports profiles (`debug`, `release`, `release-asan`, `release-assertions`, `debug-local`), and implements build-then-exec arg routing where the first unrecognized arg starts the exec-args for the produced binary.
- External integrations are first-class runtime surface rather than libraries: Postgres/MySQL/SQLite (`src/sql/`), Valkey/Redis (`src/valkey/`), S3 with SigV4 signing (`src/s3_signing/`), and an npm registry client (`src/install/npm.rs`).
- Persistence for the package manager is the lockfile subsystem under `src/install/lockfile/`, which supports both the text `bun.lock` and legacy binary `bun.lockb` formats plus migration from other package managers (`src/install/migration/`, `src/install/pnpm.rs`).
- CI is a programmatically generated Buildkite pipeline: `.buildkite/ci.ts` (1,997 lines) emits build steps, baked machine-image steps, and sharded test steps whose parallelism is platform-dependent (20 shards on Linux, 8 on Windows, 2 on macOS) with per-platform timeouts.
Feature catalog
User-facing surface splits four ways: the CLI subcommand table, the `Bun` global namespace, the Node.js compatibility layer, and Web APIs. The CLI covers running scripts, package management (install/add/update/remove/link/patch/publish/audit/why/dedupe/prune/outdated), bundling (`bun build`, including `--compile` standalone executables), testing (`bun test`), a REPL, `bunx`, `bun init`/`bun create`, and self-upgrade. The `Bun` global is defined by a JSC hash table in `BunObject.cpp` exposing ~100 properties — `Bun.serve`, `Bun.file`, `Bun.write`, `Bun.spawn`, `Bun.build`, `Bun.sql`, `Bun.redis`, `Bun.s3`, `Bun.secrets`, `Bun.CSRF`, `Bun.$` shell, FFI, and hashing/compression helpers. Node compatibility is implemented per-module across `src/js/node/*.ts` and `src/runtime/node/*.rs`. Bake (`src/runtime/bake/`) adds a hot-reloading dev server and server-components framework toolkit on top of the bundler and `Bun.serve`.
- The CLI exposes 34 dispatchable commands including `run`, `test`, `build`, `install`, `add`, `update`, `remove`, `link`/`unlink`, `patch`/`patch-commit`, `outdated`, `update --interactive`, `publish`, `audit`, `why`, `dedupe`, `prune`, `pm`, `x` (bunx), `repl`, `exec`, `init`, `create`, `upgrade`, and a `fuzzilli` fuzzing entry point.
- `bun pm` is itself a subcommand hub with its own verbs — `pm ls`, `pm why`, `pm view`, `pm pkg`, `pm licenses`, `pm trust`/`untrusted`/`default-trusted`, `pm version`, and a `pm diff` implementation split across five files (`pm_diff_command.rs`, `pm_diff_semantic.rs`, `pm_diff_normalize.rs`, `pm_diff_relayout.rs`, `pm_diff_profile.rs`).
- The `Bun` global's full property list is a JSC static hash table in `BunObject.cpp`, including `serve`, `file`, `write`, `spawn`/`spawnSync`, `build`, `listen`/`connect`/`udpSocket`, `password`, `hash`, `which`, `mmap`, `inspect`, `peek`, `plugin`, `Transpiler`, `embeddedFiles`, `randomUUIDv5`/`v7`, `stringWidth`/`stripANSI`/`wrapAnsi`, and `generateHeapSnapshot`.
- Data-format parsers are exposed as lazily constructed `Bun` properties: `Bun.TOML`, `Bun.YAML`, `Bun.XML`, `Bun.JSON5`, `Bun.JSONC`, `Bun.JSONL`, and `Bun.markdown`, backed by dedicated Rust objects.
- `Bun.serve()` is implemented as a const-generic `NewServer<SSL, DEBUG>` monomorphized four ways (HTTP/HTTPS × normal/debug) over uWebSockets, with per-request state in a 4,794-line `RequestContext.rs` that also handles WebSocket upgrades, range requests, and cookie maps.
- Bake is a dev-server + SSR framework toolkit that attaches to `Bun.serve`, holds all build state in memory with an incremental graph, and is explicitly designed as infrastructure for frameworks rather than being a framework itself; it ships a React framework (`src/runtime/bake/bun-framework-react`) and an HMR client runtime.
- `bun build --compile` produces standalone executables by appending a `bun_standalone_graph::Graph` to the binary; boot takes a dedicated `#[cold]` path that splices `compile_exec_argv` and `BUN_OPTIONS` into argv before handing off to `RunCommand::boot_standalone`.
- The test runner is a Jest-compatible implementation in `src/runtime/test_runner/` with `expect` matchers, snapshot support, structured diffing, and a `CommandLineReporter` driven from `test_command.rs::run_all_tests`.
- Node.js compatibility is implemented module-by-module in TypeScript (`src/js/node/` — `fs`, `http`, `http2`, `net`, `tls`, `dgram`, `cluster`, `worker_threads`, `vm`, `quic`, `wasi`, `inspector`, `domain`, `trace_events`) with native bindings alongside in `src/runtime/node/` (`node_fs.rs` is the second-largest Rust file in the repo).
- Web/platform APIs live in `src/runtime/webcore/`: `fetch`, `Request`/`Response`, `Blob`, `FormData`, `ReadableStream` and sinks, `TextEncoder`/`TextDecoder`, `CompressionStream`, `CookieMap`, `FileReader`, `Crypto`, and an `ObjectURLRegistry`.
- `bun:ffi` compiles C at runtime via a vendored TinyCC (`bun_tcc_sys::State`), exposing `cc`, `linkSymbols`, `dlopen`, and JS callbacks, with bundled stdlib headers shipped as source files next to the implementation.
- `Bun.secrets` is an OS-keychain integration with three platform backends — macOS Keychain, Linux libsecret, and Windows Credential Manager — behind one JS object.
Data flow
Three main pipelines dominate. (1) Source → runtime: the JSC module loader asks `Bun__transpile*` hooks in `jsc_hooks.rs`, which resolve through `bun_resolver`, parse with `bun_js_parser`, print with `bun_js_printer`, and hand a `ResolvedSource` back to JSC; results are cached in a `RuntimeTranspilerStore` and an arena that is reset between graphs. (2) Package manifests → lockfile → disk: `install_with_manager` loads the lockfile, resolves dependencies via the registry client and task queue, runs a security scan, extracts tarballs, links binaries, then runs lifecycle scripts and saves the lockfile. (3) Bundling: `BundleV2` owns a `Graph` of parse tasks fanned out to worker threads, then `LinkerContext` performs tree-shaking, chunking, and sourcemap emission. Cross-thread movement is explicit: `ConcurrentTask`/`DeferredTaskQueue` marshal work onto the JS thread, and the HTTP client runs on its own `HTTPThread`.
- Module loading is split across the crate boundary on purpose: `ModuleLoader` and `FetchFlags` live in `bun_jsc`, while the actual `transpile_source_code`/`fetch_builtin_module` bodies and the `Bun__transpile*`/`Bun__resolve*` C++ entry points live in `bun_runtime::jsc_hooks` and are reached through link-time `extern "Rust"` declarations.
- The transpiler arena is owned by `ModuleLoader::transpile_source_code_arena` and reset between module graphs via `ModuleLoader::reset_arena`, which takes `&mut VirtualMachine` rather than `&mut self` because `ModuleLoader` is a value field of the VM and taking both would alias.
- Entry-point evaluation flows `vm.load_entry_point(entry)` → a JSC internal promise; a rejected promise is routed to `vm.uncaught_exception` with an `is_rejection` flag derived from `entry_point_result.evaluated_as_cjs`, so a CJS entry's top-level throw is reported as `uncaughtException` and an ESM entry's as `unhandledRejection`, matching Node.
- Environment data enters through `bun_dotenv::Loader` wired onto the transpiler, is published to the HTTP client via `bun_http::async_http::load_env`, and on Windows the UTF-16 OS environment block is converted to WTF-8 in `main()` before anything reads `process.env`.
- CLI configuration merges three inputs — argv (parsed by `Arguments::parse` into a `ContextData`), the `BUN_OPTIONS` env var (spliced into argv), and `bunfig.toml` parsed by `src/bunfig/bunfig.rs` — with `ctx.args`/`ctx.debug`/`ctx.runtime_options` as the state owner passed down to every command.
- Registry credentials flow from two sources into one `api::NpmRegistry` value: `.npmrc`-style `_authToken` segments parsed in `src/install/npm.rs`, and `[install.scopes]` entries in bunfig where explicit `username`/`password`/`token` keys replace any credentials embedded in the URL as a set.
- `install_with_manager` is the state machine for installs: prefetch DNS for the registry, load the lockfile from cwd, reconcile config version, diff the root package, enqueue dependency resolution, run tasks, then save the lockfile — with a `mark_loaded_packages()` checkpoint so packages appended by manifest fetches are distinguishable from lockfile pins.
- The security scanner runs user-supplied scanning code out-of-process and talks to it over two dedicated file descriptors — fd 3 for the child's JSON result and fd 4 for the parent's package list, read to EOF — rather than stdin/stdout, so scanner console output can't corrupt the protocol.
- Lifecycle scripts are spawned through `bun_spawn` from `src/install/lifecycle_script_runner.rs`, which is deliberately placed in the lower `bun_install` crate so `bun run` and package-install script execution share one implementation (`RunCommand::replace_package_manager_run` is a thin re-export).
- Bundling state is owned by `BundleV2`, which holds a `Graph`, a `PathToSourceIndexMap`, `ParseTask`s, and an optional second `ssr_transpiler` used for the client half of a Server Components build; linking and chunking happen in the separate `LinkerContext`.
- The dev server keeps every build artifact in memory behind an `IncrementalGraph` with the stated invariant that editing a file must rebundle exactly that file, and exposes a `memory_cost_detailed` breakdown so that in-memory retention is measurable.
- Cross-thread data movement is centralized in `src/event_loop/`: `ConcurrentTask` and `DeferredTaskQueue` enqueue onto the JS thread, `AnyEventLoop` abstracts over the uWS loop on POSIX and libuv on Windows, and `MiniEventLoop`/`SpawnSyncEventLoop` serve non-JS contexts like the package manager.
Behavior traces
Three flows walked end to end. Startup (`bun foo.ts`): C runtime → `main` → allocator/crash/stdio init → `Cli::start` → `Command::start` → fast-path detection of a run entrypoint → `init` → `RunCommand::exec_with_cfg` → `boot` → `VirtualMachine::init_with_module_graph` → `Run::start` → `load_entry_point` → event-loop drain → `on_before_exit`. Install: `Tag::InstallCommand` → `InstallCommand::exec` → `PackageManager::init` → `install_with_manager` → lockfile load → resolution → task run → scanner → installer → lifecycle scripts → `save_lockfile`. Serve/dev: `Bun.serve` constructs a monomorphized `NewServer`, uWS delivers a request into `RequestContext`, and under Bake the DevServer checks the Host header, resolves a route through `FrameworkRouter`, incrementally bundles, and pushes HMR events over a WebSocket.
- Startup step 0 is argv capture (`bun_core::init_argv`) explicitly before `bun_crash_handler::init()`, because the crash handler's panic path prints the command line from `bun_core::argv()`.
- On Linux, `main()` calls `pregrow_fd_table()` — a `dup_at_least(stdin, 1023)` followed by `close` — before any thread exists, because `expand_fdtable()` waits for an RCU grace period once the process is multi-threaded.
- `Cli::start` sets the main-thread id for crash reporting, records the start time, initializes a process-lifetime `Log` and a `CLI_ARENA` mimalloc arena via `RacyCell` (sound because it runs single-threaded), then calls `Command::start(log)` and routes any error through `handle_root_error` after printing accumulated diagnostics.
- `Command::start` short-circuits: if `argv[1]` `looks_like_run_entrypoint`, it jumps straight to `exec_auto_or_run(Tag::AutoCommand)` without decoding the subcommand keyword table at all, and only otherwise calls `which()` to classify.
- `RunCommand::boot` calls `VirtualMachine::init_with_module_graph`, then moves preloads and passthrough argv onto the VM, leaks the entry path to `'static` (because `vm.main` is a backref and the runner never returns), wires the transpiler from the context, calls `configure_defines()`, and loads env into the HTTP client before constructing `Run` and calling `.start()`.
- `Run::start` takes the JSC API lock and never drops it (the function is `-> !`), then in order: enables CPU/heap profilers, adds conditional globals (`Bun__ExposeNodeModuleGlobals` for `--eval`, `addGc` for `--expose-gc`), performs optional Redis and SQL preconnects under the lock, enables the hot/watch reloader, and only then loads the entry point.
- After the entry graph evaluates, `Run::start` runs one opportunistic `release_weak_refs()` + `run_gc(false)` to drop transpiler garbage — but skips it for standalone executables, whose code blocks came from an embedded bytecode cache and would only need re-decoding.
- The core run-loop has two shapes: under `--watch`/`--hot` it loops forever (`tick` → `auto_tick_active` → `on_before_exit` → `tick_possibly_forever`) so a rejected entry keeps the process alive; otherwise it drains until the loop is no longer alive, optionally prints the entry result for `--print`, and calls `on_before_exit` once.
- For `--print` with a pending top-level promise, the runner attaches `Bun__onResolveEntryPointResult`/`Bun__onRejectEntryPointResult` (C-ABI shims emitted by `generate-host-exports.ts`), re-drains the loop, and then prints through `ConsoleObject::message_with_type_and_level`.
- `bun install` begins by prefetching DNS for the default registry — but only when there is no HTTP proxy, the mode is not offline, and the registry hostname is not already an IP address.
- Lockfile loading uses a single raw-pointer provenance root with an explicit SAFETY note, because `load_from_cwd` needs `manager`, `manager.lockfile`, and `manager.log` borrowed simultaneously and borrowck cannot express the disjointness.
- The install path decides whether to force a lockfile rewrite by OR-ing four conditions — an explicit force flag, a changed config version, a migration from another package manager, and a binary lockfile encountered while `save-text-lockfile` is set.
Testing signals
Testing is dominated by 2,149 JS/TS test files run through Bun's own Jest-compatible runner, orchestrated by a Node.js script (`scripts/runner.node.ts`, 3,504 lines) that deliberately avoids Bun APIs and imports nothing, spawning each file in a separate process so crashes are attributable. Coverage is heaviest in `test/js` (1,307 files) and `test/regression` (426 issue-numbered files); `test/bundler` uses a dedicated `itBundled` harness, `test/bake` covers HMR, and `test/internal` tests the build scripts themselves. Rust has thin unit coverage — 76 files contain `#[test]` out of ~700 Rust source files. CI shards the suite 20 ways on Linux with ASAN options set, and non-binary lanes (types, source lints) are excluded from Buildkite and run in GitHub Actions instead.
- There are 2,149 test files under `test/`, distributed as `test/js` 1,307, `test/regression` 426, `test/cli` 202, `test/bundler` 110, `test/napi` 60, `test/internal` 57, `test/integration` 28, `test/bake` 27, and `test/v8` 1.
- `scripts/runner.node.ts` is explicitly documented as running each `bun test` invocation in a separate process to catch crashes, as unable to use Bun APIs (it runs under Node), and as importing no dependencies so it starts fast.
- `test/harness.ts` (2,367 lines) is the shared fixture layer, exporting `bunExe()`, `bunEnv`, `tempDir`/`tempDirWithFiles`, `normalizeBunSnapshot`, platform predicates (`isMacOS`, `isMusl`, `isASAN`, `isDebug`), `describeWithContainer` for Docker-backed suites, `getFDCount`, and RSS-delta assertions for leak tests.
- Bundler tests use a purpose-built `itBundled` helper in `test/bundler/expectBundled.ts` rather than raw spawns, and the repo documents it as the required entry point for bundler/transpiler tests.
- Regression tests are namespaced by real GitHub issue number under `test/regression/issue/`, and CLAUDE.md restricts that directory to true regressions — behavior that worked in a prior release — with everything else going into the existing module test file.
- `test/internal/` tests the build system itself rather than the runtime — `build-rust-toolchain-probe.test.ts`, `build-post-link-ordering.test.ts`, `build-timings.test.ts`, `rust-check-all.test.ts`, `runner-junit.test.ts`, `parallel-allowlist.test.ts`.
- Rust unit-test coverage is sparse relative to the surface: only 76 files under `src/` contain a `#[test]` attribute and 79 contain a `#[cfg(test)]` module, against ~701 Rust source files — correctness is overwhelmingly asserted from the JS side against a built binary. (inferred)
- CI test steps are sharded by platform — `parallelism` 20 on Linux, 8 on Windows, 2 on macOS, and 1 on the beta tier — with timeouts of 30/45/60 minutes and `soft_fail` only on the beta lane.
- The Buildkite test step explicitly excludes `integration/bun-types` and `internal/source-lints`, delegating them to `.github/workflows/bun-types.yml` and `.github/workflows/source-lints.yml` because they never touch the built binary.
- CI test jobs set `ASAN_OPTIONS=allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0` and assert the agent's actual platform via `EXPECTED_PLATFORM_OS`/`ARCH`/`ABI`/`DISTRO`/`RELEASE` before running any test, so a mislabeled agent fails loudly instead of silently testing the wrong target.
- Leak testing has dedicated infrastructure: a `testleak` npm script that enables `BUN_DESTRUCT_VM_ON_EXIT=1` with LSAN suppressions from `test/leaksan.supp`, plus in-binary suppressions compiled into `__lsan_default_suppressions` for Rust-side structural allocations.
- Flakiness and timing are managed as tracked data files rather than ad-hoc retries: `test/flaky-tests.txt`, `test/expected-durations.json`, `test/parallel-allowlist.json`, `test/parallel-denylist.txt`, and `test/expectations.txt`.
Risk map
The dominant risk class is memory safety across a Rust/C++/JSC boundary where `unsafe` is routine, raw pointers are stored across event-loop turns, and GC interaction is manual — `REVIEW.md` names this the most-blocked review category. Security-sensitive paths include registry credential handling, lifecycle scripts and the out-of-process security scanner, runtime C compilation via TinyCC, `Bun.secrets` keychain access, S3 SigV4 signing, CSRF token HMACs, the dev server's Host-header check, and the self-upgrade download path. Fragile coupling shows up where a value must stay in sync with an out-of-repo consumer (crash-report tag chars and `bun.report`) or with generated code. Blast-radius hotspots are the very large single files: `bundle_v2.rs` (8,079 lines), `VirtualMachine.rs` (7,700), `DevServer.rs` (6,792), `RequestContext.rs` (4,794), `server/mod.rs` (4,372).
- `Run::start` holds the JSC API lock in a guard that is never dropped, justified only by the function's `-> !` return type — any future refactor that makes it return would leak or double-release the lock.
- The run path deliberately leaks the entry path (`Box::leak(entry_path)`) because `vm.main` stores a raw backref into it and the hot reloader also retains it; correctness depends on the process never unwinding out of `start()`.
- CPU/heap profiler config casts `ctx`-owned `Box<[u8]>` borrows to `'static` through `std::ptr::from_ref`, relying on the context being process-lifetime — a lifetime erasure that is correct only for this one caller.
- `Tag::char()` must stay byte-for-byte in sync with a decoder in a separate repository (`oven-sh/bun.report`), and the comment pins a specific commit URL — a silent cross-repo coupling where adding a command breaks crash-report demangling with no local test.
- Crash reports are uploaded to a default remote base URL (`https://bun.report`) encoding the command tag, platform, and address trace; there is an override and a disable path, but the default is outbound network on crash.
- Registry credentials are parsed from `_authToken` segments and from bunfig scope objects into a single `NpmRegistry` value, meaning a bunfig or `.npmrc` parsing bug is directly a credential-disclosure or credential-confusion risk across registries.
- `bun install` executes arbitrary package-authored lifecycle scripts via `bun_spawn`, gated by a `trustedDependencies` allowlist plus a checked-in `default-trusted-dependencies.txt` — the allowlist is the only thing between an install and arbitrary code execution.
- The security scanner runs third-party scanner modules in a child process communicating over fds 3 and 4; the child reads the packages JSON to EOF and a write failure only prints to stderr and exits 1, so protocol-level failures depend on the parent correctly distinguishing scan failure from clean scan.
- `bun:ffi` compiles and executes attacker-influenced C at runtime through a vendored TinyCC fork, and `dlopen`s arbitrary shared libraries — an intentional capability with no sandbox, so any path that lets untrusted input reach `Bun.FFI.cc` is a full compromise.
- `bun upgrade` downloads release tarballs from GitHub, verifies a `sha256:` digest only when `version.digest.tag.is_supported()`, and additionally shells out to an external verify step — so an unsupported digest tag silently degrades integrity checking to whatever the verify subprocess does.
- The dev server's `is_allowed_host_header` allows any bare IP literal as a Host value; combined with binding to a non-loopback address this widens the accepted origin set beyond the configured hostname.
- CSRF tokens are HMAC-signed with a caller-supplied secret and an optional `session_id`; an empty `session_id` slice explicitly means the token is bound to no principal, so a caller omitting it produces tokens valid for any session.
Extension points
Extension happens at five layers. New CLI subcommands add a `Tag` variant plus a dispatch arm and an out-lined `#[cold]` body. New JS classes are declared in `*.classes.ts` and generated into paired Rust/C++ bindings, or hand-written as the three-class JSC pattern. The `Bun` global is a static hash table in `BunObject.cpp` — adding a property is a table row plus a callback. Userland extends the bundler and runtime through `Bun.plugin` (regex-filter groups of onResolve/onLoad callbacks) and native plugins via `packages/bun-native-plugin-rs`/`-api`. Frameworks extend Bake through `FrameworkRouter` and the `UserOptions` framework description. The install path is extensible through a user-supplied security scanner module and bunfig-declared registries and scopes.
- Adding a CLI command means adding a `Tag` variant, a unique crash-report `char()`, an arm in `Command::start`'s match, and an out-lined `#[cold] #[inline(never)]` exec fn — the file documents this convention so `start` stays a jump table.
- Package-manager commands are registered through a macro table mapping an exec fn to a `(Tag, handler)` pair — `exec_install => (InstallCommand, InstallCommand::exec)` and seventeen siblings — so new pm verbs plug into one list.
- New JS-visible classes are declared in a `.classes.ts` file (`define({ name, proto, values, JSType })`) and `src/codegen/generate-classes.ts` emits the prototype, constructor, subspace, and Rust binding — ~25 such definitions exist under `src/runtime/api/` alone.
- Properties on the `Bun` global are added as rows in the `BunObject.cpp` static hash table, choosing between `Function`, `PropertyCallback` (lazily constructed), and `CustomAccessor` — no separate registration call is needed.
- `Bun.plugin` is implemented as `Zig::BunPlugin` holding `Group`s of JIT-compiled `JSC::RegExp` filters paired with JS callbacks plus a `VirtualModuleMap`, targeted per `BunPluginTarget` — that is the userland hook for onResolve/onLoad.
- Native (non-JS) bundler plugins are a supported extension surface with published scaffolding: a C API in `packages/bun-native-bundler-plugin-api` and a Rust crate in `packages/bun-native-plugin-rs`.
- Bake is explicitly designed as framework infrastructure rather than a framework, with `UserOptions`/`Framework` parsed `from_js` in `bake_body.rs` and routing delegated to a pluggable `FrameworkRouter` keyed by `OpaqueFileId`.
- Reference framework integrations ship in-tree as plugin packages — `packages/bun-plugin-svelte`, `packages/bun-plugin-yaml`, and `src/runtime/bake/bun-framework-react` — which double as the worked examples for third parties.
- `bun install` accepts a user-supplied security scanner module: `scanner-entry.ts` is templated with `__SCANNER_MODULE__`/`__SUPPRESS_ERROR__`, runs the module in a child process, and returns typed `Bun.Security.Advisory[]` results.
- Registries and per-scope credentials are declarative extension points in `bunfig.toml` — `[install]` `registry` and `[install.scopes]` entries each parse into an `api::NpmRegistry` accepting either a URL string or an object with `url`/`username`/`password`/`token`.
- Node module surface is extended by adding a TypeScript file under `src/js/node/` and, when native work is needed, a paired binding in `src/runtime/node/*_binding.rs`; the bundler picks new modules up through `internal-module-registry-scanner.ts`.
- Host functions callable from bundled JS are registered through `js2native` codegen: `$rust(ffi.rs, Bun__FFI__cc)` resolves to a Rust path derived from the file basename, which is why `ffi/mod.rs` re-exports `bun__ffi__cc` under a codegen-expected module path.
Contributor handoff
Read `CLAUDE.md`, `REVIEW.md`, `src/AGENTS.md`, and `src/js/AGENTS.md` before writing anything — they encode rules that are enforced, not suggestions. The single most important operational fact: never run `bun test` or `bun <file>` directly; use `bun bd` (which builds the debug binary and execs it with trailing args) so your native changes are actually exercised. Prefer in-tree helpers over `std` — `clippy.toml` mechanically bans `std::fs::*`, `std::env::var`, `std::thread::spawn`, and `std::mem::zeroed`. Add tests to the existing file for the module you changed, not a new file, and reserve `test/regression/issue/<n>.test.ts` for real regressions with a real issue number. Branch names must start with `claude/`. The one documented exception to the build requirement is `packages/bun-types/**/*.d.ts`, which can be tested with the system Bun.
- Build and run with `bun bd` — it builds the debug binary and passes trailing args through to it; `bun bd test <file>` is the only valid way to run tests against your changes, and `bun bd` must not be given a timeout.
- Every build script supports build-then-exec with the same arg routing: build flags first, then the first unrecognized arg (or an explicit `--`) starts the exec args, and build output is suppressed unless the build fails.
- `clippy.toml` is a hard boundary: it lists disallowed methods with replacement guidance — `std::fs::*` → `bun_sys::file`/`dir`, `std::env::var` → `bun_core::env_var`, `std::thread::spawn` → `bun_threading::spawn_named`, `std::mem::zeroed` → `MaybeUninit` (zeroed is UB for most Bun types).
- Error handling follows a fixed shape: each crate defines a `thiserror` enum at `<crate>/error.rs` re-exported as `crate::Error`/`crate::Result`; the syscall boundary uses `bun_sys::Maybe<T>`; OOM routes through `bun_alloc::AllocError`; and `.unwrap()` is only for invariants you can prove, never for anything user input or the OS can reach.
- `mordant.toml` runs a ratcheting Rust lint pass against `mordant-baseline.toml`: a PR fails only when it adds a new finding for an existing (lint, file) pair, so expect to either fix or regenerate the baseline via `bun run rust:mordant:baseline`.
- Platform-gated code is not type-checked unless that target is built — run `bun run rust:check-all` (which drives `scripts/rust-check-all.ts` across linux/macos/windows × x64/aarch64) after touching any `#[cfg(...)]` branch.
- Built-in JS in `src/js/` must be tamper-resistant: `$`-prefixed intrinsics and primordial-safe calls (`$isJSArray`, `map.$get`, `$call`), globals captured at module load, `node:`-prefixed requires, and never routing internal logic through user-overridable machinery like `Array.isArray`.
- Tests must copy harness conventions exactly: `tempDir` from `"harness"` (never `tmpdirSync` or `fs.mkdtempSync`), `port: 0` always, `normalizeBunSnapshot` for snapshot output, `Promise.all` to drain stdout/stderr/exited concurrently, and stdout assertions before the exit-code assertion.
- Never use `setTimeout`/`sleep` to wait for a condition in a test — poll with a deadline or await the event; reviewers treat a bare sleep of 50ms or more outside a bounded poll loop as requiring a comment naming why no observable signal exists.
- The single most-repeated trap in this repo's instructions: the JSC conservative stack scanner is never the cause of a bug you are debugging; a surviving object is held by a real reference, and blaming the scanner requires a heap snapshot plus a debugger session as PR artifacts.
- Arena allocation is a known memory trap: values in a `bumpalo`/`Arena` do not run `Drop` on reset, so anything owning a heap allocation or refcount must be freed explicitly first, mirroring the original Zig `deinit()` ordering.
- Verify exception-check correctness with `BUN_JSC_validateExceptionChecks=1 BUN_JSC_dumpSimulatedThrows=1 bun bd <command>` rather than adding suppression-list entries, and silence debug logging with `BUN_DEBUG_QUIET_LOGS=1` or enable one scope with `BUN_DEBUG_<SCOPE>=1`.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 7870 | 54% |
| TypeScript | 3488 | 24% |
| Markdown | 2307 | docs/config |
| Rust | 1536 | 10% |
| C | 900 | 6% |
| JSON | 868 | docs/config |
| C++ | 607 | 4% |
| TOML | 144 | 1% |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
src/ (1353 files) packages/ (312 files)
Repository structure
| Top-level path | Files |
|---|---|
src/ | 1353 |
test/ | 1352 |
bench/ | 374 |
docs/ | 358 |
packages/ | 312 |
scripts/ | 80 |
.github/ | 45 |
patches/ | 30 |
Docs found: AGENTS.mdCLAUDE.mdCONTRIBUTING.mdLICENSE.mdREADME.mddocs/README.md
Package
Cargo.toml — v0.0.0, 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/auto-assign-types.yml— GitHub Actions workflow.github/workflows/auto-close-duplicates.yml— GitHub Actions workflow.github/workflows/auto-label-claude-prs.yml— GitHub Actions workflow.github/workflows/bun-types.yml— GitHub Actions workflow.github/workflows/cancel-buildkite-on-pr-close.yml— GitHub Actions workflow.github/workflows/claude-dedupe-issues.yml— GitHub Actions workflow.github/workflows/claude-find-issues-for-pr.yml— GitHub Actions workflow
Structural health signals
Average score 5.3/10 across 96 analyzed source files; 89 hotspots surfaced.
src/http_jsc/websocket_client/WebSocketUpgradeClient.rs— 1174 lines, score 4 (Large file, Deep nesting)src/bundler/bundle_v2.rs— 1035 lines, score 4.7 (Large file, Deep nesting)src/jsc/VirtualMachine.rs— 1027 lines, score 4.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
bf80d21c69c32026-09-22 — node:tls: keep unread data and still emit 'close' when a wrapped transport closes (#42176)1016a7afb04a2026-09-21 — secrets: add a persist option for Windows Credential Manager entries (#42161)f5bb689293fa2026-09-21 — streams: deliver a direct stream's chunk to read() before reader.closed settles (#43752)46461c337ab32026-09-21 — node:net: return false from write() when the send fails at once (#43755)fd3cace51ddc2026-09-21 — napi: run external buffer finalizers on the JS thread when a collection ends on the collector thread (#43757)