plumber-glob
Version:
File globbing operation for Plumber pipelines
131 lines (107 loc) • 3.92 kB
JavaScript
var operation = require('plumber').operation;
var Supervisor = require('plumber').Supervisor;
var Rx = require('plumber').Rx;
var Minimatch = require('minimatch').Minimatch;
var flatten = require('flatten');
var path = require('path');
var Gaze = require('gaze').Gaze;
function identity(x){ return x; }
// FIXME: native helper?
// compose(f, g)(x) == f(g(x))
function compose(f, g) {
return function() {
return f(g.apply(null, arguments));
};
}
function uniqueByPath() {
var memo = {};
return function(resource) {
var resPath = resource.path().absolute();
var seen = memo[resPath];
memo[resPath] = true;
return ! seen;
};
}
function asMinimatch(pattern) {
return new Minimatch(pattern);
}
function asAbsolutePath(relativePath) {
return path.resolve(relativePath);
}
// Returns an Observable of events for the gazed patterns
function gazeObservable(patterns) {
var gazer = new Gaze(patterns);
var gazeObs = Rx.Observable.create(function(observer) {
gazer.on('all', function(event, file) {
observer.onNext(file);
});
gazer.on('end', function() {
observer.onCompleted();
});
// Don't pass any arguments to close() as it has undocumented
// broken behaviours if you do
return function(){ gazer.close(); };
});
/* Gazing multiple patterns is actually completely broken in Gaze
* 0.6 as all gazes fire if any of the watched patterns match:
*
* https://github.com/shama/gaze/issues/104
*
* The workaround is to check whether the event is for a file that
* matches our pattern here. Tedious, but necessary.
*/
var patternMatchers = patterns.map(asAbsolutePath).map(asMinimatch);
return gazeObs.filter(function(file) {
return patternMatchers.some(function(mm) {
return mm.match(file);
});
});
}
function excludeMatching(excludedPatterns) {
var exclusionMatchers = excludedPatterns.map(asMinimatch);
return function(resource) {
var resPath = resource.path().absolute();
return ! exclusionMatchers.some(function(mm) {
return mm.match(resPath);
});
};
}
function globOperation(mapper, excludedPatterns) {
// The glob function is an alias for glob.pattern
function glob(/* patterns... */) {
return glob.pattern.apply(null, arguments);
};
glob.pattern = function(/* patterns... */) {
var patterns = flatten([].slice.call(arguments)).map(mapper);
// FIXME: do we really need the supervisor then?
var supervisor = new Supervisor();
// TODO: glob here, though how to make Resource from data?
var glob = supervisor.glob.bind(supervisor);
function globResources() {
return Rx.Observable.fromArray(patterns).
map(glob).
mergeAll().
filter(uniqueByPath()).
filter(excludeMatching(excludedPatterns));
}
// TODO: or new upstream operation? merge instead of concat?
return operation.concatExecutions(function() {
// TODO: gaze in supervisor
var changes = gazeObservable(patterns);
var gazedResources = changes.map(globResources);
return Rx.Observable.return(globResources()).concat(gazedResources);
});
};
glob.exclude = function(/* patterns... */) {
var excludeList = flatten([].slice.call(arguments)).map(mapper);
return globOperation(mapper, excludedPatterns.concat(excludeList));
};
// recursively compose mappers
glob.within = function(directory) {
return globOperation(compose(mapper, function(file) {
return path.join(directory, file);
}), excludedPatterns);
};
return glob;
};
module.exports = globOperation(identity, []);