npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nds-stack/bun-watcher

v0.1.1

Published

File system watcher for Bun — recursive watching, debounce, glob filtering, zero dependencies

Readme

@nds-stack/bun-watcher

File system watcher for Bun — recursive watching, debounce, glob filtering, zero dependencies.

npm version Bun TypeScript License


Why bun-watcher

Bun doesn't have a built-in recursive file watcher with debounce and glob filtering. The alternative — chokidar — is the gold standard for Node.js file watching but includes polyfills for Node.js APIs that Bun doesn't need.

bun-watcher is a lightweight, Bun-native alternative:

  • Zero npm dependencies — uses Bun's native fs.watch() with recursive support
  • Chokidar-compatible API — drop-in replacement for most use cases
  • Native OS events — uses kqueue/inotify/ReadDirectoryChangesW, no polling
  • Debounced — batches rapid events within a configurable window
  • Binary detection — skips binary files by default for cleaner output
import { watch } from "@nds-stack/bun-watcher";

const w = watch("./src", { ignored: ["node_modules"] });

w.on("all", (event, path) => console.log(event, path));
w.on("ready", () => console.log("Ready!"));

Installation

bun add @nds-stack/bun-watcher

API

watch(paths, options?)

Creates a watcher and starts scanning immediately. Returns a Watcher instance.

| Param | Type | Default | Description | |-------|------|---------|-------------| | paths | string \| string[] | — | File or directory path(s) to watch | | options.ignored | (string \| RegExp)[] | [] | Ignore patterns. Strings match if path includes them; RegExp tests the path. | | options.ignoreInitial | boolean | false | If true, skip add events for files that exist at watch start. | | options.recursive | boolean | true | Watch subdirectories recursively. | | options.delay | number | 100 | Debounce delay in ms for coalescing rapid change events. Minimum 0 (no debounce). | | options.maxDepth | number | Infinity | Maximum directory depth to scan. | | options.ignoreBinary | boolean | true | Skip files with binary extensions (images, videos, archives, binaries, etc.). | | options.followSymlinks | boolean | false | Follow symbolic links when scanning. | | options.alwaysStat | boolean | false | Include { size, mtime } in add and change events. | | options.awaitWriteFinish | boolean \| object | false | Wait for file writes to settle before emitting change. Pass true or { stabilityThreshold: 500, pollInterval: 100 }. |

Events

| Event | Signature | Description | |-------|-----------|-------------| | all | (event, path, stat?) | Catch-all. stat provided when alwaysStat: true. | | add | (path, stat?) | New file detected. stat provided when alwaysStat: true. | | change | (path, stat?) | File modified (debounced). stat provided when alwaysStat: true. | | unlink | (path) | File deleted | | ready | () | Initial scan complete | | error | (err: Error) | Watcher error |

Methods

| Method | Description | |--------|-------------| | on(event, handler) | Register event handler. Returns this for chaining. | | off(event, handler) | Remove event handler. | | close() | Stop watching, clear timers, remove all listeners. |


How It Works

bun-watcher uses native OS file system events via fs.watch() with recursive: true:

  1. Initial scan: Bun.Glob("**/*") discovers existing files for baseline tracking
  2. File tracking: All known files are stored in a Map<string, FileStat> for tracking file metadata
  3. Native events: fs.watch (backed by kqueue/inotify/ReadDirectoryChangesW) reports file additions, deletions, and modifications in real-time
  4. Add/unlink detection: When a rename event fires, the file's existence is checked to distinguish creation from deletion
  5. Debounce: change events are delayed by delay ms. If another change happens within the window, the timer resets.
  6. Binary filtering: File extension is checked against a set of ~50 known binary extensions before processing.

This approach is fast, CPU-efficient, and cross-platform — zero overhead when no files are changing.


Error Handling

bun-watcher handles errors gracefully:

  • Watch errors: If a directory doesn't exist or is inaccessible, an error event is emitted (but the watcher continues for other paths)
  • Handler errors: Errors in event handlers are silently caught — they won't crash the watcher
  • File stat errors: If a file disappears during a scan cycle, it's silently skipped
  • close() safety: Calling close() multiple times is safe — it clears all state
const w = watch("/some/path");

w.on("error", (err) => {
  console.error("Watcher error (non-fatal):", err.message);
});

Limitations

  • Initial scan overhead: Bun.Glob("**/*") runs once at start to discover existing files. For directories with >10K files, use ignoreInitial: true to skip initial events.
  • Symlinks: Symlinks are not followed. Only regular files are tracked.
  • Atomic writes: Writes that replace a file atomically (write to temp then rename) are detected as an unlink of the old file + add of the new file.
  • No stat info by default: Without alwaysStat: true, only file path events are reported. Enable alwaysStat to receive { size, mtime } in add/change events.
  • Cross-platform semantics: Different OS kernels report events slightly differently. fs.watch's rename event may fire for both creates and deletes — the watcher uses an internal Set to distinguish between add and unlink.

Multi-Instance / Cross-Boundary

bun-watcher is per-process. In multi-instance deployments, each instance maintains its own file set. This is fine for most use cases since file watching is inherently local to a process.

For distributed file watching (e.g., watching the same NFS mount from multiple servers), combine with a message queue or shared state:

import { watch } from "@nds-stack/bun-watcher";
// import { redis } from "./redis";

const w = watch("/shared/data", { ignoreInitial: true });

w.on("change", (path) => {
  // Broadcast changes to other instances
  // redis.publish("file:changes", path);
});

Customization Guide

Custom ignore logic

Extend ignore behavior by combining patterns:

const w = watch(".", {
  ignored: [
    /\.(log|tmp)$/,        // RegExp: ignore log and tmp files
    "node_modules",          // String: ignore paths containing "node_modules"
    ".git",                  // String: ignore .git directory
  ],
});

Only watch specific extensions

const w = watch(".", {
  ignored: [/\.(?!ts$|js$)/],  // Only .ts and .js files
});

Adjust poll frequency for large projects

// For large directories, less frequent polling reduces CPU
const w = watch(".", {
  delay: 1000,    // Poll every 1 second instead of 100ms
  maxDepth: 3,    // Only scan 3 levels deep
});

Mix with other event sources

import { watch } from "@nds-stack/bun-watcher";

class BuildWatcher {
  private w = watch("./src", { ignored: ["node_modules"] });

  constructor() {
    this.w.on("change", (path) => this.onChange(path));
    this.w.on("error", (err) => console.error(err));
  }

  private onChange(path: string): void {
    console.log(`Rebuilding due to: ${path}`);
    // Trigger build...
  }

  close(): void {
    this.w.close();
  }
}

Comparison Table

| Feature | chokidar (npm) | @rabbx/watcher (npm) | bun-watcher | |---------|----------------|----------------------|-------------| | Native OS watcher | ✅ kqueue, inotify, FSEvents, polling | 🔄 auto-fallback | ✅ kqueue, inotify, ReadDirectoryChangesW | | Cross-platform | ✅ Node.js, Bun, Deno | ✅ Node.js, Bun, Deno | ✅ Bun | | Debounce | ✅ | ✅ | ✅ | | Glob filtering | ✅ (anymatch) | ✅ (include/exclude) | ✅ (string & RegExp) | | ignoreInitial | ✅ | ✅ | ✅ | | Binary detection | ❌ | ✅ | ✅ | | Max depth | ❌ | ✅ | ✅ | | Zero dependencies | ❌ (~15 deps) | ✅ | ✅ | | Bun-native | ❌ (polyfills) | ✅ | ✅ | | TypeScript types | ✅ | ✅ | ✅ | | Bundle size | ~200KB + deps | ~3KB | ~3KB |


Benchmarks

Methodology

  • Initial scan of 50 files on disk
  • Warmup: 1 cycle before measurement
  • Hardware: Bun v1.3.14 (Windows 11)

Results (ops/s — higher is better)

| Operation | bun-watcher | chokidar | @rabbx/watcher | Native Bun.Glob | |-----------|:-------------:|:----------:|:----------------:|:-----------------:| | Initial scan (50 files) | 8K | 492 | 31K^ | 135K 🥇 |

^ @rabbx/watcher fires "ready" before native watcher is set up (via microtask before async import("node:fs") resolves). bun-watcher fires "ready" only after both initial scan AND native watcher are fully ready.

Run bun run bench on your hardware to reproduce.


Real-World Example

import { watch } from "@nds-stack/bun-watcher";

interface BuildEvent {
  type: "add" | "change" | "unlink";
  path: string;
  timestamp: number;
}

class DevServer {
  private watcher = watch("./src", {
    ignored: ["node_modules", ".git", /\.(test|spec)\.(ts|js)$/],
    ignoreInitial: true,
    delay: 150,
  });

  private buildQueue: BuildEvent[] = [];
  private buildTimer: Timer | null = null;

  constructor() {
    this.watcher.on("all", (event, path) => {
      this.buildQueue.push({ type: event, path, timestamp: Date.now() });
      this.scheduleBuild();
    });
    this.watcher.on("ready", () => console.log("Watching for changes..."));
    this.watcher.on("error", (err) => console.error("Watcher error:", err));
  }

  private scheduleBuild(): void {
    if (this.buildTimer) clearTimeout(this.buildTimer);
    this.buildTimer = setTimeout(() => this.runBuild(), 500);
  }

  private async runBuild(): Promise<void> {
    const events = [...this.buildQueue];
    this.buildQueue = [];
    console.log(`Rebuilding (${events.length} change(s))...`);
    // Run build...
  }

  close(): void {
    this.watcher.close();
  }
}

License

MIT