From 2118d3fe695125d624b992e1a6cb950dfa285a95 Mon Sep 17 00:00:00 2001 From: Eli <13420359+eliasm307@users.noreply.github.com> Date: Fri, 19 Nov 2021 01:43:21 +0000 Subject: [PATCH] add auto colors functionality to resolve #279 --- README.md | 104 ++++++++++++++++-------------- bin/concurrently.js | 6 ++ index.js | 3 +- src/concurrently.js | 8 +-- src/defaults.js | 4 +- src/prefix-color-selector.js | 69 ++++++++++++++++++++ src/prefix-color-selector.spec.js | 92 ++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 src/prefix-color-selector.js create mode 100644 src/prefix-color-selector.spec.js diff --git a/README.md b/README.md index 1f740862..37a8d3a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Concurrently -[![Build Status](https://github.com/open-cli-tools/concurrently/workflows/Tests/badge.svg)](https://github.com/open-cli-tools/concurrently/actions?workflow=Tests) +[![Build Status](https://github.com/open-cli-tools/concurrently/workflows/Tests/badge.svg)](https://github.com/open-cli-tools/concurrently/actions?workflow=Tests) [![Coverage Status](https://coveralls.io/repos/github/open-cli-tools/concurrently/badge.svg?branch=master)](https://coveralls.io/github/open-cli-tools/concurrently?branch=master) [![NPM Badge](https://nodei.co/npm/concurrently.png?downloads=true)](https://www.npmjs.com/package/concurrently) @@ -11,6 +11,7 @@ Like `npm run watch-js & npm run watch-less` but better. ![](docs/demo.gif) **Table of contents** + - [Concurrently](#concurrently) - [Why](#why) - [Install](#install) @@ -32,10 +33,10 @@ tired of opening terminals and made **concurrently**. **Features:** -* Cross platform (including Windows) -* Output is easy to follow with prefixes -* With `--kill-others` switch, all commands are killed if one dies -* Spawns commands with [spawn-command](https://github.com/mmalecki/spawn-command) +- Cross platform (including Windows) +- Output is easy to follow with prefixes +- With `--kill-others` switch, all commands are killed if one dies +- Spawns commands with [spawn-command](https://github.com/mmalecki/spawn-command) ## Install @@ -54,6 +55,7 @@ npm install concurrently --save ## Usage Remember to surround separate commands with quotes: + ```bash concurrently "command1 arg" "command2 arg" ``` @@ -230,6 +232,7 @@ For more details, visit https://github.com/open-cli-tools/concurrently ``` ## Programmatic Usage + concurrently can be used programmatically by using the API documented below: ### `concurrently(commands[, options])` @@ -238,34 +241,35 @@ concurrently can be used programmatically by using the API documented below: with the shape `{ command, name, prefixColor, env, cwd }`. - `options` (optional): an object containing any of the below: - - `cwd`: the working directory to be used by all commands. Can be overriden per command. + - `cwd`: the working directory to be used by all commands. Can be overriden per command. Default: `process.cwd()`. - - `defaultInputTarget`: the default input target when reading from `inputStream`. + - `defaultInputTarget`: the default input target when reading from `inputStream`. Default: `0`. - - `handleInput`: when `true`, reads input from `process.stdin`. - - `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams) + - `handleInput`: when `true`, reads input from `process.stdin`. + - `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams) to read the input from. Should only be used in the rare instance you would like to stream anything other than `process.stdin`. Overrides `handleInput`. - - `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)). - - `killOthers`: an array of exitting conditions that will cause a process to kill others. + - `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)). + - `killOthers`: an array of exitting conditions that will cause a process to kill others. Can contain any of `success` or `failure`. - - `maxProcesses`: how many processes should run at once. - - `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams) + - `maxProcesses`: how many processes should run at once. + - `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams) to write logs to. Default: `process.stdout`. - - `prefix`: the prefix type to use when logging processes output. - Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`). - Default: the name of the process, or its index if no name is set. - - `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk). - If concurrently would run more commands than there are colors, the last color is repeated. - Prefix colors specified per-command take precedence over this list. - - `prefixLength`: how many characters to show when prefixing with `command`. Default: `10` - - `raw`: whether raw mode should be used, meaning strictly process output will + - `prefix`: the prefix type to use when logging processes output. + Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`). + Default: the name of the process, or its index if no name is set. + - `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk). + If concurrently would run more commands than there are colors, the last color is repeated. + Prefix colors specified per-command take precedence over this list. + - `colors`: let colours be selected to vary automatically where not explicitly defined + - `prefixLength`: how many characters to show when prefixing with `command`. Default: `10` + - `raw`: whether raw mode should be used, meaning strictly process output will be logged, without any prefixes, colouring or extra stuff. - - `successCondition`: the condition to consider the run was successful. + - `successCondition`: the condition to consider the run was successful. If `first`, only the first process to exit will make up the success of the run; if `last`, the last process that exits will determine whether the run succeeds. Anything else means all processes should exit successfully. - - `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`. - - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`. - - `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format) + - `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`. + - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`. + - `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format) to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ` > Returns: a `Promise` that resolves if the run was successful (according to `successCondition` option), @@ -277,35 +281,41 @@ concurrently can be used programmatically by using the API documented below: Example: ```js -const concurrently = require('concurrently'); -concurrently([ - 'npm:watch-*', - { command: 'nodemon', name: 'server' }, - { command: 'deploy', name: 'deploy', env: { PUBLIC_KEY: '...' } }, - { command: 'watch', name: 'watch', cwd: path.resolve(__dirname, 'scripts/watchers')} -], { - prefix: 'name', - killOthers: ['failure', 'success'], +const concurrently = require("concurrently"); +concurrently( + [ + "npm:watch-*", + { command: "nodemon", name: "server" }, + { command: "deploy", name: "deploy", env: { PUBLIC_KEY: "..." } }, + { + command: "watch", + name: "watch", + cwd: path.resolve(__dirname, "scripts/watchers"), + }, + ], + { + prefix: "name", + killOthers: ["failure", "success"], restartTries: 3, - cwd: path.resolve(__dirname, 'scripts'), -}).then(success, failure); + cwd: path.resolve(__dirname, "scripts"), + } +).then(success, failure); ``` ## FAQ -* Process exited with code *null*? - - From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event: +- Process exited with code _null_? - > This event is emitted after the child process ends. If the process - > terminated normally, code is the final exit code of the process, - > otherwise null. If the process terminated due to receipt of a signal, - > signal is the string name of the signal, otherwise null. + From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event: + > This event is emitted after the child process ends. If the process + > terminated normally, code is the final exit code of the process, + > otherwise null. If the process terminated due to receipt of a signal, + > signal is the string name of the signal, otherwise null. - So *null* means the process didn't terminate normally. This will make **concurrent** - to return non-zero exit code too. + So _null_ means the process didn't terminate normally. This will make **concurrent** + to return non-zero exit code too. -* Does this work with the npm-replacements [yarn](https://github.com/yarnpkg/yarn) or [pnpm](https://pnpm.js.org/)? +- Does this work with the npm-replacements [yarn](https://github.com/yarnpkg/yarn) or [pnpm](https://pnpm.js.org/)? - Yes! In all examples above, you may replace "`npm`" with "`yarn`" or "`pnpm`". + Yes! In all examples above, you may replace "`npm`" with "`yarn`" or "`pnpm`". diff --git a/bin/concurrently.js b/bin/concurrently.js index 9dc31fc6..6c7a7dbb 100755 --- a/bin/concurrently.js +++ b/bin/concurrently.js @@ -97,6 +97,11 @@ const args = yargs default: defaults.prefixColors, type: 'string' }, + 'color': { + describe: 'Automatically adds varying prefix colors where commands do not have a prefix color defined', + default: defaults.color, + type: 'boolean' + }, 'l': { alias: 'prefix-length', describe: @@ -167,6 +172,7 @@ concurrently(args._.map((command, index) => ({ hide: args.hide.split(','), prefix: args.prefix, prefixColors: args.prefixColors.split(','), + color: args.color, prefixLength: args.prefixLength, restartDelay: args.restartAfter, restartTries: args.restartTries, diff --git a/index.js b/index.js index 5abc24f0..2a9ca2c6 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,8 @@ module.exports = exports = (commands, options = {}) => { conditions: options.killOthers }) ], - prefixColors: options.prefixColors || [] + prefixColors: options.prefixColors || [], + color: options.color }); }; diff --git a/src/concurrently.js b/src/concurrently.js index 06b3574e..a7d6a816 100644 --- a/src/concurrently.js +++ b/src/concurrently.js @@ -8,6 +8,7 @@ const ExpandNpmShortcut = require('./command-parser/expand-npm-shortcut'); const ExpandNpmWildcard = require('./command-parser/expand-npm-wildcard'); const CompletionListener = require('./completion-listener'); +const PrefixColorSelector = require('./prefix-color-selector'); const getSpawnOpts = require('./get-spawn-opts'); const Command = require('./command'); @@ -26,19 +27,18 @@ module.exports = (commands, options) => { options = _.defaults(options, defaults); + const prefixColorSelector = new PrefixColorSelector(options); + const commandParsers = [ new StripQuotes(), new ExpandNpmShortcut(), new ExpandNpmWildcard() ]; - let lastColor = ''; commands = _(commands) .map(mapToCommandInfo) .flatMap(command => parseCommand(command, commandParsers)) .map((command, index) => { - // Use documented behaviour of repeating last color when specifying more commands than colors - lastColor = options.prefixColors && options.prefixColors[index] || lastColor; return new Command( Object.assign({ index, @@ -47,7 +47,7 @@ module.exports = (commands, options) => { env: command.env, cwd: command.cwd || options.cwd, }), - prefixColor: lastColor, + prefixColor: prefixColorSelector.getNextColor(index), killProcess: options.kill, spawn: options.spawn, }, command) diff --git a/src/defaults.js b/src/defaults.js index 695c0dd1..7ee6d972 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -29,5 +29,7 @@ module.exports = { // Refer to https://date-fns.org/v2.0.1/docs/format timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS', // Current working dir passed as option to spawn command. Default: process.cwd() - cwd: undefined + cwd: undefined, + // Adding prefix colors automatically + color: false, }; diff --git a/src/prefix-color-selector.js b/src/prefix-color-selector.js new file mode 100644 index 00000000..44dab570 --- /dev/null +++ b/src/prefix-color-selector.js @@ -0,0 +1,69 @@ +const chalk = require('chalk'); + +module.exports = class PrefixColorSelector { + get ACCEPTABLE_CONSOLE_COLORS() { + // colors picked randomly, can be amended if required + return [ + chalk.cyan, + chalk.yellow, + chalk.magenta, + chalk.grey, + chalk.bgBlueBright, + chalk.bgMagenta, + chalk.magentaBright, + chalk.bgBlack, + chalk.bgWhite, + chalk.bgCyan, + chalk.bgGreen, + chalk.bgYellow, + chalk.bgRed, + chalk.bgGreenBright, + chalk.bgGrey, + chalk.blueBright, + ] + // filter out duplicates + .filter((chalkColor, index, arr) => { + return arr.indexOf( chalkColor ) === index; + }) + .map(chalkColor => chalkColor.bold); + } + + constructor(options) { + this.userDefinedPrefixColors = options.prefixColors; + this.canAutoSelectColors = options.color; + } + + getNextColor(index) { + const cannotSelectColor = !this.userDefinedPrefixColors && !this.canAutoSelectColors; + if (cannotSelectColor) { return ''; } + + const userDefinedColorForCurrentCommand = this.userDefinedPrefixColors && typeof index === 'number' && this.userDefinedPrefixColors[index]; + + if (!this.canAutoSelectColors) { + // original behaviour + // Use documented behaviour of repeating last color when specifying more commands than colors + this.lastColor = userDefinedColorForCurrentCommand || this.lastColor; + return this.lastColor; + } + + // user preference takes priority if defined + if (userDefinedColorForCurrentCommand) { + this.lastColor = userDefinedColorForCurrentCommand; + return userDefinedColorForCurrentCommand; + } + + // auto selection requested and no user preference defined, select next auto color + if (!this.autoColors || !this.autoColors.length) { this.refillAutoColors(); } + + // prevent consecutive colors from being the same (ie when transitioning from user colours to auto colours) + const nextColor = this.autoColors.shift(); + + this.lastColor = nextColor !== this.lastColor ? nextColor : this.getNextColor(); + return this.lastColor; + } + + refillAutoColors() { + // make sure auto colors are not empty after refill + this.autoColors = [...this.ACCEPTABLE_CONSOLE_COLORS]; + } +}; diff --git a/src/prefix-color-selector.spec.js b/src/prefix-color-selector.spec.js new file mode 100644 index 00000000..c2404be1 --- /dev/null +++ b/src/prefix-color-selector.spec.js @@ -0,0 +1,92 @@ + +const PrefixColorSelector = require('./prefix-color-selector'); + +let prefixColorSelector; + +it('does not produce a color if it should not', () => { + const config = { + prefixColors: false, + color: false + }; + prefixColorSelector = new PrefixColorSelector(config); + + let selectedColor = prefixColorSelector.getNextColor(0); + expect(selectedColor).toBe(''); + selectedColor = prefixColorSelector.getNextColor(1); + expect(selectedColor).toBe(''); + selectedColor = prefixColorSelector.getNextColor(2); + expect(selectedColor).toBe(''); +}); + +it('uses user defined prefix colors only if not allowed to use auto colors', () => { + const config = { + prefixColors: ['red', 'green', 'blue'], + color: false + }; + prefixColorSelector = new PrefixColorSelector(config); + + let selectedColor = prefixColorSelector.getNextColor(0); + expect(selectedColor).toBe('red'); + selectedColor = prefixColorSelector.getNextColor(1); + expect(selectedColor).toBe('green'); + selectedColor = prefixColorSelector.getNextColor(2); + expect(selectedColor).toBe('blue'); + + // uses last color if no more user defined colors + selectedColor = prefixColorSelector.getNextColor(3); + expect(selectedColor).toBe('blue'); + selectedColor = prefixColorSelector.getNextColor(4); + expect(selectedColor).toBe('blue'); +}); + +it('uses user defined colors then recurring auto colors without repeating consecutive colors', () => { + const config = { + prefixColors: ['red', 'green'], + color: true + }; + + prefixColorSelector = new PrefixColorSelector(config); + + jest.spyOn(prefixColorSelector, 'ACCEPTABLE_CONSOLE_COLORS', 'get') + .mockReturnValue(['green', 'blue']); + + let selectedColor = prefixColorSelector.getNextColor(0); + expect(selectedColor).toBe('red'); + selectedColor = prefixColorSelector.getNextColor(1); + expect(selectedColor).toBe('green'); + + // auto colors now, does not repeat last user color of green + selectedColor = prefixColorSelector.getNextColor(2); + expect(selectedColor).toBe('blue'); + + selectedColor = prefixColorSelector.getNextColor(3); + expect(selectedColor).toBe('green'); + + selectedColor = prefixColorSelector.getNextColor(4); + expect(selectedColor).toBe('blue'); + +}); + +it('has more than 1 auto color defined', () => { + prefixColorSelector = new PrefixColorSelector({}); + // ! code assumes this always has more than one entry, so make sure + expect(prefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length).toBeGreaterThan(1); +}); + +it('can use only auto colors and does not repeat consecutive colors', () => { + const config = { + prefixColors: [], + color: true + }; + + prefixColorSelector = new PrefixColorSelector(config); + + let previousColor; + let selectedColor ; + Array(prefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length * 2) + .forEach((_, index) => { + previousColor = selectedColor; + selectedColor = prefixColorSelector.getNextColor(index); + expect(selectedColor).not.toBe(previousColor); + }); +}); \ No newline at end of file