node-watch
Advanced tools
Comparing version 0.4.1 to 0.5.0
500
lib/watch.js
@@ -1,279 +0,313 @@ | ||
/** | ||
* Module dependencies. | ||
*/ | ||
var fs = require('fs') | ||
, path = require('path') | ||
, events = require('events'); | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var util = require('util'); | ||
var events = require('events'); | ||
var hasNativeRecursive = require('./has-native-recursive'); | ||
var is = require('./is'); | ||
/** | ||
* Utility functions to synchronously test whether the giving path | ||
* is a file or a directory or a symbolic link. | ||
*/ | ||
var is = function(ret) { | ||
var shortcuts = { | ||
'file': 'File' | ||
, 'dir': 'Directory' | ||
, 'sym': 'SymbolicLink' | ||
}; | ||
Object.keys(shortcuts).forEach(function(method) { | ||
var stat = fs[method === 'sym' ? 'lstatSync' :'statSync']; | ||
ret[method] = function(fpath) { | ||
try { | ||
var yes = stat(fpath)['is' + shortcuts[method]](); | ||
memo.push(fpath, method); | ||
return yes; | ||
} catch(e) {} | ||
var EVENT_UPDATE = 'update'; | ||
var EVENT_REMOVE = 'remove'; | ||
function makeArray(arr, offset) { | ||
return is.array(arr) | ||
? arr : [].slice.call(arr, offset || 0); | ||
} | ||
function hasDup(arr) { | ||
return makeArray(arr).some(function(v, i, self) { | ||
return self.indexOf(v) !== i; | ||
}); | ||
} | ||
function unique(arr) { | ||
return makeArray(arr).filter(function(v, i, self) { | ||
return self.indexOf(v) === i; | ||
}); | ||
} | ||
function assign(obj/*, props */) { | ||
if (Object.assign) { | ||
return Object.assign.apply(Object, arguments); | ||
} | ||
return makeArray(arguments, 1) | ||
.reduce(function(mix, prop) { | ||
for (var name in prop) { | ||
if (prop.hasOwnProperty(name)) { | ||
mix[name] = prop[name]; | ||
} | ||
} | ||
return mix; | ||
}, obj); | ||
} | ||
function guard(fn) { | ||
return function(arg, action) { | ||
if (is.func(fn)) { | ||
if (fn(arg)) action(); | ||
} else { | ||
action(); | ||
} | ||
} | ||
} | ||
function composeMessage(names) { | ||
return makeArray(names).map(function(n) { | ||
if (!is.exists(n)) return [EVENT_REMOVE, n]; | ||
else return [EVENT_UPDATE, n]; | ||
}); | ||
return ret; | ||
}({}); | ||
} | ||
function getMessages(cache) { | ||
var dup = hasDup(cache.map(function(c) { | ||
return c.replace(/^[~#]+|[~#]+$/, ''); | ||
})); | ||
/** | ||
* Get sub-directories in a directory. | ||
*/ | ||
var sub = function(parent, cb) { | ||
if (is.dir(parent)) { | ||
fs.readdir(parent, function(err, all) { | ||
all && all.forEach(function(f) { | ||
var sdir = path.join(parent, f); | ||
if (is.dir(sdir)) { | ||
cb.call(null, sdir) | ||
} | ||
}); | ||
// saving file from an editor maybe? | ||
if (dup) { | ||
var filtered = cache.filter(function(m) { | ||
return is.exists(m) | ||
}); | ||
return composeMessage(unique(filtered)); | ||
} | ||
}; | ||
else { | ||
return composeMessage(cache); | ||
} | ||
} | ||
function debounce(fn, delay) { | ||
var pending, timer, cache = []; | ||
var info = fn.info; | ||
function handle() { | ||
getMessages(cache).forEach(function(msg) { | ||
fn.apply(null, msg); | ||
}); | ||
timer = pending = null; | ||
cache = []; | ||
} | ||
return function(evt, name) { | ||
name = path.join(info.fpath, name); | ||
cache.push(name); | ||
/** | ||
* Mixing object properties. | ||
*/ | ||
var mixin = function() { | ||
var mix = {}; | ||
[].forEach.call(arguments, function(arg) { | ||
for (var name in arg) { | ||
if (arg.hasOwnProperty(name)) { | ||
mix[name] = arg[name]; | ||
if (!pending) { | ||
pending = true; | ||
} | ||
if (!timer) { | ||
timer = setTimeout(handle, delay || 200); | ||
} | ||
} | ||
} | ||
function getSubDirectories(dir, fn) { | ||
if (is.directory(dir)) { | ||
fs.readdir(dir, function(err, all) { | ||
if (err) { | ||
// don't throw permission errors. | ||
if (!/^(EPERM|EACCES)$/.test(err.code)) throw err; | ||
else console.warn('Warning: Cannot access %s.', dir); | ||
} | ||
else if (is.array(all)) { | ||
all.forEach(function(f) { | ||
var sdir = path.join(dir, f); | ||
if (is.directory(sdir)) fn(sdir); | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
function Watcher() { | ||
events.EventEmitter.call(this); | ||
this.watchers = {}; | ||
} | ||
util.inherits(Watcher, events.EventEmitter); | ||
Watcher.prototype.expose = function() { | ||
var self = this; | ||
var methods = [ | ||
'on', 'emit', 'close', 'isClosed', 'listeners', 'once', | ||
'setMaxListeners', 'getMaxListeners' | ||
]; | ||
return methods.reduce(function(expose, name) { | ||
expose[name] = function() { | ||
return self[name].apply(self, arguments); | ||
} | ||
}); | ||
return mix; | ||
}; | ||
return expose; | ||
}, {}); | ||
} | ||
Watcher.prototype.isClosed = function() { | ||
return !Object.keys(this.watchers).length | ||
} | ||
/** | ||
* A container for memorizing names of files or directories. | ||
*/ | ||
var memo = function(memo) { | ||
return { | ||
push: function(name, type) { | ||
memo[name] = type; | ||
}, | ||
has: function(name) { | ||
return {}.hasOwnProperty.call(memo, name); | ||
}, | ||
update: function(name) { | ||
if (!is.file(name) || !is.dir(name)) { | ||
delete memo[name]; | ||
Watcher.prototype.close = function(fullPath) { | ||
var self = this; | ||
if (fullPath) { | ||
var watcher = this.watchers[fullPath]; | ||
if (watcher && watcher.close) { | ||
watcher.close(); | ||
delete self.watchers[fullPath]; | ||
} | ||
getSubDirectories(fullPath, function(fpath) { | ||
self.close(fpath); | ||
}); | ||
} else { | ||
var self = this; | ||
Object.keys(self.watchers).forEach(function(fpath) { | ||
var watcher = self.watchers[fpath]; | ||
if (watcher && watcher.close) { | ||
watcher.close(); | ||
} | ||
return true; | ||
} | ||
}; | ||
}({}); | ||
}); | ||
this.watchers = {}; | ||
} | ||
}; | ||
Watcher.prototype.add = function(watcher, info) { | ||
var self = this; | ||
info = info || {}; | ||
var fullPath = path.resolve(info.fpath); | ||
this.watchers[fullPath] = watcher; | ||
/** | ||
* A Container for storing unique and valid filenames. | ||
*/ | ||
var fileNameCache = function(cache) { | ||
return { | ||
push: function(name) { | ||
cache[name] = 1; | ||
return this; | ||
}, | ||
each: function() { | ||
var temp = Object.keys(cache).filter(function(name){ | ||
return is.file(name) || memo.has(name) && memo.update(name); | ||
var callback = function(evt, name) { | ||
if (info.options.recursive) { | ||
hasNativeRecursive(function(has) { | ||
if (!has) { | ||
var fullPath = path.resolve(name); | ||
// remove watcher on removal | ||
if (evt == EVENT_REMOVE) { | ||
self.close(fullPath); | ||
} | ||
// watch new created directory | ||
else if (is.directory(name) && !self.watchers[fullPath]) { | ||
var filterGuard = guard(info.options.filter); | ||
filterGuard(name, function() { | ||
self.watchDirectory(name, info.options); | ||
}); | ||
} | ||
} | ||
}); | ||
temp.forEach.apply(temp, arguments); | ||
return this; | ||
}, | ||
clear: function(){ | ||
cache = {}; | ||
return this; | ||
} | ||
}; | ||
}({}); | ||
/** | ||
* Abstracting the way of avoiding duplicate function call. | ||
*/ | ||
var worker = function() { | ||
var free = true; | ||
return { | ||
busydoing: function(cb) { | ||
if (free) { | ||
free = false; | ||
cb.call(); | ||
// watch single file | ||
if (info.compareName) { | ||
if (info.compareName(name)) { | ||
self.emit('change', evt, name); | ||
} | ||
}, | ||
free: function() { | ||
free = true; | ||
} | ||
} | ||
}(); | ||
// watch directory | ||
else { | ||
var filterGuard = guard(info.options.filter); | ||
filterGuard(name, function() { | ||
self.emit('change', evt, name); | ||
}); | ||
} | ||
}; | ||
callback.info = info; | ||
watcher.on('change', debounce(callback)); | ||
watcher.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
} | ||
/** | ||
* Delay function call and ignore invalid filenames. | ||
*/ | ||
var normalizeCall = function(fname, options, cb, watcher) { | ||
// Store each name of the modifying or temporary files generated by an editor. | ||
fileNameCache.push(fname); | ||
Watcher.prototype.watchFile = function(file, options, fn) { | ||
var parent = path.join(file, '../'); | ||
var opts = assign({}, options, { | ||
recursive: false, | ||
filter: null | ||
}); | ||
worker.busydoing(function() { | ||
// A heuristic delay of the write-to-file process. | ||
setTimeout(function() { | ||
var watcher = fs.watch(parent, opts); | ||
this.add(watcher, { | ||
type: 'file', | ||
fpath: parent, | ||
options: opts, | ||
compareName: function(n) { | ||
return is.sameFile(n, file); | ||
} | ||
}); | ||
// When the write-to-file process is done, send all filtered filenames | ||
// to the callback function and call it. | ||
fileNameCache | ||
.each(function(f) { | ||
// Watch new created directory. | ||
if (options.recursive && !memo.has(f) && is.dir(f)) { | ||
watch(f, options, cb, watcher); | ||
} | ||
cb && cb.call(null, f); | ||
watcher.emit('change', f); | ||
}).clear(); | ||
if (is.func(fn)) { | ||
this.on('change', fn); | ||
} | ||
} | ||
worker.free(); | ||
}, 100); | ||
Watcher.prototype.watchDirectory = function(dir, options, fn) { | ||
var self = this; | ||
var watcher = fs.watch(dir, options); | ||
self.add(watcher, { | ||
type: 'dir', | ||
fpath: dir, | ||
options: options | ||
}); | ||
}; | ||
if (is.func(fn)) { | ||
self.on('change', fn); | ||
} | ||
/** | ||
* Watcher class to simulate FSWatcher | ||
*/ | ||
var Watcher = function Watcher() { | ||
this.watchers = []; | ||
this.closed = false; | ||
this.close = function() { | ||
this.watchers.forEach(function(watcher) { | ||
watcher.close(); | ||
if (options.recursive) { | ||
hasNativeRecursive(function(has) { | ||
if (has) return false; | ||
getSubDirectories(dir, function(d) { | ||
var filterGuard = guard(options.filter); | ||
filterGuard(d, function() { | ||
self.watchDirectory(d, options); | ||
}); | ||
}); | ||
}); | ||
this.watchers = []; | ||
this.closed = true; | ||
}; | ||
this.addWatcher = function(watcher, cb) { | ||
var self = this; | ||
this.watchers.push(watcher); | ||
} | ||
} | ||
watcher.on('error', function(err) { | ||
self.emit('error', err); | ||
function composeWatcher(watchers) { | ||
var watcher = new Watcher(); | ||
watchers.forEach(function(w) { | ||
w.on('change', function(evt, name) { | ||
watcher.emit('change', evt, name); | ||
}); | ||
}; | ||
}; | ||
}); | ||
watcher.close = function() { | ||
watchers.forEach(function(w) { | ||
w.close(); | ||
}); | ||
} | ||
return watcher; | ||
} | ||
Watcher.prototype.__proto__ = events.EventEmitter.prototype; | ||
function watch(fpath, options, fn) { | ||
var watcher = new Watcher(); | ||
if (is.array(fpath)) { | ||
return composeWatcher(unique(fpath).map(function(f) { | ||
return watch(f, options, fn); | ||
})); | ||
}; | ||
/** | ||
* Option handler for the `watch` function. | ||
*/ | ||
var handleOptions = function(origin, defaultOptions) { | ||
return function() { | ||
var args = [].slice.call(arguments); | ||
args[3] = new Watcher; | ||
if (Object.prototype.toString.call(args[1]) === '[object Function]') { | ||
args[2] = args[1]; | ||
} | ||
if (!Array.isArray(args[0])) { | ||
args[0] = [args[0]]; | ||
} | ||
//overwrite default options | ||
args[1] = mixin(defaultOptions, args[1]); | ||
//handle multiple files. | ||
args[0].forEach(function(path) { | ||
origin.apply(null, [path].concat(args.slice(1))); | ||
}); | ||
return args[3]; | ||
if (!is.exists(fpath)) { | ||
watcher.emit('error', | ||
new Error(fpath + ' does not exist.') | ||
); | ||
} | ||
}; | ||
if (is.func(options)) { | ||
fn = options; | ||
options = {}; | ||
} | ||
/** | ||
* Ignore the recursive option on platforms which natively support it, | ||
* or temporarily set it to false for optimization. | ||
*/ | ||
var noRecursive = function(option) { | ||
return mixin(option, { recursive: false }); | ||
}; | ||
if (arguments.length < 2) { | ||
options = {}; | ||
} | ||
/** | ||
* Watch a file or a directory (recursively by default). | ||
* | ||
* @param {String} fpath | ||
* @options {Object} options | ||
* @param {Function} cb | ||
* | ||
* Options: | ||
* `recursive`: Watch it recursively or not (defaults to true). | ||
* `followSymLinks`: Follow symbolic links or not (defaults to false). | ||
* `maxSymLevel`: The max number of following symbolic links (defaults to 1). | ||
* `filter`: Filter function(fullPath:string) => boolean (defaults to () => true ). | ||
* | ||
* Example: | ||
* | ||
* watch('fpath', { recursive: true }, function(file) { | ||
* console.log(file, ' changed'); | ||
* }); | ||
*/ | ||
function watch(fpath, options, cb, watcher) { | ||
var skip = watcher.closed || !options.filter(fpath) || ( | ||
is.sym(fpath) && !(options.followSymLinks && options.maxSymLevel--) | ||
); | ||
if (skip) return; | ||
// Due to the unstable fs.watch(), if the `fpath` is a file then | ||
// switch to watch its parent directory instead of watch it directly. | ||
// Once the logged filename matches it then triggers the callback function. | ||
if (is.file(fpath)) { | ||
var parent = path.resolve(fpath, '..'); | ||
watcher.addWatcher(fs.watch(parent, noRecursive(options)).on('change', function(evt, fname) { | ||
if (path.basename(fpath) === fname) { | ||
normalizeCall(fname, options, cb, watcher); | ||
} | ||
}), cb); | ||
watcher.watchFile(fpath, options, fn); | ||
} | ||
else if (is.dir(fpath)) { | ||
watcher.addWatcher(fs.watch(fpath, noRecursive(options)).on('change', function(evt, fname) { | ||
normalizeCall(path.join(fpath, fname || ''), options, cb, watcher); | ||
}), cb); | ||
if (options.recursive) { | ||
// Recursively watch its sub-directories. | ||
sub(fpath, function(dir) { | ||
watch(dir, options, cb, watcher); | ||
}); | ||
} | ||
else if (is.directory(fpath)) { | ||
watcher.watchDirectory(fpath, options, fn); | ||
} | ||
return watcher.expose(); | ||
} | ||
/** | ||
* Set default options and expose. | ||
*/ | ||
module.exports = handleOptions(watch, { | ||
recursive: true | ||
, followSymLinks: false | ||
, maxSymLevel: 1 | ||
, filter: function(fullPath) { return true; } | ||
}); | ||
module.exports = watch; |
{ | ||
"description": "fs.watch() wrapper of Nodejs ", | ||
"description": "A neat fs.watch wrapper", | ||
"license": "MIT", | ||
@@ -14,3 +14,3 @@ "name": "node-watch", | ||
], | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"bugs": { | ||
@@ -27,6 +27,5 @@ "url": "https://github.com/yuanchuan/node-watch/issues" | ||
"devDependencies": { | ||
"fs-extra": "^0.30.0", | ||
"mocha": "^2.5.3", | ||
"tmp": "0.0.28" | ||
"fs-extra": "^2.0.0", | ||
"mocha": "^3.2.0" | ||
} | ||
} |
130
README.md
@@ -1,4 +0,5 @@ | ||
#Node-watch | ||
A [fs.watch](http://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener) wrapper to watch files or directories(recursively by default). | ||
# node-watch | ||
A neat [fs.watch](http://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener) wrapper. | ||
[![NPM](https://nodei.co/npm/node-watch.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/node-watch.png/) | ||
@@ -18,32 +19,45 @@ | ||
watch('somedir_or_somefile', function(filename) { | ||
console.log(filename, ' changed.'); | ||
watch('somedir_or_somefile', { recursive: true }, function(evt, name) { | ||
console.log(name, ' changed.'); | ||
}); | ||
``` | ||
### Why fs.watch wrapper | ||
This is a completely rewritten version, **much faster** and in a more **memory-efficient** way. | ||
So with recent nodejs versions under OS X or Windows you can do something like this: | ||
```js | ||
// watch the whole disk | ||
watch('/', { recursive: true }, console.log); | ||
``` | ||
### Why | ||
* Some editors will generate temporary files which will cause the callback function to be triggered multiple times. | ||
* when watching a single file the callback function will only be triggered one time and then is seem to be unwatched. | ||
* Missing an option to watch a directory recursively. | ||
* When watching a single file the callback function will only be triggered once. | ||
* <del>Missing an option to watch a directory recursively.</del> | ||
* Recursive watch is not supported on Linux or in older versions of nodejs. | ||
### The difference | ||
This module **currently** does not differentiate event like `rename` or `delete`. Once there is a change, the callback function will be triggered. | ||
### Notice | ||
* The `recursive` option is defaults to be `false` since v0.5.0. | ||
* Parameters in the callback function always provide event name since v0.5.0. | ||
### Options | ||
`recursive`:Watch it recursively or not (defaults to **true**). | ||
### Events | ||
`followSymLinks`: Follow symbolic links or not (defaults to **false**). | ||
The events provided by the callback function would be either `update` or `remove`. | ||
`maxSymLevel`: The max number of following symbolic links, in order to prevent circular links (defaults to **1**). | ||
```js | ||
watch('./', function(evt, name) { | ||
`filter`: node-watch will only watch elements that pass the test implemented by the provided function. The filter function is provided with a full path string argument(defaults to ```(fullPath) => true``` ). | ||
if (evt == 'remove') { | ||
// on delete | ||
} | ||
if (evt == 'update') { | ||
// on create or modify | ||
} | ||
```js | ||
watch('somedir', { recursive: false, followSymLinks: true }, function(filename) { | ||
console.log(filename, ' changed.'); | ||
}); | ||
@@ -54,14 +68,13 @@ ``` | ||
Since v0.4.0 `watch()` will return a [fs.FSWatcher](https://nodejs.org/api/fs.html#fs_class_fs_fswatcher) like object, | ||
so you can close the watcher or detect change by `change` event instead of the old callback function. | ||
`watch` function returns a [fs.FSWatcher](https://nodejs.org/api/fs.html#fs_class_fs_fswatcher) like object as the same as `fs.watch`. | ||
```js | ||
var watcher = watch('./'); | ||
var watcher = watch('./', { recursive: true }); | ||
watcher.on('change', function(file) { | ||
// | ||
watcher.on('change', function(evt, name) { | ||
// callback | ||
}); | ||
watcher.on('error', function(err) { | ||
// | ||
// handle error | ||
}); | ||
@@ -74,42 +87,61 @@ | ||
###FAQ | ||
### Extra options | ||
#### 1. How to watch mutiple files or directories | ||
* `filter`: Filter files or directories or skip to watch them. | ||
```js | ||
watch(['file1', 'file2'], function(file) { | ||
// | ||
}); | ||
var options = { | ||
recursive: true, | ||
filter : function(name) { | ||
return !/node_modules/.test(name); | ||
} | ||
}; | ||
// ignore node_modules | ||
watch('mydir', options, console.log); | ||
``` | ||
#### 2. How to filter files | ||
### Other ways to filter | ||
You can write your own filter function as a higher-order function. For example: | ||
a) filtering directly inside the callback function: | ||
```js | ||
var filter = function(pattern, fn) { | ||
return function(filename) { | ||
if (pattern.test(filename)) { | ||
fn(filename); | ||
watch('./', { recursive: true }, function(evt, name) { | ||
// ignore node_modules | ||
if (!/node_modules/.test(name)) { | ||
// do something | ||
} | ||
}); | ||
``` | ||
b) filtering with higher order function: | ||
```js | ||
function filter(pattern, fn) { | ||
return function(evt, name) { | ||
if (pattern.test(name)) { | ||
fn(evt, name); | ||
} | ||
} | ||
} | ||
// only watch for js files | ||
watch('mydir', filter(/\.js$/, function(filename) { | ||
// | ||
})); | ||
// watch only for js files | ||
watch('.', filter(/\.js$/, console.log)); | ||
``` | ||
Alternatively, supply a filter function in the options object. For example: | ||
### Misc | ||
##### 1. Watch mutiple files or directories in one place | ||
```js | ||
// don't watch node_modules folder | ||
var options = { | ||
filter : function(filename) { | ||
return !/node_modules/.test(filename); | ||
} | ||
}; | ||
watch('mydir', options, function(filename) { | ||
// | ||
})); | ||
watch(['file1', 'file2'], console.log); | ||
``` | ||
The second approach helps avoiding the [max open files](http://stackoverflow.com/questions/3734932/max-open-files-for-working-process) limit | ||
##### 2. Catch errors after deleting a watched directory on Windows | ||
```js | ||
watch('somedir', console.log) | ||
.on('error', function() { | ||
// ignore it if you wish. | ||
}); | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
23470
2
11
706
145
7
1