255 lines
6.1 KiB
TypeScript
255 lines
6.1 KiB
TypeScript
import { exec, execSync } from 'child_process';
|
|
import * as yargsParser from 'yargs-parser';
|
|
|
|
export const LARGE_BUFFER = 1024 * 1000000;
|
|
|
|
function loadEnvVars(path?: string) {
|
|
if (path) {
|
|
const result = require('dotenv').config({ path });
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
} else {
|
|
try {
|
|
require('dotenv').config();
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
export type Json = { [k: string]: any };
|
|
export interface RunCommandsBuilderOptions extends Json {
|
|
command?: string;
|
|
commands?: (
|
|
| {
|
|
command: string;
|
|
forwardAllArgs?: boolean;
|
|
}
|
|
| string
|
|
)[];
|
|
color?: boolean;
|
|
parallel?: boolean;
|
|
readyWhen?: string;
|
|
cwd?: string;
|
|
args?: string;
|
|
envFile?: string;
|
|
outputPath?: string;
|
|
}
|
|
|
|
const propKeys = [
|
|
'command',
|
|
'commands',
|
|
'color',
|
|
'parallel',
|
|
'readyWhen',
|
|
'cwd',
|
|
'args',
|
|
'envFile',
|
|
'outputPath',
|
|
];
|
|
|
|
export interface NormalizedRunCommandsBuilderOptions
|
|
extends RunCommandsBuilderOptions {
|
|
commands: {
|
|
command: string;
|
|
forwardAllArgs?: boolean;
|
|
}[];
|
|
parsedArgs: { [k: string]: any };
|
|
}
|
|
|
|
export default async function (
|
|
options: RunCommandsBuilderOptions
|
|
): Promise<{ success: boolean }> {
|
|
// Special handling of extra options coming through Angular CLI
|
|
if (options['--']) {
|
|
const { _, ...overrides } = yargsParser(options['--'] as string[], {
|
|
configuration: { 'camel-case-expansion': false },
|
|
});
|
|
options = { ...options, ...overrides };
|
|
delete options['--'];
|
|
}
|
|
|
|
loadEnvVars(options.envFile);
|
|
const normalized = normalizeOptions(options);
|
|
|
|
if (options.readyWhen && !options.parallel) {
|
|
throw new Error(
|
|
'ERROR: Bad builder config for @nrwl/run-commands - "readyWhen" can only be used when parallel=true'
|
|
);
|
|
}
|
|
|
|
try {
|
|
const success = options.parallel
|
|
? await runInParallel(normalized)
|
|
: await runSerially(normalized);
|
|
return { success };
|
|
} catch (e) {
|
|
throw new Error(
|
|
`ERROR: Something went wrong in @nrwl/run-commands - ${e.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function runInParallel(options: NormalizedRunCommandsBuilderOptions) {
|
|
const procs = options.commands.map((c) =>
|
|
createProcess(
|
|
c.command,
|
|
options.readyWhen,
|
|
options.color,
|
|
options.cwd
|
|
).then((result) => ({
|
|
result,
|
|
command: c.command,
|
|
}))
|
|
);
|
|
|
|
if (options.readyWhen) {
|
|
const r = await Promise.race(procs);
|
|
if (!r.result) {
|
|
process.stderr.write(
|
|
`Warning: @nrwl/run-commands 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-commands command "${f.command}" exited with non-zero status code`
|
|
);
|
|
});
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeOptions(
|
|
options: RunCommandsBuilderOptions
|
|
): NormalizedRunCommandsBuilderOptions {
|
|
options.parsedArgs = parseArgs(options);
|
|
|
|
if (options.command) {
|
|
options.commands = [{ command: options.command }];
|
|
options.parallel = false;
|
|
} else {
|
|
options.commands = options.commands.map((c) =>
|
|
typeof c === 'string' ? { command: c } : c
|
|
);
|
|
}
|
|
(options as NormalizedRunCommandsBuilderOptions).commands.forEach((c) => {
|
|
c.command = transformCommand(
|
|
c.command,
|
|
(options as NormalizedRunCommandsBuilderOptions).parsedArgs,
|
|
c.forwardAllArgs ?? true
|
|
);
|
|
});
|
|
return options as any;
|
|
}
|
|
|
|
async function runSerially(options: NormalizedRunCommandsBuilderOptions) {
|
|
for (const c of options.commands) {
|
|
createSyncProcess(c.command, options.color, options.cwd);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function createProcess(
|
|
command: string,
|
|
readyWhen: string,
|
|
color: boolean,
|
|
cwd: string
|
|
): Promise<boolean> {
|
|
return new Promise((res) => {
|
|
const childProcess = exec(command, {
|
|
maxBuffer: LARGE_BUFFER,
|
|
env: processEnv(color),
|
|
cwd,
|
|
});
|
|
/**
|
|
* 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);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function createSyncProcess(command: string, color: boolean, cwd: string) {
|
|
execSync(command, {
|
|
env: processEnv(color),
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
cwd,
|
|
});
|
|
}
|
|
|
|
function processEnv(color: boolean) {
|
|
const env = { ...process.env };
|
|
if (color) {
|
|
env.FORCE_COLOR = `${color}`;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
function transformCommand(
|
|
command: string,
|
|
args: { [key: string]: string },
|
|
forwardAllArgs: boolean
|
|
) {
|
|
if (command.indexOf('{args.') > -1) {
|
|
const regex = /{args\.([^}]+)}/g;
|
|
return command.replace(regex, (_, group: string) => args[group]);
|
|
} else if (Object.keys(args).length > 0 && forwardAllArgs) {
|
|
const stringifiedArgs = Object.keys(args)
|
|
.map((a) => `--${a}=${args[a]}`)
|
|
.join(' ');
|
|
return `${command} ${stringifiedArgs}`;
|
|
} else {
|
|
return command;
|
|
}
|
|
}
|
|
|
|
function parseArgs(options: RunCommandsBuilderOptions) {
|
|
const args = options.args;
|
|
if (!args) {
|
|
const unknownOptionsTreatedAsArgs = Object.keys(options)
|
|
.filter((p) => propKeys.indexOf(p) === -1)
|
|
.reduce((m, c) => ((m[c] = options[c]), m), {});
|
|
return unknownOptionsTreatedAsArgs;
|
|
}
|
|
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;
|
|
}, {});
|
|
}
|