nx
Version:
488 lines (487 loc) • 17.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.LARGE_BUFFER = void 0;
exports.default = default_1;
exports.interpolateArgsIntoCommand = interpolateArgsIntoCommand;
const child_process_1 = require("child_process");
const path = require("path");
const yargsParser = require("yargs-parser");
const npm_run_path_1 = require("npm-run-path");
const chalk = require("chalk");
const pseudo_terminal_1 = require("../../tasks-runner/pseudo-terminal");
const exit_codes_1 = require("../../utils/exit-codes");
const task_env_1 = require("../../tasks-runner/task-env");
exports.LARGE_BUFFER = 1024 * 1000000;
let pseudoTerminal;
const childProcesses = new Set();
function loadEnvVarsFile(path, env = {}) {
(0, task_env_1.unloadDotEnvFile)(path, env);
const result = (0, task_env_1.loadAndExpandDotEnvFile)(path, env);
if (result.error) {
throw result.error;
}
}
const propKeys = [
'command',
'commands',
'color',
'no-color',
'parallel',
'no-parallel',
'readyWhen',
'cwd',
'args',
'envFile',
'__unparsed__',
'env',
'usePty',
'streamOutput',
'verbose',
'forwardAllArgs',
'tty',
];
async function default_1(options, context) {
registerProcessListener();
const normalized = normalizeOptions(options);
if (normalized.readyWhenStatus.length && !normalized.parallel) {
throw new Error('ERROR: Bad executor config for run-commands - "readyWhen" can only be used when "parallel=true".');
}
if (options.commands.find((c) => c.prefix || c.prefixColor || c.color || c.bgColor) &&
!options.parallel) {
throw new Error('ERROR: Bad executor config for run-commands - "prefix", "prefixColor", "color" and "bgColor" can only be set when "parallel=true".');
}
try {
const result = options.parallel
? await runInParallel(normalized, context)
: await runSerially(normalized, context);
return result;
}
catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.error(e);
}
throw new Error(`ERROR: Something went wrong in run-commands - ${e.message}`);
}
}
async function runInParallel(options, context) {
const procs = options.commands.map((c) => createProcess(null, c, options.readyWhenStatus, options.color, calculateCwd(options.cwd, context), options.env ?? {}, true, options.usePty, options.streamOutput, options.tty, options.envFile).then((result) => ({
result,
command: c.command,
})));
let terminalOutput = '';
if (options.readyWhenStatus.length) {
const r = await Promise.race(procs);
terminalOutput += r.result.terminalOutput;
if (!r.result.success) {
const output = `Warning: command "${r.command}" exited with non-zero status code`;
terminalOutput += output;
if (options.streamOutput) {
process.stderr.write(output);
}
return { success: false, terminalOutput };
}
else {
return { success: true, terminalOutput };
}
}
else {
const r = await Promise.all(procs);
terminalOutput += r.map((f) => f.result.terminalOutput).join('');
const failed = r.filter((v) => !v.result.success);
if (failed.length > 0) {
const output = failed
.map((f) => `Warning: command "${f.command}" exited with non-zero status code`)
.join('\r\n');
terminalOutput += output;
if (options.streamOutput) {
process.stderr.write(output);
}
return {
success: false,
terminalOutput,
};
}
else {
return {
success: true,
terminalOutput,
};
}
}
}
function normalizeOptions(options) {
if (options.readyWhen && typeof options.readyWhen === 'string') {
options.readyWhenStatus = [
{ stringToMatch: options.readyWhen, found: false },
];
}
else {
options.readyWhenStatus =
options.readyWhen?.map((stringToMatch) => ({
stringToMatch,
found: false,
})) ?? [];
}
if (options.command) {
options.commands = [
{
command: Array.isArray(options.command)
? options.command.join(' ')
: options.command,
},
];
options.parallel = options.readyWhenStatus?.length > 0;
}
else {
options.commands = options.commands.map((c) => typeof c === 'string' ? { command: c } : c);
}
if (options.args && Array.isArray(options.args)) {
options.args = options.args.join(' ');
}
const unparsedCommandArgs = yargsParser(options.__unparsed__, {
configuration: {
'parse-numbers': false,
'parse-positional-numbers': false,
'dot-notation': false,
'camel-case-expansion': false,
},
});
options.unknownOptions = Object.keys(options)
.filter((p) => propKeys.indexOf(p) === -1 && unparsedCommandArgs[p] === undefined)
.reduce((m, c) => ((m[c] = options[c]), m), {});
options.parsedArgs = parseArgs(unparsedCommandArgs, options.unknownOptions, options.args);
options.unparsedCommandArgs = unparsedCommandArgs;
options.commands.forEach((c) => {
c.command = interpolateArgsIntoCommand(c.command, options, c.forwardAllArgs ?? options.forwardAllArgs ?? true);
});
return options;
}
async function runSerially(options, context) {
pseudoTerminal ??= pseudo_terminal_1.PseudoTerminal.isSupported() ? (0, pseudo_terminal_1.getPseudoTerminal)() : null;
let terminalOutput = '';
for (const c of options.commands) {
const result = await createProcess(pseudoTerminal, c, [], options.color, calculateCwd(options.cwd, context), options.processEnv ?? options.env ?? {}, false, options.usePty, options.streamOutput, options.tty, options.envFile);
terminalOutput += result.terminalOutput;
if (!result.success) {
const output = `Warning: command "${c.command}" exited with non-zero status code`;
result.terminalOutput += output;
if (options.streamOutput) {
process.stderr.write(output);
}
return { success: false, terminalOutput };
}
}
return { success: true, terminalOutput };
}
async function createProcess(pseudoTerminal, commandConfig, readyWhenStatus = [], color, cwd, env, isParallel, usePty = true, streamOutput = true, tty, envFile) {
env = processEnv(color, cwd, env, envFile);
// The rust runCommand is always a tty, so it will not look nice in parallel and if we need prefixes
// currently does not work properly in windows
if (pseudoTerminal &&
process.env.NX_NATIVE_COMMAND_RUNNER !== 'false' &&
!commandConfig.prefix &&
readyWhenStatus.length === 0 &&
!isParallel &&
usePty) {
let terminalOutput = chalk.dim('> ') + commandConfig.command + '\r\n\r\n';
if (streamOutput) {
process.stdout.write(terminalOutput);
}
const cp = pseudoTerminal.runCommand(commandConfig.command, {
cwd,
jsEnv: env,
quiet: !streamOutput,
tty,
});
childProcesses.add(cp);
return new Promise((res) => {
cp.onOutput((output) => {
terminalOutput += output;
});
cp.onExit((code) => {
if (code >= 128) {
process.exit(code);
}
else {
res({ success: code === 0, terminalOutput });
}
});
});
}
return nodeProcess(commandConfig, cwd, env, readyWhenStatus, streamOutput);
}
function nodeProcess(commandConfig, cwd, env, readyWhenStatus, streamOutput = true) {
let terminalOutput = chalk.dim('> ') + commandConfig.command + '\r\n\r\n';
if (streamOutput) {
process.stdout.write(terminalOutput);
}
return new Promise((res) => {
const childProcess = (0, child_process_1.exec)(commandConfig.command, {
maxBuffer: exports.LARGE_BUFFER,
env,
cwd,
windowsHide: false,
});
childProcesses.add(childProcess);
childProcess.stdout.on('data', (data) => {
const output = addColorAndPrefix(data, commandConfig);
terminalOutput += output;
if (streamOutput) {
process.stdout.write(output);
}
if (readyWhenStatus.length && isReady(readyWhenStatus, data.toString())) {
res({ success: true, terminalOutput });
}
});
childProcess.stderr.on('data', (err) => {
const output = addColorAndPrefix(err, commandConfig);
terminalOutput += output;
if (streamOutput) {
process.stderr.write(output);
}
if (readyWhenStatus.length && isReady(readyWhenStatus, err.toString())) {
res({ success: true, terminalOutput });
}
});
childProcess.on('error', (err) => {
const ouptput = addColorAndPrefix(err.toString(), commandConfig);
terminalOutput += ouptput;
if (streamOutput) {
process.stderr.write(ouptput);
}
res({ success: false, terminalOutput });
});
childProcess.on('exit', (code) => {
childProcesses.delete(childProcess);
if (!readyWhenStatus.length || isReady(readyWhenStatus)) {
res({ success: code === 0, terminalOutput });
}
});
});
}
function addColorAndPrefix(out, config) {
if (config.prefix) {
out = out
.split('\n')
.map((l) => {
let prefixText = config.prefix;
if (config.prefixColor && chalk[config.prefixColor]) {
prefixText = chalk[config.prefixColor](prefixText);
}
prefixText = chalk.bold(prefixText);
return l.trim().length > 0 ? `${prefixText} ${l}` : l;
})
.join('\n');
}
if (config.color && chalk[config.color]) {
out = chalk[config.color](out);
}
if (config.bgColor && chalk[config.bgColor]) {
out = chalk[config.bgColor](out);
}
return out;
}
function calculateCwd(cwd, context) {
if (!cwd)
return context.root;
if (path.isAbsolute(cwd))
return cwd;
return path.join(context.root, cwd);
}
/**
* Env variables are processed in the following order:
* - env option from executor options
* - env file from envFile option if provided
* - local env variables
*/
function processEnv(color, cwd, envOptionFromExecutor, envFile) {
let localEnv = (0, npm_run_path_1.env)({ cwd: cwd ?? process.cwd() });
localEnv = {
...process.env,
...localEnv,
};
if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false' && envFile) {
loadEnvVarsFile(envFile, localEnv);
}
let res = {
...localEnv,
...envOptionFromExecutor,
};
// need to override PATH to make sure we are using the local node_modules
if (localEnv.PATH)
res.PATH = localEnv.PATH; // UNIX-like
if (localEnv.Path)
res.Path = localEnv.Path; // Windows
if (color) {
res.FORCE_COLOR = `${color}`;
}
return res;
}
function interpolateArgsIntoCommand(command, opts, forwardAllArgs) {
if (command.indexOf('{args.') > -1) {
const regex = /{args\.([^}]+)}/g;
return command.replace(regex, (_, group) => opts.parsedArgs[group] !== undefined ? opts.parsedArgs[group] : '');
}
else if (forwardAllArgs) {
let args = '';
if (Object.keys(opts.unknownOptions ?? {}).length > 0) {
const unknownOptionsArgs = Object.keys(opts.unknownOptions)
.filter((k) => typeof opts.unknownOptions[k] !== 'object' &&
opts.parsedArgs[k] === opts.unknownOptions[k])
.map((k) => `--${k}=${opts.unknownOptions[k]}`)
.map(wrapArgIntoQuotesIfNeeded)
.join(' ');
if (unknownOptionsArgs) {
args += ` ${unknownOptionsArgs}`;
}
}
if (opts.args) {
args += ` ${opts.args}`;
}
if (opts.__unparsed__?.length > 0) {
const filteredParsedOptions = filterPropKeysFromUnParsedOptions(opts.__unparsed__, opts.parsedArgs);
if (filteredParsedOptions.length > 0) {
args += ` ${filteredParsedOptions
.map(wrapArgIntoQuotesIfNeeded)
.join(' ')}`;
}
}
return `${command}${args}`;
}
else {
return command;
}
}
function parseArgs(unparsedCommandArgs, unknownOptions, args) {
if (!args) {
return { ...unknownOptions, ...unparsedCommandArgs };
}
return {
...unknownOptions,
...yargsParser(args.replace(/(^"|"$)/g, ''), {
configuration: { 'camel-case-expansion': true },
}),
...unparsedCommandArgs,
};
}
/**
* This function filters out the prop keys from the unparsed options
* @param __unparsed__ e.g. ['--prop1', 'value1', '--prop2=value2', '--args=test']
* @param unparsedCommandArgs e.g. { prop1: 'value1', prop2: 'value2', args: 'test'}
* @returns filtered options that are not part of the propKeys array e.g. ['--prop1', 'value1', '--prop2=value2']
*/
function filterPropKeysFromUnParsedOptions(__unparsed__, parseArgs = {}) {
const parsedOptions = [];
for (let index = 0; index < __unparsed__.length; index++) {
const element = __unparsed__[index];
if (element.startsWith('--')) {
const key = element.replace('--', '');
if (element.includes('=')) {
// key can be in the format of --key=value or --key.subkey=value (e.g. env.foo=bar)
if (!propKeys.includes(key.split('=')[0].split('.')[0])) {
// check if the key is part of the propKeys array
parsedOptions.push(element);
}
}
else {
// check if the next element is a value for the key
if (propKeys.includes(key)) {
if (index + 1 < __unparsed__.length &&
parseArgs[key] &&
__unparsed__[index + 1].toString() === parseArgs[key].toString()) {
index++; // skip the next element
}
}
else {
parsedOptions.push(element);
}
}
}
else {
parsedOptions.push(element);
}
}
return parsedOptions;
}
let registered = false;
function registerProcessListener() {
if (registered) {
return;
}
registered = true;
// When the nx process gets a message, it will be sent into the task's process
process.on('message', (message) => {
// this.publisher.publish(message.toString());
if (pseudoTerminal) {
pseudoTerminal.sendMessageToChildren(message);
}
childProcesses.forEach((p) => {
if ('connected' in p && p.connected) {
p.send(message);
}
});
});
// Terminate any task processes on exit
process.on('exit', () => {
childProcesses.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill();
}
});
});
process.on('SIGINT', () => {
childProcesses.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// we exit here because we don't need to write anything to cache.
process.exit((0, exit_codes_1.signalToCode)('SIGINT'));
});
process.on('SIGTERM', () => {
childProcesses.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// no exit here because we expect child processes to terminate which
// will store results to the cache and will terminate this process
});
process.on('SIGHUP', () => {
childProcesses.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// no exit here because we expect child processes to terminate which
// will store results to the cache and will terminate this process
});
}
function wrapArgIntoQuotesIfNeeded(arg) {
if (arg.includes('=')) {
const [key, value] = arg.split('=');
if (key.startsWith('--') &&
value.includes(' ') &&
!(value[0] === "'" || value[0] === '"')) {
return `${key}="${value}"`;
}
return arg;
}
else if (arg.includes(' ') && !(arg[0] === "'" || arg[0] === '"')) {
return `"${arg}"`;
}
else {
return arg;
}
}
function isReady(readyWhenStatus = [], data) {
if (data) {
for (const readyWhenElement of readyWhenStatus) {
if (data.toString().indexOf(readyWhenElement.stringToMatch) > -1) {
readyWhenElement.found = true;
break;
}
}
}
return readyWhenStatus.every((readyWhenElement) => readyWhenElement.found);
}
;