UNPKG

ptap

Version:

pafang assets build tool.

247 lines (217 loc) 6.21 kB
/** * Module dependencies. */ var fs = require('fs'), path = require('path'); /** * 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) { ret[method] = function(fpath) { var stat = fs[method === 'sym' ? 'lstatSync' :'statSync']; if (fs.existsSync(fpath)) { memo.push(fpath, method); return stat(fpath)['is' + shortcuts[method]](); } return false; } }); return ret; }({}); /** * 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) } }); }); } }; /** * 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]; } } }); return mix; }; /** * 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]; } return true; } }; }({}); /** * 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); }); 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(); } }, free: function() { free = true; } } }(); /** * Delay function call and ignore invalid filenames. */ var normalizeCall = function(fname, options, cb) { // Store each name of the modifying or temporary files generated by an editor. fileNameCache.push(fname); worker.busydoing(function() { // A heuristic delay of the write-to-file process. setTimeout(function() { // 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); } cb.call(null, f); }).clear(); worker.free(); }, 100); }); }; /** * Catch exception on Windows when deleting a directory. */ var catchException = function() { console.log(arguments)}; /** * Option handler for the `watch` function. */ var handleOptions = function(origin, defaultOptions) { return function() { var args = [].slice.call(arguments); 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))); }); } }; /** * 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). * * Example: * * watch('fpath', {recursive: true}, function(file) { * console.log(file, ' changed'); * }); */ function watch(fpath, options, cb) { if (is.sym(fpath) && !(options.followSymLinks && options.maxSymLevel--)) { 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, '..'); fs.watch(parent, options, function(evt, fname) { if (path.basename(fpath) === fname) { normalizeCall(fpath, options, cb); } }).on('error', catchException); } else if (is.dir(fpath)) { fs.watch(fpath, options, function(evt, fname) { if (fname) { normalizeCall(path.join(fpath, fname), options, cb); } }).on('error', catchException); if (options.recursive) { // Recursively watch its sub-directories. sub(fpath, function(dir) { watch(dir, options, cb); }); } } }; /** * Set default options and expose. */ module.exports = handleOptions(watch, { recursive: true , followSymLinks: false , maxSymLevel: 1 });