452 lines
15 KiB
JavaScript
452 lines
15 KiB
JavaScript
|
/*
|
||
|
* grunt
|
||
|
* http://gruntjs.com/
|
||
|
*
|
||
|
* Copyright (c) 2013 "Cowboy" Ben Alman
|
||
|
* Licensed under the MIT license.
|
||
|
* https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
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: {}};
|
||
|
|
||
|
// The last specified tasks message.
|
||
|
var lastInfo;
|
||
|
|
||
|
// Number of levels of recursion when loading tasks in collections.
|
||
|
var loadTaskDepth = 0;
|
||
|
|
||
|
// Keep track of the number of log.error() calls.
|
||
|
var errorcount;
|
||
|
|
||
|
// 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)) {
|
||
|
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 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().pluck('src').flatten().uniq().value();
|
||
|
}.bind(this)
|
||
|
});
|
||
|
// 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);
|
||
|
// 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) {
|
||
|
// Add and remove task.
|
||
|
registry.untasks.push(oldname);
|
||
|
registry.tasks.push(newname);
|
||
|
// Actually rename task.
|
||
|
return parent.renameTask.apply(task, arguments);
|
||
|
};
|
||
|
|
||
|
// 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) || {});
|
||
|
// 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 valid target properties, running a task for each.
|
||
|
targets.filter(isValidMultiTaskTarget).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.error('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 = allInit ? null : grunt.option('gruntfile') ||
|
||
|
grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true});
|
||
|
|
||
|
var msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...';
|
||
|
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') || []).forEach(task.loadNpmTasks);
|
||
|
// Load all user-specified --tasks.
|
||
|
(grunt.option('tasks') || []).forEach(task.loadTasks);
|
||
|
};
|