@clo4/cli 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -2
- package/dist/index.d.ts +234 -0
- package/dist/index.js +1229 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +325 -0
- package/dist/testing.js +430 -0
- package/dist/testing.js.map +1 -0
- package/package.json +62 -4
package/dist/testing.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// src/util.ts
|
|
2
|
+
function isArray(o) {
|
|
3
|
+
return Array.isArray(o);
|
|
4
|
+
}
|
|
5
|
+
/** Convert the value, if any, to an array with 0 or more elements */
|
|
6
|
+
function makeArray(thing) {
|
|
7
|
+
if (thing === void 0) return [];
|
|
8
|
+
if (isArray(thing)) return thing.slice();
|
|
9
|
+
return [thing];
|
|
10
|
+
}
|
|
11
|
+
function assert(expr, msg = "") {
|
|
12
|
+
if (!expr) throw new Error(msg);
|
|
13
|
+
}
|
|
14
|
+
globalThis.process;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the minimum number of arguments that satisfies the args.
|
|
17
|
+
*
|
|
18
|
+
* Accepts the raw spec value (a single arg, an array, or `undefined`) so the
|
|
19
|
+
* hot path doesn't have to copy it into an array first.
|
|
20
|
+
*/
|
|
21
|
+
function getMinArgs(args) {
|
|
22
|
+
if (args === void 0) return 0;
|
|
23
|
+
if (!isArray(args)) return args.isOptional ? 0 : 1;
|
|
24
|
+
let count = 0;
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i].isOptional) break;
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
/** Returns the maximum number of arguments that satisfies the args. */
|
|
32
|
+
function getMaxArgs(args) {
|
|
33
|
+
if (args === void 0) return 0;
|
|
34
|
+
if (!isArray(args)) return args.isVariadic ? Infinity : 1;
|
|
35
|
+
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
36
|
+
return args.length;
|
|
37
|
+
}
|
|
38
|
+
// src/testing.ts
|
|
39
|
+
const repr = JSON.stringify;
|
|
40
|
+
/**
|
|
41
|
+
* Run a function once for each `args` property in the spec (recursive)
|
|
42
|
+
*
|
|
43
|
+
* This will convert single args to an array with one item.
|
|
44
|
+
*/
|
|
45
|
+
function forEachArgArray(spec, fn, path = []) {
|
|
46
|
+
path = path.concat(spec);
|
|
47
|
+
fn(makeArray(spec.args), path);
|
|
48
|
+
for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
|
|
49
|
+
for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
|
|
50
|
+
}
|
|
51
|
+
/** Run a function once for each `options` property in the spec (recursive) */
|
|
52
|
+
function forEachOptionArray(spec, fn, path = []) {
|
|
53
|
+
path = path.concat(spec);
|
|
54
|
+
fn(makeArray(spec.options), path);
|
|
55
|
+
for (const subcommand of makeArray(spec.subcommands)) forEachOptionArray(subcommand, fn, path);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a function once for each `subcommands` property in the spec (recursive)
|
|
59
|
+
*
|
|
60
|
+
* This will also run the function for the root spec itself.
|
|
61
|
+
*/
|
|
62
|
+
function forEachSubcommand(spec, fn, path = []) {
|
|
63
|
+
path = path.concat(spec);
|
|
64
|
+
fn(spec, path);
|
|
65
|
+
for (const subcommand of makeArray(spec.subcommands)) forEachSubcommand(subcommand, fn, path);
|
|
66
|
+
}
|
|
67
|
+
function joinNames(names) {
|
|
68
|
+
return makeArray(names).sort((a, b) => a.length - b.length).join("|");
|
|
69
|
+
}
|
|
70
|
+
function isSameName(a, b) {
|
|
71
|
+
if (isArray(a) && isArray(b)) return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
|
|
72
|
+
return a === b;
|
|
73
|
+
}
|
|
74
|
+
/** Compares spec values by shape. Any two functions count as the same shape. */
|
|
75
|
+
function isSameStructure(a, b) {
|
|
76
|
+
if (a === b) return true;
|
|
77
|
+
if (typeof a === "function" && typeof b === "function") return true;
|
|
78
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
79
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
80
|
+
const left = a;
|
|
81
|
+
const right = b;
|
|
82
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => isSameStructure(left[key], right[key]));
|
|
83
|
+
}
|
|
84
|
+
function namedArrayToString(...parts) {
|
|
85
|
+
return `\`${parts.map((named) => joinNames(named.name)).join(" ")}\``;
|
|
86
|
+
}
|
|
87
|
+
/** Asserts that required arguments do not come after an optional argument */
|
|
88
|
+
function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
|
|
89
|
+
forEachArgArray(spec, (args, path) => {
|
|
90
|
+
let remainingMustBeOptional = false;
|
|
91
|
+
let firstOptionalArgIndex = null;
|
|
92
|
+
for (const [index, arg] of args.entries()) if (remainingMustBeOptional) assert(arg.isOptional, `In ${namedArrayToString(...path)}, argument ${arg.name || index} is required, but it must be optional since the argument at index ${firstOptionalArgIndex} is optional`);
|
|
93
|
+
else if (arg.isOptional) {
|
|
94
|
+
remainingMustBeOptional = true;
|
|
95
|
+
firstOptionalArgIndex ??= index;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Asserts that `isRepeatable` is only assigned a positive integer */
|
|
100
|
+
function assertRepeatableOptionsArePositiveIntegers(spec) {
|
|
101
|
+
forEachOptionArray(spec, (options, path) => {
|
|
102
|
+
for (const [index, option] of options.entries()) if (typeof option.isRepeatable === "number") {
|
|
103
|
+
assert(Number.isSafeInteger(option.isRepeatable), `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to a number that is not an integer`);
|
|
104
|
+
assert(!(option.isRepeatable < 1), `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to a number below 1, which means it can't be used at all`);
|
|
105
|
+
assert(option.isRepeatable !== 1, `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to 1, which is no different that omitting the property entirely`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Asserts that options have unique names in their scope */
|
|
110
|
+
function assertOptionsHaveLocallyUniqueNames(spec) {
|
|
111
|
+
forEachSubcommand(spec, (spec, path) => {
|
|
112
|
+
const optionNames = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const [index, option] of makeArray(spec.options).entries()) for (const name of makeArray(option.name)) {
|
|
114
|
+
assert(!optionNames.has(name), `Option ${namedArrayToString(...path, option)} (index ${index}) has a non-unique name, '${name}'. Option names must be unique.`);
|
|
115
|
+
optionNames.add(name);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Asserts that options don't shadow a persistent option */
|
|
120
|
+
function assertOptionsDoNotShadowPersistentOptions(spec) {
|
|
121
|
+
forEachOptionArray(spec, (options, path) => {
|
|
122
|
+
const persistentOptionNames = new Set(path.slice(0, -1).flatMap((subcommand) => subcommand.options ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name) : []));
|
|
123
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(!persistentOptionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Asserts that a subcommand's option isn't a different option with exactly the
|
|
128
|
+
* same names as one on a parent.
|
|
129
|
+
*
|
|
130
|
+
* Parsed values are looked up by name, so that parent's value would be
|
|
131
|
+
* unreadable. A shared name is fine when each option has a name of its own, and
|
|
132
|
+
* when the options are identical, such as an option kept in a constant.
|
|
133
|
+
*
|
|
134
|
+
* This skips persistent parent options, because
|
|
135
|
+
* {@link assertOptionsDoNotShadowPersistentOptions} covers those and
|
|
136
|
+
* `allowShadowingPersistentOptions` opts out of it.
|
|
137
|
+
*/
|
|
138
|
+
function assertOptionsHaveDifferentNamesFromParents(spec) {
|
|
139
|
+
forEachOptionArray(spec, (options, path) => {
|
|
140
|
+
const parents = path.slice(0, -1);
|
|
141
|
+
for (const [index, option] of options.entries()) for (const parent of parents) for (const parentOption of makeArray(parent.options)) assert(!(!parentOption.isPersistent && isSameName(parentOption.name, option.name) && !isSameStructure({
|
|
142
|
+
...parentOption,
|
|
143
|
+
name: 0
|
|
144
|
+
}, {
|
|
145
|
+
...option,
|
|
146
|
+
name: 0
|
|
147
|
+
})), `The option ${namedArrayToString(...path, option)} (index ${index}) has the same names as a different option, ${namedArrayToString(parent, parentOption)}, so the parent's value can't be read. Add a name that only one of them uses.`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** Asserts that subcommands don't share a name with a sibling subcommand */
|
|
151
|
+
function assertSubcommandsHaveLocallyUniqueNames(spec) {
|
|
152
|
+
forEachSubcommand(spec, (spec, path) => {
|
|
153
|
+
const names = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const [index, subcommand] of makeArray(spec.subcommands).entries()) for (const name of makeArray(subcommand.name)) {
|
|
155
|
+
assert(!names.has(name), `The subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a non-unique name, ${repr(name)}. Subcommand names must be unique among the subcommands in the same array.`);
|
|
156
|
+
names.add(name);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Asserts that option names starting with a single dash aren't longer
|
|
162
|
+
* than two characters, unless parserDirectives.flagsArePosixNoncompliant is
|
|
163
|
+
* set on an ancestor command.
|
|
164
|
+
*/
|
|
165
|
+
function assertLongOptionNamesDoNotStartWithSingleDash(spec) {
|
|
166
|
+
forEachOptionArray(spec, (options, path) => {
|
|
167
|
+
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
168
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(!((name.startsWith("-") || name.startsWith("+")) && !name.startsWith("--") && name.length > 2), `Option ${namedArrayToString(...path, option)} (index ${index}) has a name that the parser is unable to parse, as it starts with a single '${name[0]}'. Tokens starting with that character are interpreted as short options, so passing '${name}' will be interpreted as '${Array.from(name.slice(1), (letter) => name[0] + letter).join(" ")}'. \`parserDirectives.flagsArePosixNoncompliant\` must be \`true\` on an ancestor command.`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Asserts that all option names start with dashes
|
|
173
|
+
*/
|
|
174
|
+
function assertOptionNamesStartWithDashes(spec) {
|
|
175
|
+
forEachOptionArray(spec, (options, path) => {
|
|
176
|
+
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
177
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name.startsWith("-") || name.startsWith("+"), `Option ${namedArrayToString(...path, option)} (index ${index}) doesn't start with a dash or plus, the parser will be unable to find matching options without \`parserDirectives.flagsAreNonPosixCompliant\` being set to 'true' on an ancestor command`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Asserts that nothing is named exactly '--'
|
|
182
|
+
*/
|
|
183
|
+
function assertNothingIsNamedDashDash(spec) {
|
|
184
|
+
forEachOptionArray(spec, (options, path) => {
|
|
185
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name !== "--", `Option ${namedArrayToString(...path, option)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
|
|
186
|
+
});
|
|
187
|
+
forEachSubcommand(spec, (subcommands, path) => {
|
|
188
|
+
for (const [index, subcommand] of makeArray(subcommands).entries()) for (const name of makeArray(subcommand.name)) assert(name !== "--", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Asserts that no option arg separator is an empty string
|
|
193
|
+
*/
|
|
194
|
+
function assertOptionArgSeparatorsHaveCharacters(spec) {
|
|
195
|
+
forEachSubcommand(spec, (subcommands, path) => {
|
|
196
|
+
for (const [index, subcommand] of makeArray(subcommands).entries()) {
|
|
197
|
+
const separators = makeArray(subcommand.parserDirectives?.optionArgSeparators);
|
|
198
|
+
for (const [sepIndex, separator] of separators.entries()) assert(separator !== "", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Asserts that options named "+" or "-" takes one arg
|
|
204
|
+
*/
|
|
205
|
+
function assertPlusMinusOptionsTakeOneArg(spec) {
|
|
206
|
+
forEachOptionArray(spec, (options, path) => {
|
|
207
|
+
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
208
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) {
|
|
209
|
+
if (name !== "-" && name !== "+") continue;
|
|
210
|
+
assert(option.args && !Array.isArray(option.args), `Option ${namedArrayToString(...path, option)} (index ${index}) must take exactly one argument`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Asserts that names do not have leading or trailing whitespace
|
|
216
|
+
*/
|
|
217
|
+
function assertNamesHaveNoExtraWhitespace(spec) {
|
|
218
|
+
forEachOptionArray(spec, (options, path) => {
|
|
219
|
+
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name === name.trim(), `Option ${namedArrayToString(...path, option)} (index ${index}) has a name with extra whitespace`);
|
|
220
|
+
});
|
|
221
|
+
forEachSubcommand(spec, (subcommands, path) => {
|
|
222
|
+
for (const [index, subcommand] of makeArray(subcommands).entries()) for (const name of makeArray(subcommand.name)) assert(name === name.trim(), `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a name with extra whitespace`);
|
|
223
|
+
});
|
|
224
|
+
forEachArgArray(spec, (args, path) => {
|
|
225
|
+
for (const [index, arg] of makeArray(args).entries()) for (const name of makeArray(arg.name)) assert(name === name.trim(), `Arg in ${namedArrayToString(...path)} (at index ${index}) has a name with extra whitespace`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Asserts that everything has a description
|
|
230
|
+
*/
|
|
231
|
+
function assertEverythingHasDescription(spec) {
|
|
232
|
+
assert(spec.description, `Spec has no description`);
|
|
233
|
+
forEachOptionArray(spec, (options, path) => {
|
|
234
|
+
for (const [index, option] of makeArray(options).entries()) assert(option.description, `Option ${namedArrayToString(...path, option)} (index ${index}) has no description`);
|
|
235
|
+
});
|
|
236
|
+
forEachSubcommand(spec, (subcommands, path) => {
|
|
237
|
+
for (const [index, subcommand] of makeArray(subcommands).entries()) assert(subcommand.description, `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has no description`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Asserts that descriptions have line length < 69
|
|
242
|
+
*/
|
|
243
|
+
function assertDescriptionLineLengthUnder69(spec) {
|
|
244
|
+
if (spec.description) assert(spec.description.split("\n").every((line) => line.length <= 68), `Spec has a description line over 68 characters`);
|
|
245
|
+
forEachOptionArray(spec, (options, path) => {
|
|
246
|
+
for (const [index, option] of makeArray(options).entries()) if (option.description) assert(option.description.split("\n").every((line) => line.length <= 68), `Option ${namedArrayToString(...path, option)} (index ${index}) has a description line over 68 characters`);
|
|
247
|
+
});
|
|
248
|
+
forEachSubcommand(spec, (subcommands, path) => {
|
|
249
|
+
for (const [index, subcommand] of makeArray(subcommands).entries()) if (subcommand.description) assert(subcommand.description.split("\n").every((line) => line.length <= 68), `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a description line over 68 characters`);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function assertRequiresSeparatorTakesOneArg(spec) {
|
|
253
|
+
forEachOptionArray(spec, (options, path) => {
|
|
254
|
+
for (const [index, option] of makeArray(options).entries()) if (option.requiresSeparator) {
|
|
255
|
+
const args = makeArray(option.args);
|
|
256
|
+
assert(getMinArgs(args) <= 1, `Option ${namedArrayToString(...path, option)} (index ${index}) takes a minimum of ${getMinArgs(args)} arg(s), but because of requiresSeparator, it must instead be 0 or 1`);
|
|
257
|
+
assert(getMaxArgs(args) === 1, `Option ${namedArrayToString(...path, option)} (index ${index}) takes a maximum of ${getMaxArgs(args)} arg(s), but because of requiresSeparator, it's only able to take 1`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function assertCommonOptionsArePersistent(spec) {
|
|
262
|
+
const failures = [];
|
|
263
|
+
forEachSubcommand(spec, (subcommand, path) => {
|
|
264
|
+
if (!subcommand.subcommands) return;
|
|
265
|
+
for (const option of makeArray(subcommand.options)) {
|
|
266
|
+
const allChildSubcommands = [];
|
|
267
|
+
forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));
|
|
268
|
+
if (allChildSubcommands.every((cmd) => makeArray(cmd.options).some((o) => isSameName(option.name, o.name))) && !failures.some(([name]) => isSameName(name, option.name))) failures.push([option.name, namedArrayToString(...path, option)]);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
assert(failures.length === 0, `The following options were also defined by all subcommands. Instead, define the option once, and use \`isPersistent: true\` to persist it across all subcommands.
|
|
272
|
+
${failures.map(([, line]) => " * " + line).join("\n")}
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Asserts that references to other options by name actually refer to options
|
|
277
|
+
* that exist in scope
|
|
278
|
+
*/
|
|
279
|
+
function assertOptionNameReferencesExist(spec) {
|
|
280
|
+
forEachOptionArray(spec, (options, path) => {
|
|
281
|
+
const optionNames = /* @__PURE__ */ new Set([...path.slice(0, -1).flatMap((subcommand) => subcommand.options ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name) : []), ...options.flatMap((option) => option.name)]);
|
|
282
|
+
for (const [index, option] of makeArray(options).entries()) {
|
|
283
|
+
for (const name of makeArray(option.dependsOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) depends on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
284
|
+
for (const name of makeArray(option.exclusiveOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Asserts that commands that will match subcommands by prefix do not take arguments
|
|
290
|
+
*/
|
|
291
|
+
function assertPrefixMatchCommandsHaveNoArguments(spec) {
|
|
292
|
+
forEachSubcommand(spec, (subcommand, path) => {
|
|
293
|
+
if (!path.some((cmd) => cmd.parserDirectives?.subcommandsMatchUniquePrefix === true)) return;
|
|
294
|
+
assert(!(subcommand.subcommands && subcommand.args), `The command ${namedArrayToString(...path)} has at least one argument and subcommand, but matches subcommands based on unique prefixes. To fix this, use \`parserDirectives: { subcommandsMatchUniquePrefix: false }\``);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function getName(namingStyle, names) {
|
|
298
|
+
return names[namingStyle];
|
|
299
|
+
}
|
|
300
|
+
async function runStep(context, name, fn) {
|
|
301
|
+
if (context) {
|
|
302
|
+
await context.step(name, fn);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await fn();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Validate your CLI in your test suite
|
|
309
|
+
*
|
|
310
|
+
* This enforces certain constraints that the type system cannot. The parser
|
|
311
|
+
* may ignore these issues, producing correct output that _appears_ to be
|
|
312
|
+
* wrong.
|
|
313
|
+
*/
|
|
314
|
+
function test(spec, options = {}) {
|
|
315
|
+
const { allowShadowingPersistentOptions = false, allowNoDescription = false, allowLongDescriptionLines = false, allowMatchingSubcommandPrefixAndArgs = false, namingStyle = "sentence" } = options;
|
|
316
|
+
return async (t) => {
|
|
317
|
+
await runStep(t, getName(namingStyle, {
|
|
318
|
+
sentence: "Required arguments don't follow optional arguments",
|
|
319
|
+
function: "assertRequiredArgumentsDoNotFollowOptionalArguments"
|
|
320
|
+
}), () => {
|
|
321
|
+
assertRequiredArgumentsDoNotFollowOptionalArguments(spec);
|
|
322
|
+
});
|
|
323
|
+
await runStep(t, getName(namingStyle, {
|
|
324
|
+
sentence: "Repeatable options that are numbers are integers greater than 1",
|
|
325
|
+
function: "assertRepeatableOptionsArePositiveIntegers"
|
|
326
|
+
}), () => {
|
|
327
|
+
assertRepeatableOptionsArePositiveIntegers(spec);
|
|
328
|
+
});
|
|
329
|
+
await runStep(t, getName(namingStyle, {
|
|
330
|
+
sentence: "Long option names don't start with a single dash",
|
|
331
|
+
function: "assertLongOptionNamesDoNotStartWithSingleDash"
|
|
332
|
+
}), () => {
|
|
333
|
+
assertLongOptionNamesDoNotStartWithSingleDash(spec);
|
|
334
|
+
});
|
|
335
|
+
await runStep(t, getName(namingStyle, {
|
|
336
|
+
sentence: "Option names all start with dashes",
|
|
337
|
+
function: "assertOptionNamesStartWithDashes"
|
|
338
|
+
}), () => {
|
|
339
|
+
assertOptionNamesStartWithDashes(spec);
|
|
340
|
+
});
|
|
341
|
+
await runStep(t, getName(namingStyle, {
|
|
342
|
+
sentence: "Options have locally unique names",
|
|
343
|
+
function: "assertOptionsHaveLocallyUniqueNames"
|
|
344
|
+
}), () => {
|
|
345
|
+
assertOptionsHaveLocallyUniqueNames(spec);
|
|
346
|
+
});
|
|
347
|
+
if (!allowShadowingPersistentOptions) await runStep(t, getName(namingStyle, {
|
|
348
|
+
sentence: "Options don't shadow persistent options",
|
|
349
|
+
function: "assertOptionsDoNotShadowPersistentOptions"
|
|
350
|
+
}), () => {
|
|
351
|
+
assertOptionsDoNotShadowPersistentOptions(spec);
|
|
352
|
+
});
|
|
353
|
+
await runStep(t, getName(namingStyle, {
|
|
354
|
+
sentence: "Different options on a subcommand and its parents have different names",
|
|
355
|
+
function: "assertOptionsHaveDifferentNamesFromParents"
|
|
356
|
+
}), () => {
|
|
357
|
+
assertOptionsHaveDifferentNamesFromParents(spec);
|
|
358
|
+
});
|
|
359
|
+
await runStep(t, getName(namingStyle, {
|
|
360
|
+
sentence: "Subcommands have locally unique names",
|
|
361
|
+
function: "assertSubcommandsHaveLocallyUniqueNames"
|
|
362
|
+
}), () => {
|
|
363
|
+
assertSubcommandsHaveLocallyUniqueNames(spec);
|
|
364
|
+
});
|
|
365
|
+
await runStep(t, getName(namingStyle, {
|
|
366
|
+
sentence: "Nothing is named '--'",
|
|
367
|
+
function: "assertNothingIsNamedDashDash"
|
|
368
|
+
}), () => {
|
|
369
|
+
assertNothingIsNamedDashDash(spec);
|
|
370
|
+
});
|
|
371
|
+
await runStep(t, getName(namingStyle, {
|
|
372
|
+
sentence: "Option arg separators are not an empty string",
|
|
373
|
+
function: "assertOptionArgSeparatorsHaveCharacters"
|
|
374
|
+
}), () => {
|
|
375
|
+
assertOptionArgSeparatorsHaveCharacters(spec);
|
|
376
|
+
});
|
|
377
|
+
await runStep(t, getName(namingStyle, {
|
|
378
|
+
sentence: "Options named '-' or '+' take exactly one argument",
|
|
379
|
+
function: "assertPlusMinusOptionsTakeOneArg"
|
|
380
|
+
}), () => {
|
|
381
|
+
assertPlusMinusOptionsTakeOneArg(spec);
|
|
382
|
+
});
|
|
383
|
+
await runStep(t, getName(namingStyle, {
|
|
384
|
+
sentence: "Names have no extra leading or trailing whitespace",
|
|
385
|
+
function: "assertNamesHaveNoExtraWhitespace"
|
|
386
|
+
}), () => {
|
|
387
|
+
assertNamesHaveNoExtraWhitespace(spec);
|
|
388
|
+
});
|
|
389
|
+
if (!allowNoDescription) await runStep(t, getName(namingStyle, {
|
|
390
|
+
sentence: "Everything has a description",
|
|
391
|
+
function: "assertEverythingHasDescription"
|
|
392
|
+
}), () => {
|
|
393
|
+
assertEverythingHasDescription(spec);
|
|
394
|
+
});
|
|
395
|
+
if (!allowLongDescriptionLines) await runStep(t, getName(namingStyle, {
|
|
396
|
+
sentence: "Each line in descriptions is less than or equal to 68 characters",
|
|
397
|
+
function: "assertDescriptionLineLengthUnder69"
|
|
398
|
+
}), () => {
|
|
399
|
+
assertDescriptionLineLengthUnder69(spec);
|
|
400
|
+
});
|
|
401
|
+
await runStep(t, getName(namingStyle, {
|
|
402
|
+
sentence: "Options with `requiresSeparator` take exactly one argument",
|
|
403
|
+
function: "assertRequiresSeparatorTakesOneArg"
|
|
404
|
+
}), () => {
|
|
405
|
+
assertRequiresSeparatorTakesOneArg(spec);
|
|
406
|
+
});
|
|
407
|
+
await runStep(t, getName(namingStyle, {
|
|
408
|
+
sentence: "Common options are not defined on every subcommand",
|
|
409
|
+
function: "assertCommonOptionsArePersistent"
|
|
410
|
+
}), () => {
|
|
411
|
+
assertCommonOptionsArePersistent(spec);
|
|
412
|
+
});
|
|
413
|
+
await runStep(t, getName(namingStyle, {
|
|
414
|
+
sentence: "References to other options by name are valid",
|
|
415
|
+
function: "assertOptionNameReferencesExist"
|
|
416
|
+
}), () => {
|
|
417
|
+
assertOptionNameReferencesExist(spec);
|
|
418
|
+
});
|
|
419
|
+
if (!allowMatchingSubcommandPrefixAndArgs) await runStep(t, getName(namingStyle, {
|
|
420
|
+
sentence: "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
|
|
421
|
+
function: "assertPrefixMatchCommandsHaveNoArguments"
|
|
422
|
+
}), () => {
|
|
423
|
+
assertPrefixMatchCommandsHaveNoArguments(spec);
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const testSpec = test;
|
|
428
|
+
export { assertCommonOptionsArePersistent, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertLongOptionNamesDoNotStartWithSingleDash, assertNamesHaveNoExtraWhitespace, assertNothingIsNamedDashDash, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionNamesStartWithDashes, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveDifferentNamesFromParents, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRequiredArgumentsDoNotFollowOptionalArguments, assertRequiresSeparatorTakesOneArg, assertSubcommandsHaveLocallyUniqueNames, test, testSpec };
|
|
429
|
+
|
|
430
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/util.ts","../src/testing.ts"],"sourcesContent":["import type { NonEmptyArray } from \"./types.js\";\n\nexport function isArray<T>(o: T | readonly T[]): o is readonly T[] {\n return Array.isArray(o);\n}\n\n/** Convert the value, if any, to an array with 0 or more elements */\nexport function makeArray<T>(thing: T | readonly T[] | undefined): T[] {\n if (thing === undefined) return [];\n if (isArray(thing)) {\n return thing.slice();\n }\n return [thing];\n}\n\n/**\n * Like {@link makeArray}, but without copying when the input is already an\n * array. Only safe when the caller treats the result as read-only — use it on\n * the hot path to avoid slicing read-only spec metadata.\n */\nexport function asArray<T>(thing: T | readonly T[] | undefined): readonly T[] {\n if (thing === undefined) return [];\n if (isArray(thing)) return thing;\n return [thing];\n}\n\n/** Convert the value to an array with at least one element */\nexport function makeArray1<T>(thing: T | NonEmptyArray<T>): [T, ...T[]] {\n if (isArray(thing)) {\n // We know that this array has at least one element in it\n return thing.slice() as [T, ...T[]];\n }\n return [thing];\n}\n\n/** Set many keys in a Map to the same value */\nexport function setEach<K, V>(map: Map<K, V>, keys: K | readonly K[], value: V): void {\n if (isArray(keys)) {\n for (const key of keys) {\n map.set(key, value);\n }\n } else {\n map.set(keys, value);\n }\n}\n\nexport function assert(expr: unknown, msg = \"\"): asserts expr {\n if (!expr) {\n throw new Error(msg);\n }\n}\n\n// Output {{{\n\ninterface WriteSyncFs {\n writeSync(fd: number, buffer: Uint8Array, offset: number, length: number): number;\n}\n\ninterface NodeProcess {\n getBuiltinModule?(id: \"node:fs\"): WriteSyncFs;\n stdout?: { write(data: Uint8Array): unknown };\n stderr?: { write(data: Uint8Array): unknown };\n}\n\nconst nodeProcess = (globalThis as { process?: NodeProcess }).process;\n\n/**\n * Write text to stdout (1) or stderr (2) with a synchronous `write` on the file\n * descriptor.\n *\n * The first `console.log` in a Node process builds the stdout stream, which\n * costs 1-3 ms. That's more than parsing takes, so all of this library's output\n * goes through here instead. Without `node:fs` it falls back to `console`.\n */\nfunction writeFd(fd: 1 | 2, text: string): void {\n const fs = nodeProcess?.getBuiltinModule?.(\"node:fs\");\n if (!fs) {\n (fd === 1 ? console.log : console.error)(text.endsWith(\"\\n\") ? text.slice(0, -1) : text);\n return;\n }\n const bytes = new TextEncoder().encode(text);\n let offset = 0;\n try {\n while (offset < bytes.length) {\n offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);\n }\n } catch (error) {\n const code = (error as { code?: string }).code;\n // A non-blocking pipe that's full: let Node's stream queue the rest.\n if (code === \"EAGAIN\") {\n (fd === 1 ? nodeProcess?.stdout : nodeProcess?.stderr)?.write(bytes.subarray(offset));\n }\n // Anything else, like EPIPE from `cli --help | head -1`, is dropped, the\n // same as `console` does.\n }\n}\n\n/**\n * Where this library's output goes. Tests replace `write` to capture output.\n *\n * @internal\n */\nexport const output: { write: (fd: 1 | 2, text: string) => void } = { write: writeFd };\n\n/** Print a message to stdout, trimming trailing whitespace. */\nexport function print(text: string): void {\n output.write(1, `${text.trimEnd()}\\n`);\n}\n\n/** Print an error message to stderr with the shared `Error:` prefix. */\nexport function printActionError(message: string): void {\n output.write(2, `Error: ${message}\\n`);\n}\n\n// }}}\n\n/**\n * Returns the minimum number of arguments that satisfies the args.\n *\n * Accepts the raw spec value (a single arg, an array, or `undefined`) so the\n * hot path doesn't have to copy it into an array first.\n */\nexport function getMinArgs(\n args: { isOptional?: boolean } | readonly { isOptional?: boolean }[] | undefined,\n): number {\n if (args === undefined) return 0;\n if (!isArray(args)) return args.isOptional ? 0 : 1;\n let count = 0;\n for (let i = 0; i < args.length; i++) {\n if (args[i].isOptional) {\n break;\n }\n count++;\n }\n return count;\n}\n\n/** Returns the maximum number of arguments that satisfies the args. */\nexport function getMaxArgs(\n args: { isVariadic?: boolean } | readonly { isVariadic?: boolean }[] | undefined,\n): number {\n if (args === undefined) return 0;\n if (!isArray(args)) return args.isVariadic ? Infinity : 1;\n // Loop over the array backwards because variadic arguments are\n // more likely to be at the end of the array. They don't _need_\n // to be, but it's more idiomatic.\n for (let i = 0; i < args.length; i++) {\n if (args[args.length - 1 - i].isVariadic) {\n return Infinity;\n }\n }\n // None of the arguments are variadic, max is the number of args\n return args.length;\n}\n","/**\n * Supplementary module to make testing your CLI easier.\n *\n * @module\n */\n\nimport type { Arg, Option, SingleOrArray, Subcommand } from \"./types.js\";\nimport { assert, isArray, makeArray } from \"./util.js\";\nimport { getMaxArgs, getMinArgs } from \"./parse.js\";\n\nconst repr = JSON.stringify;\n\n// TODO: one decorators are stable (ts 4.9?), use decorators this file\n\n/**\n * Run a function once for each `args` property in the spec (recursive)\n *\n * This will convert single args to an array with one item.\n */\nfunction forEachArgArray(\n spec: Subcommand,\n fn: (args: Arg[], path: Subcommand[]) => void,\n path: Subcommand[] = [],\n): void {\n path = path.concat(spec);\n fn(makeArray(spec.args), path);\n for (const option of makeArray(spec.options)) {\n fn(makeArray(option.args), path);\n }\n for (const subcommand of makeArray(spec.subcommands)) {\n forEachArgArray(subcommand, fn, path);\n }\n}\n\n/** Run a function once for each `options` property in the spec (recursive) */\nfunction forEachOptionArray(\n spec: Subcommand,\n fn: (options: Option[], path: Subcommand[]) => void,\n path: Subcommand[] = [],\n): void {\n path = path.concat(spec);\n fn(makeArray(spec.options), path);\n for (const subcommand of makeArray(spec.subcommands)) {\n forEachOptionArray(subcommand, fn, path);\n }\n}\n\n/**\n * Run a function once for each `subcommands` property in the spec (recursive)\n *\n * This will also run the function for the root spec itself.\n */\nfunction forEachSubcommand(\n spec: Subcommand,\n fn: (spec: Subcommand, path: Subcommand[]) => void,\n path: Subcommand[] = [],\n): void {\n path = path.concat(spec);\n fn(spec, path);\n for (const subcommand of makeArray(spec.subcommands)) {\n forEachSubcommand(subcommand, fn, path);\n }\n}\n\nfunction joinNames(names: SingleOrArray<string>) {\n return makeArray(names)\n .sort((a, b) => a.length - b.length)\n .join(\"|\");\n}\n\nfunction isSameName(a: string | readonly string[], b: string | readonly string[]) {\n if (isArray(a) && isArray(b)) {\n return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));\n }\n // This might compare a string to an array, but if that happens the correct\n // result is `false` anyway, so this behaves correctly\n return a === b;\n}\n\n/** Compares spec values by shape. Any two functions count as the same shape. */\nfunction isSameStructure(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a === \"function\" && typeof b === \"function\") return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const keys = new Set([...Object.keys(left), ...Object.keys(right)]);\n return [...keys].every((key) => isSameStructure(left[key], right[key]));\n}\n\nfunction namedArrayToString(...parts: { name: SingleOrArray<string> }[]) {\n const names = parts.map((named) => joinNames(named.name));\n return `\\`${names.join(\" \")}\\``;\n}\n\n/** Asserts that required arguments do not come after an optional argument */\nexport function assertRequiredArgumentsDoNotFollowOptionalArguments(spec: Subcommand): void {\n forEachArgArray(spec, (args, path) => {\n let remainingMustBeOptional = false;\n let firstOptionalArgIndex: number | null = null;\n for (const [index, arg] of args.entries()) {\n if (remainingMustBeOptional) {\n assert(\n arg.isOptional,\n `In ${namedArrayToString(...path)}, argument ${arg.name || index} is required, but it must be optional since the argument at index ${firstOptionalArgIndex} is optional`,\n );\n } else if (arg.isOptional) {\n remainingMustBeOptional = true;\n firstOptionalArgIndex ??= index;\n }\n }\n });\n}\n\n/** Asserts that `isRepeatable` is only assigned a positive integer */\nexport function assertRepeatableOptionsArePositiveIntegers(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of options.entries()) {\n if (typeof option.isRepeatable === \"number\") {\n assert(\n Number.isSafeInteger(option.isRepeatable),\n `The option ${namedArrayToString(...path, option)} (index ${index}) has its \\`isRepeatable\\` value set to a number that is not an integer`,\n );\n assert(\n !(option.isRepeatable < 1),\n `The option ${namedArrayToString(...path, option)} (index ${index}) has its \\`isRepeatable\\` value set to a number below 1, which means it can't be used at all`,\n );\n assert(\n option.isRepeatable !== 1,\n `The option ${namedArrayToString(...path, option)} (index ${index}) has its \\`isRepeatable\\` value set to 1, which is no different that omitting the property entirely`,\n );\n }\n }\n });\n}\n\n/** Asserts that options have unique names in their scope */\nexport function assertOptionsHaveLocallyUniqueNames(spec: Subcommand): void {\n forEachSubcommand(spec, (spec, path) => {\n const optionNames = new Set<string>();\n for (const [index, option] of makeArray(spec.options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n !optionNames.has(name),\n `Option ${namedArrayToString(...path, option)} (index ${index}) has a non-unique name, '${name}'. Option names must be unique.`,\n );\n optionNames.add(name);\n }\n }\n });\n}\n\n/** Asserts that options don't shadow a persistent option */\nexport function assertOptionsDoNotShadowPersistentOptions(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n const persistentOptionNames = new Set(\n path\n .slice(0, -1)\n .flatMap((subcommand) =>\n subcommand.options\n ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name)\n : [],\n ),\n );\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n !persistentOptionNames.has(name),\n `The option ${namedArrayToString(...path, option)} (index ${index}) shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that a subcommand's option isn't a different option with exactly the\n * same names as one on a parent.\n *\n * Parsed values are looked up by name, so that parent's value would be\n * unreadable. A shared name is fine when each option has a name of its own, and\n * when the options are identical, such as an option kept in a constant.\n *\n * This skips persistent parent options, because\n * {@link assertOptionsDoNotShadowPersistentOptions} covers those and\n * `allowShadowingPersistentOptions` opts out of it.\n */\nexport function assertOptionsHaveDifferentNamesFromParents(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n const parents = path.slice(0, -1);\n for (const [index, option] of options.entries()) {\n for (const parent of parents) {\n for (const parentOption of makeArray(parent.options)) {\n const isClash =\n !parentOption.isPersistent &&\n isSameName(parentOption.name, option.name) &&\n !isSameStructure({ ...parentOption, name: 0 }, { ...option, name: 0 });\n assert(\n !isClash,\n `The option ${namedArrayToString(...path, option)} (index ${index}) has the same names as a different option, ${namedArrayToString(parent, parentOption)}, so the parent's value can't be read. Add a name that only one of them uses.`,\n );\n }\n }\n }\n });\n}\n\n/** Asserts that subcommands don't share a name with a sibling subcommand */\nexport function assertSubcommandsHaveLocallyUniqueNames(spec: Subcommand): void {\n forEachSubcommand(spec, (spec, path) => {\n const names = new Set<string>();\n for (const [index, subcommand] of makeArray(spec.subcommands).entries()) {\n for (const name of makeArray(subcommand.name)) {\n assert(\n !names.has(name),\n `The subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a non-unique name, ${repr(name)}. Subcommand names must be unique among the subcommands in the same array.`,\n );\n names.add(name);\n }\n }\n });\n}\n\n/**\n * Asserts that option names starting with a single dash aren't longer\n * than two characters, unless parserDirectives.flagsArePosixNoncompliant is\n * set on an ancestor command.\n */\nexport function assertLongOptionNamesDoNotStartWithSingleDash(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n // TODO: check if this is correct\n const skip =\n path\n .map((command) => command.parserDirectives?.flagsArePosixNoncompliant)\n .filter((value) => value !== undefined)\n .at(-1) || false;\n\n if (skip) return;\n\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n !((name.startsWith(\"-\") || name.startsWith(\"+\")) && !name.startsWith(\"--\") && name.length > 2),\n `Option ${namedArrayToString(...path, option)} (index ${index}) has a name that the parser is unable to parse, as it starts with a single '${name[0]}'. Tokens starting with that character are interpreted as short options, so passing '${name}' will be interpreted as '${Array.from(name.slice(1), (letter) => name[0] + letter).join(\" \")}'. \\`parserDirectives.flagsArePosixNoncompliant\\` must be \\`true\\` on an ancestor command.`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that all option names start with dashes\n */\nexport function assertOptionNamesStartWithDashes(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n // TODO: check if this is correct\n const skip =\n path\n .map((command) => command.parserDirectives?.flagsArePosixNoncompliant)\n .filter((value) => value !== undefined)\n .at(-1) || false;\n\n if (skip) return;\n\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n name.startsWith(\"-\") || name.startsWith(\"+\"),\n `Option ${namedArrayToString(...path, option)} (index ${index}) doesn't start with a dash or plus, the parser will be unable to find matching options without \\`parserDirectives.flagsAreNonPosixCompliant\\` being set to 'true' on an ancestor command`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that nothing is named exactly '--'\n */\nexport function assertNothingIsNamedDashDash(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n name !== \"--\",\n `Option ${namedArrayToString(...path, option)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`,\n );\n }\n }\n });\n forEachSubcommand(spec, (subcommands, path) => {\n for (const [index, subcommand] of makeArray(subcommands).entries()) {\n for (const name of makeArray(subcommand.name)) {\n assert(\n name !== \"--\",\n `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that no option arg separator is an empty string\n */\nexport function assertOptionArgSeparatorsHaveCharacters(spec: Subcommand): void {\n // FIXME: `subcommands` isn't an array\n forEachSubcommand(spec, (subcommands, path) => {\n for (const [index, subcommand] of makeArray(subcommands).entries()) {\n const separators = makeArray(subcommand.parserDirectives?.optionArgSeparators);\n for (const [sepIndex, separator] of separators.entries()) {\n assert(\n separator !== \"\",\n `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that options named \"+\" or \"-\" takes one arg\n */\nexport function assertPlusMinusOptionsTakeOneArg(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n // TODO: check if this is correct\n const skip =\n path\n .map((command) => command.parserDirectives?.flagsArePosixNoncompliant)\n .filter((value) => value !== undefined)\n .at(-1) || false;\n\n if (skip) return;\n\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n if (name !== \"-\" && name !== \"+\") {\n continue;\n }\n assert(\n option.args && !Array.isArray(option.args),\n `Option ${namedArrayToString(...path, option)} (index ${index}) must take exactly one argument`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that names do not have leading or trailing whitespace\n */\nexport function assertNamesHaveNoExtraWhitespace(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.name)) {\n assert(\n name === name.trim(),\n `Option ${namedArrayToString(...path, option)} (index ${index}) has a name with extra whitespace`,\n );\n }\n }\n });\n // FIXME: `subcommands` isn't an array\n forEachSubcommand(spec, (subcommands, path) => {\n for (const [index, subcommand] of makeArray(subcommands).entries()) {\n for (const name of makeArray(subcommand.name)) {\n assert(\n name === name.trim(),\n `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a name with extra whitespace`,\n );\n }\n }\n });\n forEachArgArray(spec, (args, path) => {\n for (const [index, arg] of makeArray(args).entries()) {\n for (const name of makeArray(arg.name)) {\n assert(\n name === name.trim(),\n `Arg in ${namedArrayToString(...path)} (at index ${index}) has a name with extra whitespace`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that everything has a description\n */\nexport function assertEverythingHasDescription(spec: Subcommand): void {\n assert(spec.description, `Spec has no description`);\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of makeArray(options).entries()) {\n assert(\n option.description,\n `Option ${namedArrayToString(...path, option)} (index ${index}) has no description`,\n );\n }\n });\n // FIXME: `subcommands` isn't an array\n forEachSubcommand(spec, (subcommands, path) => {\n for (const [index, subcommand] of makeArray(subcommands).entries()) {\n assert(\n subcommand.description,\n `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has no description`,\n );\n }\n });\n}\n\n/**\n * Asserts that descriptions have line length < 69\n */\nexport function assertDescriptionLineLengthUnder69(spec: Subcommand): void {\n if (spec.description) {\n assert(\n spec.description.split(\"\\n\").every((line) => line.length <= 68),\n `Spec has a description line over 68 characters`,\n );\n }\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of makeArray(options).entries()) {\n if (option.description) {\n assert(\n option.description.split(\"\\n\").every((line) => line.length <= 68),\n `Option ${namedArrayToString(...path, option)} (index ${index}) has a description line over 68 characters`,\n );\n }\n }\n });\n // FIXME: `subcommands` isn't an array\n forEachSubcommand(spec, (subcommands, path) => {\n for (const [index, subcommand] of makeArray(subcommands).entries()) {\n if (subcommand.description) {\n assert(\n subcommand.description.split(\"\\n\").every((line) => line.length <= 68),\n `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a description line over 68 characters`,\n );\n }\n }\n });\n}\n\nexport function assertRequiresSeparatorTakesOneArg(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n for (const [index, option] of makeArray(options).entries()) {\n if (option.requiresSeparator) {\n const args = makeArray(option.args);\n assert(\n getMinArgs(args) <= 1,\n `Option ${namedArrayToString(...path, option)} (index ${index}) takes a minimum of ${getMinArgs(args)} arg(s), but because of requiresSeparator, it must instead be 0 or 1`,\n );\n assert(\n getMaxArgs(args) === 1,\n `Option ${namedArrayToString(...path, option)} (index ${index}) takes a maximum of ${getMaxArgs(args)} arg(s), but because of requiresSeparator, it's only able to take 1`,\n );\n }\n }\n });\n}\n\nexport function assertCommonOptionsArePersistent(spec: Subcommand): void {\n const failures: [string | readonly string[], string][] = [];\n forEachSubcommand(spec, (subcommand, path) => {\n if (!subcommand.subcommands) {\n return;\n }\n for (const option of makeArray(subcommand.options)) {\n const allChildSubcommands: Subcommand[] = [];\n forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));\n\n const allShareOption = allChildSubcommands.every((cmd) =>\n makeArray(cmd.options).some((o) => isSameName(option.name, o.name)),\n );\n\n if (allShareOption && !failures.some(([name]) => isSameName(name, option.name))) {\n failures.push([option.name, namedArrayToString(...path, option)]);\n }\n }\n });\n assert(\n failures.length === 0,\n `The following options were also defined by all subcommands. Instead, define the option once, and use \\`isPersistent: true\\` to persist it across all subcommands.\n${failures.map(([, line]) => \" * \" + line).join(\"\\n\")}\n`,\n );\n}\n\n/**\n * Asserts that references to other options by name actually refer to options\n * that exist in scope\n */\nexport function assertOptionNameReferencesExist(spec: Subcommand): void {\n forEachOptionArray(spec, (options, path) => {\n const optionNames = new Set([\n ...path\n .slice(0, -1)\n .flatMap((subcommand) =>\n subcommand.options\n ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name)\n : [],\n ),\n ...options.flatMap((option) => option.name),\n ]);\n for (const [index, option] of makeArray(options).entries()) {\n for (const name of makeArray(option.dependsOn)) {\n assert(\n optionNames.has(name),\n `The option ${namedArrayToString(...path, option)} (index ${index}) depends on an option named ${repr(name)}, which doesn't exist in its scope`,\n );\n }\n for (const name of makeArray(option.exclusiveOn)) {\n assert(\n optionNames.has(name),\n `The option ${namedArrayToString(...path, option)} (index ${index}) is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`,\n );\n }\n }\n });\n}\n\n/**\n * Asserts that commands that will match subcommands by prefix do not take arguments\n */\nexport function assertPrefixMatchCommandsHaveNoArguments(spec: Subcommand): void {\n forEachSubcommand(spec, (subcommand, path) => {\n // Mirror the parser's default: prefix matching is OFF unless a command on\n // the path turns it on. `scan` in parse.ts initializes the flag to `false` and\n // only ever sets it `true`, so the ambiguity this guards against can only\n // arise when some ancestor (or the command itself) opts in.\n const matchesPrefix = path.some((cmd) => cmd.parserDirectives?.subcommandsMatchUniquePrefix === true);\n\n if (!matchesPrefix) return;\n\n assert(\n !(subcommand.subcommands && subcommand.args),\n `The command ${namedArrayToString(\n ...path,\n )} has at least one argument and subcommand, but matches subcommands based on unique prefixes. To fix this, use \\`parserDirectives: { subcommandsMatchUniquePrefix: false }\\``,\n );\n });\n}\n\nexport type TestNamingStyle = \"sentence\" | \"function\";\n\n/** Options that can be provided to `test` */\nexport interface TestOptions {\n /**\n * Allows options to shadow persistent options.\n *\n * This behavior may be desirable if you want to override an option on a\n * particular subcommand, but not for others.\n *\n * Be aware that this is not idiomatic.\n */\n allowShadowingPersistentOptions?: boolean;\n\n /**\n * Allows subcommands and options to not have a description\n */\n allowNoDescription?: boolean;\n\n /**\n * Allows descriptions to have lines over 68 characters long\n */\n allowLongDescriptionLines?: boolean;\n\n /**\n * Allow commands to have `parserDirectives.subcommandsMatchUniquePrefix` with args\n *\n * This is not allowed by default because it's probably a mistake. It makes\n * arguments ambiguous, users may not know if their invocation will run a\n * subcommand or the command.\n */\n allowMatchingSubcommandPrefixAndArgs?: boolean;\n\n /**\n * Change the naming style of the tests.\n *\n * - `sentence`: Use proper sentence casing, punctuation, etc\n * - `function`: Use the name of the assert function that will be executed\n */\n namingStyle?: TestNamingStyle;\n}\n\nfunction getName(namingStyle: TestNamingStyle, names: { [K in TestNamingStyle]: string }) {\n return names[namingStyle];\n}\n\nexport interface TestContextLike {\n step(name: string, fn: () => unknown | Promise<unknown>): unknown | Promise<unknown>;\n}\n\nasync function runStep(\n context: TestContextLike | undefined,\n name: string,\n fn: () => unknown | Promise<unknown>,\n): Promise<void> {\n if (context) {\n await context.step(name, fn);\n return;\n }\n await fn();\n}\n\n/**\n * Validate your CLI in your test suite\n *\n * This enforces certain constraints that the type system cannot. The parser\n * may ignore these issues, producing correct output that _appears_ to be\n * wrong.\n */\nexport function test(spec: Subcommand, options: TestOptions = {}): (t?: TestContextLike) => Promise<void> {\n const {\n allowShadowingPersistentOptions = false,\n allowNoDescription = false,\n allowLongDescriptionLines = false,\n allowMatchingSubcommandPrefixAndArgs = false,\n namingStyle = \"sentence\",\n } = options;\n return async (t) => {\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Required arguments don't follow optional arguments\",\n function: \"assertRequiredArgumentsDoNotFollowOptionalArguments\",\n }),\n () => {\n assertRequiredArgumentsDoNotFollowOptionalArguments(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Repeatable options that are numbers are integers greater than 1\",\n function: \"assertRepeatableOptionsArePositiveIntegers\",\n }),\n () => {\n assertRepeatableOptionsArePositiveIntegers(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Long option names don't start with a single dash\",\n function: \"assertLongOptionNamesDoNotStartWithSingleDash\",\n }),\n () => {\n assertLongOptionNamesDoNotStartWithSingleDash(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Option names all start with dashes\",\n function: \"assertOptionNamesStartWithDashes\",\n }),\n () => {\n assertOptionNamesStartWithDashes(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Options have locally unique names\",\n function: \"assertOptionsHaveLocallyUniqueNames\",\n }),\n () => {\n assertOptionsHaveLocallyUniqueNames(spec);\n },\n );\n if (!allowShadowingPersistentOptions) {\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Options don't shadow persistent options\",\n function: \"assertOptionsDoNotShadowPersistentOptions\",\n }),\n () => {\n assertOptionsDoNotShadowPersistentOptions(spec);\n },\n );\n }\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Different options on a subcommand and its parents have different names\",\n function: \"assertOptionsHaveDifferentNamesFromParents\",\n }),\n () => {\n assertOptionsHaveDifferentNamesFromParents(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Subcommands have locally unique names\",\n function: \"assertSubcommandsHaveLocallyUniqueNames\",\n }),\n () => {\n assertSubcommandsHaveLocallyUniqueNames(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Nothing is named '--'\",\n function: \"assertNothingIsNamedDashDash\",\n }),\n () => {\n assertNothingIsNamedDashDash(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Option arg separators are not an empty string\",\n function: \"assertOptionArgSeparatorsHaveCharacters\",\n }),\n () => {\n assertOptionArgSeparatorsHaveCharacters(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Options named '-' or '+' take exactly one argument\",\n function: \"assertPlusMinusOptionsTakeOneArg\",\n }),\n () => {\n assertPlusMinusOptionsTakeOneArg(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Names have no extra leading or trailing whitespace\",\n function: \"assertNamesHaveNoExtraWhitespace\",\n }),\n () => {\n assertNamesHaveNoExtraWhitespace(spec);\n },\n );\n if (!allowNoDescription) {\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Everything has a description\",\n function: \"assertEverythingHasDescription\",\n }),\n () => {\n assertEverythingHasDescription(spec);\n },\n );\n }\n if (!allowLongDescriptionLines) {\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Each line in descriptions is less than or equal to 68 characters\",\n function: \"assertDescriptionLineLengthUnder69\",\n }),\n () => {\n assertDescriptionLineLengthUnder69(spec);\n },\n );\n }\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Options with `requiresSeparator` take exactly one argument\",\n function: \"assertRequiresSeparatorTakesOneArg\",\n }),\n () => {\n assertRequiresSeparatorTakesOneArg(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Common options are not defined on every subcommand\",\n function: \"assertCommonOptionsArePersistent\",\n }),\n () => {\n assertCommonOptionsArePersistent(spec);\n },\n );\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"References to other options by name are valid\",\n function: \"assertOptionNameReferencesExist\",\n }),\n () => {\n assertOptionNameReferencesExist(spec);\n },\n );\n if (!allowMatchingSubcommandPrefixAndArgs) {\n await runStep(\n t,\n getName(namingStyle, {\n sentence: \"Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands\",\n function: \"assertPrefixMatchCommandsHaveNoArguments\",\n }),\n () => {\n assertPrefixMatchCommandsHaveNoArguments(spec);\n },\n );\n }\n };\n}\n\nexport const testSpec = test;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clo4/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "clo4's CLI framework: fully declarative, fast",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"arg-parser",
|
|
7
|
+
"args",
|
|
8
|
+
"argument-parser",
|
|
9
|
+
"cli",
|
|
10
|
+
"command-line",
|
|
11
|
+
"help",
|
|
12
|
+
"options",
|
|
13
|
+
"shell-completion",
|
|
14
|
+
"subcommands",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/clo4/cli.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./testing": {
|
|
32
|
+
"types": "./dist/testing.d.ts",
|
|
33
|
+
"import": "./dist/testing.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"build:bench": "tsdown --config tsdown.bench.config.ts",
|
|
42
|
+
"bench": "pnpm build && pnpm build:bench && node bench/dist/run.js",
|
|
43
|
+
"check": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.bench.json --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"lint": "oxlint",
|
|
46
|
+
"lint:fix": "oxlint --fix",
|
|
47
|
+
"fmt": "oxfmt",
|
|
48
|
+
"fmt:check": "oxfmt --check",
|
|
49
|
+
"verify": "pnpm fmt:check && pnpm lint && pnpm check && pnpm build && pnpm test"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.6.6",
|
|
53
|
+
"cac": "^7.1.0",
|
|
54
|
+
"commander": "^15.0.0",
|
|
55
|
+
"oxfmt": "^0.75.0",
|
|
56
|
+
"oxlint": "^1.90.0",
|
|
57
|
+
"rolldown": "^1.2.16",
|
|
58
|
+
"tsdown": "^0.23.4",
|
|
59
|
+
"typescript": "^7.0.2",
|
|
60
|
+
"vitest": "^5.0.5",
|
|
61
|
+
"yargs": "^18.2.0"
|
|
62
|
+
},
|
|
63
|
+
"packageManager": "pnpm@12.16.1"
|
|
64
|
+
}
|