Permalink
Browse files

MDL-49817 grunt: handle multiple watched files changed at once

Includes multiple changes to the shifter task to simplify and
support this:
* Use grunt.file for shifter yui 'module' detection rather than our own
  70 line function
* Use grunt.util.spawn rather than our own exec for shifter
* Improve behaviour on various yui subdirectories

We have to add the 'async' depndency to npm because we are running
multiple async operations in the single task. We use async.eachSeries to
run each shifter job sequentally (else the output would be unreadable
when running async).

We also run shifter in non-recursive mode on the module directory so its
not building everything (thanks to Ryan for pointing this out!)
  • Loading branch information...
danpoltawski committed Jan 21, 2016
1 parent 0b777a0 commit 1aa454eda43051eae45953dab0bca24b21e24f70
Showing with 87 additions and 128 deletions.
  1. +67 −123 Gruntfile.js
  2. +19 −5 npm-shrinkwrap.json
  3. +1 −0 package.json
View
@@ -25,7 +25,6 @@
module.exports = function(grunt) {
var path = require('path'),
fs = require('fs'),
tasks = {},
cwd = process.env.PWD || process.cwd(),
inAMD = path.basename(cwd) == 'amd';
@@ -92,55 +91,62 @@ module.exports = function(grunt) {
files: ['**/yui/src/**/*.js'],
tasks: ['shifter']
},
},
shifter: {
options: {
recursive: true,
paths: [cwd]
}
}
});
/**
* Shifter task. Is configured with a path to a specific file or a directory,
* in the case of a specific file it will work out the right module to be built.
*
* Note that this task runs the invidiaul shifter jobs async (becase it spawns
* so be careful to to call done().
*/
tasks.shifter = function() {
var exec = require('child_process').spawn,
var async = require('async'),
done = this.async(),
args = [],
options = {
recursive: true,
watch: false,
walk: false,
module: false
},
shifter;
options = grunt.config('shifter.options');
grunt.log.ok("Running shifter on " + cwd);
// Run the shifter processes one at a time to avoid confusing output.
async.eachSeries(options.paths, function (src, filedone) {
var args = [];
args.push( path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
// Always ignore the node_modules directory.
args.push('--excludes', 'node_modules');
// Determine the most appropriate options to run with based upon the current location.
if (path.basename(cwd) === 'src') {
// Detect whether we're in a src directory.
if (grunt.file.isMatch('**/yui/**/*.js', src)) {
// When passed a JS file, build our containing module (this happen with
// watch).
grunt.log.debug('Shifter passed a specific JS file');
src = path.dirname(path.dirname(src));
options.recursive = false;
} else if (grunt.file.isMatch('**/yui/src', src)) {
// When in a src directory --walk all modules.
grunt.log.debug('In a src directory');
args.push('--walk');
options.walk = true;
} else if (path.basename(path.dirname(cwd)) === 'src') {
// Detect whether we're in a module directory.
options.recursive = false;
} else if (grunt.file.isMatch('**/yui/src/*', src)) {
// When in module, only build our module.
grunt.log.debug('In a module directory');
options.module = true;
}
if (grunt.option('watch')) {
if (!options.walk && !options.module) {
grunt.fail.fatal('Unable to watch unless in a src or module directory');
}
// It is not advisable to run with recursivity and watch - this
// leads to building the build directory in a race-like fashion.
grunt.log.debug('Detected a watch - disabling recursivity');
options.recursive = false;
args.push('--watch');
} else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
// When in module src, only build our module.
grunt.log.debug('In a source directory');
src = path.dirname(src);
options.recursive = false;
}
if (options.recursive) {
args.push('--recursive');
if (grunt.option('watch')) {
grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
}
// Always ignore the node_modules directory.
args.push('--excludes', 'node_modules');
// Add the stderr option if appropriate
if (grunt.option('verbose')) {
args.push('--lint-stderr');
@@ -152,19 +158,17 @@ module.exports = function(grunt) {
var execShifter = function() {
shifter = exec("node", args, {
cwd: cwd,
stdio: 'inherit',
env: process.env
});
// Tidy up after exec.
shifter.on('exit', function (code) {
grunt.log.ok("Running shifter on " + src);
grunt.util.spawn({
cmd: "node",
args: args,
opts: {cwd: src, stdio: 'inherit', env: process.env}
}, function (error, result, code) {
if (code) {
grunt.fail.fatal('Shifter failed with code: ' + code);
} else {
grunt.log.ok('Shifter build complete.');
done();
filedone();
}
});
};
@@ -174,79 +178,15 @@ module.exports = function(grunt) {
execShifter();
} else {
// Check that there are yui modules otherwise shifter ends with exit code 1.
var found = false;
var hasYuiModules = function(directory, callback) {
fs.readdir(directory, function(err, files) {
if (err) {
return callback(err, null);
}
// If we already found a match there is no need to continue scanning.
if (found === true) {
return;
}
// We need to track the number of files to know when we return a result.
var pending = files.length;
// We first check files, so if there is a match we don't need further
// async calls and we just return a true.
for (var i = 0; i < files.length; i++) {
if (files[i] === 'yui') {
return callback(null, true);
}
}
// Iterate through subdirs if there were no matches.
files.forEach(function (file) {
var p = path.join(directory, file);
var stat = fs.statSync(p);
if (!stat.isDirectory()) {
pending--;
} else {
// We defer the pending-1 until we scan the whole dir and subdirs.
hasYuiModules(p, function(err, result) {
if (err) {
return callback(err);
}
if (result === true) {
// Once we get a true we notify the caller.
found = true;
return callback(null, true);
}
pending--;
if (pending === 0) {
// Notify the caller that the whole dir has been scaned and there are no matches.
return callback(null, false);
}
});
}
// No subdirs here, otherwise the return would be deferred until all subdirs are scanned.
if (pending === 0) {
return callback(null, false);
}
});
});
};
hasYuiModules(cwd, function(err, result) {
if (err) {
grunt.fail.fatal(err.message);
}
if (result === true) {
execShifter();
} else {
grunt.log.ok('No YUI modules to build.');
done();
}
});
if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
args.push('--recursive');
execShifter();
} else {
grunt.log.ok('No YUI modules to build.');
filedone();
}
}
}, done);
};
tasks.startup = function() {
@@ -263,17 +203,21 @@ module.exports = function(grunt) {
}
};
// On watch, we dynamically modify config to build only affected files. This
// method is slightly complicated to deal with multiple changed files at once (copied
// from the grunt-contrib-watch readme).
var changedFiles = Object.create(null);
var onChange = grunt.util._.debounce(function() {
var files = Object.keys(changedFiles);
grunt.config('jshint.amd.src', files);
grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglify_rename }]);
grunt.config('shifter.options.paths', files);
changedFiles = Object.create(null);
}, 200);
// On watch, we dynamically modify config to build only affected files.
grunt.event.on('watch', function(action, filepath) {
grunt.config('jshint.amd.src', filepath);
grunt.config('uglify.amd.files', [{ expand: true, src: filepath, rename: uglify_rename }]);
if (filepath.match('yui')) {
// Set the cwd to the base directory for yui modules which have changed.
cwd = filepath.split(path.sep + 'yui' + path.sep + 'src').shift();
} else {
cwd = process.env.PWD || process.cwd();
}
changedFiles[filepath] = action;
onChange();
});
// Register NPM tasks.
View

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
View
@@ -3,6 +3,7 @@
"private": true,
"description": "Moodle",
"devDependencies": {
"async": "^1.5.2",
"grunt": "0.4.5",
"grunt-contrib-jshint": "0.11.3",
"grunt-contrib-less": "1.1.0",

0 comments on commit 1aa454e

Please sign in to comment.