feat(watchers): add Rust-powered watcher backend with runtime fallback and cross-platform test coverage

This commit is contained in:
2026-03-23 14:15:31 +00:00
parent ca9a66e03e
commit 7def7020c6
26 changed files with 10383 additions and 2870 deletions

113
readme.md
View File

@@ -1,6 +1,6 @@
# @push.rocks/smartwatch
A lightweight, cross-runtime file watcher with glob pattern support for **Node.js**, **Deno**, and **Bun**. Zero heavyweight dependencies — just native file watching APIs for maximum performance. 🚀
A cross-runtime file watcher with glob pattern support for Node.js, Deno, and Bun.
## Issue Reporting and Security
@@ -16,14 +16,25 @@ pnpm add @push.rocks/smartwatch
## Features
🌐 **Cross-Runtime** — Works seamlessly on Node.js 20+, Deno, and Bun
🔍 **Glob Pattern Support** — Watch files using familiar patterns like `**/*.ts` and `src/**/*.{js,jsx}`
📡 **RxJS Observables** — Subscribe to file system events using reactive streams
🔄 **Dynamic Watching** — Add or remove watch patterns at runtime
**Native Performance**Uses `fs.watch()` on Node.js/Bun and `Deno.watchFs()` on Deno
**Write Stabilization**Built-in debouncing prevents duplicate events during file writes
🎯 **TypeScript First** — Full TypeScript support with comprehensive type definitions
📦 **Minimal Footprint** — No chokidar, no FSEvents bindings — just ~500 lines of focused code
- **Cross-Runtime** — Works on Node.js 20+, Deno, and Bun
- **Glob Pattern Support** — Watch files using patterns like `**/*.ts` and `src/**/*.{js,jsx}`
- **RxJS Observables** — Subscribe to file system events using reactive streams
- **Dynamic Watching** — Add or remove watch patterns at runtime
- **Write Stabilization** — Built-in debouncing and awaitWriteFinish support for atomic writes
- **TypeScript First** — Full TypeScript support with comprehensive type definitions
## How It Works
smartwatch selects the best file watching backend for the current runtime:
| Runtime | Backend |
|-----------------|----------------------------------|
| **Node.js/Bun** | [chokidar](https://github.com/paulmillr/chokidar) v5 (uses `fs.watch()` internally) |
| **Deno** | Native `Deno.watchFs()` API |
On Node.js and Bun, chokidar provides robust cross-platform file watching with features like atomic write detection, inode tracking, and write stabilization. On Deno, native APIs are used directly with built-in debouncing and temporary file filtering.
Glob patterns are handled through [picomatch](https://github.com/micromatch/picomatch) — base paths are extracted from patterns and watched natively, while events are filtered through matchers before emission.
## Usage
@@ -34,8 +45,8 @@ import { Smartwatch } from '@push.rocks/smartwatch';
// Create a watcher with glob patterns
const watcher = new Smartwatch([
'./src/**/*.ts', // Watch all TypeScript files in src
'./public/assets/**/*' // Watch all files in public/assets
'./src/**/*.ts',
'./public/assets/**/*'
]);
// Start watching
@@ -49,15 +60,9 @@ Use RxJS observables to react to file system changes:
```typescript
// Get an observable for file changes
const changeObservable = await watcher.getObservableFor('change');
changeObservable.subscribe({
next: ([path, stats]) => {
console.log(`File changed: ${path}`);
console.log(`New size: ${stats?.size} bytes`);
},
error: (err) => {
console.error(`Error: ${err}`);
}
changeObservable.subscribe(([path, stats]) => {
console.log(`File changed: ${path}`);
console.log(`New size: ${stats?.size} bytes`);
});
// Watch for new files
@@ -103,7 +108,6 @@ watcher.remove('./src/**/*.test.ts');
### Stopping the Watcher
```typescript
// Stop watching when done
await watcher.stop();
```
@@ -113,38 +117,31 @@ await watcher.stop();
import { Smartwatch } from '@push.rocks/smartwatch';
async function watchProject() {
// Initialize with patterns
const watcher = new Smartwatch([
'./src/**/*.ts',
'./package.json'
]);
// Start the watcher
await watcher.start();
console.log('👀 Watching for changes...');
console.log('Watching for changes...');
// Subscribe to changes
const changes = await watcher.getObservableFor('change');
changes.subscribe(([path, stats]) => {
console.log(`📝 Modified: ${path}`);
console.log(` Size: ${stats?.size ?? 'unknown'} bytes`);
console.log(`Modified: ${path} (${stats?.size ?? 'unknown'} bytes)`);
});
// Subscribe to new files
const additions = await watcher.getObservableFor('add');
additions.subscribe(([path]) => {
console.log(`New file: ${path}`);
console.log(`New file: ${path}`);
});
// Subscribe to deletions
const deletions = await watcher.getObservableFor('unlink');
deletions.subscribe(([path]) => {
console.log(`🗑️ Deleted: ${path}`);
console.log(`Deleted: ${path}`);
});
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Stopping watcher...');
await watcher.stop();
process.exit(0);
});
@@ -153,41 +150,6 @@ async function watchProject() {
watchProject();
```
## How It Works
smartwatch uses native file watching APIs for each runtime:
| Runtime | API Used |
|-----------------|----------------------------------|
| **Node.js 20+** | `fs.watch({ recursive: true })` |
| **Deno** | `Deno.watchFs()` |
| **Bun** | Node.js compatibility layer |
### Under the Hood
Native file watching APIs don't support glob patterns directly, so smartwatch handles pattern matching internally:
1. **Base path extraction** — Extracts the static portion from each glob pattern (e.g., `./src/` from `./src/**/*.ts`)
2. **Efficient watching** — Native watchers monitor only the base directories
3. **Pattern filtering** — Events are filtered through [picomatch](https://github.com/micromatch/picomatch) matchers before emission
4. **Event deduplication** — Built-in throttling prevents duplicate events from rapid file operations
### Write Stabilization
smartwatch includes built-in write stabilization (similar to chokidar's `awaitWriteFinish`). When a file is being written, events are held until the file size stabilizes, preventing multiple events for a single write operation.
Default settings:
- **Stability threshold**: 300ms
- **Poll interval**: 100ms
## Requirements
| Runtime | Version |
|-----------------|----------------------------------------|
| **Node.js** | 20+ (required for native recursive watching) |
| **Deno** | Any version with `Deno.watchFs()` support |
| **Bun** | Uses Node.js compatibility |
## API Reference
### `Smartwatch`
@@ -226,18 +188,13 @@ type TFsEvent = 'add' | 'addDir' | 'change' | 'error' | 'unlink' | 'unlinkDir' |
type TSmartwatchStatus = 'idle' | 'starting' | 'watching';
```
## Why smartwatch?
## Requirements
| Feature | smartwatch | chokidar |
|-------------------------|----------------------|--------------------|
| Native API | ✅ Direct `fs.watch` | ❌ FSEvents bindings |
| Cross-runtime | ✅ Node, Deno, Bun | ❌ Node only |
| Dependencies | 4 small packages | ~20 packages |
| Write stabilization | ✅ Built-in | ✅ Built-in |
| Glob support | ✅ picomatch | ✅ anymatch |
| Bundle size | ~15KB | ~200KB+ |
If you need a lightweight file watcher without native compilation headaches, smartwatch has you covered.
| Runtime | Version |
|-----------------|----------------------------------------|
| **Node.js** | 20+ |
| **Deno** | Any version with `Deno.watchFs()` support |
| **Bun** | Uses Node.js compatibility layer |
## License and Legal Information