sfdx-node
Version:
Utility to wrap the Salesforce CLI for use in NodeJS
179 lines (170 loc) • 6.63 kB
JavaScript
const _ = require('lodash');
const Promise = require('bluebird');
const { getAllSFDXCommands } = require('./lib/read-plugins');
let sfdxErrors = [];
let hookCounter = 0;
const sfdxApi = {};
const realStdoutWrite = process.stdout.write;
const realStderrWrite = process.stderr.write;
// Catch errors thrown by Salesforce CLI commands
process.on('cmdError', errObj => {
sfdxErrors.push(errObj);
});
/**
* Restore the original stdout and stderr write methods.
*/
const unhookStd = () => {
process.stdout.write = realStdoutWrite;
process.stderr.write = realStderrWrite;
};
/**
* Method only returns true if input value is true, boolean or string (case-insensitive), e.g. true, True, TRUE.
* Any other value is considered as falsy.
* @param {*} inputValue Input value to convert to boolean
*/
const getBooleanValue = (inputValue) => {
// Return as is, if input value is already boolean
if (_.isBoolean(inputValue)) {
return inputValue;
}
// Return true if the the input is a string 'true', in whatever case.
if (typeof inputValue === 'string' && _.toLower(inputValue) === 'true') {
return true;
}
return false;
};
/**
* Takes input objects flags & opts, and converts them to argument array as accepted by Salesforce CLI commands
* @param {Object} flags Command parameters
* @param {Object} opts Special command arguments accepted by a few specific commands, such as force:alias:set
*/
const transformArgs = (flags = {}, opts = {}) => {
const argsObj = {
argv: [],
quiet: true,
rejectOnError: false,
};
_.forEach(flags, (flagValue, flagName) => {
// convert all parameter names to lower case
flagName = _.toLower(flagName);
if (flagName === '_quiet') { // Determine if output needs to be suppressed
argsObj.quiet = getBooleanValue(flagValue);
} else if (flagName === '_rejectonerror') { // Determine if we need to reject in case the Salesforce CLI command fails
argsObj.rejectOnError = getBooleanValue(flagValue);
} else {
// For parameters with boolean values, convert them to a single flag argument (without any value) and push into the args array
if (_.isBoolean(flagValue)) {
if (flagValue) {
argsObj.argv.push(`--${flagName}`);
}
} else {
// For parameters with non-boolean values, push parameter name and value as string into the args array
argsObj.argv.push(`--${flagName}`, `${flagValue}`);
}
}
});
// If available, push the special commnand parameters into the args array
if (opts && _.isArray(opts.args)) {
argsObj.argv = _.concat(argsObj.argv, opts.args);
}
return argsObj;
};
/**
* Creates a custom function for a command which accepts command parameters as function arguments. This custom
* function actually executes the Salesforce CLI command.
* @param {*} cmdId Command ID, e.g. force:alias:set
* @param {*} cmdName Command Name as exported by the command module, e.g. AliasSetCommand
* @param {*} cmdFile Command module file
*/
const _createCommand = (cmdId, cmdName, cmdFile) => (flags, opts) => new Promise((resolve, reject) => {
const cmdModule = require(cmdFile);
let cmd = cmdModule[cmdName] || cmdModule.default;
if (!cmd) {
const exportedKeys = _.keys(cmdModule);
// console.log(exportedKeys);
if (exportedKeys.length) {
_.forEach(exportedKeys, (key) => {
// console.log(key, typeof cmdModule[key], typeof cmdModule[key].run);
if (typeof cmdModule[key] === 'function' && typeof cmdModule[key].run === 'function') {
cmd = cmdModule[key];
return false;
}
return true;
});
} else if (typeof cmdModule === 'function' && typeof cmdModule.run === 'function') {
// console.log(typeof cmdModule, typeof cmdModule.run);
cmd = cmdModule;
} else {
return reject(new Error(`Unable to load command ${cmdId}`));
}
}
// console.log(typeof cmd, typeof cmd.run);
cmd.id = cmdId;
const cmdArgs = transformArgs(flags, opts);
let currentHookFlag = false;
// If output needs to be suppressed, hook into the stdout and stderr streams and increase the counter
if (cmdArgs.quiet) {
const hookStd = require('hook-std');
hookStd(() => { });
hookCounter++;
currentHookFlag = true;
}
// Empty error array before each SFDX call so that the logs from one command execution do not impact the subsequent commands
sfdxErrors = [];
cmd.run(cmdArgs.argv)
.then((sfdxResult) => {
// If output was suppressed, decrease the counter. If counter reaches zero, restore the original stdout and stderr streams.
if (cmdArgs.quiet && currentHookFlag && hookCounter > 0) {
hookCounter--;
currentHookFlag = false;
if (hookCounter === 0) {
unhookStd();
}
}
// In case there were any errors and rejection is required, throw the errors
if (cmdArgs.rejectOnError && sfdxErrors.length) {
throw sfdxErrors;
}
// Revert exitCode set by SFDX as we don't want CI scripts to exit due to this
if (process.exitCode) {
process.exitCode = 0;
}
resolve(sfdxResult);
})
.catch((sfdxErr) => {
// If output was suppressed, decrease the counter. If counter reaches zero, restore the original stdout and stderr streams.
if (cmdArgs.quiet && currentHookFlag && hookCounter > 0) {
hookCounter--;
currentHookFlag = false;
if (hookCounter === 0) {
unhookStd();
}
}
// Revert exitCode set by SFDX as we don't want CI scripts to exit due to this
if (process.exitCode) {
process.exitCode = 0;
}
// Ensure that the error is thrown as an array of plain JS objects, in format => { message: "some error", stack: "stack trace for the error" }
const errors = require('./lib/process-errors');
reject(errors.processAllErrors(sfdxErr));
});
});
/**
* Loops through all the commands and creates progrmmatically executable version of each Salesforce CLI command.
*/
const buildAllCommands = () => {
const commands = getAllSFDXCommands();
_.forEach(commands, (cmdObj) => {
const { namespace, topic, methodName, commandId, commandExportedClassName, commandFile } = cmdObj;
let commandPath;
if (methodName) {
commandPath = `${namespace}.${topic}.${methodName}`;
} else {
commandPath = `${namespace}.${topic}`;
}
_.set(sfdxApi, commandPath, _createCommand(commandId, commandExportedClassName, commandFile));
});
};
buildAllCommands();
// console.log(sfdxApi);
module.exports = sfdxApi;