initial commit

This commit is contained in:
i2p
2026-08-27 21:09:14 +00:00
commit a5b6d59437
12681 changed files with 3253832 additions and 0 deletions
@@ -0,0 +1,184 @@
---
name: implementing-jsc-classes-cpp
description: Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.
---
# Implementing JavaScript Classes in C++
## Class Structure
For publicly accessible Constructor and Prototype, create 3 classes:
1. **`class Foo : public JSC::DestructibleObject`** - if C++ fields exist; otherwise use `JSC::constructEmptyObject` with `putDirectOffset`
2. **`class FooPrototype : public JSC::JSNonFinalObject`**
3. **`class FooConstructor : public JSC::InternalFunction`**
No public constructor? Only Prototype and class needed.
## Iso Subspaces
Classes with C++ fields need subspaces in:
- `src/jsc/bindings/webcore/DOMClientIsoSubspaces.h`
- `src/jsc/bindings/webcore/DOMIsoSubspaces.h`
```cpp
template<typename MyClassT, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) {
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return WebCore::subspaceForImpl<MyClassT, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForMyClassT.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForMyClassT = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForMyClassT.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForMyClassT = std::forward<decltype(space)>(space); });
}
```
## Property Definitions
```cpp
static JSC_DECLARE_HOST_FUNCTION(jsFooProtoFuncMethod);
static JSC_DECLARE_CUSTOM_GETTER(jsFooGetter_property);
static const HashTableValue JSFooPrototypeTableValues[] = {
{ "property"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsFooGetter_property, 0 } },
{ "method"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFooProtoFuncMethod, 1 } },
};
```
## Prototype Class
```cpp
class JSFooPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSFooPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) {
JSFooPrototype* prototype = new (NotNull, allocateCell<JSFooPrototype>(vm)) JSFooPrototype(vm, structure);
prototype->finishCreation(vm);
return prototype;
}
template<typename, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { return &vm.plainObjectSpace(); }
DECLARE_INFO;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) {
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
private:
JSFooPrototype(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) {}
void finishCreation(JSC::VM& vm);
};
void JSFooPrototype::finishCreation(VM& vm) {
Base::finishCreation(vm);
reifyStaticProperties(vm, JSFoo::info(), JSFooPrototypeTableValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
```
## Getter/Setter/Function Definitions
```cpp
// Getter
JSC_DEFINE_CUSTOM_GETTER(jsFooGetter_prop, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName)) {
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSFoo* thisObject = jsDynamicCast<JSFoo*>(JSValue::decode(thisValue));
if (UNLIKELY(!thisObject)) {
Bun::throwThisTypeError(*globalObject, scope, "JSFoo"_s, "prop"_s);
return {};
}
return JSValue::encode(jsBoolean(thisObject->value()));
}
// Function
JSC_DEFINE_HOST_FUNCTION(jsFooProtoFuncMethod, (JSGlobalObject* globalObject, CallFrame* callFrame)) {
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSFoo*>(callFrame->thisValue());
if (UNLIKELY(!thisObject)) {
Bun::throwThisTypeError(*globalObject, scope, "Foo"_s, "method"_s);
return {};
}
return JSValue::encode(thisObject->doSomething(vm, globalObject));
}
```
## Constructor Class
```cpp
class JSFooConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSFooConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype) {
JSFooConstructor* constructor = new (NotNull, JSC::allocateCell<JSFooConstructor>(vm)) JSFooConstructor(vm, structure);
constructor->finishCreation(vm, prototype);
return constructor;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { return &vm.internalFunctionSpace(); }
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) {
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
private:
JSFooConstructor(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure, callFoo, constructFoo) {}
void finishCreation(JSC::VM& vm, JSC::JSObject* prototype) {
Base::finishCreation(vm, 0, "Foo"_s);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly);
}
};
```
## Structure Caching
Add to `ZigGlobalObject.h`:
```cpp
JSC::LazyClassStructure m_JSFooClassStructure;
```
Initialize in `ZigGlobalObject.cpp`:
```cpp
m_JSFooClassStructure.initLater([](LazyClassStructure::Initializer& init) {
Bun::initJSFooClassStructure(init);
});
```
Visit in `visitChildrenImpl`:
```cpp
m_JSFooClassStructure.visit(visitor);
```
## Expose to Zig
```cpp
extern "C" JSC::EncodedJSValue Bun__JSFooConstructor(Zig::GlobalObject* globalObject) {
return JSValue::encode(globalObject->m_JSFooClassStructure.constructor(globalObject));
}
extern "C" EncodedJSValue Bun__Foo__toJS(Zig::GlobalObject* globalObject, Foo* foo) {
auto* structure = globalObject->m_JSFooClassStructure.get(globalObject);
return JSValue::encode(JSFoo::create(globalObject->vm(), structure, globalObject, WTFMove(foo)));
}
```
Include `#include "root.h"` at the top of C++ files.
@@ -0,0 +1,131 @@
---
name: implementing-jsc-classes-rust
description: Creates JavaScript classes using Bun's Rust bindings generator (.classes.ts). Use when implementing new JS APIs in Rust with JSC integration, prototypes, or constructors.
---
# Bun's JavaScriptCore Class Bindings Generator
Bridge JavaScript and Rust through `.classes.ts` definitions and Rust implementations.
## Architecture
1. **JavaScript Interface Definition** (`.classes.ts` files)
2. **Rust Implementation** (`.rs` files)
3. **Generated Code**`src/codegen/generate-classes.ts` emits C++ + Rust into `${BUN_CODEGEN_DIR}/generated_classes.rs`, `include!`d as `crate::generated_classes` in `bun_runtime`. Run `bun bd` to regenerate.
## Class Definition (.classes.ts)
```typescript
export default [
define({
name: "Glob",
construct: true,
finalize: true,
hasPendingActivity: true,
proto: {
scan: { fn: "scan", length: 1 },
match: { fn: "match", length: 1 },
},
}),
];
```
Options:
- `construct`: Has a public `new Foo()` constructor
- `finalize`: Needs cleanup beyond `Drop` (rarely — see Finalize below)
- `hasPendingActivity`: GC keep-alive while async work is in flight
- `proto`: Methods (`fn:`), getters (`getter: true`, optionally `cache: true`)
- `values: [...]`: WriteBarrier slots for JS values the native side holds (callbacks, buffers)
## Rust Implementation
```rust
use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult};
use std::sync::atomic::{AtomicUsize, Ordering};
#[bun_jsc::JsClass]
pub struct Glob {
pattern: Box<[u8]>,
has_pending_activity: AtomicUsize,
}
impl Glob {
pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<Box<Glob>> {
let arg = frame.argument(0);
let pattern = bun_core::String::from_js(arg, global)?.to_utf8_bytes().into();
Ok(Box::new(Glob { pattern, has_pending_activity: AtomicUsize::new(0) }))
}
#[bun_jsc::host_fn(method)]
pub fn r#match(&self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
// ...
Ok(JSValue::TRUE)
}
pub fn has_pending_activity(&self) -> bool {
self.has_pending_activity.load(Ordering::SeqCst) > 0
}
}
```
### Canonical signatures
| Hook | Signature |
| ------------------- | ------------------------------------------------------------------------------------ |
| constructor | `pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<Box<Self>>` |
| method (`fn:`) | `pub fn name(&self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue>` |
| getter | `pub fn get_x(this: &Self, global: &JSGlobalObject) -> JsResult<JSValue>` |
| finalize | `pub fn finalize(self: Box<Self>)` — or omit; the blanket `JsFinalize` just drops |
| hasPendingActivity | `pub fn has_pending_activity(&self) -> bool` |
A missing or mis-typed hook is a **compile error** in `cargo check -p bun_runtime` — the generated code calls the inherent method directly.
## Hooking into the generated module
`#[bun_jsc::JsClass]` on the struct implements the `JsClass` trait (`to_js`, `from_js`, `from_js_direct`, `get_constructor`) by binding the C++ externs. Attribute knobs: `no_constructor`, `no_finalize`, `estimated_size`.
The codegen also emits a `js_$T` module with the cached-value accessors. Re-export it when you need `*_set_cached` / `*_get_cached` or `detach_ptr`:
```rust
pub use crate::generated_classes::js_Glob as js;
// or
bun_jsc::impl_js_class_via_generated!(Archive => crate::generated_classes::js_Archive);
```
The `js_$T` module surface:
```rust
pub fn from_js(value: JSValue) -> Option<NonNull<T>>;
pub fn from_js_direct(value: JSValue) -> Option<NonNull<T>>;
pub fn get_constructor(global: &JSGlobalObject) -> JSValue;
pub fn to_js(this: *mut T, global: &JSGlobalObject) -> JSValue; // ownership transfer
pub fn detach_ptr(value: JSValue);
// per cached getter / `values: [...]` entry:
pub fn <field>_set_cached(this_value: JSValue, global: &JSGlobalObject, value: JSValue);
pub fn <field>_get_cached(this_value: JSValue) -> Option<JSValue>;
```
## Finalize
Most classes need nothing — `#[bun_jsc::JsClass]` wires the blanket `JsFinalize` whose default is `drop(Box<Self>)`. Override only when you must release a JS handle or defer to a heap helper:
```rust
pub fn finalize(self: Box<Self>) {
bun_ptr::finalize_js_box(self, |this| this.this_value.with_mut(|v| v.finalize()));
}
```
Override with an **inherent** method, never `impl JsFinalize for T`.
## Holding JS values
Never store raw `JSValue` in a struct field. Declare a slot in `.classes.ts` (`values: ["callback"]` or a `cache: true` getter) and read/write it through `js::callback_set_cached(this_value, global, v)` / `js::callback_get_cached(this_value)`. The slot is a `WriteBarrier` visited by the GC, so the value stays alive without a `Strong`.
## Reference implementations
- `src/runtime/api/glob.rs` + `Glob.classes.ts` — constructor, methods, `hasPendingActivity`, default finalize
- `src/runtime/api/cron.rs` + `cron.classes.ts``noConstructor`, cached getter, `values: [...]`, custom finalize
- `src/runtime/image/Image.rs:56` — the `pub use crate::generated_classes::js_Image as js;` one-liner
- `src/jsc/host_fn.rs` — the host-fn adapters the codegen dispatches through
- `src/jsc_macros/lib.rs``#[bun_jsc::JsClass]` proc-macro source
@@ -0,0 +1,366 @@
---
name: javascriptcore-garbage-collector
description: JSC GC reference for Bun. Use for use-after-free, JS object leaks, "collected too early", or when touching WriteBarrier, visitChildren, visitAdditionalChildren, JSRef, JSC::Strong/Weak, hasPendingActivity, ensureStillAlive, addOpaqueRoot, reportExtraMemoryAllocated, IsoSubspace, HeapAnalyzer, finalize.
---
# JavaScriptCore's Garbage Collector (Riptide)
Riptide is **non-moving, generational, parallel, mostly-concurrent, conservative-on-the-stack**. Understanding those five words prevents most GC bugs in Bun.
## The mental model
The heap is a graph. GC does a breadth-first search from **roots** → marks everything it reaches → everything unmarked is freed (lazily, on next allocation from that block). It does NOT compact or move objects — pointers stay stable for an object's lifetime.
**Two collection modes:**
- **Eden GC**: only scans newly-allocated objects + remembered set. Fast, frequent.
- **Full GC**: scans everything. Slower, rarer.
**It runs concurrently.** Marking happens on background threads _while JS is executing_; the mutator only stops at brief safepoints. `visitChildren` runs **off the main thread, racing with your code**.
## How the VM gathers roots
Roots are not a hardcoded list — they are **marking constraints** registered with `Heap::addMarkingConstraint()` and run to fixpoint. The built-in set lives in `Heap::addCoreConstraints()` (`vendor/WebKit/Source/JavaScriptCore/heap/Heap.cpp:2970`):
| Tag | Name | What it marks |
| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cs` | Conservative Scan | Native stack + registers of every JS thread, scanned word-by-word (`gatherStackRoots``ConservativeRoots`). Also JIT stub routines. World is stopped for this. |
| `Msr` | Misc Small Roots | `vm.smallStrings`, `m_protectedValues` (`JSValueProtect`/`gcProtect`), `MarkedArgumentBuffer` lists, `vm.exception()` / `lastException()` / `m_terminationException` |
| `Sh` | Strong Handles | `m_handleSet.visitStrongHandles()` — every `JSC::Strong<T>`. Also `vm().visitAggregate()` (atom string tables etc.) |
| `D` | Debugger | Sampling profiler, type profiler, ShadowChicken |
| `Ws` | Weak Sets | Iterates every `WeakBlock`; calls `WeakHandleOwner::isReachableFromOpaqueRoots()` to decide whether a weak ref should _become_ strong this cycle |
| `O` | Output | Calls `visitOutputConstraints()` on already-marked cells in output-constraint subspaces (executables, WeakMaps). This is the "re-run after marking discovers more" hook |
| `Jw` | JIT Worklist | CodeBlocks queued for compilation |
| `Cb` | CodeBlocks | Executing/compiling CodeBlocks |
Bun registers an additional constraint, `DOMGCOutputConstraint` (`src/jsc/bindings/BunGCOutputConstraint.cpp`), which calls `visitOutputConstraints` on every marked cell in Bun's output-constraint subspaces (event targets, generated classes with `visitAdditionalChildren`, etc.).
**Constraint volatility** controls when they re-run during the fixpoint:
- `GreyedByExecution` — may produce new grey cells whenever the mutator runs (re-run after every resume)
- `GreyedByMarking` — may produce new grey cells when _other_ marking happens (re-run after each drain)
- `SeldomGreyed` — usually doesn't add anything; run last
## Object layout: the 8-byte JSCell header
Every GC-managed object inherits `JSCell` (`runtime/JSCell.h`):
```
| StructureID (4) | indexingTypeAndMisc (1) | JSType (1) | flags (1) | cellState (1) |
```
- `StructureID` — compressed hidden-class pointer
- `indexingTypeAndMisc` — 2 bits are an embedded `WTF::Lock` (the **cell lock**); always CAS this byte
- `cellState` — inlined GC color, used by the write barrier
Out-of-line, in the `MarkedBlock` footer (or `PreciseAllocation` header for objects >~8KB):
- `isMarked` bit — survived last GC
- `isNewlyAllocated` bit — allocated since last GC
Liveness = `isMarked || isNewlyAllocated` (with logical-versioning so blocks aren't swept eagerly).
## CellState and the write barrier
`vendor/WebKit/Source/JavaScriptCore/heap/CellState.h`:
```cpp
PossiblyBlack = 0 // visited (or old-space-pending-rescan during full GC)
DefinitelyWhite = 1 // new / unmarked
PossiblyGrey = 2 // on the mark stack
```
Generational + concurrent GC share **one** retreating-wavefront barrier:
```cpp
// After: obj->field = newValue
if (obj->cellState <= blackThreshold) // 0 normally, bumped while GC is marking
writeBarrierSlowPath(obj); // → put obj on remembered set / revisit
```
**You almost never write this by hand.** Use `WriteBarrier<T>` as the field type and call `.set(vm, owner, value)` — it stores then barriers. A raw `JSCell*` / `JSValue` member without a `WriteBarrier` wrapper is a bug: eden GC will free the target out from under you.
`LazyProperty<Owner, T>`, `LazyClassStructure`, and `WriteBarrierStructureID` are barrier-aware variants for lazily-initialized fields and structures.
## Allocation: where objects live
`bmalloc/libpas` provides pages; JSC carves them up:
- **`MarkedBlock`** — 16KB block, fixed cell size (segregated free list). Footer holds bitvectors. 16-byte minimum cell alignment. `addr & ~(16KB-1)` → block, so liveness checks are O(1).
- **`PreciseAllocation`** — large objects (>~8KB), individually `malloc`'d, 96-byte GC header. Always returns addresses with `addr % 16 == 8` so `ptr & 8` distinguishes them from MarkedBlock cells.
- **`CompleteSubspace`** — size-segregated set of `BlockDirectory`s for general JS objects.
- **`IsoSubspace`** — one subspace per C++ type (security: a freed cell can only be reused for the _same_ type, defeating type-confusion UAF). **Every Bun class with native fields needs its own IsoSubspace**`subspaceFor<T>` in the header, slot in `BunClientData`/`DOMIsoSubspaces`.
**Allocation may trigger GC.** A safepoint exists at every allocation. Never assume "I just allocated X, so Y from before is still alive" unless Y is rooted.
## Conservative stack scanning — what it does and doesn't guarantee
`vendor/WebKit/Source/JavaScriptCore/heap/ConservativeRoots.cpp` walks the native stack/registers word-by-word (after `MachineThreads::tryCopyOtherThreadStacks` snapshots them). Any aligned word inside a live `MarkedBlock` cell or `PreciseAllocation` is a root.
**This means:** a `JSCell*` / `JSValue` in a C++/Rust local variable _usually_ keeps the object alive — no `Handle`/`Local` ceremony like V8.
**This does NOT mean you're always safe.** The compiler may dead-store-eliminate the local after its last visible use, or never spill it. If you extract an interior pointer (`string->characters8()`, butterfly storage, typed-array `vector()`) and then call something that can allocate, the original cell may no longer be on the stack:
```cpp
JSC::EnsureStillAliveScope keepAlive(cell); // RAII: forces cell onto stack until scope end
// ... use interior pointer, call things that allocate ...
```
or `ensureStillAliveHere(cell)`. In Rust: `value.ensure_still_alive()`.
## `visitChildren` — the per-cell tracing hook
```cpp
// In header:
DECLARE_VISIT_CHILDREN;
WriteBarrier<JSObject> m_callback;
WriteBarrier<Unknown> m_cachedValue;
// In .cpp:
template<typename Visitor>
void JSFoo::visitChildrenImpl(JSCell* cell, Visitor& visitor) {
auto* thisObject = jsCast<JSFoo*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor); // ALWAYS call base first
visitor.append(thisObject->m_callback);
visitor.append(thisObject->m_cachedValue);
}
DEFINE_VISIT_CHILDREN(JSFoo);
```
**Rules — runs concurrently on a GC thread:**
- No allocation. No `toJS`, no `jsString`, nothing that touches `vm.heap`.
- No `ref()`/`deref()` of `RefCounted` (not thread-safe).
- No locks the main thread might also take while allocating (deadlock).
- If a field can be torn by a racing mutator, take `Locker locker { thisObject->cellLock() }` in both `visitChildren` and the mutating site.
- Forgetting to `append()` a `WriteBarrier` field → use-after-free, often eden-GC-only, often only under load.
## `visitAdditionalChildren` and output constraints
`visitChildren` only sees the cell's own fields. When a JS wrapper's liveness should propagate to **other JS objects reachable through native state** (event listeners, observers, the JS values held inside a wrapped C++ object), Bun uses the WebCore pattern:
```cpp
// Custom hook called from BOTH places:
template<typename Visitor>
void JSFoo::visitAdditionalChildren(Visitor& visitor) {
wrapped().listeners().visitJSEventListeners(visitor);
visitor.addOpaqueRoot(&wrapped());
}
// 1) From visitChildren (normal marking):
DEFINE_VISIT_CHILDREN_WITH_MODIFIER(..., JSFoo) {
...
thisObject->visitAdditionalChildren(visitor);
}
// 2) From visitOutputConstraints (constraint fixpoint re-scan):
template<typename Visitor>
void JSFoo::visitOutputConstraints(JSCell* cell, Visitor& visitor) {
auto* thisObject = jsCast<JSFoo*>(cell);
Base::visitOutputConstraints(thisObject, visitor);
thisObject->visitAdditionalChildren(visitor);
}
```
**Why two entry points?** `visitChildren` runs once when the cell turns grey. But marking may later discover that some _other_ native object (an opaque root) is live, which retroactively makes more of _this_ cell's references live. `visitOutputConstraints` is re-invoked by `DOMGCOutputConstraint` during the constraint fixpoint to catch that.
To make a class participate, its IsoSubspace must be registered as an **output-constraint subspace** (`clientSubspaceFor*` with `outputConstraint` in `BunClientData` / generated `ZigGeneratedClasses.cpp`). The codegen does this automatically when `.classes.ts` has `hasPendingActivity`, `own` properties, or event-target semantics.
## Opaque roots — liveness through non-JSCell pointers
When native objects form a graph that should keep wrappers alive:
```cpp
// In some wrapper's visitAdditionalChildren:
visitor.addOpaqueRoot(nativePtr); // "nativePtr is reachable"
// Elsewhere, deciding whether ANOTHER wrapper survives:
bool JSBarOwner::isReachableFromOpaqueRoots(Handle<Unknown> h, void* ctx,
AbstractSlotVisitor& v, ASCIILiteral* reason) {
auto* bar = static_cast<Bar*>(ctx);
if (UNLIKELY(reason)) *reason = "Bar is in document tree"_s;
return v.containsOpaqueRoot(bar->ownerNode());
}
```
The opaque-root set is just a `HashSet<void*>` rebuilt each cycle. It's how DOM trees stay alive as a unit.
## `JSC::Weak<T>`, `WeakImpl`, `WeakBlock`, `WeakHandleOwner`
`JSC::Weak<T>` (`vendor/WebKit/Source/JavaScriptCore/heap/Weak.h`) is the GC-aware weak pointer. It does **not** keep its target alive; `.get()` returns `nullptr` after the target is collected.
Under the hood:
- Each `Weak<T>` owns a `WeakImpl*` (`vendor/WebKit/Source/JavaScriptCore/heap/WeakImpl.h`): `{ JSValue, WeakHandleOwner* (low bits = state), void* context }`. State is `Live → Dead → Finalized → Deallocated`.
- `WeakImpl`s are slab-allocated in 1KB **`WeakBlock`s** (`vendor/WebKit/Source/JavaScriptCore/heap/WeakBlock.h`, `blockSize = 1024`). Every `MarkedBlock` and `PreciseAllocation` has a `WeakSet` — a linked list of `WeakBlock`s for cells in that container.
- During the `Ws` constraint, each `WeakBlock::visit()` walks its `WeakImpl`s; for each one whose target is **not yet marked**, it calls `WeakHandleOwner::isReachableFromOpaqueRoots(handle, context, visitor, &reason)`. Return `true` → the target is marked (the weak ref is "upgraded" this cycle). This is how `hasPendingActivity()` and opaque-root reachability keep wrappers alive even when nothing strongly references them.
- After marking, `WeakBlock::reap()` flips unmarked `Live` impls to `Dead`. `WeakBlock::sweep()` later runs `WeakHandleOwner::finalize(handle, context)` on each `Dead` impl, then frees the slot. **`finalize` runs on the mutator thread but the cell is already dead — do not touch its JS fields.** Typical use: drop the wrapper from a native→JS wrapper cache.
```cpp
struct MyOwner final : public JSC::WeakHandleOwner {
bool isReachableFromOpaqueRoots(Handle<Unknown>, void* ctx,
AbstractSlotVisitor& v, ASCIILiteral*) override {
return static_cast<NativeThing*>(ctx)->hasPendingActivity();
}
void finalize(Handle<Unknown>, void* ctx) override {
static_cast<NativeThing*>(ctx)->m_wrapper = nullptr;
}
};
JSC::Weak<JSFoo> m_wrapper { jsFoo, &myOwnerSingleton, nativeThing };
```
`Weak<T>` is **move-only** (allocates a `WeakImpl`). Don't put it in a hot path; cache it.
## `JSRef` — the native↔wrapper reference pattern
When a native object needs to hold a reference back to its own JS wrapper, **use `JSRef`** (`src/jsc/JSRef.rs`), not `gcProtect`, not a raw `JSValue` field, and usually not `Strong` directly.
`JSRef` is a tagged union with three states:
- `Weak` — a bare `JSValue`. Does **not** keep the wrapper alive. Valid only because the wrapper's `finalize()` will flip this to `Finalized` before the cell is freed, so `try_get()` returns `None` instead of a dangling pointer. (This is _not_ a `JSC::Weak`; it's cheaper — no `WeakImpl` allocation.)
- `Strong` — wraps `bun_jsc::Strong` (a `JSC::Strong<Unknown>` root). Keeps the wrapper alive.
- `Finalized` — terminal; `try_get()` returns `None`.
Pattern: **strong while busy, weak while idle.**
```rust
this_value: JSRef, // initialized with JSRef::empty()
// On construction / when work starts:
self.this_value.set_strong(js_wrapper, global); // or .upgrade(global)
// When the last in-flight operation completes:
self.this_value.downgrade(); // Strong -> Weak, GC may now collect
// In any callback that needs the wrapper:
let Some(js_this) = self.this_value.try_get() else { return };
// In the codegen'd finalize():
self.this_value.finalize();
```
See `ServerWebSocket`, `UDPSocket`, `MySQLConnection`, `ValkeyClient` for real examples.
**`JSRef` requires a finalizer.** The `Weak` state is only sound because the codegen'd `finalize()` flips it to `Finalized` before the cell is reused. If your `.classes.ts` entry has `finalize: true` (almost all native-backed classes do), `JSRef` is the default choice for self-references.
**`JSRef` vs `hasPendingActivity`:** prefer `JSRef`. `hasPendingActivity: true` is a GC-thread-polled atomic predicate; its only real justification is when **many concurrent operations** independently keep the wrapper alive and there's no single place to call `upgrade()`/`downgrade()` — i.e., refcount-style liveness where the count is touched from multiple threads. That's uncommon. If you can identify "work started" / "work finished" edges, use `JSRef`. Don't add `hasPendingActivity` reflexively; it costs a constraint-fixpoint poll on every GC.
## `gcProtect` / `JSValueProtect` — almost never
`gcProtect()` / `JSValueProtect()` push into `Heap::m_protectedValues` (a ref-counted root map, visited by the `Msr` constraint). It's the legacy C-API mechanism. **Avoid it in Bun:**
- It's a raw global root with manual unprotect — easy to leak.
- It has no owner, so heap snapshots can't attribute the retention.
- `bun_jsc::Strong` / `JSRef` give the same guarantee with RAII and a destructor.
The only legitimate uses are inside the JSC C API shims themselves, or one-off debugging.
## Extra-memory reporting — `reportExtraMemoryAllocated` / `reportExtraMemoryVisited`
The GC schedules itself by bytes-allocated-since-last-GC. It only sees JSCell allocations, so a 32-byte wrapper around a 50MB native buffer looks like 32 bytes → GC never triggers → OOM.
**Contract — both halves are required:**
```cpp
// 1) When the native memory is allocated (or the wrapper takes ownership):
vm.heap.reportExtraMemoryAllocated(ownerCell, byteCount);
// 2) In visitChildren, every time the cell is visited:
visitor.reportExtraMemoryVisited(thisObject->wrapped().byteSize());
```
- `reportExtraMemoryAllocated` adds to the "since last GC" counter and may **immediately trigger a GC** (it's a safepoint). Call it _after_ the cell is fully constructed.
- `reportExtraMemoryVisited` adds to the "live bytes after this GC" counter, which sets the next trigger threshold. **If you forget this half**, the heap's high-water mark drifts down each cycle and you get back-to-back full GCs (the "GC death spiral").
- If the size changes over time, report the delta on growth (`reportExtraMemoryAllocated(cell, newSize - oldSize)`) and report the current size in `visitChildren`.
- `deprecatedReportExtraMemory` exists for callers that can't satisfy the visit-side half — avoid it.
In `.classes.ts`, `estimatedSize: true` generates the `reportExtraMemoryVisited` side; you implement `estimated_size()` in Rust. You still call `reportExtraMemoryAllocated` (or the binding's helper) at allocation time.
## `HeapAnalyzer` — heap snapshots and labelling
`vendor/WebKit/Source/JavaScriptCore/heap/HeapAnalyzer.h` is the abstract visitor used to build heap snapshots (Web Inspector "Heap Snapshot", and Bun's V8-compatible `BunV8HeapSnapshotBuilder`). When a snapshot is requested, marking runs with an analyzer attached and each cell's `analyzeHeap` static is called:
```cpp
void JSFoo::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) {
auto* thisObject = jsCast<JSFoo*>(cell);
Base::analyzeHeap(cell, analyzer);
analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped());
analyzer.setLabelForCell(cell, thisObject->wrapped().url().string());
if (auto* child = thisObject->m_callback.get())
analyzer.analyzePropertyNameEdge(cell, child, vm.propertyNames->callback.impl());
}
```
API (`HeapAnalyzer`):
- `analyzeNode(cell)` — record a node
- `analyzeEdge(from, to, RootMarkReason)` / `analyzePropertyNameEdge` / `analyzeVariableNameEdge` / `analyzeIndexEdge` — record a labelled edge
- `setWrappedObjectForCell(cell, void*)` — link wrapper → native pointer
- `setLabelForCell(cell, String)` — human-readable name in the snapshot
- `setOpaqueRootReachabilityReasonForCell` — why a weakly-held wrapper survived
If your class shows up as an opaque blob in heap snapshots, implement `analyzeHeap`.
## How to keep things alive (decision table)
| Scenario | Mechanism |
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| JSCell field pointing to another JSCell | `WriteBarrier<T>` member + `visitor.append(m_field)` in `visitChildren` |
| Native state inside the wrapped C++ object holds JS values | `visitAdditionalChildren` + register subspace as output-constraint |
| C++/Rust local across allocation/call | Conservative scan (free) — add `EnsureStillAliveScope` / `value.ensure_still_alive()` if extracting interior pointers or seeing release-only crashes |
| Native object holds its own JS wrapper (class has `finalize: true`) | **`JSRef`** — `upgrade()` when work starts, `downgrade()` when idle. **This is the default.** |
| Native object owns an arbitrary JS value (callback, options object) | `bun_jsc::Strong` — drop in `finalize()`. Watch for cycles |
| C++ non-GC object owns a JS value as a root | `JSC::Strong<T>`. **Danger:** cycle if the JS value can reach back → leak |
| Weak ref with resurrection predicate / finalize callback (C++) | `JSC::Weak<T>` + `WeakHandleOwner` |
| Wrapper kept alive by **many concurrent operations** with no single busy/idle edge | `.classes.ts` `hasPendingActivity: true` (atomic flag polled on GC thread). **Uncommon — prefer `JSRef` if you can.** |
| Group of wrappers share lifetime via a native graph | `visitor.addOpaqueRoot(ptr)` + `containsOpaqueRoot(ptr)` |
| Temporarily forbid GC in a critical section | `DeferGC deferGC(vm)` — defers until scope exit. Never hold across user JS |
| Tell GC about off-heap memory you own | `reportExtraMemoryAllocated` on alloc **and** `reportExtraMemoryVisited` in `visitChildren` |
| ~~Mark a value as root from C API~~ | ~~`gcProtect` / `JSValueProtect`~~**avoid**; use `bun_jsc::Strong` / `JSRef` instead |
## Destruction & finalizers
- `static constexpr bool needsDestruction = true` → C++ destructor runs when the cell is swept. Sweep is **lazy** (next allocation from that block, or `IncrementalSweeper`), so destruction is delayed arbitrarily. Do not rely on it for prompt resource release — expose explicit `close()`/`dispose()`.
- In `.classes.ts`, `finalize: true` → native `finalize()` called from the destructor. Same laziness applies.
- `WeakHandleOwner::finalize` runs earlier (at weak-reap time) but the cell is already dead; only use it to clear caches.
- Destructors run on the mutator thread but **other JS objects may already be swept** — do not dereference `WriteBarrier` fields in a destructor.
## Debugging GC issues
```bash
# Force synchronous, frequent GC — turns rare races into immediate crashes
BUN_JSC_collectContinuously=1 BUN_JSC_useConcurrentGC=0 bun-debug test.js
# Zero free cells so UAF reads are obvious
BUN_JSC_scribbleFreeCells=1
# Validate the GC's own bookkeeping
BUN_JSC_verifyGC=1 BUN_JSC_verboseVerifyGC=1
# See what's being collected / heap growth
BUN_JSC_logGC=2 BUN_JSC_showObjectStatistics=1
# Force GC from JS
Bun.gc(true) // sync full GC
require('bun:jsc').heapStats()
```
If a bug only reproduces with concurrent GC **on** → missing write barrier or `visitChildren` race.
If it only reproduces with `collectContinuously=1` → something isn't rooted across an allocation.
If memory grows but `heapStats().heapSize` doesn't → missing `reportExtraMemoryAllocated`.
If GC runs constantly with little garbage → missing `reportExtraMemoryVisited`.
## Key source files
- `vendor/WebKit/Source/JavaScriptCore/heap/Heap.cpp``collectImpl`, `addCoreConstraints` (root list)
- `vendor/WebKit/Source/JavaScriptCore/heap/SlotVisitor.cpp` / `SlotVisitorInlines.h``drain()`, `append`, `addOpaqueRoot`, `reportExtraMemoryVisited`
- `vendor/WebKit/Source/JavaScriptCore/heap/MarkedBlock.h`, `vendor/WebKit/Source/JavaScriptCore/heap/PreciseAllocation.h` — cell containers, `isLive`
- `vendor/WebKit/Source/JavaScriptCore/heap/CellState.h`, `runtime/WriteBarrier.h`, `runtime/WriteBarrierInlines.h`
- `vendor/WebKit/Source/JavaScriptCore/heap/ConservativeRoots.cpp`, `vendor/WebKit/Source/JavaScriptCore/heap/MachineStackMarker.cpp`
- `vendor/WebKit/Source/JavaScriptCore/heap/Weak.h`, `vendor/WebKit/Source/JavaScriptCore/heap/WeakImpl.h`, `vendor/WebKit/Source/JavaScriptCore/heap/WeakBlock.h`, `vendor/WebKit/Source/JavaScriptCore/heap/WeakSet.h`, `vendor/WebKit/Source/JavaScriptCore/heap/WeakHandleOwner.h`
- `vendor/WebKit/Source/JavaScriptCore/heap/HeapAnalyzer.h`, `vendor/WebKit/Source/JavaScriptCore/heap/HeapSnapshotBuilder.cpp`, Bun: `vendor/WebKit/Source/JavaScriptCore/heap/BunV8HeapSnapshotBuilder.cpp`
- `vendor/WebKit/Source/JavaScriptCore/heap/DeferGC.h`, `vendor/WebKit/Source/JavaScriptCore/heap/Strong.h`, `vendor/WebKit/Source/JavaScriptCore/heap/HandleSet.h`
- `runtime/JSCell.h` / `JSCellInlines.h` — header layout, `visitChildren` base
- Bun: `src/jsc/bindings/BunGCOutputConstraint.cpp`, `ZigGeneratedClasses.cpp` (codegen'd `visitChildren` / `visitOutputConstraints`)
+96
View File
@@ -0,0 +1,96 @@
---
name: rust-system-calls
description: Guides using bun_sys for system calls and file I/O in Rust. Use when implementing file operations, opening fds, or any syscall path instead of std::fs or libc.
---
# System Calls & File I/O in Rust
Use `bun_sys` instead of `std::fs` or raw `libc` for cross-platform syscalls with proper error handling.
## bun_sys::File (Preferred)
For most file operations, use the `bun_sys::File` wrapper. It owns the fd and closes on `Drop`.
```rust
use bun_sys::{File, Fd, O};
let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
let mut buf = vec![0u8; 4096];
let n = file.read_all(&mut buf)?; // loops until EOF or full
// `file` closes on Drop.
```
### Complete Example
```rust
use bun_sys::{File, Fd, O};
pub fn write_file(path: &[u8], data: &[u8]) -> Result<(), bun_sys::Error> {
let file = File::openat(Fd::cwd(), path, O::WRONLY | O::CREAT | O::TRUNC, 0o664)?;
file.write_all(data)?;
Ok(())
}
```
## Why bun_sys?
| Aspect | bun_sys | std::fs / libc |
| ----------- | ---------------------------------- | ---------------------- |
| Return Type | `Maybe<T>` with rich `Error` | `io::Error` (lossy) |
| Windows | Full support with libuv fallback | Incomplete/POSIX-ish |
| Error Info | errno, syscall tag, path, fd | errno only |
| EINTR | Automatic retry | Manual handling |
| Paths | `&[u8]` (WTF-8 safe) | `&Path` (UTF-8 lossy) |
## Error Handling with Maybe<T>
`bun_sys` functions return `Maybe<T> = Result<T, bun_sys::Error>`. Propagate with `?`; convert to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
```rust
use bun_sys_jsc::ErrorJsc;
use bun_sys::{File, Fd, O};
let file = match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
Ok(f) => f,
Err(err) => return Ok(err.to_js(global)?),
};
```
`bun_sys::Error` carries `errno`, `syscall: Tag`, and `path: Box<[u8]>`. To branch on errno:
```rust
match bun_sys::unlink(path) {
Ok(()) => {}
Err(e) if e.errno() == bun_c::ENOENT => {} // already gone
Err(e) => return Err(e),
}
```
## Key Types and Functions
- `Fd` (`bun_core::Fd`) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
- `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)`
- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
## Path Buffers
Use `bun_paths` for joining/normalization and the path-buffer pool to avoid 64 KB stack allocations on Windows:
```rust
use bun_paths::{path_buffer_pool, resolve_path::{self, platform}};
let mut buf = path_buffer_pool::get();
let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[dir, name]);
let file = File::openat(Fd::cwd(), joined, O::RDONLY, 0)?;
```
## Common Mistakes
- **Don't `.unwrap()`** a `bun_sys` result that user input or the OS can cause to fail at runtime — return the error.
- **Don't use `std::fs::File`** — it loses the syscall tag and path needed for Node-compatible error objects.
- **Don't allocate `PathBuffer` on the stack** in hot paths — use `path_buffer_pool::get()`.
- **Don't forget `Drop`** alone closes a `File` — never `file.fd().close()` while the `File` is still live (double close).
See `src/CLAUDE.md` for the full `bun_core`/`bun_sys` reference.
+50
View File
@@ -0,0 +1,50 @@
---
name: slowest-tests
description: Find the top-N slowest test files in CI from a recent BuildKite run, optionally posting the results to a Slack channel as a formatted table. Use when asked to find slow CI tests, "what's making CI slow", or to post a slow-test report to Slack.
---
# Slowest CI tests
Args: `[N] [#channel]` — both optional. `N` defaults to 500. If `#channel` is omitted, just print the table and stop.
## 1. Gather the data
Run the script. With no build number it auto-picks the most recent finished build from a merged PR.
```bash
bun run ci:slowest # top 500 from a recent merged-PR build → TSV on stdout
bun run ci:slowest 47324 100 # specific build, top 100
bun run ci:slowest --json > /tmp/slow.json
```
The script (`scripts/ci-slowest-tests.ts`) does the heavy lifting:
- Lists `test-bun` jobs from `bk build view <N>`, skipping `retried: true`.
- Fetches each job's `raw_log_url` directly with `Authorization: Bearer $BUILDKITE_TOKEN`. **Do not use `bk job log` — it hangs indefinitely on some Windows/alpine jobs.**
- Caches logs to `$TMPDIR/bun-ci-logs-<build>/` so re-runs are instant.
- Parses `_bk;t=<ms> ... --- [N/TOTAL] <file>` group headers, normalising backslashes to `/` and stripping `[attempt #N]` retries.
- Aggregates each file's duration as the **max across all platforms** (a file appears once per platform; shards within a platform are disjoint).
- Drops `package.json` / non-JS entries — those are setup steps, not tests.
If the script can't find a build automatically (rare — it walks the last 10 merged PRs), pick one yourself: `gh pr list --state merged --limit 10 --json number,headRefName`, then `bk build list --branch <headRefName>` and pass the first build with a `finished_at`. Merged-PR builds usually report `state: failed` because of flaky tests — that's fine, the timing data is still valid.
## 2. Post to Slack (only if a channel was given)
**`slack_send_message` does NOT support markdown tables** — its markdown→blocks converter rejects `| a | b |` syntax with `invalid_blocks`. Don't use Canvases either; they render tables but the MCP proxy 502s above ~10 KB and the result is clunky.
Procedure:
1. Look up the channel ID with `slack_search_channels` (the user gives a name, you need the `C…` ID).
2. Write the full N-row markdown table to `~/code/tmp/top<N>-slow-tests.md`, then upload it as a **secret gist**: `gh gist create <file> --desc "Bun CI: top N slowest test files (build #<num>)"`. (The Slack MCP has no file-upload tool; secret gist is the agreed fallback. Do **not** ask the user to attach anything manually.)
3. Post the **main** message: header (build link + gist link, "Rest in thread.") followed by the **top 20** bullets. Row format:
```
• 325s `test/js/bun/cron/in-process-cron.test.ts` 🐧 x64-baseline
• 96s `test/js/bun/http/serve-body-leak.test.ts` 🐧 x64-asan
```
- seconds: plain text, left-aligned, padded so the backticks line up
- filepath: code-font, **full path including `test/` prefix and extension** — do not strip anything
- platform: standard Unicode emoji only (🐧 linux, 🪟 windows, 🍎 macOS — never workspace-custom shortcodes) followed by the arch/variant **verbatim** from the job name (`x64-asan`, `aarch64`, `x64-baseline` — do not abbreviate)
4. Reply in-thread (`thread_ts` = the main message) with rows 21N in the same bullet format, packed into chunks under 4800 chars each (~70 rows per chunk). Post chunks sequentially so they stay ordered.
+79
View File
@@ -0,0 +1,79 @@
---
description: Re-sync src/react_compiler/ against upstream facebook/react. Use when bumping the React Compiler, when upstream lands a fix we need, or when src/react_compiler/UPSTREAM_PORTED is stale.
---
# Re-syncing the React Compiler
Bun integrates the React Compiler by **directly lowering Bun's AST into the
compiler's HIR** and **directly emitting Bun's AST from codegen**, skipping
upstream's Babel-shaped `react_compiler_ast` intermediate entirely. Nothing is
vendored — every upstream crate Bun uses has been ported into
`src/react_compiler/` and is built as part of the `bun_react_compiler` crate.
There are two kinds of port:
- **Whole-crate ports** (`hir/`, `diagnostics/`, `ssa/`, `inference/`,
`typeinference/`, `optimization/`, `validation/`, `reactive_scopes/`,
`utils/`) — byte-for-byte copies of the upstream crate's `src/`, modulo
crate-name/import rewrites. Upstream diffs apply mechanically.
- **AST-boundary ports** (`lowering/build_hir/`, `lowering/*.rs`, `codegen.rs`,
`pipeline.rs`, `program.rs`, `imports.rs`, `compile_result.rs`) — re-typed
onto `bun_ast` using the mapping in `src/react_compiler/DESIGN.md`. Upstream
diffs are re-ported by hand. Upstream's `gating.rs` is folded into
`program.rs`; `suppression.rs` is handled by the lexer
(`js_parser/lexer.rs`) and consumed in `program.rs`;
`identifier_loc_index.rs` is not needed because Bun's `Ref` already
provides binding identity.
`react_compiler_ast`, `react_compiler_lowering`, and the `react_compiler`
umbrella crate are **not** in Bun's tree at all — they exist upstream only as
the porting reference for the AST-boundary files.
## Sync procedure
1. **Produce the upstream diff.** The script sparse-fetches facebook/react into
a temp dir (nothing is written to the repo) and prints, per ported file, the
diff between `src/react_compiler/UPSTREAM_PORTED` and upstream's tip:
```sh
scripts/sync-react-compiler.sh # or pass an explicit <sha>
```
Output is grouped into three sections: whole-crate ports, AST-boundary
ports, and any new upstream file that newly references `react_compiler_ast`
(i.e. a new boundary file that needs a fresh Bun port).
2. **Apply whole-crate diffs mechanically.** For each hunk under the
whole-crate section, apply it to the corresponding `src/react_compiler/<dir>/`
file. The only systematic edit is import paths (`react_compiler_hir::` →
`crate::hir::`, etc.); everything else lands verbatim.
3. **Re-port AST-boundary diffs by hand.** For each hunk under the
AST-boundary section, re-port it into the named Bun file using the
type-mapping table in `src/react_compiler/DESIGN.md`: where upstream reads
`react_compiler_ast::expressions::Expression::Foo`, the Bun port reads
`bun_ast::expr::Data::EFoo`; where upstream constructs
`react_compiler_ast::statements::Statement::Foo { … }`, the Bun port calls
`Stmt::alloc(S::Foo { … }, loc)`. Keep control flow, pass ordering,
variable names, and comments 1:1 with upstream — only the AST reads/writes
change.
For large diffs, fan out one agent per file with the upstream diff + the Bun
port + DESIGN.md as context, then adversarially review each port.
4. **Handle new boundary files.** If the third section lists any file, write a
fresh Bun port of it under `src/react_compiler/`, add it to both arrays in
`scripts/sync-react-compiler.sh`, and add a row to the layout table in
`DESIGN.md`.
5. **Verify.**
```sh
cargo check -p bun_react_compiler
bun bd test test/bundler/transpiler/react-compiler.test.ts
```
Snapshots will change if codegen changed upstream — review the diff against
upstream's new fixture output and update with `bun bd test -u` if it
matches.
6. **Update the port marker** to the `UPSTREAM_HEAD` the script printed:
```sh
echo <new-sha> > src/react_compiler/UPSTREAM_PORTED
```
+46
View File
@@ -0,0 +1,46 @@
---
name: verify
description: Verify a Bun runtime change by driving the debug binary end-to-end.
---
# Verify a Bun runtime change
Build and drive the debug binary directly — never `bun test`, never import-and-call.
## Build
```sh
bun bd --version # builds ./build/debug/bun-debug and prints its version
```
## Drive
For any JS-visible change, run the debug binary with `-e` and observe stdout:
```sh
bun bd -e '<repro>' # builds, then runs; sets BUN_DEBUG_QUIET_LOGS for you
```
For worker/subprocess-shaped changes, spawn a subprocess (still `-e`) so worker teardown / event-loop-idle paths are exercised. Cross-check against `node -e '<same repro>'` for Node-compat changes.
## Gotchas
- **A `src/js/**` edit can silently not reach the binary.** `bundle-modules`
regenerates `build/<cfg>/codegen/InternalModuleRegistryConstants.h`, but the C++
TU that embeds it is not always recompiled, so the build succeeds while the
binary still runs the OLD JS. Gate on the binary, not the build: ask the binary
you just built — `bun bd -e 'console.log(<Class>.toString().includes("<new-id>"))'`
(or run `./build/<cfg>/bun` directly). Plain `bun` is the system Bun on $PATH and
never has your edit, so it answers about the wrong binary.
If false, `touch src/jsc/bindings/InternalModuleRegistry.cpp` and rebuild.
- **Prefix every `bun bd` with `PATH="$HOME/.cargo/bin:$PATH"`** — Homebrew's `rust`
formula shadows the pinned nightly, and `bun bd` dies with `the option 'Z' is only
accepted on the nightly compiler`. `bun bd` re-runs cargo on every invocation, so
this is needed for follow-up runs too, not just the first build.
- `node:cluster` changes can't be driven with `-e`: `cluster.fork()` re-execs `argv[1]`, so workers need a real file on disk. Write a scratch script and run `./build/debug/bun-debug <file>`.
- Only one `bun bd` per worktree at a time — a second one blocks on the build lock and looks like a runtime hang. Build once, then drive `./build/debug/bun-debug` directly under `timeout`.
- `BUN_DEBUG_QUIET_LOGS=1` suppresses debug-build log spam.
- Debug builds print `[cachefs]`/`[sys]` lines to stdout; filter them before diffing
output against `node`.
- MessagePort's `.on/.off` are added by requiring `worker_threads` — plain `new MessageChannel()` ports only have `addEventListener` until then.
- The debug+asan build is 10-100× slower than release; large-allocation stress tests can time out locally while passing in CI.
@@ -0,0 +1,222 @@
---
name: writing-bundler-tests
description: Guides writing bundler tests using itBundled/expectBundled in test/bundler/. Use when creating or modifying bundler, transpiler, or code transformation tests.
---
# Writing Bundler Tests
Bundler tests use `itBundled()` from `test/bundler/expectBundled.ts` to test Bun's bundler.
## Basic Usage
```typescript
import { describe } from "bun:test";
import { itBundled, dedent } from "./expectBundled";
describe("bundler", () => {
itBundled("category/TestName", {
files: {
"index.js": `console.log("hello");`,
},
run: {
stdout: "hello",
},
});
});
```
Test ID format: `category/TestName` (e.g., `banner/CommentBanner`, `minify/Empty`)
## File Setup
```typescript
{
files: {
"index.js": `console.log("test");`,
"lib.ts": `export const foo = 123;`,
"nested/file.js": `export default {};`,
},
entryPoints: ["index.js"], // defaults to first file
runtimeFiles: { // written AFTER bundling
"extra.js": `console.log("added later");`,
},
}
```
## Bundler Options
```typescript
{
outfile: "/out.js",
outdir: "/out",
format: "esm" | "cjs" | "iife",
target: "bun" | "browser" | "node",
// Minification
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
// Code manipulation
banner: "// copyright",
footer: "// end",
define: { "PROD": "true" },
external: ["lodash"],
// Advanced
sourceMap: "inline" | "external",
splitting: true,
treeShaking: true,
drop: ["console"],
}
```
## Runtime Verification
```typescript
{
run: {
stdout: "expected output", // exact match
stdout: /regex/, // pattern match
partialStdout: "contains this", // substring
stderr: "error output",
exitCode: 1,
env: { NODE_ENV: "production" },
runtime: "bun" | "node",
// Runtime errors
error: "ReferenceError: x is not defined",
},
}
```
## Bundle Errors/Warnings
```typescript
{
bundleErrors: {
"/file.js": ["error message 1", "error message 2"],
},
bundleWarnings: {
"/file.js": ["warning message"],
},
}
```
## Dead Code Elimination (DCE)
Add markers in source code:
```javascript
// KEEP - this should survive
const used = 1;
// REMOVE - this should be eliminated
const unused = 2;
```
```typescript
{
dce: true,
dceKeepMarkerCount: 5, // expected KEEP markers
}
```
## Capture Pattern
Verify exact transpilation with `capture()`:
```typescript
itBundled("string/Folding", {
files: {
"index.ts": `capture(\`\${1 + 1}\`);`,
},
capture: ['"2"'], // expected captured value
minifySyntax: true,
});
```
## Post-Bundle Assertions
```typescript
{
onAfterBundle(api) {
api.expectFile("out.js").toContain("console.log");
api.assertFileExists("out.js");
const content = api.readFile("out.js");
expect(content).toMatchSnapshot();
const values = api.captureFile("out.js");
expect(values).toEqual(["2"]);
},
}
```
## Common Patterns
**Simple output verification:**
```typescript
itBundled("banner/Comment", {
banner: "// copyright",
files: { "a.js": `console.log("Hello")` },
onAfterBundle(api) {
api.expectFile("out.js").toContain("// copyright");
},
});
```
**Multi-file CJS/ESM interop:**
```typescript
itBundled("cjs/ImportSyntax", {
files: {
"entry.js": `import lib from './lib.cjs'; console.log(lib);`,
"lib.cjs": `exports.foo = 'bar';`,
},
run: { stdout: '{"foo":"bar"}' },
});
```
**Error handling:**
```typescript
itBundled("edgecase/InvalidLoader", {
files: { "index.js": `...` },
bundleErrors: {
"index.js": ["Unsupported loader type"],
},
});
```
## Test Organization
```text
test/bundler/
├── bundler_banner.test.ts
├── bundler_string.test.ts
├── bundler_minify.test.ts
├── bundler_cjs.test.ts
├── bundler_edgecase.test.ts
├── bundler_splitting.test.ts
├── css/
├── transpiler/
└── expectBundled.ts
```
## Running Tests
```bash
bun bd test test/bundler/bundler_banner.test.ts
BUN_BUNDLER_TEST_FILTER="banner/Comment" bun bd test bundler_banner.test.ts
BUN_BUNDLER_TEST_DEBUG=1 bun bd test bundler_minify.test.ts
```
## Key Points
- Use `dedent` for readable multi-line code
- File paths are relative (e.g., `/index.js`)
- Use `capture()` to verify exact transpilation results
- Use `.toMatchSnapshot()` for complex outputs
- Pass array to `run` for multiple test scenarios
@@ -0,0 +1,94 @@
---
name: writing-dev-server-tests
description: Guides writing HMR/Dev Server tests in test/bake/. Use when creating or modifying dev server, hot reloading, or bundling tests.
---
# Writing HMR/Dev Server Tests
Dev server tests validate hot-reloading robustness and reliability.
## File Structure
- `test/bake/bake-harness.ts` - shared utilities: `devTest`, `prodTest`, `devAndProductionTest`, `Dev` class, `Client` class
- `test/bake/client-fixture.mjs` - subprocess for `Client` (page loading, IPC queries)
- `test/bake/dev/*.test.ts` - dev server and hot reload tests
- `test/bake/dev-and-prod.ts` - tests running on both dev and production mode
## Test Categories
- `bundle.test.ts` - DevServer-specific bundling bugs
- `css.test.ts` - CSS bundling issues
- `plugins.test.ts` - development mode plugins
- `ecosystem.test.ts` - library compatibility (prefer concrete bugs over full package tests)
- `esm.test.ts` - ESM features in development
- `html.test.ts` - HTML file handling
- `react-spa.test.ts` - React, react-refresh transform, server components
- `sourcemap.test.ts` - source map correctness
## devTest Basics
```ts
import { devTest, emptyHtmlFile } from "../bake-harness";
devTest("html file is watched", {
files: {
"index.html": emptyHtmlFile({
scripts: ["/script.ts"],
body: "<h1>Hello</h1>",
}),
"script.ts": `console.log("hello");`,
},
async test(dev) {
await dev.fetch("/").expect.toInclude("<h1>Hello</h1>");
await dev.patch("index.html", { find: "Hello", replace: "World" });
await dev.fetch("/").expect.toInclude("<h1>World</h1>");
await using c = await dev.client("/");
await c.expectMessage("hello");
await c.expectReload(async () => {
await dev.patch("index.html", { find: "World", replace: "Bar" });
});
await c.expectMessage("hello");
},
});
```
## Key APIs
- **`files`**: Initial filesystem state
- **`dev.fetch()`**: HTTP requests
- **`dev.client()`**: Opens browser instance
- **`dev.write/patch/delete`**: Filesystem mutations (wait for hot-reload automatically)
- **`c.expectMessage()`**: Assert console.log output
- **`c.expectReload()`**: Wrap code that causes hard reload
**Important**: Use `dev.write/patch/delete` instead of `node:fs` - they wait for hot-reload.
## Testing Errors
```ts
devTest("import then create", {
files: {
"index.html": `<!DOCTYPE html><html><head></head><body><script type="module" src="/script.ts"></script></body></html>`,
"script.ts": `import data from "./data"; console.log(data);`,
},
async test(dev) {
const c = await dev.client("/", {
errors: ['script.ts:1:18: error: Could not resolve: "./data"'],
});
await c.expectReload(async () => {
await dev.write("data.ts", "export default 'data';");
});
await c.expectMessage("data");
},
});
```
Specify expected errors with the `errors` option:
```ts
await dev.delete("other.ts", {
errors: ['index.ts:1:16: error: Could not resolve: "./other"'],
});
```