UNPKG

16 kBJavaScriptView Raw
1'use strict';
2
3// Keep track of the number of log.error() calls and the last specified tasks message.
4var errorcount, lastInfo;
5
6var grunt = require('../grunt');
7
8// Nodejs libs.
9var path = require('path');
10
11// Extend generic "task" util lib.
12var parent = grunt.util.task.create();
13
14// The module to be exported.
15var task = module.exports = Object.create(parent);
16
17// A temporary registry of tasks and metadata.
18var registry = {tasks: [], untasks: [], meta: {}};
19
20// Number of levels of recursion when loading tasks in collections.
21var loadTaskDepth = 0;
22
23// Override built-in registerTask.
24task.registerTask = function(name) {
25 // Add task to registry.
26 registry.tasks.push(name);
27 // Register task.
28 parent.registerTask.apply(task, arguments);
29 // This task, now that it's been registered.
30 var thisTask = task._tasks[name];
31 // Metadata about the current task.
32 thisTask.meta = grunt.util._.clone(registry.meta);
33 // Override task function.
34 var _fn = thisTask.fn;
35 thisTask.fn = function(arg) {
36 // Guaranteed to always be the actual task name.
37 var name = thisTask.name;
38 // Initialize the errorcount for this task.
39 errorcount = grunt.fail.errorcount;
40 // Return the number of errors logged during this task.
41 Object.defineProperty(this, 'errorCount', {
42 enumerable: true,
43 get: function() {
44 return grunt.fail.errorcount - errorcount;
45 }
46 });
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 // Return an options object with the specified defaults overwritten by task-
52 // specific overrides, via the "options" property.
53 this.options = function() {
54 var args = [{}].concat(grunt.util.toArray(arguments)).concat([
55 grunt.config([name, 'options'])
56 ]);
57 var options = grunt.util._.extend.apply(null, args);
58 grunt.verbose.writeflags(options, 'Options');
59 return options;
60 };
61 // If this task was an alias or a multi task called without a target,
62 // only log if in verbose mode.
63 var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log';
64 // Actually log.
65 grunt[logger].header('Running "' + this.nameArgs + '"' +
66 (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task');
67 // If --debug was specified, log the path to this task's source file.
68 grunt[logger].debug('Task source: ' + thisTask.meta.filepath);
69 // Actually run the task.
70 return _fn.apply(this, arguments);
71 };
72 return task;
73};
74
75// Multi task targets can't start with _ or be a reserved property (options).
76function isValidMultiTaskTarget(target) {
77 return !/^_|^options$/.test(target);
78}
79
80// Normalize multi task files.
81task.normalizeMultiTaskFiles = function(data, target) {
82 var prop, obj;
83 var files = [];
84 if (grunt.util.kindOf(data) === 'object') {
85 if ('src' in data || 'dest' in data) {
86 obj = {};
87 for (prop in data) {
88 if (prop !== 'options') {
89 obj[prop] = data[prop];
90 }
91 }
92 files.push(obj);
93 } else if (grunt.util.kindOf(data.files) === 'object') {
94 for (prop in data.files) {
95 files.push({src: data.files[prop], dest: grunt.config.process(prop)});
96 }
97 } else if (Array.isArray(data.files)) {
98 grunt.util._.flattenDeep(data.files).forEach(function(obj) {
99 var prop;
100 if ('src' in obj || 'dest' in obj) {
101 files.push(obj);
102 } else {
103 for (prop in obj) {
104 files.push({src: obj[prop], dest: grunt.config.process(prop)});
105 }
106 }
107 });
108 }
109 } else {
110 files.push({src: data, dest: grunt.config.process(target)});
111 }
112
113 // If no src/dest or files were specified, return an empty files array.
114 if (files.length === 0) {
115 grunt.verbose.writeln('File: ' + '[no files]'.yellow);
116 return [];
117 }
118
119 // Process all normalized file objects.
120 files = grunt.util._(files).chain().forEach(function(obj) {
121 if (!('src' in obj) || !obj.src) { return; }
122 // Normalize .src properties to flattened array.
123 if (Array.isArray(obj.src)) {
124 obj.src = grunt.util._.flatten(obj.src);
125 } else {
126 obj.src = [obj.src];
127 }
128 }).map(function(obj) {
129 // Build options object, removing unwanted properties.
130 var expandOptions = grunt.util._.extend({}, obj);
131 delete expandOptions.src;
132 delete expandOptions.dest;
133
134 // Expand file mappings.
135 if (obj.expand) {
136 return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) {
137 // Copy obj properties to result.
138 var result = grunt.util._.extend({}, obj);
139 // Make a clone of the orig obj available.
140 result.orig = grunt.util._.extend({}, obj);
141 // Set .src and .dest, processing both as templates.
142 result.src = grunt.config.process(mapObj.src);
143 result.dest = grunt.config.process(mapObj.dest);
144 // Remove unwanted properties.
145 ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) {
146 delete result[prop];
147 });
148 return result;
149 });
150 }
151
152 // Copy obj properties to result, adding an .orig property.
153 var result = grunt.util._.extend({}, obj);
154 // Make a clone of the orig obj available.
155 result.orig = grunt.util._.extend({}, obj);
156
157 if ('src' in result) {
158 // Expose an expand-on-demand getter method as .src.
159 Object.defineProperty(result, 'src', {
160 enumerable: true,
161 get: function fn() {
162 var src;
163 if (!('result' in fn)) {
164 src = obj.src;
165 // If src is an array, flatten it. Otherwise, make it into an array.
166 src = Array.isArray(src) ? grunt.util._.flatten(src) : [src];
167 // Expand src files, memoizing result.
168 fn.result = grunt.file.expand(expandOptions, src);
169 }
170 return fn.result;
171 }
172 });
173 }
174
175 if ('dest' in result) {
176 result.dest = obj.dest;
177 }
178
179 return result;
180 }).flatten().value();
181
182 // Log this.file src and dest properties when --verbose is specified.
183 if (grunt.option('verbose')) {
184 files.forEach(function(obj) {
185 var output = [];
186 if ('src' in obj) {
187 output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow);
188 }
189 if ('dest' in obj) {
190 output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow));
191 }
192 if (output.length > 0) {
193 grunt.verbose.writeln('Files: ' + output.join(' '));
194 }
195 });
196 }
197
198 return files;
199};
200
201// This is the most common "multi task" pattern.
202task.registerMultiTask = function(name, info, fn) {
203 // If optional "info" string is omitted, shuffle arguments a bit.
204 if (fn == null) {
205 fn = info;
206 info = 'Custom multi task.';
207 }
208 // Store a reference to the task object, in case the task gets renamed.
209 var thisTask;
210 task.registerTask(name, info, function(target) {
211 // Guaranteed to always be the actual task name.
212 var name = thisTask.name;
213 // Arguments (sans target) as an array.
214 this.args = grunt.util.toArray(arguments).slice(1);
215 // If a target wasn't specified, run this task once for each target.
216 if (!target || target === '*') {
217 return task.runAllTargets(name, this.args);
218 } else if (!isValidMultiTaskTarget(target)) {
219 throw new Error('Invalid target "' + target + '" specified.');
220 }
221 // Fail if any required config properties have been omitted.
222 this.requiresConfig([name, target]);
223 // Return an options object with the specified defaults overwritten by task-
224 // and/or target-specific overrides, via the "options" property.
225 this.options = function() {
226 var targetObj = grunt.config([name, target]);
227 var args = [{}].concat(grunt.util.toArray(arguments)).concat([
228 grunt.config([name, 'options']),
229 grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {}
230 ]);
231 var options = grunt.util._.extend.apply(null, args);
232 grunt.verbose.writeflags(options, 'Options');
233 return options;
234 };
235 // Expose the current target.
236 this.target = target;
237 // Recreate flags object so that the target isn't set as a flag.
238 this.flags = {};
239 this.args.forEach(function(arg) { this.flags[arg] = true; }, this);
240 // Expose data on `this` (as well as task.current).
241 this.data = grunt.config([name, target]);
242 // Expose normalized files object.
243 this.files = task.normalizeMultiTaskFiles(this.data, target);
244 // Expose normalized, flattened, uniqued array of src files.
245 Object.defineProperty(this, 'filesSrc', {
246 enumerable: true,
247 get: function() {
248 return grunt.util._(this.files).chain().map('src').flatten().uniq().value();
249 }.bind(this)
250 });
251 // Call original task function, passing in the target and any other args.
252 return fn.apply(this, this.args);
253 });
254
255 thisTask = task._tasks[name];
256 thisTask.multi = true;
257};
258
259// Init tasks don't require properties in config, and as such will preempt
260// config loading errors.
261task.registerInitTask = function(name, info, fn) {
262 task.registerTask(name, info, fn);
263 task._tasks[name].init = true;
264};
265
266// Override built-in renameTask to use the registry.
267task.renameTask = function(oldname, newname) {
268 var result;
269 try {
270 // Actually rename task.
271 result = parent.renameTask.apply(task, arguments);
272 // Add and remove task.
273 registry.untasks.push(oldname);
274 registry.tasks.push(newname);
275 // Return result.
276 return result;
277 } catch (e) {
278 grunt.log.error(e.message);
279 }
280};
281
282// If a property wasn't passed, run all task targets in turn.
283task.runAllTargets = function(taskname, args) {
284 // Get an array of sub-property keys under the given config object.
285 var targets = Object.keys(grunt.config.getRaw(taskname) || {});
286 // Remove invalid target properties.
287 targets = targets.filter(isValidMultiTaskTarget);
288 // Fail if there are no actual properties to iterate over.
289 if (targets.length === 0) {
290 grunt.log.error('No "' + taskname + '" targets found.');
291 return false;
292 }
293 // Iterate over all targets, running a task for each.
294 targets.forEach(function(target) {
295 // Be sure to pass in any additionally specified args.
296 task.run([taskname, target].concat(args || []).join(':'));
297 });
298};
299
300// Load tasks and handlers from a given tasks file.
301var loadTaskStack = [];
302function loadTask(filepath) {
303 // In case this was called recursively, save registry for later.
304 loadTaskStack.push(registry);
305 // Reset registry.
306 registry = {tasks: [], untasks: [], meta: {info: lastInfo, filepath: filepath}};
307 var filename = path.basename(filepath);
308 var msg = 'Loading "' + filename + '" tasks...';
309 var regCount = 0;
310 var fn;
311 try {
312 // Load taskfile.
313 fn = require(path.resolve(filepath));
314 if (typeof fn === 'function') {
315 fn.call(grunt, grunt);
316 }
317 grunt.verbose.write(msg).ok();
318 // Log registered/renamed/unregistered tasks.
319 ['un', ''].forEach(function(prefix) {
320 var list = grunt.util._.chain(registry[prefix + 'tasks']).uniq().sort().value();
321 if (list.length > 0) {
322 regCount++;
323 grunt.verbose.writeln((prefix ? '- ' : '+ ') + grunt.log.wordlist(list));
324 }
325 });
326 if (regCount === 0) {
327 grunt.verbose.warn('No tasks were registered or unregistered.');
328 }
329 } catch (e) {
330 // Something went wrong.
331 grunt.log.write(msg).error().verbose.error(e.stack).or.error(e);
332 }
333 // Restore registry.
334 registry = loadTaskStack.pop() || {};
335}
336
337// Log a message when loading tasks.
338function loadTasksMessage(info) {
339 // Only keep track of names of top-level loaded tasks and collections,
340 // not sub-tasks.
341 if (loadTaskDepth === 0) { lastInfo = info; }
342 grunt.verbose.subhead('Registering ' + info + ' tasks.');
343}
344
345// Load tasks and handlers from a given directory.
346function loadTasks(tasksdir) {
347 try {
348 var files = grunt.file.glob.sync('*.{js,coffee}', {cwd: tasksdir, maxDepth: 1});
349 // Load tasks from files.
350 files.forEach(function(filename) {
351 loadTask(path.join(tasksdir, filename));
352 });
353 } catch (e) {
354 grunt.log.verbose.error(e.stack).or.error(e);
355 }
356}
357
358// Load tasks and handlers from a given directory.
359task.loadTasks = function(tasksdir) {
360 loadTasksMessage('"' + tasksdir + '"');
361 if (grunt.file.exists(tasksdir)) {
362 loadTasks(tasksdir);
363 } else {
364 grunt.log.error('Tasks directory "' + tasksdir + '" not found.');
365 }
366};
367
368// Load tasks and handlers from a given locally-installed Npm module (installed
369// relative to the base dir).
370task.loadNpmTasks = function(name) {
371 loadTasksMessage('"' + name + '" local Npm module');
372 var root = path.resolve('node_modules');
373 var pkgfile = path.join(root, name, 'package.json');
374 var pkg = grunt.file.exists(pkgfile) ? grunt.file.readJSON(pkgfile) : {keywords: []};
375
376 // Process collection plugins.
377 if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) {
378 loadTaskDepth++;
379 Object.keys(pkg.dependencies).forEach(function(depName) {
380 // Npm sometimes pulls dependencies out if they're shared, so find
381 // upwards if not found locally.
382 var filepath = grunt.file.findup('node_modules/' + depName, {
383 cwd: path.resolve('node_modules', name),
384 nocase: true
385 });
386 if (filepath) {
387 // Load this task plugin recursively.
388 task.loadNpmTasks(path.relative(root, filepath));
389 }
390 });
391 loadTaskDepth--;
392 return;
393 }
394
395 // Process task plugins.
396 var tasksdir = path.join(root, name, 'tasks');
397 if (grunt.file.exists(tasksdir)) {
398 loadTasks(tasksdir);
399 } else {
400 grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?');
401 }
402};
403
404// Initialize tasks.
405task.init = function(tasks, options) {
406 if (!options) { options = {}; }
407
408 // Were only init tasks specified?
409 var allInit = tasks.length > 0 && tasks.every(function(name) {
410 var obj = task._taskPlusArgs(name).task;
411 return obj && obj.init;
412 });
413
414 // Get any local Gruntfile or tasks that might exist. Use --gruntfile override
415 // if specified, otherwise search the current directory or any parent.
416 var gruntfile, msg;
417 if (allInit || options.gruntfile === false) {
418 gruntfile = null;
419 } else {
420 gruntfile = grunt.option('gruntfile') ||
421 grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true});
422 msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...';
423 }
424
425 if (options.gruntfile === false) {
426 // Grunt was run as a lib with {gruntfile: false}.
427 } else if (gruntfile && grunt.file.exists(gruntfile)) {
428 grunt.verbose.writeln().write(msg).ok();
429 // Change working directory so that all paths are relative to the
430 // Gruntfile's location (or the --base option, if specified).
431 process.chdir(grunt.option('base') || path.dirname(gruntfile));
432 // Load local tasks, if the file exists.
433 loadTasksMessage('Gruntfile');
434 loadTask(gruntfile);
435 } else if (options.help || allInit) {
436 // Don't complain about missing Gruntfile.
437 } else if (grunt.option('gruntfile')) {
438 // If --config override was specified and it doesn't exist, complain.
439 grunt.log.writeln().write(msg).error();
440 grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
441 } else if (!grunt.option('help')) {
442 grunt.verbose.writeln().write(msg).error();
443 grunt.log.writelns(
444 'A valid Gruntfile could not be found. Please see the getting ' +
445 'started guide for more information on how to configure grunt: ' +
446 'http://gruntjs.com/getting-started'
447 );
448 grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
449 }
450
451 // Load all user-specified --npm tasks.
452 (grunt.option('npm') || []).map(String).forEach(task.loadNpmTasks);
453 // Load all user-specified --tasks.
454 (grunt.option('tasks') || []).map(String).forEach(task.loadTasks);
455};