├── .gitignore ├── index.js ├── test ├── fixtures │ ├── files │ │ ├── bar.js │ │ ├── baz.js │ │ └── foo.js │ └── tasks │ │ ├── one.js │ │ └── two.js ├── task │ ├── build_context.js │ ├── build_method.js │ └── command_data.js ├── runner │ ├── get_command_task.js │ ├── get_command_target.js │ ├── load_task_files.js │ ├── get_task_targets.js │ ├── index_commands.js │ ├── find_task_files.js │ ├── build_task_list.js │ ├── parse_register.js │ └── parse_commands.js └── index.js ├── bin ├── README.md └── index.js ├── runner ├── lib │ ├── get_command_target.js │ ├── get_command_task.js │ ├── utils │ │ └── bind_many.js │ ├── build_task_list.js │ ├── get_task_targets.js │ ├── index_commands.js │ ├── load_task_files.js │ ├── find_task_files.js │ ├── build_task.js │ ├── parse_register.js │ └── parse_commands.js ├── README.md └── index.js ├── task ├── README.md └── index.js ├── legacy ├── lib │ ├── template.js │ ├── option.js │ ├── fail.js │ ├── help.js │ ├── cli.js │ ├── log.js │ └── file.js └── index.js ├── .jshintrc ├── README.md ├── package.json ├── watch ├── lib │ └── runner.js └── index.js └── Gruntfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./runner'); 2 | -------------------------------------------------------------------------------- /test/fixtures/files/bar.js: -------------------------------------------------------------------------------- 1 | console.log('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/files/baz.js: -------------------------------------------------------------------------------- 1 | console.log('baz'); 2 | -------------------------------------------------------------------------------- /test/fixtures/files/foo.js: -------------------------------------------------------------------------------- 1 | console.log('foo'); 2 | -------------------------------------------------------------------------------- /test/task/build_context.js: -------------------------------------------------------------------------------- 1 | const Task = require('../../task'); 2 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # bin 2 | 3 | ### Responsibilities 4 | 5 | * Find and load local version of grunt. 6 | -------------------------------------------------------------------------------- /runner/lib/get_command_target.js: -------------------------------------------------------------------------------- 1 | module.exports = function (command) { 2 | return command.split(':')[1]; 3 | }; 4 | -------------------------------------------------------------------------------- /runner/lib/get_command_task.js: -------------------------------------------------------------------------------- 1 | module.exports = function (command) { 2 | return command.split(':')[0]; 3 | }; 4 | -------------------------------------------------------------------------------- /task/README.md: -------------------------------------------------------------------------------- 1 | # task 2 | 3 | ### Responsibilities 4 | 5 | * Generate Orchestrator-compatible methods from existing grunt tasks. 6 | -------------------------------------------------------------------------------- /legacy/lib/template.js: -------------------------------------------------------------------------------- 1 | const date = require('dateformat'); 2 | 3 | exports.date = date; 4 | exports.today = function (format) { 5 | return date(new Date(), format); 6 | }; 7 | -------------------------------------------------------------------------------- /runner/README.md: -------------------------------------------------------------------------------- 1 | # runner 2 | 3 | ### Responsibilities 4 | 5 | * Maintain a registry of tasks. 6 | * Construct configurations for tasks. 7 | * Validate commands and orchestrate task running. 8 | * Provide help system. 9 | -------------------------------------------------------------------------------- /runner/lib/utils/bind_many.js: -------------------------------------------------------------------------------- 1 | module.exports = function (methods, object, context) { 2 | if (!context) { 3 | context = object; 4 | } 5 | methods.forEach(function (method) { 6 | object[method] = object[method].bind(context); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "esnext": true, 5 | "node": true, 6 | "globals": { 7 | "describe": true, 8 | "it": true, 9 | "expect": true, 10 | "beforeEach": true, 11 | "afterEach": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/tasks/one.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.loaded++; 3 | if (grunt.registerTask) { 4 | grunt.registerTask('one', 'task one', function () { 5 | console.log('running task one with context', this); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/tasks/two.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.loaded++; 3 | if (grunt.registerMultiTask) { 4 | grunt.registerMultiTask('two', 'task two', function () { 5 | console.log('running task two with context', this); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/runner/get_command_task.js: -------------------------------------------------------------------------------- 1 | const getCommandTask = require('../../runner/lib/get_command_task'); 2 | 3 | describe('getCommandTask', function () { 4 | 5 | it('should extract the base task from a grunt command', function () { 6 | expect(getCommandTask('task:target')).to.equal('task'); 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /test/runner/get_command_target.js: -------------------------------------------------------------------------------- 1 | const getCommandTarget = require('../../runner/lib/get_command_target'); 2 | 3 | describe('getCommandTarget', function () { 4 | 5 | it('should extract the target from a grunt command', function () { 6 | expect(getCommandTarget('task:target')).to.equal('target'); 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /test/runner/load_task_files.js: -------------------------------------------------------------------------------- 1 | const loadTaskFiles = require('../../runner/lib/load_task_files'); 2 | 3 | describe('loadTaskFiles', function () { 4 | 5 | it('should take an array of module paths, require them, and inject grunt into each.', function () { 6 | var tasks = [ 7 | __dirname+'/../fixtures/tasks/one', 8 | __dirname+'/../fixtures/tasks/two' 9 | ]; 10 | var grunt = {loaded:0}; 11 | loadTaskFiles(tasks, grunt); 12 | expect(grunt.loaded).to.equal(2); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /runner/lib/build_task_list.js: -------------------------------------------------------------------------------- 1 | const buildTask = require('./build_task'); 2 | 3 | module.exports = function (config, tasks, commands) { 4 | var taskList = []; 5 | Object.keys(commands).forEach(function (taskName) { 6 | var task = tasks[taskName]; 7 | var toRun = commands[taskName]; 8 | toRun.forEach(function (command) { 9 | taskList.push({ 10 | name: command, 11 | method: buildTask(config, task, command.split(':')) 12 | }); 13 | }); 14 | }); 15 | return taskList; 16 | }; 17 | -------------------------------------------------------------------------------- /test/runner/get_task_targets.js: -------------------------------------------------------------------------------- 1 | const getTaskTargets = require('../../runner/lib/get_task_targets'); 2 | 3 | describe('getTaskTargets', function () { 4 | 5 | it('should extract valid targets from a task config', function () { 6 | var config = { 7 | options: {}, 8 | targetOne: ['test/fixtures/files/*.js'], 9 | targetTwo: { 10 | src: ['test/fixtures/files/*.js'] 11 | } 12 | }; 13 | expect(getTaskTargets(config, 'task')).to.deep.equal(['task:targetOne','task:targetTwo']); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /runner/lib/get_task_targets.js: -------------------------------------------------------------------------------- 1 | /* 2 | Find all valid target names for a task configuration. 3 | TODO: do something about _target names. 4 | 5 | e.g. 6 | task: { 7 | options: {}, 8 | targetOne: ['test/fixtures/files/*.js'], 9 | targetTwo: { 10 | src: ['test/fixtures/files/*.js'] 11 | } 12 | }; 13 | = 14 | ['task:targetOne', 'task:targetTwo'] 15 | */ 16 | 17 | module.exports = function (config, taskName) { 18 | return Object.keys(config).reduce(function (result, key) { 19 | if (key != 'options') { 20 | result.push(taskName+':'+key); 21 | } 22 | return result; 23 | }, []); 24 | }; 25 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | global.expect = require('chai').expect; 2 | 3 | describe('Runner', function () { 4 | require('./runner/build_task_list'); 5 | require('./runner/find_task_files'); 6 | require('./runner/get_command_target'); 7 | require('./runner/get_command_task'); 8 | require('./runner/get_task_targets'); 9 | require('./runner/index_commands'); 10 | require('./runner/load_task_files'); 11 | require('./runner/parse_commands'); 12 | require('./runner/parse_register'); 13 | }); 14 | /* 15 | describe('Task', function () { 16 | require('./task/build_context'); 17 | require('./task/build_method'); 18 | require('./task/command_data'); 19 | }); 20 | */ 21 | -------------------------------------------------------------------------------- /test/runner/index_commands.js: -------------------------------------------------------------------------------- 1 | const indexCommands = require('../../runner/lib/index_commands'); 2 | 3 | describe('indexCommands', function () { 4 | 5 | it('index commands by their root task name', function () { 6 | var commands = [ 7 | 'single:arg1:arg2', 8 | 'multi', 9 | 'multi:target:arg1:arg2', 10 | 'multi:target:arg1', 11 | 'alias:arg1:arg2' 12 | ]; 13 | var result = { 14 | single: ['single:arg1:arg2'], 15 | multi: ['multi','multi:target:arg1:arg2','multi:target:arg1'], 16 | alias: ['alias:arg1:arg2'] 17 | }; 18 | expect(indexCommands(commands)).to.deep.equal(result); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /runner/lib/index_commands.js: -------------------------------------------------------------------------------- 1 | /* 2 | Index an array of grunt commands by their task name. 3 | 4 | e.g. 5 | grunt nodeunit jshint:lib jshint:test 6 | = 7 | { 8 | nodeunit: ['nodeunit'], 9 | jshint: ['jshint:lib','jshint:test'] 10 | } 11 | */ 12 | var getCommandTask = require('./get_command_task'); 13 | 14 | module.exports = function (commands) { 15 | return commands.reduce(function (tasks, command) { 16 | var taskName = getCommandTask(command); 17 | var task = tasks[taskName]; 18 | if (!task) { 19 | tasks[taskName] = []; 20 | task = tasks[taskName]; 21 | } 22 | task.push(command); 23 | return tasks; 24 | }, {}); 25 | }; 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt 2 | > v1.0 alpha 3 | 4 | ### Install 5 | 1. Clone repo. 6 | 2. From repo dir: `sudo npm link` 7 | 2. From project to test: `npm link grunt-next` 8 | 3. run `gn` 9 | 10 | ### Powered by: 11 | * [liftoff] - cli handling 12 | * [orchestrator] - task orchestration 13 | * [expander] - template powered configurations 14 | * [configfiles] - extract files from configurations using [globule] 15 | 16 | [liftoff]: http://github.com/tkellen/node-liftoff 17 | [orchestrator]: http://github.com/robrich/orchestrator 18 | [expander]: http://github.com/tkellen/node-expander 19 | [configfiles]: http://github.com/tkellen/node-configfiles 20 | [globule]: http://github.com/cowboy/node-globule 21 | -------------------------------------------------------------------------------- /runner/lib/load_task_files.js: -------------------------------------------------------------------------------- 1 | /* 2 | Given an array of files, this requires each, expecting to get a function. 3 | If one is found, it will be invoked with the provided context. This is 4 | how grunt is passed into 5 | module.exports = function (grunt) { ... } 6 | 7 | If you are building a tool that will have an ecosystem of plugins, please 8 | do NOT do this. Tightly coupling plugins with the modules that consume 9 | them makes it a giant pain in the ass to change the plugin consumer (in 10 | this case grunt) without breaking the entire ecosystem. 11 | */ 12 | module.exports = function (tasks, context) { 13 | tasks.forEach(function (taskFile) { 14 | var task = require(taskFile); 15 | if (typeof task === 'function') { 16 | task.call(null, context); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /runner/lib/find_task_files.js: -------------------------------------------------------------------------------- 1 | /* 2 | Given an array of folders or module names, this finds and returns a 3 | flattened array of paths to all task files found within. 4 | */ 5 | 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | const _ = require('lodash'); 9 | 10 | module.exports = function (input, node_module) { 11 | var taskDir; 12 | if (!Array.isArray(input)) { 13 | input = [input]; 14 | } 15 | return _.flatten(input.map(function (task) { 16 | if(task === 'grunt-contrib-watch') { 17 | return [path.join(__dirname,'..','..','watch','index.js')]; 18 | } 19 | if (node_module) { 20 | taskDir = path.resolve('node_modules', task, 'tasks'); 21 | } else { 22 | taskDir = path.resolve(task); 23 | } 24 | return glob.sync(taskDir+'/*.js'); 25 | })); 26 | }; 27 | -------------------------------------------------------------------------------- /test/runner/find_task_files.js: -------------------------------------------------------------------------------- 1 | const findTasks = require('../../runner/lib/find_task_files'); 2 | const glob = require('glob'); 3 | const path = require('path'); 4 | 5 | describe('findTaskFiles', function () { 6 | 7 | it('should take an array of folder names and find all task files within', function () { 8 | var taskFiles = findTasks(['fixtures/tasks']); 9 | var expectedTaskFiles = glob.sync('fixtures/tasks/*.js'); 10 | expect(taskFiles).to.deep.equal(expectedTaskFiles); 11 | }); 12 | 13 | it('should take an array of module names and find all task files within their locally installed node_module', function () { 14 | var taskFiles = findTasks(['grunt-contrib-jshint'], true); 15 | var expectedTaskFiles = [path.resolve(glob.sync('node_modules/grunt-contrib-jshint/tasks/*.js')[0])]; 16 | expect(taskFiles).to.deep.equal(expectedTaskFiles); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /runner/lib/build_task.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = function (config, task, command) { 4 | 5 | // This hack makes it possible for <%= grunt.task.current.X %> type 6 | // configuration strings to function correctly when running tasks 7 | // concurrently. In older versions of grunt, `grunt.task.current` 8 | // was pulled off the master grunt instance. It is now retrieved 9 | // using a mocked context at the time the task is constructed. 10 | var context = task.commandData(command); 11 | var taskConfig = config.get(task.name, { 12 | imports: { grunt: { task: { current: context } } } 13 | }); 14 | 15 | if(task.isMulti()) { 16 | var options = taskConfig.options||{}; 17 | // find the target for this multitask 18 | taskConfig = taskConfig[context.target]||{}; 19 | // merge top level options with target level 20 | taskConfig.options = _.extend(options, taskConfig.options||{}); 21 | } 22 | 23 | return task.build(taskConfig, command); 24 | }; 25 | -------------------------------------------------------------------------------- /runner/lib/parse_register.js: -------------------------------------------------------------------------------- 1 | // This normalizes the different forms of registerTask and registerMultiTask. 2 | 3 | module.exports = function (config, type) { 4 | var arity = config.length; 5 | var name = config[0]; 6 | var desc = 'Custom Task'; 7 | var fn = null; 8 | 9 | // support registerTask('name', 'desc', function () { ... }); 10 | if (arity === 3) { 11 | desc = config[1]; 12 | fn = config[2]; 13 | } else if (arity === 2) { 14 | // support registerTask('name', function () { ... }); 15 | if (typeof config[1] === 'function') { 16 | fn = config[1]; 17 | } else { 18 | // support registerTask('name', ['task', task', 'task']); 19 | if (!Array.isArray(config[1])) { 20 | fn = [config[1]]; 21 | } else { 22 | fn = config[1]; 23 | } 24 | type = 'alias'; 25 | desc = 'Alias for "'+fn.join('", "')+'" task'+(fn.length=== 1?'':'s')+'.'; 26 | } 27 | } else { 28 | throw new Error('Unable to register task.'); 29 | } 30 | 31 | return { 32 | name: name, 33 | desc: desc, 34 | type: type, 35 | fn: fn 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /legacy/lib/option.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | // The actual option data. 13 | var data = {}; 14 | 15 | // Get or set an option value. 16 | var option = module.exports = function(key, value) { 17 | var no = key.match(/^no-(.+)$/); 18 | if (arguments.length === 2) { 19 | return (data[key] = value); 20 | } else if (no) { 21 | return data[no[1]] === false; 22 | } else { 23 | return data[key]; 24 | } 25 | }; 26 | 27 | // Initialize option data. 28 | option.init = function(obj) { 29 | return (data = obj || {}); 30 | }; 31 | 32 | // List of options as flags. 33 | option.flags = function() { 34 | return Object.keys(data).filter(function(key) { 35 | // Don't display empty arrays. 36 | return !(Array.isArray(data[key]) && data[key].length === 0); 37 | }).map(function(key) { 38 | var val = data[key]; 39 | return '--' + (val === false ? 'no-' : '') + key + 40 | (typeof val === 'boolean' ? '' : '=' + val); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /legacy/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.template = require('./lib/template'); 3 | grunt.template.process = function (str) { return str }; 4 | grunt.log = require('./lib/log'); 5 | grunt.verbose = grunt.log.verbose; 6 | grunt.file = require('./lib/file'); 7 | grunt.fail = require('./lib/fail'); 8 | grunt.warn = grunt.log.warn; 9 | grunt.task = {}; 10 | var taskMethods = ['register', 'registerTask', 'registerMultiTask', 11 | 'loadTasks', 'loadNpmTasks', 'renameTask']; 12 | taskMethods.forEach(function (method) { 13 | grunt.task[method] = grunt[method]; 14 | }); 15 | Object.defineProperty(grunt, 'util', { 16 | get: function () { 17 | console.log('Grunt util is deprecated. Please see http://github.com/gruntjs/grunt-legacy-util for instructions.'); 18 | return require('grunt-legacy-util'); 19 | } 20 | }); 21 | Object.defineProperty(grunt.task, 'current', { 22 | get: function () { 23 | console.log('grunt.task.current is only available from the grunt\n'+ 24 | 'configuration. If you are a task author trying to use\n'+ 25 | 'this from within a task, you can access the same values\n'+ 26 | 'from `this`.'); 27 | return null; 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /test/runner/build_task_list.js: -------------------------------------------------------------------------------- 1 | const buildTaskList = require('../../runner/lib/build_task_list'); 2 | const expander = require('expander'); 3 | const Task = require('../../task'); 4 | 5 | describe('buildTaskList', function () { 6 | 7 | it('should convert an indexed set of commands to a hash for use by orchestrator', function () { 8 | 9 | var config = expander.interface({ 10 | single: { 11 | src: ['text/fixtures/files/*.js'] 12 | }, 13 | multi: { 14 | options: {}, 15 | target: ['test/fixtures/files/*.js'], 16 | targetTwo: { 17 | src: ['test/fixtures/files/*.js'] 18 | } 19 | } 20 | }); 21 | 22 | var tasks = { 23 | single: new Task({ 24 | name: 'single', 25 | desc: 'Single task.', 26 | type: 'single', 27 | fn: function () {} 28 | }), 29 | multi: new Task({ 30 | name: 'multi', 31 | desc: 'Multi task.', 32 | type: 'multi', 33 | fn: function () {} 34 | }) 35 | }; 36 | 37 | var indexedCommands = { 38 | multi: ['multi:target', 'multi:targetTwo'], 39 | single: ['single:arg', 'single'] 40 | }; 41 | var taskList = buildTaskList(config, tasks, indexedCommands); 42 | expect(taskList).to.have.length(4); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/runner/parse_register.js: -------------------------------------------------------------------------------- 1 | const parseRegister = require('../../runner/lib/parse_register'); 2 | 3 | describe('parseRegister', function () { 4 | 5 | it('should support two arguments with a function', function () { 6 | var fn = function () {}; 7 | var task = parseRegister(['test', fn], 'single'); 8 | 9 | expect(task).to.deep.equal({ 10 | name: 'test', 11 | desc: 'Custom Task', 12 | fn: fn, 13 | type: 'single' 14 | }); 15 | }); 16 | 17 | it('should support two arguments with a deps string', function () { 18 | var task = parseRegister(['test', 'default']); 19 | 20 | expect(task).to.deep.equal({ 21 | name: 'test', 22 | desc: 'Alias for "default" task.', 23 | fn: ['default'], 24 | type: 'alias' 25 | }); 26 | }); 27 | 28 | it('should support two arguments with a deps array', function () { 29 | var task = parseRegister(['test', ['dep1', 'dep2']]); 30 | 31 | expect(task).to.deep.equal({ 32 | name: 'test', 33 | desc: 'Alias for "dep1", "dep2" tasks.', 34 | fn: ['dep1', 'dep2'], 35 | type: 'alias' 36 | }); 37 | }); 38 | 39 | it('should support three arguments with a description', function () { 40 | var fn = function () {}; 41 | var task = parseRegister(['test', 'test task', fn], 'single'); 42 | 43 | expect(task).to.deep.equal({ 44 | name: 'test', 45 | desc: 'test task', 46 | fn: fn, 47 | type: 'single' 48 | }); 49 | }); 50 | 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/task/build_method.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Task = require('../../task'); 3 | const Promise = require('bluebird'); 4 | 5 | describe('buildMethod', function () { 6 | 7 | it('should return a function that will call the supplied method with a provided context', function (done) { 8 | var context = {property:true}; 9 | var method = buildMethod(function () { 10 | expect(this).to.deep.equal(context); 11 | done(); 12 | }, context); 13 | method(); 14 | }); 15 | 16 | it('should expose the provided context as a property of the generated method', function () { 17 | var context = {property:true}; 18 | var method = buildMethod(function () {}, context); 19 | expect(method.context).to.deep.equal(context); 20 | }); 21 | 22 | it('if the provided method does not create an internal promise, the generated method should return sync', function () { 23 | var method = buildMethod(function () { 24 | return "sync"; 25 | }, {}); 26 | expect(method()).to.equal("sync"); 27 | }); 28 | 29 | it('if the provided method internally creates a promise, the generated method return it', function (done) { 30 | var method = buildMethod(function () { 31 | var deferred = Promise.defer(); 32 | deferred.resolve("async"); 33 | this.deferred = deferred; 34 | return deferred.promise; 35 | }, {}); 36 | method().then(function (val) { 37 | expect(val).to.equal("async"); 38 | done(); 39 | }); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /runner/lib/parse_commands.js: -------------------------------------------------------------------------------- 1 | /* 2 | Parse an array of grunt commands doing the following: 3 | 4 | 1. Remove commands for tasks that do not exist. 5 | 2. Recursively trace aliases to their actual task. 6 | 3. Expand multi task commands without targets to run all targets. 7 | */ 8 | 9 | const _ = require('lodash'); 10 | const getCommandTask = require('./get_command_task'); 11 | const getCommandTarget = require('./get_command_target'); 12 | const getTaskTargets = require('./get_task_targets'); 13 | 14 | const parseCommands = function (config, tasks, commands) { 15 | var results = []; 16 | commands.forEach(function (run) { 17 | var taskName = getCommandTask(run); 18 | 19 | if(!tasks.hasOwnProperty(taskName)) { 20 | console.log('Task "'+taskName+'" is not found.'); 21 | return; 22 | } 23 | 24 | var task = tasks[taskName]; 25 | var taskConfig = config.getRaw(taskName); 26 | var target = getCommandTarget(run); 27 | 28 | if (task.isMulti()) { 29 | if (target && !taskConfig[target]) { 30 | console.log('Task "'+taskName+':'+target+'" not found.'); 31 | return; 32 | } 33 | if (!target) { 34 | var targets = getTaskTargets(taskConfig, taskName); 35 | results.push(parseCommands(config, tasks, targets)); 36 | return; 37 | } 38 | } 39 | if (task.isAlias()) { 40 | results.push(parseCommands(config, tasks, task.fn)); 41 | return; 42 | } 43 | results.push(run); 44 | }); 45 | return _.flatten(results); 46 | }; 47 | 48 | module.exports = parseCommands; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-next", 3 | "description": "Grunt Next", 4 | "version": "0.2.0", 5 | "homepage": "https://github.com/gruntjs/grunt-next", 6 | "author": { 7 | "name": "Tyler Kellen", 8 | "url": "http://goingslowly.com/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/gruntjs/grunt-next.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/gruntjs/grunt-next/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/gruntjs/grunt-next/blob/master/LICENSE" 21 | } 22 | ], 23 | "bin": { 24 | "gn": "bin/index.js" 25 | }, 26 | "engines": { 27 | "node": ">= 0.10.0" 28 | }, 29 | "main": "runner/index", 30 | "scripts": { 31 | "test": "mocha -R spec test/index.js" 32 | }, 33 | "keywords": [], 34 | "dependencies": { 35 | "liftoff": "~0.9.3", 36 | "lodash": "~2.4.1", 37 | "pretty-hrtime": "~0.2.0", 38 | "orchestrator": "~0.3.3", 39 | "chalk": "~0.4.0", 40 | "glob": "~3.2.8", 41 | "bluebird": "~1.0.4", 42 | "configfiles": "~0.3.3", 43 | "dateformat": "~1.0.7-1.2.3", 44 | "eventemitter2": "~0.4.13", 45 | "minimatch": "~0.2.14", 46 | "js-yaml": "~3.0.1", 47 | "rimraf": "~2.2.6", 48 | "iconv-lite": "~0.2.11", 49 | "colors": "~0.6.2", 50 | "nopt": "~2.2.0", 51 | "coffee-script": "~1.7.1", 52 | "expander": "~0.3.3", 53 | "grunt-legacy-util": "~0.1.2", 54 | "gaze": "0.5.1" 55 | }, 56 | "devDependencies": { 57 | "grunt-contrib-jshint": "~0.8.0", 58 | "chai": "~1.9.0", 59 | "mocha": "~1.17.1", 60 | "grunt": "~0.4.2", 61 | "grunt-contrib-concat": "~0.3.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /watch/lib/runner.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits; 2 | const EE = require('events').EventEmitter; 3 | const spawn = require('child_process').spawn; 4 | const Orchestrator = require('orchestrator'); 5 | 6 | function Runner (grunt) { 7 | if (!(this instanceof Runner)) { return new Runner(grunt); } 8 | EE.call(this); 9 | this.grunt = grunt; 10 | this.running = false; 11 | this.runner = null; 12 | this.spawner = null; 13 | } 14 | module.exports = Runner; 15 | inherits(Runner, EE); 16 | 17 | Runner.prototype.run = function (request, options, cb) { 18 | var self = this; 19 | 20 | // Interrupt current tasks 21 | if (this.running && options.interrupt === true) { 22 | return this.interrupt(function() { 23 | self.run(request, options, cb); 24 | }); 25 | } 26 | 27 | this.running = true; 28 | var start = Date.now(); 29 | this[options.spawn ? 'spawn' : 'nospawn'](request, options, function() { 30 | self.running = false; 31 | cb(Date.now() - start); 32 | }); 33 | }; 34 | 35 | Runner.prototype.nospawn = function (request, options, cb) { 36 | var grunt = this.grunt; 37 | var runner = this.runner; 38 | var commands = grunt.parseCommands(request); 39 | var taskList = grunt.buildTasks(commands); 40 | runner = grunt.buildRunner(taskList); 41 | runner.start(commands, cb); 42 | }; 43 | 44 | Runner.prototype.spawn = function (request, options, cb) { 45 | if (!Array.isArray(request)) { request = [request]; } 46 | // TODO: Use wait-grunt instead here 47 | // TODO: Use mtime to write a manifest so changed files are available to spawn'd tasks 48 | this.spawner = this.grunt.util.spawn({ 49 | grunt: true, 50 | opts: { 51 | stdio: 'inherit', 52 | cwd: options.cwd, 53 | }, 54 | args: request.concat(options.cliArgs || []), 55 | }, cb); 56 | }; 57 | 58 | // Interrupt the current running tasks 59 | Runner.prototype.interrupt = function (cb) { 60 | this.running = false; 61 | this.runner = null; 62 | this.spawner = null; 63 | this.emit('interrupt'); 64 | if (this.runner) { 65 | this.runner.stop(); 66 | cb(); 67 | } else if (this.spawner) { 68 | this.spawner.on('close', cb); 69 | this.spawner.kill(); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /test/runner/parse_commands.js: -------------------------------------------------------------------------------- 1 | const Task = require('../../task'); 2 | const expander = require('expander'); 3 | const parseCommands = require('../../runner/lib/parse_commands'); 4 | 5 | describe('parseCommands', function () { 6 | 7 | var config = expander.interface({ 8 | single: { 9 | src: ['text/fixtures/files/*.js'] 10 | }, 11 | multi: { 12 | options: {}, 13 | target: ['test/fixtures/files/*.js'], 14 | targetTwo: { 15 | src: ['test/fixtures/files/*.js'] 16 | } 17 | } 18 | }); 19 | 20 | var tasks = { 21 | single: new Task({ 22 | name: 'single', 23 | desc: 'Single task.', 24 | type: 'single', 25 | fn: function () {} 26 | }), 27 | multi: new Task({ 28 | name: 'multi', 29 | desc: 'Multi task.', 30 | type: 'multi', 31 | fn: function () {} 32 | }), 33 | alias: new Task({ 34 | name: 'alias', 35 | desc: 'Alias task.', 36 | type: 'alias', 37 | fn: ['multi:targetTwo'] 38 | }), 39 | nested_alias: new Task({ 40 | name: 'nested_alias', 41 | desc: 'Nested alias task.', 42 | type: 'alias', 43 | fn: ['alias'] 44 | }), 45 | multi_alias: new Task({ 46 | name: 'multi_alias', 47 | desc: 'Alias task to multi task.', 48 | type: 'alias', 49 | fn: ['multi'] 50 | }) 51 | }; 52 | 53 | it('should remove invalid task requests', function () { 54 | var commands = parseCommands(config, tasks, ['empty']); 55 | expect(commands).to.deep.equal([]); 56 | 57 | commands = parseCommands(config, tasks, ['invalid_alias']); 58 | expect(commands).to.deep.equal([]); 59 | }); 60 | 61 | it('should expand multi task commands without targets', function () { 62 | var commands = parseCommands(config, tasks, ['multi']); 63 | expect(commands).to.deep.equal(['multi:target','multi:targetTwo']); 64 | 65 | commands = parseCommands(config, tasks, ['multi_alias']); 66 | expect(commands).to.deep.equal(['multi:target','multi:targetTwo']); 67 | }); 68 | 69 | it('should recursively resolve aliases', function () { 70 | var commands = parseCommands(config, tasks, ['nested_alias']); 71 | expect(commands).to.deep.equal(['multi:targetTwo']); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // temporarily make coffee-script configs work automatically 4 | require('coffee-script/register'); 5 | 6 | const prettyTime = require('pretty-hrtime'); 7 | const chalk = require('chalk'); 8 | const Liftoff = require('liftoff'); 9 | 10 | const cli = new Liftoff({ 11 | moduleName: 'grunt-next', 12 | configName: 'Gruntfile', 13 | processTitle: 'grunt-next', 14 | cwdFlag: 'base', 15 | searchHome: true 16 | }) 17 | 18 | cli.on('require', function (name, module) { 19 | if (name === 'coffee-script') { 20 | module.register(); 21 | } 22 | console.log('Requiring external module:', name); 23 | }); 24 | 25 | cli.on('requireFail', function (name, err) { 26 | console.log('Unable to require external module:', name, err); 27 | }); 28 | 29 | cli.launch(handler); 30 | 31 | function handler (env) { 32 | var argv = env.argv; 33 | var tasks = argv._; 34 | var commands = tasks.length ? tasks : ['default']; 35 | 36 | if (!env.configPath) { 37 | console.log('No Gruntfile found.'); 38 | process.exit(1); 39 | } 40 | if (!env.modulePath) { 41 | console.log('No local installation of grunt-next found.'); 42 | process.exit(1); 43 | } 44 | 45 | var Grunt = require(env.modulePath); 46 | if(process.cwd != env.cwd) { 47 | process.chdir(env.cwd); 48 | } 49 | 50 | var grunt = new Grunt(env); 51 | logEvents(grunt); 52 | 53 | if (grunt.option('debug')) { 54 | grunt.on('run.*', function (msg) { 55 | console.log(this.event, msg); 56 | }); 57 | grunt.on('register', function (msg) { 58 | console.log(this.event, msg); 59 | }); 60 | } 61 | 62 | require(env.configPath)(grunt); 63 | grunt.run(commands); 64 | }; 65 | 66 | function formatError (e) { 67 | if (!e.err) return e.message; 68 | if (e.err.message) return e.err.message; 69 | return JSON.stringify(e.err); 70 | } 71 | 72 | function logEvents (emitter) { 73 | 74 | emitter.on('task_start', function (e) { 75 | console.log('Running', "'"+chalk.cyan(e.task)+"'..."); 76 | }); 77 | emitter.on('task_stop', function (e) { 78 | var time = prettyTime(e.hrDuration); 79 | console.log('Finished', "'"+chalk.cyan(e.task)+"'", 'in', chalk.magenta(time)); 80 | }); 81 | emitter.on('task_not_found', function (err) { 82 | console.log(chalk.red("Task '"+err.task+"' was not defined in your Gruntfile but you tried to run it.")); 83 | console.log('Please check the documentation for proper Gruntfile formatting.'); 84 | process.exit(1); 85 | }); 86 | 87 | }; 88 | -------------------------------------------------------------------------------- /legacy/lib/fail.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var grunt = { 13 | option: require('./option') 14 | }; 15 | 16 | // The module to be exported. 17 | var fail = module.exports = {}; 18 | 19 | // Error codes. 20 | fail.code = { 21 | FATAL_ERROR: 1, 22 | MISSING_GRUNTFILE: 2, 23 | TASK_FAILURE: 3, 24 | TEMPLATE_ERROR: 4, 25 | INVALID_AUTOCOMPLETE: 5, 26 | WARNING: 6, 27 | }; 28 | 29 | // DRY it up! 30 | function writeln(e, mode) { 31 | grunt.log.muted = false; 32 | var msg = String(e.message || e); 33 | if (!grunt.option('no-color')) { msg += '\x07'; } // Beep! 34 | if (mode === 'warn') { 35 | msg = 'Warning: ' + msg + ' '; 36 | msg += (grunt.option('force') ? 'Used --force, continuing.'.underline : 'Use --force to continue.'); 37 | msg = msg.yellow; 38 | } else { 39 | msg = ('Fatal error: ' + msg).red; 40 | } 41 | grunt.log.writeln(msg); 42 | } 43 | 44 | // If --stack is enabled, log the appropriate error stack (if it exists). 45 | function dumpStack(e) { 46 | if (grunt.option('stack')) { 47 | if (e.origError && e.origError.stack) { 48 | console.log(e.origError.stack); 49 | } else if (e.stack) { 50 | console.log(e.stack); 51 | } 52 | } 53 | } 54 | 55 | // A fatal error occurred. Abort immediately. 56 | fail.fatal = function(e, errcode) { 57 | writeln(e, 'fatal'); 58 | dumpStack(e); 59 | grunt.util.exit(typeof errcode === 'number' ? errcode : fail.code.FATAL_ERROR); 60 | }; 61 | 62 | // Keep track of error and warning counts. 63 | fail.errorcount = 0; 64 | fail.warncount = 0; 65 | 66 | // A warning occurred. Abort immediately unless -f or --force was used. 67 | fail.warn = function(e, errcode) { 68 | var message = typeof e === 'string' ? e : e.message; 69 | fail.warncount++; 70 | writeln(message, 'warn'); 71 | // If -f or --force aren't used, stop script processing. 72 | if (!grunt.option('force')) { 73 | dumpStack(e); 74 | grunt.log.writeln().fail('Aborted due to warnings.'); 75 | grunt.util.exit(typeof errcode === 'number' ? errcode : fail.code.WARNING); 76 | } 77 | }; 78 | 79 | // This gets called at the very end. 80 | fail.report = function() { 81 | if (fail.warncount > 0) { 82 | grunt.log.writeln().fail('Done, but with warnings.'); 83 | } else { 84 | grunt.log.writeln().success('Done, without errors.'); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /test/task/command_data.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Task = require('../../task'); 3 | 4 | var config = { 5 | options: { 6 | setting: true, 7 | someKey: true 8 | }, 9 | targetOne: ['test/fixtures/files/*.js'], 10 | targetTwo: { 11 | src: ['test/fixtures/files/*.js'], 12 | options: { 13 | setting: false 14 | } 15 | } 16 | }; 17 | 18 | var context = buildContext(config, ['test','targetTwo'], 'multi'); 19 | 20 | describe('buildContext', function () { 21 | 22 | describe('async', function () { 23 | it('should be a function', function () { 24 | expect(context.async).to.be.a('function'); 25 | }); 26 | 27 | it('should resolve an internal promise if called with no arguments', function (done) { 28 | var cb = context.async(); 29 | context.deferred.promise.then(function () { 30 | done(); 31 | }); 32 | cb(); 33 | }); 34 | 35 | it('should reject the internal promise if called with no arguments', function (done) { 36 | var cb = context.async(); 37 | context.deferred.promise.catch(function (e) { 38 | expect(e).to.equal('error'); 39 | done(); 40 | }); 41 | cb('error'); 42 | }); 43 | }); 44 | 45 | describe('options', function () { 46 | it('should be a function', function () { 47 | expect(context.options).to.be.a('function'); 48 | }); 49 | 50 | it('should return the merged task options if no defaults are provided', function () { 51 | expect(context.options({})).to.deep.equal({ 52 | setting: false, 53 | someKey: true 54 | }); 55 | }); 56 | it('should return the merged task options, with defaults supplied', function () { 57 | expect(context.options({defaultKey:true})).to.deep.equal({ 58 | setting: false, 59 | someKey: true, 60 | defaultKey: true 61 | }); 62 | }); 63 | }); 64 | 65 | describe('files', function () { 66 | it('should contain an array src/dest pairs', function () { 67 | expect(context.files).to.deep.equal([ 68 | { src: ['test/fixtures/files/bar.js', 69 | 'test/fixtures/files/baz.js', 70 | 'test/fixtures/files/foo.js'] } 71 | ]); 72 | }); 73 | }); 74 | 75 | describe('filesSrc', function () { 76 | it('should contain an array of sources', function () { 77 | expect(context.filesSrc).to.deep.equal([ 78 | 'test/fixtures/files/bar.js', 79 | 'test/fixtures/files/baz.js', 80 | 'test/fixtures/files/foo.js' 81 | ]); 82 | }); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /watch/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | const _ = require('lodash'); 3 | const runner = require('./lib/runner.js')(grunt); 4 | 5 | grunt.registerMultiTask('watch', function() { 6 | var gaze = require('gaze'); 7 | var name = this.name || 'watch'; 8 | var files = this.data.files; 9 | var tasks = this.data.tasks; 10 | var target = this.target; 11 | var options = this.options({ 12 | spawn: false, 13 | cliArgs: _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.env.argv._)), 14 | atBegin: false, 15 | interrupt: false, 16 | event: ['all'], 17 | cwd: grunt.env.cwd, 18 | livereload: false, // TODO: not yet implemented 19 | }); 20 | 21 | // When files are changed, call the runner 22 | var changedFiles = Object.create(null); 23 | grunt.config([name, target, 'changed'].join('.'), []); 24 | var changed = _.debounce(function(tasks, options, cb) { 25 | grunt.config([name, target, 'changed'].join('.'), Object.keys(changedFiles)); 26 | if (typeof tasks === 'function') { 27 | // If tasks is a custom function, run that instead 28 | var start = Date.now(); 29 | tasks([name, target].join('.'), options, function() { 30 | cb(Date.now() - start); 31 | }); 32 | } else { 33 | // Otherwise use the built in runner 34 | runner.run(tasks, options, cb); 35 | } 36 | }, 100); 37 | 38 | // Start up gaze on the file patterns 39 | function start() { 40 | gaze(files, function () { 41 | grunt.log.writeln('Watching ' + target + '...'); 42 | this.on('all', function(event, filepath) { 43 | 44 | // Skip events not specified 45 | if (!_.contains(options.event, 'all') && !_.contains(options.event, event)) { 46 | return; 47 | } 48 | 49 | // Track changed files and notify changed has occurred 50 | changedFiles[filepath] = event; 51 | changed(tasks, options, function(time) { 52 | grunt.log.writeln('Completed "' + target + '" tasks in ' + (time || 0 / 1000) + ' s. Waiting...'); 53 | changedFiles = Object.create(null); 54 | }); 55 | }); 56 | }); 57 | } 58 | 59 | // If we want to run tasks at the beginning 60 | if (options.atBegin) { 61 | grunt.log.writeln('Running "' + target + '" at beginning...'); 62 | changed(tasks, options, start); 63 | } else { 64 | start(); 65 | } 66 | }); 67 | }; 68 | 69 | // TODO: doesnt yet use mtime for traversing file dependencies for changed files 70 | // TODO: fix formatting on completed time displayed 71 | // TODO: no options.dateFormat implementation 72 | // TODO: no advanced options.cwd implementation 73 | // TODO: no livereload (does it still belong in the watch? - shama) 74 | // TODO: no watch event (I want to nix it - shama) 75 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | meta: { 4 | pkg: require('./package.json'), 5 | }, 6 | tests: 'tests/fixtures', 7 | jshint: { 8 | options: { 9 | jshintrc: '.jshintrc' 10 | }, 11 | runner: ['runner/**/*.js'], 12 | task: ['task/**/*.js'], 13 | tests: ['test/**/*.js'], 14 | changed: '<%= watch.files.changed %>' 15 | }, 16 | multi: { 17 | tara: {}, 18 | tyler: {} 19 | }, 20 | concat: { 21 | options: { 22 | banner: '/*! <%= meta.pkg.name %> - v<%= meta.pkg.version %>\n' + 23 | ' * License: <%= meta.pkg.licenses[0].type %>\n' + 24 | ' * Date: <%= grunt.template.today("yyyy-mm-dd") %>\n' + 25 | ' * Target: <%= grunt.task.current.target %>\n' + 26 | ' */\n' 27 | }, 28 | test: { 29 | src: 'test/fixtures/files/*.js', 30 | dest: 'tmp/concat.js' 31 | }, 32 | dude: { 33 | src: 'test/fixtures/files/*.js', 34 | dest: 'tmp/dude.js' 35 | } 36 | }, 37 | watch: { 38 | files: { 39 | files: ['test/fixtures/**/*.js'], 40 | tasks: ['series1', 'jshint:changed'] 41 | }, 42 | tasks: { 43 | options: { spawn: true }, 44 | files: ['test/fixtures/tasks/*.js'], 45 | tasks: ['series0'] 46 | }, 47 | custom: { 48 | files: ['test/fixtures/**/*.js'], 49 | // Specify a custom task runner of your choosing 50 | tasks: function (target, options, done) { 51 | // Get changed files 52 | var changedFiles = grunt.config(target + '.changed'); 53 | // Do something custom 54 | grunt.log.writeln('My custom task runner for ' + target); 55 | // Call when done 56 | done(); 57 | } 58 | }, 59 | } 60 | }); 61 | grunt.registerTask('series0', function () { 62 | var done = this.async(); 63 | setTimeout(function () { 64 | console.log('series0'); 65 | done(); 66 | }, 1000); 67 | }); 68 | grunt.registerTask('series1', function () { 69 | var done = this.async(); 70 | setTimeout(function () { 71 | console.log('series1'); 72 | done(); 73 | }, 400); 74 | }); 75 | grunt.registerTask('series2', function () { 76 | console.log('series2'); 77 | }); 78 | grunt.registerTask('single', function () { 79 | console.log(this); 80 | }); 81 | grunt.registerMultiTask('multi', function () { 82 | console.log(this); 83 | }); 84 | 85 | grunt.loadTasks('watch'); 86 | grunt.loadNpmTasks('grunt-contrib-jshint'); 87 | grunt.loadNpmTasks('grunt-contrib-concat'); 88 | grunt.registerTask('nestedAlias', 'jshint'); 89 | grunt.registerTask('deeplyNestedAlias', 'nestedAlias'); 90 | grunt.registerTask('series', ['series0', 'series1', 'series2']); 91 | grunt.registerTask('default', 'deeplyNestedAlias'); 92 | 93 | }; 94 | -------------------------------------------------------------------------------- /task/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Promise = require('bluebird'); 3 | const configFiles = require('configFiles'); 4 | 5 | function Task (opts) { 6 | _.extend(this, opts); 7 | } 8 | 9 | Task.prototype.isMulti = function () { 10 | return this.type === 'multi'; 11 | }; 12 | 13 | Task.prototype.isSingle = function () { 14 | return this.type === 'single'; 15 | }; 16 | 17 | Task.prototype.isAlias = function () { 18 | return this.type === 'alias'; 19 | }; 20 | 21 | Task.prototype.commandData = function (command) { 22 | var data = {}; 23 | 24 | // Task name. 25 | data.name = command[0]; 26 | 27 | // Extract target/args depending on task type. 28 | if(this.isMulti()) { 29 | data.target = command[1]; 30 | data.args = command.slice(2); 31 | } else { 32 | data.args = command.slice(1); 33 | } 34 | 35 | // The arguments passed to the task in string form. 36 | data.nameArgs = data.args.join(':'); 37 | 38 | // Store flags for each argument passed to the task. 39 | data.flags = data.args.reduce(function (flags, item) { 40 | flags[item] = true; 41 | return flags; 42 | }, {}); 43 | 44 | return data; 45 | }; 46 | 47 | Task.prototype.buildContext = function (config) { 48 | var context = {}; 49 | 50 | context.data = config; 51 | context.options = config.options; 52 | context.files = configFiles(config); 53 | 54 | // Shallow merge task options over provided defaults. 55 | context.options = function (defaults) { 56 | return _.extend({}, defaults, config.options); 57 | }; 58 | 59 | // Lazily extract source files for this task/target. 60 | Object.defineProperty(context, 'filesSrc', { 61 | get: function () { 62 | return _.uniq(_.flatten(_.pluck(context.files||[], 'src'))); 63 | } 64 | }); 65 | 66 | // This is a hack to make grunt's `this.async()` method work with 67 | // Orchestrator. It returns a "callback" function that will reject 68 | // or resolve an internal promise. 69 | context.async = function () { 70 | context.deferred = Promise.defer(); 71 | return function (msg) { 72 | // this needs to be brought in line with grunt 73 | if (msg) { 74 | context.deferred.reject(msg); 75 | } else { 76 | context.deferred.resolve(true); 77 | } 78 | }; 79 | }; 80 | 81 | return context; 82 | }; 83 | 84 | 85 | Task.prototype.build = function (config, command) { 86 | return function () { 87 | // build the context object for this task 88 | var context = _.extend(this.buildContext(config), this.commandData(command)); 89 | 90 | // Execute the actual task method from registerTask or registerMultiTask. 91 | var ret = this.fn.call(context); 92 | 93 | // Conditionally returning a promise in this manner is a very bad 94 | // practice but we need a way to properly hint to Orchestrator when 95 | // an async task is complete. Typically this is performed by taking 96 | // in a callback, but we can't do that for historical reasons--there 97 | // are several thousand grunt plugins in the wild. 98 | if (!context.deferred) { 99 | return ret; 100 | } else { 101 | return context.deferred.promise; 102 | } 103 | }.bind(this); 104 | }; 105 | 106 | module.exports = Task; 107 | -------------------------------------------------------------------------------- /runner/index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const pkg = require('../package'); 3 | const Task = require('../task'); 4 | const legacy = require('../legacy'); 5 | const expander = require('expander'); 6 | const bindMany = require('./lib/utils/bind_many'); 7 | const parseRegister = require('./lib/parse_register'); 8 | const findTaskFiles = require('./lib/find_task_files'); 9 | const loadTaskFiles = require('./lib/load_task_files'); 10 | const parseCommands = require('./lib/parse_commands'); 11 | const indexCommands = require('./lib/index_commands'); 12 | const buildTaskList = require('./lib/build_task_list'); 13 | const EventEmitter2 = require('eventemitter2').EventEmitter2; 14 | const Orchestrator = require('orchestrator'); 15 | 16 | function Grunt (env) { 17 | this.env = env; 18 | this.events = this; 19 | this.tasks = []; 20 | this.option = expander.interface(this.env.argv); 21 | EventEmitter2.call(this, {wildcard:true}); 22 | bindMany(['loadTasks', 'loadNpmTasks'], this); 23 | legacy(this); 24 | } 25 | util.inherits(Grunt, EventEmitter2); 26 | 27 | Grunt.prototype.init = function (data) { 28 | this.config = expander.interface(data, { 29 | imports: { 30 | grunt: { 31 | template: this.template, 32 | option: this.option 33 | } 34 | } 35 | }); 36 | }; 37 | Grunt.prototype.initConfig = Grunt.prototype.init; 38 | Grunt.prototype.package = pkg; 39 | Grunt.prototype.version = pkg.version; 40 | 41 | Grunt.prototype.register = function (task, constructor) { 42 | this.tasks[task.name] = new constructor(task); 43 | }; 44 | Grunt.prototype.registerTask = function () { 45 | this.register(parseRegister(arguments, 'single'), Task); 46 | }; 47 | Grunt.prototype.registerMultiTask = function () { 48 | this.register(parseRegister(arguments, 'multi'), Task); 49 | }; 50 | Grunt.prototype.loadTasks = function (input) { 51 | loadTaskFiles(findTaskFiles(input), this); 52 | }; 53 | Grunt.prototype.loadNpmTasks = function (input) { 54 | loadTaskFiles(findTaskFiles(input, true), this); 55 | }; 56 | Grunt.prototype.renameTask = function (oldName, newName) { 57 | console.log(oldName, newName); 58 | }; 59 | 60 | Grunt.prototype.parseCommands = function (request) { 61 | // remove invalid requests / resolve aliases / expand multi tasks 62 | var commands = parseCommands(this.config, this.tasks, request); 63 | this.emit('run.parseCommands', commands); 64 | // return em 65 | return commands; 66 | }; 67 | 68 | Grunt.prototype.buildTasks = function (commands) { 69 | // group commands by their root task 70 | var indexedCommands = indexCommands(commands); 71 | this.emit('run.indexCommands', indexedCommands); 72 | // build a listing of tasks to put into orchestrator 73 | var taskList = buildTaskList(this.config, this.tasks, indexedCommands); 74 | this.emit('run.buildTaskList', taskList); 75 | // return em 76 | return taskList; 77 | }; 78 | 79 | Grunt.prototype.buildRunner = function (taskList) { 80 | // build an orchestration 81 | var runner = new Orchestrator(); 82 | taskList.forEach(function (task) { 83 | runner.add(task.name, task.method); 84 | }); 85 | 86 | // emit some stuff (the next v of orchestrator uses ee2) 87 | runner.on('task_start', function (e) { 88 | this.emit('task_start', e); 89 | }.bind(this)); 90 | runner.on('task_stop', function (e) { 91 | this.emit('task_stop', e); 92 | }.bind(this)); 93 | 94 | return runner; 95 | }; 96 | 97 | Grunt.prototype.run = function (request) { 98 | var commands = this.parseCommands(request); 99 | var taskList = this.buildTasks(commands); 100 | var runner = this.buildRunner(taskList); 101 | runner.start(commands); 102 | }; 103 | 104 | module.exports = Grunt; 105 | -------------------------------------------------------------------------------- /legacy/lib/help.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var grunt = require('../grunt'); 13 | 14 | // Nodejs libs. 15 | var path = require('path'); 16 | 17 | // Set column widths. 18 | var col1len = 0; 19 | exports.initCol1 = function(str) { 20 | col1len = Math.max(col1len, str.length); 21 | }; 22 | exports.initWidths = function() { 23 | // Widths for options/tasks table output. 24 | exports.widths = [1, col1len, 2, 76 - col1len]; 25 | }; 26 | 27 | // Render an array in table form. 28 | exports.table = function(arr) { 29 | arr.forEach(function(item) { 30 | grunt.log.writetableln(exports.widths, ['', grunt.util._.pad(item[0], col1len), '', item[1]]); 31 | }); 32 | }; 33 | 34 | // Methods to run, in-order. 35 | exports.queue = [ 36 | 'initOptions', 37 | 'initTasks', 38 | 'initWidths', 39 | 'header', 40 | 'usage', 41 | 'options', 42 | 'optionsFooter', 43 | 'tasks', 44 | 'footer', 45 | ]; 46 | 47 | // Actually display stuff. 48 | exports.display = function() { 49 | exports.queue.forEach(function(name) { exports[name](); }); 50 | }; 51 | 52 | 53 | // Header. 54 | exports.header = function() { 55 | grunt.log.writeln('Grunt: The JavaScript Task Runner (v' + grunt.version + ')'); 56 | }; 57 | 58 | // Usage info. 59 | exports.usage = function() { 60 | grunt.log.header('Usage'); 61 | grunt.log.writeln(' ' + path.basename(process.argv[1]) + ' [options] [task [task ...]]'); 62 | }; 63 | 64 | // Options. 65 | exports.initOptions = function() { 66 | // Build 2-column array for table view. 67 | exports._options = Object.keys(grunt.cli.optlist).map(function(long) { 68 | var o = grunt.cli.optlist[long]; 69 | var col1 = '--' + (o.negate ? 'no-' : '') + long + (o.short ? ', -' + o.short : ''); 70 | exports.initCol1(col1); 71 | return [col1, o.info]; 72 | }); 73 | }; 74 | 75 | exports.options = function() { 76 | grunt.log.header('Options'); 77 | exports.table(exports._options); 78 | }; 79 | 80 | exports.optionsFooter = function() { 81 | grunt.log.writeln().writelns( 82 | 'Options marked with * have methods exposed via the grunt API and should ' + 83 | 'instead be specified inside the Gruntfile wherever possible.' 84 | ); 85 | }; 86 | 87 | // Tasks. 88 | exports.initTasks = function() { 89 | // Initialize task system so that the tasks can be listed. 90 | grunt.task.init([], {help: true}); 91 | 92 | // Build object of tasks by info (where they were loaded from). 93 | exports._tasks = []; 94 | Object.keys(grunt.task._tasks).forEach(function(name) { 95 | exports.initCol1(name); 96 | var task = grunt.task._tasks[name]; 97 | exports._tasks.push(task); 98 | }); 99 | }; 100 | 101 | exports.tasks = function() { 102 | grunt.log.header('Available tasks'); 103 | if (exports._tasks.length === 0) { 104 | grunt.log.writeln('(no tasks found)'); 105 | } else { 106 | exports.table(exports._tasks.map(function(task) { 107 | var info = task.info; 108 | if (task.multi) { info += ' *'; } 109 | return [task.name, info]; 110 | })); 111 | 112 | grunt.log.writeln().writelns( 113 | 'Tasks run in the order specified. Arguments may be passed to tasks that ' + 114 | 'accept them by using colons, like "lint:files". Tasks marked with * are ' + 115 | '"multi tasks" and will iterate over all sub-targets if no argument is ' + 116 | 'specified.' 117 | ); 118 | } 119 | 120 | grunt.log.writeln().writelns( 121 | 'The list of available tasks may change based on tasks directories or ' + 122 | 'grunt plugins specified in the Gruntfile or via command-line options.' 123 | ); 124 | }; 125 | 126 | // Footer. 127 | exports.footer = function() { 128 | grunt.log.writeln().writeln('For more information, see http://gruntjs.com/'); 129 | }; 130 | -------------------------------------------------------------------------------- /legacy/lib/cli.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var grunt = require('../grunt'); 13 | 14 | // Nodejs libs. 15 | var path = require('path'); 16 | 17 | // External libs. 18 | var nopt = require('nopt'); 19 | 20 | // This is only executed when run via command line. 21 | var cli = module.exports = function(options, done) { 22 | // CLI-parsed options override any passed-in "default" options. 23 | if (options) { 24 | // For each default option... 25 | Object.keys(options).forEach(function(key) { 26 | if (!(key in cli.options)) { 27 | // If this option doesn't exist in the parsed cli.options, add it in. 28 | cli.options[key] = options[key]; 29 | } else if (cli.optlist[key].type === Array) { 30 | // If this option's type is Array, append it to any existing array 31 | // (or create a new array). 32 | [].push.apply(cli.options[key], options[key]); 33 | } 34 | }); 35 | } 36 | 37 | // Run tasks. 38 | grunt.tasks(cli.tasks, cli.options, done); 39 | }; 40 | 41 | // Default options. 42 | var optlist = cli.optlist = { 43 | help: { 44 | short: 'h', 45 | info: 'Display this help text.', 46 | type: Boolean 47 | }, 48 | base: { 49 | info: 'Specify an alternate base path. By default, all file paths are relative to the Gruntfile. (grunt.file.setBase) *', 50 | type: path 51 | }, 52 | color: { 53 | info: 'Disable colored output.', 54 | type: Boolean, 55 | negate: true 56 | }, 57 | gruntfile: { 58 | info: 'Specify an alternate Gruntfile. By default, grunt looks in the current or parent directories for the nearest Gruntfile.js or Gruntfile.coffee file.', 59 | type: path 60 | }, 61 | debug: { 62 | short: 'd', 63 | info: 'Enable debugging mode for tasks that support it.', 64 | type: [Number, Boolean] 65 | }, 66 | stack: { 67 | info: 'Print a stack trace when exiting with a warning or fatal error.', 68 | type: Boolean 69 | }, 70 | force: { 71 | short: 'f', 72 | info: 'A way to force your way past warnings. Want a suggestion? Don\'t use this option, fix your code.', 73 | type: Boolean 74 | }, 75 | tasks: { 76 | info: 'Additional directory paths to scan for task and "extra" files. (grunt.loadTasks) *', 77 | type: Array 78 | }, 79 | npm: { 80 | info: 'Npm-installed grunt plugins to scan for task and "extra" files. (grunt.loadNpmTasks) *', 81 | type: Array 82 | }, 83 | write: { 84 | info: 'Disable writing files (dry run).', 85 | type: Boolean, 86 | negate: true 87 | }, 88 | verbose: { 89 | short: 'v', 90 | info: 'Verbose mode. A lot more information output.', 91 | type: Boolean 92 | }, 93 | version: { 94 | short: 'V', 95 | info: 'Print the grunt version. Combine with --verbose for more info.', 96 | type: Boolean 97 | }, 98 | // Even though shell auto-completion is now handled by grunt-cli, leave this 99 | // option here for display in the --help screen. 100 | completion: { 101 | info: 'Output shell auto-completion rules. See the grunt-cli documentation for more information.', 102 | type: String 103 | }, 104 | }; 105 | 106 | // Parse `optlist` into a form that nopt can handle. 107 | var aliases = {}; 108 | var known = {}; 109 | 110 | Object.keys(optlist).forEach(function(key) { 111 | var short = optlist[key].short; 112 | if (short) { 113 | aliases[short] = '--' + key; 114 | } 115 | known[key] = optlist[key].type; 116 | }); 117 | 118 | var parsed = nopt(known, aliases, process.argv, 2); 119 | cli.tasks = parsed.argv.remain; 120 | cli.options = parsed; 121 | delete parsed.argv; 122 | 123 | // Initialize any Array options that weren't initialized. 124 | Object.keys(optlist).forEach(function(key) { 125 | if (optlist[key].type === Array && !(key in cli.options)) { 126 | cli.options[key] = []; 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /legacy/lib/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var grunt = { 13 | util: require('grunt-legacy-util'), 14 | option: require('./option'), 15 | fail: 0 16 | }; 17 | 18 | // Nodejs libs. 19 | var util = require('util'); 20 | 21 | // The module to be exported. 22 | var log = module.exports = {}; 23 | 24 | // External lib. Requiring this here modifies the String prototype! 25 | var colors = require('colors'); 26 | 27 | // Disable colors if --no-colors was passed. 28 | log.initColors = function() { 29 | var util = grunt.util; 30 | if (grunt.option('no-color')) { 31 | // String color getters should just return the string. 32 | colors.mode = 'none'; 33 | // Strip colors from strings passed to console.log. 34 | util.hooker.hook(console, 'log', function() { 35 | var args = util.toArray(arguments); 36 | return util.hooker.filter(this, args.map(function(arg) { 37 | return util.kindOf(arg) === 'string' ? colors.stripColors(arg) : arg; 38 | })); 39 | }); 40 | } 41 | }; 42 | 43 | // Temporarily suppress output. 44 | var suppressOutput; 45 | 46 | // Allow external muting of output. 47 | log.muted = false; 48 | 49 | // True once anything has actually been logged. 50 | var hasLogged; 51 | 52 | // Parse certain markup in strings to be logged. 53 | function markup(str) { 54 | str = str || ''; 55 | // Make _foo_ underline. 56 | str = str.replace(/(\s|^)_(\S|\S[\s\S]+?\S)_(?=[\s,.!?]|$)/g, '$1' + '$2'.underline); 57 | // Make *foo* bold. 58 | str = str.replace(/(\s|^)\*(\S|\S[\s\S]+?\S)\*(?=[\s,.!?]|$)/g, '$1' + '$2'.bold); 59 | return str; 60 | } 61 | 62 | // Similar to util.format in the standard library, however it'll always 63 | // cast the first argument to a string and treat it as the format string. 64 | function format(args) { 65 | // Args is a argument array so copy it in order to avoid wonky behavior. 66 | args = [].slice.call(args, 0); 67 | if (args.length > 0) { 68 | args[0] = String(args[0]); 69 | } 70 | return util.format.apply(util, args); 71 | } 72 | 73 | function write(msg) { 74 | msg = msg || ''; 75 | // Actually write output. 76 | if (!log.muted && !suppressOutput) { 77 | hasLogged = true; 78 | // Users should probably use the colors-provided methods, but if they 79 | // don't, this should strip extraneous color codes. 80 | if (grunt.option('no-color')) { msg = colors.stripColors(msg); } 81 | // Actually write to stdout. 82 | process.stdout.write(markup(msg)); 83 | } 84 | } 85 | 86 | function writeln(msg) { 87 | // Write blank line if no msg is passed in. 88 | msg = msg || ''; 89 | write(msg + '\n'); 90 | } 91 | 92 | // Write output. 93 | log.write = function() { 94 | write(format(arguments)); 95 | return log; 96 | }; 97 | 98 | // Write a line of output. 99 | log.writeln = function() { 100 | writeln(format(arguments)); 101 | return log; 102 | }; 103 | 104 | log.warn = function() { 105 | var msg = format(arguments); 106 | if (arguments.length > 0) { 107 | writeln('>> '.red + grunt.util._.trim(msg).replace(/\n/g, '\n>> '.red)); 108 | } else { 109 | writeln('ERROR'.red); 110 | } 111 | return log; 112 | }; 113 | log.error = function() { 114 | grunt.fail.errorcount++; 115 | log.warn.apply(log, arguments); 116 | return log; 117 | }; 118 | log.ok = function() { 119 | var msg = format(arguments); 120 | if (arguments.length > 0) { 121 | writeln('>> '.green + grunt.util._.trim(msg).replace(/\n/g, '\n>> '.green)); 122 | } else { 123 | writeln('OK'.green); 124 | } 125 | return log; 126 | }; 127 | log.errorlns = function() { 128 | var msg = format(arguments); 129 | log.error(log.wraptext(77, msg)); 130 | return log; 131 | }; 132 | log.oklns = function() { 133 | var msg = format(arguments); 134 | log.ok(log.wraptext(77, msg)); 135 | return log; 136 | }; 137 | log.success = function() { 138 | var msg = format(arguments); 139 | writeln(msg.green); 140 | return log; 141 | }; 142 | log.fail = function() { 143 | var msg = format(arguments); 144 | writeln(msg.red); 145 | return log; 146 | }; 147 | log.header = function() { 148 | var msg = format(arguments); 149 | // Skip line before header, but not if header is the very first line output. 150 | if (hasLogged) { writeln(); } 151 | writeln(msg.underline); 152 | return log; 153 | }; 154 | log.subhead = function() { 155 | var msg = format(arguments); 156 | // Skip line before subhead, but not if subhead is the very first line output. 157 | if (hasLogged) { writeln(); } 158 | writeln(msg.bold); 159 | return log; 160 | }; 161 | // For debugging. 162 | log.debug = function() { 163 | var msg = format(arguments); 164 | if (grunt.option('debug')) { 165 | writeln('[D] ' + msg.magenta); 166 | } 167 | return log; 168 | }; 169 | 170 | // Write a line of a table. 171 | log.writetableln = function(widths, texts) { 172 | writeln(log.table(widths, texts)); 173 | return log; 174 | }; 175 | 176 | // Wrap a long line of text to 80 columns. 177 | log.writelns = function() { 178 | var msg = format(arguments); 179 | writeln(log.wraptext(80, msg)); 180 | return log; 181 | }; 182 | 183 | // Display flags in verbose mode. 184 | log.writeflags = function(obj, prefix) { 185 | var wordlist; 186 | if (Array.isArray(obj)) { 187 | wordlist = log.wordlist(obj); 188 | } else if (typeof obj === 'object' && obj) { 189 | wordlist = log.wordlist(Object.keys(obj).map(function(key) { 190 | var val = obj[key]; 191 | return key + (val === true ? '' : '=' + JSON.stringify(val)); 192 | })); 193 | } 194 | writeln((prefix || 'Flags') + ': ' + (wordlist || '(none)'.cyan)); 195 | return log; 196 | }; 197 | 198 | // Create explicit "verbose" and "notverbose" functions, one for each already- 199 | // defined log function, that do the same thing but ONLY if -v or --verbose is 200 | // specified (or not specified). 201 | log.verbose = {}; 202 | log.notverbose = {}; 203 | 204 | // Iterate over all exported functions. 205 | Object.keys(log).filter(function(key) { 206 | return typeof log[key] === 'function'; 207 | }).forEach(function(key) { 208 | // Like any other log function, but suppresses output if the "verbose" option 209 | // IS NOT set. 210 | log.verbose[key] = function() { 211 | suppressOutput = !grunt.option('verbose'); 212 | log[key].apply(log, arguments); 213 | suppressOutput = false; 214 | return log.verbose; 215 | }; 216 | // Like any other log function, but suppresses output if the "verbose" option 217 | // IS set. 218 | log.notverbose[key] = function() { 219 | suppressOutput = grunt.option('verbose'); 220 | log[key].apply(log, arguments); 221 | suppressOutput = false; 222 | return log.notverbose; 223 | }; 224 | }); 225 | 226 | // A way to switch between verbose and notverbose modes. For example, this will 227 | // write 'foo' if verbose logging is enabled, otherwise write 'bar': 228 | // verbose.write('foo').or.write('bar'); 229 | log.verbose.or = log.notverbose; 230 | log.notverbose.or = log.verbose; 231 | 232 | // Static methods. 233 | 234 | // Pretty-format a word list. 235 | log.wordlist = function(arr, options) { 236 | options = grunt.util._.defaults(options || {}, { 237 | separator: ', ', 238 | color: 'cyan' 239 | }); 240 | return arr.map(function(item) { 241 | return options.color ? String(item)[options.color] : item; 242 | }).join(options.separator); 243 | }; 244 | 245 | // Return a string, uncolored (suitable for testing .length, etc). 246 | log.uncolor = function(str) { 247 | return str.replace(/\x1B\[\d+m/g, ''); 248 | }; 249 | 250 | // Word-wrap text to a given width, permitting ANSI color codes. 251 | log.wraptext = function(width, text) { 252 | // notes to self: 253 | // grab 1st character or ansi code from string 254 | // if ansi code, add to array and save for later, strip from front of string 255 | // if character, add to array and increment counter, strip from front of string 256 | // if width + 1 is reached and current character isn't space: 257 | // slice off everything after last space in array and prepend it to string 258 | // etc 259 | 260 | // This result array will be joined on \n. 261 | var result = []; 262 | var matches, color, tmp; 263 | var captured = []; 264 | var charlen = 0; 265 | 266 | while (matches = text.match(/(?:(\x1B\[\d+m)|\n|(.))([\s\S]*)/)) { 267 | // Updated text to be everything not matched. 268 | text = matches[3]; 269 | 270 | // Matched a color code? 271 | if (matches[1]) { 272 | // Save last captured color code for later use. 273 | color = matches[1]; 274 | // Capture color code. 275 | captured.push(matches[1]); 276 | continue; 277 | 278 | // Matched a non-newline character? 279 | } else if (matches[2]) { 280 | // If this is the first character and a previous color code was set, push 281 | // that onto the captured array first. 282 | if (charlen === 0 && color) { captured.push(color); } 283 | // Push the matched character. 284 | captured.push(matches[2]); 285 | // Increment the current charlen. 286 | charlen++; 287 | // If not yet at the width limit or a space was matched, continue. 288 | if (charlen <= width || matches[2] === ' ') { continue; } 289 | // The current charlen exceeds the width and a space wasn't matched. 290 | // "Roll everything back" until the last space character. 291 | tmp = captured.lastIndexOf(' '); 292 | text = captured.slice(tmp === -1 ? tmp : tmp + 1).join('') + text; 293 | captured = captured.slice(0, tmp); 294 | } 295 | 296 | // The limit has been reached. Push captured string onto result array. 297 | result.push(captured.join('')); 298 | 299 | // Reset captured array and charlen. 300 | captured = []; 301 | charlen = 0; 302 | } 303 | 304 | result.push(captured.join('')); 305 | return result.join('\n'); 306 | }; 307 | 308 | // todo: write unit tests 309 | // 310 | // function logs(text) { 311 | // [4, 6, 10, 15, 20, 25, 30, 40].forEach(function(n) { 312 | // log(n, text); 313 | // }); 314 | // } 315 | // 316 | // function log(n, text) { 317 | // console.log(Array(n + 1).join('-')); 318 | // console.log(wrap(n, text)); 319 | // } 320 | // 321 | // var text = 'this is '.red + 'a simple'.yellow.inverse + ' test of'.green + ' ' + 'some wrapped'.blue + ' text over '.inverse.magenta + 'many lines'.red; 322 | // logs(text); 323 | // 324 | // var text = 'foolish '.red.inverse + 'monkeys'.yellow + ' eating'.green + ' ' + 'delicious'.inverse.blue + ' bananas '.magenta + 'forever'.red; 325 | // logs(text); 326 | // 327 | // var text = 'foolish monkeys eating delicious bananas forever'.rainbow; 328 | // logs(text); 329 | 330 | // Format output into columns, wrapping words as-necessary. 331 | log.table = function(widths, texts) { 332 | var rows = []; 333 | widths.forEach(function(width, i) { 334 | var lines = log.wraptext(width, texts[i]).split('\n'); 335 | lines.forEach(function(line, j) { 336 | var row = rows[j]; 337 | if (!row) { row = rows[j] = []; } 338 | row[i] = line; 339 | }); 340 | }); 341 | 342 | var lines = []; 343 | rows.forEach(function(row) { 344 | var txt = ''; 345 | var column; 346 | for (var i = 0; i < row.length; i++) { 347 | column = row[i] || ''; 348 | txt += column; 349 | var diff = widths[i] - log.uncolor(column).length; 350 | if (diff > 0) { txt += grunt.util.repeat(diff, ' '); } 351 | } 352 | lines.push(txt); 353 | }); 354 | 355 | return lines.join('\n'); 356 | }; 357 | -------------------------------------------------------------------------------- /legacy/lib/file.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var grunt = { 13 | verbose: require('./log').verbose, 14 | util: require('grunt-legacy-util'), 15 | option: function () { return false } 16 | }; 17 | 18 | // Nodejs libs. 19 | var fs = require('fs'); 20 | var path = require('path'); 21 | 22 | // The module to be exported. 23 | var file = module.exports = {}; 24 | 25 | // External libs. 26 | file.glob = require('glob'); 27 | file.minimatch = require('minimatch'); 28 | 29 | var YAML = require('js-yaml'); 30 | var rimraf = require('rimraf'); 31 | var iconv = require('iconv-lite'); 32 | 33 | // Windows? 34 | var win32 = process.platform === 'win32'; 35 | 36 | // Normalize \\ paths to / paths. 37 | var unixifyPath = function(filepath) { 38 | if (win32) { 39 | return filepath.replace(/\\/g, '/'); 40 | } else { 41 | return filepath; 42 | } 43 | }; 44 | 45 | // Change the current base path (ie, CWD) to the specified path. 46 | file.setBase = function() { 47 | var dirpath = path.join.apply(path, arguments); 48 | process.chdir(dirpath); 49 | }; 50 | 51 | // Process specified wildcard glob patterns or filenames against a 52 | // callback, excluding and uniquing files in the result set. 53 | var processPatterns = function(patterns, fn) { 54 | // Filepaths to return. 55 | var result = []; 56 | // Iterate over flattened patterns array. 57 | grunt.util._.flatten(patterns).forEach(function(pattern) { 58 | // If the first character is ! it should be omitted 59 | var exclusion = pattern.indexOf('!') === 0; 60 | // If the pattern is an exclusion, remove the ! 61 | if (exclusion) { pattern = pattern.slice(1); } 62 | // Find all matching files for this pattern. 63 | var matches = fn(pattern); 64 | if (exclusion) { 65 | // If an exclusion, remove matching files. 66 | result = grunt.util._.difference(result, matches); 67 | } else { 68 | // Otherwise add matching files. 69 | result = grunt.util._.union(result, matches); 70 | } 71 | }); 72 | return result; 73 | }; 74 | 75 | // Match a filepath or filepaths against one or more wildcard patterns. Returns 76 | // all matching filepaths. 77 | file.match = function(options, patterns, filepaths) { 78 | if (grunt.util.kindOf(options) !== 'object') { 79 | filepaths = patterns; 80 | patterns = options; 81 | options = {}; 82 | } 83 | // Return empty set if either patterns or filepaths was omitted. 84 | if (patterns == null || filepaths == null) { return []; } 85 | // Normalize patterns and filepaths to arrays. 86 | if (!Array.isArray(patterns)) { patterns = [patterns]; } 87 | if (!Array.isArray(filepaths)) { filepaths = [filepaths]; } 88 | // Return empty set if there are no patterns or filepaths. 89 | if (patterns.length === 0 || filepaths.length === 0) { return []; } 90 | // Return all matching filepaths. 91 | return processPatterns(patterns, function(pattern) { 92 | return file.minimatch.match(filepaths, pattern, options); 93 | }); 94 | }; 95 | 96 | // Match a filepath or filepaths against one or more wildcard patterns. Returns 97 | // true if any of the patterns match. 98 | file.isMatch = function() { 99 | return file.match.apply(file, arguments).length > 0; 100 | }; 101 | 102 | // Return an array of all file paths that match the given wildcard patterns. 103 | file.expand = function() { 104 | var args = grunt.util.toArray(arguments); 105 | // If the first argument is an options object, save those options to pass 106 | // into the file.glob.sync method. 107 | var options = grunt.util.kindOf(args[0]) === 'object' ? args.shift() : {}; 108 | // Use the first argument if it's an Array, otherwise convert the arguments 109 | // object to an array and use that. 110 | var patterns = Array.isArray(args[0]) ? args[0] : args; 111 | // Return empty set if there are no patterns or filepaths. 112 | if (patterns.length === 0) { return []; } 113 | // Return all matching filepaths. 114 | var matches = processPatterns(patterns, function(pattern) { 115 | // Find all matching files for this pattern. 116 | return file.glob.sync(pattern, options); 117 | }); 118 | // Filter result set? 119 | if (options.filter) { 120 | matches = matches.filter(function(filepath) { 121 | filepath = path.join(options.cwd || '', filepath); 122 | try { 123 | if (typeof options.filter === 'function') { 124 | return options.filter(filepath); 125 | } else { 126 | // If the file is of the right type and exists, this should work. 127 | return fs.statSync(filepath)[options.filter](); 128 | } 129 | } catch(e) { 130 | // Otherwise, it's probably not the right type. 131 | return false; 132 | } 133 | }); 134 | } 135 | return matches; 136 | }; 137 | 138 | var pathSeparatorRe = /[\/\\]/g; 139 | 140 | // Build a multi task "files" object dynamically. 141 | file.expandMapping = function(patterns, destBase, options) { 142 | options = grunt.util._.defaults({}, options, { 143 | rename: function(destBase, destPath) { 144 | return path.join(destBase || '', destPath); 145 | } 146 | }); 147 | var files = []; 148 | var fileByDest = {}; 149 | // Find all files matching pattern, using passed-in options. 150 | file.expand(options, patterns).forEach(function(src) { 151 | var destPath = src; 152 | // Flatten? 153 | if (options.flatten) { 154 | destPath = path.basename(destPath); 155 | } 156 | // Change the extension? 157 | if (options.ext) { 158 | destPath = destPath.replace(/(\.[^\/]*)?$/, options.ext); 159 | } 160 | // Generate destination filename. 161 | var dest = options.rename(destBase, destPath, options); 162 | // Prepend cwd to src path if necessary. 163 | if (options.cwd) { src = path.join(options.cwd, src); } 164 | // Normalize filepaths to be unix-style. 165 | dest = dest.replace(pathSeparatorRe, '/'); 166 | src = src.replace(pathSeparatorRe, '/'); 167 | // Map correct src path to dest path. 168 | if (fileByDest[dest]) { 169 | // If dest already exists, push this src onto that dest's src array. 170 | fileByDest[dest].src.push(src); 171 | } else { 172 | // Otherwise create a new src-dest file mapping object. 173 | files.push({ 174 | src: [src], 175 | dest: dest, 176 | }); 177 | // And store a reference for later use. 178 | fileByDest[dest] = files[files.length - 1]; 179 | } 180 | }); 181 | return files; 182 | }; 183 | 184 | // Like mkdir -p. Create a directory and any intermediary directories. 185 | file.mkdir = function(dirpath, mode) { 186 | if (grunt.option('no-write')) { return; } 187 | // Set directory mode in a strict-mode-friendly way. 188 | if (mode == null) { 189 | mode = parseInt('0777', 8) & (~process.umask()); 190 | } 191 | dirpath.split(pathSeparatorRe).reduce(function(parts, part) { 192 | parts += part + '/'; 193 | var subpath = path.resolve(parts); 194 | if (!file.exists(subpath)) { 195 | try { 196 | fs.mkdirSync(subpath, mode); 197 | } catch(e) { 198 | throw grunt.util.error('Unable to create directory "' + subpath + '" (Error code: ' + e.code + ').', e); 199 | } 200 | } 201 | return parts; 202 | }, ''); 203 | }; 204 | 205 | // Recurse into a directory, executing callback for each file. 206 | file.recurse = function recurse(rootdir, callback, subdir) { 207 | var abspath = subdir ? path.join(rootdir, subdir) : rootdir; 208 | fs.readdirSync(abspath).forEach(function(filename) { 209 | var filepath = path.join(abspath, filename); 210 | if (fs.statSync(filepath).isDirectory()) { 211 | recurse(rootdir, callback, unixifyPath(path.join(subdir || '', filename || ''))); 212 | } else { 213 | callback(unixifyPath(filepath), rootdir, subdir, filename); 214 | } 215 | }); 216 | }; 217 | 218 | // The default file encoding to use. 219 | file.defaultEncoding = 'utf8'; 220 | // Whether to preserve the BOM on file.read rather than strip it. 221 | file.preserveBOM = false; 222 | 223 | // Read a file, return its contents. 224 | file.read = function(filepath, options) { 225 | if (!options) { options = {}; } 226 | var contents; 227 | grunt.verbose.write('Reading ' + filepath + '...'); 228 | try { 229 | contents = fs.readFileSync(String(filepath)); 230 | // If encoding is not explicitly null, convert from encoded buffer to a 231 | // string. If no encoding was specified, use the default. 232 | if (options.encoding !== null) { 233 | contents = iconv.decode(contents, options.encoding || file.defaultEncoding); 234 | // Strip any BOM that might exist. 235 | if (!file.preserveBOM && contents.charCodeAt(0) === 0xFEFF) { 236 | contents = contents.substring(1); 237 | } 238 | } 239 | grunt.verbose.ok(); 240 | return contents; 241 | } catch(e) { 242 | grunt.verbose.error(); 243 | throw grunt.util.error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').', e); 244 | } 245 | }; 246 | 247 | // Read a file, parse its contents, return an object. 248 | file.readJSON = function(filepath, options) { 249 | var src = file.read(filepath, options); 250 | var result; 251 | grunt.verbose.write('Parsing ' + filepath + '...'); 252 | try { 253 | result = JSON.parse(src); 254 | grunt.verbose.ok(); 255 | return result; 256 | } catch(e) { 257 | grunt.verbose.error(); 258 | throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.message + ').', e); 259 | } 260 | }; 261 | 262 | // Read a YAML file, parse its contents, return an object. 263 | file.readYAML = function(filepath, options) { 264 | var src = file.read(filepath, options); 265 | var result; 266 | grunt.verbose.write('Parsing ' + filepath + '...'); 267 | try { 268 | result = YAML.load(src); 269 | grunt.verbose.ok(); 270 | return result; 271 | } catch(e) { 272 | grunt.verbose.error(); 273 | throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.problem + ').', e); 274 | } 275 | }; 276 | 277 | // Write a file. 278 | file.write = function(filepath, contents, options) { 279 | if (!options) { options = {}; } 280 | var nowrite = grunt.option('no-write'); 281 | grunt.verbose.write((nowrite ? 'Not actually writing ' : 'Writing ') + filepath + '...'); 282 | // Create path, if necessary. 283 | file.mkdir(path.dirname(filepath)); 284 | try { 285 | // If contents is already a Buffer, don't try to encode it. If no encoding 286 | // was specified, use the default. 287 | if (!Buffer.isBuffer(contents)) { 288 | contents = iconv.encode(contents, options.encoding || file.defaultEncoding); 289 | } 290 | // Actually write file. 291 | if (!nowrite) { 292 | fs.writeFileSync(filepath, contents); 293 | } 294 | grunt.verbose.ok(); 295 | return true; 296 | } catch(e) { 297 | grunt.verbose.error(); 298 | throw grunt.util.error('Unable to write "' + filepath + '" file (Error code: ' + e.code + ').', e); 299 | } 300 | }; 301 | 302 | // Read a file, optionally processing its content, then write the output. 303 | file.copy = function(srcpath, destpath, options) { 304 | if (!options) { options = {}; } 305 | // If a process function was specified, and noProcess isn't true or doesn't 306 | // match the srcpath, process the file's source. 307 | var process = options.process && options.noProcess !== true && 308 | !(options.noProcess && file.isMatch(options.noProcess, srcpath)); 309 | // If the file will be processed, use the encoding as-specified. Otherwise, 310 | // use an encoding of null to force the file to be read/written as a Buffer. 311 | var readWriteOptions = process ? options : {encoding: null}; 312 | // Actually read the file. 313 | var contents = file.read(srcpath, readWriteOptions); 314 | if (process) { 315 | grunt.verbose.write('Processing source...'); 316 | try { 317 | contents = options.process(contents, srcpath); 318 | grunt.verbose.ok(); 319 | } catch(e) { 320 | grunt.verbose.error(); 321 | throw grunt.util.error('Error while processing "' + srcpath + '" file.', e); 322 | } 323 | } 324 | // Abort copy if the process function returns false. 325 | if (contents === false) { 326 | grunt.verbose.writeln('Write aborted.'); 327 | } else { 328 | file.write(destpath, contents, readWriteOptions); 329 | } 330 | }; 331 | 332 | // Delete folders and files recursively 333 | file.delete = function(filepath, options) { 334 | filepath = String(filepath); 335 | 336 | var nowrite = grunt.option('no-write'); 337 | if (!options) { 338 | options = {force: grunt.option('force') || false}; 339 | } 340 | 341 | grunt.verbose.write((nowrite ? 'Not actually deleting ' : 'Deleting ') + filepath + '...'); 342 | 343 | if (!file.exists(filepath)) { 344 | grunt.verbose.error(); 345 | grunt.log.warn('Cannot delete nonexistent file.'); 346 | return false; 347 | } 348 | 349 | // Only delete cwd or outside cwd if --force enabled. Be careful, people! 350 | if (!options.force) { 351 | if (file.isPathCwd(filepath)) { 352 | grunt.verbose.error(); 353 | grunt.fail.warn('Cannot delete the current working directory.'); 354 | return false; 355 | } else if (!file.isPathInCwd(filepath)) { 356 | grunt.verbose.error(); 357 | grunt.fail.warn('Cannot delete files outside the current working directory.'); 358 | return false; 359 | } 360 | } 361 | 362 | try { 363 | // Actually delete. Or not. 364 | if (!nowrite) { 365 | rimraf.sync(filepath); 366 | } 367 | grunt.verbose.ok(); 368 | return true; 369 | } catch(e) { 370 | grunt.verbose.error(); 371 | throw grunt.util.error('Unable to delete "' + filepath + '" file (' + e.message + ').', e); 372 | } 373 | }; 374 | 375 | // True if the file path exists. 376 | file.exists = function() { 377 | var filepath = path.join.apply(path, arguments); 378 | return fs.existsSync(filepath); 379 | }; 380 | 381 | // True if the file is a symbolic link. 382 | file.isLink = function() { 383 | var filepath = path.join.apply(path, arguments); 384 | return file.exists(filepath) && fs.lstatSync(filepath).isSymbolicLink(); 385 | }; 386 | 387 | // True if the path is a directory. 388 | file.isDir = function() { 389 | var filepath = path.join.apply(path, arguments); 390 | return file.exists(filepath) && fs.statSync(filepath).isDirectory(); 391 | }; 392 | 393 | // True if the path is a file. 394 | file.isFile = function() { 395 | var filepath = path.join.apply(path, arguments); 396 | return file.exists(filepath) && fs.statSync(filepath).isFile(); 397 | }; 398 | 399 | // Is a given file path absolute? 400 | file.isPathAbsolute = function() { 401 | var filepath = path.join.apply(path, arguments); 402 | return path.resolve(filepath) === filepath.replace(/[\/\\]+$/, ''); 403 | }; 404 | 405 | // Do all the specified paths refer to the same path? 406 | file.arePathsEquivalent = function(first) { 407 | first = path.resolve(first); 408 | for (var i = 1; i < arguments.length; i++) { 409 | if (first !== path.resolve(arguments[i])) { return false; } 410 | } 411 | return true; 412 | }; 413 | 414 | // Are descendant path(s) contained within ancestor path? Note: does not test 415 | // if paths actually exist. 416 | file.doesPathContain = function(ancestor) { 417 | ancestor = path.resolve(ancestor); 418 | var relative; 419 | for (var i = 1; i < arguments.length; i++) { 420 | relative = path.relative(path.resolve(arguments[i]), ancestor); 421 | if (relative === '' || /\w+/.test(relative)) { return false; } 422 | } 423 | return true; 424 | }; 425 | 426 | // Test to see if a filepath is the CWD. 427 | file.isPathCwd = function() { 428 | var filepath = path.join.apply(path, arguments); 429 | try { 430 | return file.arePathsEquivalent(process.cwd(), fs.realpathSync(filepath)); 431 | } catch(e) { 432 | return false; 433 | } 434 | }; 435 | 436 | // Test to see if a filepath is contained within the CWD. 437 | file.isPathInCwd = function() { 438 | var filepath = path.join.apply(path, arguments); 439 | try { 440 | return file.doesPathContain(process.cwd(), fs.realpathSync(filepath)); 441 | } catch(e) { 442 | return false; 443 | } 444 | }; 445 | --------------------------------------------------------------------------------