fb-watchman provides Node.js bindings for Facebook's Watchman file watching service, enabling efficient filesystem monitoring and change detection in JavaScript applications. It offers a Client class that communicates with the Watchman daemon through a socket connection using the BSER (Binary Serialization) protocol, supporting advanced file querying operations and change subscriptions.
npm install fb-watchmanconst watchman = require('fb-watchman');
const client = new watchman.Client();For ES modules:
import watchman from 'fb-watchman';
const client = new watchman.Client();const watchman = require('fb-watchman');
const client = new watchman.Client();
// Handle connection events
client.on('connect', () => {
console.log('Connected to Watchman');
});
client.on('error', (error) => {
console.error('Error:', error);
});
// Check capabilities
client.capabilityCheck({ required: ['relative_root'] }, (error, resp) => {
if (error) {
console.error('Capability check failed:', error);
return;
}
console.log('Watchman version:', resp.version);
});
// Watch a project directory
client.command(['watch-project', process.cwd()], (error, resp) => {
if (error) {
console.error('Watch failed:', error);
return;
}
console.log('Watching:', resp.watch);
// Subscribe to file changes
client.command(['subscribe', resp.watch, 'mysubscription', {
expression: ['allof', ['match', '*.js']],
fields: ['name', 'size', 'exists', 'type']
}], (error, resp) => {
if (error) {
console.error('Subscribe failed:', error);
return;
}
console.log('Subscription established:', resp.subscribe);
});
});
// Listen for file change notifications
client.on('subscription', (resp) => {
console.log('Files changed:', resp.files);
});
// Clean up
process.on('exit', () => {
client.end();
});fb-watchman is built around several key components:
Creates a new Watchman client instance with optional configuration.
/**
* Creates a new Watchman client instance
* @param {Object} options - Optional configuration
* @param {string} options.watchmanBinaryPath - Absolute path to watchman binary
*/
function Client(options);Establishes connection to the Watchman service, either via existing socket or by spawning the Watchman process.
/**
* Establishes connection to Watchman service
*/
Client.prototype.connect();
/**
* Closes the connection to the Watchman service
*/
Client.prototype.end();Sends commands to the Watchman service with automatic connection and queuing.
/**
* Sends a command to the Watchman service
* @param {Array} args - Command arguments array
* @param {Function} done - Optional callback function for response
*/
Client.prototype.command(args, done);Usage Examples:
// Watch a directory
client.command(['watch-project', '/path/to/project'], (error, resp) => {
if (error) {
console.error('Watch failed:', error);
return;
}
console.log('Watching:', resp.watch);
});
// Query files
client.command(['query', '/path/to/project', {
expression: ['allof', ['type', 'f'], ['suffix', 'js']],
fields: ['name', 'size', 'mtime_ms']
}], (error, resp) => {
if (error) {
console.error('Query failed:', error);
return;
}
console.log('Files:', resp.files);
});
// Create subscription
client.command(['subscribe', '/path/to/project', 'mysubscription', {
expression: ['allof', ['match', '*.js']],
fields: ['name', 'size', 'exists', 'type']
}], (error, resp) => {
if (error) {
console.error('Subscribe failed:', error);
return;
}
console.log('Subscription:', resp.subscribe);
});
// Unsubscribe
client.command(['unsubscribe', '/path/to/project', 'mysubscription'], (error, resp) => {
if (error) {
console.error('Unsubscribe failed:', error);
return;
}
console.log('Unsubscribed from:', resp.unsubscribe);
});
// Get server version
client.command(['version'], (error, resp) => {
if (error) {
console.error('Version query failed:', error);
return;
}
console.log('Watchman version:', resp.version);
});Checks if the Watchman server supports specific capabilities, ensuring compatibility across versions.
/**
* Checks if Watchman server supports specified capabilities
* @param {Object} caps - Capability requirements
* @param {Array} caps.optional - List of optional capabilities
* @param {Array} caps.required - List of required capabilities
* @param {Function} done - Callback function for capability check results
*/
Client.prototype.capabilityCheck(caps, done);
/**
* Internal helper for testing - synthesizes capability check results for older Watchman versions
* @param {Object} resp - Version response object
* @param {Array} optional - List of optional capabilities
* @param {Array} required - List of required capabilities
* @returns {Object} Response with synthesized capabilities
*/
Client.prototype._synthesizeCapabilityCheck(resp, optional, required);Usage Examples:
// Check required capabilities
client.capabilityCheck({
required: ['relative_root', 'wildmatch']
}, (error, resp) => {
if (error) {
console.error('Missing required capabilities:', error);
return;
}
console.log('All required capabilities supported');
console.log('Capabilities:', resp.capabilities);
});
// Check optional capabilities
client.capabilityCheck({
optional: ['term-dirname', 'cmd-watch-del-all'],
required: ['relative_root']
}, (error, resp) => {
if (error) {
console.error('Capability check failed:', error);
return;
}
if (resp.capabilities['term-dirname']) {
console.log('Advanced directory matching supported');
}
if (resp.capabilities['cmd-watch-del-all']) {
console.log('Watch deletion command supported');
}
});Known Capability Requirements:
The client internally tracks minimum Watchman versions for various capabilities:
cmd-watch-del-all: Requires Watchman 3.1.1+cmd-watch-project: Requires Watchman 3.1+relative_root: Requires Watchman 3.3+term-dirname: Requires Watchman 3.1+term-idirname: Requires Watchman 3.1+wildmatch: Requires Watchman 3.7+The Client class extends EventEmitter and emits several events for handling connections and data.
/**
* Events emitted by Client instance:
*
* 'connect' - Emitted when connection to Watchman is established
* 'error' - Emitted when an error occurs (error)
* 'end' - Emitted when connection to Watchman is terminated
* 'subscription' - Emitted when receiving file change notifications (response)
* 'log' - Emitted when receiving log messages from Watchman (response)
*/Usage Examples:
// Connection lifecycle
client.on('connect', () => {
console.log('Connected to Watchman daemon');
});
client.on('end', () => {
console.log('Connection to Watchman closed');
});
client.on('error', (error) => {
console.error('Watchman error:', error);
if (error.watchmanResponse) {
console.error('Watchman response:', error.watchmanResponse);
}
});
// File change notifications
client.on('subscription', (resp) => {
console.log('Subscription:', resp.subscription);
console.log('Root directory:', resp.root);
resp.files.forEach(file => {
console.log('File changed:', file.name);
console.log('Size:', file.size);
console.log('Exists:', file.exists);
console.log('Type:', file.type);
});
});
// Log messages from Watchman
client.on('log', (resp) => {
console.log('Watchman log:', resp.log);
});/**
* Client constructor options
*/
interface ClientOptions {
/** Absolute path to the watchman binary executable */
watchmanBinaryPath?: string;
}
/**
* Capability check parameters
*/
interface CapabilityCheck {
/** List of optional capabilities to check */
optional?: string[];
/** List of required capabilities that must be supported */
required?: string[];
}
/**
* Capability check response
*/
interface CapabilityResponse {
/** Watchman server version */
version: string;
/** Map of capability names to support status */
capabilities: { [capability: string]: boolean };
/** Error message if required capabilities are missing */
error?: string;
}
/**
* Watch project response
*/
interface WatchResponse {
/** Root directory being watched */
watch: string;
/** Relative path if using existing watch at higher level */
relative_path?: string;
/** Warning message from Watchman */
warning?: string;
}
/**
* Subscription response
*/
interface SubscriptionResponse {
/** Subscription name */
subscribe: string;
/** Root directory being watched */
root: string;
/** Array of file objects matching subscription criteria */
files: FileInfo[];
/** Subscription name for notifications */
subscription: string;
}
/**
* File information object
*/
interface FileInfo {
/** File name relative to root */
name: string;
/** File size in bytes */
size?: number;
/** Whether file exists */
exists?: boolean;
/** File type ('f' for file, 'd' for directory) */
type?: string;
/** File mode/permissions */
mode?: number;
/** Modification time in milliseconds */
mtime_ms?: number;
/** Whether this is a new file */
new?: boolean;
}
/**
* Query expression for file matching
*/
type QueryExpression =
| ['allof', ...QueryExpression[]]
| ['anyof', ...QueryExpression[]]
| ['not', QueryExpression]
| ['match', string]
| ['imatch', string]
| ['pcre', string]
| ['ipcre', string]
| ['name', string]
| ['iname', string]
| ['type', 'f' | 'd' | 'l']
| ['suffix', string]
| ['dirname', string]
| ['idirname', string];
/**
* Common Watchman commands
*/
type WatchmanCommand =
| ['version']
| ['version', { optional?: string[]; required?: string[] }]
| ['watch-project', string]
| ['query', string, { expression?: QueryExpression; fields?: string[] }]
| ['subscribe', string, string, { expression?: QueryExpression; fields?: string[]; relative_root?: string; since?: string }]
| ['unsubscribe', string, string]
| ['clock', string];fb-watchman provides comprehensive error handling for various failure scenarios:
Connection Errors:
ENOENT - Watchman binary not found in PATH (with helpful installation message)EACCES - Permission denied when executing Watchman binaryCommunication Errors:
Watchman Service Errors:
Error Objects:
// Errors include watchmanResponse property when available
try {
client.command(['invalid-command'], (error, resp) => {
if (error) {
console.error('Command failed:', error.message);
if (error.watchmanResponse) {
console.error('Watchman details:', error.watchmanResponse.error);
}
}
});
} catch (error) {
console.error('Unexpected error:', error);
}