3 var grunt = require('../grunt');
6 var path = require('path');
8 // Extend generic "task" util lib.
9 var parent = grunt.util.task.create();
11 // The module to be exported.
12 var task = module.exports = Object.create(parent);
14 // A temporary registry of tasks and metadata.
15 var registry = {tasks: [], untasks: [], meta: {}};
17 // The last specified tasks message.
20 // Number of levels of recursion when loading tasks in collections.
21 var loadTaskDepth = 0;
23 // Keep track of the number of log.error() calls.
26 // Override built-in registerTask.
27 task.registerTask = function(name) {
28 // Add task to registry.
29 registry.tasks.push(name);
31 parent.registerTask.apply(task, arguments);
32 // This task, now that it's been registered.
33 var thisTask = task._tasks[name];
34 // Metadata about the current task.
35 thisTask.meta = grunt.util._.clone(registry.meta);
36 // Override task function.
37 var _fn = thisTask.fn;
38 thisTask.fn = function(arg) {
39 // Guaranteed to always be the actual task name.
40 var name = thisTask.name;
41 // Initialize the errorcount for this task.
42 errorcount = grunt.fail.errorcount;
43 // Return the number of errors logged during this task.
44 Object.defineProperty(this, 'errorCount', {
47 return grunt.fail.errorcount - errorcount;
50 // Expose task.requires on `this`.
51 this.requires = task.requires.bind(task);
52 // Expose config.requires on `this`.
53 this.requiresConfig = grunt.config.requires;
54 // Return an options object with the specified defaults overwritten by task-
55 // specific overrides, via the "options" property.
56 this.options = function() {
57 var args = [{}].concat(grunt.util.toArray(arguments)).concat([
58 grunt.config([name, 'options'])
60 var options = grunt.util._.extend.apply(null, args);
61 grunt.verbose.writeflags(options, 'Options');
64 // If this task was an alias or a multi task called without a target,
65 // only log if in verbose mode.
66 var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log';
68 grunt[logger].header('Running "' + this.nameArgs + '"' +
69 (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task');
70 // If --debug was specified, log the path to this task's source file.
71 grunt[logger].debug('Task source: ' + thisTask.meta.filepath);
72 // Actually run the task.
73 return _fn.apply(this, arguments);
78 // Multi task targets can't start with _ or be a reserved property (options).
79 function isValidMultiTaskTarget(target) {
80 return !/^_|^options$/.test(target);
83 // Normalize multi task files.
84 task.normalizeMultiTaskFiles = function(data, target) {
87 if (grunt.util.kindOf(data) === 'object') {
88 if ('src' in data || 'dest' in data) {
91 if (prop !== 'options') {
92 obj[prop] = data[prop];
96 } else if (grunt.util.kindOf(data.files) === 'object') {
97 for (prop in data.files) {
98 files.push({src: data.files[prop], dest: grunt.config.process(prop)});
100 } else if (Array.isArray(data.files)) {
101 grunt.util._.flattenDeep(data.files).forEach(function(obj) {
103 if ('src' in obj || 'dest' in obj) {
107 files.push({src: obj[prop], dest: grunt.config.process(prop)});
113 files.push({src: data, dest: grunt.config.process(target)});
116 // If no src/dest or files were specified, return an empty files array.
117 if (files.length === 0) {
118 grunt.verbose.writeln('File: ' + '[no files]'.yellow);
122 // Process all normalized file objects.
123 files = grunt.util._(files).chain().forEach(function(obj) {
124 if (!('src' in obj) || !obj.src) { return; }
125 // Normalize .src properties to flattened array.
126 if (Array.isArray(obj.src)) {
127 obj.src = grunt.util._.flatten(obj.src);
131 }).map(function(obj) {
132 // Build options object, removing unwanted properties.
133 var expandOptions = grunt.util._.extend({}, obj);
134 delete expandOptions.src;
135 delete expandOptions.dest;
137 // Expand file mappings.
139 return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) {
140 // Copy obj properties to result.
141 var result = grunt.util._.extend({}, obj);
142 // Make a clone of the orig obj available.
143 result.orig = grunt.util._.extend({}, obj);
144 // Set .src and .dest, processing both as templates.
145 result.src = grunt.config.process(mapObj.src);
146 result.dest = grunt.config.process(mapObj.dest);
147 // Remove unwanted properties.
148 ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) {
155 // Copy obj properties to result, adding an .orig property.
156 var result = grunt.util._.extend({}, obj);
157 // Make a clone of the orig obj available.
158 result.orig = grunt.util._.extend({}, obj);
160 if ('src' in result) {
161 // Expose an expand-on-demand getter method as .src.
162 Object.defineProperty(result, 'src', {
166 if (!('result' in fn)) {
168 // If src is an array, flatten it. Otherwise, make it into an array.
169 src = Array.isArray(src) ? grunt.util._.flatten(src) : [src];
170 // Expand src files, memoizing result.
171 fn.result = grunt.file.expand(expandOptions, src);
178 if ('dest' in result) {
179 result.dest = obj.dest;
183 }).flatten().value();
185 // Log this.file src and dest properties when --verbose is specified.
186 if (grunt.option('verbose')) {
187 files.forEach(function(obj) {
190 output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow);
193 output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow));
195 if (output.length > 0) {
196 grunt.verbose.writeln('Files: ' + output.join(' '));
204 // This is the most common "multi task" pattern.
205 task.registerMultiTask = function(name, info, fn) {
206 // If optional "info" string is omitted, shuffle arguments a bit.
209 info = 'Custom multi task.';
211 // Store a reference to the task object, in case the task gets renamed.
213 task.registerTask(name, info, function(target) {
214 // Guaranteed to always be the actual task name.
215 var name = thisTask.name;
216 // Arguments (sans target) as an array.
217 this.args = grunt.util.toArray(arguments).slice(1);
218 // If a target wasn't specified, run this task once for each target.
219 if (!target || target === '*') {
220 return task.runAllTargets(name, this.args);
221 } else if (!isValidMultiTaskTarget(target)) {
222 throw new Error('Invalid target "' + target + '" specified.');
224 // Fail if any required config properties have been omitted.
225 this.requiresConfig([name, target]);
226 // Return an options object with the specified defaults overwritten by task-
227 // and/or target-specific overrides, via the "options" property.
228 this.options = function() {
229 var targetObj = grunt.config([name, target]);
230 var args = [{}].concat(grunt.util.toArray(arguments)).concat([
231 grunt.config([name, 'options']),
232 grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {}
234 var options = grunt.util._.extend.apply(null, args);
235 grunt.verbose.writeflags(options, 'Options');
238 // Expose the current target.
239 this.target = target;
240 // Recreate flags object so that the target isn't set as a flag.
242 this.args.forEach(function(arg) { this.flags[arg] = true; }, this);
243 // Expose data on `this` (as well as task.current).
244 this.data = grunt.config([name, target]);
245 // Expose normalized files object.
246 this.files = task.normalizeMultiTaskFiles(this.data, target);
247 // Expose normalized, flattened, uniqued array of src files.
248 Object.defineProperty(this, 'filesSrc', {
251 return grunt.util._(this.files).chain().map('src').flatten().uniq().value();
254 // Call original task function, passing in the target and any other args.
255 return fn.apply(this, this.args);
258 thisTask = task._tasks[name];
259 thisTask.multi = true;
262 // Init tasks don't require properties in config, and as such will preempt
263 // config loading errors.
264 task.registerInitTask = function(name, info, fn) {
265 task.registerTask(name, info, fn);
266 task._tasks[name].init = true;
269 // Override built-in renameTask to use the registry.
270 task.renameTask = function(oldname, newname) {
273 // Actually rename task.
274 result = parent.renameTask.apply(task, arguments);
275 // Add and remove task.
276 registry.untasks.push(oldname);
277 registry.tasks.push(newname);
281 grunt.log.error(e.message);
285 // If a property wasn't passed, run all task targets in turn.
286 task.runAllTargets = function(taskname, args) {
287 // Get an array of sub-property keys under the given config object.
288 var targets = Object.keys(grunt.config.getRaw(taskname) || {});
289 // Remove invalid target properties.
290 targets = targets.filter(isValidMultiTaskTarget);
291 // Fail if there are no actual properties to iterate over.
292 if (targets.length === 0) {
293 grunt.log.error('No "' + taskname + '" targets found.');
296 // Iterate over all targets, running a task for each.
297 targets.forEach(function(target) {
298 // Be sure to pass in any additionally specified args.
299 task.run([taskname, target].concat(args || []).join(':'));
303 // Load tasks and handlers from a given tasks file.
304 var loadTaskStack = [];
305 function loadTask(filepath) {
306 // In case this was called recursively, save registry for later.
307 loadTaskStack.push(registry);
309 registry = {tasks: [], untasks: [], meta: {info: lastInfo, filepath: filepath}};
310 var filename = path.basename(filepath);
311 var msg = 'Loading "' + filename + '" tasks...';
316 fn = require(path.resolve(filepath));
317 if (typeof fn === 'function') {
318 fn.call(grunt, grunt);
320 grunt.verbose.write(msg).ok();
321 // Log registered/renamed/unregistered tasks.
322 ['un', ''].forEach(function(prefix) {
323 var list = grunt.util._.chain(registry[prefix + 'tasks']).uniq().sort().value();
324 if (list.length > 0) {
326 grunt.verbose.writeln((prefix ? '- ' : '+ ') + grunt.log.wordlist(list));
329 if (regCount === 0) {
330 grunt.verbose.warn('No tasks were registered or unregistered.');
333 // Something went wrong.
334 grunt.log.write(msg).error().verbose.error(e.stack).or.error(e);
337 registry = loadTaskStack.pop() || {};
340 // Log a message when loading tasks.
341 function loadTasksMessage(info) {
342 // Only keep track of names of top-level loaded tasks and collections,
344 if (loadTaskDepth === 0) { lastInfo = info; }
345 grunt.verbose.subhead('Registering ' + info + ' tasks.');
348 // Load tasks and handlers from a given directory.
349 function loadTasks(tasksdir) {
351 var files = grunt.file.glob.sync('*.{js,coffee}', {cwd: tasksdir, maxDepth: 1});
352 // Load tasks from files.
353 files.forEach(function(filename) {
354 loadTask(path.join(tasksdir, filename));
357 grunt.log.verbose.error(e.stack).or.error(e);
361 // Load tasks and handlers from a given directory.
362 task.loadTasks = function(tasksdir) {
363 loadTasksMessage('"' + tasksdir + '"');
364 if (grunt.file.exists(tasksdir)) {
367 grunt.log.error('Tasks directory "' + tasksdir + '" not found.');
371 // Load tasks and handlers from a given locally-installed Npm module (installed
372 // relative to the base dir).
373 task.loadNpmTasks = function(name) {
374 loadTasksMessage('"' + name + '" local Npm module');
375 var root = path.resolve('node_modules');
376 var pkgfile = path.join(root, name, 'package.json');
377 var pkg = grunt.file.exists(pkgfile) ? grunt.file.readJSON(pkgfile) : {keywords: []};
379 // Process collection plugins.
380 if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) {
382 Object.keys(pkg.dependencies).forEach(function(depName) {
383 // Npm sometimes pulls dependencies out if they're shared, so find
384 // upwards if not found locally.
385 var filepath = grunt.file.findup('node_modules/' + depName, {
386 cwd: path.resolve('node_modules', name),
390 // Load this task plugin recursively.
391 task.loadNpmTasks(path.relative(root, filepath));
398 // Process task plugins.
399 var tasksdir = path.join(root, name, 'tasks');
400 if (grunt.file.exists(tasksdir)) {
403 grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?');
408 task.init = function(tasks, options) {
409 if (!options) { options = {}; }
411 // Were only init tasks specified?
412 var allInit = tasks.length > 0 && tasks.every(function(name) {
413 var obj = task._taskPlusArgs(name).task;
414 return obj && obj.init;
417 // Get any local Gruntfile or tasks that might exist. Use --gruntfile override
418 // if specified, otherwise search the current directory or any parent.
420 if (allInit || options.gruntfile === false) {
423 gruntfile = grunt.option('gruntfile') ||
424 grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true});
425 msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...';
428 if (options.gruntfile === false) {
429 // Grunt was run as a lib with {gruntfile: false}.
430 } else if (gruntfile && grunt.file.exists(gruntfile)) {
431 grunt.verbose.writeln().write(msg).ok();
432 // Change working directory so that all paths are relative to the
433 // Gruntfile's location (or the --base option, if specified).
434 process.chdir(grunt.option('base') || path.dirname(gruntfile));
435 // Load local tasks, if the file exists.
436 loadTasksMessage('Gruntfile');
438 } else if (options.help || allInit) {
439 // Don't complain about missing Gruntfile.
440 } else if (grunt.option('gruntfile')) {
441 // If --config override was specified and it doesn't exist, complain.
442 grunt.log.writeln().write(msg).error();
443 grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
444 } else if (!grunt.option('help')) {
445 grunt.verbose.writeln().write(msg).error();
447 'A valid Gruntfile could not be found. Please see the getting ' +
448 'started guide for more information on how to configure grunt: ' +
449 'http://gruntjs.com/getting-started'
451 grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
454 // Load all user-specified --npm tasks.
455 (grunt.option('npm') || []).map(String).forEach(task.loadNpmTasks);
456 // Load all user-specified --tasks.
457 (grunt.option('tasks') || []).map(String).forEach(task.loadTasks);