Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Syntax reminders:

Conventions:

- Prefer `@import` at the **bottom** of the file.
- It's `@import("bun")` not `@import("root").bun`
- Prefer `@import` at the **bottom** of the file, but the auto formatter will move them so you don't need to worry about it.
- Prefer `@import("bun")`. Not `@import("root").bun` or `@import("../bun.zig")`.
- You must be patient with the build.
135 changes: 105 additions & 30 deletions src/Watcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,48 @@ fn watchLoop(this: *Watcher) bun.sys.Maybe(void) {
return .success;
}

/// Register a file descriptor with kqueue on macOS without validation.
///
/// Preconditions (caller must ensure):
/// - `fd` is a valid, open file descriptor
/// - `fd` is not already registered with this kqueue
/// - `watchlist_id` matches the entry's index in the watchlist
///
/// Note: This function does not propagate kevent registration errors.
/// If registration fails, the file will not be watched but no error is returned.
pub fn addFileDescriptorToKQueueWithoutChecks(this: *Watcher, fd: bun.FileDescriptor, watchlist_id: usize) void {
const KEvent = std.c.Kevent;

// https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
var event = std.mem.zeroes(KEvent);

event.flags = std.c.EV.ADD | std.c.EV.CLEAR | std.c.EV.ENABLE;
// we want to know about the vnode
event.filter = std.c.EVFILT.VNODE;

event.fflags = std.c.NOTE.WRITE | std.c.NOTE.RENAME | std.c.NOTE.DELETE;

// id
event.ident = @intCast(fd.native());

// Store the index for fast filtering later
event.udata = @as(usize, @intCast(watchlist_id));
var events: [1]KEvent = .{event};

// This took a lot of work to figure out the right permutation
// Basically:
// - We register the event here.
// our while(true) loop above receives notification of changes to any of the events created here.
_ = std.posix.system.kevent(
this.platform.fd.unwrap().?.native(),
@as([]KEvent, events[0..1]).ptr,
1,
@as([]KEvent, events[0..1]).ptr,
0,
null,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fn appendFileAssumeCapacity(
this: *Watcher,
fd: bun.FileDescriptor,
Expand Down Expand Up @@ -350,36 +392,7 @@ fn appendFileAssumeCapacity(
};

if (comptime Environment.isMac) {
const KEvent = std.c.Kevent;

// https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
var event = std.mem.zeroes(KEvent);

event.flags = std.c.EV.ADD | std.c.EV.CLEAR | std.c.EV.ENABLE;
// we want to know about the vnode
event.filter = std.c.EVFILT.VNODE;

event.fflags = std.c.NOTE.WRITE | std.c.NOTE.RENAME | std.c.NOTE.DELETE;

// id
event.ident = @intCast(fd.native());

// Store the hash for fast filtering later
event.udata = @as(usize, @intCast(watchlist_id));
var events: [1]KEvent = .{event};

// This took a lot of work to figure out the right permutation
// Basically:
// - We register the event here.
// our while(true) loop above receives notification of changes to any of the events created here.
_ = std.posix.system.kevent(
this.platform.fd.unwrap().?.native(),
@as([]KEvent, events[0..1]).ptr,
1,
@as([]KEvent, events[0..1]).ptr,
0,
null,
);
this.addFileDescriptorToKQueueWithoutChecks(fd, watchlist_id);
} else if (comptime Environment.isLinux) {
// var file_path_to_use_ = std.mem.trimRight(u8, file_path_, "/");
// var buf: [bun.MAX_PATH_BYTES+1]u8 = undefined;
Expand Down Expand Up @@ -612,6 +625,68 @@ pub fn addDirectory(
return this.appendDirectoryAssumeCapacity(fd, file_path, hash, clone_file_path);
}

pub fn addFileByPathSlow(
this: *Watcher,
file_path: string,
loader: options.Loader,
) bool {
const hash = getHash(file_path);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Check if already watched (with lock to avoid race with removal)
{
this.mutex.lock();
const already_watched = this.indexOf(hash) != null;
this.mutex.unlock();

if (already_watched) {
return true;
}
}
Comment on lines +648 to +656

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a nested block?


// Only open fd if we might need it
var fd: bun.FileDescriptor = bun.invalid_fd;
if (Environment.isMac) {
const path_z = std.posix.toPosixPath(file_path) catch return false;
switch (bun.sys.open(&path_z, bun.c.O_EVTONLY, 0)) {
.result => |opened| fd = opened,
.err => return false,
}
}

const res = this.addFile(fd, file_path, hash, loader, bun.invalid_fd, null, true);
switch (res) {
.result => {
// On macOS, addFile may have found the file already watched (race)
// and returned success without using our fd. Close it if unused.
if (comptime Environment.isMac) {
if (fd.isValid()) {
this.mutex.lock();
const maybe_idx = this.indexOf(hash);
const stored_fd = if (maybe_idx) |idx|
this.watchlist.items(.fd)[idx]
else
bun.invalid_fd;
this.mutex.unlock();

// Only close if entry exists and stored fd differs from ours.
// Race scenarios:
// 1. Entry removed (maybe_idx == null): our fd was stored then closed by flushEvictions → don't close
// 2. Entry exists with different fd: another thread added entry, addFile didn't use our fd → close ours
// 3. Entry exists with same fd: our fd was stored → don't close
if (maybe_idx != null and stored_fd.native() != fd.native()) {
fd.close();
}
}
}
Comment thread
Jarred-Sumner marked this conversation as resolved.
Outdated
return true;
},
.err => {
if (fd.isValid()) fd.close();
return false;
},
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub fn addFile(
this: *Watcher,
fd: bun.FileDescriptor,
Expand Down
11 changes: 6 additions & 5 deletions src/bun.js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ pub const Run = struct {
}

switch (this.ctx.debug.hot_reload) {
.hot => jsc.hot_reloader.HotReloader.enableHotModuleReloading(vm),
.watch => jsc.hot_reloader.WatchReloader.enableHotModuleReloading(vm),
.hot => jsc.hot_reloader.HotReloader.enableHotModuleReloading(vm, this.entry_path),
.watch => jsc.hot_reloader.WatchReloader.enableHotModuleReloading(vm, this.entry_path),
else => {},
}

Expand All @@ -328,6 +328,7 @@ pub const Run = struct {
promise.setHandled(vm.global.vm());

if (vm.hot_reload != .none or handled) {
vm.addMainToWatcherIfNeeded();
vm.eventLoop().tick();
vm.eventLoop().tickPossiblyForever();
} else {
Expand Down Expand Up @@ -389,21 +390,21 @@ pub const Run = struct {

{
if (this.vm.isWatcherEnabled()) {
vm.handlePendingInternalPromiseRejection();
vm.reportExceptionInHotReloadedModuleIfNeeded();

while (true) {
while (vm.isEventLoopAlive()) {
vm.tick();

// Report exceptions in hot-reloaded modules
vm.handlePendingInternalPromiseRejection();
vm.reportExceptionInHotReloadedModuleIfNeeded();

vm.eventLoop().autoTickActive();
}

vm.onBeforeExit();

vm.handlePendingInternalPromiseRejection();
vm.reportExceptionInHotReloadedModuleIfNeeded();

vm.eventLoop().tickPossiblyForever();
}
Expand Down
20 changes: 18 additions & 2 deletions src/bun.js/VirtualMachine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -677,12 +677,28 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
return handled;
}

pub fn handlePendingInternalPromiseRejection(this: *jsc.VirtualMachine) void {
var promise = this.pending_internal_promise.?;
pub fn reportExceptionInHotReloadedModuleIfNeeded(this: *jsc.VirtualMachine) void {
const promise_opt = this.pending_internal_promise;
if (promise_opt == null) {
this.addMainToWatcherIfNeeded();
return;
}
var promise = promise_opt.?;

@taylordotfish taylordotfish Oct 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edit: see followup suggestion; this can be even simpler

Suggested change
if (promise_opt == null) {
this.addMainToWatcherIfNeeded();
return;
}
var promise = promise_opt.?;
var promise = promise_opt orelse {
this.addMainToWatcherIfNeeded();
return;
};


if (promise.status(this.global.vm()) == .rejected and !promise.isHandled(this.global.vm())) {
this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue());
promise.setHandled(this.global.vm());
}

this.addMainToWatcherIfNeeded();
Comment thread
Jarred-Sumner marked this conversation as resolved.
Outdated
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub fn addMainToWatcherIfNeeded(this: *jsc.VirtualMachine) void {
if (this.isWatcherEnabled()) {
const main = this.main;
if (main.len == 0) return;
_ = this.bun_watcher.addFileByPathSlow(main, this.transpiler.options.loader(std.fs.path.extension(main)));
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub fn defaultOnUnhandledRejection(this: *jsc.VirtualMachine, _: *JSGlobalObject, value: JSValue) void {
Expand Down
Loading