├── .babelrc ├── index.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── test-helpers └── chai-extensions.js ├── lib ├── cli.js └── run.js ├── test ├── cli_test.js └── run_test.js ├── package.json ├── README.md ├── .eslintrc └── CONTRIBUTING.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./dist/cli') 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log* 3 | *.DS_Store 4 | dist/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | test-helpers/ 3 | .babelrc 4 | .eslintrc 5 | lib/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run lint 6 | - npm test 7 | -------------------------------------------------------------------------------- /test-helpers/chai-extensions.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import sinonChai from 'sinon-chai' 3 | 4 | chai.use(sinonChai) 5 | 6 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | import meow from 'meow' 2 | import { runTasks } from './run' 3 | 4 | const cli = meow(` 5 | Usage 6 | $ npm-run-parallel 7 | 8 | Examples 9 | $ npm-run-parallel task:a task:b 10 | `) 11 | 12 | runTasks(cli.input) 13 | 14 | -------------------------------------------------------------------------------- /test/cli_test.js: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import pkg from '../package.json' 3 | import { expect } from 'chai' 4 | 5 | describe('lib/cli', () => { 6 | const tasks = [ 7 | '__test_fixture__', 8 | '__test_fixture_2__' 9 | ] 10 | let stdout 11 | 12 | before(() => { 13 | return execa('node', [pkg.bin, ...tasks]) 14 | .then(output => { 15 | stdout = output.stdout 16 | }) 17 | }) 18 | 19 | it('launches the task runner', () => { 20 | tasks.forEach(task => { 21 | expect(stdout).to.contain(`${pkg.name}@${pkg.version} ${task}`) 22 | }) 23 | }) 24 | 25 | }) 26 | -------------------------------------------------------------------------------- /test/run_test.js: -------------------------------------------------------------------------------- 1 | import { runTasks } from '../lib/run' 2 | import childProcess from 'child_process' 3 | import { expect } from 'chai' 4 | import { stub } from 'sinon' 5 | 6 | const mockStream = { pause: stub(), addListener: stub(), resume: stub() } 7 | 8 | describe('lib/run', () => { 9 | 10 | describe('runTasks', () => { 11 | const tasks = [ 12 | '__test_fixture__', 13 | '__test_fixture_2__' 14 | ] 15 | 16 | before(() => { 17 | stub(childProcess, 'exec').returns({ 18 | stdout: mockStream, 19 | stderr: mockStream, 20 | kill: stub() 21 | }) 22 | 23 | runTasks(tasks) 24 | }) 25 | 26 | after(() => { 27 | childProcess.exec.restore() 28 | }) 29 | 30 | it('executes an `npm run` for each task', () => { 31 | tasks.forEach(task => { 32 | expect(childProcess.exec).to.have.been.calledWith(`npm run ${task}`) 33 | }) 34 | }) 35 | 36 | }) 37 | 38 | }) 39 | 40 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { Observable } from 'rx' 4 | import RxNode from 'rx-node' 5 | import { exec } from 'child_process' 6 | 7 | const addExitOnInterrupt = taskProcess => process.on('SIGINT', taskProcess.kill) 8 | 9 | const executeTask = task => { 10 | const taskProcess = exec(`npm run ${task}`) 11 | 12 | addExitOnInterrupt(taskProcess) 13 | 14 | return taskProcess 15 | } 16 | 17 | const createOutputStreamsFromProcess = ({ stdout, stderr }) => { 18 | const stdoutStream = RxNode.fromReadableStream(stdout) 19 | 20 | const stderrStream = RxNode.fromReadableStream(stderr) 21 | .select(e => process.stderr.write(e) && process.exit(1)) 22 | 23 | return stdoutStream.merge(stderrStream) 24 | } 25 | 26 | const runTask = task => { 27 | const taskProcess = executeTask(task) 28 | return createOutputStreamsFromProcess(taskProcess) 29 | } 30 | 31 | export const runTasks = tasks => { 32 | Observable.from(tasks) 33 | .selectMany(runTask) 34 | .subscribe( 35 | output => process.stdout.write(output) 36 | ) 37 | } 38 | 39 | /* eslint-enable no-console */ 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-run-parallel", 3 | "version": "0.6.0", 4 | "description": "Run npm tasks in parallel and exit when they are all done.", 5 | "bin": "index.js", 6 | "scripts": { 7 | "build": "babel $npm_package_config_source_dir -d $npm_package_config_output_dir", 8 | "lint": "npm run lint:eslint", 9 | "lint:fix": "npm run lint:eslint:fix", 10 | "lint:eslint": "eslint $npm_package_config_source_dir", 11 | "lint:eslint:fix": "npm run lint:eslint -- --fix", 12 | "prepare": "npm run build", 13 | "start": "node $npm_package_main", 14 | "test": "mocha --compilers js:babel-register $npm_package_config_tests --require $npm_package_config_test_helpers", 15 | "test:watch": "npm test -- --watch", 16 | "watch": "npm run build -- --watch", 17 | "__test_fixture__": "echo", 18 | "__test_fixture_2__": "echo" 19 | }, 20 | "config": { 21 | "source_dir": "lib", 22 | "tests": "test/*.js", 23 | "test_helpers": "test-helpers/*.js", 24 | "output_dir": "dist" 25 | }, 26 | "keywords": [ 27 | "npm", 28 | "run", 29 | "script", 30 | "scripts", 31 | "task", 32 | "tasks", 33 | "parallel", 34 | "stream", 35 | "concurrent" 36 | ], 37 | "author": "Ian McNally ", 38 | "license": "ISC", 39 | "dependencies": { 40 | "meow": "^3.7.0", 41 | "rx": "^4.0.8", 42 | "rx-node": "^1.0.1" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.5.1", 46 | "babel-preset-es2015": "^6.5.0", 47 | "babel-register": "^6.5.2", 48 | "chai": "^3.5.0", 49 | "eslint": "^2.2.0", 50 | "execa": "^0.2.2", 51 | "mocha": "^2.4.5", 52 | "sinon": "^1.17.3", 53 | "sinon-chai": "^2.8.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Run npm tasks in parallel and exit when they are all done. 2 | 3 | ![Build Status](https://travis-ci.org/ianmcnally/npm-run-parallel.svg?branch=master) 4 | 5 | ## Installation 6 | 7 | Locally (recommended, especially with npm scripts): 8 | 9 | `npm install npm-run-parallel` 10 | 11 | Globally, as a CLI: 12 | 13 | `npm install -g npm-run-parallel` 14 | 15 | ## Usage 16 | 17 | From the command line, or an npm script: 18 | 19 | `npm-run-parallel ` 20 | 21 | So, if I want to run `npm install` and `npm test` in parallel (a contrived example, I know!): 22 | 23 | `npm-run-parallel install test` 24 | 25 | The process will complete when both `npm install` and `npm test` finish. 26 | 27 | _Note: you can run custom npm scripts, too. Just like you would `install` or `test` above._ 28 | 29 | ## Why? 30 | 31 | On a given project, there are likely some long or expensive npm scripts, like asset compilation or multi-directory dependency installs. It's faster to do these in parallel. 32 | 33 | A common way to run any shell command (which an npm script is) is the `&` operator, which creates a subprocess. This subprocess runs separately, so the original process can never tell how or when the subprocess ended. In a similar vein, the `|` operator can pipe the output of one command to another. This can feel like _streaming_, but passing the output of one npm script to another is both a hack and prone to erroring out. 34 | 35 | Some build tools, like gulp, accomplish parallel commands (a.k.a. tasks) with streaming libraries. The recent trend towards npm scripts from these walled-garden tools necessitated emulating that functionality. So `npm-run-parallel` all the tasks! 36 | 37 | #### Do I get colored terminal output from my tasks? 38 | 39 | Yes! It's awesome. I learned a lot about streams and stdout in the process of making this. 40 | 41 | ## Contributing 42 | 43 | See [CONTRIBUTING.md](https://github.com/ianmcnally/npm-run-parallel/blob/master/CONTRIBUTING.md) for the code of conduct. 44 | 45 | ### Local development 46 | 47 | - `npm run watch` - Compile source code, and watch for changes 48 | 49 | - `npm run lint` - Lints source code (eslint) 50 | 51 | - `npm run test:watch` - Run tests and watch for file changes 52 | 53 | #### Other commands 54 | 55 | - `npm run build` - Compiles source code (also used in `prepublish`) 56 | 57 | - `npm run test` - Run unit tests once 58 | 59 | - `npm run start --` - Emulates the cli interface (_note: pass it task arguments. Useful for manual testing._) 60 | 61 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 2 5 | ], 6 | "quotes": [ 7 | 2, 8 | "single" 9 | ], 10 | "linebreak-style": [ 11 | 2, 12 | "unix" 13 | ], 14 | "semi": [ 15 | 2, 16 | "never" 17 | ], 18 | "func-style": [2, "expression"], 19 | "new-parens": 2, 20 | "no-array-constructor": 2, 21 | "no-mixed-spaces-and-tabs": 2, 22 | "no-multiple-empty-lines": 2, 23 | "no-var": 2, 24 | "prefer-template": 2, 25 | "constructor-super": 2, 26 | "no-this-before-super": 2, 27 | "object-shorthand": 2, 28 | "prefer-arrow-callback": 2, 29 | "prefer-const": 2, 30 | "prefer-spread": 2, 31 | "prefer-reflect": 2, 32 | "no-console": 2, 33 | "no-constant-condition": 2, 34 | "no-debugger": 2, 35 | "no-dupe-args": 2, 36 | "no-dupe-keys": 2, 37 | "no-duplicate-case": 2, 38 | "no-empty-character-class": 2, 39 | "no-empty": 2, 40 | "no-ex-assign": 2, 41 | "no-extra-boolean-cast": 2, 42 | "no-extra-semi": 2, 43 | "no-func-assign": 2, 44 | "no-inner-declarations": 2, 45 | "no-invalid-regexp": 2, 46 | "no-irregular-whitespace": 2, 47 | "no-negated-in-lhs": 2, 48 | "no-obj-calls": 2, 49 | "no-regex-spaces": 2, 50 | "no-sparse-arrays": 2, 51 | "no-unreachable": 2, 52 | "use-isnan": 2, 53 | "valid-typeof": 2, 54 | "dot-notation": 2, 55 | "eqeqeq": 2, 56 | "no-alert": 2, 57 | "no-caller": 2, 58 | "no-else-return": 2, 59 | "no-eval": 2, 60 | "no-extend-native": 2, 61 | "no-extra-bind": 2, 62 | "no-fallthrough": 2, 63 | "no-floating-decimal": 2, 64 | "no-implicit-coercion": 2, 65 | "no-implied-eval": 2, 66 | "no-invalid-this": 2, 67 | "no-iterator": 2, 68 | "no-labels": 2, 69 | "no-lone-blocks": 2, 70 | "no-loop-func": 2, 71 | "no-multi-spaces": 2, 72 | "no-native-reassign": 2, 73 | "no-new-wrappers": 2, 74 | "no-param-reassign": 2, 75 | "no-proto": 2, 76 | "no-redeclare": 2, 77 | "no-return-assign": 2, 78 | "no-script-url": 2, 79 | "no-self-compare": 2, 80 | "no-sequences": 2, 81 | "no-useless-call": 2, 82 | "no-void": 2, 83 | "no-warning-comments": 2, 84 | "no-with": 2, 85 | "vars-on-top": 2, 86 | "wrap-iife": 2, 87 | "yoda": [2, "never"], 88 | "no-undef": 2, 89 | "no-delete-var": 2, 90 | "no-unused-vars": 2, 91 | "arrow-parens": [2, "as-needed"], 92 | "arrow-spacing": 2, 93 | "object-curly-spacing": [2, "always"] 94 | }, 95 | "env": { 96 | "es6": true, 97 | "node": true, 98 | "mocha": true 99 | }, 100 | "globals": { 101 | "chai": true, 102 | "expect": true, 103 | "sinon": true 104 | }, 105 | "extends": "eslint:recommended", 106 | "ecmaFeatures": { 107 | "experimentalObjectRestSpread": true, 108 | "blockBindings": true 109 | }, 110 | "parserOptions": { 111 | "sourceType": "module" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | --------------------------------------------------------------------------------