@salesforce/command
Version:
Salesforce CLI base command class
246 lines • 9.15 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DocOpts = void 0;
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
const ts_types_1 = require("@salesforce/ts-types");
const ts_types_2 = require("@salesforce/ts-types");
const ts_types_3 = require("@salesforce/ts-types");
const sfdxFlags_1 = require("./sfdxFlags");
/**
* DocOpts generator for SfdxCommands. See http://docopt.org/.
*
* flag.exclusive: groups elements when one of the mutually exclusive cases is a required flag: (--apple | --orange)
* flag.exclusive: groups elements when none of the mutually exclusive cases is required (optional flags): [--apple | --orange]
* flag.dependsOn: specifies that if one element is present, then another one is required: (--apple --orange)
* cmd.variableArgs: produces 'name=value'
*
* @example
* {
* name: 'classnames',
* required: true,
* exclusive: ['suitenames']
* ...
* },{
* name: 'suitenames',
* type: 'array'
* required: true
* ...
* }
*
* Results in:
* Usage: <%= command.id %> (-n <string> | -s <array>)
*
* @example
* {
* name: 'classnames',
* ...
* excludes: ['suitenames']
* },{
* name: 'suitenames',
* ...
* }
*
* Results in:
* Usage: <%= command.id %> [-n <string> | -s <string>]
*
* @example
* {
* name: 'classnames',
* ...
* dependsOn: ['suitenames']
* },{
* name: 'suitenames',
* type: 'flag'
* ...
* }
*
* Results in:
* Usage: <%= command.id %> (-n <string> -s)
*
* TODO:
* - Support nesting, eg:
* Usage: my_program (--either-this <and-that> | <or-this>)
* Usage: my_program [(<one-argument> <another-argument>)]
*
* @param cmdDef
*/
class DocOpts {
constructor(cmd) {
this.cmd = cmd;
// Create a new map with references to the flags that we can manipulate.
this.flags = {};
// @ts-ignore
this.flagList = (0, ts_types_2.definiteEntriesOf)(this.cmd.flags)
.filter(([, v]) => !v.hidden)
.map(([k, v]) => {
const { description, ...rest } = v;
const flag = { description: (0, ts_types_1.ensure)(description), ...rest, name: k };
// @ts-ignore
this.flags[k] = flag;
return flag;
});
}
static generate(cmdDef) {
return new DocOpts(cmdDef).toString();
}
toString() {
try {
const groups = Object.values(this.groupFlagElements());
// Protected field
const varargs = this.cmd.getVarArgsConfig();
let varargsElement = '';
if (varargs) {
varargsElement = 'name=value...';
const isRequired = (0, ts_types_1.isPlainObject)(varargs) && varargs.required;
if (!isRequired) {
varargsElement = `[${varargsElement}]`;
}
varargsElement = `${varargsElement} `;
}
return `<%= command.id %> ${varargsElement}${groups.join(' ')}`;
}
catch (e) {
// If there is an error, just return no usage so we don't fail command help.
return '';
}
}
/**
* Group flags that dependOn (and) and are exclusive (or).
*/
groupFlagElements() {
const groups = this.categorizeFlags();
const elementMap = {};
// Generate all doc opt elements for combining
this.generateElements(elementMap, groups.requiredFlags);
this.generateElements(elementMap, groups.optionalFlags);
this.generateElements(elementMap, groups.sometimesBuiltinFlags);
this.generateElements(elementMap, groups.alwaysBuiltinFlags);
for (const flag of this.flagList) {
if ((0, ts_types_1.isArray)(flag.dependsOn)) {
this.combineElementsToFlag(elementMap, flag.name, flag.dependsOn, ' ');
}
if ((0, ts_types_1.isArray)(flag.exclusive)) {
this.combineElementsToFlag(elementMap, flag.name, flag.exclusive, ' | ');
}
}
// Since combineElementsToFlag deletes the references in this.flags when it combines
// them, this will go through the remaining list of uncombined elements.
for (const remainingFlagName of Object.keys(this.flags)) {
const remainingFlag = (0, ts_types_1.ensure)(this.flags[remainingFlagName]);
if (!remainingFlag.required) {
elementMap[remainingFlag.name] = `[${elementMap[remainingFlag.name] ?? ''}]`;
}
}
return elementMap;
}
/**
* Combine doc opt elements to another flag's doc opt element. This is for supporting
* things like "and" (dependsOn) and "or" (exclusive).
*
* This will probably break down on complex dependsOn / exclusive flag structures.
* For example, a flag that depends on a flag that depends on another flag.
*
* See tests to see what is supported.
*
* @param elementMap All doc opt elements.
* @param flagName The name of the flag to combine to.
* @param flagNames The other flag names to combine to flagName.
* @param unionString How to combine the doc opt elements.
*/
combineElementsToFlag(elementMap, flagName, flagNames, unionString) {
if (!this.flags[flagName]) {
return;
}
let isRequired = (0, ts_types_1.ensure)(this.flags[flagName]).required;
if (!(0, ts_types_1.isBoolean)(isRequired) || !isRequired) {
isRequired = flagNames.reduce((required, toCombine) => required || this.cmd.flags[toCombine].required || false, false);
}
for (const toCombine of flagNames) {
elementMap[flagName] = `${elementMap[flagName] ?? ''}${unionString}${elementMap[toCombine] ?? ''}`;
// We handled this flag, don't handle it again
delete elementMap[toCombine];
delete this.flags[toCombine];
}
if (isRequired) {
elementMap[flagName] = `(${elementMap[flagName] ?? ''})`;
}
else {
elementMap[flagName] = `[${elementMap[flagName] ?? ''}]`;
}
// We handled this flag, don't handle it again
delete this.flags[flagName];
}
/**
* Categorize flags into required, optional, builtin opt-in, and mandatory builtin
* flags. This is the order they should appear in the doc opts.
*
* For example, flags defined on the actual command should some before standard
* fields like --json.
*/
categorizeFlags() {
const alwaysBuiltinFlags = [];
const alwaysBuiltinFlagKeys = Object.keys(sfdxFlags_1.requiredBuiltinFlags);
const sometimesBuiltinFlags = [];
const sometimesBuiltinFlagKeys = Object.keys(sfdxFlags_1.optionalBuiltinFlags);
const requiredFlags = [];
const optionalFlags = [];
// We should also group on depends (AND, OR)
for (const flag of this.flagList) {
if (alwaysBuiltinFlagKeys.find((key) => key === flag.name)) {
alwaysBuiltinFlags.push(flag);
}
else if (sometimesBuiltinFlagKeys.find((key) => key === flag.name)) {
sometimesBuiltinFlags.push(flag);
}
else if (flag.required) {
requiredFlags.push(flag);
}
else {
optionalFlags.push(flag);
}
}
return {
requiredFlags,
optionalFlags,
sometimesBuiltinFlags,
alwaysBuiltinFlags,
};
}
/**
* Generate doc opt elements for all flags.
*
* @param elementMap The map to add the elements to.
* @param flagGroups The flags to generate elements for.
*/
// eslint-disable-next-line class-methods-use-this
generateElements(elementMap = {}, flagGroups) {
const elementStrs = [];
for (const flag of flagGroups) {
const kind = (0, ts_types_1.ensure)((0, ts_types_1.getString)(flag, 'kind'));
// not all flags have short names
const flagName = flag.char ? `-${flag.char}` : `--${flag.name}`;
let type = '';
if (kind !== 'boolean') {
if (kind === 'enum') {
const options = (0, ts_types_1.ensureArray)((0, ts_types_3.get)(flag, 'options'));
type = ` ${options.join('|')}`;
}
else {
type = ` <${kind || 'string'}>`;
}
}
const element = `${flagName}${type}`;
elementMap[flag.name] = element;
elementStrs.push(element);
}
return elementStrs;
}
}
exports.DocOpts = DocOpts;
//# sourceMappingURL=docOpts.js.map
;