UNPKG

@atlaskit/build-utils

Version:

Collection of utilities to used during the release process of Atlaskit

183 lines (171 loc) 5.54 kB
import concurrently from 'concurrently'; import stream from 'stream'; import stripAnsi from 'strip-ansi'; export type Options = concurrently.Options & { // cd to cwd before executing the command cwd?: string; // Strips ansi codes from command output stripAnsi?: boolean; // A prefix that prepends to each line of output rather than each chunk linePrefix?: string; // Execute commands sequentially rather than in parallel sequential?: boolean; // Function that measures success of the first watch output, triggers onWatchSuccess watchFirstSuccessCondition?: (output: string) => boolean; // Function that measures success of watch output, triggers onWatchSuccess watchSuccessCondition?: (output: string) => boolean; // Triggered when a successful watch compilation has finished, triggered by either // watchFirstSuccessCondition or watchSuccessCondition onWatchSuccess?: (result: { firstSuccess: boolean }) => void; }; /** * Simple proxy of process.stdout that we can use to observe spawned concurrently process output. * It's not possible to listen to process.stdout directly - https://github.com/nodejs/node/issues/8033 */ class StdoutProxy extends stream.Writable { stripAnsi?: boolean = false; linePrefix?: string; constructor( customOpts: Pick<Options, 'linePrefix' | 'stripAnsi'> = {}, opts?: stream.WritableOptions, ) { super(opts); if (customOpts.linePrefix) { this.linePrefix = customOpts.linePrefix; } if (customOpts.stripAnsi) { this.stripAnsi = customOpts.stripAnsi; } } _write(chunk: any, _: string, callback: () => void): void { const output: string = chunk.toString().trimEnd('\n'); if (output !== '') { this.log(output); } this.emit('data', output); callback(); } log(output: string) { let finalOutput = output; if (this.stripAnsi) { finalOutput = stripAnsi(output); } if (this.linePrefix) { const lines = finalOutput.split('\n').filter(l => l !== ''); lines.forEach(line => { console.log(this.linePrefix, line); }); } else { console.log(finalOutput); } } } const defaultOpts: Pick< concurrently.Options, 'killOthers' | 'prefix' | 'raw' | 'outputStream' > = { // Will kill other processes when one fails killOthers: ['failure'], // Opt out of default logging prefix of index/name - bolt does most of this for us already prefix: 'none', // Raw mode will strictly output only raw output, rather than extra stuff // that concurrently outputs. We enable the extra output for now. raw: false, }; /** * Parses process stdout for the provided success conditions and triggers `onWatchSuccess` callback on success */ function listenForSuccess({ stdoutProxy, watchFirstSuccessCondition, watchSuccessCondition, onWatchSuccess, }: { stdoutProxy: StdoutProxy; watchFirstSuccessCondition?: (output: string) => boolean; watchSuccessCondition?: (output: string) => boolean; onWatchSuccess?: (args: { firstSuccess: boolean }) => any; }): () => void { let hasSucceededOnce = !watchFirstSuccessCondition; if ( onWatchSuccess && !(watchFirstSuccessCondition || watchSuccessCondition) ) { // eslint-disable-next-line no-throw-literal throw 'Must provide either watchSuccessCondition or watchFirstSuccessCondition with onWatchSuccess'; } else if (!onWatchSuccess) { return () => {}; } const listener = (output: string) => { let successful = false; if (watchSuccessCondition && hasSucceededOnce) { successful = watchSuccessCondition(output); } else if (!hasSucceededOnce && watchFirstSuccessCondition) { successful = watchFirstSuccessCondition(output); } if (successful) { onWatchSuccess({ firstSuccess: !hasSucceededOnce }); hasSucceededOnce = true; } }; stdoutProxy.on('data', listener); return () => { stdoutProxy.removeListener('data', listener); }; } export default async function runCommands( commands: string[], opts: Options = {}, ): Promise<any> { const { stripAnsi, sequential, watchFirstSuccessCondition, watchSuccessCondition, onWatchSuccess, linePrefix, cwd, ...concurrentlyOpts } = opts; if (commands.length === 0) { return; } const stdoutProxy = new StdoutProxy({ linePrefix, stripAnsi }); if (commands.length === 1 || !sequential) { const unsubscribe = listenForSuccess({ stdoutProxy, watchFirstSuccessCondition, watchSuccessCondition, onWatchSuccess, }); let result; try { const mappedCommands = commands.map(c => cwd ? `cd "${cwd}" && ${c}` : c, ); result = await concurrently(mappedCommands, { ...defaultOpts, ...concurrentlyOpts, // Set output stream to our stdoutProxy rather than stdout so we can listen to stdout. // Used as part of watch mode to identify when recompiles are finished outputStream: stdoutProxy, }); } catch (e) { if (e.constructor === Error) { // Hide internal concurrently stack trace as it does not provide anything useful // See https://github.com/kimmobrunfeldt/concurrently/issues/181 throw Error('Command failed'); } // Other types of errors should be thrown as-is throw e; } unsubscribe(); return result; } else { let result; for (const command of commands) { result = await runCommands([command], opts); } return result; } }