var path = require('path'); var fs = require('fs'); var readdirp = require('readdirp'); var use_fs_watch = process.platform === 'win32' || process.env.USE_FS_WATCH; module.exports = function() { var watched_files = {}; var watched_directories = {}; var check_dir_pause = 1000; var checkInterval = undefined; // @api public // Watches the directory passed and its contained files // accepts args as an object. // @param root(string): the root directory to watch // @param fileFilter(array): ignore these files // @param directoryFilter(array): ignore these files // @param listener(fn(file)): on file change event this will be called // @param complete(fn): on complete of file watching setup function watchDirectory(args) { readdirp({ root: args.root, fileFilter: args.fileFilter, directoryFilter: args.directoryFilter }, function(err, res) { res.files.forEach(function(file) { watchFile(file, args.listener, args.partial); }); typeof args.complete == "function" && args.complete(); }); !args.partial && (checkInterval = setInterval(function() {checkDirectory(args)}, check_dir_pause)); } // @api public // Watches the files passed // accepts args as an object. // @param files(array): a list of files to watch // @param listener(fn(file)): on file change event this will be called // @param complete(fn): on complete of file watching setup function watchFiles(args) { args.files.forEach(function(file) { var o = { fullPath: fs.realpathSync(file), name: fs.realpathSync(file).split('/').pop() }; o.fullParentDir = o.fullPath.split('/').slice(0, o.fullPath.split('/').length - 1).join('/') watchFile(o, args.listener); }); typeof args.complete == "function" && args.complete(); } function unwatchAll() { if (use_fs_watch) { Object.keys(watched_files).forEach(function(key) { watched_files[key].close(); }); } else { Object.keys(watched_files).forEach(function(key) { fs.unwatchFile(key); }); } clearInterval(checkInterval); watched_files = {}; watched_directories = {}; } // Checks to see if something in the directory has changed function checkDirectory(args) { Object.keys(watched_directories).forEach(function(path) { var lastModified = watched_directories[path]; fs.stat(path, function(err, stats) { var stats_stamp = lastModified; if (!err) { stats_stamp = (new Date(stats.mtime)).getTime(); } if (stats_stamp != lastModified) { watched_directories[path] = stats_stamp; watchDirectory({ root: path, listener: args.listener, fileFilter: args.fileFilter, directoryFilter: args.directoryFilter, partial: true }); } }); }); } // sets the absolute path to the file from the current working dir function setAbsolutePath(file) { file.absolutePath = path.resolve(process.cwd(), file.fullPath); return file.absolutePath; } // Watches the file passed and its containing directory // on change calls given listener with file object function watchFile(file, cb, partial) { setAbsolutePath(file); storeDirectory(file); if (!watched_files[file.fullPath]) { if (use_fs_watch) { (function() { watched_files[file.fullPath] = fs.watch(file.fullPath, function() { typeof cb === "function" && cb(file); }); partial && cb(file); })(file, cb); } else { (function(file, cb) { watched_files[file.fullPath] = true; fs.watchFile(file.fullPath, {interval: 150}, function() { typeof cb === "function" && cb(file); }); partial && cb(file); })(file, cb); } } } // Sets up a store of the folders being watched // and saves the last modification timestamp for it function storeDirectory(file) { var directory = file.fullParentDir; if (!watched_directories[directory]) { fs.stat(directory, function(err, stats) { if (err) { watched_directories[directory] = (new Date).getTime(); } else { watched_directories[directory] = (new Date(stats.mtime)).getTime(); } }); } } // distinguish between files and directories // @returns {Object} contains directories and files array function distinguishPaths(paths) { paths = Array.isArray(paths) ? paths : [paths]; var result = { directories: [], files: [] }; paths.forEach(function(name) { if (fs.statSync(name).isDirectory()) { result.directories.push(name); } else { result.files.push(name); } }); return result; }; // for functions accepts an object as paramter // copy the object and modify with attributes function extend(prototype, attributes) { var object = {}; Object.keys(prototype).forEach(function(key) { object[key] = prototype[key]; }); Object.keys(attributes).forEach(function(key) { object[key] = attributes[key]; }); return object; }; // watch files if the paths refer to files, or directories function watchPaths(args) { var result = distinguishPaths(args.path) if (result.directories.length) { result.directories.forEach(function(directory) { watchDirectory(extend(args, {root: directory})); }); } if (result.files.length) watchFiles(extend(args, {files: result.files})); } return { watchDirectory: watchDirectory, watchFiles: watchFiles, watchPaths: watchPaths, unwatchAll: unwatchAll }; }