typescriptintermediate

Node.js Recursive File Watcher

Watch files and directories for changes with debouncing and glob pattern filtering.

typescript
import { watch, stat } from 'node:fs/promises';
import { join, extname, relative } from 'node:path';

interface WatchOptions {
  dir: string;
  extensions?: string[];
  ignore?: string[];
  debounceMs?: number;
  onChange: (event: string, path: string) => void;
}

async function watchDirectory(opts: WatchOptions) {
  const { dir, extensions, ignore = ['node_modules', '.git'], debounceMs = 300, onChange } = opts;
  const timers = new Map<string, NodeJS.Timeout>();

  console.log(`Watching ${dir} for changes...`);

  const watcher = watch(dir, { recursive: true });

  for await (const event of watcher) {
    const filename = event.filename;
    if (!filename) continue;

    // Skip ignored paths
    if (ignore.some((i) => filename.includes(i))) continue;

    // Filter by extension
    if (extensions && !extensions.includes(extname(filename))) continue;

    // Debounce rapid changes
    const existing = timers.get(filename);
    if (existing) clearTimeout(existing);

    timers.set(
      filename,
      setTimeout(() => {
        timers.delete(filename);
        onChange(event.eventType ?? 'change', join(dir, filename));
      }, debounceMs)
    );
  }
}

// Usage
awatchDirectory({
  dir: './src',
  extensions: ['.ts', '.tsx', '.json'],
  ignore: ['node_modules', '.git', 'dist'],
  debounceMs: 200,
  onChange: (event, path) => {
    console.log(`[${event}] ${path}`);
    // Trigger rebuild, test run, etc.
  },
});

Use Cases

  • Custom hot-reload during development
  • File-based trigger for build automation
  • Monitoring config file changes

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.