Socket
Socket
Sign inDemoInstall

node-watch

Package Overview
Dependencies
0
Maintainers
1
Versions
40
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.4.1 to 0.5.0

lib/has-native-recursive.js

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"
}
}

@@ -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

SocketSocket SOC 2 Logo

Product

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

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc