task.js 15 KB

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