initial commit
This commit is contained in:
@@ -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`)
|
||||
@@ -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.
|
||||
@@ -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 21–N in the same bullet format, packed into chunks under 4800 chars each (~70 rows per chunk). Post chunks sequentially so they stay ordered.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"'],
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user