@atlaskit/build-utils
Version:
Collection of utilities to used during the release process of Atlaskit
183 lines (171 loc) • 5.54 kB
text/typescript
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;
}
}