RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Diff/oven-sh-bun-test-claude ↔ oven-sh-bun-src-claude

Comparison

A · CLAUDE.md · oven-sh/bunB · CLAUDE.md · oven-sh/bun
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections012170%
Commands0530%
Section tags22425%

What each file covers

Sections

0 shared · 12 only in A · 17 only in B
  • − Testing style
  • − Spawning processes
  • − No timeouts
  • − Use port 0 to get a random port
  • − Creating temporary files
  • − Strings
  • − Test Organization
  • − Nested/complex object equality
  • − Common Imports from `harness`
  • − Error Testing
  • − Avoid dynamic import & require
  • − Test Utilities
  • + Rust
  • + Prefer `bun_core` / `bun_sys` over `std`
  • + `bun_sys` — System Calls (`src/sys/`)
  • + Strings (`bun_core::String` and `bun_core::strings`)
  • + Paths (`bun_paths`)
  • + URL Parsing (`bun_jsc::URL`)
  • + MIME Types (`bun_http_types::MimeType`)
  • + Memory & Allocators
  • + Environment Variables (`bun_core::env_var`)
  • + Logging (`bun_core::output`)
  • + Spawning Subprocesses
  • + JSC Interop & FFI Safety
  • + Pointer provenance at FFI boundaries
  • + `Strong` / `Weak` JS handles
  • + Refcount transfer on `to_js()` / `create()`
  • + Cross-thread string hazards
  • + Common Patterns

Commands

0 shared · 5 only in A · 3 only in B
  • − bun bd test <...test file>
  • − bun bd <...cmd>
  • − bun:test
  • − bun bd <file>
  • − bun bd test <file>
  • + cargo build
  • + cargo check -p <crate>
  • + bun bd

Section tags

2 shared · 2 only in A · 4 only in B
  • − test
  • − testing-strategy
  • + setup
  • + build
  • + types
  • + performance
  •   code-style
  •   do-not

Line diff

+264 added−171 removed77 unchanged22.6% identical
oven-sh/bun · test/CLAUDE.md
@@ −1 @@
1To run tests:
2 
3```sh
4bun bd test <...test file>
5```
6 
7To run a command with your debug build of Bun:
 
 
 
 
 
 
8 
9```sh
10bun bd <...cmd>
11```
12 
13Note that compiling Bun may take up to 2.5 minutes. It is slow!
 
 
 
 
 
 
14 
15**CRITICAL**: Do not use `bun test` to run tests. It will not have your changes. `bun bd test <...test file>` is the correct command, which compiles your code automatically.
16 
17## Testing style
 
18 
19Use `bun:test` with files that end in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. If it's a test/js/node/test/{parallel,sequential}/\*.js without a .test.extension, use `bun bd <file>` instead of `bun bd test <file>` since those expect exit code 0 and don't use bun's test runner.
 
 
 
 
 
 
 
 
 
 
 
20 
21- **Do not write flaky tests**. Unless explicitly asked, **never wait for time to pass in tests**. Always wait for the condition to be met instead of waiting for an arbitrary amount of time. **Never use hardcoded port numbers**. Always use `port: 0` to get a random port.
22- **Prefer concurrent tests over sequential tests**: When multiple tests in the same file spawn processes or write files, make them concurrent with `test.concurrent` or `describe.concurrent` unless it's very difficult to make them concurrent.
23 
24### Spawning processes
25 
26#### Spawning Bun in tests
 
27 
28When spawning Bun processes, use `bunExe` and `bunEnv` from `harness`. This ensures the same build of Bun is used to run the test and ensures debug logging is silenced.
 
 
 
 
29 
30##### Use `-e` for single-file tests
31 
32```ts
33import { bunEnv, bunExe, tempDir } from "harness";
34import { test, expect } from "bun:test";
 
 
35 
36test("single-file test spawns a Bun process", async () => {
37 await using proc = Bun.spawn({
38 cmd: [bunExe(), "-e", "console.log('Hello, world!')"],
39 env: bunEnv,
40 });
41 
42 const [stdout, stderr, exitCode] = await Promise.all([
43 proc.stdout.text(),
44 proc.stderr.text(),
45 proc.exited,
46 ]);
 
 
 
47 
48 expect(stderr).toBe("");
49 expect(stdout).toBe("Hello, world!\n");
50 expect(exitCode).toBe(0);
51});
 
 
 
 
 
 
 
 
 
 
 
 
 
52```
53 
54##### When multi-file tests are required:
55 
56```ts
57import { bunEnv, bunExe, tempDir } from "harness";
58import { test, expect } from "bun:test";
 
 
 
59 
60test("multi-file test spawns a Bun process", async () => {
61 // If a test MUST use multiple files:
62 using dir = tempDir("my-test-prefix", {
63 "my.fixture.ts": `
64 import { foo } from "./foo.ts";
65 foo();
66 `,
67 "foo.ts": `
68 export function foo() {
69 console.log("Hello, world!");
70 }
71 `,
72 });
73 
74 await using proc = Bun.spawn({
75 cmd: [bunExe(), "my.fixture.ts"],
76 env: bunEnv,
77 cwd: String(dir),
78 });
79 
80 const [stdout, stderr, exitCode] = await Promise.all([
 
 
 
 
 
 
 
 
 
81 
82 // ReadableStream in Bun supports:
83 // - `await stream.text()`
84 // - `await stream.json()`
85 // - `await stream.bytes()`
86 // - `await stream.blob()`
87 proc.stdout.text(),
88 proc.stderr.text(),
89 
90 proc.exitCode,
91 ]);
 
92 
93 expect(stdout).toBe("Hello, world!");
94 expect(stderr).toBe("");
95 expect(exitCode).toBe(0);
 
 
 
 
 
 
96```
97 
98When a test file spawns a Bun process, we like for that file to end in `*-fixture.ts`. This is a convention that helps us identify the file as a test fixture and not a test itself.
 
99 
100Generally, `await using` or `using` is a good idea to ensure proper resource cleanup. This works in most Bun APIs like Bun.listen, Bun.connect, Bun.spawn, Bun.serve, etc.
 
101 
102#### Async/await in tests
 
 
103 
104Prefer async/await over callbacks.
 
105 
106When callbacks must be used and it's just a single callback, use `Promise.withResolvers` to create a promise that can be resolved or rejected from a callback.
107 
108```ts
109const ws = new WebSocket("ws://localhost:8080");
110const { promise, resolve, reject } = Promise.withResolvers<void>(); // Can specify any type here for resolution value
111ws.onopen = resolve;
112ws.onclose = reject;
113await promise;
 
 
 
 
 
 
 
114```
115 
116If it's several callbacks, it's okay to use callbacks. We aren't a stickler for this.
 
 
 
 
117 
118### No timeouts
119 
120**CRITICAL**: Do not set a timeout on tests. Bun already has timeouts.
 
121 
122### Use port 0 to get a random port
 
123 
124Most APIs in Bun support `port: 0` to get a random port. Never hardcode ports. Avoid using your own random port number function.
 
 
125 
126### Creating temporary files
127 
128Use `tempDirWithFiles` to create a temporary directory with files.
129 
130```ts
131import { tempDir } from "harness";
132import path from "node:path";
 
 
133 
134test("creates a temporary directory with files", () => {
135 using dir = tempDir("my-test-prefix", {
136 "file.txt": "Hello, world!",
137 });
138 
139 expect(await Bun.file(path.join(String(dir), "file.txt")).text()).toBe(
140 "Hello, world!",
141 );
142});
143```
144 
145### Strings
146 
147To create a repetitive string, use `Buffer.alloc(count, fill).toString()` instead of `"A".repeat(count)`. "".repeat is very slow in debug JavaScriptCore builds.
 
 
 
 
 
148 
149### Test Organization
 
 
 
 
150 
151- Use `describe` blocks for grouping related tests
152- **Add tests to the existing test file for the code you're changing** — do not create a new file. Tests are organized by module (e.g., `/test/js/bun/`, `/test/js/node/`, `/test/js/web/`).
153- `/test/regression/issue/${issueNumber}.test.ts` is **only** for bugs that have a GitHub issue number **and** are true regressions (worked in a previous release, then broke). An issue number alone does not qualify — if it was never correct, put the test in the module's existing test file instead.
154- Integration tests are in `/test/integration/`
155 
156### Nested/complex object equality
 
157 
158Prefer usage of `.toEqual` rather than many `.toBe` assertions for nested or complex objects.
 
159 
160<example>
 
 
 
161 
162BAD (try to avoid doing this):
163 
164```ts
165expect(result).toHaveLength(3);
166expect(result[0].optional).toBe(null);
167expect(result[1].optional).toBe("middle-value"); // CRITICAL: middle item's value must be preserved
168expect(result[2].optional).toBe(null);
 
 
 
169```
170 
171**GOOD (always prefer this):**
172 
173```ts
174expect(result).toEqual([
175 { optional: null },
176 { optional: "middle-value" }, // CRITICAL: middle item's value must be preserved
177 { optional: null },
178]);
179```
180 
181</example>
182 
183### Common Imports from `harness`
184 
185```ts
186import {
187 bunExe, // Path to Bun executable
188 bunEnv, // Environment variables for Bun
189 tempDirWithFiles, // Create temporary test directories with files
190 tmpdirSync, // Create empty temporary directory
191 isMacOS, // Platform checks
192 isWindows,
193 isPosix,
194 gcTick, // Trigger garbage collection
195 withoutAggressiveGC, // Disable aggressive GC for performance tests
196} from "harness";
197```
198 
199### Error Testing
 
 
 
200 
201Always check exit codes and test error scenarios:
202 
203```ts
204test("handles errors", async () => {
205 await using proc = Bun.spawn({
206 cmd: [bunExe(), "run", "invalid.js"],
207 env: bunEnv,
208 });
209 
210 const exitCode = await proc.exited;
211 expect(exitCode).not.toBe(0);
212 
213 // For synchronous errors
214 expect(() => someFunction()).toThrow("Expected error message");
215});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216```
217 
218### Avoid dynamic import & require
 
 
219 
220**Only** use dynamic import or require when the test is specifically testing something relataed to dynamic import or require. Otherwise, **always use module-scope import statements**.
221 
222**BAD, do not do this**:
 
 
 
 
223 
224```ts
225test("foo", async () => {
226 // BAD: Unnecessary usage of dynamic import.
227 const { readFile } = await import("node:fs");
228 
229 expect(await readFile("ok.txt")).toBe("");
230});
231```
 
 
 
 
232 
233**GOOD, do this:**
234 
235```ts
236import { readFile } from "node:fs";
237test("foo", async () => {
238 expect(await readFile("ok.txt")).toBe("");
239});
240```
 
 
241 
242### Test Utilities
 
 
 
 
243 
244- Use `describe.each()` for parameterized tests
245- Use `toMatchSnapshot()` for snapshot testing
246- Use `beforeAll()`, `afterEach()`, `beforeEach()` for setup/teardown
247- Track resources (servers, clients) in arrays for cleanup in `afterEach()`
248 
oven-sh/bun · src/CLAUDE.md
@@ +1 @@
1## Rust
2 
3`src/` is a Cargo workspace (rooted at the repo's top-level `Cargo.toml`, ~200
4member crates). The runtime is built as `libbun_rust.a` via `cargo build -p
5bun_bin` (driven by `scripts/build/rust.ts`). Key crates:
6 
7- `bun_core` (`src/bun_core/`) — strings, formatting, logging, env vars, allocator/heap helpers, the foundation everything else uses
8- `bun_sys` (`src/sys/`) — cross-platform syscall wrappers (`File`, `Fd`, `Dir`, `Error`)
9- `bun_paths` (`src/paths/`) — path joining/normalization, the path-buffer pool
10- `bun_jsc` (`src/jsc/`) — JSC value types, `Strong`/`Weak`, FFI imports, `URL`
11- `bun_runtime` (`src/runtime/`) — JS-visible APIs (server, fetch, node compat, crypto)
12- `bun_js_parser`, `bun_js_printer`, `bun_resolver`, `bun_bundler`, `bun_install`, `bun_collections`, `bun_threading`, `bun_alloc` — the rest of the pipeline
13- `bun_bin` (`src/bun_bin/`) — the staticlib root that `cargo build` links
14 
15Conventions:
 
 
16 
17- `cargo check -p <crate>` for fast iteration; `bun bd` builds and links everything.
18- Don't `.unwrap()` a fallible path that user input or the OS can hit at runtime — return the error. `.unwrap()` is for invariants you can prove.
19- The C ABI / syscall boundary uses `bun_sys::Maybe<T>` (= `Result<T, bun_sys::Error>`); ordinary Rust code uses `Result<T, E>` with `?`.
20- Each crate defines its own `Error` enum (a `thiserror::Error` at `<crate>/error.rs`, re-exported as `crate::Error` + `crate::Result`). Errno codes nest via `Sys(#[from] bun_errno::SystemErrno)`; OOM via `Alloc(#[from] bun_alloc::AllocError)`. `bun_sys::Error` is the rich syscall error (errno + syscall tag + path); `From<bun_sys::Error> for bun_errno::SystemErrno` exists for `?`-chaining.
21- NEVER add comments to deleted code blocks.
22- Do not add comments that reference context from the transcript.
23- Avoid adding comments where not necessary.
24 
25## Prefer `bun_core` / `bun_sys` over `std`
26 
27The `std` equivalents either lose OS error info, allocate where we have pools,
28or don't match the cross-platform behavior the runtime needs.
29 
30| Instead of | Use |
31| --------------------------------------- | ------------------------------------------------------------------------------------ |
32| `std::fs::File` | `bun_sys::File` (owns the fd; closes on `Drop`) |
33| `std::fs::read` / `write` | `bun_sys::File::read_from` / `File::create` + `write_all` |
34| `std::path::Path::join` | `bun_paths::resolve_path::join` / `join_string_buf` |
35| `std::path::Path::parent`/`file_name` | `bun_paths::dirname` / `bun_paths::basename` |
36| `std::env::var` | `bun_core::env_var::*::get()` (typed + cached) |
37| `String::from_utf8` for JS-visible strs | `bun_core::String::clone_utf8` / `borrow_utf8` |
38| `&str` operations on byte slices | `bun_core::strings::*` (SIMD-backed `&[u8]` ops) |
39| `eprintln!` for debug logging | `bun_core::declare_scope!` + `scoped_log!` |
40| `std::process::Command` | `bun_core::util::spawn_sync_inherit` (CLI helpers) or `bun_spawn_sys` (full control) |
41| `Box::new` + raw ptr round-trip | `bun_core::heap::{into_raw, take, destroy}` |
42 
43## `bun_sys` — System Calls (`src/sys/`)
 
44 
45Syscall wrappers preserve errno via `Maybe<T> = Result<T, bun_sys::Error>`.
46 
47```rust
48use bun_sys::{File, Fd, O};
49 
50let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
51let mut buf = vec![0u8; 4096];
52let n = file.read_all(&mut buf)?; // loops until EOF or full
53// `file` closes on Drop.
54```
55 
56Key types and functions:
57 
58- `Fd` (`bun_core::Fd`, re-exported) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
59- `File::open(path: &ZStr, flags, mode)` / `File::openat(dir: Fd, path: &[u8], flags, mode)` / `File::make_open(...)` (creates parent dirs) / `File::create(dir, path, truncate)`
60- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
61- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
62- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
63 
64`bun_sys::Error` carries `errno`, `syscall: Tag`, `path: Box<[u8]>`. Convert
65to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
 
 
 
66 
67```rust
68use bun_sys_jsc::ErrorJsc;
69match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
70 Ok(f) => f,
71 Err(err) => return Ok(err.to_js(global)?),
72}
73// Internally: err.to_system_error().to_error_instance(global)
74```
75 
76## Strings (`bun_core::String` and `bun_core::strings`)
77 
78`bun_core::String` is the FFI-compatible 5-variant tagged union shared with C++
79(`BunString` in `BunString.cpp`). It bridges Rust and JSC and can hold a
80`WTFStringImpl` (Latin-1 or UTF-16). **Latin-1 is NOT UTF-8** — bytes 128–255
81are single chars in Latin-1 but invalid UTF-8 — so converting either direction
82requires a real encoder, not a cast.
83 
84```rust
85use bun_core::String;
86 
87let s = String::clone_utf8(utf8_bytes); // copies into a WTFStringImpl
88let s = String::borrow_utf8(utf8_bytes); // no copy; caller keeps slice alive
89let s = String::static_(b"literal"); // 'static slice, never freed
90 
91let utf8: ZigStringSlice = s.to_utf8(); // ref-holding view; falls back to allocating a copy
92let owned: Vec<u8> = s.to_utf8_bytes();
93```
94 
95To/from JS values, use the `bun_jsc::StringJsc` extension trait:
96 
97```rust
98use bun_jsc::StringJsc;
99let js: JSValue = s.to_js(global)?;
100let s = bun_core::String::from_js(value, global)?;
101let err = s.to_error_instance(global);
102```
103 
104`bun_core::strings` is the SIMD-backed `&[u8]` toolkit. Use it instead of
105`std::str` / `std::iter` for searching and comparing byte slices:
 
 
 
 
 
 
 
 
 
 
 
106 
107```rust
108use bun_core::strings;
 
 
 
109 
110strings::index_of(haystack, needle) // Option<usize>
111strings::contains(haystack, needle) // bool
112strings::eql(a, b) // bool
113strings::starts_with(s, prefix) // bool
114strings::ends_with(s, suffix) // bool
115strings::has_prefix_comptime(s, b"x") // 'static comparand
116strings::has_suffix_comptime(s, b"x")
117strings::first_non_ascii(s) // Option<u32>
118strings::to_utf16_alloc(...) // encoding conversions
119```
120 
121## Paths (`bun_paths`)
 
 
 
 
 
 
122 
123Path helpers operate on `&[u8]` and are platform-parameterized via the
124`Platform` const-generic (`Posix`, `Windows`, `Loose`, `Nt`; `platform::Auto`
125picks the host). Never use `std::path` for runtime path logic.
126 
127```rust
128use bun_paths::{dirname, basename};
129use bun_paths::resolve_path::{self, platform};
130 
131let dir = dirname(path); // Option<&[u8]>
132let name = basename(path); // &[u8]
133let joined = resolve_path::join::<platform::Auto>(&[a, b]); // &'static [u8] (threadlocal buf)
134let joined = resolve_path::join_string_buf::<platform::Auto>(&mut buf, &[a, b]); // caller buf
135let rel = resolve_path::relative(from, to);
136```
137 
138Use the path-buffer pool to avoid 64 KB stack allocations on Windows
139(`PathBuffer` is `[u8; PATH_MAX_BYTES]`, ~64 KB on Windows):
140 
141```rust
142use bun_paths::path_buffer_pool;
143 
144let mut buf = path_buffer_pool::get(); // PoolGuard<PathBuffer>, returns to pool on Drop
145let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[a, b]);
146```
147 
148`bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows
149and the narrow (`u8`) variant on POSIX.
150 
151## URL Parsing (`bun_jsc::URL`)
152 
153WHATWG-compliant, backed by WebKit's URL parser. Returns `None` for invalid input.
154 
155```rust
156use bun_jsc::URL;
157 
158let url = URL::from_utf8(href)?; // Option<NonNull<URL>>
159// caller owns the C++ object — destroy it when done:
160// unsafe { URL::destroy(url.as_ptr()) }
161 
162url.protocol() // bun_core::String
163url.pathname() // bun_core::String
164url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!)
165url.port() // u32 (u32::MAX = unset; otherwise u16 range)
166```
167 
168`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url`
169do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes
170`hostname()`, which returns the host WITH the port (also the opposite of JS
171`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname`
172are effectively swapped relative to their JS namesakes.
173 
174## MIME Types (`bun_http_types::MimeType`)
175 
176```rust
177use bun_http_types::{MimeType, mime_type};
178 
179let mime = mime_type::by_extension(b"html"); // MimeType
180let mime = mime_type::by_extension_no_default(b"xyz"); // Option<MimeType>
181 
182mime.category // Category::Javascript | Css | Html | Json | Image | Text | Wasm | ...
183mime.category.is_text_like()
184```
185 
186Common constants: `JAVASCRIPT`, `JSON`, `HTML`, `CSS`, `TEXT`, `WASM`, `ICO`, `OTHER`.
187 
188## Memory & Allocators
189 
190The `#[global_allocator]` is mimalloc (or `std::alloc::System` under
191`cfg(bun_asan)`), so plain `Box`/`Vec`/`String` already use it. When pairing
192with C/C++ that may free the bytes, route through `bun_alloc::default_alloc`
193rather than `mi_*` directly — under ASAN the global allocator is libc's, so a
194`mi_free`/`mi_usable_size` on `Box`-owned memory is an allocator mismatch.
195 
196OOM handling: do not let a runtime OOM unwind into FFI. Use
197`bun_core::handle_oom` (or the `.unwrap_or_oom()` extension) to convert
198`Result<T, AllocError>` into a controlled crash:
 
199 
200```rust
201use bun_core::{handle_oom, UnwrapOrOom};
202let buf = handle_oom(allocator.alloc(size));
203let v = vec.try_reserve(n).unwrap_or_oom();
204```
205 
206Heap round-trips that need to cross FFI use `bun_core::heap`:
207 
208```rust
209use bun_core::heap;
210let raw: *mut T = heap::into_raw(Box::new(value)); // hand ownership to C
211let boxed: Box<T> = unsafe { heap::take(raw) }; // reclaim ownership
212unsafe { heap::destroy(raw) }; // reclaim + drop in one step
213```
214 
215**Arena gotcha:** values allocated in `bun_alloc::MimallocArena` (the AST
216allocator and similar) do **not** run `Drop` when the arena resets — the
217backing pages are bulk-freed. If a type owns a heap allocation, refcount, or
218fd, free it explicitly before the arena resets. Don't rely on `Drop` for
219correctness in arena-backed code.
220 
221## Environment Variables (`bun_core::env_var`)
 
 
 
222 
223Typed, cached accessors. Each known env var is a module with a `get()`
224returning the right type (`Option<...>` if no default).
225 
226```rust
227use bun_core::env_var;
228 
229env_var::HOME::get() // Option<&[u8]>
230env_var::CI::get() // bool (has default)
231env_var::BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS::get() // u64 (has default)
232```
233 
234## Logging (`bun_core::output`)
235 
236Scoped debug logging. Declare a scope once per module; gate with
237`BUN_DEBUG_<SCOPE>=1` at runtime; the body dead-strips in release builds.
238 
239```rust
240bun_core::declare_scope!(my_feature, hidden); // hidden: opt-in via BUN_DEBUG_my_feature=1
241// or `visible` to log by default in debug builds
242 
243bun_core::scoped_log!(my_feature, "processing {} items", count);
244```
245 
246User-facing colored output (auto-detects TTY, strips ANSI when piped):
247 
248```rust
249bun_core::pretty!("<green>success<r>: {}\n", msg);
250bun_core::prettyln!("done");
251bun_core::pretty_errorln!("<red>error<r>: {}", msg);
 
 
252```
253 
254## Spawning Subprocesses
255 
256For simple inherit-stdio CLI helpers:
257 
258```rust
259use bun_core::util::spawn_sync_inherit;
260let status = spawn_sync_inherit(&[b"git", b"status"])?;
 
 
 
 
 
 
 
 
 
261```
262 
263For full control (pipes, custom env, posix_spawn flags) use `bun_spawn_sys`
264(`src/spawn_sys/`). The runtime `Bun.spawn` implementation lives in
265`src/runtime/api/bun/{spawn.rs, process.rs, subprocess.rs}` — look there for
266the JS-facing path.
267 
268## JSC Interop & FFI Safety
269 
270These are the patterns that trip people up. Get them wrong and you get
271crashes that only reproduce under load or in CI.
 
 
 
 
272 
273### Pointer provenance at FFI boundaries
 
274 
275If a callback may free `self` (close, error, GC finalize), do **not**
276materialize `&self`/`&mut self` at the boundary — a `&self`-derived raw
277pointer carries `SharedReadOnly` provenance, and `Box::from_raw`/dealloc
278through it is UB. Pass and dispatch off `*mut Self` until the body proves
279ownership. `src/io/PipeWriter.rs`'s `impl_streaming_writer_parent!` macro
280encodes the three modes:
281 
282- `borrow = mut` — body forms `&mut *this`; safe when nothing re-enters
283- `borrow = shared` — body forms `&*this`; safe when re-entrant code only needs `&Self`
284- `borrow = ptr` — body calls `Self::method(this, ..)` with `this: *mut Self`; required when the callback may free `self`
285 
286### `Strong` / `Weak` JS handles
287 
288`bun_jsc::Strong` keeps a JS value alive; it is `!Send`/`!Sync` and must be
289created and dropped on the JS thread.
290 
291```rust
292use bun_jsc::Strong;
293let strong = Strong::create(value, global);
294let v: JSValue = strong.get();
295// drop(strong) releases the GC handle
296```
297 
298`bun_jsc::Weak<T>` is the GC-cleared variant. For raw values without a `Strong`
299wrapper, `JSValue::protect()` / `unprotect()` and `ensure_still_alive()` are
300available, but `Strong` is preferred — it can't be forgotten or unbalanced.
301 
302### Refcount transfer on `to_js()` / `create()`
303 
304A `to_js()` / `create()` that returns a wrapped pointer **transfers** the
305caller's `+1` to the JS wrapper. Do not `ref()` again before the return; the
306finalizer derefs once. The leak-or-UAF symptoms of getting this wrong are
307distinctive: an extra `ref()` leaks until process exit; a missing `ref()` on a
308non-transferring path UAFs at GC.
309 
310### Cross-thread string hazards
 
 
 
311 
312`AtomString`s live in a per-thread table. Never deref one from another thread —
313it trips `wasRemoved` in `AtomStringImpl::remove()`. If a `bun_core::String`
314may be dropped from a non-JS thread (HTTP worker, threadpool, dying VM), build
315it via `String::clone_utf8` (a plain `WTFStringImpl` with an atomic refcount),
316not from an interned/atomized JS string. See the comment in
317`src/runtime/webcore/fetch/FetchTasklet.rs` near `Response::init` for the
318canonical example of this bug class and its fix.
319 
320## Common Patterns
321 
322```rust
323// Read a file, return JS error on failure
324let contents = match bun_sys::File::openat(Fd::cwd(), path, O::RDONLY, 0)
325 .and_then(|f| f.read_to_end())
326{
327 Ok(bytes) => bytes,
328 Err(err) => return Ok(err.to_js(global)?),
329};
330 
331// Heap-allocated FFI handle with explicit lifecycle
332let raw = bun_core::heap::into_raw(Box::new(MyHandle::new()));
333register_with_c(raw);
334// ... later, in the matching teardown callback:
335unsafe { bun_core::heap::destroy(raw) };
336 
337// Hashing
338bun_wyhash::hash(bytes) // u64
339bun_wyhash::hash_with_seed(seed, bytes)
340```
341 
@@ −1 +1 @@
1−To run tests:
1+## Rust
22  
3−```sh
4−bun bd test <...test file>
5−```
3+`src/` is a Cargo workspace (rooted at the repo's top-level `Cargo.toml`, ~200
4+member crates). The runtime is built as `libbun_rust.a` via `cargo build -p
5+bun_bin` (driven by `scripts/build/rust.ts`). Key crates:
66  
7−To run a command with your debug build of Bun:
7+- `bun_core` (`src/bun_core/`) — strings, formatting, logging, env vars, allocator/heap helpers, the foundation everything else uses
8+- `bun_sys` (`src/sys/`) — cross-platform syscall wrappers (`File`, `Fd`, `Dir`, `Error`)
9+- `bun_paths` (`src/paths/`) — path joining/normalization, the path-buffer pool
10+- `bun_jsc` (`src/jsc/`) — JSC value types, `Strong`/`Weak`, FFI imports, `URL`
11+- `bun_runtime` (`src/runtime/`) — JS-visible APIs (server, fetch, node compat, crypto)
12+- `bun_js_parser`, `bun_js_printer`, `bun_resolver`, `bun_bundler`, `bun_install`, `bun_collections`, `bun_threading`, `bun_alloc` — the rest of the pipeline
13+- `bun_bin` (`src/bun_bin/`) — the staticlib root that `cargo build` links
814  
9−```sh
10−bun bd <...cmd>
11−```
15+Conventions:
1216  
13−Note that compiling Bun may take up to 2.5 minutes. It is slow!
17+- `cargo check -p <crate>` for fast iteration; `bun bd` builds and links everything.
18+- Don't `.unwrap()` a fallible path that user input or the OS can hit at runtime — return the error. `.unwrap()` is for invariants you can prove.
19+- The C ABI / syscall boundary uses `bun_sys::Maybe<T>` (= `Result<T, bun_sys::Error>`); ordinary Rust code uses `Result<T, E>` with `?`.
20+- Each crate defines its own `Error` enum (a `thiserror::Error` at `<crate>/error.rs`, re-exported as `crate::Error` + `crate::Result`). Errno codes nest via `Sys(#[from] bun_errno::SystemErrno)`; OOM via `Alloc(#[from] bun_alloc::AllocError)`. `bun_sys::Error` is the rich syscall error (errno + syscall tag + path); `From<bun_sys::Error> for bun_errno::SystemErrno` exists for `?`-chaining.
21+- NEVER add comments to deleted code blocks.
22+- Do not add comments that reference context from the transcript.
23+- Avoid adding comments where not necessary.
1424  
15−**CRITICAL**: Do not use `bun test` to run tests. It will not have your changes. `bun bd test <...test file>` is the correct command, which compiles your code automatically.
25+## Prefer `bun_core` / `bun_sys` over `std`
1626  
17−## Testing style
27+The `std` equivalents either lose OS error info, allocate where we have pools,
28+or don't match the cross-platform behavior the runtime needs.
1829  
19−Use `bun:test` with files that end in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. If it's a test/js/node/test/{parallel,sequential}/\*.js without a .test.extension, use `bun bd <file>` instead of `bun bd test <file>` since those expect exit code 0 and don't use bun's test runner.
30+| Instead of | Use |
31+| --------------------------------------- | ------------------------------------------------------------------------------------ |
32+| `std::fs::File` | `bun_sys::File` (owns the fd; closes on `Drop`) |
33+| `std::fs::read` / `write` | `bun_sys::File::read_from` / `File::create` + `write_all` |
34+| `std::path::Path::join` | `bun_paths::resolve_path::join` / `join_string_buf` |
35+| `std::path::Path::parent`/`file_name` | `bun_paths::dirname` / `bun_paths::basename` |
36+| `std::env::var` | `bun_core::env_var::*::get()` (typed + cached) |
37+| `String::from_utf8` for JS-visible strs | `bun_core::String::clone_utf8` / `borrow_utf8` |
38+| `&str` operations on byte slices | `bun_core::strings::*` (SIMD-backed `&[u8]` ops) |
39+| `eprintln!` for debug logging | `bun_core::declare_scope!` + `scoped_log!` |
40+| `std::process::Command` | `bun_core::util::spawn_sync_inherit` (CLI helpers) or `bun_spawn_sys` (full control) |
41+| `Box::new` + raw ptr round-trip | `bun_core::heap::{into_raw, take, destroy}` |
2042  
21−- **Do not write flaky tests**. Unless explicitly asked, **never wait for time to pass in tests**. Always wait for the condition to be met instead of waiting for an arbitrary amount of time. **Never use hardcoded port numbers**. Always use `port: 0` to get a random port.
22−- **Prefer concurrent tests over sequential tests**: When multiple tests in the same file spawn processes or write files, make them concurrent with `test.concurrent` or `describe.concurrent` unless it's very difficult to make them concurrent.
43+## `bun_sys` — System Calls (`src/sys/`)
2344  
24−### Spawning processes
45+Syscall wrappers preserve errno via `Maybe<T> = Result<T, bun_sys::Error>`.
2546  
26−#### Spawning Bun in tests
47+```rust
48+use bun_sys::{File, Fd, O};
2749  
28−When spawning Bun processes, use `bunExe` and `bunEnv` from `harness`. This ensures the same build of Bun is used to run the test and ensures debug logging is silenced.
50+let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
51+let mut buf = vec![0u8; 4096];
52+let n = file.read_all(&mut buf)?; // loops until EOF or full
53+// `file` closes on Drop.
54+```
2955  
30−##### Use `-e` for single-file tests
56+Key types and functions:
3157  
32−```ts
33−import { bunEnv, bunExe, tempDir } from "harness";
34−import { test, expect } from "bun:test";
58+- `Fd` (`bun_core::Fd`, re-exported) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
59+- `File::open(path: &ZStr, flags, mode)` / `File::openat(dir: Fd, path: &[u8], flags, mode)` / `File::make_open(...)` (creates parent dirs) / `File::create(dir, path, truncate)`
60+- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
61+- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
62+- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
3563  
36−test("single-file test spawns a Bun process", async () => {
37− await using proc = Bun.spawn({
38− cmd: [bunExe(), "-e", "console.log('Hello, world!')"],
39− env: bunEnv,
40− });
64+`bun_sys::Error` carries `errno`, `syscall: Tag`, `path: Box<[u8]>`. Convert
65+to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
4166  
42− const [stdout, stderr, exitCode] = await Promise.all([
43− proc.stdout.text(),
44− proc.stderr.text(),
45− proc.exited,
46− ]);
67+```rust
68+use bun_sys_jsc::ErrorJsc;
69+match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
70+ Ok(f) => f,
71+ Err(err) => return Ok(err.to_js(global)?),
72+}
73+// Internally: err.to_system_error().to_error_instance(global)
74+```
4775  
48− expect(stderr).toBe("");
49− expect(stdout).toBe("Hello, world!\n");
50− expect(exitCode).toBe(0);
51−});
76+## Strings (`bun_core::String` and `bun_core::strings`)
77+ 
78+`bun_core::String` is the FFI-compatible 5-variant tagged union shared with C++
79+(`BunString` in `BunString.cpp`). It bridges Rust and JSC and can hold a
80+`WTFStringImpl` (Latin-1 or UTF-16). **Latin-1 is NOT UTF-8** — bytes 128–255
81+are single chars in Latin-1 but invalid UTF-8 — so converting either direction
82+requires a real encoder, not a cast.
83+ 
84+```rust
85+use bun_core::String;
86+ 
87+let s = String::clone_utf8(utf8_bytes); // copies into a WTFStringImpl
88+let s = String::borrow_utf8(utf8_bytes); // no copy; caller keeps slice alive
89+let s = String::static_(b"literal"); // 'static slice, never freed
90+ 
91+let utf8: ZigStringSlice = s.to_utf8(); // ref-holding view; falls back to allocating a copy
92+let owned: Vec<u8> = s.to_utf8_bytes();
5293 ```
5394  
54−##### When multi-file tests are required:
95+To/from JS values, use the `bun_jsc::StringJsc` extension trait:
5596  
56−```ts
57−import { bunEnv, bunExe, tempDir } from "harness";
58−import { test, expect } from "bun:test";
97+```rust
98+use bun_jsc::StringJsc;
99+let js: JSValue = s.to_js(global)?;
100+let s = bun_core::String::from_js(value, global)?;
101+let err = s.to_error_instance(global);
102+```
59103  
60−test("multi-file test spawns a Bun process", async () => {
61− // If a test MUST use multiple files:
62− using dir = tempDir("my-test-prefix", {
63− "my.fixture.ts": `
64− import { foo } from "./foo.ts";
65− foo();
66− `,
67− "foo.ts": `
68− export function foo() {
69− console.log("Hello, world!");
70− }
71− `,
72− });
104+`bun_core::strings` is the SIMD-backed `&[u8]` toolkit. Use it instead of
105+`std::str` / `std::iter` for searching and comparing byte slices:
73106  
74− await using proc = Bun.spawn({
75− cmd: [bunExe(), "my.fixture.ts"],
76− env: bunEnv,
77− cwd: String(dir),
78− });
107+```rust
108+use bun_core::strings;
79109  
80− const [stdout, stderr, exitCode] = await Promise.all([
110+strings::index_of(haystack, needle) // Option<usize>
111+strings::contains(haystack, needle) // bool
112+strings::eql(a, b) // bool
113+strings::starts_with(s, prefix) // bool
114+strings::ends_with(s, suffix) // bool
115+strings::has_prefix_comptime(s, b"x") // 'static comparand
116+strings::has_suffix_comptime(s, b"x")
117+strings::first_non_ascii(s) // Option<u32>
118+strings::to_utf16_alloc(...) // encoding conversions
119+```
81120  
82− // ReadableStream in Bun supports:
83− // - `await stream.text()`
84− // - `await stream.json()`
85− // - `await stream.bytes()`
86− // - `await stream.blob()`
87− proc.stdout.text(),
88− proc.stderr.text(),
121+## Paths (`bun_paths`)
89122  
90− proc.exitCode,
91− ]);
123+Path helpers operate on `&[u8]` and are platform-parameterized via the
124+`Platform` const-generic (`Posix`, `Windows`, `Loose`, `Nt`; `platform::Auto`
125+picks the host). Never use `std::path` for runtime path logic.
92126  
93− expect(stdout).toBe("Hello, world!");
94− expect(stderr).toBe("");
95− expect(exitCode).toBe(0);
127+```rust
128+use bun_paths::{dirname, basename};
129+use bun_paths::resolve_path::{self, platform};
130+ 
131+let dir = dirname(path); // Option<&[u8]>
132+let name = basename(path); // &[u8]
133+let joined = resolve_path::join::<platform::Auto>(&[a, b]); // &'static [u8] (threadlocal buf)
134+let joined = resolve_path::join_string_buf::<platform::Auto>(&mut buf, &[a, b]); // caller buf
135+let rel = resolve_path::relative(from, to);
96136 ```
97137  
98−When a test file spawns a Bun process, we like for that file to end in `*-fixture.ts`. This is a convention that helps us identify the file as a test fixture and not a test itself.
138+Use the path-buffer pool to avoid 64 KB stack allocations on Windows
139+(`PathBuffer` is `[u8; PATH_MAX_BYTES]`, ~64 KB on Windows):
99140  
100−Generally, `await using` or `using` is a good idea to ensure proper resource cleanup. This works in most Bun APIs like Bun.listen, Bun.connect, Bun.spawn, Bun.serve, etc.
141+```rust
142+use bun_paths::path_buffer_pool;
101143  
102−#### Async/await in tests
144+let mut buf = path_buffer_pool::get(); // PoolGuard<PathBuffer>, returns to pool on Drop
145+let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[a, b]);
146+```
103147  
104−Prefer async/await over callbacks.
148+`bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows
149+and the narrow (`u8`) variant on POSIX.
105150  
106−When callbacks must be used and it's just a single callback, use `Promise.withResolvers` to create a promise that can be resolved or rejected from a callback.
151+## URL Parsing (`bun_jsc::URL`)
107152  
108−```ts
109−const ws = new WebSocket("ws://localhost:8080");
110−const { promise, resolve, reject } = Promise.withResolvers<void>(); // Can specify any type here for resolution value
111−ws.onopen = resolve;
112−ws.onclose = reject;
113−await promise;
153+WHATWG-compliant, backed by WebKit's URL parser. Returns `None` for invalid input.
154+ 
155+```rust
156+use bun_jsc::URL;
157+ 
158+let url = URL::from_utf8(href)?; // Option<NonNull<URL>>
159+// caller owns the C++ object — destroy it when done:
160+// unsafe { URL::destroy(url.as_ptr()) }
161+ 
162+url.protocol() // bun_core::String
163+url.pathname() // bun_core::String
164+url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!)
165+url.port() // u32 (u32::MAX = unset; otherwise u16 range)
114166 ```
115167  
116−If it's several callbacks, it's okay to use callbacks. We aren't a stickler for this.
168+`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url`
169+do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes
170+`hostname()`, which returns the host WITH the port (also the opposite of JS
171+`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname`
172+are effectively swapped relative to their JS namesakes.
117173  
118−### No timeouts
174+## MIME Types (`bun_http_types::MimeType`)
119175  
120−**CRITICAL**: Do not set a timeout on tests. Bun already has timeouts.
176+```rust
177+use bun_http_types::{MimeType, mime_type};
121178  
122−### Use port 0 to get a random port
179+let mime = mime_type::by_extension(b"html"); // MimeType
180+let mime = mime_type::by_extension_no_default(b"xyz"); // Option<MimeType>
123181  
124−Most APIs in Bun support `port: 0` to get a random port. Never hardcode ports. Avoid using your own random port number function.
182+mime.category // Category::Javascript | Css | Html | Json | Image | Text | Wasm | ...
183+mime.category.is_text_like()
184+```
125185  
126−### Creating temporary files
186+Common constants: `JAVASCRIPT`, `JSON`, `HTML`, `CSS`, `TEXT`, `WASM`, `ICO`, `OTHER`.
127187  
128−Use `tempDirWithFiles` to create a temporary directory with files.
188+## Memory & Allocators
129189  
130−```ts
131−import { tempDir } from "harness";
132−import path from "node:path";
190+The `#[global_allocator]` is mimalloc (or `std::alloc::System` under
191+`cfg(bun_asan)`), so plain `Box`/`Vec`/`String` already use it. When pairing
192+with C/C++ that may free the bytes, route through `bun_alloc::default_alloc`
193+rather than `mi_*` directly — under ASAN the global allocator is libc's, so a
194+`mi_free`/`mi_usable_size` on `Box`-owned memory is an allocator mismatch.
133195  
134−test("creates a temporary directory with files", () => {
135− using dir = tempDir("my-test-prefix", {
136− "file.txt": "Hello, world!",
137− });
196+OOM handling: do not let a runtime OOM unwind into FFI. Use
197+`bun_core::handle_oom` (or the `.unwrap_or_oom()` extension) to convert
198+`Result<T, AllocError>` into a controlled crash:
138199  
139− expect(await Bun.file(path.join(String(dir), "file.txt")).text()).toBe(
140− "Hello, world!",
141− );
142−});
200+```rust
201+use bun_core::{handle_oom, UnwrapOrOom};
202+let buf = handle_oom(allocator.alloc(size));
203+let v = vec.try_reserve(n).unwrap_or_oom();
143204 ```
144205  
145−### Strings
206+Heap round-trips that need to cross FFI use `bun_core::heap`:
146207  
147−To create a repetitive string, use `Buffer.alloc(count, fill).toString()` instead of `"A".repeat(count)`. "".repeat is very slow in debug JavaScriptCore builds.
208+```rust
209+use bun_core::heap;
210+let raw: *mut T = heap::into_raw(Box::new(value)); // hand ownership to C
211+let boxed: Box<T> = unsafe { heap::take(raw) }; // reclaim ownership
212+unsafe { heap::destroy(raw) }; // reclaim + drop in one step
213+```
148214  
149−### Test Organization
215+**Arena gotcha:** values allocated in `bun_alloc::MimallocArena` (the AST
216+allocator and similar) do **not** run `Drop` when the arena resets — the
217+backing pages are bulk-freed. If a type owns a heap allocation, refcount, or
218+fd, free it explicitly before the arena resets. Don't rely on `Drop` for
219+correctness in arena-backed code.
150220  
151−- Use `describe` blocks for grouping related tests
152−- **Add tests to the existing test file for the code you're changing** — do not create a new file. Tests are organized by module (e.g., `/test/js/bun/`, `/test/js/node/`, `/test/js/web/`).
153−- `/test/regression/issue/${issueNumber}.test.ts` is **only** for bugs that have a GitHub issue number **and** are true regressions (worked in a previous release, then broke). An issue number alone does not qualify — if it was never correct, put the test in the module's existing test file instead.
154−- Integration tests are in `/test/integration/`
221+## Environment Variables (`bun_core::env_var`)
155222  
156−### Nested/complex object equality
223+Typed, cached accessors. Each known env var is a module with a `get()`
224+returning the right type (`Option<...>` if no default).
157225  
158−Prefer usage of `.toEqual` rather than many `.toBe` assertions for nested or complex objects.
226+```rust
227+use bun_core::env_var;
159228  
160−<example>
229+env_var::HOME::get() // Option<&[u8]>
230+env_var::CI::get() // bool (has default)
231+env_var::BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS::get() // u64 (has default)
232+```
161233  
162−BAD (try to avoid doing this):
234+## Logging (`bun_core::output`)
163235  
164−```ts
165−expect(result).toHaveLength(3);
166−expect(result[0].optional).toBe(null);
167−expect(result[1].optional).toBe("middle-value"); // CRITICAL: middle item's value must be preserved
168−expect(result[2].optional).toBe(null);
236+Scoped debug logging. Declare a scope once per module; gate with
237+`BUN_DEBUG_<SCOPE>=1` at runtime; the body dead-strips in release builds.
238+ 
239+```rust
240+bun_core::declare_scope!(my_feature, hidden); // hidden: opt-in via BUN_DEBUG_my_feature=1
241+// or `visible` to log by default in debug builds
242+ 
243+bun_core::scoped_log!(my_feature, "processing {} items", count);
169244 ```
170245  
171−**GOOD (always prefer this):**
246+User-facing colored output (auto-detects TTY, strips ANSI when piped):
172247  
173−```ts
174−expect(result).toEqual([
175− { optional: null },
176− { optional: "middle-value" }, // CRITICAL: middle item's value must be preserved
177− { optional: null },
178−]);
248+```rust
249+bun_core::pretty!("<green>success<r>: {}\n", msg);
250+bun_core::prettyln!("done");
251+bun_core::pretty_errorln!("<red>error<r>: {}", msg);
179252 ```
180253  
181−</example>
254+## Spawning Subprocesses
182255  
183−### Common Imports from `harness`
256+For simple inherit-stdio CLI helpers:
184257  
185−```ts
186−import {
187− bunExe, // Path to Bun executable
188− bunEnv, // Environment variables for Bun
189− tempDirWithFiles, // Create temporary test directories with files
190− tmpdirSync, // Create empty temporary directory
191− isMacOS, // Platform checks
192− isWindows,
193− isPosix,
194− gcTick, // Trigger garbage collection
195− withoutAggressiveGC, // Disable aggressive GC for performance tests
196−} from "harness";
258+```rust
259+use bun_core::util::spawn_sync_inherit;
260+let status = spawn_sync_inherit(&[b"git", b"status"])?;
197261 ```
198262  
199−### Error Testing
263+For full control (pipes, custom env, posix_spawn flags) use `bun_spawn_sys`
264+(`src/spawn_sys/`). The runtime `Bun.spawn` implementation lives in
265+`src/runtime/api/bun/{spawn.rs, process.rs, subprocess.rs}` — look there for
266+the JS-facing path.
200267  
201−Always check exit codes and test error scenarios:
268+## JSC Interop & FFI Safety
202269  
203−```ts
204−test("handles errors", async () => {
205− await using proc = Bun.spawn({
206− cmd: [bunExe(), "run", "invalid.js"],
207− env: bunEnv,
208− });
270+These are the patterns that trip people up. Get them wrong and you get
271+crashes that only reproduce under load or in CI.
209272  
210− const exitCode = await proc.exited;
211− expect(exitCode).not.toBe(0);
273+### Pointer provenance at FFI boundaries
212274  
213− // For synchronous errors
214− expect(() => someFunction()).toThrow("Expected error message");
215−});
275+If a callback may free `self` (close, error, GC finalize), do **not**
276+materialize `&self`/`&mut self` at the boundary — a `&self`-derived raw
277+pointer carries `SharedReadOnly` provenance, and `Box::from_raw`/dealloc
278+through it is UB. Pass and dispatch off `*mut Self` until the body proves
279+ownership. `src/io/PipeWriter.rs`'s `impl_streaming_writer_parent!` macro
280+encodes the three modes:
281+ 
282+- `borrow = mut` — body forms `&mut *this`; safe when nothing re-enters
283+- `borrow = shared` — body forms `&*this`; safe when re-entrant code only needs `&Self`
284+- `borrow = ptr` — body calls `Self::method(this, ..)` with `this: *mut Self`; required when the callback may free `self`
285+ 
286+### `Strong` / `Weak` JS handles
287+ 
288+`bun_jsc::Strong` keeps a JS value alive; it is `!Send`/`!Sync` and must be
289+created and dropped on the JS thread.
290+ 
291+```rust
292+use bun_jsc::Strong;
293+let strong = Strong::create(value, global);
294+let v: JSValue = strong.get();
295+// drop(strong) releases the GC handle
216296 ```
217297  
218−### Avoid dynamic import & require
298+`bun_jsc::Weak<T>` is the GC-cleared variant. For raw values without a `Strong`
299+wrapper, `JSValue::protect()` / `unprotect()` and `ensure_still_alive()` are
300+available, but `Strong` is preferred — it can't be forgotten or unbalanced.
219301  
220−**Only** use dynamic import or require when the test is specifically testing something relataed to dynamic import or require. Otherwise, **always use module-scope import statements**.
302+### Refcount transfer on `to_js()` / `create()`
221303  
222−**BAD, do not do this**:
304+A `to_js()` / `create()` that returns a wrapped pointer **transfers** the
305+caller's `+1` to the JS wrapper. Do not `ref()` again before the return; the
306+finalizer derefs once. The leak-or-UAF symptoms of getting this wrong are
307+distinctive: an extra `ref()` leaks until process exit; a missing `ref()` on a
308+non-transferring path UAFs at GC.
223309  
224−```ts
225−test("foo", async () => {
226− // BAD: Unnecessary usage of dynamic import.
227− const { readFile } = await import("node:fs");
310+### Cross-thread string hazards
228311  
229− expect(await readFile("ok.txt")).toBe("");
230−});
231−```
312+`AtomString`s live in a per-thread table. Never deref one from another thread —
313+it trips `wasRemoved` in `AtomStringImpl::remove()`. If a `bun_core::String`
314+may be dropped from a non-JS thread (HTTP worker, threadpool, dying VM), build
315+it via `String::clone_utf8` (a plain `WTFStringImpl` with an atomic refcount),
316+not from an interned/atomized JS string. See the comment in
317+`src/runtime/webcore/fetch/FetchTasklet.rs` near `Response::init` for the
318+canonical example of this bug class and its fix.
232319  
233−**GOOD, do this:**
320+## Common Patterns
234321  
235−```ts
236−import { readFile } from "node:fs";
237−test("foo", async () => {
238− expect(await readFile("ok.txt")).toBe("");
239−});
240−```
322+```rust
323+// Read a file, return JS error on failure
324+let contents = match bun_sys::File::openat(Fd::cwd(), path, O::RDONLY, 0)
325+ .and_then(|f| f.read_to_end())
326+{
327+ Ok(bytes) => bytes,
328+ Err(err) => return Ok(err.to_js(global)?),
329+};
241330  
242−### Test Utilities
331+// Heap-allocated FFI handle with explicit lifecycle
332+let raw = bun_core::heap::into_raw(Box::new(MyHandle::new()));
333+register_with_c(raw);
334+// ... later, in the matching teardown callback:
335+unsafe { bun_core::heap::destroy(raw) };
243336  
244−- Use `describe.each()` for parameterized tests
245−- Use `toMatchSnapshot()` for snapshot testing
246−- Use `beforeAll()`, `afterEach()`, `beforeEach()` for setup/teardown
247−- Track resources (servers, clients) in arrays for cleanup in `afterEach()`
337+// Hashing
338+bun_wyhash::hash(bytes) // u64
339+bun_wyhash::hash_with_seed(seed, bytes)
340+```
248341  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack