| 'use strict'; |
| |
| // Keep track of the number of log.error() calls and the last specified tasks message. |
| var errorcount, lastInfo; |
| |
| var grunt = require('../grunt'); |
| |
| // Nodejs libs. |
| var path = require('path'); |
| |
| // Extend generic "task" util lib. |
| var parent = grunt.util.task.create(); |
| |
| // The module to be exported. |
| var task = module.exports = Object.create(parent); |
| |
| // A temporary registry of tasks and metadata. |
| var registry = {tasks: [], untasks: [], meta: {}}; |
| |
| // Number of levels of recursion when loading tasks in collections. |
| var loadTaskDepth = 0; |
| |
| // Override built-in registerTask. |
| task.registerTask = function(name) { |
| // Add task to registry. |
| registry.tasks.push(name); |
| // Register task. |
| parent.registerTask.apply(task, arguments); |
| // This task, now that it's been registered. |
| var thisTask = task._tasks[name]; |
| // Metadata about the current task. |
| thisTask.meta = grunt.util._.clone(registry.meta); |
| // Override task function. |
| var _fn = thisTask.fn; |
| thisTask.fn = function(arg) { |
| // Guaranteed to always be the actual task name. |
| var name = thisTask.name; |
| // Initialize the errorcount for this task. |
| errorcount = grunt.fail.errorcount; |
| // Return the number of errors logged during this task. |
| Object.defineProperty(this, 'errorCount', { |
| enumerable: true, |
| get: function() { |
| return grunt.fail.errorcount - errorcount; |
| } |
| }); |
| // Expose task.requires on `this`. |
| this.requires = task.requires.bind(task); |
| // Expose config.requires on `this`. |
| this.requiresConfig = grunt.config.requires; |
| // Return an options object with the specified defaults overwritten by task- |
| // specific overrides, via the "options" property. |
| this.options = function() { |
| var args = [{}].concat(grunt.util.toArray(arguments)).concat([ |
| grunt.config([name, 'options']) |
| ]); |
| var options = grunt.util._.extend.apply(null, args); |
| grunt.verbose.writeflags(options, 'Options'); |
| return options; |
| }; |
| // If this task was an alias or a multi task called without a target, |
| // only log if in verbose mode. |
| var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log'; |
| // Actually log. |
| grunt[logger].header('Running "' + this.nameArgs + '"' + |
| (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task'); |
| // If --debug was specified, log the path to this task's source file. |
| grunt[logger].debug('Task source: ' + thisTask.meta.filepath); |
| // Actually run the task. |
| return _fn.apply(this, arguments); |
| }; |
| return task; |
| }; |
| |
| // Multi task targets can't start with _ or be a reserved property (options). |
| function isValidMultiTaskTarget(target) { |
| return !/^_|^options$/.test(target); |
| } |
| |
| // Normalize multi task files. |
| task.normalizeMultiTaskFiles = function(data, target) { |
| var prop, obj; |
| var files = []; |
| if (grunt.util.kindOf(data) === 'object') { |
| if ('src' in data || 'dest' in data) { |
| obj = {}; |
| for (prop in data) { |
| if (prop !== 'options') { |
| obj[prop] = data[prop]; |
| } |
| } |
| files.push(obj); |
| } else if (grunt.util.kindOf(data.files) === 'object') { |
| for (prop in data.files) { |
| files.push({src: data.files[prop], dest: grunt.config.process(prop)}); |
| } |
| } else if (Array.isArray(data.files)) { |
| grunt.util._.flattenDeep(data.files).forEach(function(obj) { |
| var prop; |
| if ('src' in obj || 'dest' in obj) { |
| files.push(obj); |
| } else { |
| for (prop in obj) { |
| files.push({src: obj[prop], dest: grunt.config.process(prop)}); |
| } |
| } |
| }); |
| } |
| } else { |
| files.push({src: data, dest: grunt.config.process(target)}); |
| } |
| |
| // If no src/dest or files were specified, return an empty files array. |
| if (files.length === 0) { |
| grunt.verbose.writeln('File: ' + '[no files]'.yellow); |
| return []; |
| } |
| |
| // Process all normalized file objects. |
| files = grunt.util._(files).chain().forEach(function(obj) { |
| if (!('src' in obj) || !obj.src) { return; } |
| // Normalize .src properties to flattened array. |
| if (Array.isArray(obj.src)) { |
| obj.src = grunt.util._.flatten(obj.src); |
| } else { |
| obj.src = [obj.src]; |
| } |
| }).map(function(obj) { |
| // Build options object, removing unwanted properties. |
| var expandOptions = grunt.util._.extend({}, obj); |
| delete expandOptions.src; |
| delete expandOptions.dest; |
| |
| // Expand file mappings. |
| if (obj.expand) { |
| return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) { |
| // Copy obj properties to result. |
| var result = grunt.util._.extend({}, obj); |
| // Make a clone of the orig obj available. |
| result.orig = grunt.util._.extend({}, obj); |
| // Set .src and .dest, processing both as templates. |
| result.src = grunt.config.process(mapObj.src); |
| result.dest = grunt.config.process(mapObj.dest); |
| // Remove unwanted properties. |
| ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) { |
| delete result[prop]; |
| }); |
| return result; |
| }); |
| } |
| |
| // Copy obj properties to result, adding an .orig property. |
| var result = grunt.util._.extend({}, obj); |
| // Make a clone of the orig obj available. |
| result.orig = grunt.util._.extend({}, obj); |
| |
| if ('src' in result) { |
| // Expose an expand-on-demand getter method as .src. |
| Object.defineProperty(result, 'src', { |
| enumerable: true, |
| get: function fn() { |
| var src; |
| if (!('result' in fn)) { |
| src = obj.src; |
| // If src is an array, flatten it. Otherwise, make it into an array. |
| src = Array.isArray(src) ? grunt.util._.flatten(src) : [src]; |
| // Expand src files, memoizing result. |
| fn.result = grunt.file.expand(expandOptions, src); |
| } |
| return fn.result; |
| } |
| }); |
| } |
| |
| if ('dest' in result) { |
| result.dest = obj.dest; |
| } |
| |
| return result; |
| }).flatten().value(); |
| |
| // Log this.file src and dest properties when --verbose is specified. |
| if (grunt.option('verbose')) { |
| files.forEach(function(obj) { |
| var output = []; |
| if ('src' in obj) { |
| output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow); |
| } |
| if ('dest' in obj) { |
| output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow)); |
| } |
| if (output.length > 0) { |
| grunt.verbose.writeln('Files: ' + output.join(' ')); |
| } |
| }); |
| } |
| |
| return files; |
| }; |
| |
| // This is the most common "multi task" pattern. |
| task.registerMultiTask = function(name, info, fn) { |
| // If optional "info" string is omitted, shuffle arguments a bit. |
| if (fn == null) { |
| fn = info; |
| info = 'Custom multi task.'; |
| } |
| // Store a reference to the task object, in case the task gets renamed. |
| var thisTask; |
| task.registerTask(name, info, function(target) { |
| // Guaranteed to always be the actual task name. |
| var name = thisTask.name; |
| // Arguments (sans target) as an array. |
| this.args = grunt.util.toArray(arguments).slice(1); |
| // If a target wasn't specified, run this task once for each target. |
| if (!target || target === '*') { |
| return task.runAllTargets(name, this.args); |
| } else if (!isValidMultiTaskTarget(target)) { |
| throw new Error('Invalid target "' + target + '" specified.'); |
| } |
| // Fail if any required config properties have been omitted. |
| this.requiresConfig([name, target]); |
| // Return an options object with the specified defaults overwritten by task- |
| // and/or target-specific overrides, via the "options" property. |
| this.options = function() { |
| var targetObj = grunt.config([name, target]); |
| var args = [{}].concat(grunt.util.toArray(arguments)).concat([ |
| grunt.config([name, 'options']), |
| grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {} |
| ]); |
| var options = grunt.util._.extend.apply(null, args); |
| grunt.verbose.writeflags(options, 'Options'); |
| return options; |
| }; |
| // Expose the current target. |
| this.target = target; |
| // Recreate flags object so that the target isn't set as a flag. |
| this.flags = {}; |
| this.args.forEach(function(arg) { this.flags[arg] = true; }, this); |
| // Expose data on `this` (as well as task.current). |
| this.data = grunt.config([name, target]); |
| // Expose normalized files object. |
| this.files = task.normalizeMultiTaskFiles(this.data, target); |
| // Expose normalized, flattened, uniqued array of src files. |
| Object.defineProperty(this, 'filesSrc', { |
| enumerable: true, |
| get: function() { |
| return grunt.util._(this.files).chain().map('src').flatten().uniq().value(); |
| }.bind(this) |
| }); |
| // Call original task function, passing in the target and any other args. |
| return fn.apply(this, this.args); |
| }); |
| |
| thisTask = task._tasks[name]; |
| thisTask.multi = true; |
| }; |
| |
| // Init tasks don't require properties in config, and as such will preempt |
| // config loading errors. |
| task.registerInitTask = function(name, info, fn) { |
| task.registerTask(name, info, fn); |
| task._tasks[name].init = true; |
| }; |
| |
| // Override built-in renameTask to use the registry. |
| task.renameTask = function(oldname, newname) { |
| var result; |
| try { |
| // Actually rename task. |
| result = parent.renameTask.apply(task, arguments); |
| // Add and remove task. |
| registry.untasks.push(oldname); |
| registry.tasks.push(newname); |
| // Return result. |
| return result; |
| } catch (e) { |
| grunt.log.error(e.message); |
| } |
| }; |
| |
| // If a property wasn't passed, run all task targets in turn. |
| task.runAllTargets = function(taskname, args) { |
| // Get an array of sub-property keys under the given config object. |
| var targets = Object.keys(grunt.config.getRaw(taskname) || {}); |
| // Remove invalid target properties. |
| targets = targets.filter(isValidMultiTaskTarget); |
| // Fail if there are no actual properties to iterate over. |
| if (targets.length === 0) { |
| grunt.log.error('No "' + taskname + '" targets found.'); |
| return false; |
| } |
| // Iterate over all targets, running a task for each. |
| targets.forEach(function(target) { |
| // Be sure to pass in any additionally specified args. |
| task.run([taskname, target].concat(args || []).join(':')); |
| }); |
| }; |
| |
| // Load tasks and handlers from a given tasks file. |
| var loadTaskStack = []; |
| function loadTask(filepath) { |
| // In case this was called recursively, save registry for later. |
| loadTaskStack.push(registry); |
| // Reset registry. |
| registry = {tasks: [], untasks: [], meta: {info: lastInfo, filepath: filepath}}; |
| var filename = path.basename(filepath); |
| var msg = 'Loading "' + filename + '" tasks...'; |
| var regCount = 0; |
| var fn; |
| try { |
| // Load taskfile. |
| fn = require(path.resolve(filepath)); |
| if (typeof fn === 'function') { |
| fn.call(grunt, grunt); |
| } |
| grunt.verbose.write(msg).ok(); |
| // Log registered/renamed/unregistered tasks. |
| ['un', ''].forEach(function(prefix) { |
| var list = grunt.util._.chain(registry[prefix + 'tasks']).uniq().sort().value(); |
| if (list.length > 0) { |
| regCount++; |
| grunt.verbose.writeln((prefix ? '- ' : '+ ') + grunt.log.wordlist(list)); |
| } |
| }); |
| if (regCount === 0) { |
| grunt.verbose.warn('No tasks were registered or unregistered.'); |
| } |
| } catch (e) { |
| // Something went wrong. |
| grunt.log.write(msg).error().verbose.error(e.stack).or.error(e); |
| } |
| // Restore registry. |
| registry = loadTaskStack.pop() || {}; |
| } |
| |
| // Log a message when loading tasks. |
| function loadTasksMessage(info) { |
| // Only keep track of names of top-level loaded tasks and collections, |
| // not sub-tasks. |
| if (loadTaskDepth === 0) { lastInfo = info; } |
| grunt.verbose.subhead('Registering ' + info + ' tasks.'); |
| } |
| |
| // Load tasks and handlers from a given directory. |
| function loadTasks(tasksdir) { |
| try { |
| var files = grunt.file.glob.sync('*.{js,coffee}', {cwd: tasksdir, maxDepth: 1}); |
| // Load tasks from files. |
| files.forEach(function(filename) { |
| loadTask(path.join(tasksdir, filename)); |
| }); |
| } catch (e) { |
| grunt.log.verbose.error(e.stack).or.error(e); |
| } |
| } |
| |
| // Load tasks and handlers from a given directory. |
| task.loadTasks = function(tasksdir) { |
| loadTasksMessage('"' + tasksdir + '"'); |
| if (grunt.file.exists(tasksdir)) { |
| loadTasks(tasksdir); |
| } else { |
| grunt.log.error('Tasks directory "' + tasksdir + '" not found.'); |
| } |
| }; |
| |
| // Load tasks and handlers from a given locally-installed Npm module (installed |
| // relative to the base dir). |
| task.loadNpmTasks = function(name) { |
| loadTasksMessage('"' + name + '" local Npm module'); |
| var root = path.resolve('node_modules'); |
| var pkgfile = path.join(root, name, 'package.json'); |
| var pkg = grunt.file.exists(pkgfile) ? grunt.file.readJSON(pkgfile) : {keywords: []}; |
| |
| // Process collection plugins. |
| if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) { |
| loadTaskDepth++; |
| Object.keys(pkg.dependencies).forEach(function(depName) { |
| // Npm sometimes pulls dependencies out if they're shared, so find |
| // upwards if not found locally. |
| var filepath = grunt.file.findup('node_modules/' + depName, { |
| cwd: path.resolve('node_modules', name), |
| nocase: true |
| }); |
| if (filepath) { |
| // Load this task plugin recursively. |
| task.loadNpmTasks(path.relative(root, filepath)); |
| } |
| }); |
| loadTaskDepth--; |
| return; |
| } |
| |
| // Process task plugins. |
| var tasksdir = path.join(root, name, 'tasks'); |
| if (grunt.file.exists(tasksdir)) { |
| loadTasks(tasksdir); |
| } else { |
| grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?'); |
| } |
| }; |
| |
| // Initialize tasks. |
| task.init = function(tasks, options) { |
| if (!options) { options = {}; } |
| |
| // Were only init tasks specified? |
| var allInit = tasks.length > 0 && tasks.every(function(name) { |
| var obj = task._taskPlusArgs(name).task; |
| return obj && obj.init; |
| }); |
| |
| // Get any local Gruntfile or tasks that might exist. Use --gruntfile override |
| // if specified, otherwise search the current directory or any parent. |
| var gruntfile, msg; |
| if (allInit || options.gruntfile === false) { |
| gruntfile = null; |
| } else { |
| gruntfile = grunt.option('gruntfile') || |
| grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true}); |
| msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...'; |
| } |
| |
| if (options.gruntfile === false) { |
| // Grunt was run as a lib with {gruntfile: false}. |
| } else if (gruntfile && grunt.file.exists(gruntfile)) { |
| grunt.verbose.writeln().write(msg).ok(); |
| // Change working directory so that all paths are relative to the |
| // Gruntfile's location (or the --base option, if specified). |
| process.chdir(grunt.option('base') || path.dirname(gruntfile)); |
| // Load local tasks, if the file exists. |
| loadTasksMessage('Gruntfile'); |
| loadTask(gruntfile); |
| } else if (options.help || allInit) { |
| // Don't complain about missing Gruntfile. |
| } else if (grunt.option('gruntfile')) { |
| // If --config override was specified and it doesn't exist, complain. |
| grunt.log.writeln().write(msg).error(); |
| grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE); |
| } else if (!grunt.option('help')) { |
| grunt.verbose.writeln().write(msg).error(); |
| grunt.log.writelns( |
| 'A valid Gruntfile could not be found. Please see the getting ' + |
| 'started guide for more information on how to configure grunt: ' + |
| 'http://gruntjs.com/getting-started' |
| ); |
| grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE); |
| } |
| |
| // Load all user-specified --npm tasks. |
| (grunt.option('npm') || []).map(String).forEach(task.loadNpmTasks); |
| // Load all user-specified --tasks. |
| (grunt.option('tasks') || []).map(String).forEach(task.loadTasks); |
| }; |