Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite hub.js and add task dependencies. #48

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# grunt-hub

A Grunt task to watch and run tasks on multiple Grunt projects.
A Grunt task to watch and run tasks on multiple Grunt projects, optionally in order of dependencies between Gruntfiles.

## Create a Grunt Hub

Expand Down Expand Up @@ -103,10 +103,28 @@ You can override tasks on the cli with args: `grunt hub:all:watch` will run the

#### options

##### `concurrent`
Default: `3`
##### `dependencyFn`
Default: no dependencies

Set to the number of concurrent task runs to spawn.
Set to either a string or a function. If a string, must be one of the supported default dependency functions below. If a function, it will be passed two arguments--(1) an absolute path to a Gruntfile, and (2) an array containing absolute paths to all Gruntfiles matched by the glob--and is expected to return an array of absolute paths to the Gruntfiles the given Gruntfile depends on. (Note: the `hub` task will fail if `dependencyFn` returns a Gruntfile that is not matched by the `src` glob, or if paths returned are not absolute.)

If a `dependencyFn` is specified, then tasks for each Gruntfile will only run once the tasks for all dependent Gruntfiles have completed. If an error occurs running `dependencyFn`, an error is printed, and the task proceeds under the assumption that Gruntfile has no dependencies.

Default dependency functions:
* `"bower"` - Attempt to read a `bower.json` from the same directory as the Gruntfile. Any relative paths in `dependencies` or `devDependencies` from that `bower.json` will be searched for Gruntfiles, and any Gruntfiles found will be returned as dependencies.
* Example: `projectA/bower.json` has the following lines in its `dependencies`: (`"projectB": "../projectB"`) and (`"projectC": "../projectC"`). The dependencies for `projectA/Gruntfile.js` will then be `["/path/to/projectB/Gruntfile.js", "/path/to/projectC/Gruntfile.js"]`, assuming that both files exist and are in the `src` glob.

##### ~~`concurrent`~~
~~Default: `3`~~

~~Set to the number of concurrent task runs to spawn.~~

***Currently disabled, pending merge of [caolan/async#637](https://github.com/caolan/async/pull/637).***

##### `bufferOutput`
Default: `false`

Set to `true` to buffer output of running tasks, and print all output from a task as a batch once it completes.

##### `allowSelf`
Default: `false`
Expand Down
236 changes: 168 additions & 68 deletions tasks/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,95 +14,195 @@ module.exports = function(grunt) {
var async = require('async');
var _ = require('lodash');

var defaultDependencyFns = {
// Dependency function to depend on a project's bower dependencies.
'bower': function(gruntfile, allGruntfiles) {
var projectPath = path.dirname(gruntfile);
var bowerJson = require(path.join(projectPath, "bower.json"));
var bowerDependencies = _.extend(bowerJson.dependencies, bowerJson.devDependencies);
var dependencies = [];
// for each dependency...
Object.keys(bowerDependencies).forEach(function(dependencyName) {
var dependencyValue = bowerDependencies[dependencyName];
var dependencyPath = path.resolve(projectPath, dependencyValue);
// check if there's a Gruntfile we know about in that directory...
allGruntfiles.forEach(function(gruntfile2) {
if (path.dirname(gruntfile2) == dependencyPath) {
// and depend on that Gruntfile if so.
dependencies.push(gruntfile2);
}
});
});
return dependencies;
}
}

grunt.registerMultiTask('hub', 'Run multiple grunt projects', function() {
var options = this.options({
concurrent: 3,
allowSelf: false
// TODO: Re-enable this once caolan/async#637 is merged.
/* concurrent: 3, */
allowSelf: false,
bufferOutput: false,
dependencyFn: false
});
var args = (this.args.length < 1) ? false : this.args;
var args = this.args;

if (typeof options.dependencyFn === 'string' && !defaultDependencyFns[options.dependencyFn]) {
grunt.log.error('Named dependency function "%s" not supported. (options: [%s])',
options.dependencyFn,
Object.keys(defaultDependencyFns).join(', '));
return;
}

var cliArgs = process.argv.slice(2)
.filter(function(arg, index, arr) {
return (
// Remove arguments that were tasks to this Gruntfile.
!(_.contains(grunt.cli.tasks, arg)) &&
// Remove "--gruntfile=project/Gruntfile.js" and "--gruntfile".
!(/^--gruntfile(=.*)?/.test(arg)) &&
// Remove anything that follows "--gruntfile" (i.e. as its argument).
!(index > 0 && arr[index-1] === '--gruntfile')
);
});

var done = this.async();
var errorCount = 0;
// Get process.argv options without grunt.cli.tasks to pass to child processes
var cliArgs = _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks));
// Get it's own gruntfile
var ownGruntfile = grunt.option('gruntfile') || grunt.file.expand({filter: 'isFile'}, '{G,g}runtfile.{js,coffee}')[0];
ownGruntfile = path.resolve(process.cwd(), ownGruntfile || '');

var lastGruntFileWritten;
function write(gruntfile, buf, isError) {
if (gruntfile !== lastGruntFileWritten) {
grunt.log.writeln('');
grunt.log.writeln('');
grunt.log.writeln(chalk.cyan('>> ') + gruntfile + ':\n');
}
grunt.log[(isError) ? 'error' : 'write'](buf);
lastGruntFileWritten = gruntfile;
}
// Manage buffered and unbuffered output from gruntfiles.
var outputManager = {
lastGruntFileWritten: undefined,
outputBuffers: {},

// our queue for concurrently ran tasks
var queue = async.queue(function(run, next) {
var skipNext = false;
grunt.log.ok('Running [' + run.tasks + '] on ' + run.gruntfile);
if (cliArgs) {
cliArgs = cliArgs.filter(function(currentValue) {
if (skipNext) return (skipNext = false);
var out = /^--gruntfile(=?)/.exec(currentValue);
if (out) {
if (out[1] !== '=') skipNext = true;
return false;
init: function(gruntfile) {
if (options.bufferOutput) {
outputManager.outputBuffers[gruntfile] = [];
}
},
write: function(gruntfile, fn, data) {
if (options.bufferOutput) {
outputManager.outputBuffers[gruntfile].push({
fn: fn,
data: data
});
}
else {
if (gruntfile !== outputManager.lastGruntFileWritten) {
grunt.log.writeln('');
grunt.log.writeln('');
grunt.log.writeln(chalk.cyan('>> ') + gruntfile + ':\n');
}
return true;
});
fn(data);
outputManager.lastGruntFileWritten = gruntfile;
}
},
flush: function(gruntfile) {
if (options.bufferOutput) {
grunt.log.writeln('');
grunt.log.writeln(chalk.cyan('>> ') + 'From ' + gruntfile + ':\n');
outputManager.outputBuffers[gruntfile].forEach(function(lineData) {
lineData.fn(lineData.data);
});
outputManager.outputBuffers[gruntfile] = [];
}
}
var child = grunt.util.spawn({
// Use grunt to run the tasks
grunt: true,
// Run from dirname of gruntfile
opts: {cwd: path.dirname(run.gruntfile)},
// Run task to be run and any cli options
args: run.tasks.concat(cliArgs || [], '--gruntfile=' + run.gruntfile)
}, function(err, res, code) {
if (err) { errorCount++; }
next();
});
child.stdout.on('data', function(buf) {
write(run.gruntfile, buf);
});
child.stderr.on('data', function(buf) {
write(run.gruntfile, buf, true);
});
}, options.concurrent);
}

var errorCount = 0;
var asyncTasks = {};

// When the queue is all done
queue.drain = function() {
done((errorCount === 0));
};
// Create the async task callbacks and dependencies.
// See https://github.com/caolan/async#auto.
this.files.forEach(function(filesMapping) {

this.files.forEach(function(files) {
var gruntfiles = grunt.file.expand({filter: 'isFile'}, files.src);
// Display a warning if no files were matched
var gruntfiles = grunt.file.expand({filter: 'isFile'}, filesMapping.src)
.map(function(gruntfile) {
return path.resolve(gruntfile);
});
if (!options.allowSelf) {
gruntfiles = _.without(gruntfiles, ownGruntfile);
}
if (!gruntfiles.length) {
grunt.log.warn('No Gruntfiles matched the file patterns: "' + files.orig.src.join(', ') + '"');
grunt.log.warn('No Gruntfiles matched the file patterns: "' + filesMapping.orig.src.join(', ') + '"');
return;
}

gruntfiles.forEach(function(gruntfile) {
gruntfile = path.resolve(process.cwd(), gruntfile);

// Skip it's own gruntfile. Prevents infinite loops.
if (!options.allowSelf && gruntfile === ownGruntfile) { return; }
// Get the dependencies for this Gruntfile.
var dependencies = [];
var dependencyFn = options.dependencyFn;
if (dependencyFn) {
if (typeof dependencyFn === 'string') {
dependencyFn = defaultDependencyFns[dependencyFn];
}
try {
dependencies = dependencyFn(gruntfile, gruntfiles);
}
catch (e) {
grunt.log.error(
'Could not get dependencies for Gruntfile (' + e.message + '): ' + gruntfile + '. ' +
'Assuming no dependencies.');
}

dependencies.forEach(function(dependency) {
if (!_.contains(gruntfiles, dependency)) {
grunt.log.warn('Dependency "' + dependency + '" not contained in src glob (dependency of ' + gruntfile + ')');
}
})
}

queue.push({
gruntfile: gruntfile,
tasks: args || files.tasks || ['default']
});
// Get the subtasks to run.
var gruntTasksToRun = (
(args.length < 1 ? false : args) ||
filesMapping.tasks ||
['default']
);

// Create the async task function to run once all dependencies have run.
// Output is collected and printed as a batch once the task completes.
var asyncTaskFn = function(callback) {
grunt.log.writeln('');
grunt.log.writeln(chalk.cyan('>> ') + 'Running [' + gruntTasksToRun + '] on ' + gruntfile);

outputManager.init(gruntfile);

// Spawn the child process.
var child = grunt.util.spawn({
grunt: true,
opts: {cwd: path.dirname(gruntfile)},
args: [].concat(gruntTasksToRun, cliArgs || [], '--gruntfile=' + gruntfile)
}, function(err, res, code) {
if (err) { errorCount++; }
outputManager.flush(gruntfile);
callback(err);
});

// Buffer its stdout and stderr, to be printed on completion.
child.stdout.on('data', function(data) {
outputManager.write(gruntfile, grunt.log.write, data);
});
child.stderr.on('data', function(data) {
outputManager.write(gruntfile, grunt.log.error, data);
});
};

asyncTasks[gruntfile] = dependencies.concat([asyncTaskFn]);
});
});

//After processing all files and queueing them, make sure that at least one file is queued
if (queue.idle()) {
// If the queue is idle, assume nothing was queued and call done() immediately after sending warning
grunt.warn('No Gruntfiles matched any of the provided file patterns');
done();
if (_.isEmpty(asyncTasks)) {
grunt.warn('No Gruntfiles matched any of the provided file patterns');
}
else {
var done = this.async();
async.auto(asyncTasks, function(err, results) {
grunt.log.writeln('');
grunt.log.writeln(chalk.cyan('>> ') + 'From ' + ownGruntfile + ':');
done(err);
// TODO: Re-enable this once caolan/async#637 is merged.
}/*, options.concurrency*/);
}

});
Expand Down