lib/fsExt.js
function watchRecursive(dir, opts, onChange, cb, repeated){ // cb(err, files)
    if(arguments.length === 3) {
        cb = arguments[2];
        onChange = arguments[1];
        opts = {};
    }
    opts = object.extend({}, opts || {}, { includeRootDir:true });
    if(typeof cb !== 'function') throw new Error('Wrong arguments');
    
    var maxRepeats = 5;
    repeated = repeated || 0;
    
    // walk dir and setup watchers
    dir = walk(dir, opts, function(err, items){
        // sometimes, when setuping watch right after moving, or renaming "EPERM" error may occur, just wait and try again
        if(err && err.code === 'EPERM'){
            if(repeated < maxRepeats) setTimeout(function(){
                watchRecursive(dir, opts, onChange, cb, repeated+1);
            }, 100);
            else cb(new Error('fsExt.watchRecursive: exploring folder failed').cause(err));
        }
        else if(err) {
            cb(new Error('fsExt.watchRecursive: exploring folder failed').cause(err));
        }
        else {
            watchDir(dir, items, cb);
        }
    }, opts.rootDir);
    
    function watchDir(dir, items, done){
        for(var key in items){
            if(items[key].isDir && !_dirWatchers[ items[key].fullPath ]) { // avoid duplicite watchers
                // set dir watch
                try {
                    var item = items[key];
                    
                    _dirWatchers[ item.fullPath ] = fs.watch(item.fullPath, { persistent: opts.watchPersistent || false })
                    .on('change', function(op, fileName){
                        var onChangeOpts = object.extend({}, opts, { rootDir: opts.rootDir||dir }),
                            watcher = this;
                            
                        watchRecursive(watcher.fullPath, onChangeOpts, onChange, function(err, descendants){
                            if(err && err.code==='ENOENT') return;
                            else if(err) onChange(new Error('fsExt.watchRecursive onChange: setup nested watch failed').cause(err));
                            else {
                                var changes = compareDirFiles(watcher.id, watcher.children, descendants);
                                
                                // remove watchers if whole folder is removed
                                for(var i=0;i<changes.length;i++){
                                    if(changes[i].event === 'removed' && changes[i].file.isDir){
                                        delete _dirWatchers[changes[i].file.fullPath];
                                    }
                                }
                                
                                if(changes.length) onChange(null, changes);
                            }
                        });
                        
                    }).on('error', function(err){
                        this.close();
                        _dirWatchers[ this.fullPath ] = null;
                    });
                    
                    object.extend(_dirWatchers[ item.fullPath ], { children: getDirChildren(item.id, items) }, item);
                }
                catch(err){
                    return done(new Error('fsExt.watchRecursive: setup watcher failed').cause(err));
                }
            }
        }
        done(null, items);
    }
}
function watchRecursive(dir, opts, onChange, cb, repeated){ // cb(err, files)
    if(arguments.length === 3) {
        cb = arguments[2];
        onChange = arguments[1];
        opts = {};
    }
    opts = object.extend({}, opts || {}, { includeRootDir:true });
    if(typeof cb !== 'function') throw new Error('Wrong arguments');
    
    var maxRepeats = 5;
    repeated = repeated || 0;
    
    // walk dir and setup watchers
    dir = walk(dir, opts, function(err, items){
        // sometimes, when setuping watch right after moving, or renaming "EPERM" error may occur, just wait and try again
        if(err && err.code === 'EPERM'){
            if(repeated < maxRepeats) setTimeout(function(){
                watchRecursive(dir, opts, onChange, cb, repeated+1);
            }, 100);
            else cb(new Error('fsExt.watchRecursive: exploring folder failed').cause(err));
        }
        else if(err) {
            cb(new Error('fsExt.watchRecursive: exploring folder failed').cause(err));
        }
        else {
            watchDir(dir, items, cb);
        }
    }, opts.rootDir);
    
    function watchDir(dir, items, done){
        for(var key in items){
            if(items[key].isDir && !_dirWatchers[ items[key].fullPath ]) { // avoid duplicite watchers
                // set dir watch
                try {
                    var item = items[key];
                    
                    _dirWatchers[ item.fullPath ] = fs.watch(item.fullPath, { persistent: opts.watchPersistent || false })
                    .on('change', function(op, fileName){
                        var onChangeOpts = object.extend({}, opts, { rootDir: opts.rootDir||dir }),
                            watcher = this;
                            
                        watchRecursive(watcher.fullPath, onChangeOpts, onChange, function(err, descendants){
                            if(err && err.code==='ENOENT') return;
                            else if(err) onChange(new Error('fsExt.watchRecursive onChange: setup nested watch failed').cause(err));
                            else {
                                var changes = compareDirFiles(watcher.id, watcher.children, descendants);
                                
                                // remove watchers if whole folder is removed
                                for(var i=0;i<changes.length;i++){
                                    if(changes[i].event === 'removed' && changes[i].file.isDir){
                                        delete _dirWatchers[changes[i].file.fullPath];
                                    }
                                }
                                
                                if(changes.length) onChange(null, changes);
                            }
                        });
                        
                    }).on('error', function(err){
                        this.close();
                        _dirWatchers[ this.fullPath ] = null;
                    });
                    
                    object.extend(_dirWatchers[ item.fullPath ], { children: getDirChildren(item.id, items) }, item);
                }
                catch(err){
                    return done(new Error('fsExt.watchRecursive: setup watcher failed').cause(err));
                }
            }
        }
        done(null, items);
    }
}
watching directory recursive
@param {String} dir root dir to watch
@param {Object} opts watch / walk options
@param {Function} onChange fired when change
@param {Function} cb executed when all watchers are set