├── .editorconfig ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── package.json └── tasks ├── hub.js └── init ├── hub.js └── hub ├── Gruntfile.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | hub: { 4 | all: { 5 | src: ['../*/Gruntfile.js'], 6 | tasks: ['jshint'], 7 | }, 8 | }, 9 | jshint: { 10 | files: ['Gruntfile.js', 'tasks/**/*js'], 11 | options: { jshintrc: '.jshintrc' }, 12 | }, 13 | }); 14 | grunt.loadNpmTasks('grunt-contrib-jshint'); 15 | grunt.loadTasks('tasks'); 16 | grunt.registerTask('default', ['jshint']); 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Kyle Robinson Young 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-hub 2 | 3 | A Grunt task to watch and run tasks on multiple Grunt projects. 4 | 5 | ## Create a Grunt Hub 6 | 7 | A Grunt Hub is just a folder with a [Gruntfile][getting_started] and this 8 | grunt plugin installed. To create one do: 9 | 10 | ``` 11 | mkdir grunt-hub && cd grunt-hub 12 | npm install grunt-hub 13 | cp -R node_modules/grunt-hub/tasks/init/hub/* . 14 | ``` 15 | 16 | Then edit the Gruntfile file to point to your other Grunt projects and run: 17 | `grunt hub`. 18 | 19 | ### Integrate With an Existing Grunt Project 20 | 21 | Install this grunt plugin next to your project's 22 | [Gruntfile][getting_started] with: `npm install grunt-hub` 23 | 24 | Then add this line to your project's Gruntfile: 25 | 26 | ```javascript 27 | grunt.loadNpmTasks('grunt-hub'); 28 | ``` 29 | 30 | ## Watching Forever 31 | 32 | The common use for grunt-hub is for a development server. Where you would 33 | like to watch multiple projects and compile the SASS or concat/minify JS upon 34 | every project as you edit. 35 | 36 | Depending on your system, there are various ways to ensure the grunt-hub stays 37 | alive. Such as with 38 | [upstart and monit](http://howtonode.org/deploying-node-upstart-monit). 39 | 40 | A simple way is to use `nohup` and create a `start.sh` script: 41 | 42 | ```sh 43 | #!/bin/sh 44 | DIR=`dirname $0` 45 | /usr/bin/nohup /usr/local/bin/grunt --base $DIR hub --no-color & 46 | echo "Grunt Hub Started" 47 | ``` 48 | 49 | and a `stop.sh` script: 50 | 51 | ```sh 52 | #!/bin/sh 53 | ps -ef | sed -n '/grunt/{/grep/!p;}' | awk '{print$2}' | xargs -I kill {} 54 | echo "Grunt Hub Stopped" 55 | ``` 56 | 57 | Put these in your grunt-hub folder and run `./start.sh` to start and 58 | `./stop.sh` to stop. 59 | 60 | ### Using [forever](https://npmjs.org/package/forever) 61 | 62 | `forever` is a another great way to watch multiple grunt projects forever. 63 | 64 | * Install `npm install forever grunt grunt-cli grunt-hub --save-dev` 65 | * Add a start script to your `package.json`: 66 | 67 | ```json 68 | { 69 | "name": "my-grunt-hub", 70 | "version": "0.1.0", 71 | "scripts": { 72 | "start": "forever ./node_modules/.bin/grunt hub" 73 | } 74 | } 75 | ``` 76 | 77 | * Now you can start your hub with `npm start`. 78 | 79 | ## Configuring 80 | 81 | This plugin includes a `hub` task and overrides the `watch` task. 82 | 83 | ### `hub` task 84 | 85 | The hub task is for running tasks on multiple projects. It would like to know 86 | which Gruntfiles to use and which tasks to run on each Grunt project. For example 87 | if I would like to `lint` and `test` on every Grunt project one folder up: 88 | 89 | ```javascript 90 | grunt.initConfig({ 91 | hub: { 92 | all: { 93 | src: ['../*/Gruntfile.js'], 94 | tasks: ['jshint', 'nodeunit'], 95 | }, 96 | }, 97 | }); 98 | ``` 99 | 100 | If `tasks` were omitted, it will run the `default` tasks. 101 | 102 | You can override tasks on the cli with args: `grunt hub:all:watch` will run the `watch` task on all projects instead of `jshint, nodeunit`. 103 | 104 | #### options 105 | 106 | ##### `concurrent` 107 | Default: `3` 108 | 109 | Set to the number of concurrent task runs to spawn. 110 | 111 | ##### `allowSelf` 112 | Default: `false` 113 | 114 | By default, hub will skip its own Gruntfile. Set to `true` to allow hub to 115 | include itself. 116 | 117 | **Note:** Only set this for tasks which are not part of the `default` 118 | task of their respective Gruntfile, or an infinite loop will occur. 119 | 120 | ``` 121 | hub: { 122 | all: { 123 | options: { 124 | allowSelf: true 125 | }, 126 | src: ['./Gruntfile.js', '../client1/Gruntfile.js', '../client2/Gruntfile.js'], 127 | }, 128 | }, 129 | ``` 130 | 131 | ## Where did the `watch` task go? 132 | 133 | It isn't necessary. Just `npm install grunt-contrib-watch --save-dev` into your project folders. Then either add the `watch` task to your tasks list in your hub task config. Or run with `grunt hub:target:watch`. 134 | 135 | ## Contributing 136 | 137 | Please open an issue or send a pull request. Thanks! 138 | 139 | ## Release History 140 | 141 | * Please view the [commit history](https://github.com/shama/grunt-hub/commits/master) for future release history. 142 | * 0.7.0 Update async to ~0.9.0, warn when files not found and finish task when idle. Thanks @dylancwood! 143 | * 0.6.2 Fix syntax error. Thanks @eugeneiiim! 144 | * 0.6.1 Fix path.resolve must be strings for ownGruntfile. Thanks @terribleplan! 145 | * 0.6.0 Removed unneeded watch task. Fix issue with Gruntfiles not named Gruntfile. Removed deprecated grunt.util libs. Ability to override tasks via the cli. 146 | * 0.5.0 Run hub tasks in parallel. Add concurrent option to hub. Better error handling/printing. Thanks @plestik! 147 | * 0.4.0 Support for Grunt v0.4. 148 | * 0.3.6 Propagate exit codes. Thanks @wachunga! 149 | * 0.3.5 Update for latest grunt. Thanks @akinofftz! 150 | * 0.3.4 Allow watch task to be renamed. 151 | * 0.3.3 Fix issue with grunt-hub passing it's own tasks. Minor refactoring. 152 | * 0.3.2 Fix dep to `grunt-lib-contrib`. Include options in verbose output. Better spawn grunt in hub task. 153 | * 0.3.1 Update to gaze@0.2.0. Only spawn one at a time. Add `interrupt` option. Allow `tasks` to be undefined. Update to run on Grunt v0.4. 154 | * 0.3.0 Use [gaze](https://github.com/shama/gaze) for watching, Grunt v0.4 compatibility 155 | * 0.2.0 refactor: make easier to upgrade to Grunt v0.4, windows support, fix issue with mutliple watch targets 156 | * 0.1.1 add copyable template for a grunt hub 157 | * 0.1.0 initial release 158 | 159 | ## License 160 | 161 | Copyright (c) 2018 Kyle Robinson Young 162 | Licensed under the MIT license. 163 | 164 | 165 | [grunt]: https://github.com/gruntjs/grunt 166 | [getting_started]: http://gruntjs.com/getting-started 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-hub", 3 | "description": "A Grunt task to watch and run tasks on multiple Grunt projects", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/shama/grunt-hub", 6 | "author": { 7 | "name": "Kyle Robinson Young", 8 | "email": "kyle@dontkry.com", 9 | "url": "http://dontkry.com" 10 | }, 11 | "repository": "shama/grunt-hub", 12 | "bugs": { 13 | "url": "https://github.com/shama/grunt-hub/issues" 14 | }, 15 | "license": "MIT", 16 | "engines": { 17 | "node": ">= 0.8.0" 18 | }, 19 | "scripts": { 20 | "test": "grunt" 21 | }, 22 | "dependencies": { 23 | "chalk": "^2.4.1", 24 | "async": "^2.6.1", 25 | "lodash": "^4.17.5" 26 | }, 27 | "devDependencies": { 28 | "grunt-contrib-jshint": "^2.0.0", 29 | "grunt": "^1.0.3" 30 | }, 31 | "keywords": [ 32 | "gruntplugin", 33 | "watch" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tasks/hub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-hub 3 | * https://github.com/shama/grunt-hub 4 | * 5 | * Copyright (c) 2018 Kyle Robinson Young 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | module.exports = function(grunt) { 10 | 'use strict'; 11 | 12 | var path = require('path'); 13 | var chalk = require('chalk'); 14 | var async = require('async'); 15 | var _ = require('lodash'); 16 | 17 | grunt.registerMultiTask('hub', 'Run multiple grunt projects', function() { 18 | var options = this.options({ 19 | concurrent: 3, 20 | allowSelf: false 21 | }); 22 | var args = (this.args.length < 1) ? false : this.args; 23 | 24 | var done = this.async(); 25 | var errorCount = 0; 26 | // Get process.argv options without grunt.cli.tasks to pass to child processes 27 | var cliArgs = _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks)); 28 | // Get it's own gruntfile 29 | var ownGruntfile = grunt.option('gruntfile') || grunt.file.expand({filter: 'isFile'}, '{G,g}runtfile.{js,coffee}')[0]; 30 | ownGruntfile = path.resolve(process.cwd(), ownGruntfile || ''); 31 | 32 | var lastGruntFileWritten; 33 | function write(gruntfile, buf, isError) { 34 | if (gruntfile !== lastGruntFileWritten) { 35 | grunt.log.writeln(''); 36 | grunt.log.writeln(''); 37 | grunt.log.writeln(chalk.cyan('>> ') + gruntfile + ':\n'); 38 | } 39 | grunt.log[(isError) ? 'error' : 'write'](buf); 40 | lastGruntFileWritten = gruntfile; 41 | } 42 | 43 | // our queue for concurrently ran tasks 44 | var queue = async.queue(function(run, next) { 45 | var skipNext = false; 46 | grunt.log.ok('Running [' + run.tasks + '] on ' + run.gruntfile); 47 | if (cliArgs) { 48 | cliArgs = cliArgs.filter(function(currentValue) { 49 | if (skipNext) { 50 | return (skipNext = false); 51 | } 52 | var out = /^--gruntfile(=?)/.exec(currentValue); 53 | if (out) { 54 | if (out[1] !== '=') { 55 | skipNext = true; 56 | } 57 | return false; 58 | } 59 | return true; 60 | }); 61 | } 62 | var child = grunt.util.spawn({ 63 | // Use grunt to run the tasks 64 | grunt: true, 65 | // Run from dirname of gruntfile 66 | opts: {cwd: path.dirname(run.gruntfile)}, 67 | // Run task to be run and any cli options 68 | args: run.tasks.concat(cliArgs || [], '--gruntfile=' + run.gruntfile) 69 | }, function(err, res, code) { 70 | if (err) { errorCount++; } 71 | next(); 72 | }); 73 | child.stdout.on('data', function(buf) { 74 | write(run.gruntfile, buf); 75 | }); 76 | child.stderr.on('data', function(buf) { 77 | write(run.gruntfile, buf, true); 78 | }); 79 | }, options.concurrent); 80 | 81 | // When the queue is all done 82 | queue.drain = function() { 83 | done((errorCount === 0)); 84 | }; 85 | 86 | this.files.forEach(function(files) { 87 | var gruntfiles = grunt.file.expand({filter: 'isFile'}, files.src); 88 | // Display a warning if no files were matched 89 | if (!gruntfiles.length) { 90 | grunt.log.warn('No Gruntfiles matched the file patterns: "' + files.orig.src.join(', ') + '"'); 91 | } 92 | gruntfiles.forEach(function(gruntfile) { 93 | gruntfile = path.resolve(process.cwd(), gruntfile); 94 | 95 | // Skip it's own gruntfile. Prevents infinite loops. 96 | if (!options.allowSelf && gruntfile === ownGruntfile) { return; } 97 | 98 | queue.push({ 99 | gruntfile: gruntfile, 100 | tasks: args || files.tasks || ['default'] 101 | }); 102 | }); 103 | }); 104 | 105 | //After processing all files and queueing them, make sure that at least one file is queued 106 | if (queue.idle()) { 107 | // If the queue is idle, assume nothing was queued and call done() immediately after sending warning 108 | grunt.warn('No Gruntfiles matched any of the provided file patterns'); 109 | done(); 110 | } 111 | 112 | }); 113 | 114 | }; 115 | -------------------------------------------------------------------------------- /tasks/init/hub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-hub 3 | * https://github.com/shama/grunt-hub 4 | * 5 | * Copyright (c) 2014 Kyle Robinson Young 6 | * Licensed under the MIT license. 7 | */ 8 | /*jshint node:true*/ 9 | 10 | exports.description = 'Create a grunt hub.'; 11 | exports.notes = 'Just a placeholder grunt template.'; 12 | exports.warnOn = '*'; 13 | exports.template = function(grunt, init, done) { 14 | done(); 15 | }; 16 | -------------------------------------------------------------------------------- /tasks/init/hub/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example Grunt Hub 3 | * 4 | * Edit the hub.all.src to point to your Gruntfile locations. 5 | * Then run `grunt`. 6 | */ 7 | module.exports = function(grunt) { 8 | 'use strict'; 9 | 10 | grunt.initConfig({ 11 | hub: { 12 | all: { 13 | src: ['../*/Gruntfile.js'], 14 | tasks: ['jshint'], 15 | }, 16 | }, 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-hub'); 20 | 21 | grunt.registerTask('default', ['hub']); 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/init/hub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-grunt-hub", 3 | "version": "1.0.0", 4 | "description": "A grunt hub", 5 | "repository": "user/repo", 6 | "devDependencies": { 7 | "grunt": "^1.0.3" 8 | } 9 | } 10 | --------------------------------------------------------------------------------