Better file system watching for Node.js with normalized APIs and accurate detailed events
npx @tessl/cli install tessl/npm-watchr@6.11.0Watchr provides a normalized API for file system watching across different Node.js versions. It supports nested/recursive file and directory watching with accurate detailed events for file/directory creations, updates, and deletions. The library offers two main concepts: Watcher (wraps native file system watching with reliability and deep watching support) and Stalker (manages multiple watchers for the same path efficiently).
npm install watchrconst { open, create, Stalker, Watcher } = require('watchr');For destructuring specific components:
const { open } = require('watchr'); // Simple usage
const { create } = require('watchr'); // Advanced usage
const { Stalker, Watcher } = require('watchr'); // Direct class accessSimple file watching with the convenience function:
const watchr = require('watchr');
// Define change listener
function listener(changeType, fullPath, currentStat, previousStat) {
switch (changeType) {
case 'update':
console.log('File updated:', fullPath);
break;
case 'create':
console.log('File created:', fullPath);
break;
case 'delete':
console.log('File deleted:', fullPath);
break;
}
}
// Start watching with error handling
function next(err) {
if (err) return console.log('Watch failed:', err);
console.log('Watch successful');
}
// Watch the current directory
const stalker = watchr.open(process.cwd(), listener, next);
// Stop watching when done
stalker.close();Advanced usage with configuration:
const { create } = require('watchr');
const stalker = create('/path/to/watch');
// Configure watching options
stalker.setConfig({
stat: null,
interval: 5007,
persistent: true,
catchupDelay: 2000,
preferredMethods: ['watch', 'watchFile'],
followLinks: true,
ignorePaths: false,
ignoreHiddenFiles: false,
ignoreCommonPatterns: true,
ignoreCustomPatterns: null
});
// Set up event listeners
stalker.on('change', listener);
stalker.on('log', console.log);
stalker.once('close', (reason) => {
console.log('Watcher closed:', reason);
});
// Start watching
stalker.watch(next);Watchr uses a two-layer architecture:
fs.watch, automatically falls back to fs.watchFile if neededQuick setup for basic file watching needs using the convenience function.
/**
* Alias for creating a new Stalker with basic configuration and immediate watching
* @param {string} path - The path to watch
* @param {function} changeListener - The change listener for the Watcher
* @param {function} next - The completion callback for Watcher#watch
* @returns {Stalker} stalker instance
*/
function open(path, changeListener, next);Create stalkers with full configuration control for complex watching scenarios.
/**
* Alias for creating a new Stalker instance
* @param {...any} args - Arguments passed to Stalker constructor
* @returns {Stalker} stalker instance
*/
function create(...args);The Stalker class manages multiple watchers for the same path, ensuring efficient resource usage.
/**
* A watcher of the watchers. Events listened to on the stalker are proxied to the attached watcher.
* When the watcher is closed, the stalker's listeners are removed.
* When all stalkers for a watcher are removed, the watcher will close.
*/
class Stalker extends EventEmitter {
/**
* Create a new Stalker for the given path
* @param {string} path - The path to watch
*/
constructor(path);
/**
* Close the stalker, and if it is the last stalker for the path, close the watcher too
* @param {string} [reason] - Optional reason to provide for closure
* @returns {Stalker} this instance for chaining
*/
close(reason);
/**
* Configure the underlying watcher with various options
* @param {...any} args - Arguments passed to watcher's setConfig method
* @returns {Stalker} this instance for chaining
*/
setConfig(...args);
/**
* Start watching the path and its children
* @param {...any} args - Arguments passed to watcher's watch method
* @returns {Stalker} this instance for chaining
*/
watch(...args);
}Direct access to the Watcher class for specialized use cases.
/**
* Watches a path and if it's a directory, its children too.
* Emits change events for updates, deletions, and creations.
*/
class Watcher extends EventEmitter {
/**
* Create a new Watcher for the given path
* @param {string} path - The path to watch
*/
constructor(path);
/**
* Configure the Watcher with various options
* @param {WatcherOpts} opts - Configuration options
* @returns {Watcher} this instance for chaining
*/
setConfig(opts);
/**
* Setup watching for the path and its children
* @param {ResetOpts} [opts] - Watch options
* @param {function} next - Completion callback with signature (error) => void
* @returns {Watcher} this instance for chaining
*/
watch(opts, next);
watch(next);
/**
* Close the watching abilities of this watcher and its children
* @param {string} [reason='unknown'] - Reason for closure
* @returns {Watcher} this instance for chaining
*/
close(reason);
/**
* Get the stat for the path of the watcher
* @param {ResetOpts} opts - Options
* @param {function} next - Callback with signature (error, stat) => void
* @returns {Watcher} this instance for chaining
*/
getStat(opts, next);
/**
* Emit a log event with the given arguments
* @param {...any} args - Arguments for logging
* @returns {Watcher} this instance for chaining
*/
log(...args);
}Both Stalker and Watcher classes extend EventEmitter and emit the following events:
/**
* Emitted when a file or directory change is detected
* @param {string} changeType - 'update', 'create', or 'delete'
* @param {string} fullPath - Full path to the changed file/directory
* @param {Stats|null} currentStat - Current Stats object (null for deletions)
* @param {Stats|null} previousStat - Previous Stats object (null for creations)
*/
stalker.on('change', (changeType, fullPath, currentStat, previousStat) => {
// Handle the change
});/**
* Emitted when the watcher is closed
* @param {string} reason - String describing why the watcher was closed
*/
stalker.on('close', (reason) => {
// Handle closure
});/**
* Emitted for debugging information
* @param {string} logLevel - Log level string
* @param {...any} args - Additional logging arguments
*/
stalker.on('log', (logLevel, ...args) => {
// Handle log message
});/**
* Emitted when an error occurs
* @param {Error} error - Error object
*/
stalker.on('error', (error) => {
// Handle error
});/**
* Configuration options for Watcher
*/
interface WatcherOpts {
/** Pre-existing stat object for the path */
stat?: Stats;
/** Polling interval for watchFile method (default: 5007) */
interval?: number;
/** Whether watching should keep the process alive (default: true) */
persistent?: boolean;
/** Delay after change events for accurate detection (default: 2000) */
catchupDelay?: number;
/** Order of watch methods to attempt (default: ['watch', 'watchFile']) */
preferredMethods?: Array<'watch' | 'watchFile'>;
/** Whether to follow symlinks (default: true) */
followLinks?: boolean;
/** Array of paths to ignore or false to ignore none (default: false) */
ignorePaths?: Array<string> | false;
/** Whether to ignore files/dirs starting with '.' (default: false) */
ignoreHiddenFiles?: boolean;
/** Whether to ignore common patterns like .git, .svn (default: true) */
ignoreCommonPatterns?: boolean;
/** Custom regex for ignoring paths */
ignoreCustomPatterns?: RegExp;
}
/**
* Options for watch operations
*/
interface ResetOpts {
/** Whether to close existing watchers and setup new ones (default: false) */
reset?: boolean;
}Watchr provides comprehensive error handling through events and callbacks:
const stalker = create('/path/to/watch');
// Handle errors through events
stalker.on('error', (error) => {
console.error('Watch error:', error.message);
// Implement error recovery logic
});
// Handle errors in watch callback
stalker.watch((error) => {
if (error) {
console.error('Failed to start watching:', error.message);
return;
}
console.log('Watching started successfully');
});Common error scenarios:
fs.watch and fs.watchFile failWatching with custom ignore patterns:
const stalker = create('/project/directory');
stalker.setConfig({
ignoreHiddenFiles: true,
ignoreCommonPatterns: true,
ignoreCustomPatterns: /\.(log|tmp)$/,
ignorePaths: ['/project/directory/node_modules']
});
stalker.on('change', (changeType, fullPath) => {
console.log(`${changeType}: ${fullPath}`);
});
stalker.watch((err) => {
if (err) throw err;
console.log('Watching project directory...');
});Watching with specific method preference:
const stalker = create('/slow/network/path');
// Prefer polling for network paths
stalker.setConfig({
preferredMethods: ['watchFile'],
interval: 2000, // Check every 2 seconds
persistent: false // Don't keep process alive
});
stalker.watch((err) => {
if (err) throw err;
console.log('Polling network path...');
});Multiple watchers on same path:
// Multiple stalkers can watch the same path efficiently
const stalker1 = create('/shared/path');
const stalker2 = create('/shared/path');
stalker1.on('change', (type, path) => {
console.log('Stalker 1 detected:', type, path);
});
stalker2.on('change', (type, path) => {
console.log('Stalker 2 detected:', type, path);
});
// Only one underlying watcher is created
stalker1.watch(() => console.log('Stalker 1 ready'));
stalker2.watch(() => console.log('Stalker 2 ready'));