Files
bun.sh/src/Watcher.zig
robobun 11aedbe402 fix(fs.watch): emit 'change' events for files in watched directories on Linux (#26009)
## Summary
- Fixes #3657 - `fs.watch` on directory doesn't emit `change` events for
files created after watch starts

When watching a directory with `fs.watch`, files created after the watch
was established would only emit a 'rename' event on creation, but
subsequent modifications would not emit 'change' events.

## Root Cause

The issue was twofold:
1. `watch_dir_mask` in INotifyWatcher.zig was missing `IN.MODIFY`, so
the inotify system call was not subscribed to file modification events
for watched directories.
2. When directory events were processed in path_watcher.zig, all events
were hardcoded to emit 'rename' instead of properly distinguishing
between file creation/deletion ('rename') and file modification
('change').

## Changes

- Adds `IN.MODIFY` to `watch_dir_mask` to receive modification events
- Adds a `create` flag to `WatchEvent.Op` to track `IN.CREATE` events
- Updates directory event processing to emit 'change' for pure write
events and 'rename' for create/delete/move events

## Test plan
- [x] Added regression test `test/regression/issue/3657.test.ts`
- [x] Verified test fails with system Bun (before fix)
- [x] Verified test passes with debug build (after fix)
- [x] Verified manual reproduction from issue now works correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:46:20 -08:00

785 lines
26 KiB
Zig

//! Bun's cross-platform filesystem watcher. Runs on its own thread.
const Watcher = @This();
const DebugLogScope = bun.Output.Scoped(.watcher, .visible);
const log = DebugLogScope.log;
// This will always be [max_count]WatchEvent,
// We avoid statically allocating because it increases the binary size.
watch_events: []WatchEvent = &.{},
changed_filepaths: [max_count]?[:0]u8,
/// The platform-specific implementation of the watcher
platform: Platform,
watchlist: WatchList,
watched_count: usize,
mutex: Mutex,
fs: *bun.fs.FileSystem,
allocator: std.mem.Allocator,
watchloop_handle: ?std.Thread.Id = null,
cwd: string,
thread: std.Thread = undefined,
running: bool = true,
close_descriptors: bool = false,
evict_list: [max_eviction_count]WatchItemIndex = undefined,
evict_list_i: WatchItemIndex = 0,
ctx: *anyopaque,
onFileUpdate: *const fn (this: *anyopaque, events: []WatchEvent, changed_files: []?[:0]u8, watchlist: WatchList) void,
onError: *const fn (this: *anyopaque, err: bun.sys.Error) void,
thread_lock: bun.safety.ThreadLock = .initUnlocked(),
pub const max_count = 128;
pub const requires_file_descriptors = switch (Environment.os) {
.mac => true,
else => false,
};
pub const Event = WatchEvent;
pub const Item = WatchItem;
pub const ItemList = WatchList;
pub const WatchList = std.MultiArrayList(WatchItem);
pub const HashType = u32;
const no_watch_item: WatchItemIndex = std.math.maxInt(WatchItemIndex);
/// Initializes a watcher. Each watcher is tied to some context type, which
/// receives watch callbacks on the watcher thread. This function does not
/// actually start the watcher thread.
///
/// const watcher = try Watcher.init(T, instance_of_t, fs, bun.default_allocator)
/// errdefer watcher.deinit(false);
/// try watcher.start();
///
/// To integrate a started watcher into module resolution:
///
/// transpiler.resolver.watcher = watcher.getResolveWatcher();
///
/// To integrate a started watcher into bundle_v2:
///
/// bundle_v2.bun_watcher = watcher;
pub fn init(comptime T: type, ctx: *T, fs: *bun.fs.FileSystem, allocator: std.mem.Allocator) !*Watcher {
const wrapped = struct {
fn onFileUpdateWrapped(ctx_opaque: *anyopaque, events: []WatchEvent, changed_files: []?[:0]u8, watchlist: WatchList) void {
T.onFileUpdate(@ptrCast(@alignCast(ctx_opaque)), events, changed_files, watchlist);
}
fn onErrorWrapped(ctx_opaque: *anyopaque, err: bun.sys.Error) void {
if (@hasDecl(T, "onWatchError")) {
T.onWatchError(@ptrCast(@alignCast(ctx_opaque)), err);
} else {
T.onError(@ptrCast(@alignCast(ctx_opaque)), err);
}
}
};
const watcher = try allocator.create(Watcher);
errdefer allocator.destroy(watcher);
watcher.* = .{
.fs = fs,
.allocator = allocator,
.watched_count = 0,
.watchlist = WatchList{},
.mutex = .{},
.cwd = fs.top_level_dir,
.ctx = ctx,
.onFileUpdate = &wrapped.onFileUpdateWrapped,
.onError = &wrapped.onErrorWrapped,
.platform = .{},
.watch_events = try allocator.alloc(WatchEvent, max_count),
.changed_filepaths = [_]?[:0]u8{null} ** max_count,
};
try Platform.init(&watcher.platform, fs.top_level_dir);
// Initialize trace file if BUN_WATCHER_TRACE env var is set
WatcherTrace.init();
return watcher;
}
/// Write trace events to the trace file if enabled.
/// This runs on the watcher thread, so no locking is needed.
pub fn writeTraceEvents(this: *Watcher, events: []WatchEvent, changed_files: []?[:0]u8) void {
WatcherTrace.writeEvents(this, events, changed_files);
}
pub fn start(this: *Watcher) !void {
bun.assert(this.watchloop_handle == null);
this.thread = try std.Thread.spawn(.{}, threadMain, .{this});
}
pub fn deinit(this: *Watcher, close_descriptors: bool) void {
if (this.watchloop_handle != null) {
this.mutex.lock();
defer this.mutex.unlock();
this.close_descriptors = close_descriptors;
this.running = false;
} else {
if (close_descriptors and this.running) {
const fds = this.watchlist.items(.fd);
for (fds) |fd| {
fd.close();
}
}
this.watchlist.deinit(this.allocator);
const allocator = this.allocator;
allocator.destroy(this);
}
}
pub fn getHash(filepath: string) HashType {
return @as(HashType, @truncate(bun.hash(filepath)));
}
pub const WatchItemIndex = u16;
pub const max_eviction_count = 8096;
// TODO: some platform-specific behavior is implemented in
// this file instead of the platform-specific file.
// ideally, the constants above can be inlined
const Platform = switch (Environment.os) {
.linux => @import("./watcher/INotifyWatcher.zig"),
.mac => @import("./watcher/KEventWatcher.zig"),
.windows => WindowsWatcher,
.wasm => @compileError("Unsupported platform"),
};
pub const WatchEvent = struct {
index: WatchItemIndex,
op: Op,
name_off: u8 = 0,
name_len: u8 = 0,
pub fn names(this: WatchEvent, buf: []?[:0]u8) []?[:0]u8 {
if (this.name_len == 0) return &[_]?[:0]u8{};
return buf[this.name_off..][0..this.name_len];
}
pub const Sorter = void;
pub fn sortByIndex(_: Sorter, event: WatchEvent, rhs: WatchEvent) bool {
return event.index < rhs.index;
}
pub fn merge(this: *WatchEvent, other: WatchEvent) void {
this.name_len += other.name_len;
this.op = Op.merge(this.op, other.op);
}
pub const Op = packed struct(u8) {
delete: bool = false,
metadata: bool = false,
rename: bool = false,
write: bool = false,
move_to: bool = false,
create: bool = false,
_padding: u2 = 0,
pub fn merge(before: Op, after: Op) Op {
return .{
.delete = before.delete or after.delete,
.write = before.write or after.write,
.metadata = before.metadata or after.metadata,
.rename = before.rename or after.rename,
.move_to = before.move_to or after.move_to,
.create = before.create or after.create,
};
}
pub fn format(op: Op, w: *std.Io.Writer) !void {
try w.writeAll("{");
var first = true;
inline for (comptime std.meta.fieldNames(Op)) |name| {
if (comptime std.mem.eql(u8, name, "_padding")) continue;
if (@field(op, name)) {
if (!first) {
try w.writeAll(",");
}
first = false;
try w.writeAll(name);
}
}
try w.writeAll("}");
}
};
};
pub const WatchItem = struct {
file_path: string,
// filepath hash for quick comparison
hash: u32,
loader: options.Loader,
fd: bun.FileDescriptor,
count: u32,
parent_hash: u32,
kind: Kind,
package_json: ?*PackageJSON,
eventlist_index: if (Environment.isLinux) Platform.EventListIndex else u0 = 0,
pub const Kind = enum { file, directory };
};
fn threadMain(this: *Watcher) !void {
this.watchloop_handle = std.Thread.getCurrentId();
this.thread_lock.lock();
Output.Source.configureNamedThread("File Watcher");
defer Output.flush();
log("Watcher started", .{});
switch (this.watchLoop()) {
.err => |err| {
this.watchloop_handle = null;
this.platform.stop();
if (this.running) {
this.onError(this.ctx, err);
}
},
.result => {},
}
// deinit and close descriptors if needed
if (this.close_descriptors) {
const fds = this.watchlist.items(.fd);
for (fds) |fd| {
fd.close();
}
}
this.watchlist.deinit(this.allocator);
// Close trace file if open
WatcherTrace.deinit();
const allocator = this.allocator;
allocator.destroy(this);
}
pub fn flushEvictions(this: *Watcher) void {
if (this.evict_list_i == 0) return;
defer this.evict_list_i = 0;
// swapRemove messes up the order
// But, it only messes up the order if any elements in the list appear after the item being removed
// So if we just sort the list by the biggest index first, that should be fine
std.sort.insertion(
WatchItemIndex,
this.evict_list[0..this.evict_list_i],
{},
comptime std.sort.desc(WatchItemIndex),
);
var slice = this.watchlist.slice();
const fds = slice.items(.fd);
var last_item = no_watch_item;
for (this.evict_list[0..this.evict_list_i]) |item| {
// catch duplicates, since the list is sorted, duplicates will appear right after each other
if (item == last_item) continue;
if (!Environment.isWindows) {
// on mac and linux we can just close the file descriptor
// we don't need to call inotify_rm_watch on linux because it gets removed when the file descriptor is closed
if (fds[item].isValid()) {
fds[item].close();
}
}
last_item = item;
}
last_item = no_watch_item;
// This is split into two passes because reading the slice while modified is potentially unsafe.
for (this.evict_list[0..this.evict_list_i]) |item| {
if (item == last_item or this.watchlist.len <= item) continue;
this.watchlist.swapRemove(item);
last_item = item;
}
}
fn watchLoop(this: *Watcher) bun.sys.Maybe(void) {
while (this.running) {
// individual platform implementation will call onFileUpdate
switch (Platform.watchLoopCycle(this)) {
.err => |err| return .{ .err = err },
.result => |iter| iter,
}
}
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,
);
}
fn appendFileAssumeCapacity(
this: *Watcher,
fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
loader: options.Loader,
parent_hash: HashType,
package_json: ?*PackageJSON,
comptime clone_file_path: bool,
) bun.sys.Maybe(void) {
if (comptime Environment.isWindows) {
// on windows we can only watch items that are in the directory tree of the top level dir
const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path);
if (rel == .unrelated) {
Output.warn("File {s} is not in the project directory and will not be watched\n", .{file_path});
return .success;
}
}
const watchlist_id = this.watchlist.len;
const file_path_: string = if (comptime clone_file_path)
bun.asByteSlice(bun.handleOom(this.allocator.dupeZ(u8, file_path)))
else
file_path;
var item = WatchItem{
.file_path = file_path_,
.fd = fd,
.hash = hash,
.count = 0,
.loader = loader,
.parent_hash = parent_hash,
.package_json = package_json,
.kind = .file,
};
if (comptime Environment.isMac) {
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;
// bun.copy(u8, &buf, file_path_to_use_);
// buf[file_path_to_use_.len] = 0;
var buf = file_path_.ptr;
const slice: [:0]const u8 = buf[0..file_path_.len :0];
item.eventlist_index = switch (this.platform.watchPath(slice)) {
.err => |err| return .{ .err = err },
.result => |r| r,
};
}
this.watchlist.appendAssumeCapacity(item);
return .success;
}
fn appendDirectoryAssumeCapacity(
this: *Watcher,
stored_fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
comptime clone_file_path: bool,
) bun.sys.Maybe(WatchItemIndex) {
if (comptime Environment.isWindows) {
// on windows we can only watch items that are in the directory tree of the top level dir
const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path);
if (rel == .unrelated) {
Output.warn("Directory {s} is not in the project directory and will not be watched\n", .{file_path});
return .{ .result = no_watch_item };
}
}
const fd = brk: {
if (stored_fd.isValid()) break :brk stored_fd;
break :brk switch (bun.sys.openA(file_path, 0, 0)) {
.err => |err| return .{ .err = err },
.result => |fd| fd,
};
};
const file_path_: string = if (comptime clone_file_path)
bun.asByteSlice(bun.handleOom(this.allocator.dupeZ(u8, file_path)))
else
file_path;
const parent_hash = getHash(bun.fs.PathName.init(file_path_).dirWithTrailingSlash());
const watchlist_id = this.watchlist.len;
var item = WatchItem{
.file_path = file_path_,
.fd = fd,
.hash = hash,
.count = 0,
.loader = options.Loader.file,
.parent_hash = parent_hash,
.kind = .directory,
.package_json = null,
};
if (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;
// monitor:
// - Write
// - Rename
// - Delete
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,
);
} else if (Environment.isLinux) {
const buf = bun.path_buffer_pool.get();
defer {
bun.path_buffer_pool.put(buf);
}
const path: [:0]const u8 = if (clone_file_path and file_path_.len > 0 and file_path_[file_path_.len - 1] == 0)
file_path_[0 .. file_path_.len - 1 :0]
else brk: {
const trailing_slash = if (file_path_.len > 1) std.mem.trimRight(u8, file_path_, &.{ 0, '/' }) else file_path_;
@memcpy(buf[0..trailing_slash.len], trailing_slash);
buf[trailing_slash.len] = 0;
break :brk buf[0..trailing_slash.len :0];
};
item.eventlist_index = switch (this.platform.watchDir(path)) {
.err => |err| return .{ .err = err.withPath(file_path) },
.result => |r| r,
};
}
this.watchlist.appendAssumeCapacity(item);
return .{
.result = @as(WatchItemIndex, @truncate(this.watchlist.len - 1)),
};
}
// Below is platform-independent
pub fn appendFileMaybeLock(
this: *Watcher,
fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
loader: options.Loader,
dir_fd: bun.FileDescriptor,
package_json: ?*PackageJSON,
comptime clone_file_path: bool,
comptime lock: bool,
) bun.sys.Maybe(void) {
if (comptime lock) this.mutex.lock();
defer if (comptime lock) this.mutex.unlock();
bun.assert(file_path.len > 1);
const pathname = bun.fs.PathName.init(file_path);
const parent_dir = pathname.dirWithTrailingSlash();
const parent_dir_hash: HashType = getHash(parent_dir);
var parent_watch_item: ?WatchItemIndex = null;
const autowatch_parent_dir = (comptime FeatureFlags.watch_directories) and this.isEligibleDirectory(parent_dir);
if (autowatch_parent_dir) {
var watchlist_slice = this.watchlist.slice();
if (dir_fd.isValid()) {
const fds = watchlist_slice.items(.fd);
if (std.mem.indexOfScalar(bun.FileDescriptor, fds, dir_fd)) |i| {
parent_watch_item = @as(WatchItemIndex, @truncate(i));
}
}
if (parent_watch_item == null) {
const hashes = watchlist_slice.items(.hash);
if (std.mem.indexOfScalar(HashType, hashes, parent_dir_hash)) |i| {
parent_watch_item = @as(WatchItemIndex, @truncate(i));
}
}
}
bun.handleOom(this.watchlist.ensureUnusedCapacity(this.allocator, 1 + @as(usize, @intCast(@intFromBool(parent_watch_item == null)))));
if (autowatch_parent_dir) {
parent_watch_item = parent_watch_item orelse switch (this.appendDirectoryAssumeCapacity(dir_fd, parent_dir, parent_dir_hash, clone_file_path)) {
.err => |err| return .{ .err = err.withPath(parent_dir) },
.result => |r| r,
};
}
switch (this.appendFileAssumeCapacity(
fd,
file_path,
hash,
loader,
parent_dir_hash,
package_json,
clone_file_path,
)) {
.err => |err| return .{ .err = err.withPath(file_path) },
.result => {},
}
if (DebugLogScope.isVisible()) {
const cwd_len_with_slash = if (this.cwd[this.cwd.len - 1] == '/') this.cwd.len else this.cwd.len + 1;
log("<d>Added <b>{s}<r><d> to watch list.<r>", .{
if (file_path.len > cwd_len_with_slash and bun.strings.startsWith(file_path, this.cwd))
file_path[cwd_len_with_slash..]
else
file_path,
});
}
return .success;
}
inline fn isEligibleDirectory(this: *Watcher, dir: string) bool {
return strings.contains(dir, this.fs.top_level_dir) and !strings.contains(dir, "node_modules");
}
pub fn appendFile(
this: *Watcher,
fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
loader: options.Loader,
dir_fd: bun.FileDescriptor,
package_json: ?*PackageJSON,
comptime clone_file_path: bool,
) bun.sys.Maybe(void) {
return appendFileMaybeLock(this, fd, file_path, hash, loader, dir_fd, package_json, clone_file_path, true);
}
pub fn addDirectory(
this: *Watcher,
fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
comptime clone_file_path: bool,
) bun.sys.Maybe(WatchItemIndex) {
this.mutex.lock();
defer this.mutex.unlock();
if (this.indexOf(hash)) |idx| {
return .{ .result = @truncate(idx) };
}
bun.handleOom(this.watchlist.ensureUnusedCapacity(this.allocator, 1));
return this.appendDirectoryAssumeCapacity(fd, file_path, hash, clone_file_path);
}
/// Lazily watch a file by path (slow path).
///
/// This function is used when a file needs to be watched but was not
/// encountered during the normal import graph traversal. On macOS, it
/// opens a file descriptor with O_EVTONLY to obtain an inode reference.
///
/// Thread-safe: uses internal locking to prevent race conditions.
///
/// Returns:
/// - true if the file is successfully added to the watchlist or already watched
/// - false if the file cannot be opened or added to the watchlist
pub fn addFileByPathSlow(
this: *Watcher,
file_path: string,
loader: options.Loader,
) bool {
if (file_path.len == 0) return false;
const hash = getHash(file_path);
// 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;
}
}
// 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) and 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();
}
}
return true;
},
.err => {
if (fd.isValid()) fd.close();
return false;
},
}
}
pub fn addFile(
this: *Watcher,
fd: bun.FileDescriptor,
file_path: string,
hash: HashType,
loader: options.Loader,
dir_fd: bun.FileDescriptor,
package_json: ?*PackageJSON,
comptime clone_file_path: bool,
) bun.sys.Maybe(void) {
// This must lock due to concurrent transpiler
this.mutex.lock();
defer this.mutex.unlock();
if (this.indexOf(hash)) |index| {
if (comptime FeatureFlags.atomic_file_watcher) {
// On Linux, the file descriptor might be out of date.
if (fd.isValid()) {
var fds = this.watchlist.items(.fd);
fds[index] = fd;
}
}
return .success;
}
return this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, clone_file_path, false);
}
pub fn indexOf(this: *Watcher, hash: HashType) ?u32 {
for (this.watchlist.items(.hash), 0..) |other, i| {
if (hash == other) {
return @as(u32, @truncate(i));
}
}
return null;
}
pub fn remove(this: *Watcher, hash: HashType) void {
this.mutex.lock();
defer this.mutex.unlock();
if (this.indexOf(hash)) |index| {
this.removeAtIndex(@truncate(index), hash, &[_]HashType{}, .file);
}
}
pub fn removeAtIndex(this: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void {
bun.assert(index != no_watch_item);
this.evict_list[this.evict_list_i] = index;
this.evict_list_i += 1;
if (comptime kind == .directory) {
for (parents) |parent| {
if (parent == hash) {
this.evict_list[this.evict_list_i] = @as(WatchItemIndex, @truncate(parent));
this.evict_list_i += 1;
}
}
}
}
pub fn getResolveWatcher(watcher: *Watcher) bun.resolver.AnyResolveWatcher {
return bun.resolver.ResolveWatcher(*@This(), onMaybeWatchDirectory).init(watcher);
}
pub fn onMaybeWatchDirectory(watch: *Watcher, file_path: string, dir_fd: bun.StoredFileDescriptorType) void {
// We don't want to watch:
// - Directories outside the root directory
// - Directories inside node_modules
if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) {
_ = watch.addDirectory(dir_fd, file_path, getHash(file_path), false);
}
}
const string = []const u8;
const WatcherTrace = @import("./watcher/WatcherTrace.zig");
const WindowsWatcher = @import("./watcher/WindowsWatcher.zig");
const options = @import("./options.zig");
const std = @import("std");
const PackageJSON = @import("./resolver/package_json.zig").PackageJSON;
const bun = @import("bun");
const Environment = bun.Environment;
const FeatureFlags = bun.FeatureFlags;
const Mutex = bun.Mutex;
const Output = bun.Output;
const strings = bun.strings;