Utilities for watching file trees in Node.js applications with EventEmitter patterns and CLI support
npx @tessl/cli install tessl/npm-watch@1.0.0Watch provides utilities for watching file trees in Node.js applications. It offers two primary interfaces: watchTree for simple callback-based file monitoring, and createMonitor for event-driven monitoring with EventEmitter patterns. The library supports comprehensive filtering options and includes a command-line interface for automated command execution on file changes.
npm install watchconst watch = require("watch");For individual imports:
const { watchTree, unwatchTree, createMonitor, walk } = require("watch");const watch = require("watch");
// Simple file watching with callback
watch.watchTree("/path/to/watch", function (f, curr, prev) {
if (typeof f == "object" && prev === null && curr === null) {
// Finished walking the tree
console.log("Watching", Object.keys(f).length, "files");
} else if (prev === null) {
// New file
console.log("New file:", f);
} else if (curr.nlink === 0) {
// File removed
console.log("Removed:", f);
} else {
// File changed
console.log("Changed:", f);
}
});
// Event-driven monitoring
watch.createMonitor("/path/to/watch", function (monitor) {
monitor.on("created", function (f, stat) {
console.log("Created:", f);
});
monitor.on("changed", function (f, curr, prev) {
console.log("Changed:", f);
});
monitor.on("removed", function (f, stat) {
console.log("Removed:", f);
});
// Stop monitoring
// monitor.stop();
});Monitor file and directory changes with customizable filtering and polling options.
/**
* Watch a directory tree for file changes
* @param {string} root - Directory path to watch
* @param {WatchOptions|WatchCallback} [options] - Watch configuration options (optional)
* @param {WatchCallback} callback - Called on file changes and setup completion
* Note: If options is not provided, the second parameter becomes the callback
*/
function watchTree(root, options, callback);
/**
* Stop watching a directory tree
* @param {string} root - Directory path to stop watching
*/
function unwatchTree(root);
interface WatchOptions {
/** Skip files starting with "." */
ignoreDotFiles?: boolean;
/** Function to filter files/directories (return true to include) */
filter?: (filepath: string, stat: Stats) => boolean;
/** Polling interval in seconds */
interval?: number;
/** Skip unreadable directories silently (handles EACCES errors) */
ignoreUnreadableDir?: boolean;
/** Skip files with permission issues silently (handles EPERM errors) */
ignoreNotPermitted?: boolean;
/** Skip directories matching this pattern */
ignoreDirectoryPattern?: RegExp;
}
/**
* Callback for watchTree file changes
* @param {string|object} f - File path or file hash object (on completion)
* @param {Stats|null} curr - Current file stats
* @param {Stats|null} prev - Previous file stats (null for new files)
*/
type WatchCallback = (f: string | object, curr: Stats | null, prev: Stats | null) => void;Callback Behavior:
f is an object (file hash), curr and prev are nullprev is null, curr contains file statscurr.nlink is 0 (file's link count is zero)curr and prev contain stats, and modification times differImportant: The function uses fs.watchFile internally and only triggers callbacks for actual content changes (based on mtime comparison), not just access times.
Create an EventEmitter-based monitor for advanced file system event handling.
/**
* Create an EventEmitter-based file monitor
* @param {string} root - Directory path to monitor
* @param {WatchOptions|MonitorCallback} [options] - Monitor configuration options (optional)
* @param {MonitorCallback} callback - Called with monitor object when ready
* Note: If options is not provided, the second parameter becomes the callback
*/
function createMonitor(root, options, callback);
/**
* Callback for createMonitor setup completion
* @param {Monitor} monitor - The configured monitor instance
*/
type MonitorCallback = (monitor: Monitor) => void;
interface Monitor extends EventEmitter {
/** Hash of file paths to current stat objects */
files: { [filepath: string]: Stats };
/** Stop monitoring (calls unwatchTree) */
stop(): void;
}Monitor Events:
'created' - New file created: (filename: string, stat: Stats)'removed' - File removed: (filename: string, stat: Stats)'changed' - File modified: (filename: string, curr: Stats, prev: Stats)Note: The monitor includes deduplication logic to prevent duplicate events for the same file and action type within rapid succession.
Recursively walk a directory tree and build a file hash without watching.
/**
* Recursively walk a directory tree
* @param {string} dir - Directory to walk
* @param {WalkOptions|WalkCallback} [options] - Walk configuration options (optional)
* @param {WalkCallback} callback - Called with results when complete
* Note: If options is not provided, the second parameter becomes the callback
*/
function walk(dir, options, callback);
interface WalkOptions {
/** Skip files starting with "." */
ignoreDotFiles?: boolean;
/** Function to filter files/directories (return true to include) */
filter?: (filepath: string, stat: Stats) => boolean;
/** Skip unreadable directories silently (handles EACCES errors) */
ignoreUnreadableDir?: boolean;
/** Skip files with permission issues silently (handles EPERM errors) */
ignoreNotPermitted?: boolean;
/** Skip directories matching this pattern */
ignoreDirectoryPattern?: RegExp;
}
/**
* Callback for walk completion
* @param {Error|null} err - Error if walk failed
* @param {object} files - Hash of file paths to stat objects
*/
type WalkCallback = (err: Error | null, files: { [filepath: string]: Stats }) => void;Execute commands automatically when files change, with throttling and filtering support. Install globally with npm install watch -g to use the watch command anywhere.
# Basic usage
watch <command> [...directory]
# With options
watch "npm test" ./src --wait=2 --ignoreDotFiles
# Available options:
--wait=<seconds>, -w # Throttle command execution (seconds)
--filter=<file>, -f # Path to filter function module (exports filter function)
--interval=<seconds>, -i # Polling interval (default: 0.2)
--ignoreDotFiles, -d # Ignore dot files
--ignoreUnreadable, -u # Ignore unreadable directories
--ignoreDirectoryPattern=<regexp>, -p # Ignore matching directoriesThe CLI tool watches specified directories (defaults to current working directory) and executes the given command whenever files change. The --wait option prevents rapid repeated executions.
Filter Module Example:
The --filter option accepts a path to a JavaScript file that exports a filter function:
// myfilter.js
module.exports = function (filepath, stat) {
// Return true to include file in watching, false to exclude
return stat.isDirectory() || filepath.match(/\.(js|ts|json)$/);
};Then use: watch "npm test" ./src --filter=./myfilter.js
/**
* Node.js fs.Stats object containing file metadata
*/
interface Stats {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
size: number;
mtime: Date;
nlink: number;
// ... other fs.Stats properties
}
/**
* Filter function type for determining which files to include
* @param {string} filepath - Full path to file or directory
* @param {Stats} stat - File system stats object
* @returns {boolean} True to include file in watching/walking
*/
type FilterFunction = (filepath: string, stat: Stats) => boolean;const watch = require("watch");
// Custom filter function
function sourceFilter(f, stat) {
return stat.isDirectory() || f.match(/\.(js|ts|json)$/);
}
watch.watchTree("./src", {
filter: sourceFilter,
ignoreDotFiles: true,
ignoreDirectoryPattern: /node_modules/
}, function (f, curr, prev) {
if (typeof f !== "object") {
console.log("Source file changed:", f);
}
});const watch = require("watch");
let monitor;
watch.createMonitor("./project", {
interval: 1,
ignoreDotFiles: true
}, function (m) {
monitor = m;
monitor.on("created", (f) => console.log("+ " + f));
monitor.on("removed", (f) => console.log("- " + f));
monitor.on("changed", (f) => console.log("~ " + f));
});
// Cleanup on exit
process.on("SIGINT", function() {
if (monitor) {
monitor.stop();
}
process.exit();
});const watch = require("watch");
watch.walk("./docs", {
ignoreDotFiles: true,
filter: (f, stat) => stat.isDirectory() || f.endsWith('.md')
}, function (err, files) {
if (err) throw err;
console.log("Found", Object.keys(files).length, "markdown files:");
Object.keys(files).forEach(file => {
if (files[file].isFile()) {
console.log("-", file);
}
});
});