5 * Copyright (c) 2012 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
10 var grunt = require('../grunt');
13 var path = require('path');
14 var fs = require('fs');
15 // In Nodejs 0.8.0, existsSync moved from path -> fs.
16 var existsSync = fs.existsSync || path.existsSync;
18 // Extend generic "task" utils lib.
19 var parent = grunt.utils.task.create();
21 // The module to be exported.
22 var task = module.exports = Object.create(parent);
24 // An internal registry of tasks and handlers.
27 // Keep track of the number of log.error() calls.
30 // Override built-in registerTask.
31 task.registerTask = function(name, info, fn) {
32 // Add task to registry.
33 registry.tasks.push(name);
35 parent.registerTask.apply(task, arguments);
36 // This task, now that it's been registered.
37 var thisTask = task._tasks[name];
38 // Override task function.
39 var _fn = thisTask.fn;
40 thisTask.fn = function(arg) {
41 // Initialize the errorcount for this task.
42 errorcount = grunt.fail.errorcount;
43 // Return the number of errors logged during this task.
44 this.__defineGetter__('errorCount', function() {
45 return grunt.fail.errorcount - errorcount;
47 // Expose task.requires on `this`.
48 this.requires = task.requires.bind(task);
49 // Expose config.requires on `this`.
50 this.requiresConfig = grunt.config.requires;
51 // If this task was an alias or a multi task called without a target,
52 // only log if in verbose mode.
53 var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log';
55 grunt[logger].header('Running "' + this.nameArgs + '"' +
56 (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task');
57 // Actually run the task.
58 return _fn.apply(this, arguments);
63 // This is the most common "multi task" pattern.
64 task.registerMultiTask = function(name, info, fn) {
65 // Store a reference to the task object, in case the task gets renamed.
67 task.registerTask(name, info, function(target) {
68 // Guaranteed to always be the actual task name.
69 var name = thisTask.name;
70 // If a target wasn't specified, run this task once for each target.
71 if (!target || target === '*') {
72 return task.runAllTargets(name, grunt.utils.toArray(arguments).slice(1));
74 // Fail if any required config properties have been omitted.
75 this.requiresConfig([name, target]);
76 // Expose data on `this` (as well as task.current).
77 this.data = grunt.config([name, target]);
78 // Expose file object on `this` (as well as task.current).
80 // Handle data structured like either:
82 // {prop: {src: [srcfiles], dest: 'destfile'}}.
83 if (grunt.utils.kindOf(this.data) === 'object') {
84 if ('src' in this.data) { this.file.src = this.data.src; }
85 if ('dest' in this.data) { this.file.dest = this.data.dest; }
87 this.file.src = this.data;
88 this.file.dest = target;
90 // Process src as a template (recursively, if necessary).
92 this.file.src = grunt.utils.recurse(this.file.src, function(src) {
93 if (typeof src !== 'string') { return src; }
94 return grunt.template.process(src);
97 // Process dest as a template.
99 this.file.dest = grunt.template.process(this.file.dest);
101 // Expose the current target.
102 this.target = target;
103 // Remove target from args.
104 this.args = grunt.utils.toArray(arguments).slice(1);
105 // Recreate flags object so that the target isn't set as a flag.
107 this.args.forEach(function(arg) { this.flags[arg] = true; }, this);
108 // Call original task function, passing in the target and any other args.
109 return fn.apply(this, this.args);
111 thisTask = task._tasks[name];
112 thisTask.multi = true;
115 // Init tasks don't require properties in config, and as such will preempt
116 // config loading errors.
117 task.registerInitTask = function(name, info, fn) {
118 task.registerTask(name, info, fn);
119 task._tasks[name].init = true;
122 // Override built-in registerHelper to use the registry.
123 task.registerHelper = function(name, fn) {
124 // Add task to registry.
125 registry.helpers.push(name);
126 // Actually register task.
127 return parent.registerHelper.apply(task, arguments);
130 // If a property wasn't passed, run all task targets in turn.
131 task.runAllTargets = function(taskname, args) {
132 // Get an array of sub-property keys under the given config object.
133 var targets = Object.keys(grunt.config(taskname) || {});
134 // Fail if there are no actual properties to iterate over.
135 if (targets.length === 0) {
136 grunt.log.error('No "' + taskname + '" targets found.');
139 // Iterate over all properties not starting with _, running a task for each.
140 targets.filter(function(target) {
141 return !/^_/.test(target);
142 }).forEach(function(target) {
143 // Be sure to pass in any additionally specified args.
144 task.run([taskname, target].concat(args || []).join(':'));
148 // Load tasks and handlers from a given tasks file.
149 var loadTaskStack = [];
150 function loadTask(filepath) {
151 // In case this was called recursively, save registry for later.
152 loadTaskStack.push({tasks: registry.tasks, helpers: registry.helpers});
155 registry.helpers = [];
156 var filename = path.basename(filepath);
157 var msg = 'Loading "' + filename + '" tasks and helpers...';
161 fn = require(path.resolve(filepath));
162 if (typeof fn === 'function') {
163 fn.call(grunt, grunt);
165 grunt.verbose.write(msg).ok();
166 if (registry.tasks.length === 0 && registry.helpers.length === 0) {
167 grunt.verbose.error('No new tasks or helpers found.');
169 if (registry.tasks.length > 0) {
170 grunt.verbose.writeln('Tasks: ' + grunt.log.wordlist(registry.tasks));
172 if (registry.helpers.length > 0) {
173 grunt.verbose.writeln('Helpers: ' + grunt.log.wordlist(registry.helpers));
177 // Something went wrong.
178 grunt.log.write(msg).error().verbose.error(e.stack).or.error(e);
181 var obj = loadTaskStack.pop() || {};
182 registry.tasks = obj.tasks || [];
183 registry.helpers = obj.helpers || [];
186 // Log a message when loading tasks.
187 function loadTasksMessage(info) {
188 grunt.verbose.subhead('Registering ' + info + ' tasks.');
191 // Load tasks and handlers from a given directory.
192 function loadTasks(tasksdir) {
194 fs.readdirSync(tasksdir).filter(function(filename) {
195 // Filter out non-.js files.
196 return path.extname(filename).toLowerCase() === '.js';
197 }).forEach(function(filename) {
199 loadTask(path.join(tasksdir, filename));
202 grunt.log.verbose.error(e.stack).or.error(e);
206 // Directories to be searched for tasks files and "extra" files.
207 task.searchDirs = [];
209 // Return an array of all task-specific file paths that match the given
210 // wildcard patterns. Instead of returing a string for each file path, return
211 // an object with useful properties. When coerced to String, each object will
212 // yield its absolute path.
213 function expandByMethod(method) {
214 var args = grunt.utils.toArray(arguments).slice(1);
215 // If the first argument is an options object, remove and save it for later.
216 var options = grunt.utils.kindOf(args[0]) === 'object' ? args.shift() : {};
217 // Use the first argument if it's an Array, otherwise convert the arguments
218 // object to an array and use that.
219 var patterns = Array.isArray(args[0]) ? args[0] : args;
221 // When any returned array item is used in a string context, return the
223 var toString = function() { return this.abs; };
224 // Iterate over all searchDirs.
225 task.searchDirs.forEach(function(dirpath) {
226 // Create an array of absolute patterns.
227 var args = patterns.map(function(pattern) {
228 return path.join(dirpath, pattern);
230 // Add the options object back onto the beginning of the arguments array.
231 args.unshift(options);
232 // Expand the paths in case a wildcard was passed.
233 grunt.file[method].apply(null, args).forEach(function(abspath) {
234 var relpath = abspath.slice(dirpath.length + 1);
235 if (relpath in filepaths) { return; }
236 // Update object at this relpath only if it doesn't already exist.
237 filepaths[relpath] = {
240 base: abspath.slice(0, dirpath.length),
245 // Return an array of objects.
246 return Object.keys(filepaths).map(function(relpath) {
247 return filepaths[relpath];
251 // A few type-specific task expansion methods. These methods all return arrays
253 task.expand = expandByMethod.bind(task, 'expand');
254 task.expandDirs = expandByMethod.bind(task, 'expandDirs');
255 task.expandFiles = expandByMethod.bind(task, 'expandFiles');
257 // Get a single task file path.
258 task.getFile = function() {
259 var filepath = path.join.apply(path, arguments);
260 var fileobj = task.expand(filepath)[0];
261 return fileobj ? String(fileobj) : null;
264 // Read JSON defaults from task files (if they exist), merging them into one.
266 var readDefaults = {};
267 task.readDefaults = function() {
268 var filepath = path.join.apply(path, arguments);
269 var result = readDefaults[filepath];
272 result = readDefaults[filepath] = {};
273 // Find all matching taskfiles.
274 filepaths = task.searchDirs.map(function(dirpath) {
275 return path.join(dirpath, filepath);
276 }).filter(function(filepath) {
277 return existsSync(filepath) && fs.statSync(filepath).isFile();
279 // Load defaults data.
280 if (filepaths.length) {
281 grunt.verbose.subhead('Loading data from ' + filepath);
282 // Since extras path order goes from most-specific to least-specific, only
283 // add-in properties that don't already exist.
284 filepaths.forEach(function(filepath) {
285 grunt.utils._.defaults(result, grunt.file.readJSON(filepath));
292 // Load tasks and handlers from a given directory.
293 task.loadTasks = function(tasksdir) {
294 loadTasksMessage('"' + tasksdir + '"');
295 if (existsSync(tasksdir)) {
296 task.searchDirs.unshift(tasksdir);
299 grunt.log.error('Tasks directory "' + tasksdir + '" not found.');
303 // Load tasks and handlers from a given locally-installed Npm module (installed
304 // relative to the base dir).
305 task.loadNpmTasks = function(name) {
306 loadTasksMessage('"' + name + '" local Npm module');
307 var root = path.resolve('node_modules');
308 var pkgfile = path.join(root, name, 'package.json');
309 var pkg = existsSync(pkgfile) ? grunt.file.readJSON(pkgfile) : {};
311 // Process collection plugins.
312 if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) {
313 Object.keys(pkg.dependencies).forEach(function(depName) {
314 // Npm sometimes pulls dependencies out if they're shared, so find
315 // upwards if not found locally.
316 var filepath = grunt.file.findup(path.resolve('node_modules', name),
317 'node_modules/' + depName);
319 // Load this task plugin recursively.
320 task.loadNpmTasks(path.relative(root, filepath));
326 // Process task plugins.
327 var tasksdir = path.join(root, name, 'tasks');
328 if (existsSync(tasksdir)) {
329 task.searchDirs.unshift(tasksdir);
332 grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?');
336 // Load tasks and handlers from a given Npm module, installed relative to the
337 // version of grunt being run.
338 function loadNpmTasksWithRequire(name) {
339 loadTasksMessage('"' + name + '" npm module');
342 dirpath = require.resolve(name);
343 dirpath = path.resolve(path.join(path.dirname(dirpath), 'tasks'));
344 if (existsSync(dirpath)) {
345 task.searchDirs.unshift(dirpath);
351 grunt.log.error('Npm module "' + name + '" not found. Is it installed?');
355 task.init = function(tasks, options) {
356 if (!options) { options = {}; }
358 // Load all built-in tasks.
359 var tasksdir = path.resolve(__dirname, '../../tasks');
360 task.searchDirs.unshift(tasksdir);
361 loadTasksMessage('built-in');
364 // Grunt was loaded from a Npm-installed plugin bin script. Load any tasks
365 // that were specified via grunt.npmTasks.
366 grunt._npmTasks.forEach(loadNpmTasksWithRequire);
368 // Were only init tasks specified?
369 var allInit = tasks.length > 0 && tasks.every(function(name) {
370 var obj = task._taskPlusArgs(name).task;
371 return obj && obj.init;
374 // Get any local configfile or tasks that might exist. Use --config override
375 // if specified, otherwise search the current directory or any parent.
376 var configfile = allInit ? null : grunt.option('config') ||
377 grunt.file.findup(process.cwd(), 'grunt.js');
379 var msg = 'Reading "' + path.basename(configfile) + '" config file...';
380 if (configfile && existsSync(configfile)) {
381 grunt.verbose.writeln().write(msg).ok();
382 // Change working directory so that all paths are relative to the
383 // configfile's location (or the --base option, if specified).
384 process.chdir(grunt.option('base') || path.dirname(configfile));
385 // Load local tasks, if the file exists.
386 loadTask(configfile);
387 } else if (options.help || allInit) {
388 // Don't complain about missing config file.
389 } else if (grunt.option('config')) {
390 // If --config override was specified and it doesn't exist, complain.
391 grunt.log.writeln().write(msg).error();
392 grunt.fatal('Unable to find "' + configfile + '" config file.', 2);
393 } else if (!grunt.option('help')) {
394 grunt.verbose.writeln().write(msg).error();
395 grunt.fatal('Unable to find "grunt.js" config file. Do you need any --help?', 2);
398 // Load all user-specified --npm tasks.
399 (grunt.option('npm') || []).forEach(task.loadNpmTasks);
400 // Load all user-specified --tasks.
401 (grunt.option('tasks') || []).forEach(task.loadTasks);
403 // Load user .grunt tasks.
404 tasksdir = grunt.file.userDir('tasks');
406 task.searchDirs.unshift(tasksdir);
407 loadTasksMessage('user');
411 // Search dirs should be unique and fully normalized absolute paths.
412 task.searchDirs = grunt.utils._.uniq(task.searchDirs).map(function(filepath) {
413 return path.resolve(filepath);