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 { 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; }, {}); }