--- /dev/null
+'use strict';
+
+var grunt = require('../grunt');
+
+// Nodejs libs.
+var path = require('path');
+
+// Extend generic "task" util lib.
+var parent = grunt.util.task.create();
+
+// The module to be exported.
+var task = module.exports = Object.create(parent);
+
+// A temporary registry of tasks and metadata.
+var registry = {tasks: [], untasks: [], meta: {}};
+
+// The last specified tasks message.
+var lastInfo;
+
+// Number of levels of recursion when loading tasks in collections.
+var loadTaskDepth = 0;
+
+// Keep track of the number of log.error() calls.
+var errorcount;
+
+// Override built-in registerTask.
+task.registerTask = function(name) {
+ // Add task to registry.
+ registry.tasks.push(name);
+ // Register task.
+ parent.registerTask.apply(task, arguments);
+ // This task, now that it's been registered.
+ var thisTask = task._tasks[name];
+ // Metadata about the current task.
+ thisTask.meta = grunt.util._.clone(registry.meta);
+ // Override task function.
+ var _fn = thisTask.fn;
+ thisTask.fn = function(arg) {
+ // Guaranteed to always be the actual task name.
+ var name = thisTask.name;
+ // Initialize the errorcount for this task.
+ errorcount = grunt.fail.errorcount;
+ // Return the number of errors logged during this task.
+ Object.defineProperty(this, 'errorCount', {
+ enumerable: true,
+ get: function() {
+ return grunt.fail.errorcount - errorcount;
+ }
+ });
+ // Expose task.requires on `this`.
+ this.requires = task.requires.bind(task);
+ // Expose config.requires on `this`.
+ this.requiresConfig = grunt.config.requires;
+ // Return an options object with the specified defaults overwritten by task-
+ // specific overrides, via the "options" property.
+ this.options = function() {
+ var args = [{}].concat(grunt.util.toArray(arguments)).concat([
+ grunt.config([name, 'options'])
+ ]);
+ var options = grunt.util._.extend.apply(null, args);
+ grunt.verbose.writeflags(options, 'Options');
+ return options;
+ };
+ // If this task was an alias or a multi task called without a target,
+ // only log if in verbose mode.
+ var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log';
+ // Actually log.
+ grunt[logger].header('Running "' + this.nameArgs + '"' +
+ (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task');
+ // If --debug was specified, log the path to this task's source file.
+ grunt[logger].debug('Task source: ' + thisTask.meta.filepath);
+ // Actually run the task.
+ return _fn.apply(this, arguments);
+ };
+ return task;
+};
+
+// Multi task targets can't start with _ or be a reserved property (options).
+function isValidMultiTaskTarget(target) {
+ return !/^_|^options$/.test(target);
+}
+
+// Normalize multi task files.
+task.normalizeMultiTaskFiles = function(data, target) {
+ var prop, obj;
+ var files = [];
+ if (grunt.util.kindOf(data) === 'object') {
+ if ('src' in data || 'dest' in data) {
+ obj = {};
+ for (prop in data) {
+ if (prop !== 'options') {
+ obj[prop] = data[prop];
+ }
+ }
+ files.push(obj);
+ } else if (grunt.util.kindOf(data.files) === 'object') {
+ for (prop in data.files) {
+ files.push({src: data.files[prop], dest: grunt.config.process(prop)});
+ }
+ } else if (Array.isArray(data.files)) {
+ grunt.util._.flattenDeep(data.files).forEach(function(obj) {
+ var prop;
+ if ('src' in obj || 'dest' in obj) {
+ files.push(obj);
+ } else {
+ for (prop in obj) {
+ files.push({src: obj[prop], dest: grunt.config.process(prop)});
+ }
+ }
+ });
+ }
+ } else {
+ files.push({src: data, dest: grunt.config.process(target)});
+ }
+
+ // If no src/dest or files were specified, return an empty files array.
+ if (files.length === 0) {
+ grunt.verbose.writeln('File: ' + '[no files]'.yellow);
+ return [];
+ }
+
+ // Process all normalized file objects.
+ files = grunt.util._(files).chain().forEach(function(obj) {
+ if (!('src' in obj) || !obj.src) { return; }
+ // Normalize .src properties to flattened array.
+ if (Array.isArray(obj.src)) {
+ obj.src = grunt.util._.flatten(obj.src);
+ } else {
+ obj.src = [obj.src];
+ }
+ }).map(function(obj) {
+ // Build options object, removing unwanted properties.
+ var expandOptions = grunt.util._.extend({}, obj);
+ delete expandOptions.src;
+ delete expandOptions.dest;
+
+ // Expand file mappings.
+ if (obj.expand) {
+ return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) {
+ // Copy obj properties to result.
+ var result = grunt.util._.extend({}, obj);
+ // Make a clone of the orig obj available.
+ result.orig = grunt.util._.extend({}, obj);
+ // Set .src and .dest, processing both as templates.
+ result.src = grunt.config.process(mapObj.src);
+ result.dest = grunt.config.process(mapObj.dest);
+ // Remove unwanted properties.
+ ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) {
+ delete result[prop];
+ });
+ return result;
+ });
+ }
+
+ // Copy obj properties to result, adding an .orig property.
+ var result = grunt.util._.extend({}, obj);
+ // Make a clone of the orig obj available.
+ result.orig = grunt.util._.extend({}, obj);
+
+ if ('src' in result) {
+ // Expose an expand-on-demand getter method as .src.
+ Object.defineProperty(result, 'src', {
+ enumerable: true,
+ get: function fn() {
+ var src;
+ if (!('result' in fn)) {
+ src = obj.src;
+ // If src is an array, flatten it. Otherwise, make it into an array.
+ src = Array.isArray(src) ? grunt.util._.flatten(src) : [src];
+ // Expand src files, memoizing result.
+ fn.result = grunt.file.expand(expandOptions, src);
+ }
+ return fn.result;
+ }
+ });
+ }
+
+ if ('dest' in result) {
+ result.dest = obj.dest;
+ }
+
+ return result;
+ }).flatten().value();
+
+ // Log this.file src and dest properties when --verbose is specified.
+ if (grunt.option('verbose')) {
+ files.forEach(function(obj) {
+ var output = [];
+ if ('src' in obj) {
+ output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow);
+ }
+ if ('dest' in obj) {
+ output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow));
+ }
+ if (output.length > 0) {
+ grunt.verbose.writeln('Files: ' + output.join(' '));
+ }
+ });
+ }
+
+ return files;
+};
+
+// This is the most common "multi task" pattern.
+task.registerMultiTask = function(name, info, fn) {
+ // If optional "info" string is omitted, shuffle arguments a bit.
+ if (fn == null) {
+ fn = info;
+ info = 'Custom multi task.';
+ }
+ // Store a reference to the task object, in case the task gets renamed.
+ var thisTask;
+ task.registerTask(name, info, function(target) {
+ // Guaranteed to always be the actual task name.
+ var name = thisTask.name;
+ // Arguments (sans target) as an array.
+ this.args = grunt.util.toArray(arguments).slice(1);
+ // If a target wasn't specified, run this task once for each target.
+ if (!target || target === '*') {
+ return task.runAllTargets(name, this.args);
+ } else if (!isValidMultiTaskTarget(target)) {
+ throw new Error('Invalid target "' + target + '" specified.');
+ }
+ // Fail if any required config properties have been omitted.
+ this.requiresConfig([name, target]);
+ // Return an options object with the specified defaults overwritten by task-
+ // and/or target-specific overrides, via the "options" property.
+ this.options = function() {
+ var targetObj = grunt.config([name, target]);
+ var args = [{}].concat(grunt.util.toArray(arguments)).concat([
+ grunt.config([name, 'options']),
+ grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {}
+ ]);
+ var options = grunt.util._.extend.apply(null, args);
+ grunt.verbose.writeflags(options, 'Options');
+ return options;
+ };
+ // Expose the current target.
+ this.target = target;
+ // Recreate flags object so that the target isn't set as a flag.
+ this.flags = {};
+ this.args.forEach(function(arg) { this.flags[arg] = true; }, this);
+ // Expose data on `this` (as well as task.current).
+ this.data = grunt.config([name, target]);
+ // Expose normalized files object.
+ this.files = task.normalizeMultiTaskFiles(this.data, target);
+ // Expose normalized, flattened, uniqued array of src files.
+ Object.defineProperty(this, 'filesSrc', {
+ enumerable: true,
+ get: function() {
+ return grunt.util._(this.files).chain().map('src').flatten().uniq().value();
+ }.bind(this)
+ });
+ // Call original task function, passing in the target and any other args.
+ return fn.apply(this, this.args);
+ });
+
+ thisTask = task._tasks[name];
+ thisTask.multi = true;
+};
+
+// Init tasks don't require properties in config, and as such will preempt
+// config loading errors.
+task.registerInitTask = function(name, info, fn) {
+ task.registerTask(name, info, fn);
+ task._tasks[name].init = true;
+};
+
+// Override built-in renameTask to use the registry.
+task.renameTask = function(oldname, newname) {
+ var result;
+ try {
+ // Actually rename task.
+ result = parent.renameTask.apply(task, arguments);
+ // Add and remove task.
+ registry.untasks.push(oldname);
+ registry.tasks.push(newname);
+ // Return result.
+ return result;
+ } catch (e) {
+ grunt.log.error(e.message);
+ }
+};
+
+// If a property wasn't passed, run all task targets in turn.
+task.runAllTargets = function(taskname, args) {
+ // Get an array of sub-property keys under the given config object.
+ var targets = Object.keys(grunt.config.getRaw(taskname) || {});
+ // Remove invalid target properties.
+ targets = targets.filter(isValidMultiTaskTarget);
+ // Fail if there are no actual properties to iterate over.
+ if (targets.length === 0) {
+ grunt.log.error('No "' + taskname + '" targets found.');
+ return false;
+ }
+ // Iterate over all targets, running a task for each.
+ targets.forEach(function(target) {
+ // Be sure to pass in any additionally specified args.
+ task.run([taskname, target].concat(args || []).join(':'));
+ });
+};
+
+// Load tasks and handlers from a given tasks file.
+var loadTaskStack = [];
+function loadTask(filepath) {
+ // In case this was called recursively, save registry for later.
+ loadTaskStack.push(registry);
+ // Reset registry.
+ registry = {tasks: [], untasks: [], meta: {info: lastInfo, filepath: filepath}};
+ var filename = path.basename(filepath);
+ var msg = 'Loading "' + filename + '" tasks...';
+ var regCount = 0;
+ var fn;
+ try {
+ // Load taskfile.
+ fn = require(path.resolve(filepath));
+ if (typeof fn === 'function') {
+ fn.call(grunt, grunt);
+ }
+ grunt.verbose.write(msg).ok();
+ // Log registered/renamed/unregistered tasks.
+ ['un', ''].forEach(function(prefix) {
+ var list = grunt.util._.chain(registry[prefix + 'tasks']).uniq().sort().value();
+ if (list.length > 0) {
+ regCount++;
+ grunt.verbose.writeln((prefix ? '- ' : '+ ') + grunt.log.wordlist(list));
+ }
+ });
+ if (regCount === 0) {
+ grunt.verbose.warn('No tasks were registered or unregistered.');
+ }
+ } catch (e) {
+ // Something went wrong.
+ grunt.log.write(msg).error().verbose.error(e.stack).or.error(e);
+ }
+ // Restore registry.
+ registry = loadTaskStack.pop() || {};
+}
+
+// Log a message when loading tasks.
+function loadTasksMessage(info) {
+ // Only keep track of names of top-level loaded tasks and collections,
+ // not sub-tasks.
+ if (loadTaskDepth === 0) { lastInfo = info; }
+ grunt.verbose.subhead('Registering ' + info + ' tasks.');
+}
+
+// Load tasks and handlers from a given directory.
+function loadTasks(tasksdir) {
+ try {
+ var files = grunt.file.glob.sync('*.{js,coffee}', {cwd: tasksdir, maxDepth: 1});
+ // Load tasks from files.
+ files.forEach(function(filename) {
+ loadTask(path.join(tasksdir, filename));
+ });
+ } catch (e) {
+ grunt.log.verbose.error(e.stack).or.error(e);
+ }
+}
+
+// Load tasks and handlers from a given directory.
+task.loadTasks = function(tasksdir) {
+ loadTasksMessage('"' + tasksdir + '"');
+ if (grunt.file.exists(tasksdir)) {
+ loadTasks(tasksdir);
+ } else {
+ grunt.log.error('Tasks directory "' + tasksdir + '" not found.');
+ }
+};
+
+// Load tasks and handlers from a given locally-installed Npm module (installed
+// relative to the base dir).
+task.loadNpmTasks = function(name) {
+ loadTasksMessage('"' + name + '" local Npm module');
+ var root = path.resolve('node_modules');
+ var pkgfile = path.join(root, name, 'package.json');
+ var pkg = grunt.file.exists(pkgfile) ? grunt.file.readJSON(pkgfile) : {keywords: []};
+
+ // Process collection plugins.
+ if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) {
+ loadTaskDepth++;
+ Object.keys(pkg.dependencies).forEach(function(depName) {
+ // Npm sometimes pulls dependencies out if they're shared, so find
+ // upwards if not found locally.
+ var filepath = grunt.file.findup('node_modules/' + depName, {
+ cwd: path.resolve('node_modules', name),
+ nocase: true
+ });
+ if (filepath) {
+ // Load this task plugin recursively.
+ task.loadNpmTasks(path.relative(root, filepath));
+ }
+ });
+ loadTaskDepth--;
+ return;
+ }
+
+ // Process task plugins.
+ var tasksdir = path.join(root, name, 'tasks');
+ if (grunt.file.exists(tasksdir)) {
+ loadTasks(tasksdir);
+ } else {
+ grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?');
+ }
+};
+
+// Initialize tasks.
+task.init = function(tasks, options) {
+ if (!options) { options = {}; }
+
+ // Were only init tasks specified?
+ var allInit = tasks.length > 0 && tasks.every(function(name) {
+ var obj = task._taskPlusArgs(name).task;
+ return obj && obj.init;
+ });
+
+ // Get any local Gruntfile or tasks that might exist. Use --gruntfile override
+ // if specified, otherwise search the current directory or any parent.
+ var gruntfile, msg;
+ if (allInit || options.gruntfile === false) {
+ gruntfile = null;
+ } else {
+ gruntfile = grunt.option('gruntfile') ||
+ grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true});
+ msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...';
+ }
+
+ if (options.gruntfile === false) {
+ // Grunt was run as a lib with {gruntfile: false}.
+ } else if (gruntfile && grunt.file.exists(gruntfile)) {
+ grunt.verbose.writeln().write(msg).ok();
+ // Change working directory so that all paths are relative to the
+ // Gruntfile's location (or the --base option, if specified).
+ process.chdir(grunt.option('base') || path.dirname(gruntfile));
+ // Load local tasks, if the file exists.
+ loadTasksMessage('Gruntfile');
+ loadTask(gruntfile);
+ } else if (options.help || allInit) {
+ // Don't complain about missing Gruntfile.
+ } else if (grunt.option('gruntfile')) {
+ // If --config override was specified and it doesn't exist, complain.
+ grunt.log.writeln().write(msg).error();
+ grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
+ } else if (!grunt.option('help')) {
+ grunt.verbose.writeln().write(msg).error();
+ grunt.log.writelns(
+ 'A valid Gruntfile could not be found. Please see the getting ' +
+ 'started guide for more information on how to configure grunt: ' +
+ 'http://gruntjs.com/getting-started'
+ );
+ grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
+ }
+
+ // Load all user-specified --npm tasks.
+ (grunt.option('npm') || []).map(String).forEach(task.loadNpmTasks);
+ // Load all user-specified --tasks.
+ (grunt.option('tasks') || []).map(String).forEach(task.loadTasks);
+};