UNPKG

13.8 kBJavaScriptView Raw
1/*
2 * grunt
3 * https://github.com/cowboy/grunt
4 *
5 * Copyright (c) 2012 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * http://benalman.com/about/license/
8 */
9
10var grunt = require('../grunt');
11
12// Nodejs libs.
13var path = require('path');
14var fs = require('fs');
15
16// Extend generic "task" utils lib.
17var parent = grunt.utils.task.create();
18
19// The module to be exported.
20var task = module.exports = Object.create(parent);
21
22// An internal registry of tasks and handlers.
23var registry = {};
24
25// Keep track of the number of log.error() calls.
26var errorcount;
27
28// Override built-in registerTask.
29task.registerTask = function(name, info, fn) {
30 // Add task to registry.
31 registry.tasks.push(name);
32 // Register task.
33 parent.registerTask.apply(task, arguments);
34 // This task, now that it's been registered.
35 var thisTask = task._tasks[name];
36 // Override task function.
37 var _fn = thisTask.fn;
38 thisTask.fn = function(arg) {
39 // Initialize the errorcount for this task.
40 errorcount = grunt.fail.errorcount;
41 // Return the number of errors logged during this task.
42 this.__defineGetter__('errorCount', function() {
43 return grunt.fail.errorcount - errorcount;
44 });
45 // Expose task.requires on `this`.
46 this.requires = task.requires.bind(task);
47 // Expose config.requires on `this`.
48 this.requiresConfig = grunt.config.requires;
49 // If this task was an alias or a multi task called without a target,
50 // only log if in verbose mode.
51 var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log';
52 // Actually log.
53 grunt[logger].header('Running "' + this.nameArgs + '"' +
54 (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task');
55 // Actually run the task.
56 return _fn.apply(this, arguments);
57 };
58 return task;
59};
60
61// This is the most common "multi task" pattern.
62task.registerMultiTask = function(name, info, fn) {
63 task.registerTask(name, info, function(target) {
64 // If a target wasn't specified, run this task once for each target.
65 if (!target || target === '*') {
66 return task.runAllTargets(name, grunt.utils.toArray(arguments).slice(1));
67 }
68 // Fail if any required config properties have been omitted.
69 this.requiresConfig([name, target]);
70 // Expose data on `this` (as well as task.current).
71 this.data = grunt.config([name, target]);
72 // Expose file object on `this` (as well as task.current).
73 this.file = {};
74 // Handle data structured like either:
75 // 'prop': [srcfiles]
76 // {prop: {src: [srcfiles], dest: 'destfile'}}.
77 if (grunt.utils.kindOf(this.data) === 'object') {
78 if ('src' in this.data) { this.file.src = this.data.src; }
79 if ('dest' in this.data) { this.file.dest = this.data.dest; }
80 } else {
81 this.file.src = this.data;
82 this.file.dest = target;
83 }
84 // Process src as a template (recursively, if necessary).
85 if (this.file.src) {
86 this.file.src = grunt.utils.recurse(this.file.src, function(src) {
87 if (typeof src !== 'string') { return src; }
88 return grunt.template.process(src);
89 });
90 }
91 // Process dest as a template.
92 if (this.file.dest) {
93 this.file.dest = grunt.template.process(this.file.dest);
94 }
95 // Expose the current target.
96 this.target = target;
97 // Remove target from args.
98 this.args = grunt.utils.toArray(arguments).slice(1);
99 // Recreate flags object so that the target isn't set as a flag.
100 this.flags = {};
101 this.args.forEach(function(arg) { this.flags[arg] = true; }, this);
102 // Call original task function, passing in the target and any other args.
103 return fn.apply(this, this.args);
104 });
105 task._tasks[name].multi = true;
106};
107
108// Init tasks don't require properties in config, and as such will preempt
109// config loading errors.
110task.registerInitTask = function(name, info, fn) {
111 task.registerTask(name, info, fn);
112 task._tasks[name].init = true;
113};
114
115// Override built-in registerHelper to use the registry.
116task.registerHelper = function(name, fn) {
117 // Add task to registry.
118 registry.helpers.push(name);
119 // Actually register task.
120 return parent.registerHelper.apply(task, arguments);
121};
122
123// If a property wasn't passed, run all task targets in turn.
124task.runAllTargets = function(taskname, args) {
125 // Get an array of sub-property keys under the given config object.
126 var targets = Object.keys(grunt.config(taskname) || {});
127 // Fail if there are no actual properties to iterate over.
128 if (targets.length === 0) {
129 grunt.log.error('No "' + taskname + '" targets found.');
130 return false;
131 }
132 // Iterate over all properties not starting with _, running a task for each.
133 targets.filter(function(target) {
134 return !/^_/.test(target);
135 }).forEach(function(target) {
136 // Be sure to pass in any additionally specified args.
137 task.run([taskname, target].concat(args || []).join(':'));
138 });
139};
140
141// Load tasks and handlers from a given tasks file.
142var loadTaskStack = [];
143function loadTask(filepath) {
144 // In case this was called recursively, save registry for later.
145 loadTaskStack.push({tasks: registry.tasks, helpers: registry.helpers});
146 // Reset registry.
147 registry.tasks = [];
148 registry.helpers = [];
149 var file = path.basename(filepath);
150 var msg = 'Loading "' + file + '" tasks and helpers...';
151 var fn;
152 try {
153 // Load taskfile.
154 fn = require(path.resolve(filepath));
155 if (typeof fn === 'function') {
156 fn.call(grunt, grunt);
157 }
158 grunt.verbose.write(msg).ok();
159 if (registry.tasks.length === 0 && registry.helpers.length === 0) {
160 grunt.verbose.error('No new tasks or helpers found.');
161 } else {
162 if (registry.tasks.length > 0) {
163 grunt.verbose.writeln('Tasks: ' + grunt.log.wordlist(registry.tasks));
164 }
165 if (registry.helpers.length > 0) {
166 grunt.verbose.writeln('Helpers: ' + grunt.log.wordlist(registry.helpers));
167 }
168 }
169 } catch(e) {
170 // Something went wrong.
171 grunt.log.write(msg).error().verbose.error(e.stack).or.error(e);
172 }
173 // Restore registry.
174 var obj = loadTaskStack.pop() || {};
175 registry.tasks = obj.tasks || [];
176 registry.helpers = obj.helpers || [];
177}
178
179// Log a message when loading tasks.
180function loadTasksMessage(info) {
181 grunt.verbose.subhead('Registering ' + info + ' tasks.');
182}
183
184// Load tasks and handlers from a given directory.
185function loadTasks(tasksdir) {
186 try {
187 fs.readdirSync(tasksdir).filter(function(filename) {
188 // Filter out non-.js files.
189 return path.extname(filename).toLowerCase() === '.js';
190 }).forEach(function(filename) {
191 // Load task.
192 loadTask(path.join(tasksdir, filename));
193 });
194 } catch(e) {
195 grunt.log.verbose.error(e.stack).or.error(e);
196 }
197}
198
199// Directories to be searched for tasks files and "extra" files.
200task.searchDirs = [];
201
202// Return an array of all task-specific file paths that match the given
203// wildcard patterns. Instead of returing a string for each file path, return
204// an object with useful properties. When coerced to String, each object will
205// yield its absolute path.
206function expandByMethod(method) {
207 var args = grunt.utils.toArray(arguments).slice(1);
208 // If the first argument is an options object, remove and save it for later.
209 var options = grunt.utils.kindOf(args[0]) === 'object' ? args.shift() : {};
210 // Use the first argument if it's an Array, otherwise convert the arguments
211 // object to an array and use that.
212 var patterns = Array.isArray(args[0]) ? args[0] : args;
213 var filepaths = {};
214 // When any returned array item is used in a string context, return the
215 // absolute path.
216 var toString = function() { return this.abs; };
217 // Iterate over all searchDirs.
218 task.searchDirs.forEach(function(dirpath) {
219 // Create an array of absolute patterns.
220 var args = patterns.map(function(pattern) {
221 return path.join(dirpath, pattern);
222 });
223 // Add the options object back onto the beginning of the arguments array.
224 args.unshift(options);
225 // Expand the paths in case a wildcard was passed.
226 grunt.file[method].apply(null, args).forEach(function(abspath) {
227 var relpath = abspath.slice(dirpath.length + 1);
228 if (relpath in filepaths) { return; }
229 // Update object at this relpath only if it doesn't already exist.
230 filepaths[relpath] = {
231 abs: abspath,
232 rel: relpath,
233 base: abspath.slice(0, dirpath.length),
234 toString: toString
235 };
236 });
237 });
238 // Return an array of objects.
239 return Object.keys(filepaths).map(function(relpath) {
240 return filepaths[relpath];
241 });
242}
243
244// A few type-specific task expansion methods. These methods all return arrays
245// of file objects.
246task.expand = expandByMethod.bind(task, 'expand');
247task.expandDirs = expandByMethod.bind(task, 'expandDirs');
248task.expandFiles = expandByMethod.bind(task, 'expandFiles');
249
250// Get a single task file path.
251task.getFile = function() {
252 var filepath = path.join.apply(path, arguments);
253 var fileobj = task.expand(filepath)[0];
254 return fileobj ? String(fileobj) : null;
255};
256
257// Read JSON defaults from task files (if they exist), merging them into one.
258// data object.
259var readDefaults = {};
260task.readDefaults = function() {
261 var filepath = path.join.apply(path, arguments);
262 var result = readDefaults[filepath];
263 var filepaths;
264 if (!result) {
265 result = readDefaults[filepath] = {};
266 // Find all matching taskfiles.
267 filepaths = task.searchDirs.map(function(dirpath) {
268 return path.join(dirpath, filepath);
269 }).filter(function(filepath) {
270 return path.existsSync(filepath) && fs.statSync(filepath).isFile();
271 });
272 // Load defaults data.
273 if (filepaths.length) {
274 grunt.verbose.subhead('Loading data from ' + filepath);
275 // Since extras path order goes from most-specific to least-specific, only
276 // add-in properties that don't already exist.
277 filepaths.forEach(function(filepath) {
278 grunt.utils._.defaults(result, grunt.file.readJSON(filepath));
279 });
280 }
281 }
282 return result;
283};
284
285// Load tasks and handlers from a given directory.
286task.loadTasks = function(tasksdir) {
287 loadTasksMessage('"' + tasksdir + '"');
288 if (path.existsSync(tasksdir)) {
289 task.searchDirs.unshift(tasksdir);
290 loadTasks(tasksdir);
291 } else {
292 grunt.log.error('Tasks directory "' + tasksdir + '" not found.');
293 }
294};
295
296// Load tasks and handlers from a given locally-installed Npm module (installed
297// relative to the base dir).
298task.loadNpmTasks = function(name) {
299 loadTasksMessage('"' + name + '" local Npm module');
300 var tasksdir = path.resolve('node_modules', name, 'tasks');
301 if (path.existsSync(tasksdir)) {
302 task.searchDirs.unshift(tasksdir);
303 loadTasks(tasksdir);
304 } else {
305 grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?');
306 }
307};
308
309// Load tasks and handlers from a given Npm module, installed relative to the
310// version of grunt being run.
311function loadNpmTasksWithRequire(name) {
312 loadTasksMessage('"' + name + '" npm module');
313 var dirpath;
314 try {
315 dirpath = require.resolve(name);
316 dirpath = path.resolve(path.join(path.dirname(dirpath), 'tasks'));
317 if (path.existsSync(dirpath)) {
318 task.searchDirs.unshift(dirpath);
319 loadTasks(dirpath);
320 return;
321 }
322 } catch (e) {}
323
324 grunt.log.error('Npm module "' + name + '" not found. Is it installed?');
325}
326
327// Initialize tasks.
328task.init = function(tasks, options) {
329 if (!options) { options = {}; }
330
331 // Load all built-in tasks.
332 var tasksdir = path.resolve(__dirname, '../../tasks');
333 task.searchDirs.unshift(tasksdir);
334 loadTasksMessage('built-in');
335 loadTasks(tasksdir);
336
337 // Grunt was loaded from a Npm-installed plugin bin script. Load any tasks
338 // that were specified via grunt.npmTasks.
339 grunt._npmTasks.forEach(loadNpmTasksWithRequire);
340
341 // Were only init tasks specified?
342 var allInit = tasks.length > 0 && tasks.every(function(name) {
343 var obj = task._taskPlusArgs(name).task;
344 return obj && obj.init;
345 });
346
347 // Get any local configfile or tasks that might exist. Use --config override
348 // if specified, otherwise search the current directory or any parent.
349 var configfile = allInit ? null : grunt.option('config') ||
350 grunt.file.findup(process.cwd(), 'grunt.js');
351
352 var msg = 'Reading "' + path.basename(configfile) + '" config file...';
353 if (configfile && path.existsSync(configfile)) {
354 grunt.verbose.writeln().write(msg).ok();
355 // Change working directory so that all paths are relative to the
356 // configfile's location (or the --base option, if specified).
357 process.chdir(grunt.option('base') || path.dirname(configfile));
358 // Load local tasks, if the file exists.
359 loadTask(configfile);
360 } else if (options.help || allInit) {
361 // Don't complain about missing config file.
362 } else if (grunt.option('config')) {
363 // If --config override was specified and it doesn't exist, complain.
364 grunt.log.writeln().write(msg).error();
365 grunt.fatal('Unable to find "' + configfile + '" config file.', 2);
366 } else if (!grunt.option('help')) {
367 grunt.verbose.writeln().write(msg).error();
368 grunt.fatal('Unable to find "grunt.js" config file. Do you need any --help?', 2);
369 }
370
371 // Load all user-specified --npm tasks.
372 (grunt.option('npm') || []).forEach(task.loadNpmTasks);
373 // Load all user-specified --tasks.
374 (grunt.option('tasks') || []).forEach(task.loadTasks);
375
376 // Load user .grunt tasks.
377 tasksdir = grunt.file.userDir('tasks');
378 if (tasksdir) {
379 task.searchDirs.unshift(tasksdir);
380 loadTasksMessage('user');
381 loadTasks(tasksdir);
382 }
383
384 // Search dirs should be unique and fully normalized absolute paths.
385 task.searchDirs = grunt.utils._.uniq(task.searchDirs).map(function(filepath) {
386 return path.resolve(filepath);
387 });
388};