diff --git a/src/command.ts b/src/command.ts index 4d54d1e8..9c76e66b 100644 --- a/src/command.ts +++ b/src/command.ts @@ -2,20 +2,50 @@ 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, + + /** + * The current working directory of the process when spawned. + */ cwd?: string, 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, @@ -29,8 +59,19 @@ export interface TimerEvent { endDate?: Date; } +/** + * Subtype of NodeJS's child_process including only what's actually needed for a command to work. + */ export type ChildProcess = EventEmitter & Pick; -export type KillProcess = (pid: number, signal?: string | number) => void; + +/** + * 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 { @@ -87,6 +128,9 @@ export class Command implements CommandInfo { 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; @@ -125,7 +169,10 @@ export class Command implements CommandInfo { this.stdin = child.stdin; } - kill(code?: string | number) { + /** + * 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); @@ -133,6 +180,9 @@ export class Command implements CommandInfo { } }; +/** + * Pipes all events emitted by `stream` into `subject`. + */ function pipeTo(stream: Rx.Observable, subject: Rx.Subject) { stream.subscribe(event => subject.next(event)); } diff --git a/src/completion-listener.ts b/src/completion-listener.ts index d4960d50..27cb4460 100644 --- a/src/completion-listener.ts +++ b/src/completion-listener.ts @@ -1,15 +1,35 @@ import * as Rx from 'rxjs'; import { bufferCount, switchMap, take } from 'rxjs/operators'; -import { CloseEvent, Command } from './command'; +import { 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. + */ export type SuccessCondition = 'first' | 'last' | 'all'; +/** + * 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; @@ -31,6 +51,11 @@ export class CompletionListener { } } + /** + * 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 { const closeStreams = commands.map(command => command.close); return Rx.merge(...closeStreams) diff --git a/src/concurrently.ts b/src/concurrently.ts index 260e6188..f741c38d 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -22,21 +22,74 @@ const defaults: ConcurrentlyOptions = { cwd: undefined, }; +/** + * A command that is to be passed into `concurrently()`. + * If value is a string, then that's the command's command line. + * Fine grained options can be defined by using the object format. + */ export type ConcurrentlyCommandInput = string | Partial; + export type ConcurrentlyOptions = { logger?: Logger, + + /** + * Which stream should the commands output be written to. + */ outputStream?: Writable, group?: boolean, prefixColors?: string[], + + /** + * Maximum number of commands to run at once. + * + * If undefined, then all processes will start in parallel. + * Setting this value to 1 will achieve sequential running. + */ maxProcesses?: number, + + /** + * Whether commands should be spawned in raw mode. + * Defaults to false. + */ raw?: boolean, + + /** + * The current working directory of commands which didn't specify one. + * Defaults to `process.cwd()`. + */ cwd?: string, + + /** + * @see CompletionListener + */ successCondition?: SuccessCondition, + + /** + * Which flow controllers should be applied on commands spawned by concurrently. + * Defaults to an empty array. + */ controllers: FlowController[], + + /** + * A function that will spawn commands. + * Defaults to the `spawn-command` module. + */ spawn: SpawnCommand, + + /** + * A function that will kill processes. + * Defaults to the `tree-kill` module. + */ kill: KillProcess, }; +/** + * Core concurrently functionality -- spawns the given commands concurrently and + * returns a promise that will await for them to finish. + * + * @see CompletionListener + * @returns A promise that resolves when the commands ran successfully, or rejects otherwise. + */ export function concurrently(baseCommands: ConcurrentlyCommandInput[], baseOptions?: Partial) { assert.ok(Array.isArray(baseCommands), '[concurrently] commands should be an array'); assert.notStrictEqual(baseCommands.length, 0, '[concurrently] no commands provided'); diff --git a/src/flow-control/flow-controller.ts b/src/flow-control/flow-controller.ts index d7c6c306..4ba9411b 100644 --- a/src/flow-control/flow-controller.ts +++ b/src/flow-control/flow-controller.ts @@ -1,5 +1,11 @@ import { Command } from '../command'; +/** + * Interface for a class that controls and/or watches the behavior of commands. + * + * This may include logging their output, creating interactions between them, or changing when they + * actually finish. + */ export interface FlowController { handle(commands: Command[]): { commands: Command[], onFinish?: () => void }; } diff --git a/src/flow-control/input-handler.ts b/src/flow-control/input-handler.ts index 6fe73db0..6383e360 100644 --- a/src/flow-control/input-handler.ts +++ b/src/flow-control/input-handler.ts @@ -6,6 +6,15 @@ import * as defaults from '../defaults'; import { Logger } from '../logger'; import { FlowController } from './flow-controller'; +/** + * Sends input from concurrently through to commands. + * + * Input can start with a command identifier, in which case it will be sent to that specific command. + * For instance, `0:bla` will send `bla` to command at index `0`, and `server:stop` will send `stop` + * to command with name `server`. + * + * If the input doesn't start with a command identifier, it is then always sent to the default target. + */ export class InputHandler implements FlowController { private readonly logger: Logger; private readonly defaultInputTarget: CommandIdentifier; diff --git a/src/flow-control/kill-on-signal.ts b/src/flow-control/kill-on-signal.ts index 507dc411..5471fa4a 100644 --- a/src/flow-control/kill-on-signal.ts +++ b/src/flow-control/kill-on-signal.ts @@ -3,6 +3,10 @@ import { map } from 'rxjs/operators'; import { Command } from '../command'; import { FlowController } from './flow-controller'; +/** + * Watches the main concurrently process for signals and sends the same signal down to each spawned + * command. + */ export class KillOnSignal implements FlowController { private readonly process: EventEmitter; diff --git a/src/flow-control/kill-others.ts b/src/flow-control/kill-others.ts index 27660078..cbe6d025 100644 --- a/src/flow-control/kill-others.ts +++ b/src/flow-control/kill-others.ts @@ -7,6 +7,9 @@ import { filter, map } from 'rxjs/operators'; export type ProcessCloseCondition = 'failure' | 'success'; +/** + * Sends a SIGTERM signal to all commands when one of the exits with a matching condition. + */ export class KillOthers implements FlowController { private readonly logger: Logger; private readonly conditions: ProcessCloseCondition[]; diff --git a/src/flow-control/log-error.ts b/src/flow-control/log-error.ts index f9296e7a..b31772f0 100644 --- a/src/flow-control/log-error.ts +++ b/src/flow-control/log-error.ts @@ -2,6 +2,9 @@ import { Command } from '../command'; import { Logger } from '../logger'; import { FlowController } from './flow-controller'; +/** + * Logs when commands failed executing, e.g. due to the executable not existing in the system. + */ export class LogError implements FlowController { private readonly logger: Logger; diff --git a/src/flow-control/log-exit.ts b/src/flow-control/log-exit.ts index f789900c..32bc31f5 100644 --- a/src/flow-control/log-exit.ts +++ b/src/flow-control/log-exit.ts @@ -2,6 +2,9 @@ import { Command } from '../command'; import { Logger } from '../logger'; import { FlowController } from './flow-controller'; +/** + * Logs the exit code/signal of commands. + */ export class LogExit implements FlowController { private readonly logger: Logger; diff --git a/src/flow-control/log-output.ts b/src/flow-control/log-output.ts index bafe6b0d..e7bb9d26 100644 --- a/src/flow-control/log-output.ts +++ b/src/flow-control/log-output.ts @@ -2,6 +2,9 @@ import { Command } from '../command'; import { Logger } from '../logger'; import { FlowController } from './flow-controller'; +/** + * Logs the stdout and stderr output of commands. + */ export class LogOutput implements FlowController { private readonly logger: Logger; constructor({ logger }: { logger: Logger }) { diff --git a/src/flow-control/log-timings.ts b/src/flow-control/log-timings.ts index bab792c4..e440bda6 100644 --- a/src/flow-control/log-timings.ts +++ b/src/flow-control/log-timings.ts @@ -14,6 +14,10 @@ interface TimingInfo { killed: boolean, command: string, } + +/** + * Logs timing information about commands as they start/stop and then a summary when all commands finish. + */ export class LogTimings implements FlowController { static mapCloseEventToTimingInfo({ command, timings, killed, exitCode }: CloseEvent): TimingInfo { const readableDurationMs = (timings.endDate.getTime() - timings.startDate.getTime()).toLocaleString(); diff --git a/src/flow-control/restart-process.ts b/src/flow-control/restart-process.ts index 16479580..63ad2b11 100644 --- a/src/flow-control/restart-process.ts +++ b/src/flow-control/restart-process.ts @@ -5,6 +5,9 @@ import * as defaults from '../defaults'; import { Logger } from '../logger'; import { FlowController } from './flow-controller'; +/** + * Restarts commands that fail up to a defined number of times. + */ export class RestartProcess implements FlowController { private readonly logger: Logger; private readonly scheduler?: Rx.SchedulerLike; diff --git a/src/index.ts b/src/index.ts index f0926b8e..ca13be25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,31 @@ import { Logger } from './logger'; export type ConcurrentlyOptions = BaseConcurrentlyOptions & { // Logger options + /** + * Which command(s) should have their output hidden. + */ hide?: CommandIdentifier | CommandIdentifier[], + + /** + * The prefix format to use when logging a command's output. + * Defaults to the command's index. + */ prefix?: string, + + /** + * How many characters should a prefix have at most, used when the prefix format is `command`. + */ prefixLength?: number, + + /** + * Whether output should be formatted to include prefixes and whether "event" logs will be logged. + */ raw?: boolean, + + /** + * Date format used when logging date/time. + * @see https://date-fns.org/v2.0.1/docs/format + */ timestampFormat?: string, // Input handling options @@ -27,13 +48,34 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & { pauseInputStreamOnFinish?: boolean, // Restarting options + /** + * How much time in milliseconds to wait before restarting a command. + * + * @see RestartProcess + */ restartDelay?: number, + + /** + * How many times commands should be restarted when they exit with a failure. + * + * @see RestartProcess + */ restartTries?: number, // Process killing options + /** + * Under which condition(s) should other commands be killed when the first one exits. + * + * @see KillOthers + */ killOthers?: ProcessCloseCondition | ProcessCloseCondition[], // Timing options + /** + * Whether to output timing information for processes. + * + * @see LogTimings + */ timings?: boolean, }; diff --git a/src/logger.ts b/src/logger.ts index c5993632..9e79efb5 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -12,14 +12,46 @@ export class Logger { private readonly prefixFormat?: string; private readonly prefixLength: number; private readonly timestampFormat: string; + + /** + * Last character emitted. + * If `undefined`, then nothing has been logged yet. + */ private lastChar?: string; + + /** + * Observable that emits when there's been output logged. + * If `command` is is `undefined`, then the log is for a global event. + */ readonly output = new Rx.Subject<{ command: Command | undefined, text: string }>(); constructor({ hide, prefixFormat, prefixLength, raw = false, timestampFormat }: { + /** + * Which command(s) should have their output hidden. + */ hide?: CommandIdentifier | CommandIdentifier[], + + /** + * Whether output should be formatted to include prefixes and whether "event" logs will be + * logged. + */ raw?: boolean, + + /** + * The prefix format to use when logging a command's output. + * Defaults to the command's index. + */ prefixFormat?: string, + + /** + * How many characters should a prefix have at most, used when the prefix format is `command`. + */ prefixLength?: number, + + /** + * Date format used when logging date/time. + * @see https://date-fns.org/v2.0.1/docs/format + */ timestampFormat?: string, }) { // To avoid empty strings from hiding the output of commands that don't have a name, @@ -85,6 +117,11 @@ export class Logger { return color(text); } + /** + * Logs an event for a command (e.g. start, stop). + * + * If raw mode is on, then nothing is logged. + */ logCommandEvent(text: string, command: Command) { if (this.raw) { return; @@ -102,6 +139,11 @@ export class Logger { return this.log(prefix + (prefix ? ' ' : ''), text, command); } + /** + * Logs a global event (e.g. sending signals to processes). + * + * If raw mode is on, then nothing is logged. + */ logGlobalEvent(text: string) { if (this.raw) { return; @@ -110,6 +152,11 @@ export class Logger { this.log(chalk.reset('-->') + ' ', chalk.reset(text) + '\n'); } + /** + * Logs a table from an input object array, like `console.table`. + * + * Each row is a single input item, and they are presented in the input order. + */ logTable(tableContents: any[]) { // For now, can only print array tables with some content. if (this.raw || !Array.isArray(tableContents) || !tableContents.length) { diff --git a/src/output-writer.ts b/src/output-writer.ts index 9a70ccec..ad71adcd 100644 --- a/src/output-writer.ts +++ b/src/output-writer.ts @@ -2,6 +2,9 @@ import { Writable } from 'stream'; import { Command } from './command'; import * as Rx from 'rxjs'; +/** + * Class responsible for actually writing output onto a writable stream. + */ export class OutputWriter { private readonly outputStream: Writable; private readonly group: boolean;