nx/packages/workspace/src/builders/run-commands/run-commands.builder.ts

189 lines
5.0 KiB
TypeScript

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<RunCommandsBuilderOptions> {
run(
config: BuilderConfiguration<RunCommandsBuilderOptions>
): Observable<BuildEvent> {
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<RunCommandsBuilderOptions>
) {
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<RunCommandsBuilderOptions>
) {
const failedCommand = await config.options.commands.reduce<
Promise<string | null>
>(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<boolean> {
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;
}, {});
}
}