import { Builder, BuilderConfiguration, BuildEvent } from '@angular-devkit/architect'; import { Observable } from 'rxjs'; import { exec } from 'child_process'; try { require('dotenv').config(); } catch (e) {} export interface RunCommandsBuilderOptions { commands: { command: string }[]; parallel?: boolean; readyWhen?: string; args?: string; parsedArgs?: { [k: string]: string }; } export default class RunCommandsBuilder implements Builder { run( config: BuilderConfiguration ): Observable { config.options.parsedArgs = { ...(config.options as any), ...this.parseArgs(config.options.args) }; return Observable.create(async observer => { if (!config || !config.options || !config.options.commands) { observer.error( 'ERROR: Bad builder config for @nrwl/run-command - "commands" option is required' ); return; } if (config.options.readyWhen && !config.options.parallel) { observer.error( 'ERROR: Bad builder config for @nrwl/run-command - "readyWhen" can only be used when parallel=true' ); return; } if (config.options.commands.some(c => !c.command)) { observer.error( 'ERROR: Bad builder config for @nrwl/run-command - "command" option is required' ); return; } try { const success = config.options.parallel ? await this.runInParallel(config) : await this.runSerially(config); observer.next({ success }); observer.complete(); } catch (e) { observer.error( `ERROR: Something went wrong in @nrwl/run-command - ${e.message}` ); } }); } private async runInParallel( config: BuilderConfiguration ) { const procs = config.options.commands.map(c => this.createProcess( this.transformCommand(c.command, config.options.parsedArgs), config.options.readyWhen ).then(result => ({ result, command: c.command })) ); if (config.options.readyWhen) { const r = await Promise.race(procs); if (!r.result) { process.stderr.write( `Warning: @nrwl/run-command command "${ r.command }" exited with non-zero status code` ); return false; } else { return true; } } else { const r = await Promise.all(procs); const failed = r.filter(v => !v.result); if (failed.length > 0) { failed.forEach(f => { process.stderr.write( `Warning: @nrwl/run-command command "${ f.command }" exited with non-zero status code` ); }); return false; } else { return true; } } } private async runSerially( config: BuilderConfiguration ) { const failedCommand = await config.options.commands.reduce< Promise >(async (m, c) => { if ((await m) === null) { const success = await this.createProcess( this.transformCommand(c.command, config.options.parsedArgs), config.options.readyWhen ); return !success ? c.command : null; } else { return m; } }, Promise.resolve(null)); if (failedCommand) { process.stderr.write( `Warning: @nrwl/run-command command "${failedCommand}" exited with non-zero status code` ); return false; } return true; } private createProcess(command: string, readyWhen: string): Promise { return new Promise(res => { const TEN_MEGABYTES = 1024 * 10000; const childProcess = exec(command, { maxBuffer: TEN_MEGABYTES }); /** * Ensure the child process is killed when the parent exits */ process.on('exit', () => childProcess.kill()); childProcess.stdout.on('data', data => { process.stdout.write(data); if (readyWhen && data.toString().indexOf(readyWhen) > -1) { res(true); } }); childProcess.stderr.on('data', err => { process.stderr.write(err); if (readyWhen && err.toString().indexOf(readyWhen) > -1) { res(true); } }); childProcess.on('close', code => { if (!readyWhen) { res(code === 0); } }); }); } private transformCommand(command: string, args: any) { const regex = /{args\.([^}]+)}/g; return command.replace(regex, (_, group: string) => args[group]); } private parseArgs(args: string) { if (!args) { return {}; } return args .split(' ') .map(t => t.trim()) .reduce((m, c) => { if (!c.startsWith('--')) { throw new Error(`Invalid args: ${args}`); } const [key, value] = c.substring(2).split('='); if (!key || !value) { throw new Error(`Invalid args: ${args}`); } m[key] = value; return m; }, {}); } }