Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: open-cli-tools/concurrently
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 943219cd4ebbd9773fe5c883cd56a4893a0d1973
Choose a base ref
...
head repository: open-cli-tools/concurrently
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1220aeb66c99ee65ba464c789dbe0f6055afaf07
Choose a head ref

Commits on Sep 7, 2020

  1. build(deps): bump yargs-parser from 13.1.1 to 13.1.2 (#243)

    Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 13.1.1 to 13.1.2.
    - [Release notes](https://github.com/yargs/yargs-parser/releases)
    - [Changelog](https://github.com/yargs/yargs-parser/blob/master/docs/CHANGELOG-full.md)
    - [Commits](https://github.com/yargs/yargs-parser/commits)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 7, 2020
    Copy the full SHA
    7f1bd27 View commit details

Commits on Sep 21, 2020

  1. Copy the full SHA
    81aa338 View commit details
  2. Copy the full SHA
    7700363 View commit details
  3. Copy the full SHA
    b62a2a1 View commit details

Commits on Jan 7, 2021

  1. Copy the full SHA
    e17f693 View commit details
  2. Copy the full SHA
    2e1ad01 View commit details

Commits on Feb 20, 2021

  1. Support for hex colors in prefixColor (#261)

    Closes #260
    Ramakrishna authored Feb 20, 2021
    Copy the full SHA
    6fcbf19 View commit details
  2. Add cwd option to programmatic API (#246)

    Closes #216
    thdk authored Feb 20, 2021
    Copy the full SHA
    986bc3d View commit details
  3. 6.0.0

    gustavohenke committed Feb 20, 2021
    Copy the full SHA
    343d1ab View commit details

Commits on Apr 5, 2021

  1. Copy the full SHA
    9fa0544 View commit details
  2. 6.0.1

    gustavohenke committed Apr 5, 2021
    Copy the full SHA
    e36e8c1 View commit details

Commits on Apr 12, 2021

  1. Copy the full SHA
    7710c21 View commit details
  2. Fix input handler when input contains a colon (#269)

    Patrick Sachs authored Apr 12, 2021
    Copy the full SHA
    330cf92 View commit details
  3. 6.0.2

    gustavohenke committed Apr 12, 2021
    Copy the full SHA
    ccb6b8d View commit details

Commits on May 6, 2021

  1. Run npm audit fix

    gustavohenke committed May 6, 2021
    Copy the full SHA
    70b92da View commit details

Commits on May 8, 2021

  1. Copy the full SHA
    ecf1c5b View commit details
  2. 6.1.0

    gustavohenke committed May 8, 2021
    Copy the full SHA
    aad79fa View commit details

Commits on May 9, 2021

  1. Copy the full SHA
    e8f0706 View commit details
  2. Copy the full SHA
    d38ab32 View commit details

Commits on May 24, 2021

  1. 6.2.0

    gustavohenke committed May 24, 2021
    Copy the full SHA
    becac73 View commit details

Commits on May 27, 2021

  1. Correctly reexport flow controllers

    Fixes #278
    gustavohenke authored May 27, 2021
    Copy the full SHA
    10ff00c View commit details

Commits on Jul 28, 2021

  1. Add GH actions workflow

    aivenkimmob committed Jul 28, 2021
    Copy the full SHA
    fe5c01c View commit details

Commits on Jul 29, 2021

  1. Copy the full SHA
    f574848 View commit details

Commits on Aug 3, 2021

  1. Copy the full SHA
    07a7de1 View commit details

Commits on Aug 8, 2021

  1. Copy the full SHA
    c295062 View commit details
  2. 6.2.1

    gustavohenke committed Aug 8, 2021
    Copy the full SHA
    0bc490f View commit details

Commits on Sep 25, 2021

  1. Copy the full SHA
    b16585f View commit details
  2. Copy the full SHA
    24e51ad View commit details
  3. Copy the full SHA
    8bcdf7f View commit details

Commits on Sep 27, 2021

  1. Remove read-pkg

    Fixes #274
    gustavohenke committed Sep 27, 2021
    Copy the full SHA
    7263ffe View commit details
  2. Fix linting

    gustavohenke committed Sep 27, 2021
    Copy the full SHA
    875d375 View commit details
  3. 6.2.2

    gustavohenke committed Sep 27, 2021
    Copy the full SHA
    105445c View commit details

Commits on Oct 2, 2021

  1. Copy the full SHA
    ed8d792 View commit details
  2. 6.3.0

    gustavohenke committed Oct 2, 2021
    Copy the full SHA
    08eda4f View commit details
  3. Add option and CLI flag to hide command output (#173)

    Oliver Vartiainen authored Oct 2, 2021
    Copy the full SHA
    88b8d19 View commit details
  4. Copy the full SHA
    475fb53 View commit details

Commits on Oct 3, 2021

  1. Simplify .gitignore

    gustavohenke authored Oct 3, 2021
    Copy the full SHA
    66ed4bf View commit details

Commits on Nov 13, 2021

  1. 6.4.0

    gustavohenke committed Nov 13, 2021
    Copy the full SHA
    0da5d93 View commit details

Commits on Nov 18, 2021

  1. Copy the full SHA
    59de6e4 View commit details

Commits on Dec 5, 2021

  1. Copy the full SHA
    c04740a View commit details

Commits on Dec 17, 2021

  1. Add support for options in environment variables (#289)

    Co-authored-by: Gustavo Henke <guhenke@gmail.com>
    aidansteele and gustavohenke authored Dec 17, 2021
    Copy the full SHA
    7578774 View commit details
  2. Copy the full SHA
    f8119bf View commit details
  3. npm audit fix

    gustavohenke committed Dec 17, 2021
    Copy the full SHA
    ce799d6 View commit details
  4. 6.5.0

    gustavohenke committed Dec 17, 2021
    Copy the full SHA
    ecc5fa0 View commit details

Commits on Dec 19, 2021

  1. Copy the full SHA
    dd54b9f View commit details
  2. 6.5.1

    gustavohenke committed Dec 19, 2021
    Copy the full SHA
    041e090 View commit details
  3. Copy the full SHA
    1c1934b View commit details
  4. Copy the full SHA
    a23a465 View commit details
  5. Copy the full SHA
    7fcbda1 View commit details
  6. Copy the full SHA
    01d109a View commit details
Showing with 13,949 additions and 6,037 deletions.
  1. +5 −17 .editorconfig
  2. +23 −21 .eslintrc.json
  3. +1 −0 .gitattributes
  4. +14 −0 .github/actions/setup-npm-cache/action.yml
  5. +104 −0 .github/workflows/ci.yml
  6. +3 −20 .gitignore
  7. +4 −0 .prettierrc.json
  8. +0 −12 .travis.yml
  9. +8 −0 .vscode/extensions.json
  10. +8 −4 .vscode/settings.json
  11. +10 −5 CONTRIBUTING.md
  12. +223 −94 README.md
  13. +0 −20 appveyor.yml
  14. +0 −173 bin/concurrently.js
  15. +204 −89 bin/{concurrently.spec.js → concurrently.spec.ts}
  16. +226 −0 bin/concurrently.ts
  17. +86 −0 bin/epilogue.ts
  18. +0 −42 bin/epilogue.txt
  19. +2 −1 bin/fixtures/read-echo.js
  20. +6 −0 declarations/spawn-command.d.ts
  21. +8 −58 index.js
  22. +9 −0 index.mjs
  23. +11 −0 jest.config.js
  24. +9,184 −3,950 package-lock.json
  25. +62 −30 package.json
  26. +20 −0 src/command-parser/command-parser.ts
  27. +68 −0 src/command-parser/expand-arguments.spec.ts
  28. +42 −0 src/command-parser/expand-arguments.ts
  29. +0 −13 src/command-parser/expand-npm-shortcut.js
  30. +14 −15 src/command-parser/{expand-npm-shortcut.spec.js → expand-npm-shortcut.spec.ts}
  31. +21 −0 src/command-parser/expand-npm-shortcut.ts
  32. +0 −34 src/command-parser/expand-npm-wildcard.js
  33. +0 −58 src/command-parser/expand-npm-wildcard.spec.js
  34. +128 −0 src/command-parser/expand-npm-wildcard.spec.ts
  35. +72 −0 src/command-parser/expand-npm-wildcard.ts
  36. +0 −12 src/command-parser/strip-quotes.js
  37. +0 −20 src/command-parser/strip-quotes.spec.js
  38. +35 −0 src/command-parser/strip-quotes.spec.ts
  39. +18 −0 src/command-parser/strip-quotes.ts
  40. +0 −60 src/command.js
  41. +0 −167 src/command.spec.js
  42. +270 −0 src/command.spec.ts
  43. +195 −0 src/command.ts
  44. +0 −39 src/completion-listener.js
  45. +0 −88 src/completion-listener.spec.js
  46. +208 −0 src/completion-listener.spec.ts
  47. +101 −0 src/completion-listener.ts
  48. +0 −87 src/concurrently.js
  49. +0 −118 src/concurrently.spec.js
  50. +274 −0 src/concurrently.spec.ts
  51. +246 −0 src/concurrently.ts
  52. +0 −29 src/defaults.js
  53. +82 −0 src/defaults.ts
  54. +51 −0 src/fixtures/fake-command.ts
  55. +0 −16 src/flow-control/fixtures/fake-command.js
  56. +11 −0 src/flow-control/flow-controller.ts
  57. +0 −39 src/flow-control/input-handler.js
  58. +0 −79 src/flow-control/input-handler.spec.js
  59. +128 −0 src/flow-control/input-handler.spec.ts
  60. +78 −0 src/flow-control/input-handler.ts
  61. +0 −30 src/flow-control/kill-on-signal.js
  62. +17 −18 src/flow-control/{kill-on-signal.spec.js → kill-on-signal.spec.ts}
  63. +42 −0 src/flow-control/kill-on-signal.ts
  64. +0 −35 src/flow-control/kill-others.js
  65. +31 −24 src/flow-control/{kill-others.spec.js → kill-others.spec.ts}
  66. +58 −0 src/flow-control/kill-others.ts
  67. +0 −20 src/flow-control/log-error.js
  68. +0 −40 src/flow-control/log-error.spec.js
  69. +50 −0 src/flow-control/log-error.spec.ts
  70. +30 −0 src/flow-control/log-error.ts
  71. +0 −13 src/flow-control/log-exit.js
  72. +11 −12 src/flow-control/{log-exit.spec.js → log-exit.spec.ts}
  73. +27 −0 src/flow-control/log-exit.ts
  74. +0 −14 src/flow-control/log-output.js
  75. +10 −11 src/flow-control/{log-output.spec.js → log-output.spec.ts}
  76. +22 −0 src/flow-control/log-output.ts
  77. +125 −0 src/flow-control/log-timings.spec.ts
  78. +102 −0 src/flow-control/log-timings.ts
  79. +0 −51 src/flow-control/restart-process.js
  80. +43 −34 src/flow-control/{restart-process.spec.js → restart-process.spec.ts}
  81. +82 −0 src/flow-control/restart-process.ts
  82. +0 −13 src/get-spawn-opts.js
  83. +0 −18 src/get-spawn-opts.spec.js
  84. +34 −0 src/get-spawn-opts.spec.ts
  85. +49 −0 src/get-spawn-opts.ts
  86. +165 −0 src/index.ts
  87. +0 −109 src/logger.js
  88. +0 −185 src/logger.spec.js
  89. +359 −0 src/logger.spec.ts
  90. +256 −0 src/logger.ts
  91. +99 −0 src/output-writer.spec.ts
  92. +60 −0 src/output-writer.ts
  93. +14 −0 tsconfig.json
22 changes: 5 additions & 17 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
# Top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
# Unix-style newlines with newline ending and space indentation for all files
[*]
end_of_line = lf
insert_final_newline = true

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4

# Tab indentation (no size specified)
[*.js]
indent_style = space
# 4 space indentation and max line length of 100 in JavaScript/TypeScript files
[*.{js,ts}]
indent_size = 4
max_line_length = 100

# Matches the exact files package.json and .travis.yml
[{package.json,.travis.yml,Gruntfile.js}]
indent_style = space
indent_size = 2

[Makefile]
indent_style = tab
indent_size = 4
44 changes: 23 additions & 21 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"root": true,
"env": {
"browser": false,
"node": true
},
"parserOptions": {
"ecmaVersion": 6
},
"rules": {
"block-spacing": "error",
"curly": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"indent": ["error", 4],
"keyword-spacing": "error",
"no-var": "error",
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": "error",
"space-before-blocks": "error",
"space-before-function-paren": ["error", "never"]
}
"root": true,
"env": {
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 8
},
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"curly": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"no-var": "error",
"no-console": "error",
"prefer-const": "error",
"prefer-object-spread": "error",
"prettier/prettier": ["error"]
}
}
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
14 changes: 14 additions & 0 deletions .github/actions/setup-npm-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See https://github.com/actions/cache/blob/main/examples.md#node---npm
name: Setup NPM cache
runs:
using: composite
steps:
- id: npm-cache-dir
run: echo "::set-output name=dir::$(npm config get cache)"
shell: bash
- uses: actions/cache@v3
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
104 changes: 104 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Continuous Integration
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint-format:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

- name: Setup NPM cache
uses: ./.github/actions/setup-npm-cache

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Format
run: npm run format

test:
name: Test (Node.js ${{ matrix.node }} on ${{ matrix.os.name }})
runs-on: ${{ matrix.os.version }}
strategy:
fail-fast: false
matrix:
node:
- 12
- 14
- 16
- 17
os:
- name: Ubuntu
version: ubuntu-latest
- name: Windows
version: windows-latest
- name: macOS
version: macOS-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

- name: Setup NPM cache
uses: ./.github/actions/setup-npm-cache

- name: Print versions
run: |
node --version
npm --version
- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Test
run: npm test
shell: bash
env:
SHELL: '/bin/bash'

- name: Submit coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os.name }}
parallel: true

coverage:
name: Coverage
needs: test
runs-on: ubuntu-latest
steps:
- name: Finish coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
23 changes: 3 additions & 20 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
# Outputs
/dist

# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules

# OS X
.DS_Store

# Vagrant directory
.vagrant
4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"arrowParens": "avoid"
}
12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

8 changes: 8 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"orta.vscode-jest"
]
}
12 changes: 8 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"eslint.enable": true,
"jest.autoEnable": true,
"jest.showCoverageOnLoad": true,
"editor.rulers": [100]
"jest.showCoverageOnLoad": true,
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript, typescript]": {
"editor.formatOnSave": false
},
"editor.rulers": [100]
}
15 changes: 10 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -8,16 +8,21 @@ updated.

# Maintaining

## Code Format & Linting

Code format and lint checks are performed locally when committing to ensure the changes align with the configured rules of this repository. This happens with the help of the tools [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) and [lint-staged](https://github.com/okonet/lint-staged) which are automatically installed and configured on `npm install` (no further steps required).

In case of problems, a corresponding message is displayed in your terminal.
Please fix them and then run the commit command again.

## Test

Tests can be run with command:
Tests can be run with the command:

```bash
npm run test
npm test
```

## Release

* Commit all changes
* Run `./node_modules/.bin/releasor --bump minor`, which will create new tag and publish code to GitHub and npm. See https://github.com/kimmobrunfeldt/releasor for options
* Edit GitHub release notes
Use [np](https://www.npmjs.com/package/np) to create a new release.
317 changes: 223 additions & 94 deletions README.md

Large diffs are not rendered by default.

20 changes: 0 additions & 20 deletions appveyor.yml

This file was deleted.

173 changes: 0 additions & 173 deletions bin/concurrently.js

This file was deleted.

293 changes: 204 additions & 89 deletions bin/concurrently.spec.js → bin/concurrently.spec.ts

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions bin/concurrently.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import * as defaults from '../src/defaults';
import concurrently from '../src/index';
import { epilogue } from './epilogue';

// Clean-up arguments (yargs expects only the arguments after the program name)
const cleanArgs = hideBin(process.argv);
// Find argument separator (double dash)
const argsSepIdx = cleanArgs.findIndex(arg => arg === '--');
// Arguments before separator
const argsBeforeSep = argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs;
// Arguments after separator
const argsAfterSep = argsSepIdx >= 0 ? cleanArgs.slice(argsSepIdx + 1) : [];

const args = yargs(argsBeforeSep)
.usage('$0 [options] <command ...>')
.help('h')
.alias('h', 'help')
.version()
.alias('version', 'v')
.alias('version', 'V')
// TODO: Add some tests for this.
.env('CONCURRENTLY')
.options({
// General
'max-processes': {
alias: 'm',
describe:
'How many processes should run at once.\n' +
'New processes only spawn after all restart tries of a process.',
type: 'number',
},
names: {
alias: 'n',
describe:
'List of custom names to be used in prefix template.\n' +
'Example names: "main,browser,server"',
type: 'string',
},
'name-separator': {
describe:
'The character to split <names> on. Example usage:\n' +
'concurrently -n "styles|scripts|server" --name-separator "|"',
default: defaults.nameSeparator,
},
success: {
alias: 's',
describe:
'Which command(s) must exit with code 0 in order for concurrently exit with ' +
'code 0 too. Options are:\n' +
'- "first" for the first command to exit;\n' +
'- "last" for the last command to exit;\n' +
'- "all" for all commands;\n' +
// Note: not a typo. Multiple commands can have the same name.
'- "command-{name}"/"command-{index}" for the commands with that name or index;\n' +
'- "!command-{name}"/"!command-{index}" for all commands but the ones with that ' +
'name or index.\n',
default: defaults.success,
},
raw: {
alias: 'r',
describe:
'Output only raw output of processes, disables prettifying ' +
'and concurrently coloring.',
type: 'boolean',
},
// This one is provided for free. Chalk reads this itself and removes colours.
// https://www.npmjs.com/package/chalk#chalksupportscolor
'no-color': {
describe: 'Disables colors from logging',
type: 'boolean',
},
hide: {
describe:
'Comma-separated list of processes to hide the output.\n' +
'The processes can be identified by their name or index.',
default: defaults.hide,
type: 'string',
},
group: {
alias: 'g',
describe: 'Order the output as if the commands were run sequentially.',
type: 'boolean',
},
timings: {
describe: 'Show timing information for all processes.',
type: 'boolean',
default: defaults.timings,
},
'passthrough-arguments': {
alias: 'P',
describe:
'Passthrough additional arguments to commands (accessible via placeholders) ' +
'instead of treating them as commands.',
type: 'boolean',
default: defaults.passthroughArguments,
},

// Kill others
'kill-others': {
alias: 'k',
describe: 'Kill other processes if one exits or dies.',
type: 'boolean',
},
'kill-others-on-fail': {
describe: 'Kill other processes if one exits with non zero status code.',
type: 'boolean',
},

// Prefix
prefix: {
alias: 'p',
describe:
'Prefix used in logging for each process.\n' +
'Possible values: index, pid, time, command, name, none, or a template. ' +
'Example template: "{time}-{pid}"',
defaultDescription: 'index or name (when --names is set)',
type: 'string',
},
'prefix-colors': {
alias: 'c',
describe:
'Comma-separated list of chalk colors to use on prefixes. ' +
'If there are more commands than colors, the last color will be repeated.\n' +
'- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray \n' +
'or any hex values for colors, eg #23de43\n' +
'- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
'See https://www.npmjs.com/package/chalk for more information.',
default: defaults.prefixColors,
type: 'string',
},
'prefix-length': {
alias: 'l',
describe:
'Limit how many characters of the command is displayed in prefix. ' +
'The option can be used to shorten the prefix when it is set to "command"',
default: defaults.prefixLength,
type: 'number',
},
'timestamp-format': {
alias: 't',
describe: 'Specify the timestamp in moment/date-fns format.',
default: defaults.timestampFormat,
type: 'string',
},

// Restarting
'restart-tries': {
describe:
'How many times a process that died should restart.\n' +
'Negative numbers will make the process restart forever.',
default: defaults.restartTries,
type: 'number',
},
'restart-after': {
describe: 'Delay time to respawn the process, in milliseconds.',
default: defaults.restartDelay,
type: 'number',
},

// Input
'handle-input': {
alias: 'i',
describe:
'Whether input should be forwarded to the child processes. ' +
'See examples for more information.',
type: 'boolean',
},
'default-input-target': {
default: defaults.defaultInputTarget,
describe:
'Identifier for child process to which input on stdin ' +
'should be sent if not specified at start of input.\n' +
'Can be either the index or the name of the process.',
},
})
.group(
['m', 'n', 'name-separator', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'P'],
'General'
)
.group(['p', 'c', 'l', 't'], 'Prefix styling')
.group(['i', 'default-input-target'], 'Input handling')
.group(['k', 'kill-others-on-fail'], 'Killing other processes')
.group(['restart-tries', 'restart-after'], 'Restarting')
.epilogue(epilogue)
.parseSync();

// Get names of commands by the specified separator
const names = (args.names || '').split(args.nameSeparator);
// If "passthrough-arguments" is disabled, treat additional arguments as commands
const commands = args.passthroughArguments ? args._ : [...args._, ...argsAfterSep];

concurrently(
commands.map((command, index) => ({
command: String(command),
name: names[index],
})),
{
handleInput: args.handleInput,
defaultInputTarget: args.defaultInputTarget,
killOthers: args.killOthers
? ['success', 'failure']
: args.killOthersOnFail
? ['failure']
: [],
maxProcesses: args.maxProcesses,
raw: args.raw,
hide: args.hide.split(','),
group: args.group,
prefix: args.prefix,
prefixColors: args.prefixColors.split(','),
prefixLength: args.prefixLength,
restartDelay: args.restartAfter,
restartTries: args.restartTries,
successCondition: args.success,
timestampFormat: args.timestampFormat,
timings: args.timings,
additionalArguments: args.passthroughArguments ? argsAfterSep : undefined,
}
).result.then(
() => process.exit(0),
() => process.exit(1)
);
86 changes: 86 additions & 0 deletions bin/epilogue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Add new examples here.
// Always start with `$ $0` so that it a) symbolizes a command line; and b) $0 gets replaced by the binary name uniformly.
const examples = [
{
description: 'Output nothing more than stdout+stderr of child processes',
example: '$ $0 --raw "npm run watch-less" "npm run watch-js"',
},
{
description: 'Normal output but without colors e.g. when logging to file',
example: '$ $0 --no-color "grunt watch" "http-server" > log',
},
{
description: 'Custom prefix',
example: '$ $0 --prefix "{time}-{pid}" "npm run watch" "http-server"',
},
{
description: 'Custom names and colored prefixes',
example:
'$ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"',
},
{
description: 'Configuring via environment variables with CONCURRENTLY_ prefix',
example:
'$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true $0 "echo hello" "echo world"',
},
{
description: 'Send input to default',
example: [
'$ $0 --handle-input "nodemon" "npm run watch-js"',
'rs # Sends rs command to nodemon process',
].join('\n'),
},
{
description: 'Send input to specific child identified by index',
example: ['$ $0 --handle-input "npm run watch-js" nodemon', '1:rs'].join('\n'),
},
{
description: 'Send input to specific child identified by name',
example: ['$ $0 --handle-input -n js,srv "npm run watch-js" nodemon', 'srv:rs'].join('\n'),
},
{
description: 'Shortened NPM run commands',
example: '$ $0 npm:watch-node npm:watch-js npm:watch-css',
},
{
description: 'Shortened NPM run command with wildcard (make sure to wrap it in quotes!)',
example: '$ $0 "npm:watch-*"',
},
{
description:
'Exclude patterns so that between "lint:js" and "lint:fix:js", only "lint:js" is ran',
example: '$ $0 "npm:*(!fix)"',
},
{
description: "Passthrough some additional arguments via '{<number>}' placeholder",
example: '$ $0 -P "echo {1}" -- foo',
},
{
description: "Passthrough all additional arguments via '{@}' placeholder",
example: '$ $0 -P "npm:dev-* -- {@}" -- --watch --noEmit',
},
{
description: "Passthrough all additional arguments combined via '{*}' placeholder",
example: '$ $0 -P "npm:dev-* -- {*}" -- --watch --noEmit',
},
];

const examplesString = examples
.map(({ example, description }) =>
[
` - ${description}`,
example
.split('\n')
.map(line => ` ${line}`)
.join('\n'),
].join('\n\n')
)
.join('\n\n');

export const epilogue = `
Examples:
${examplesString}
For more details, visit https://github.com/open-cli-tools/concurrently
`;
42 changes: 0 additions & 42 deletions bin/epilogue.txt

This file was deleted.

3 changes: 2 additions & 1 deletion bin/fixtures/read-echo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
process.stdin.on('data', (chunk) => {
/* eslint-disable no-console */
process.stdin.on('data', chunk => {
const line = chunk.toString().trim();
console.log(line);

6 changes: 6 additions & 0 deletions declarations/spawn-command.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'spawn-command' {
import { ChildProcess, SpawnOptions } from 'child_process';

function spawnCommand(command: string, options: SpawnOptions): ChildProcess;
export = spawnCommand;
}
66 changes: 8 additions & 58 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,8 @@
const InputHandler = require('./src/flow-control/input-handler');
const KillOnSignal = require('./src/flow-control/kill-on-signal');
const KillOthers = require('./src/flow-control/kill-others');
const LogError = require('./src/flow-control/log-error');
const LogExit = require('./src/flow-control/log-exit');
const LogOutput = require('./src/flow-control/log-output');
const RestartProcess = require('./src/flow-control/restart-process');

const concurrently = require('./src/concurrently');
const Logger = require('./src/logger');

module.exports = (commands, options = {}) => {
const logger = new Logger({
outputStream: options.outputStream || process.stdout,
prefixFormat: options.prefix,
prefixLength: options.prefixLength,
raw: options.raw,
timestampFormat: options.timestampFormat,
});

return concurrently(commands, {
maxProcesses: options.maxProcesses,
raw: options.raw,
successCondition: options.successCondition,
controllers: [
new LogError({ logger }),
new LogOutput({ logger }),
new LogExit({ logger }),
new InputHandler({
logger,
defaultInputTarget: options.defaultInputTarget,
inputStream: options.inputStream,
}),
new KillOnSignal({ process }),
new RestartProcess({
logger,
delay: options.restartDelay,
tries: options.restartTries,
}),
new KillOthers({
logger,
conditions: options.killOthers
})
]
});
};

// Export all flow controllers and the main concurrently function,
// so that 3rd-parties can use them however they want
exports.concurrently = concurrently;
exports.Logger = Logger;
exports.InputHandler = InputHandler;
exports.KillOnSignal = KillOnSignal;
exports.KillOthers = KillOthers;
exports.LogError = LogError;
exports.LogExit = LogExit;
exports.LogOutput = LogOutput;
exports.RestartProcess = RestartProcess;
//
// While in local development, make sure you've run `npm run build` first.
//

// eslint-disable-next-line @typescript-eslint/no-var-requires
const concurrently = require('./dist/src/index.js');
module.exports = exports = concurrently.default;
Object.assign(exports, concurrently);
9 changes: 9 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// While in local development, make sure you've run `npm run build` first.
//

// NOTE: the star reexport doesn't work in Node <12.20, <14.13 and <15.
export * from './dist/src/index.js';

import concurrently from './dist/src/index.js';
export default concurrently.default;
11 changes: 11 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!src/index.ts'],
testPathIgnorePatterns: ['/node_modules/', '/dist'],
};

module.exports = config;
13,134 changes: 9,184 additions & 3,950 deletions package-lock.json

Large diffs are not rendered by default.

92 changes: 62 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
{
"name": "concurrently",
"version": "5.3.0",
"version": "7.3.0",
"description": "Run commands concurrently",
"main": "index.js",
"types": "dist/src/index.d.ts",
"type": "commonjs",
"bin": {
"concurrently": "./bin/concurrently.js"
"concurrently": "./dist/bin/concurrently.js"
},
"engines": {
"node": ">=6.0.0"
"node": "^12.20.0 || ^14.13.0 || >=16.0.0"
},
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js",
"default": "./index.js",
"types": "./dist/src/index.d.ts"
},
"./package.json": "./package.json"
},
"scripts": {
"lint": "eslint . --ignore-path .gitignore",
"build": "tsc --build",
"postbuild": "chmod +x dist/bin/concurrently.js",
"clean": "tsc --build --clean",
"format": "prettier --ignore-path .gitignore --check '**/{!(package-lock).json,*.y?(a)ml,*.md}'",
"format:fix": "npm run format -- --write",
"lint": "eslint . --ext js,ts --ignore-path .gitignore",
"lint:fix": "npm run lint -- --fix",
"prepublishOnly": "npm run build",
"report-coverage": "cat coverage/lcov.info | coveralls",
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/kimmobrunfeldt/concurrently.git"
"url": "https://github.com/open-cli-tools/concurrently.git"
},
"keywords": [
"bash",
@@ -29,38 +47,52 @@
"author": "Kimmo Brunfeldt",
"license": "MIT",
"dependencies": {
"chalk": "^2.4.2",
"date-fns": "^2.0.1",
"lodash": "^4.17.15",
"read-pkg": "^4.0.1",
"rxjs": "^6.5.2",
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^6.1.0",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^13.3.0"
"yargs": "^17.3.1"
},
"devDependencies": {
"coveralls": "^3.0.4",
"eslint": "^5.16.0",
"jest": "^24.8.0",
"jest-create-mock-instance": "^1.1.0"
"@swc-node/register": "^1.5.1",
"@swc/core": "^1.2.204",
"@swc/jest": "^0.2.21",
"@types/jest": "^27.0.3",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.0",
"@types/shell-quote": "^1.7.1",
"@types/supports-color": "^8.1.1",
"@types/yargs": "^17.0.8",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"coveralls-next": "^4.1.2",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.5.1",
"jest-create-mock-instance": "^2.0.0",
"lint-staged": "^12.4.1",
"prettier": "^2.6.2",
"simple-git-hooks": "^2.7.0",
"typescript": "^4.5.4"
},
"files": [
"bin",
"!**/fixtures",
"dist",
"index.js",
"src",
"!*.spec.js"
"index.mjs",
"!**/fixtures",
"!**/*.spec.js",
"!**/*.spec.d.ts"
],
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.js"
],
"coveragePathIgnorePatterns": [
"/fixtures/",
"/node_modules/"
],
"testEnvironment": "node"
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts}": "eslint --fix",
"{!(package-lock).json,*.y?(a)ml,*.md}": "prettier --write"
}
}
20 changes: 20 additions & 0 deletions src/command-parser/command-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommandInfo } from '../command';

/**
* A command parser encapsulates a specific logic for mapping `CommandInfo` objects
* into another `CommandInfo`.
*
* A prime example is turning an abstract `npm:foo` into `npm run foo`, but it could also turn
* the prefix color of a command brighter, or maybe even prefixing each command with `time(1)`.
*/
export interface CommandParser {
/**
* Parses `commandInfo` and returns one or more `CommandInfo`s.
*
* Returning multiple `CommandInfo` is used when there are multiple possibilities of commands to
* run given the original input.
* An example of this is when the command contains a wildcard and it must be expanded into all
* viable options so that the consumer can decide which ones to run.
*/
parse(commandInfo: CommandInfo): CommandInfo | CommandInfo[];
}
68 changes: 68 additions & 0 deletions src/command-parser/expand-arguments.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { CommandInfo } from '../command';
import { ExpandArguments } from './expand-arguments';

const createCommandInfo = (command: string): CommandInfo => ({
command,
name: '',
});

it('returns command as is when no placeholders', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo foo');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });
});

it('single argument placeholder is replaced', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo {1}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });
});

it('argument placeholder is replaced and quoted properly', () => {
const parser = new ExpandArguments(['foo bar']);
const commandInfo = createCommandInfo('echo {1}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: "echo 'foo bar'" });
});

it('multiple single argument placeholders are replaced', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo {2} {1}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo bar foo' });
});

it('empty replacement with single placeholder and not enough passthrough arguments', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo {3}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });
});

it('empty replacement with all placeholder and no passthrough arguments', () => {
const parser = new ExpandArguments([]);
const commandInfo = createCommandInfo('echo {@}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });
});

it('empty replacement with combined placeholder and no passthrough arguments', () => {
const parser = new ExpandArguments([]);
const commandInfo = createCommandInfo('echo {*}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });
});

it('all arguments placeholder is replaced', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo {@}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo bar' });
});

it('combined arguments placeholder is replaced', () => {
const parser = new ExpandArguments(['foo', 'bar']);
const commandInfo = createCommandInfo('echo {*}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: "echo 'foo bar'" });
});

it('escaped argument placeholders are not replaced', () => {
const parser = new ExpandArguments(['foo', 'bar']);
// Equals to single backslash on command line
const commandInfo = createCommandInfo('echo \\{1} \\{@} \\{*}');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo {1} {@} {*}' });
});
42 changes: 42 additions & 0 deletions src/command-parser/expand-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';
import { quote } from 'shell-quote';

/**
* Replace placeholders with additional arguments.
*/
export class ExpandArguments implements CommandParser {
constructor(private readonly additionalArguments: string[]) {}

parse(commandInfo: CommandInfo) {
const command = commandInfo.command.replace(
/\\?\{([@*]|[1-9][0-9]*)\}/g,
(match, placeholderTarget) => {
// Don't replace the placeholder if it is escaped by a backslash.
if (match.startsWith('\\')) {
return match.slice(1);
}
// Replace numeric placeholder if value exists in additional arguments.
if (
!isNaN(placeholderTarget) &&
placeholderTarget <= this.additionalArguments.length
) {
return quote([this.additionalArguments[placeholderTarget - 1]]);
}
// Replace all arguments placeholder.
if (placeholderTarget === '@') {
return quote(this.additionalArguments);
}
// Replace combined arguments placeholder.
if (placeholderTarget === '*') {
return quote([this.additionalArguments.join(' ')]);
}
// Replace placeholder with empty string
// if value doesn't exist in additional arguments.
return '';
}
);

return { ...commandInfo, command };
}
}
13 changes: 0 additions & 13 deletions src/command-parser/expand-npm-shortcut.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
const ExpandNpmShortcut = require('./expand-npm-shortcut');
import { CommandInfo } from '../command';
import { ExpandNpmShortcut } from './expand-npm-shortcut';
const parser = new ExpandNpmShortcut();

const createCommandInfo = (command: string, name?: string): CommandInfo => ({
name,
command,
});

it('returns same command if no npm: prefix is present', () => {
const commandInfo = {
name: 'echo',
command: 'echo foo'
};
const commandInfo = createCommandInfo('echo foo');
expect(parser.parse(commandInfo)).toBe(commandInfo);
});

for (const npmCmd of ['npm', 'yarn', 'pnpm']) {
describe(`with ${npmCmd}: prefix`, () => {
it(`expands to "${npmCmd} run <script> <args>"`, () => {
const commandInfo = {
name: 'echo',
command: `${npmCmd}:foo -- bar`
};
const commandInfo = createCommandInfo(`${npmCmd}:foo -- bar`, 'echo');
expect(parser.parse(commandInfo)).toEqual({
...commandInfo,
name: 'echo',
command: `${npmCmd} run foo -- bar`
command: `${npmCmd} run foo -- bar`,
});
});

it('sets name to script name if none', () => {
const commandInfo = {
command: `${npmCmd}:foo -- bar`
};
const commandInfo = createCommandInfo(`${npmCmd}:foo -- bar`);
expect(parser.parse(commandInfo)).toEqual({
...commandInfo,
name: 'foo',
command: `${npmCmd} run foo -- bar`
command: `${npmCmd} run foo -- bar`,
});
});
});

}
21 changes: 21 additions & 0 deletions src/command-parser/expand-npm-shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';

/**
* Expands commands prefixed with `npm:`, `yarn:` or `pnpm:` into the full version `npm run <command>` and so on.
*/
export class ExpandNpmShortcut implements CommandParser {
parse(commandInfo: CommandInfo) {
const [, npmCmd, cmdName, args] =
commandInfo.command.match(/^(npm|yarn|pnpm):(\S+)(.*)/) || [];
if (!cmdName) {
return commandInfo;
}

return {
...commandInfo,
name: commandInfo.name || cmdName,
command: `${npmCmd} run ${cmdName}${args}`,
};
}
}
34 changes: 0 additions & 34 deletions src/command-parser/expand-npm-wildcard.js

This file was deleted.

58 changes: 0 additions & 58 deletions src/command-parser/expand-npm-wildcard.spec.js

This file was deleted.

128 changes: 128 additions & 0 deletions src/command-parser/expand-npm-wildcard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import fs from 'fs';
import { CommandInfo } from '../command';
import { ExpandNpmWildcard } from './expand-npm-wildcard';

let parser: ExpandNpmWildcard;
let readPkg: jest.Mock;

const createCommandInfo = (command: string): CommandInfo => ({
command,
name: '',
});

beforeEach(() => {
readPkg = jest.fn();
parser = new ExpandNpmWildcard(readPkg);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('ExpandNpmWildcard#readPackage', () => {
it('can read package', () => {
const expectedPackage = {
name: 'concurrently',
version: '6.4.0',
};
jest.spyOn(fs, 'readFileSync').mockImplementation(path => {
if (path === 'package.json') {
return JSON.stringify(expectedPackage);
}
return null;
});

const actualReadPackage = ExpandNpmWildcard.readPackage();
expect(actualReadPackage).toEqual(expectedPackage);
});

it('can handle errors reading package', () => {
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
throw new Error('Error reading package');
});

expect(() => ExpandNpmWildcard.readPackage()).not.toThrow();
expect(ExpandNpmWildcard.readPackage()).toEqual({});
});
});

it('returns same command if not an npm run command', () => {
const commandInfo = createCommandInfo('npm test');

expect(readPkg).not.toHaveBeenCalled();
expect(parser.parse(commandInfo)).toBe(commandInfo);
});

it('returns same command if no wildcard present', () => {
const commandInfo = createCommandInfo('npm run foo bar');

expect(readPkg).not.toHaveBeenCalled();
expect(parser.parse(commandInfo)).toBe(commandInfo);
});

it('expands to nothing if no scripts exist in package.json', () => {
readPkg.mockReturnValue({});

expect(parser.parse(createCommandInfo('npm run foo-*-baz qux'))).toEqual([]);
});

for (const npmCmd of ['npm', 'yarn', 'pnpm']) {
describe(`with an ${npmCmd}: prefix`, () => {
it('expands to all scripts matching pattern', () => {
readPkg.mockReturnValue({
scripts: {
'foo-bar-baz': '',
'foo--baz': '',
},
});

expect(parser.parse(createCommandInfo(`${npmCmd} run foo-*-baz qux`))).toEqual([
{ name: 'bar', command: `${npmCmd} run foo-bar-baz qux` },
{ name: '', command: `${npmCmd} run foo--baz qux` },
]);
});

it('uses existing command name as prefix to the wildcard match', () => {
readPkg.mockReturnValue({
scripts: {
'watch-js': '',
'watch-css': '',
},
});

expect(
parser.parse({
name: 'w:',
command: `${npmCmd} run watch-*`,
})
).toEqual([
{ name: 'w:js', command: `${npmCmd} run watch-js` },
{ name: 'w:css', command: `${npmCmd} run watch-css` },
]);
});

it('allows negation', () => {
readPkg.mockReturnValue({
scripts: {
'lint:js': '',
'lint:ts': '',
'lint:fix:js': '',
'lint:fix:ts': '',
},
});

expect(parser.parse(createCommandInfo(`${npmCmd} run lint:*(!fix)`))).toEqual([
{ name: 'js', command: `${npmCmd} run lint:js` },
{ name: 'ts', command: `${npmCmd} run lint:ts` },
]);
});

it('caches scripts upon calls', () => {
readPkg.mockReturnValue({});
parser.parse(createCommandInfo(`${npmCmd} run foo-*-baz qux`));
parser.parse(createCommandInfo(`${npmCmd} run foo-*-baz qux`));

expect(readPkg).toHaveBeenCalledTimes(1);
});
});
}
72 changes: 72 additions & 0 deletions src/command-parser/expand-npm-wildcard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import fs from 'fs';
import _ from 'lodash';
import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';

const OMISSION = /\(!([^)]+)\)/;

/**
* Finds wildcards in npm/yarn/pnpm run commands and replaces them with all matching scripts in the
* `package.json` file of the current directory.
*/
export class ExpandNpmWildcard implements CommandParser {
static readPackage() {
try {
const json = fs.readFileSync('package.json', { encoding: 'utf-8' });
return JSON.parse(json);
} catch (e) {
return {};
}
}

private scripts?: string[];

constructor(private readonly readPackage = ExpandNpmWildcard.readPackage) {}

parse(commandInfo: CommandInfo) {
const [, npmCmd, cmdName, args] =
commandInfo.command.match(/(npm|yarn|pnpm) run (\S+)([^&]*)/) || [];
const wildcardPosition = (cmdName || '').indexOf('*');

// If the regex didn't match an npm script, or it has no wildcard,
// then we have nothing to do here
if (!cmdName || wildcardPosition === -1) {
return commandInfo;
}

if (!this.scripts) {
this.scripts = Object.keys(this.readPackage().scripts || {});
}

const omissionRegex = cmdName.match(OMISSION);
const cmdNameSansOmission = cmdName.replace(OMISSION, '');
const preWildcard = _.escapeRegExp(cmdNameSansOmission.slice(0, wildcardPosition));
const postWildcard = _.escapeRegExp(cmdNameSansOmission.slice(wildcardPosition + 1));
const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);
const currentName = commandInfo.name || '';

return this.scripts
.map(script => {
const match = script.match(wildcardRegex);

if (omissionRegex) {
const toOmit = script.match(new RegExp(omissionRegex[1]));

if (toOmit) {
return;
}
}

if (match) {
return {
...commandInfo,
command: `${npmCmd} run ${script}${args}`,
// Will use an empty command name if command has no name and the wildcard match is empty,
// e.g. if `npm:watch-*` matches `npm run watch-`.
name: currentName + match[1],
};
}
})
.filter((commandInfo): commandInfo is CommandInfo => !!commandInfo);
}
}
12 changes: 0 additions & 12 deletions src/command-parser/strip-quotes.js

This file was deleted.

20 changes: 0 additions & 20 deletions src/command-parser/strip-quotes.spec.js

This file was deleted.

35 changes: 35 additions & 0 deletions src/command-parser/strip-quotes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CommandInfo } from '../command';
import { StripQuotes } from './strip-quotes';

const parser = new StripQuotes();

const createCommandInfo = (command: string): CommandInfo => ({
command,
name: '',
});

it('returns command as is if no single/double quote at the beginning', () => {
const commandInfo = createCommandInfo('echo foo');
expect(parser.parse(commandInfo)).toEqual(commandInfo);
});

it('strips single quotes', () => {
const commandInfo = createCommandInfo("'echo foo'");
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });
});

it('strips double quotes', () => {
const commandInfo = createCommandInfo('"echo foo"');
expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });
});

it('does not remove quotes if they are unbalanced', () => {
let commandInfo = createCommandInfo('"echo foo');
expect(parser.parse(commandInfo)).toEqual(commandInfo);

commandInfo = createCommandInfo("echo foo'");
expect(parser.parse(commandInfo)).toEqual(commandInfo);

commandInfo = createCommandInfo('"echo foo\'');
expect(parser.parse(commandInfo)).toEqual(commandInfo);
});
18 changes: 18 additions & 0 deletions src/command-parser/strip-quotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';

/**
* Strips quotes around commands so that they can run on the current shell.
*/
export class StripQuotes implements CommandParser {
parse(commandInfo: CommandInfo) {
let { command } = commandInfo;

// Removes the quotes surrounding a command.
if (/^"(.+?)"$/.test(command) || /^'(.+?)'$/.test(command)) {
command = command.slice(1, command.length - 1);
}

return { ...commandInfo, command };
}
}
60 changes: 0 additions & 60 deletions src/command.js

This file was deleted.

167 changes: 0 additions & 167 deletions src/command.spec.js

This file was deleted.

270 changes: 270 additions & 0 deletions src/command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { SpawnOptions } from 'child_process';
import { EventEmitter } from 'events';
import { Readable, Writable } from 'stream';
import { ChildProcess, Command, CommandInfo, KillProcess, SpawnCommand } from './command';

let process: ChildProcess;
let spawn: jest.Mocked<SpawnCommand>;
let killProcess: KillProcess;

beforeEach(() => {
process = new (class extends EventEmitter {
readonly pid = 1;
readonly stdout = new Readable({
read() {
// do nothing
},
});
readonly stderr = new Readable({
read() {
// do nothing
},
});
readonly stdin = new Writable({
write() {
// do nothing
},
});
})();
spawn = jest.fn().mockReturnValue(process);
killProcess = jest.fn();
});

const createCommand = (overrides?: Partial<CommandInfo>, spawnOpts?: SpawnOptions) =>
new Command(
{ index: 0, name: '', command: 'echo foo', ...overrides },
spawnOpts,
spawn,
killProcess
);

describe('#start()', () => {
it('spawns process with given command and options', () => {
const command = createCommand({}, { detached: true });
command.start();

expect(spawn).toHaveBeenCalledTimes(1);
expect(spawn).toHaveBeenCalledWith(command.command, { detached: true });
});

it('sets stdin, process and PID', () => {
const command = createCommand();

command.start();
expect(command.process).toBe(process);
expect(command.pid).toBe(process.pid);
expect(command.stdin).toBe(process.stdin);
});

it('shares errors to the error stream', done => {
const command = createCommand();
command.error.subscribe(data => {
expect(data).toBe('foo');
expect(command.process).toBeUndefined();
done();
});

command.start();
process.emit('error', 'foo');
});

it('shares start and close timing events to the timing stream', done => {
const command = createCommand();

const startDate = new Date();
const endDate = new Date(startDate.getTime() + 1000);
jest.spyOn(Date, 'now')
.mockReturnValueOnce(startDate.getTime())
.mockReturnValueOnce(endDate.getTime());

let callCount = 0;
command.timer.subscribe(({ startDate: actualStartDate, endDate: actualEndDate }) => {
switch (callCount) {
case 0:
expect(actualStartDate).toStrictEqual(startDate);
expect(actualEndDate).toBeUndefined();
break;
case 1:
expect(actualStartDate).toStrictEqual(startDate);
expect(actualEndDate).toStrictEqual(endDate);
done();
break;
default:
throw new Error('Unexpected call count');
}
callCount++;
});

command.start();
process.emit('close', 0, null);
});

it('shares start and error timing events to the timing stream', done => {
const command = createCommand();

const startDate = new Date();
const endDate = new Date(startDate.getTime() + 1000);
jest.spyOn(Date, 'now')
.mockReturnValueOnce(startDate.getTime())
.mockReturnValueOnce(endDate.getTime());

let callCount = 0;
command.timer.subscribe(({ startDate: actualStartDate, endDate: actualEndDate }) => {
switch (callCount) {
case 0:
expect(actualStartDate).toStrictEqual(startDate);
expect(actualEndDate).toBeUndefined();
break;
case 1:
expect(actualStartDate).toStrictEqual(startDate);
expect(actualEndDate).toStrictEqual(endDate);
done();
break;
default:
throw new Error('Unexpected call count');
}
callCount++;
});

command.start();
process.emit('error', 0, null);
});

it('shares closes to the close stream with exit code', done => {
const command = createCommand();

command.close.subscribe(data => {
expect(data.exitCode).toBe(0);
expect(data.killed).toBe(false);
expect(command.process).toBeUndefined();
done();
});

command.start();
process.emit('close', 0, null);
});

it('shares closes to the close stream with signal', done => {
const command = createCommand();

command.close.subscribe(data => {
expect(data.exitCode).toBe('SIGKILL');
expect(data.killed).toBe(false);
done();
});

command.start();
process.emit('close', null, 'SIGKILL');
});

it('shares closes to the close stream with timing information', done => {
const command = createCommand();

const startDate = new Date();
const endDate = new Date(startDate.getTime() + 1000);
jest.spyOn(Date, 'now')
.mockReturnValueOnce(startDate.getTime())
.mockReturnValueOnce(endDate.getTime());

jest.spyOn(global.process, 'hrtime')
.mockReturnValueOnce([0, 0])
.mockReturnValueOnce([1, 1e8]);

command.close.subscribe(data => {
expect(data.timings.startDate).toStrictEqual(startDate);
expect(data.timings.endDate).toStrictEqual(endDate);
expect(data.timings.durationSeconds).toBe(1.1);
done();
});

command.start();
process.emit('close', null, 'SIGKILL');
});

it('shares closes to the close stream with command info', done => {
const commandInfo = {
command: 'cmd',
name: 'name',
prefixColor: 'green',
env: { VAR: 'yes' },
};
const command = createCommand(commandInfo);

command.close.subscribe(data => {
expect(data.command).toEqual(expect.objectContaining(commandInfo));
expect(data.killed).toBe(false);
done();
});

command.start();
process.emit('close', 0, null);
});

it('shares stdout to the stdout stream', done => {
const command = createCommand();

command.stdout.subscribe(data => {
expect(data.toString()).toBe('hello');
done();
});

command.start();
process.stdout.emit('data', Buffer.from('hello'));
});

it('shares stderr to the stdout stream', done => {
const command = createCommand();

command.stderr.subscribe(data => {
expect(data.toString()).toBe('dang');
done();
});

command.start();
process.stderr.emit('data', Buffer.from('dang'));
});
});

describe('#kill()', () => {
let command: Command;
beforeEach(() => {
command = createCommand();
});

it('kills process', () => {
command.start();
command.kill();

expect(killProcess).toHaveBeenCalledTimes(1);
expect(killProcess).toHaveBeenCalledWith(command.pid, undefined);
});

it('kills process with some signal', () => {
command.start();
command.kill('SIGKILL');

expect(killProcess).toHaveBeenCalledTimes(1);
expect(killProcess).toHaveBeenCalledWith(command.pid, 'SIGKILL');
});

it('does not try to kill inexistent process', () => {
command.start();
process.emit('error');
command.kill();

expect(killProcess).not.toHaveBeenCalled();
});

it('marks the command as killed', done => {
command.start();

command.close.subscribe(data => {
expect(data.exitCode).toBe(1);
expect(data.killed).toBe(true);
done();
});

command.kill();
process.emit('close', 1, null);
});
});
195 changes: 195 additions & 0 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { ChildProcess as BaseChildProcess, SpawnOptions } from 'child_process';
import * as Rx from 'rxjs';
import { EventEmitter, Writable } from 'stream';

/**
* Identifier for a command; if string, it's the command's name, if number, it's the index.
*/
export type CommandIdentifier = string | number;

export interface CommandInfo {
/**
* Command's name.
*/
name: string;

/**
* Which command line the command has.
*/
command: string;

/**
* Which environment variables should the spawned process have.
*/
env?: Record<string, unknown>;

/**
* The current working directory of the process when spawned.
*/
cwd?: string;

/**
* Color to use on prefix of command.
*/
prefixColor?: string;
}

export interface CloseEvent {
command: CommandInfo;

/**
* The command's index among all commands ran.
*/
index: number;

/**
* Whether the command exited because it was killed.
*/
killed: boolean;

/**
* The exit code or signal for the command.
*/
exitCode: string | number;
timings: {
startDate: Date;
endDate: Date;
durationSeconds: number;
};
}

export interface TimerEvent {
startDate: Date;
endDate?: Date;
}

/**
* Subtype of NodeJS's child_process including only what's actually needed for a command to work.
*/
export type ChildProcess = EventEmitter &
Pick<BaseChildProcess, 'pid' | 'stdin' | 'stdout' | 'stderr'>;

/**
* Interface for a function that must kill the process with `pid`, optionally sending `signal` to it.
*/
export type KillProcess = (pid: number, signal?: string) => void;

/**
* Interface for a function that spawns a command and returns its child process instance.
*/
export type SpawnCommand = (command: string, options: SpawnOptions) => ChildProcess;

export class Command implements CommandInfo {
private readonly killProcess: KillProcess;
private readonly spawn: SpawnCommand;
private readonly spawnOpts: SpawnOptions;
readonly index: number;

/** @inheritdoc */
readonly name: string;

/** @inheritdoc */
readonly command: string;

/** @inheritdoc */
readonly prefixColor: string;

/** @inheritdoc */
readonly env: Record<string, unknown>;

/** @inheritdoc */
readonly cwd?: string;

readonly close = new Rx.Subject<CloseEvent>();
readonly error = new Rx.Subject<unknown>();
readonly stdout = new Rx.Subject<Buffer>();
readonly stderr = new Rx.Subject<Buffer>();
readonly timer = new Rx.Subject<TimerEvent>();

process?: ChildProcess;
stdin?: Writable;
pid?: number;
killed = false;
exited = false;

get killable() {
return !!this.process;
}

constructor(
{ index, name, command, prefixColor, env, cwd }: CommandInfo & { index: number },
spawnOpts: SpawnOptions,
spawn: SpawnCommand,
killProcess: KillProcess
) {
this.index = index;
this.name = name;
this.command = command;
this.prefixColor = prefixColor;
this.env = env;
this.cwd = cwd;
this.killProcess = killProcess;
this.spawn = spawn;
this.spawnOpts = spawnOpts;
}

/**
* Starts this command, piping output, error and close events onto the corresponding observables.
*/
start() {
const child = this.spawn(this.command, this.spawnOpts);
this.process = child;
this.pid = child.pid;
const startDate = new Date(Date.now());
const highResStartTime = process.hrtime();
this.timer.next({ startDate });

Rx.fromEvent<unknown>(child, 'error').subscribe(event => {
this.process = undefined;
const endDate = new Date(Date.now());
this.timer.next({ startDate, endDate });
this.error.next(event);
});
Rx.fromEvent<[number | null, NodeJS.Signals | null]>(child, 'close').subscribe(
([exitCode, signal]) => {
this.process = undefined;
this.exited = true;

const endDate = new Date(Date.now());
this.timer.next({ startDate, endDate });
const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);
this.close.next({
command: this,
index: this.index,
exitCode: exitCode === null ? signal : exitCode,
killed: this.killed,
timings: {
startDate,
endDate,
durationSeconds: durationSeconds + durationNanoSeconds / 1e9,
},
});
}
);
child.stdout && pipeTo(Rx.fromEvent<Buffer>(child.stdout, 'data'), this.stdout);
child.stderr && pipeTo(Rx.fromEvent<Buffer>(child.stderr, 'data'), this.stderr);
this.stdin = child.stdin;
}

/**
* Kills this command, optionally specifying a signal to send to it.
*/
kill(code?: string) {
if (this.killable) {
this.killed = true;
this.killProcess(this.pid, code);
}
}
}

/**
* Pipes all events emitted by `stream` into `subject`.
*/
function pipeTo<T>(stream: Rx.Observable<T>, subject: Rx.Subject<T>) {
stream.subscribe(event => subject.next(event));
}
39 changes: 0 additions & 39 deletions src/completion-listener.js

This file was deleted.

88 changes: 0 additions & 88 deletions src/completion-listener.spec.js

This file was deleted.

208 changes: 208 additions & 0 deletions src/completion-listener.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { TestScheduler } from 'rxjs/testing';
import { CloseEvent } from './command';
import { CompletionListener, SuccessCondition } from './completion-listener';
import { createFakeCloseEvent, FakeCommand } from './fixtures/fake-command';

let commands: FakeCommand[];
let scheduler: TestScheduler;
beforeEach(() => {
commands = [
new FakeCommand('foo', 'echo', 0),
new FakeCommand('bar', 'echo', 1),
new FakeCommand('baz', 'echo', 2),
];
scheduler = new TestScheduler(() => true);
});

const createController = (successCondition?: SuccessCondition) =>
new CompletionListener({
successCondition,
scheduler,
});

const emitFakeCloseEvent = (command: FakeCommand, event?: Partial<CloseEvent>) =>
command.close.next(createFakeCloseEvent({ ...event, command, index: command.index }));

describe('with default success condition set', () => {
it('succeeds if all processes exited with code 0', () => {
const result = createController().listen(commands);

commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it('fails if one of the processes exited with non-0 code', () => {
const result = createController().listen(commands);

commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

expect(result).rejects.toEqual(expect.anything());
});
});

describe('with success condition set to first', () => {
it('succeeds if first process to exit has code 0', () => {
const result = createController('first').listen(commands);

commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it('fails if first process to exit has non-0 code', () => {
const result = createController('first').listen(commands);

commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

return expect(result).rejects.toEqual(expect.anything());
});
});

describe('with success condition set to last', () => {
it('succeeds if last process to exit has code 0', () => {
const result = createController('last').listen(commands);

commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it('fails if last process to exit has non-0 code', () => {
const result = createController('last').listen(commands);

commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));

scheduler.flush();

return expect(result).rejects.toEqual(expect.anything());
});
});

describe.each([
// Use the middle command for both cases to make it more difficult to make a mess up
// in the implementation cause false passes.
['command-bar' as const, 'bar'],
['command-1' as const, 1],
])('with success condition set to %s', (condition, nameOrIndex) => {
it(`succeeds if command ${nameOrIndex} exits with code 0`, () => {
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 1 });
emitFakeCloseEvent(commands[1], { exitCode: 0 });
emitFakeCloseEvent(commands[2], { exitCode: 1 });

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => {
commands = [commands[0], commands[1], commands[1]];
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 1 });
emitFakeCloseEvent(commands[1], { exitCode: 0 });
emitFakeCloseEvent(commands[2], { exitCode: 0 });

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it(`fails if command ${nameOrIndex} exits with non-0 code`, () => {
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 0 });
emitFakeCloseEvent(commands[1], { exitCode: 1 });
emitFakeCloseEvent(commands[2], { exitCode: 0 });

scheduler.flush();

return expect(result).rejects.toEqual(expect.anything());
});

it(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {
commands = [commands[0], commands[1], commands[1]];
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 1 });
emitFakeCloseEvent(commands[1], { exitCode: 0 });
emitFakeCloseEvent(commands[2], { exitCode: 1 });

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it(`fails if command ${nameOrIndex} doesn't exist`, () => {
const result = createController(condition).listen([commands[0]]);

emitFakeCloseEvent(commands[0], { exitCode: 0 });
scheduler.flush();

return expect(result).rejects.toEqual(expect.anything());
});
});

describe.each([
// Use the middle command for both cases to make it more difficult to make a mess up
// in the implementation cause false passes.
['!command-bar' as const, 'bar'],
['!command-1' as const, 1],
])('with success condition set to %s', (condition, nameOrIndex) => {
it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => {
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 0 });
emitFakeCloseEvent(commands[1], { exitCode: 1 });
emitFakeCloseEvent(commands[2], { exitCode: 0 });

scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});

it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => {
const result = createController(condition).listen(commands);

emitFakeCloseEvent(commands[0], { exitCode: 1 });
emitFakeCloseEvent(commands[1], { exitCode: 1 });
emitFakeCloseEvent(commands[2], { exitCode: 0 });

scheduler.flush();

return expect(result).rejects.toEqual(expect.anything());
});

it(`succeeds if command ${nameOrIndex} doesn't exist`, () => {
const result = createController(condition).listen([commands[0]]);

emitFakeCloseEvent(commands[0], { exitCode: 0 });
scheduler.flush();

return expect(result).resolves.toEqual(expect.anything());
});
});
101 changes: 101 additions & 0 deletions src/completion-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as Rx from 'rxjs';
import { bufferCount, switchMap, take } from 'rxjs/operators';
import { CloseEvent, Command } from './command';

/**
* Defines which command(s) in a list must exit successfully (with an exit code of `0`):
*
* - `first`: only the first specified command;
* - `last`: only the last specified command;
* - `all`: all commands.
* - `command-{name|index}`: only the commands with the specified names or index.
* - `!command-{name|index}`: all commands but the ones with the specified names or index.
*/
export type SuccessCondition =
| 'first'
| 'last'
| 'all'
| `command-${string | number}`
| `!command-${string | number}`;

/**
* Provides logic to determine whether lists of commands ran successfully.
*/
export class CompletionListener {
private readonly successCondition: SuccessCondition;
private readonly scheduler?: Rx.SchedulerLike;

constructor({
successCondition = 'all',
scheduler,
}: {
/**
* How this instance will define that a list of commands ran successfully.
* Defaults to `all`.
*
* @see {SuccessCondition}
*/
successCondition?: SuccessCondition;

/**
* For testing only.
*/
scheduler?: Rx.SchedulerLike;
}) {
this.successCondition = successCondition;
this.scheduler = scheduler;
}

private isSuccess(events: CloseEvent[]) {
if (this.successCondition === 'first') {
return events[0].exitCode === 0;
} else if (this.successCondition === 'last') {
return events[events.length - 1].exitCode === 0;
}

const commandSyntaxMatch = this.successCondition.match(/^!?command-(.+)$/);
if (commandSyntaxMatch == null) {
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
return events.every(({ exitCode }) => exitCode === 0);
}

// Check `command-` syntax condition.
// Note that a command's `name` is not necessarily unique,
// in which case all of them must meet the success condition.
const nameOrIndex = commandSyntaxMatch[1];
const targetCommandsEvents = events.filter(
({ command, index }) => command.name === nameOrIndex || index === Number(nameOrIndex)
);
if (this.successCondition.startsWith('!')) {
// All commands except the specified ones must exit succesfully
return events.every(
event => targetCommandsEvents.includes(event) || event.exitCode === 0
);
}
// Only the specified commands must exit succesfully
return (
targetCommandsEvents.length > 0 &&
targetCommandsEvents.every(event => event.exitCode === 0)
);
}

/**
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
*
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
*/
listen(commands: Command[]): Promise<CloseEvent[]> {
const closeStreams = commands.map(command => command.close);
return Rx.lastValueFrom(
Rx.merge(...closeStreams).pipe(
bufferCount(closeStreams.length),
switchMap(exitInfos =>
this.isSuccess(exitInfos)
? Rx.of(exitInfos, this.scheduler)
: Rx.throwError(exitInfos, this.scheduler)
),
take(1)
)
);
}
}
87 changes: 0 additions & 87 deletions src/concurrently.js

This file was deleted.

118 changes: 0 additions & 118 deletions src/concurrently.spec.js

This file was deleted.

Loading