Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

simple-watcher

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

simple-watcher - npm Package Compare versions

Comparing version 4.0.2 to 5.0.0

bin/simple-watcher.js

236

index.js
'use strict'
const fs = require('fs')
const path = require('path')
import fs from 'fs'
import path from 'path'
const TOLERANCE = 200 // For ReadDirectoryChangesW() double reporting.
const PLATFORMS = ['win32', 'darwin']
const PLATFORMS = ['win32', 'darwin'] // Native recursive support.
// OS directory watcher.
function watchDir (dirToWatch, options, callback) {
// -----------------------------------------------------------------------------
// HELPERS AND POLYFILLS
// TODO: Refactor after fsPromises.watch is available in the next LTS version.
// -----------------------------------------------------------------------------
// AbortController falback.
export class AbortController {
constructor () {
this.callbacks = []
this.aborted = false
this.signal = {
aborted: false,
onabort: callback => this.callbacks.push(callback),
abort: () => {
this.callbacks.forEach(cb => cb())
this.signal.aborted = true
}
}
}
}
// Graceful closing.
function createWatchers (abortSignal) {
const watchers = {}
// Close all watchers on abort.
abortSignal && abortSignal.onabort(() => {
for (const entityPath of Object.keys(watchers)) {
close(entityPath)
}
})
function has (entityPath) {
return !!watchers[entityPath]
}
function add (entityPath, w) {
if (!has(entityPath)) {
watchers[entityPath] = w
w.on('error', err => watchers.close(pathToWatch))
}
}
function close (entityPath) {
if (has(entityPath)) {
watchers[entityPath].close()
delete watchers[entityPath]
}
}
return { add, has, close }
}
// Chanel support for asyc generators.
function createChannel (abortSignal) {
const messageQueue = []
const promiseQueue = []
abortSignal && abortSignal.onabort(() => {
const nextPromise = promiseQueue.shift()
nextPromise && nextPromise.resolve()
})
function put (msg) {
// Anyone waiting for a message?
if (promiseQueue.length) {
// Deliver the message to the oldest one waiting (FIFO).
const nextPromise = promiseQueue.shift()
nextPromise.resolve(msg)
} else {
// No one is waiting - queue the event.
messageQueue.push(msg)
}
}
function take () {
// Do we have queued messages?
if (messageQueue.length) {
// Deliver the oldest queued message.
return Promise.resolve(messageQueue.shift())
} else {
// No queued messages - queue the taker until a message arrives.
return new Promise((resolve, reject) => promiseQueue.push({ resolve, reject }))
}
}
return { put, take }
}
// -----------------------------------------------------------------------------
// WATCHERS
// -----------------------------------------------------------------------------
// Native recursive watcher.
function watchNative (pathToWatch, options, callback) {
const last = { filePath: null, timestamp: 0 }
const w = fs.watch(dirToWatch, { persistent: true, recursive: !options.shallow }, (event, fileName) => {
const recursive = options.recursive ?? true
// Do not create a watcher if already present.
if (options.watchers.has(pathToWatch)) {
return
}
const w = fs.watch(pathToWatch, { recursive }, async (event, fileName) => {
// On Windows fileName may actually be empty.
// In such case assume this is the working dir change.
const filePath = fileName ? path.join(dirToWatch, fileName) : dirToWatch
fs.stat(filePath, (err, stat) => {
// If error, the file was likely deleted.
const timestamp = err ? 0 : (new Date(stat.mtime)).getTime()
const ready = err || timestamp - last.timestamp >= options.tolerance
const filePath = fileName ? path.join(pathToWatch, fileName) : pathToWatch
// callback(filePath)
try {
const stat = await fs.promises.stat(filePath)
const timestamp = (new Date(stat.mtime)).getTime()
const ready = timestamp - last.timestamp >= options.tolerance
const fileMatches = filePath === last.filePath

@@ -25,41 +125,40 @@ last.filePath = filePath

if (fileMatches && !ready) {
return
// Avoid double reporting if change occurs withint the tolerance.
if (!fileMatches || ready) {
callback(filePath)
}
} catch (err) {
// File is likely deleted.
callback(filePath)
})
}
})
w.on('error', (e) => {
w.close()
})
options.watchers.add(pathToWatch, w)
}
// Fallback deep watcher.
function watchDirFallback (dirToWatch, options, callback) {
const dirs = [dirToWatch]
options.ledger = options.ledger || new Set()
// Fallback recursive watcher.
function watchFallback (pathToWatch, options, callback) {
const dirs = [pathToWatch]
for (let ii = 0; ii < dirs.length; ++ii) {
const dir = dirs[ii]
for (const dir of dirs) {
// Append dirs with descendants.
if (!options.shallow) {
for (const entityName of fs.readdirSync(dir)) {
const entityPath = path.resolve(dir, entityName)
fs.statSync(entityPath).isDirectory() && dirs.push(entityPath)
}
for (const entityName of fs.readdirSync(dir)) {
const entityPath = path.join(dir, entityName)
fs.statSync(entityPath).isDirectory() && dirs.push(entityPath)
}
options.ledger.add(dir)
watchDir(dir, { shallow: true, tolerance: options.tolerance }, (entityPath) => {
fs.stat(entityPath, (err, stat) => {
if (err) { // Entity was deleted.
options.ledger.delete(entityPath)
} else if (stat.isDirectory() && !options.ledger.has(entityPath) && !options.shallow) { // New directory added.
watchDirFallback(entityPath, options, callback)
// Shallow watch using native watcher.
watchNative(dir, { ...options, recursive: false }, async entityPath => {
try {
const stat = await fs.promises.stat(entityPath)
// Watch newly created directory.
if (stat.isDirectory()) {
watchFallback(entityPath, options, callback)
}
} catch (err) {
// Close watcher for deleted directory.
options.watchers.close(entityPath)
}
callback(entityPath)
})
callback(entityPath)
})

@@ -69,42 +168,33 @@ }

function watchFile (filePath, options, callback) {
options = options.interval ? { interval: options.interval } : {}
fs.watchFile(filePath, options, (curr, prev) => {
curr.mtime === 0 && fs.unwatchFile(filePath) // Unwatch if deleted.
callback(filePath)
})
}
export default async function* watch (pathsToWatch, options = {}) {
// Normalize paths to array.
pathsToWatch = pathsToWatch.constructor === Array ? pathsToWatch : [pathsToWatch]
function watch (entitiesToWatch, arg1, arg2) {
const callback = arg2 || arg1
const options = arg2 ? arg1 : { tolerance: TOLERANCE }
options.tolerance = process.platform === 'win32' ? (options.tolerance || TOLERANCE) : 0 // Disable tolerance if not on Windows.
options.fallback = options.fallback || !PLATFORMS.includes(process.platform)
// Set default tolerance for Windows.
options.tolerance = options.tolerance ?? (process.platform === 'win32' ? TOLERANCE : 0)
entitiesToWatch = entitiesToWatch.constructor === Array ? entitiesToWatch : [entitiesToWatch] // Normalize to array.
entitiesToWatch = entitiesToWatch.map(entityToWatch => path.resolve(entityToWatch)) // Resolve directory paths.
// Choose the the watch. function.
options.fallback = options.fallback ?? !PLATFORMS.includes(process.platform)
const watchFunction = options.fallback ? watchFallback : watchNative
for (const entityToWatch of entitiesToWatch) {
if (!fs.statSync(entityToWatch).isDirectory()) {
watchFile(entityToWatch, options, callback)
} else {
options.fallback ? watchDirFallback(entityToWatch, options, callback) : watchDir(entityToWatch, options, callback)
}
}
}
// Create watchers registry.
options.watchers = createWatchers(options.signal)
watch.main = function () {
const args = process.argv.slice(2)
const entitiesToWatch = args.filter(a => !a.startsWith('--'))
const options = { shallow: args.includes('--shallow'), fallback: args.includes('--fallback') }
// Put results to the channel.
const channel = createChannel(options.signal)
watch(entitiesToWatch, options, fileName => {
console.log(`${fileName}`)
})
}
for (const pathToWatch of pathsToWatch) {
watchFunction(path.normalize(pathToWatch), options, changedPath => {
channel.put(changedPath)
})
}
if (require.main === module) {
watch.main()
// Yield changes until aborted.
const signal = options.signal ?? { aborted: false }
while (!signal.aborted) {
const changedPath = await channel.take()
if (changedPath) { // Path will be undefined when watch is aborted.
yield changedPath
}
}
}
module.exports = watch
{
"name": "simple-watcher",
"version": "4.0.2",
"description": "\"A simple directory watcher.\"",
"version": "5.0.0",
"description": "\"A simple file s watcher.\"",
"type": "module",
"main": "index.js",
"bin": {
"simple-watcher": "./bin/simple-watcher"
"simple-watcher": "./bin/simple-watcher.js"
},
"scripts": {
"test": "node ./test/test.js -v",
"bin": "node ./bin/simple-watcher.js"
},
"engines": {
"node": ">=14.15.1"
},
"repository": {

@@ -26,3 +34,7 @@ "type": "git",

},
"homepage": "https://github.com/gavoja/simple-watcher#readme"
"homepage": "https://github.com/gavoja/simple-watcher#readme",
"devDependencies": {
"fs-extra": "^9.1.0",
"triala": "^0.4.0"
}
}

@@ -7,9 +7,10 @@ # Simple Watcher

I know there's plenty of them out there, but most don't seem to care about the `recursive` option of Node's [`fs.watch()`](https://nodejs.org/docs/latest/api/fs.html#fs_fs_watch_filename_options_listener), which **significantly** improves performance on the supported platforms, especially for large directories.
Most watchers do not seem to care about the `recursive` option of Node's [`fs.watch()`](https://nodejs.org/docs/latest/api/fs.html#fs_fs_watch_filename_options_listener), which **significantly** improves performance on the supported platforms, especially for large directories.
Features:
* Dead simple and dead lightweight.
* Simple, fast and lightweight.
* No dependencies.
* Leverages the `recursive` options on OS X and Windows; uses a fallback for other platforms.
* Leverages the `recursive` option on OS X and Windows for improved performance; uses a fallback for other platforms.
* Takes care of WinAPI's `ReadDirectoryChangesW` [double reporting](http://stackoverflow.com/questions/14036449/c-winapi-readdirectorychangesw-receiving-double-notifications).
* Modern API without callbacks.

@@ -22,3 +23,3 @@ ## Usage

Usage:
simple-watcher path1 [path2 path3 ...] [--shallow]
simple-watcher path1 [path2 path3 ...]
```

@@ -29,22 +30,17 @@

```JavaScript
const watch = require('simple-watcher')
import watch, { AbortController } from 'simple-watcher'
// Watch over file or directory:
watch('/path/to/foo', filePath => {
console.log(`Changed: ${filePath}`)
})
// The AbortController is available natively since 15.9.0.
const ac = new AbortController()
const { signal } = ac
setTimeout(() => ac.abort(), 10000)
// Watch over multiple paths:
watch(['/path/to/foo', '/path/to/bar'], filePath => {
// Watch over file or directory.
for await (const changedPath of watch('/path/to/foo'), { signal }) {
console.log(`Changed: ${filePath}`)
})
}
// Shallow watch:
watch(['/path/to/foo', '/path/to/bar'], { shallow: true }, filePath => {
// Watch over multiple paths.
for await (const changedPath of watch(['/path/to/bar', '/path/to/baz']), { signal }) {
console.log(`Changed: ${filePath}`)
})
```
## Caveats
When watching over files rather than directories, the [`fs.watchFile()`](https://nodejs.org/docs/latest/api/fs.html#fs_fs_watchfile_filename_options_listener) is used. This is to provide a polling fallback in cases where directory watching is problematic (e.g. Docker).
}
'use strict'
const watch = require('../index')
const path = require('path')
import assert from 'assert'
import fs from 'fs-extra'
import path from 'path'
import test from 'triala'
import watch, { AbortController } from '../index.js'
const toWatch = path.resolve(__dirname, 'root', 'level1-file')
const options = {
shallow: false,
fallback: true,
interval: 100
}
// Existing paths.
const PATH_TO_WATCH = path.normalize('./test/data')
const LEVEL1_DIR = path.join(PATH_TO_WATCH, 'level1-dir')
const LEVEL1_FILE = path.join(PATH_TO_WATCH, 'level1-file')
const LEVEL2_DIR = path.join(LEVEL1_DIR, 'level2-dir')
const LEVEL2_FILE = path.join(LEVEL1_DIR, 'level2-file')
const LEVEL3_FILE = path.join(LEVEL2_DIR, 'level3-file')
watch(toWatch, options, fileName => {
console.log(`${fileName}`, options.ledger)
// New paths.
const LEVEL3_DIR = path.join(LEVEL2_DIR, 'level3-dir')
const LEVEL4_FILE = path.join(LEVEL3_DIR, 'level4-file')
// WARNING: Tests are Windows specific!
test('Watcher', class {
async _watch (pathToWatch, options, action) {
const [result] = await Promise.all([
(async () => {
const changes = []
for await (const changedPath of watch(pathToWatch, options)) {
changes.push(changedPath)
}
return changes
})(),
action()
])
return result
}
async _pause (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
_touch (path) {
const time = new Date()
return fs.utimesSync(path, time, time)
}
async _timeout (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async 'Watch over directory' () {
const signal = (new AbortController()).signal
const changes = await this._watch(PATH_TO_WATCH, { signal }, async () => {
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_FILE)
await this._pause(201)
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_DIR)
this._touch(LEVEL2_FILE)
this._touch(LEVEL2_DIR)
this._touch(LEVEL3_FILE)
await this._pause(10)
signal.abort()
})
assert.deepStrictEqual(changes, [
LEVEL1_FILE,
LEVEL1_FILE,
LEVEL1_DIR,
LEVEL2_FILE,
LEVEL2_DIR,
LEVEL3_FILE
])
}
async 'Watch over directory (zero tolerance)' () {
const signal = (new AbortController()).signal
const changes = await this._watch(PATH_TO_WATCH, { signal, tolerance: 0 }, async () => {
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_DIR)
this._touch(LEVEL2_FILE)
this._touch(LEVEL2_DIR)
this._touch(LEVEL3_FILE)
await this._pause(10)
signal.abort()
})
assert.deepStrictEqual(changes, [
LEVEL1_FILE,
LEVEL1_FILE,
LEVEL1_DIR,
LEVEL2_FILE,
LEVEL2_DIR,
LEVEL3_FILE
])
}
async 'Watch over directory (fallback)' () {
const signal = (new AbortController()).signal
const changes = await this._watch(PATH_TO_WATCH, { signal, fallback: true }, async () => {
await this._pause(10) // Allow for the watchers to apply recursively.
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_DIR)
await this._pause(10)
this._touch(LEVEL2_FILE)
this._touch(LEVEL2_DIR)
await this._pause(10)
this._touch(LEVEL3_FILE)
fs.ensureDirSync(LEVEL3_DIR)
await this._pause(10)
fs.createFileSync(LEVEL4_FILE)
await this._pause(10)
signal.abort()
})
assert.deepStrictEqual(changes, [
LEVEL1_FILE,
LEVEL1_DIR,
LEVEL2_FILE,
LEVEL2_DIR,
LEVEL3_FILE,
LEVEL3_DIR,
LEVEL4_FILE
])
}
async 'Watch over file' () {
const signal = (new AbortController()).signal
const changes = await this._watch(LEVEL1_FILE, { signal }, async () => {
this._touch(LEVEL1_FILE)
this._touch(LEVEL1_FILE)
await this._pause(10)
signal.abort()
})
assert.notStrictEqual(changes, [LEVEL1_FILE])
}
async _before () {
// Clean up folders.
fs.removeSync(LEVEL3_DIR)
}
async _after () {
this._before()
}
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc