@loomcli/core 0.4.0 → 0.6.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/dist/application.d.ts +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +121 -55
- package/dist/plugin.js +496 -126
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +58 -0
- package/dist/sources.js +258 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
package/dist/validation.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { schemaOptions } from './context.js';
|
|
2
|
-
import {
|
|
2
|
+
import { escapeControlCharacters } from './controls.js';
|
|
3
|
+
import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
|
|
4
|
+
import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
|
|
5
|
+
import { booleanOptionValueRule, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
|
|
3
6
|
import { booleanValue } from './options.js';
|
|
7
|
+
import { isPlainObject } from './plain.js';
|
|
8
|
+
import { notAnObject } from './plugin-rules.js';
|
|
9
|
+
import { validatorFailed } from './rules.js';
|
|
4
10
|
/** Every declaration in validation order: the globals first, then the reading Command's own. */
|
|
5
11
|
function scoped(inputs) {
|
|
6
12
|
return [
|
|
@@ -46,6 +52,27 @@ class ValidatedInputs {
|
|
|
46
52
|
return { [input.name]: this.#values.get(input) };
|
|
47
53
|
}
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The one check every `argument()`, `option()`, and `globalOption()` call makes before it reads its
|
|
57
|
+
* config, so a call in JavaScript that supplies none, or a value of another kind, reports a
|
|
58
|
+
* declaration fault and not a TypeError. `place` is where the call sits.
|
|
59
|
+
*/
|
|
60
|
+
export function checkInputConfig(declared, place) {
|
|
61
|
+
const { config, kind, name } = declared;
|
|
62
|
+
if (isPlainObject(config)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const subject = kind === 'argument' ? 'Argument' : 'Option';
|
|
66
|
+
const site = callSite(`${subject} ${quoted(name)}`, {
|
|
67
|
+
arguments: [name, config],
|
|
68
|
+
...place,
|
|
69
|
+
});
|
|
70
|
+
throw new DeclarationError(notAnObject, {
|
|
71
|
+
correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
|
|
72
|
+
findings: [siteFinding(site, '1')],
|
|
73
|
+
sentence: `${site.subject} declares a config that is not an object.`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
49
76
|
/**
|
|
50
77
|
* Authoring's snapshot of one config. An array default is the one declared value core hands to an
|
|
51
78
|
* action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
|
|
@@ -59,8 +86,25 @@ export function captureConfig(config) {
|
|
|
59
86
|
* A declaration error names the declaration, because the author reads the declaration to fix it.
|
|
60
87
|
* An argument declares and reads under one name, so the two namings differ for options alone.
|
|
61
88
|
*/
|
|
62
|
-
function
|
|
63
|
-
return input.kind === 'argument'
|
|
89
|
+
export function declarationSubject(input) {
|
|
90
|
+
return input.kind === 'argument'
|
|
91
|
+
? `Argument ${quoted(input.name)}`
|
|
92
|
+
: `Option ${quoted(input.name)}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Where one input was declared: a global option at its `globalOption()` call, and every other input
|
|
96
|
+
* at its own `argument()` or `option()` call on the Command at `path`.
|
|
97
|
+
*/
|
|
98
|
+
export function inputPlace(input, scope) {
|
|
99
|
+
return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
|
|
103
|
+
* finding for an input's own call starts from it, so each marks the call the same way. `subject`
|
|
104
|
+
* is the sentence's name for the input.
|
|
105
|
+
*/
|
|
106
|
+
export function declaringSite(input, place, subject = declarationSubject(input)) {
|
|
107
|
+
return callSite(subject, { arguments: [input.name, input.config], ...place });
|
|
64
108
|
}
|
|
65
109
|
/**
|
|
66
110
|
* The token an operator would type for one declaration: `--file` for an option, `-F` when the
|
|
@@ -73,33 +117,44 @@ function spellingOf(input) {
|
|
|
73
117
|
return input.name;
|
|
74
118
|
}
|
|
75
119
|
const { config } = input;
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
if (config.shortOnly === true && config.short !== undefined) {
|
|
121
|
+
return `-${config.short}`;
|
|
122
|
+
}
|
|
123
|
+
// A negative-only Boolean option accepts its negative form alone.
|
|
124
|
+
return config.type === 'boolean' && config.polarity === 'negative'
|
|
125
|
+
? `--no-${input.name}`
|
|
78
126
|
: `--${input.name}`;
|
|
79
127
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
/**
|
|
129
|
+
* An input diagnostic names the declaration by kind and by the spelling that reaches it. A value an
|
|
130
|
+
* input source filled adds its source in parentheses, so the operator learns where it came from.
|
|
131
|
+
*/
|
|
132
|
+
function suppliedName(input, spelling, origin) {
|
|
133
|
+
if (input.kind === 'argument') {
|
|
134
|
+
return `Argument "${spelling}"`;
|
|
135
|
+
}
|
|
136
|
+
return origin === undefined ? `Option "${spelling}"` : `Option "${spelling}" (from ${origin})`;
|
|
83
137
|
}
|
|
84
138
|
/**
|
|
85
139
|
* The default sentence for an omitted required input. An argument and an option keep the wording
|
|
86
140
|
* each phase used before omission became one problem, and a collected input asks for one value
|
|
87
141
|
* more than a scalar does.
|
|
88
142
|
*/
|
|
89
|
-
function missingMessage(input, spelling,
|
|
143
|
+
function missingMessage(input, spelling, facts) {
|
|
144
|
+
const { collected, origin } = facts;
|
|
90
145
|
return input.kind === 'argument'
|
|
91
146
|
? `Argument "${spelling}" requires ${collected ? 'at least one value' : 'a value'}. Supply a value for "${spelling}".`
|
|
92
|
-
:
|
|
147
|
+
: `${suppliedName(input, spelling, origin)} is required. Supply ${collected ? 'at least one value' : 'a value'}.`;
|
|
93
148
|
}
|
|
94
149
|
/**
|
|
95
150
|
* A multiple option collects its occurrences and a variadic argument collects the remaining
|
|
96
|
-
* tokens, so either one carries
|
|
151
|
+
* tokens, so either one carries a `string[]` as its raw value and validates each value alone.
|
|
97
152
|
*/
|
|
98
153
|
function collects(input) {
|
|
99
154
|
return input.kind === 'option' ? input.config.multiple === true : input.config.variadic === true;
|
|
100
155
|
}
|
|
101
156
|
/**
|
|
102
|
-
* The declaration flag that sends an omitted value to its own
|
|
157
|
+
* The declaration flag that sends an omitted value to its own validator. Every declaration reads it
|
|
103
158
|
* here, and the declaration rules below reject it wherever another rule already decides absence.
|
|
104
159
|
*/
|
|
105
160
|
export function validatesOmission(input) {
|
|
@@ -114,72 +169,106 @@ function suppliedOption(options, name, collected) {
|
|
|
114
169
|
function copied(value) {
|
|
115
170
|
return Array.isArray(value) ? [...value] : value;
|
|
116
171
|
}
|
|
117
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Without a validator the raw shape is the declared default's only contract. With one, a default
|
|
174
|
+
* of several values must still be an array, because each of its values passes the validator.
|
|
175
|
+
*/
|
|
118
176
|
function holdsRawDefault(input) {
|
|
119
177
|
const value = input.config.default;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
178
|
+
if (!collects(input)) {
|
|
179
|
+
return input.config.validate !== undefined || typeof value === 'string';
|
|
180
|
+
}
|
|
181
|
+
return (Array.isArray(value) &&
|
|
182
|
+
(input.config.validate !== undefined ||
|
|
183
|
+
value.every((entry) => typeof entry === 'string')));
|
|
123
184
|
}
|
|
124
185
|
/**
|
|
125
|
-
* `validateOmitted: true` is the one way an omitted scalar reaches its
|
|
126
|
-
* that already decides absence rejects it, and the flag needs a
|
|
186
|
+
* `validateOmitted: true` is the one way an omitted scalar reaches its validator, so every other
|
|
187
|
+
* rule that already decides absence rejects it, and the flag needs a validator to receive the
|
|
188
|
+
* omission.
|
|
127
189
|
*/
|
|
128
|
-
function checkOmissionValidation(input, subject) {
|
|
190
|
+
function checkOmissionValidation(input, site, subject) {
|
|
129
191
|
const { config } = input;
|
|
192
|
+
const decided = (sentence, correction) => factFault(omissionAlreadyDecided, site, { correction, fact: 'validateOmitted', sentence });
|
|
130
193
|
if (config.required) {
|
|
131
|
-
throw
|
|
194
|
+
throw decided(`${subject} is required and declares validateOmitted.`, 'Remove validateOmitted or make the input optional.');
|
|
132
195
|
}
|
|
133
196
|
if (hasDefault(input)) {
|
|
134
|
-
throw
|
|
197
|
+
throw decided(`${subject} declares a default and validateOmitted.`, 'Remove one; the default already fills an omitted value.');
|
|
135
198
|
}
|
|
136
199
|
if (collects(input)) {
|
|
137
|
-
throw
|
|
200
|
+
throw decided(`${subject} takes several values and declares validateOmitted.`, 'Remove validateOmitted; with no values the action receives an empty array and no validator runs.');
|
|
138
201
|
}
|
|
139
202
|
if (config.validate === undefined) {
|
|
140
|
-
throw
|
|
203
|
+
throw factFault(omissionWithoutValidator, site, {
|
|
204
|
+
correction: 'Add validate or remove validateOmitted.',
|
|
205
|
+
fact: 'validateOmitted',
|
|
206
|
+
sentence: `${subject} declares validateOmitted without a validator.`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** The value rules a Boolean option may not declare, in the order its diagnostic names them. */
|
|
211
|
+
const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
|
|
212
|
+
/** Whether a `validate` value is a Standard Schema v1 object that core can call. */
|
|
213
|
+
function isStandardSchema(validator) {
|
|
214
|
+
if (validator === null || (typeof validator !== 'object' && typeof validator !== 'function')) {
|
|
215
|
+
return false;
|
|
141
216
|
}
|
|
217
|
+
const props = Reflect.get(validator, '~standard');
|
|
218
|
+
return (typeof props === 'object' &&
|
|
219
|
+
props !== null &&
|
|
220
|
+
Reflect.get(props, 'version') === 1 &&
|
|
221
|
+
typeof Reflect.get(props, 'vendor') === 'string' &&
|
|
222
|
+
typeof Reflect.get(props, 'validate') === 'function');
|
|
142
223
|
}
|
|
143
|
-
function checkDeclaration(input, subject) {
|
|
224
|
+
function checkDeclaration(input, site, subject) {
|
|
144
225
|
const { config } = input;
|
|
145
226
|
if (input.kind === 'option' && input.config.type === 'boolean') {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
227
|
+
const declared = valueRules.find((key) => key in config);
|
|
228
|
+
if (declared !== undefined) {
|
|
229
|
+
throw factFault(booleanOptionValueRule, site, {
|
|
230
|
+
correction: `Remove ${declared}; use polarity to control its absent value.`,
|
|
231
|
+
fact: declared,
|
|
232
|
+
sentence: `${subject} is Boolean and declares ${declared}.`,
|
|
233
|
+
});
|
|
151
234
|
}
|
|
152
235
|
return;
|
|
153
236
|
}
|
|
154
237
|
if (config.required !== undefined && typeof config.required !== 'boolean') {
|
|
155
|
-
throw
|
|
238
|
+
throw flagFault(site, 'required');
|
|
156
239
|
}
|
|
157
240
|
if (input.kind === 'argument' &&
|
|
158
241
|
input.config.variadic !== undefined &&
|
|
159
242
|
typeof input.config.variadic !== 'boolean') {
|
|
160
|
-
throw
|
|
243
|
+
throw flagFault(site, 'variadic');
|
|
161
244
|
}
|
|
162
245
|
// The test reads presence, not truth, so a declared `undefined` is a declaration to reject.
|
|
163
246
|
if ('validateOmitted' in config && typeof config.validateOmitted !== 'boolean') {
|
|
164
|
-
throw
|
|
247
|
+
throw flagFault(site, 'validateOmitted');
|
|
165
248
|
}
|
|
166
249
|
if (config.required && Object.hasOwn(config, 'default')) {
|
|
167
|
-
throw
|
|
250
|
+
throw factFault(requiredWithDefault, site, {
|
|
251
|
+
correction: 'Remove the default or make the input optional.',
|
|
252
|
+
fact: 'default',
|
|
253
|
+
sentence: `${subject} is required and declares a default.`,
|
|
254
|
+
});
|
|
168
255
|
}
|
|
169
256
|
if (validatesOmission(input)) {
|
|
170
|
-
checkOmissionValidation(input, subject);
|
|
257
|
+
checkOmissionValidation(input, site, subject);
|
|
171
258
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
typeof schema['~standard'].vendor !== 'string' ||
|
|
179
|
-
typeof schema['~standard'].validate !== 'function')) {
|
|
180
|
-
throw new DeclarationError(`${subject} validate must be a Standard Schema v1 object. Supply a compatible schema.`);
|
|
259
|
+
if (config.validate !== undefined && !isStandardSchema(config.validate)) {
|
|
260
|
+
throw factFault(notAValidator, site, {
|
|
261
|
+
correction: 'Supply a compatible validator.',
|
|
262
|
+
fact: 'validate',
|
|
263
|
+
sentence: `${subject} validate must be a Standard Schema v1 object.`,
|
|
264
|
+
});
|
|
181
265
|
}
|
|
182
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* One issue a validator returned, checked where it enters. Core keeps every own enumerable field
|
|
269
|
+
* the issue carries and rewrites only `path`, reducing each segment to its key, so a field core
|
|
270
|
+
* never reads, such as a validator's issue code, reaches a failure view unchanged.
|
|
271
|
+
*/
|
|
183
272
|
function readIssue(issue) {
|
|
184
273
|
if (issue === null || typeof issue !== 'object' || !('message' in issue)) {
|
|
185
274
|
throw new Error('The validator returned an invalid Standard Schema issue.');
|
|
@@ -190,7 +279,7 @@ function readIssue(issue) {
|
|
|
190
279
|
}
|
|
191
280
|
const suppliedPath = 'path' in issue ? issue.path : undefined;
|
|
192
281
|
if (suppliedPath === undefined) {
|
|
193
|
-
return { message };
|
|
282
|
+
return { ...issue, message };
|
|
194
283
|
}
|
|
195
284
|
if (!Array.isArray(suppliedPath)) {
|
|
196
285
|
throw new Error('The validator returned an invalid Standard Schema issue path.');
|
|
@@ -202,19 +291,21 @@ function readIssue(issue) {
|
|
|
202
291
|
}
|
|
203
292
|
return key;
|
|
204
293
|
});
|
|
205
|
-
return { message, path };
|
|
294
|
+
return { ...issue, message, path };
|
|
206
295
|
}
|
|
207
296
|
/**
|
|
208
297
|
* A broken validator is a fault in the declaration, whichever value reached it, so its diagnostic
|
|
209
|
-
* names the declaration
|
|
298
|
+
* names the declaration and its finding marks the validator on the call at `place`. Returned issues
|
|
299
|
+
* belong to the value, so the caller names those.
|
|
210
300
|
*/
|
|
211
|
-
async function validate(input, raw,
|
|
212
|
-
const
|
|
213
|
-
|
|
301
|
+
async function validate(input, raw, call) {
|
|
302
|
+
const { context, place } = call;
|
|
303
|
+
const validator = input.config.validate;
|
|
304
|
+
if (validator === undefined) {
|
|
214
305
|
return { value: raw };
|
|
215
306
|
}
|
|
216
307
|
try {
|
|
217
|
-
const result = await
|
|
308
|
+
const result = await validator['~standard'].validate(raw, schemaOptions(context));
|
|
218
309
|
if (result === null || typeof result !== 'object') {
|
|
219
310
|
throw new Error('The validator returned an invalid Standard Schema result.');
|
|
220
311
|
}
|
|
@@ -228,10 +319,51 @@ async function validate(input, raw, context) {
|
|
|
228
319
|
return { issues: Array.from(issues, readIssue) };
|
|
229
320
|
}
|
|
230
321
|
catch (error) {
|
|
231
|
-
|
|
232
|
-
|
|
322
|
+
// The reason is the author's detail: a distributed build shows the generic defect message.
|
|
323
|
+
const site = place === undefined ? undefined : declaringSite(input, place);
|
|
324
|
+
throw new DeclarationError(validatorFailed, {
|
|
325
|
+
correction: 'Fix the validator.',
|
|
326
|
+
findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
|
|
327
|
+
sentence: `${declarationSubject(input)} validator failed unexpectedly: ${asSentence(reasonOf(error))}`,
|
|
328
|
+
}, { cause: error });
|
|
233
329
|
}
|
|
234
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* One validation of a declared value. A multiple option or a variadic argument passes each of its
|
|
333
|
+
* values through the validator in order, and each issue reads at its value's position before its
|
|
334
|
+
* own path, so the action receives the array of outputs. Every other input passes its value once.
|
|
335
|
+
* Each call reads a fresh context whose arrays are copies, so a write to them never reaches the
|
|
336
|
+
* next call. The host is the one captured object that every call and the action share.
|
|
337
|
+
*/
|
|
338
|
+
async function validateDeclared(input, raw, call) {
|
|
339
|
+
const { context, place, signal } = call;
|
|
340
|
+
if (!collects(input) || input.config.validate === undefined) {
|
|
341
|
+
return validate(input, raw, { context: context(), place });
|
|
342
|
+
}
|
|
343
|
+
if (!Array.isArray(raw)) {
|
|
344
|
+
// The parser, the input sources, and the declaration rules only ever supply an array here.
|
|
345
|
+
throw new TypeError(`${declarationSubject(input)} reached validation without an array of values.`);
|
|
346
|
+
}
|
|
347
|
+
const outputs = [];
|
|
348
|
+
const issues = [];
|
|
349
|
+
for (const [position, value] of raw.entries()) {
|
|
350
|
+
if (signal?.aborted) {
|
|
351
|
+
// A cancelled run starts no further call; the run resolves its cancellation code instead.
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
const result = await validate(input, value, { context: context(), place });
|
|
355
|
+
if (result.issues === undefined) {
|
|
356
|
+
outputs.push(result.value);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// The issue keeps its own fields, and only its path gains the value's position.
|
|
360
|
+
for (const issue of reported(result.issues)) {
|
|
361
|
+
issues.push({ ...issue, path: [position, ...(issue.path ?? [])] });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return issues.length === 0 ? { value: outputs } : { issues };
|
|
366
|
+
}
|
|
235
367
|
/**
|
|
236
368
|
* The dotted path an issue names inside a value, or `undefined` when the issue names the value
|
|
237
369
|
* itself. Core's default text and an application's own view read a position through this one
|
|
@@ -244,73 +376,97 @@ export function issuePath(issue) {
|
|
|
244
376
|
return path === undefined || path === '' ? undefined : path;
|
|
245
377
|
}
|
|
246
378
|
/**
|
|
247
|
-
* The issues one rejection reports. A
|
|
379
|
+
* The issues one rejection reports. A validator that returned none still rejected the value, so the
|
|
248
380
|
* placeholder stands in for its silence. Reporting takes this list once: the reported problem
|
|
249
381
|
* carries it and the default text is derived from it, so a view and core read the same issues.
|
|
250
382
|
*/
|
|
251
383
|
function reported(issues) {
|
|
252
384
|
return issues.length === 0
|
|
253
|
-
? [
|
|
385
|
+
? [
|
|
386
|
+
{
|
|
387
|
+
message: 'The validator rejected this value without an explanation. Supply a different value.',
|
|
388
|
+
},
|
|
389
|
+
]
|
|
254
390
|
: issues;
|
|
255
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* One line per issue under the input's subject. A path segment can hold text the operator typed,
|
|
394
|
+
* such as a key of a record, so the line escapes the path; `issuePath` keeps it raw for a view.
|
|
395
|
+
*/
|
|
256
396
|
function messages(subject, issues) {
|
|
257
397
|
return issues.map((issue) => {
|
|
258
398
|
const path = issuePath(issue);
|
|
259
|
-
|
|
399
|
+
const position = path === undefined ? '' : ` at ${escapeControlCharacters(path)}`;
|
|
400
|
+
return `${subject}${position}: ${issue.message}`;
|
|
260
401
|
});
|
|
261
402
|
}
|
|
403
|
+
/** The fault a default of the wrong raw shape reports, by the shape its declaration expects. */
|
|
404
|
+
function defaultShapeFault(input, site, subject) {
|
|
405
|
+
const fault = (sentence, correction) => factFault(defaultShape, site, { correction, fact: 'default', sentence });
|
|
406
|
+
if (!collects(input)) {
|
|
407
|
+
return fault(`${subject} default must be a string without a validator.`, 'Supply a string default.');
|
|
408
|
+
}
|
|
409
|
+
return input.config.validate === undefined
|
|
410
|
+
? fault(`${subject} default must be an array of strings without a validator.`, 'Supply a string array default.')
|
|
411
|
+
: fault(`${subject} default must be an array.`, 'Supply an array of values.');
|
|
412
|
+
}
|
|
262
413
|
/** A declared `default: undefined` is a default, so presence is the key, never the value. */
|
|
263
414
|
function hasDefault(input) {
|
|
264
415
|
return Object.hasOwn(input.config, 'default');
|
|
265
416
|
}
|
|
266
417
|
/**
|
|
267
|
-
* Every declaration rule that reads the declaration alone. It is synchronous, so
|
|
268
|
-
*
|
|
269
|
-
* can be asynchronous, is left to `run()`. A
|
|
270
|
-
*
|
|
271
|
-
* declaration itself.
|
|
418
|
+
* Every declaration rule that reads the declaration alone. It is synchronous, so the call that
|
|
419
|
+
* declares an input applies it, and build applies it to an input a lifecycle hook declared; only
|
|
420
|
+
* validating a default through its validator, which can be asynchronous, is left to `run()`. A
|
|
421
|
+
* contributor that declares under its own name, such as a plugin, supplies the subject its
|
|
422
|
+
* diagnostics read with; every other caller is named by the declaration itself.
|
|
272
423
|
*/
|
|
273
424
|
export function checkDeclarations(inputs, named) {
|
|
274
|
-
for (const input of inputs) {
|
|
275
|
-
checkDeclaration(input, named ??
|
|
276
|
-
}
|
|
277
|
-
for (const input of inputs.filter((entry) => hasDefault(entry))) {
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
throw new DeclarationError(collects(input)
|
|
281
|
-
? `${subject} default must be an array of strings without a schema. Supply a string array default.`
|
|
282
|
-
: `${subject} default must be a string without a schema. Supply a string default.`);
|
|
425
|
+
for (const { input, site } of inputs) {
|
|
426
|
+
checkDeclaration(input, site, named ?? declarationSubject(input));
|
|
427
|
+
}
|
|
428
|
+
for (const { input, site } of inputs.filter((entry) => hasDefault(entry.input))) {
|
|
429
|
+
if (!holdsRawDefault(input)) {
|
|
430
|
+
throw defaultShapeFault(input, site, named ?? declarationSubject(input));
|
|
283
431
|
}
|
|
284
432
|
}
|
|
285
433
|
}
|
|
286
434
|
/**
|
|
287
435
|
* Every declared default, validated before any token is read. The host is captured by then, so a
|
|
288
|
-
* default's
|
|
436
|
+
* default's validator reads the same Host its action will, under the `default` phase. `places`
|
|
437
|
+
* says where each declaration sits, which a rejected default's finding rebuilds.
|
|
289
438
|
*/
|
|
290
|
-
export async function prepareInputs(inputs, host) {
|
|
439
|
+
export async function prepareInputs(inputs, host, places) {
|
|
291
440
|
const declarations = scoped(inputs);
|
|
292
|
-
checkDeclarations(declarations.map((entry) => entry.input));
|
|
293
441
|
const defaults = new Map();
|
|
294
442
|
for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
|
|
295
443
|
const { input } = entry;
|
|
296
|
-
const subject =
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
input: identityOf(entry),
|
|
300
|
-
|
|
444
|
+
const subject = declarationSubject(input);
|
|
445
|
+
const place = places.get(input);
|
|
446
|
+
const result = await validateDeclared(input, input.config.default, {
|
|
447
|
+
context: () => ({ host, input: identityOf(entry), phase: 'default' }),
|
|
448
|
+
place,
|
|
301
449
|
});
|
|
302
450
|
if (result.issues !== undefined) {
|
|
303
|
-
|
|
451
|
+
const site = place === undefined ? undefined : declaringSite(input, place);
|
|
452
|
+
throw new DeclarationError(invalidDefault, {
|
|
453
|
+
correction: 'Fix the default or its validator.',
|
|
454
|
+
findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
|
|
455
|
+
sentence: [
|
|
456
|
+
`${subject} has an invalid default.`,
|
|
457
|
+
...messages(subject, reported(result.issues)),
|
|
458
|
+
].join('\n'),
|
|
459
|
+
});
|
|
304
460
|
}
|
|
305
461
|
defaults.set(input, result.value);
|
|
306
462
|
}
|
|
307
463
|
return defaults;
|
|
308
464
|
}
|
|
309
465
|
/**
|
|
310
|
-
* An array default reaches the action as its own copy, so an action that mutates its
|
|
311
|
-
* rewrites neither the declaration nor the next invocation.
|
|
312
|
-
*
|
|
313
|
-
*
|
|
466
|
+
* An array default reaches the action as its own copy, so an action that mutates its array
|
|
467
|
+
* rewrites neither the declaration nor the next invocation. One prepared default serves every
|
|
468
|
+
* invocation of a run, and an unvalidated default is the declared array itself, so each read
|
|
469
|
+
* copies it. Every other output passes through unchanged.
|
|
314
470
|
*/
|
|
315
471
|
function freshDefault(value) {
|
|
316
472
|
return Array.isArray(value) ? [...value] : value;
|
|
@@ -339,50 +495,95 @@ function suppliedInputs(declarations, supplied) {
|
|
|
339
495
|
}
|
|
340
496
|
return { args, options };
|
|
341
497
|
}
|
|
498
|
+
/** The Boolean grammar's one issue, which a variable outside it reports. */
|
|
499
|
+
const grammarIssues = [{ message: 'Use true, false, 1, or 0.' }];
|
|
500
|
+
/**
|
|
501
|
+
* Every declaration in the order this phase reports its problems: the globals, then each plugin
|
|
502
|
+
* option whose variable is outside the grammar, then the routed Command's own declarations.
|
|
503
|
+
*/
|
|
504
|
+
function reportingOrder(invocation) {
|
|
505
|
+
const declarations = scoped(invocation.inputs);
|
|
506
|
+
const globals = declarations.filter((entry) => entry.global);
|
|
507
|
+
const rejected = invocation.plugins
|
|
508
|
+
.filter((input) => invocation.sources.rejected.has(input.name))
|
|
509
|
+
.map((input) => ({ global: true, input }));
|
|
510
|
+
return [...globals, ...rejected, ...declarations.filter((entry) => !entry.global)];
|
|
511
|
+
}
|
|
342
512
|
export async function validateValues(invocation) {
|
|
343
|
-
const { defaults, supplied } = invocation;
|
|
513
|
+
const { defaults, sources, supplied } = invocation;
|
|
344
514
|
const declarations = scoped(invocation.inputs);
|
|
515
|
+
/** Where a filled option's value came from, which its diagnostic names; argv names none. */
|
|
516
|
+
const originOf = (input) => input.kind === 'option' ? sources.labels.get(input.name) : undefined;
|
|
345
517
|
/**
|
|
346
|
-
* One reading of the tokens and the route, built anew for each
|
|
347
|
-
* tail, and every collected value are copies, so a
|
|
348
|
-
* the parser's collections, nor the tail the action receives, nor the next
|
|
349
|
-
* invocation.
|
|
518
|
+
* One reading of the tokens and the route, built anew for each validator call. The route, the
|
|
519
|
+
* tail, and every collected value are copies, so a validator that writes to them reaches neither
|
|
520
|
+
* the parser's collections, nor the tail the action receives, nor the next validator call of
|
|
521
|
+
* this invocation. Each copy is made on its first read and kept for the call, so a validator
|
|
522
|
+
* that never reads one never pays for it, however many values a list holds. The host is the
|
|
523
|
+
* captured object itself, the one the action receives.
|
|
350
524
|
*/
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
525
|
+
const contextOf = (entry) => {
|
|
526
|
+
const copies = {};
|
|
527
|
+
return {
|
|
528
|
+
get command() {
|
|
529
|
+
copies.command ??= [...invocation.command];
|
|
530
|
+
return copies.command;
|
|
531
|
+
},
|
|
532
|
+
host: invocation.host,
|
|
533
|
+
input: identityOf(entry),
|
|
534
|
+
get passthrough() {
|
|
535
|
+
copies.passthrough ??= [...invocation.passthrough];
|
|
536
|
+
return copies.passthrough;
|
|
537
|
+
},
|
|
538
|
+
phase: 'invocation',
|
|
539
|
+
get supplied() {
|
|
540
|
+
copies.supplied ??= suppliedInputs(declarations.map((declared) => declared.input), supplied);
|
|
541
|
+
return copies.supplied;
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
};
|
|
357
545
|
const values = new Map();
|
|
358
546
|
const lines = [];
|
|
359
547
|
const problems = [];
|
|
360
|
-
/** One
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
...facts(),
|
|
548
|
+
/** One rejected input, whatever rejected it, in the order this phase reaches it. */
|
|
549
|
+
const reject = (entry, issues, subject) => {
|
|
550
|
+
problems.push({
|
|
364
551
|
input: identityOf(entry),
|
|
365
|
-
|
|
552
|
+
issues,
|
|
553
|
+
reason: 'invalid',
|
|
554
|
+
spelling: spellingOf(entry.input),
|
|
555
|
+
});
|
|
556
|
+
lines.push(...messages(subject, issues));
|
|
557
|
+
};
|
|
558
|
+
/** One path for every value a validator reads, so a raw shape and its issues meet it once. */
|
|
559
|
+
const accept = async (entry, raw, spelling) => {
|
|
560
|
+
const result = await validateDeclared(entry.input, raw, {
|
|
561
|
+
context: () => contextOf(entry),
|
|
562
|
+
place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
|
|
563
|
+
signal: invocation.signal,
|
|
366
564
|
});
|
|
367
565
|
if (result.issues === undefined) {
|
|
368
566
|
values.set(entry.input, result.value);
|
|
369
567
|
return;
|
|
370
568
|
}
|
|
371
|
-
|
|
372
|
-
problems.push({ input: identityOf(entry), issues, reason: 'invalid', spelling });
|
|
373
|
-
lines.push(...messages(suppliedName(entry.input, spelling), issues));
|
|
569
|
+
reject(entry, reported(result.issues), suppliedName(entry.input, spelling, originOf(entry.input)));
|
|
374
570
|
};
|
|
375
|
-
for (const entry of
|
|
571
|
+
for (const entry of reportingOrder(invocation)) {
|
|
376
572
|
if (invocation.signal.aborted) {
|
|
377
573
|
/**
|
|
378
|
-
* A cancelled run starts no further
|
|
574
|
+
* A cancelled run starts no further validator call. The one already in flight was awaited
|
|
379
575
|
* above, and whatever this phase collected is never raised, because the run resolves its
|
|
380
576
|
* cancellation code instead.
|
|
381
577
|
*/
|
|
382
578
|
break;
|
|
383
579
|
}
|
|
384
580
|
const { input } = entry;
|
|
385
|
-
|
|
581
|
+
const variable = sources.rejected.get(input.name);
|
|
582
|
+
if (input.kind === 'option' && variable !== undefined) {
|
|
583
|
+
// A Boolean variable outside the grammar filled nothing, so it is the option's problem.
|
|
584
|
+
reject(entry, grammarIssues, suppliedName(input, spellingOf(input), variable));
|
|
585
|
+
}
|
|
586
|
+
else if (input.kind === 'option' && input.config.type === 'boolean') {
|
|
386
587
|
values.set(input, booleanValue(supplied.options, input.name, input.config));
|
|
387
588
|
}
|
|
388
589
|
else {
|
|
@@ -396,14 +597,14 @@ export async function validateValues(invocation) {
|
|
|
396
597
|
// An omitted required argument arrives here too, so omission has one class.
|
|
397
598
|
// One aggregated diagnostic covers an omitted argument and an omitted option alike.
|
|
398
599
|
problems.push({ input: identityOf(entry), reason: 'missing', spelling });
|
|
399
|
-
lines.push(missingMessage(input, spelling, collected));
|
|
600
|
+
lines.push(missingMessage(input, spelling, { collected }));
|
|
400
601
|
}
|
|
401
602
|
else if (collected && !defaults.has(input)) {
|
|
402
|
-
// No occurrence
|
|
403
|
-
|
|
603
|
+
// No occurrence has no value to validate, so the action receives an empty array.
|
|
604
|
+
values.set(input, []);
|
|
404
605
|
}
|
|
405
606
|
else if (validatesOmission(input)) {
|
|
406
|
-
// The flag sends the omission itself to the
|
|
607
|
+
// The flag sends the omission itself to the validator.
|
|
407
608
|
// An absence rule reads the context a supplied value reads, and reports input issues.
|
|
408
609
|
await accept(entry, undefined, spelling);
|
|
409
610
|
}
|
|
@@ -411,6 +612,11 @@ export async function validateValues(invocation) {
|
|
|
411
612
|
values.set(input, freshDefault(defaults.get(input)));
|
|
412
613
|
}
|
|
413
614
|
}
|
|
615
|
+
else if (input.config.required && Array.isArray(raw) && raw.length === 0) {
|
|
616
|
+
// A filled list satisfies the at-least-one rule by its length, so an empty one is missing.
|
|
617
|
+
problems.push({ input: identityOf(entry), reason: 'missing', spelling });
|
|
618
|
+
lines.push(missingMessage(input, spelling, { collected, origin: originOf(input) }));
|
|
619
|
+
}
|
|
414
620
|
else {
|
|
415
621
|
await accept(entry, raw, spelling);
|
|
416
622
|
}
|