ptap
Version:
pafang assets build tool.
247 lines (217 loc) • 6.21 kB
JavaScript
/**
* 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
});