UNPKG

9.15 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9exports.DocOpts = void 0;
10/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
11const ts_types_1 = require("@salesforce/ts-types");
12const ts_types_2 = require("@salesforce/ts-types");
13const ts_types_3 = require("@salesforce/ts-types");
14const sfdxFlags_1 = require("./sfdxFlags");
15/**
16 * DocOpts generator for SfdxCommands. See http://docopt.org/.
17 *
18 * flag.exclusive: groups elements when one of the mutually exclusive cases is a required flag: (--apple | --orange)
19 * flag.exclusive: groups elements when none of the mutually exclusive cases is required (optional flags): [--apple | --orange]
20 * flag.dependsOn: specifies that if one element is present, then another one is required: (--apple --orange)
21 * cmd.variableArgs: produces 'name=value'
22 *
23 * @example
24 * {
25 * name: 'classnames',
26 * required: true,
27 * exclusive: ['suitenames']
28 * ...
29 * },{
30 * name: 'suitenames',
31 * type: 'array'
32 * required: true
33 * ...
34 * }
35 *
36 * Results in:
37 * Usage: <%= command.id %> (-n <string> | -s <array>)
38 *
39 * @example
40 * {
41 * name: 'classnames',
42 * ...
43 * excludes: ['suitenames']
44 * },{
45 * name: 'suitenames',
46 * ...
47 * }
48 *
49 * Results in:
50 * Usage: <%= command.id %> [-n <string> | -s <string>]
51 *
52 * @example
53 * {
54 * name: 'classnames',
55 * ...
56 * dependsOn: ['suitenames']
57 * },{
58 * name: 'suitenames',
59 * type: 'flag'
60 * ...
61 * }
62 *
63 * Results in:
64 * Usage: <%= command.id %> (-n <string> -s)
65 *
66 * TODO:
67 * - Support nesting, eg:
68 * Usage: my_program (--either-this <and-that> | <or-this>)
69 * Usage: my_program [(<one-argument> <another-argument>)]
70 *
71 * @param cmdDef
72 */
73class DocOpts {
74 constructor(cmd) {
75 this.cmd = cmd;
76 // Create a new map with references to the flags that we can manipulate.
77 this.flags = {};
78 // @ts-ignore
79 this.flagList = (0, ts_types_2.definiteEntriesOf)(this.cmd.flags)
80 .filter(([, v]) => !v.hidden)
81 .map(([k, v]) => {
82 const { description, ...rest } = v;
83 const flag = { description: (0, ts_types_1.ensure)(description), ...rest, name: k };
84 // @ts-ignore
85 this.flags[k] = flag;
86 return flag;
87 });
88 }
89 static generate(cmdDef) {
90 return new DocOpts(cmdDef).toString();
91 }
92 toString() {
93 try {
94 const groups = Object.values(this.groupFlagElements());
95 // Protected field
96 const varargs = this.cmd.getVarArgsConfig();
97 let varargsElement = '';
98 if (varargs) {
99 varargsElement = 'name=value...';
100 const isRequired = (0, ts_types_1.isPlainObject)(varargs) && varargs.required;
101 if (!isRequired) {
102 varargsElement = `[${varargsElement}]`;
103 }
104 varargsElement = `${varargsElement} `;
105 }
106 return `<%= command.id %> ${varargsElement}${groups.join(' ')}`;
107 }
108 catch (e) {
109 // If there is an error, just return no usage so we don't fail command help.
110 return '';
111 }
112 }
113 /**
114 * Group flags that dependOn (and) and are exclusive (or).
115 */
116 groupFlagElements() {
117 const groups = this.categorizeFlags();
118 const elementMap = {};
119 // Generate all doc opt elements for combining
120 this.generateElements(elementMap, groups.requiredFlags);
121 this.generateElements(elementMap, groups.optionalFlags);
122 this.generateElements(elementMap, groups.sometimesBuiltinFlags);
123 this.generateElements(elementMap, groups.alwaysBuiltinFlags);
124 for (const flag of this.flagList) {
125 if ((0, ts_types_1.isArray)(flag.dependsOn)) {
126 this.combineElementsToFlag(elementMap, flag.name, flag.dependsOn, ' ');
127 }
128 if ((0, ts_types_1.isArray)(flag.exclusive)) {
129 this.combineElementsToFlag(elementMap, flag.name, flag.exclusive, ' | ');
130 }
131 }
132 // Since combineElementsToFlag deletes the references in this.flags when it combines
133 // them, this will go through the remaining list of uncombined elements.
134 for (const remainingFlagName of Object.keys(this.flags)) {
135 const remainingFlag = (0, ts_types_1.ensure)(this.flags[remainingFlagName]);
136 if (!remainingFlag.required) {
137 elementMap[remainingFlag.name] = `[${elementMap[remainingFlag.name] ?? ''}]`;
138 }
139 }
140 return elementMap;
141 }
142 /**
143 * Combine doc opt elements to another flag's doc opt element. This is for supporting
144 * things like "and" (dependsOn) and "or" (exclusive).
145 *
146 * This will probably break down on complex dependsOn / exclusive flag structures.
147 * For example, a flag that depends on a flag that depends on another flag.
148 *
149 * See tests to see what is supported.
150 *
151 * @param elementMap All doc opt elements.
152 * @param flagName The name of the flag to combine to.
153 * @param flagNames The other flag names to combine to flagName.
154 * @param unionString How to combine the doc opt elements.
155 */
156 combineElementsToFlag(elementMap, flagName, flagNames, unionString) {
157 if (!this.flags[flagName]) {
158 return;
159 }
160 let isRequired = (0, ts_types_1.ensure)(this.flags[flagName]).required;
161 if (!(0, ts_types_1.isBoolean)(isRequired) || !isRequired) {
162 isRequired = flagNames.reduce((required, toCombine) => required || this.cmd.flags[toCombine].required || false, false);
163 }
164 for (const toCombine of flagNames) {
165 elementMap[flagName] = `${elementMap[flagName] ?? ''}${unionString}${elementMap[toCombine] ?? ''}`;
166 // We handled this flag, don't handle it again
167 delete elementMap[toCombine];
168 delete this.flags[toCombine];
169 }
170 if (isRequired) {
171 elementMap[flagName] = `(${elementMap[flagName] ?? ''})`;
172 }
173 else {
174 elementMap[flagName] = `[${elementMap[flagName] ?? ''}]`;
175 }
176 // We handled this flag, don't handle it again
177 delete this.flags[flagName];
178 }
179 /**
180 * Categorize flags into required, optional, builtin opt-in, and mandatory builtin
181 * flags. This is the order they should appear in the doc opts.
182 *
183 * For example, flags defined on the actual command should some before standard
184 * fields like --json.
185 */
186 categorizeFlags() {
187 const alwaysBuiltinFlags = [];
188 const alwaysBuiltinFlagKeys = Object.keys(sfdxFlags_1.requiredBuiltinFlags);
189 const sometimesBuiltinFlags = [];
190 const sometimesBuiltinFlagKeys = Object.keys(sfdxFlags_1.optionalBuiltinFlags);
191 const requiredFlags = [];
192 const optionalFlags = [];
193 // We should also group on depends (AND, OR)
194 for (const flag of this.flagList) {
195 if (alwaysBuiltinFlagKeys.find((key) => key === flag.name)) {
196 alwaysBuiltinFlags.push(flag);
197 }
198 else if (sometimesBuiltinFlagKeys.find((key) => key === flag.name)) {
199 sometimesBuiltinFlags.push(flag);
200 }
201 else if (flag.required) {
202 requiredFlags.push(flag);
203 }
204 else {
205 optionalFlags.push(flag);
206 }
207 }
208 return {
209 requiredFlags,
210 optionalFlags,
211 sometimesBuiltinFlags,
212 alwaysBuiltinFlags,
213 };
214 }
215 /**
216 * Generate doc opt elements for all flags.
217 *
218 * @param elementMap The map to add the elements to.
219 * @param flagGroups The flags to generate elements for.
220 */
221 // eslint-disable-next-line class-methods-use-this
222 generateElements(elementMap = {}, flagGroups) {
223 const elementStrs = [];
224 for (const flag of flagGroups) {
225 const kind = (0, ts_types_1.ensure)((0, ts_types_1.getString)(flag, 'kind'));
226 // not all flags have short names
227 const flagName = flag.char ? `-${flag.char}` : `--${flag.name}`;
228 let type = '';
229 if (kind !== 'boolean') {
230 if (kind === 'enum') {
231 const options = (0, ts_types_1.ensureArray)((0, ts_types_3.get)(flag, 'options'));
232 type = ` ${options.join('|')}`;
233 }
234 else {
235 type = ` <${kind || 'string'}>`;
236 }
237 }
238 const element = `${flagName}${type}`;
239 elementMap[flag.name] = element;
240 elementStrs.push(element);
241 }
242 return elementStrs;
243 }
244}
245exports.DocOpts = DocOpts;
246//# sourceMappingURL=docOpts.js.map
\No newline at end of file