189 lines
5.0 KiB
TypeScript
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;
|
|
}, {});
|
|
}
|
|
}
|