@cnakazawa/watch
@cnakazawa/watch is a CommonJS utility for monitoring a directory tree from Node.js. It walks the tree, polls each discovered path with fs.watchFile, and reports creation, change, and removal either through one callback or an EventEmitter-style monitor. The package also installs a watch command that reruns a shell command after file changes. This scoped release is a lightly packaged form of mikeal/watch, not a modern event-driven watcher, so its simple API comes with old implementation choices.
Keep @cnakazawa/watch when compatibility with its callback behavior matters or polling is specifically useful. For a new watcher, its per-path polling, missing types, dated source, and weak error handling make chokidar or @parcel/watcher the more defensible install.
Use it if
- You maintain code that already depends on @cnakazawa/watch and need to preserve its watchTree callback contract
- You need polling because native filesystem notifications are unreliable on the mounted or networked filesystem you use
- You want a tiny CommonJS API that produces current and previous fs.Stats objects for changed files
- You need a basic command-line rerunner with an adjustable quiet period after each command starts
- You watch a large repository: the published source calls fs.watchFile for every discovered file and directory, so polling work grows with the whole tree rather than with actual changes
- You want an actively maintained dependency: version 1.0.4 was published in February 2020, while the latest commits returned for the repository's default branch are from 2017 and 43 issues remain open
- You use TypeScript and expect packaged declarations: the npm manifest has no types entry and the tarball contains no .d.ts files
- You need a clean error channel: walk failures are thrown inside the asynchronous watchTree setup callback, and createMonitor exposes change events but no documented error event
- You need native events, write coalescing, atomic-write handling, or a ready event: the README documents only polling, basic filtering, and created, changed, and removed notifications; chokidar is a better fit
Setup reality
Installation is one npm command and there are no peer dependencies or native builds, but the package's age shows immediately. Import it with require('@cnakazawa/watch'); the upstream README says require('watch') because it documents the unscoped original. The module is CommonJS and ships no TypeScript declarations, so typed projects need a local declaration or an untyped boundary. Startup is asynchronous: watchTree first walks the complete tree and then calls your callback with a files object plus null current and previous stats. Treat that special call as readiness, not as a file change. The implementation uses fs.watchFile on every path and accepts interval in seconds, converting it to milliseconds internally. The README says options pass through to fs.watchFile, but the published source constructs a fresh object containing only interval, so do not expect persistent or bigint to work. Use ignoreDirectoryPattern for node_modules and build outputs before watching a big tree. Permissions are split between ignoreUnreadableDir for EACCES during directory reads and ignoreNotPermitted for EPERM during stat calls. Stopping is manual through unwatchTree(root) or monitor.stop(). The CLI installs the generic executable name watch, runs the supplied command through a shell helper, and needs platform-specific quoting, so never interpolate untrusted filenames or arguments into that command string.
Patterns
Watch a directory tree and classify callbackswatch-tree
const watch = require('@cnakazawa/watch')
watch.watchTree('./src', (file, curr, prev) => {
if (typeof file === 'object' && curr === null && prev === null) {
console.log('ready', Object.keys(file).length)
} else if (prev === null) {
console.log('created', file)
} else if (curr.nlink === 0) {
console.log('removed', file)
} else {
console.log('changed', file)
}
})The first callback is a readiness sentinel containing the initial file map; failing to branch on it makes startup look like a file event.
Use named create, change, and remove eventscreate-monitor
const watch = require('@cnakazawa/watch')
watch.createMonitor('./src', (monitor) => {
monitor.on('created', (file, stat) => console.log('created', file, stat.size))
monitor.on('changed', (file, curr, prev) => console.log('changed', file, prev.mtime, curr.mtime))
monitor.on('removed', (file, stat) => console.log('removed', file, stat.mtime))
})Listeners are attached after the initial walk completes, so this API gives you a ready monitor but no documented error event.
Inspect the initial file mapread-initial-files
const watch = require('@cnakazawa/watch')
watch.createMonitor('./content', (monitor) => {
for (const [file, stat] of Object.entries(monitor.files)) {
if (!stat.isDirectory()) console.log(file, stat.size)
}
})monitor.files includes both directories and files, keyed by the joined paths produced during the initial walk.
Watch only JavaScript and JSON filesfilter-extensions
const path = require('node:path')
const watch = require('@cnakazawa/watch')
watch.createMonitor('./src', {
filter(file, stat) {
return stat.isDirectory() || ['.js', '.cjs', '.json'].includes(path.extname(file))
},
}, (monitor) => {
monitor.on('changed', (file) => console.log(file))
})Return true for directories or the initial walk will exclude them before it can discover matching descendants.
Skip dependency and build directoriesignore-generated-directories
const watch = require('@cnakazawa/watch')
watch.createMonitor('.', {
ignoreDirectoryPattern: /(?:^|[\/])(?:node_modules|dist|coverage)(?:[\/]|$)/,
}, (monitor) => {
monitor.on('changed', (file) => console.log(file))
})The regular expression receives full joined paths. Anchoring only to node_modules would miss nested dependency directories.
Exclude dotfiles during the walkignore-dotfiles
const watch = require('@cnakazawa/watch')
watch.watchTree('.', { ignoreDotFiles: true }, (file, curr, prev) => {
if (typeof file === 'string' && curr && prev) console.log(file)
})This checks whether each basename starts with a dot; it is not a general hidden-file test on every operating system.
Reduce polling frequencyset-poll-interval
const watch = require('@cnakazawa/watch')
watch.createMonitor('./logs', { interval: 2 }, (monitor) => {
monitor.on('changed', (file) => console.log(file))
})interval is documented in seconds and the source multiplies it by 1,000 before passing it to fs.watchFile.
Skip unreadable and forbidden pathstolerate-permissions
const watch = require('@cnakazawa/watch')
watch.createMonitor('/srv/shared', {
ignoreUnreadableDir: true,
ignoreNotPermitted: true,
}, (monitor) => {
console.log('watching', Object.keys(monitor.files).length, 'paths')
})The two flags cover different source branches: EACCES from readdir and EPERM from stat. Other errors can still be thrown asynchronously.
Stop a monitor during shutdownstop-monitor
const watch = require('@cnakazawa/watch')
watch.createMonitor('./src', (monitor) => {
const shutdown = () => {
monitor.stop()
process.exit(0)
}
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
})monitor.stop() delegates to unwatchTree for the original root and removes the fs.watchFile pollers known for that tree.
Stop a callback-based tree watcherunwatch-tree
const watch = require('@cnakazawa/watch')
const root = './src'
watch.watchTree(root, () => {})
setTimeout(() => watch.unwatchTree(root), 30_000)Pass the same root string used to start watching; the internal watcher registry is keyed by that value.
Walk a tree once without keeping watcherswalk-once
const watch = require('@cnakazawa/watch')
watch.walk('./src', { ignoreDotFiles: true }, (err, files) => {
if (err) throw err
const regularFiles = Object.entries(files)
.filter(([, stat]) => stat.isFile())
.map(([file]) => file)
console.log(regularFiles)
})walk is exported by the package source but not described in the README API, so treat it as a weakly supported convenience rather than a firm contract.
Rerun tests from the command linerun-cli-command
npx --package @cnakazawa/watch watch 'npm test' ./src --wait=1 --ignoreDotFiles --ignoreDirectoryPattern='/node_modules|dist/'The binary is named watch and executes the command through a shell helper. Quote for your shell and never construct the command from untrusted input.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| chokidar | npm | Use for maintained cross-platform watching with glob ignores, readiness, atomic-write handling, and a familiar event API |
| node-watch | npm | Use when you want a smaller fs.watch-based API and do not need chokidar's broader behavior controls |
| @parcel/watcher | npm | Use when performance on large trees matters enough to accept native binaries and platform-specific packages |