├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── gruntfile.js ├── license ├── package.json ├── readme.md ├── screenshot.png ├── tasks └── concurrent.js └── test ├── fixtures └── server.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 14 14 | - 12 15 | - 10 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | test/tmp 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const supportsColor = require('supports-color'); 3 | 4 | module.exports = grunt => { 5 | grunt.initConfig({ 6 | concurrent: { 7 | test: [ 8 | 'test1', 9 | 'test2', 10 | 'test3' 11 | ], 12 | testSequence: [ 13 | 'test4', [ 14 | 'test5', 15 | 'test6' 16 | ] 17 | ], 18 | testargs: [ 19 | 'testargs1', 20 | 'testargs2' 21 | ], 22 | log: { 23 | options: { 24 | logConcurrentOutput: true 25 | }, 26 | tasks: [ 27 | 'nodemon', 28 | 'watch' 29 | ] 30 | }, 31 | colors: [ 32 | 'colorcheck' 33 | ], 34 | indentTrue: { 35 | options: { 36 | indent: true 37 | }, 38 | tasks: [ 39 | 'testIndent' 40 | ] 41 | }, 42 | indentFalse: { 43 | options: { 44 | indent: false 45 | }, 46 | tasks: [ 47 | 'testIndent' 48 | ] 49 | }, 50 | indentFalseConcurrentOutput: { 51 | options: { 52 | logConcurrentOutput: true, 53 | indent: false 54 | }, 55 | tasks: [ 56 | 'testIndent' 57 | ] 58 | }, 59 | indentDefault: [ 60 | 'testIndent' 61 | ] 62 | }, 63 | simplemocha: { 64 | test: { 65 | src: 'test/*.js', 66 | options: { 67 | timeout: 6000 68 | } 69 | } 70 | }, 71 | clean: { 72 | test: [ 73 | 'test/tmp' 74 | ] 75 | }, 76 | watch: { 77 | scripts: { 78 | files: [ 79 | 'tasks/*.js' 80 | ], 81 | tasks: [ 82 | 'default' 83 | ] 84 | } 85 | }, 86 | nodemon: { 87 | dev: { 88 | options: { 89 | file: 'test/fixtures/server.js' 90 | } 91 | } 92 | } 93 | }); 94 | 95 | grunt.loadTasks('tasks'); 96 | grunt.loadNpmTasks('grunt-contrib-clean'); 97 | grunt.loadNpmTasks('grunt-contrib-watch'); 98 | grunt.loadNpmTasks('grunt-simple-mocha'); 99 | grunt.loadNpmTasks('grunt-nodemon'); 100 | 101 | grunt.registerTask('test1', () => { 102 | console.log('test1'); 103 | grunt.file.write('test/tmp/1'); 104 | }); 105 | 106 | grunt.registerTask('test2', function () { 107 | const cb = this.async(); 108 | setTimeout(() => { 109 | console.log('test2'); 110 | grunt.file.write('test/tmp/2'); 111 | cb(); 112 | }, 1000); 113 | }); 114 | 115 | grunt.registerTask('test3', () => { 116 | console.log('test3'); 117 | grunt.file.write('test/tmp/3'); 118 | }); 119 | 120 | grunt.registerTask('test4', () => { 121 | console.log('test4'); 122 | grunt.file.write('test/tmp/4'); 123 | }); 124 | 125 | grunt.registerTask('test5', () => { 126 | console.log('test5'); 127 | grunt.file.write('test/tmp/5'); 128 | sleep(1000); 129 | }); 130 | 131 | grunt.registerTask('test6', () => { 132 | console.log('test6'); 133 | grunt.file.write('test/tmp/6'); 134 | }); 135 | 136 | grunt.registerTask('testargs1', () => { 137 | const args = grunt.option.flags().join(); 138 | grunt.file.write('test/tmp/args1', args); 139 | }); 140 | 141 | grunt.registerTask('testargs2', () => { 142 | const args = grunt.option.flags().join(); 143 | grunt.file.write('test/tmp/args2', args); 144 | }); 145 | 146 | grunt.registerTask('colorcheck', () => { 147 | // Writes 'true' or 'false' to the file 148 | const supports = String(Boolean(supportsColor.stdout)); 149 | grunt.file.write('test/tmp/colors', supports); 150 | }); 151 | 152 | grunt.registerTask('testIndent', () => { 153 | console.log('indent test output'); 154 | }); 155 | 156 | grunt.registerTask('default', [ 157 | 'clean', 158 | 'concurrent:test', 159 | 'concurrent:testSequence', 160 | 'simplemocha', 161 | 'clean' 162 | ]); 163 | }; 164 | 165 | function sleep(milliseconds) { 166 | const start = new Date().getTime(); 167 | for (let i = 0; i < 1e7; i++) { 168 | if ((new Date().getTime() - start) > milliseconds) { 169 | break; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-concurrent", 3 | "version": "3.0.0", 4 | "description": "Run grunt tasks concurrently", 5 | "license": "MIT", 6 | "repository": "sindresorhus/grunt-concurrent", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "engines": { 14 | "node": ">=8" 15 | }, 16 | "scripts": { 17 | "test": "xo && grunt" 18 | }, 19 | "files": [ 20 | "tasks" 21 | ], 22 | "keywords": [ 23 | "gruntplugin", 24 | "concurrent", 25 | "parallel", 26 | "simultaneous", 27 | "optimize", 28 | "speed", 29 | "perf", 30 | "performance", 31 | "fast", 32 | "faster" 33 | ], 34 | "dependencies": { 35 | "arrify": "^2.0.1", 36 | "async": "^3.1.0", 37 | "indent-string": "^4.0.0", 38 | "pad-stream": "^2.0.0" 39 | }, 40 | "devDependencies": { 41 | "cross-spawn": "^6.0.5", 42 | "grunt": "^1.0.4", 43 | "grunt-cli": "^1.3.2", 44 | "grunt-contrib-clean": "^2.0.0", 45 | "grunt-contrib-watch": "^1.1.0", 46 | "grunt-nodemon": "^0.4.0", 47 | "grunt-simple-mocha": "^0.4.0", 48 | "nodemon": "^1.2.1", 49 | "path-exists": "^4.0.0", 50 | "supports-color": "^7.0.0", 51 | "xo": "^0.24.0" 52 | }, 53 | "peerDependencies": { 54 | "grunt": ">=1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # grunt-concurrent 2 | 3 | > Run grunt tasks concurrently 4 | 5 | 6 | 7 | Running slow tasks like Coffee and Sass concurrently can potentially improve your build time significantly. This task is also useful if you need to run [multiple blocking tasks](#logconcurrentoutput) like `nodemon` and `watch` at once. 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install --save-dev grunt-concurrent 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | require('load-grunt-tasks')(grunt); 19 | 20 | grunt.initConfig({ 21 | concurrent: { 22 | target1: ['coffee', 'sass'], 23 | target2: ['jshint', 'mocha'] 24 | } 25 | }); 26 | 27 | // Tasks of target1 run concurrently, after they all finished, tasks of target2 run concurrently, instead of target1 and target2 running concurrently. 28 | grunt.registerTask('default', ['concurrent:target1', 'concurrent:target2']); 29 | ``` 30 | 31 | ## Sequential tasks in concurrent target 32 | 33 | ```js 34 | grunt.initConfig({ 35 | concurrent: { 36 | target: [['jshint', 'coffee'], 'sass'] 37 | } 38 | }); 39 | ``` 40 | 41 | Now `jshint` will always be done before `coffee` and `sass` runs independent of both of them. 42 | 43 | 44 | ## Options 45 | 46 | ### limit 47 | 48 | Type: `number`\ 49 | Default: Twice the number of CPU cores with a minimum of 2 50 | 51 | Limit how many tasks that are run concurrently. 52 | 53 | ### logConcurrentOutput 54 | 55 | Type: `boolean`\ 56 | Default: `false` 57 | 58 | You can optionally log the output of your concurrent tasks by specifying the `logConcurrentOutput` option. Here is an example config which runs [grunt-nodemon](https://github.com/ChrisWren/grunt-nodemon) to launch and monitor a node server and [grunt-contrib-watch](https://github.com/gruntjs/grunt-contrib-watch) to watch for asset changes all in one terminal tab: 59 | 60 | ```js 61 | grunt.initConfig({ 62 | concurrent: { 63 | target: { 64 | tasks: ['nodemon', 'watch'], 65 | options: { 66 | logConcurrentOutput: true 67 | } 68 | } 69 | } 70 | }); 71 | 72 | grunt.loadNpmTasks('grunt-concurrent'); 73 | grunt.registerTask('default', ['concurrent:target']); 74 | ``` 75 | 76 | *The output will be messy when combining certain tasks. This option is best used with tasks that don't exit like `watch` and `nodemon` to monitor the output of long-running concurrent tasks.* 77 | 78 | ### indent 79 | 80 | Type: `boolean`\ 81 | Default: `true` 82 | 83 | You can optionally skip indenting the log output of your concurrent tasks by specifying `false`. This can be useful for running tasks in parallel for a stdout parser which expects no indentation, for example, TeamCity tests. 84 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/grunt-concurrent/ef3be7613a5b7463611a018fcf905448fe53294a/screenshot.png -------------------------------------------------------------------------------- /tasks/concurrent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const os = require('os'); 3 | const padStream = require('pad-stream'); 4 | const async = require('async'); 5 | const arrify = require('arrify'); 6 | const indentString = require('indent-string'); 7 | 8 | const subprocesses = []; 9 | 10 | module.exports = grunt => { 11 | grunt.registerMultiTask('concurrent', 'Run grunt tasks concurrently', function () { 12 | const done = this.async(); 13 | 14 | const options = this.options({ 15 | limit: Math.max((os.cpus().length || 1) * 2, 2), 16 | indent: true 17 | }); 18 | 19 | const tasks = this.data.tasks || this.data; 20 | const flags = grunt.option.flags(); 21 | 22 | if ( 23 | !flags.includes('--no-color') && 24 | !flags.includes('--no-colors') && 25 | !flags.includes('--color=false') 26 | ) { 27 | // Append the flag so that support-colors won't return false 28 | // See issue #70 for details 29 | flags.push('--color'); 30 | } 31 | 32 | if (options.limit < tasks.length) { 33 | grunt.log.oklns( 34 | 'Warning: There are more tasks than your concurrency limit. After this limit is reached no further tasks will be run until the current tasks are completed. You can adjust the limit in the concurrent task options' 35 | ); 36 | } 37 | 38 | async.eachLimit(tasks, options.limit, (task, next) => { 39 | const subprocess = grunt.util.spawn({ 40 | grunt: true, 41 | args: arrify(task).concat(flags), 42 | opts: { 43 | stdio: [ 44 | 'ignore', 45 | 'pipe', 46 | 'pipe' 47 | ] 48 | } 49 | }, (error, result) => { 50 | if (!options.logConcurrentOutput) { 51 | let output = result.stdout + result.stderr; 52 | if (options.indent) { 53 | output = indentString(output, 4); 54 | } 55 | 56 | grunt.log.writeln(`\n${output}`); 57 | } 58 | 59 | next(error); 60 | }); 61 | 62 | if (options.logConcurrentOutput) { 63 | let subStdout = subprocess.stdout; 64 | let subStderr = subprocess.stderr; 65 | if (options.indent) { 66 | subStdout = subStdout.pipe(padStream(4)); 67 | subStderr = subStderr.pipe(padStream(4)); 68 | } 69 | 70 | subStdout.pipe(process.stdout); 71 | subStderr.pipe(process.stderr); 72 | } 73 | 74 | subprocesses.push(subprocess); 75 | }, error => { 76 | if (error) { 77 | grunt.warn(error); 78 | } 79 | 80 | done(); 81 | }); 82 | }); 83 | }; 84 | 85 | function cleanup() { 86 | for (const subprocess of subprocesses) { 87 | subprocess.kill('SIGKILL'); 88 | } 89 | } 90 | 91 | // Make sure all subprocesses are killed when grunt exits 92 | process.on('exit', cleanup); 93 | process.on('SIGINT', () => { 94 | cleanup(); 95 | process.exit(); 96 | }); 97 | -------------------------------------------------------------------------------- /test/fixtures/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const net = require('net'); 3 | 4 | const server = net.createServer(socket => { 5 | socket.write('Hello world').pipe(socket); 6 | }); 7 | 8 | server.listen(0, '127.0.0.1'); 9 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env mocha */ 3 | const {strict: assert} = require('assert'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const {exec} = require('child_process'); 7 | const pathExists = require('path-exists'); 8 | const spawn = require('cross-spawn'); 9 | 10 | describe('concurrent', () => { 11 | it('runs grunt tasks successfully', () => { 12 | assert(pathExists.sync(path.join(__dirname, 'tmp/1'))); 13 | assert(pathExists.sync(path.join(__dirname, 'tmp/2'))); 14 | assert(pathExists.sync(path.join(__dirname, 'tmp/3'))); 15 | }); 16 | 17 | it('runs grunt task sequence successfully', () => { 18 | const file5 = fs.statSync(path.join(__dirname, 'tmp/5')); 19 | const file6 = fs.statSync(path.join(__dirname, 'tmp/6')); 20 | assert.ok(Date.parse(file5.ctime) < Date.parse(file6.ctime)); 21 | }); 22 | 23 | it('forwards CLI args to grunt sub-processes', done => { 24 | const expected = '--arg1=test,--arg2'; 25 | 26 | exec('grunt concurrent:testargs ' + expected, () => { 27 | const args1 = fs.readFileSync(path.join(__dirname, 'tmp/args1'), 'utf8'); 28 | const args2 = fs.readFileSync(path.join(__dirname, 'tmp/args2'), 'utf8'); 29 | assert.ok(args1.includes(expected)); 30 | assert.ok(args2.includes(expected)); 31 | done(); 32 | }); 33 | }); 34 | 35 | describe('`logConcurrentOutput` option', () => { 36 | let logOutput = ''; 37 | 38 | before(done => { 39 | let isDoneCalled = false; 40 | const subprocess = spawn('grunt', ['concurrent:log']); 41 | 42 | subprocess.stdout.setEncoding('utf8'); 43 | subprocess.stdout.on('data', data => { 44 | logOutput += data; 45 | subprocess.kill(); 46 | if (!isDoneCalled) { 47 | isDoneCalled = true; 48 | done(); 49 | } 50 | }); 51 | }); 52 | 53 | it('outputs concurrent logging', () => { 54 | const expected = 'Running "concurrent:log" (concurrent) task'; 55 | assert(logOutput.includes(expected)); 56 | }); 57 | }); 58 | 59 | describe('works with supports-color lib', () => { 60 | it('ensures that colors are supported by default', done => { 61 | exec('grunt concurrent:colors', () => { 62 | assert.equal(fs.readFileSync(path.join(__dirname, 'tmp/colors'), 'utf8'), 'true'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('doesn\'t support colors with --no-color option', done => { 68 | exec('grunt concurrent:colors --no-color', () => { 69 | assert.equal(fs.readFileSync(path.join(__dirname, 'tmp/colors'), 'utf8'), 'false'); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('`indent` option', () => { 76 | const testOutput = 'indent test output'; 77 | const indentedTestOutput = ' ' + testOutput; 78 | 79 | it('indents output when true', done => { 80 | exec('grunt concurrent:indentTrue', (error, stdout) => { 81 | assert.ok(stdout.split('\n').includes(indentedTestOutput)); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('does not indent output when false', done => { 87 | exec('grunt concurrent:indentFalse', (error, stdout) => { 88 | assert.ok(stdout.split('\n').includes(testOutput)); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('does not indent output when false and logConcurrentOutput is true', done => { 94 | exec('grunt concurrent:indentFalseConcurrentOutput', (error, stdout) => { 95 | assert.ok(stdout.split('\n').includes(testOutput)); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('indents output by default', done => { 101 | exec('grunt concurrent:indentDefault', (error, stdout) => { 102 | assert.ok(stdout.split('\n').includes(indentedTestOutput)); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | }); 108 | --------------------------------------------------------------------------------