@loomcli/core 0.6.0 → 0.8.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/NOTICE +34 -0
- package/dist/application.d.ts +11 -6
- package/dist/application.js +57 -19
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/chain.d.ts +28 -17
- package/dist/chain.js +11 -10
- package/dist/command-rules.d.ts +1 -1
- package/dist/command-rules.js +2 -2
- package/dist/command.d.ts +31 -47
- package/dist/command.js +204 -209
- package/dist/errors.d.ts +22 -21
- package/dist/errors.js +38 -18
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +8 -1
- package/dist/globals.d.ts +48 -22
- package/dist/globals.js +72 -29
- package/dist/glyphs.generated.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/input-rules.d.ts +20 -2
- package/dist/input-rules.js +47 -5
- package/dist/inspect.d.ts +33 -17
- package/dist/inspect.js +74 -87
- package/dist/locate.js +52 -60
- package/dist/options.d.ts +101 -83
- package/dist/options.js +311 -267
- package/dist/parse.d.ts +162 -0
- package/dist/parse.js +601 -0
- package/dist/plain.d.ts +52 -2
- package/dist/plain.js +228 -2
- package/dist/plugin-rules.d.ts +8 -4
- package/dist/plugin-rules.js +13 -9
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +32 -27
- package/dist/plugin.js +107 -89
- package/dist/sources.d.ts +18 -9
- package/dist/sources.js +50 -20
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/types.d.ts +70 -30
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +108 -34
- package/dist/validation.js +282 -125
- package/dist/view.d.ts +16 -11
- package/dist/view.js +13 -4
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/command.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
var _a, _b;
|
|
2
2
|
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
+
import { captureDeclaration, unreadableArgument } from './capture.js';
|
|
3
4
|
import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
|
|
4
|
-
import { quoteString, spelled } from './diagnostic-text.js';
|
|
5
|
-
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure,
|
|
5
|
+
import { elided, quoteString, spelled } from './diagnostic-text.js';
|
|
6
|
+
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, reasonOf, } from './errors.js';
|
|
6
7
|
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
7
|
-
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
|
|
8
|
-
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
8
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
|
|
9
|
+
import { buildGlobals, checkLocalOptions, frozenValues } from './globals.js';
|
|
9
10
|
import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
10
|
-
import { graphMismatch, nodeAt, resultNode
|
|
11
|
-
import { checkOptionName, compileOptions, copyValues,
|
|
12
|
-
import {
|
|
11
|
+
import { graphMismatch, nodeAt, resultNode } from './inspect.js';
|
|
12
|
+
import { checkOptionName, compileOptions, copyValues, declaredNameCorrection, emptyValues, isDeclaredName, mergeValues, repeatedAliasText, spellingMark, tableEntries, } from './options.js';
|
|
13
|
+
import { argumentSlot, candidatesOf } from './parse.js';
|
|
14
|
+
import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
|
|
13
15
|
import { brokenAttachHook, notAnObject } from './plugin-rules.js';
|
|
14
16
|
import { fillInputs } from './sources.js';
|
|
15
|
-
import {
|
|
17
|
+
import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, rejectsGlobal, validateValues, } from './validation.js';
|
|
16
18
|
/**
|
|
17
19
|
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
18
20
|
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
@@ -73,16 +75,30 @@ export function commandPlacement(path, child) {
|
|
|
73
75
|
function constructorFinding(name, options, mark) {
|
|
74
76
|
return { arguments: callArguments(name, options), call: 'new Command', mark };
|
|
75
77
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The one copy of the options that `new Command(name, options)` reads, taken once the name is
|
|
80
|
+
* judged: the options object and its `extensions` list, whose entries are what their factory built
|
|
81
|
+
* and are not copied. A read that throws is the unreadable fault of the options, a value that is not
|
|
82
|
+
* a plain object is the not-an-object fault, and a Command declared without options has none.
|
|
83
|
+
*/
|
|
84
|
+
function captureCommandOptions(name, options) {
|
|
85
|
+
if (options === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return captureDeclaration(options, (copy, read) => {
|
|
89
|
+
read.nested(copy, 'extensions', shallowList);
|
|
90
|
+
}, {
|
|
91
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
80
92
|
correction: 'Supply a Command options object.',
|
|
81
93
|
findings: [constructorFinding(name, options, '1')],
|
|
82
94
|
sentence: `${commandSentence(name)} declares options that are not an object.`,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
}),
|
|
96
|
+
unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** A named Command's options never carry the retired globals wiring. */
|
|
100
|
+
function checkCommandOptions(name, options) {
|
|
101
|
+
if ('globals' in options) {
|
|
86
102
|
throw new DeclarationError(commandGlobals, {
|
|
87
103
|
correction: 'Declare globals on the Application and register its environment.',
|
|
88
104
|
findings: [constructorFinding(name, options, '1.globals')],
|
|
@@ -90,10 +106,6 @@ function checkCommandOptions(name, options) {
|
|
|
90
106
|
});
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
|
-
/** One name rule for an argument, option, or view name: a bare token the parser can read. */
|
|
94
|
-
function isDeclaredName(name) {
|
|
95
|
-
return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
96
|
-
}
|
|
97
109
|
/**
|
|
98
110
|
* The portable name rule, for every name an operator types as a command at a shell prompt: the
|
|
99
111
|
* application name, every Command name, and every alias. The characters are the POSIX portable
|
|
@@ -149,23 +161,28 @@ export function freshState(declaration) {
|
|
|
149
161
|
};
|
|
150
162
|
}
|
|
151
163
|
/**
|
|
152
|
-
* A named Command's own declaration, checked before the value exists: the name, the
|
|
153
|
-
* the core facts, and the extension values
|
|
154
|
-
* the author passed changes nothing the declaration holds.
|
|
164
|
+
* A named Command's own declaration, checked before the value exists: the name, then the one copy
|
|
165
|
+
* of the options slot, its core facts, and the extension values it carries. A later change to the
|
|
166
|
+
* options object the author passed changes nothing the declaration holds.
|
|
155
167
|
*/
|
|
156
168
|
function namedState(name, options) {
|
|
157
169
|
if (!isPortableName(name)) {
|
|
170
|
+
// The options are read after the name is judged, so the name's finding prints them elided.
|
|
158
171
|
throw new DeclarationError(portableName, {
|
|
159
172
|
correction: portableNameCorrection,
|
|
160
|
-
findings: [
|
|
173
|
+
findings: [
|
|
174
|
+
constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
|
|
175
|
+
],
|
|
161
176
|
sentence: `Command name ${quoted(name)} is invalid.`,
|
|
162
177
|
});
|
|
163
178
|
}
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
const slot = captureCommandOptions(name, options);
|
|
180
|
+
if (slot !== undefined) {
|
|
181
|
+
checkCommandOptions(name, slot);
|
|
182
|
+
}
|
|
166
183
|
const site = {
|
|
167
184
|
at: '1',
|
|
168
|
-
declaration: { arguments: [name,
|
|
185
|
+
declaration: { arguments: [name, slot], call: 'new Command' },
|
|
169
186
|
subject: commandSentence(name),
|
|
170
187
|
};
|
|
171
188
|
// The facts are read in the order their diagnostics have always ranked.
|
|
@@ -214,13 +231,21 @@ function checkOpen(state, late) {
|
|
|
214
231
|
const inputRemedy = 'Declare arguments and options before action().';
|
|
215
232
|
/** Where one input was declared: the call on the Command at `path` that declared it. */
|
|
216
233
|
function inputSite(name, path, input) {
|
|
217
|
-
return declaringSite(input, inputPlace(input,
|
|
234
|
+
return declaringSite(input, inputPlace(input, path), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
|
|
218
235
|
}
|
|
219
236
|
/** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
|
|
220
237
|
function inputFinding(path, input, note) {
|
|
221
|
-
const site = declaringSite(input, inputPlace(input,
|
|
238
|
+
const site = declaringSite(input, inputPlace(input, path));
|
|
222
239
|
return siteFinding(site, site.named, note);
|
|
223
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The finding for an input whose name is invalid, marking the name. The config is read after the
|
|
243
|
+
* name is judged, so it prints elided.
|
|
244
|
+
*/
|
|
245
|
+
function nameFinding(path, input) {
|
|
246
|
+
const site = configUnread(declaringSite(input, inputPlace(input, path)));
|
|
247
|
+
return siteFinding(site, site.named);
|
|
248
|
+
}
|
|
224
249
|
/** The scope one Command's own options compile under: its subject, and each option's call. */
|
|
225
250
|
function localScope(name, path) {
|
|
226
251
|
return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
|
|
@@ -296,8 +321,8 @@ function checkArgumentName(command, declared, input) {
|
|
|
296
321
|
const { path } = command;
|
|
297
322
|
if (!isDeclaredName(input.name)) {
|
|
298
323
|
throw new DeclarationError(declaredName, {
|
|
299
|
-
correction:
|
|
300
|
-
findings: [
|
|
324
|
+
correction: declaredNameCorrection,
|
|
325
|
+
findings: [nameFinding(path, input)],
|
|
301
326
|
sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
|
|
302
327
|
});
|
|
303
328
|
}
|
|
@@ -317,11 +342,13 @@ function checkArgumentName(command, declared, input) {
|
|
|
317
342
|
export function declareArgument(state, declared) {
|
|
318
343
|
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
319
344
|
// A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
|
|
320
|
-
// The config is
|
|
345
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
321
346
|
const { name } = state;
|
|
322
347
|
checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
|
|
323
|
-
|
|
324
|
-
|
|
348
|
+
const input = {
|
|
349
|
+
...declared,
|
|
350
|
+
config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
|
|
351
|
+
};
|
|
325
352
|
checkOpen(state, {
|
|
326
353
|
arguments: [input.name, input.config],
|
|
327
354
|
call: 'argument',
|
|
@@ -350,11 +377,14 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
|
|
|
350
377
|
*/
|
|
351
378
|
export function declareOption(state, declared, table = noGlobals) {
|
|
352
379
|
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
353
|
-
// The config is
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const input = {
|
|
357
|
-
|
|
380
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
381
|
+
const path = pathOf(state.name);
|
|
382
|
+
checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
|
|
383
|
+
const input = {
|
|
384
|
+
...declared,
|
|
385
|
+
config: captureInputConfig(declared, { call: 'option', path }),
|
|
386
|
+
};
|
|
387
|
+
const site = inputSite(state.name, path, input);
|
|
358
388
|
checkOpen(state, {
|
|
359
389
|
arguments: [input.name, input.config],
|
|
360
390
|
call: 'option',
|
|
@@ -406,18 +436,17 @@ export function declareAlias(state, names) {
|
|
|
406
436
|
const aliases = [...state.aliases];
|
|
407
437
|
for (const [index, alias] of names.entries()) {
|
|
408
438
|
const repeated = { arguments: names, call: 'alias', mark: String(index), path };
|
|
439
|
+
const text = repeatedAliasText(commandSentence(name), alias);
|
|
409
440
|
if (alias === name) {
|
|
410
441
|
throw new DeclarationError(repeatedAlias, {
|
|
411
|
-
|
|
442
|
+
...text.own,
|
|
412
443
|
findings: [{ ...repeated, note: 'its own name' }],
|
|
413
|
-
sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
|
|
414
444
|
});
|
|
415
445
|
}
|
|
416
446
|
if (aliases.includes(alias)) {
|
|
417
447
|
throw new DeclarationError(repeatedAlias, {
|
|
418
|
-
|
|
448
|
+
...text.twice,
|
|
419
449
|
findings: [{ ...repeated, note: 'already an alias' }],
|
|
420
|
-
sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
|
|
421
450
|
});
|
|
422
451
|
}
|
|
423
452
|
aliases.push(alias);
|
|
@@ -632,7 +661,7 @@ function checkGroup(state, place) {
|
|
|
632
661
|
throw new DeclarationError(groupOption, {
|
|
633
662
|
correction: 'Register an action or remove the option.',
|
|
634
663
|
findings: [inputFinding(place.path, option, 'no action reads it')],
|
|
635
|
-
sentence: `${commandSentence(name)} declares option
|
|
664
|
+
sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
|
|
636
665
|
});
|
|
637
666
|
}
|
|
638
667
|
}
|
|
@@ -694,7 +723,7 @@ function attachChild(state, child) {
|
|
|
694
723
|
/**
|
|
695
724
|
* Walks one subtree joining an Application, once, for the rules only the Application can judge: one
|
|
696
725
|
* Command value reached through two paths, two distinct descriptors under one identity, a local
|
|
697
|
-
* option that meets a global
|
|
726
|
+
* option that meets a global option's key, spelling, or variable, and the nesting cap
|
|
698
727
|
* measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
|
|
699
728
|
* At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
|
|
700
729
|
* here fires only once the cap rises: attach reads one level, and only this walk knows each level.
|
|
@@ -1045,13 +1074,13 @@ class AttachedCommandValue {
|
|
|
1045
1074
|
checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
|
|
1046
1075
|
}
|
|
1047
1076
|
else {
|
|
1048
|
-
checkOptionName(raw.name, inputSite(declared.name, path, raw));
|
|
1077
|
+
checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
|
|
1049
1078
|
}
|
|
1050
|
-
checkInputConfig(raw, { call: raw.kind, path });
|
|
1051
1079
|
// Each kind captures its own config, so the input keeps the pairing its kind declares.
|
|
1080
|
+
const place = { call: raw.kind, path };
|
|
1052
1081
|
const input = raw.kind === 'argument'
|
|
1053
|
-
? { ...raw, config:
|
|
1054
|
-
: { ...raw, config:
|
|
1082
|
+
? { ...raw, config: captureInputConfig(raw, place) }
|
|
1083
|
+
: { ...raw, config: captureInputConfig(raw, place) };
|
|
1055
1084
|
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
1056
1085
|
}
|
|
1057
1086
|
#derive(call, declared) {
|
|
@@ -1166,16 +1195,12 @@ function nameCollisionRule(declared, held) {
|
|
|
1166
1195
|
}
|
|
1167
1196
|
return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
|
|
1168
1197
|
}
|
|
1169
|
-
/** The note a finding for one hook-declared input carries. */
|
|
1170
|
-
function hookNote(identity) {
|
|
1171
|
-
return `declared by plugin ${quoted(identity)}`;
|
|
1172
|
-
}
|
|
1173
1198
|
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
1174
1199
|
function hookClause(earlier, path) {
|
|
1175
1200
|
const { identity, input } = earlier;
|
|
1176
1201
|
return {
|
|
1177
1202
|
clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
|
|
1178
|
-
held: inputFinding(path, input,
|
|
1203
|
+
held: inputFinding(path, input, declarerNote(identity)),
|
|
1179
1204
|
kind: input.kind,
|
|
1180
1205
|
remedy: 'Install one of them.',
|
|
1181
1206
|
};
|
|
@@ -1264,13 +1289,13 @@ function readSpellings(table, claimed, placeOf) {
|
|
|
1264
1289
|
for (const [spelling, option] of table) {
|
|
1265
1290
|
if (!claimed.has(spelling)) {
|
|
1266
1291
|
const form = longs.get(option.name) ?? spelling;
|
|
1267
|
-
claimed.set(spelling, { finding: placeOf(option.name, option
|
|
1292
|
+
claimed.set(spelling, { finding: placeOf(option.name, option), form });
|
|
1268
1293
|
}
|
|
1269
1294
|
}
|
|
1270
1295
|
}
|
|
1271
1296
|
/** The place one input's spelling sits: the key of its call that yields the spelling. */
|
|
1272
|
-
function spellingPlace(site,
|
|
1273
|
-
return siteFinding(site, spellingMark(site,
|
|
1297
|
+
function spellingPlace(site, origin, note) {
|
|
1298
|
+
return siteFinding(site, spellingMark(site, origin), note);
|
|
1274
1299
|
}
|
|
1275
1300
|
/** One hook-declared option's spellings, against every spelling the table already claims. */
|
|
1276
1301
|
function checkAttachedSpelling(declared, named) {
|
|
@@ -1285,14 +1310,14 @@ function checkAttachedSpelling(declared, named) {
|
|
|
1285
1310
|
throw new DeclarationError(spellingTaken, {
|
|
1286
1311
|
correction: 'Change one of the two spellings or omit the plugin.',
|
|
1287
1312
|
findings: [
|
|
1288
|
-
spellingPlace(site, option
|
|
1313
|
+
spellingPlace(site, option, declarerNote(identity)),
|
|
1289
1314
|
...(used.finding === undefined ? [] : [used.finding]),
|
|
1290
1315
|
],
|
|
1291
1316
|
sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
|
|
1292
1317
|
});
|
|
1293
1318
|
}
|
|
1294
1319
|
}
|
|
1295
|
-
readSpellings(table, claimed, (_name,
|
|
1320
|
+
readSpellings(table, claimed, (_name, origin) => spellingPlace(site, origin, declarerNote(identity)));
|
|
1296
1321
|
}
|
|
1297
1322
|
/** The declarations of one kind, keyed by name, the first of each name winning. */
|
|
1298
1323
|
function byName(inputs) {
|
|
@@ -1306,7 +1331,7 @@ function byName(inputs) {
|
|
|
1306
1331
|
}
|
|
1307
1332
|
/** Where the globals table's options declared their spellings, a global or a plugin's option. */
|
|
1308
1333
|
function tableSpellings(globals) {
|
|
1309
|
-
return (name,
|
|
1334
|
+
return (name, origin) => {
|
|
1310
1335
|
const entry = globals.names.get(name);
|
|
1311
1336
|
if (!entry) {
|
|
1312
1337
|
return undefined;
|
|
@@ -1314,8 +1339,8 @@ function tableSpellings(globals) {
|
|
|
1314
1339
|
const { owner, site } = entry;
|
|
1315
1340
|
const note = owner.kind === 'plugin'
|
|
1316
1341
|
? `an option of plugin ${quoted(owner.identity)}`
|
|
1317
|
-
: `the global option
|
|
1318
|
-
return spellingPlace(site,
|
|
1342
|
+
: `the global option ${quoted(name)}`;
|
|
1343
|
+
return spellingPlace(site, origin, note);
|
|
1319
1344
|
};
|
|
1320
1345
|
}
|
|
1321
1346
|
/**
|
|
@@ -1342,11 +1367,11 @@ function checkAttachedInputs(declared, attached, place) {
|
|
|
1342
1367
|
};
|
|
1343
1368
|
const claimed = new Map();
|
|
1344
1369
|
readSpellings(globals.options, claimed, tableSpellings(globals));
|
|
1345
|
-
readSpellings(compileOptions(options, scope), claimed, (name,
|
|
1370
|
+
readSpellings(compileOptions(options, scope), claimed, (name, origin) => {
|
|
1346
1371
|
const local = locals.get(name);
|
|
1347
1372
|
return local === undefined || local.kind !== 'option'
|
|
1348
1373
|
? undefined
|
|
1349
|
-
: spellingPlace(scope.siteOf(local),
|
|
1374
|
+
: spellingPlace(scope.siteOf(local), origin, `the local option ${quoted(name)}`);
|
|
1350
1375
|
});
|
|
1351
1376
|
for (const entry of attached) {
|
|
1352
1377
|
const { identity, input } = entry;
|
|
@@ -1354,7 +1379,7 @@ function checkAttachedInputs(declared, attached, place) {
|
|
|
1354
1379
|
if (collision) {
|
|
1355
1380
|
throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
|
|
1356
1381
|
correction: collision.remedy,
|
|
1357
|
-
findings: [inputFinding(path, input,
|
|
1382
|
+
findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
|
|
1358
1383
|
sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
|
|
1359
1384
|
});
|
|
1360
1385
|
}
|
|
@@ -1375,7 +1400,7 @@ function bindDispatch(state, action, globals) {
|
|
|
1375
1400
|
// The graph erases the binder's generic relationship.
|
|
1376
1401
|
// It holds because attachment checks the global output requirement.
|
|
1377
1402
|
// The call or the attach that met both options rejected a collision between a global and a local.
|
|
1378
|
-
// This binder returns the
|
|
1403
|
+
// This binder returns the validated value of every global option, the plugins' included.
|
|
1379
1404
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
1380
1405
|
const globalOptions = globals.bind(values);
|
|
1381
1406
|
return action({
|
|
@@ -1470,6 +1495,7 @@ export function buildCommand(state, context) {
|
|
|
1470
1495
|
checkArgumentPlacement(place, slots[0], state.children[0]);
|
|
1471
1496
|
const result = checkFinished(hooked, hasAction, { path: context.path });
|
|
1472
1497
|
const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
|
|
1498
|
+
const table = new Map([...tableEntries(options, false), ...globals.table]);
|
|
1473
1499
|
// A hook's erased calls answer the declaration rules an authored call answers at the call.
|
|
1474
1500
|
checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
|
|
1475
1501
|
const children = new Map();
|
|
@@ -1496,9 +1522,9 @@ export function buildCommand(state, context) {
|
|
|
1496
1522
|
hidden: facts.hidden,
|
|
1497
1523
|
inputs: hooked.inputs,
|
|
1498
1524
|
name,
|
|
1499
|
-
options,
|
|
1500
1525
|
result,
|
|
1501
1526
|
routes,
|
|
1527
|
+
table,
|
|
1502
1528
|
};
|
|
1503
1529
|
}
|
|
1504
1530
|
/**
|
|
@@ -1594,7 +1620,7 @@ _b = CommandBuilder;
|
|
|
1594
1620
|
*/
|
|
1595
1621
|
class CommandDeclaration extends CommandBuilder {
|
|
1596
1622
|
constructor(name, options) {
|
|
1597
|
-
const declared = namedState(name, options);
|
|
1623
|
+
const declared = declaring(() => namedState(name, options));
|
|
1598
1624
|
super(declared.name, declared.state);
|
|
1599
1625
|
}
|
|
1600
1626
|
}
|
|
@@ -1608,17 +1634,15 @@ export function collectInputs(command) {
|
|
|
1608
1634
|
];
|
|
1609
1635
|
}
|
|
1610
1636
|
/**
|
|
1611
|
-
* Where every input of one graph was declared: each global option at its `globalOption()` call
|
|
1612
|
-
* and each Command's own inputs at their calls on the Command,
|
|
1637
|
+
* Where every input of one graph was declared: each global option at its `globalOption()` call or
|
|
1638
|
+
* in its plugin's `options` record, and each Command's own inputs at their calls on the Command,
|
|
1639
|
+
* under its path from the root.
|
|
1613
1640
|
*/
|
|
1614
1641
|
export function inputPlaces(graph) {
|
|
1615
|
-
const places = new Map();
|
|
1616
|
-
for (const input of graph.globals.inputs) {
|
|
1617
|
-
places.set(input, inputPlace(input, { global: true, path: [] }));
|
|
1618
|
-
}
|
|
1642
|
+
const places = new Map(graph.globals.sites);
|
|
1619
1643
|
const walk = (command, path) => {
|
|
1620
1644
|
for (const input of command.inputs) {
|
|
1621
|
-
places.set(input,
|
|
1645
|
+
places.set(input, declaringSite(input, inputPlace(input, path)));
|
|
1622
1646
|
}
|
|
1623
1647
|
for (const [name, child] of command.children) {
|
|
1624
1648
|
walk(child, [...path, name]);
|
|
@@ -1627,97 +1651,36 @@ export function inputPlaces(graph) {
|
|
|
1627
1651
|
walk(graph.root, []);
|
|
1628
1652
|
return places;
|
|
1629
1653
|
}
|
|
1630
|
-
/**
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
/**
|
|
1642
|
-
* Whether a token names one of the Command's children: the Command has children and the token is
|
|
1643
|
-
* no option token. Routing and `locate` read a bare word through this rule.
|
|
1644
|
-
*/
|
|
1645
|
-
export function readsAsChild(command, token) {
|
|
1646
|
-
return command.children.size > 0 && !isOptionToken(token);
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
|
|
1650
|
-
* `walked` receives the path after each name routes, so an unknown Command leaves its caller
|
|
1651
|
-
* holding the partial path walked before it.
|
|
1652
|
-
*/
|
|
1653
|
-
export function route(root, tokens, walked) {
|
|
1654
|
-
let command = root;
|
|
1655
|
-
const path = [];
|
|
1656
|
-
let index = 0;
|
|
1657
|
-
for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
|
|
1658
|
-
if (!readsAsChild(command, token)) {
|
|
1659
|
-
break;
|
|
1660
|
-
}
|
|
1661
|
-
const child = command.routes.get(token);
|
|
1662
|
-
if (!child) {
|
|
1663
|
-
throw new UnknownCommandError(token, candidatesOf(command));
|
|
1664
|
-
}
|
|
1665
|
-
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
1666
|
-
command = child.command;
|
|
1667
|
-
path.push(child.name);
|
|
1668
|
-
walked?.(Object.freeze([...path]));
|
|
1669
|
-
index += 1;
|
|
1654
|
+
/** One positional word bound to its slot: a variadic slot collects it, any other holds it. */
|
|
1655
|
+
function bindWord(values, slot, word) {
|
|
1656
|
+
const bound = values.get(slot.input);
|
|
1657
|
+
if (!slot.variadic) {
|
|
1658
|
+
values.set(slot.input, word);
|
|
1659
|
+
}
|
|
1660
|
+
else if (Array.isArray(bound)) {
|
|
1661
|
+
bound.push(word);
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
values.set(slot.input, [word]);
|
|
1670
1665
|
}
|
|
1671
|
-
return { command, path, tokens: tokens.slice(index) };
|
|
1672
1666
|
}
|
|
1673
1667
|
/**
|
|
1674
|
-
* The
|
|
1668
|
+
* The words each positional slot received. Parsing already held the first positional no slot
|
|
1669
|
+
* accepts, so every positional here has a slot. An omitted required argument binds nothing and
|
|
1675
1670
|
* reports as a missing input in the validation phase, so omission has one class whether the input
|
|
1676
|
-
* is an argument or an option.
|
|
1671
|
+
* is an argument or an option. An empty variadic tail binds nothing, so validation reads it as
|
|
1672
|
+
* `[]` or reports the omission.
|
|
1677
1673
|
*/
|
|
1678
|
-
function bindArguments(command,
|
|
1674
|
+
function bindArguments(command, positionals) {
|
|
1679
1675
|
const values = new Map();
|
|
1680
|
-
for (const [position,
|
|
1676
|
+
for (const [position, word] of positionals.entries()) {
|
|
1681
1677
|
const slot = argumentSlot(command.arguments, position);
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
// An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
|
|
1686
|
-
const bound = values.get(slot.input);
|
|
1687
|
-
if (!slot.variadic) {
|
|
1688
|
-
values.set(slot.input, token);
|
|
1689
|
-
}
|
|
1690
|
-
else if (Array.isArray(bound)) {
|
|
1691
|
-
bound.push(token);
|
|
1692
|
-
}
|
|
1693
|
-
else {
|
|
1694
|
-
values.set(slot.input, [token]);
|
|
1678
|
+
if (slot) {
|
|
1679
|
+
bindWord(values, slot, word);
|
|
1695
1680
|
}
|
|
1696
1681
|
}
|
|
1697
1682
|
return values;
|
|
1698
1683
|
}
|
|
1699
|
-
/**
|
|
1700
|
-
* The slot the positional at one index fills: the slot at that index, else a variadic last slot,
|
|
1701
|
-
* which accepts every later positional, else none. Binding and `locate` read positions through it.
|
|
1702
|
-
*/
|
|
1703
|
-
export function argumentSlot(slots, position) {
|
|
1704
|
-
const last = slots.at(-1);
|
|
1705
|
-
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
1706
|
-
}
|
|
1707
|
-
/**
|
|
1708
|
-
* Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
|
|
1709
|
-
* receives the path as routing extends it, the partial path of an unknown Command included.
|
|
1710
|
-
*/
|
|
1711
|
-
export function routeInvocation(graph, argv, walked) {
|
|
1712
|
-
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
1713
|
-
const routed = route(graph.root, scan.rest, walked);
|
|
1714
|
-
return {
|
|
1715
|
-
command: routed.command,
|
|
1716
|
-
path: routed.path,
|
|
1717
|
-
scan: scan.values,
|
|
1718
|
-
tokens: routed.tokens,
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
1684
|
/**
|
|
1722
1685
|
* The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
|
|
1723
1686
|
* value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
|
|
@@ -1739,30 +1702,25 @@ function requestOf(command, values, passthrough) {
|
|
|
1739
1702
|
});
|
|
1740
1703
|
}
|
|
1741
1704
|
/**
|
|
1742
|
-
* The
|
|
1743
|
-
* missing-subcommand error
|
|
1744
|
-
* fault holds the same way.
|
|
1705
|
+
* The held structural fault, then the callable check. A group answers no invocation of its own, so
|
|
1706
|
+
* it holds the missing-subcommand error, which ranks after every structural fault.
|
|
1745
1707
|
*/
|
|
1746
1708
|
function parseLocal(routed) {
|
|
1747
|
-
const { command, path } = routed;
|
|
1709
|
+
const { command, fault, path } = routed;
|
|
1748
1710
|
const { dispatch } = command;
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
throw new NonCallableCommandError(path, candidatesOf(command));
|
|
1752
|
-
}
|
|
1753
|
-
const parsed = parseInputs(command.options, routed.tokens);
|
|
1754
|
-
const args = bindArguments(command, path, parsed.positionals);
|
|
1755
|
-
return {
|
|
1756
|
-
args,
|
|
1757
|
-
dispatch,
|
|
1758
|
-
kind: 'parsed',
|
|
1759
|
-
options: parsed.options,
|
|
1760
|
-
passthrough: parsed.passthrough,
|
|
1761
|
-
};
|
|
1711
|
+
if (fault) {
|
|
1712
|
+
return { fault, kind: 'held' };
|
|
1762
1713
|
}
|
|
1763
|
-
|
|
1764
|
-
return { fault:
|
|
1714
|
+
if (!dispatch) {
|
|
1715
|
+
return { fault: new NonCallableCommandError(path, candidatesOf(command)), kind: 'held' };
|
|
1765
1716
|
}
|
|
1717
|
+
return {
|
|
1718
|
+
args: bindArguments(command, routed.positionals),
|
|
1719
|
+
dispatch,
|
|
1720
|
+
kind: 'parsed',
|
|
1721
|
+
options: routed.values.locals,
|
|
1722
|
+
passthrough: routed.passthrough,
|
|
1723
|
+
};
|
|
1766
1724
|
}
|
|
1767
1725
|
/**
|
|
1768
1726
|
* The node of one option a configuration source is asked about: in the graph's globals, or among
|
|
@@ -1777,21 +1735,24 @@ function requestNode(graph, path, place) {
|
|
|
1777
1735
|
}
|
|
1778
1736
|
return node;
|
|
1779
1737
|
}
|
|
1738
|
+
/** The passthrough tail a validator reads: the routed Command's, or none while a fault is held. */
|
|
1739
|
+
function tailOf(local) {
|
|
1740
|
+
return local.kind === 'parsed' ? local.passthrough : [];
|
|
1741
|
+
}
|
|
1780
1742
|
/**
|
|
1781
|
-
* The input-source stage over this run's own copies of the parsed values. The global
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1743
|
+
* The input-source stage over this run's own copies of the parsed values. The global options fill
|
|
1744
|
+
* whatever was held, and a faulted occurrence supplied nothing, so a source may fill its option;
|
|
1745
|
+
* the routed Command's own options fill only when nothing is held, because the request is `null`
|
|
1746
|
+
* otherwise. A configuration source's own options pass their validators before the source is
|
|
1747
|
+
* called, as a pass over those options alone.
|
|
1784
1748
|
*/
|
|
1785
|
-
async function fillScope(
|
|
1786
|
-
const
|
|
1749
|
+
async function fillScope(preparation, local) {
|
|
1750
|
+
const { graph, invocation, routed } = preparation;
|
|
1751
|
+
const globals = copyValues(routed.values.globals);
|
|
1787
1752
|
const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
|
|
1788
1753
|
const sources = await fillInputs({
|
|
1789
1754
|
extensions: graph.extensions,
|
|
1790
|
-
globals: {
|
|
1791
|
-
global: true,
|
|
1792
|
-
inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
|
|
1793
|
-
values: globals,
|
|
1794
|
-
},
|
|
1755
|
+
globals: { global: true, inputs: graph.globals.inputs, values: globals },
|
|
1795
1756
|
host: invocation.host,
|
|
1796
1757
|
inspected: invocation.inspected,
|
|
1797
1758
|
locals: local.kind === 'parsed'
|
|
@@ -1803,28 +1764,52 @@ async function fillScope({ graph, invocation, routed }, local) {
|
|
|
1803
1764
|
request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
|
|
1804
1765
|
signal: invocation.signal,
|
|
1805
1766
|
style: invocation.style,
|
|
1767
|
+
validate: (inputs, provenance) => validateInvocation(preparation, {
|
|
1768
|
+
local,
|
|
1769
|
+
locals,
|
|
1770
|
+
only: new Set(inputs),
|
|
1771
|
+
sources: provenance,
|
|
1772
|
+
values: globals,
|
|
1773
|
+
}),
|
|
1806
1774
|
});
|
|
1807
1775
|
return { globals, locals, sources };
|
|
1808
1776
|
}
|
|
1809
1777
|
/**
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
1778
|
+
* A validation pass over the values the input-source stage has filled: every global option
|
|
1779
|
+
* whatever was held, and the routed Command's own declarations only when nothing is held and the
|
|
1780
|
+
* Command is not a group. `only` narrows the declarations a pass validates, as the pass over a
|
|
1781
|
+
* configuration source's own options does ahead of its call, while every validator reads the same
|
|
1782
|
+
* context. The run's one pass reads that earlier pass's values and problems rather than validating
|
|
1783
|
+
* them again.
|
|
1813
1784
|
*/
|
|
1814
|
-
|
|
1815
|
-
const {
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
command: path,
|
|
1819
|
-
|
|
1785
|
+
function validateInvocation({ graph, invocation, routed }, filled) {
|
|
1786
|
+
const { local, only, sources } = filled;
|
|
1787
|
+
const parsed = local.kind === 'parsed';
|
|
1788
|
+
return validateValues({
|
|
1789
|
+
command: routed.path,
|
|
1790
|
+
declaredValues: invocation.declaredValues,
|
|
1820
1791
|
host: invocation.host,
|
|
1821
|
-
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
1822
|
-
passthrough: local
|
|
1823
|
-
|
|
1792
|
+
inputs: { globals: graph.globals.inputs, locals: parsed ? routed.command.inputs : [] },
|
|
1793
|
+
passthrough: tailOf(local),
|
|
1794
|
+
places: invocation.places,
|
|
1824
1795
|
signal: invocation.signal,
|
|
1825
|
-
sources
|
|
1826
|
-
supplied: {
|
|
1796
|
+
sources,
|
|
1797
|
+
supplied: {
|
|
1798
|
+
args: parsed ? local.args : new Map(),
|
|
1799
|
+
options: parsed ? mergeValues(filled.values, filled.locals) : filled.values,
|
|
1800
|
+
},
|
|
1801
|
+
table: routed.command.table,
|
|
1802
|
+
...(only ? { only } : {}),
|
|
1803
|
+
...(sources.validated ? { prior: sources.validated } : {}),
|
|
1827
1804
|
});
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* The dispatch a clean validation prepares. It answers with the call that dispatches, so the caller
|
|
1808
|
+
* records that the action was invoked at the moment it invokes it and no earlier failure reads as a
|
|
1809
|
+
* dispatch.
|
|
1810
|
+
*/
|
|
1811
|
+
function readyDispatch({ invocation, routed }, local, values) {
|
|
1812
|
+
const { command, path } = routed;
|
|
1828
1813
|
return {
|
|
1829
1814
|
dispatch: async (view) => {
|
|
1830
1815
|
// The channel is built at the boundary, because the view a middleware selected is read there.
|
|
@@ -1864,37 +1849,47 @@ async function readyDispatch({ graph, invocation, routed }, filled) {
|
|
|
1864
1849
|
/**
|
|
1865
1850
|
* Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
|
|
1866
1851
|
* middleware reads the request before the action runs and a takeover never observes the fault.
|
|
1867
|
-
* The phases run in order:
|
|
1868
|
-
*
|
|
1852
|
+
* The phases run in order: the structural fault parsing held, a group's missing subcommand, the
|
|
1853
|
+
* input-source stage, and validation. Either of the first two outranks a configuration source's
|
|
1854
|
+
* fault, and a source's fault takes the place of every validation problem. Validation checks the
|
|
1855
|
+
* global options whatever was held, so a middleware reads them after a local fault; a structural
|
|
1856
|
+
* fault on a global option, a rejected global option, a source's fault, or a validator's own fault
|
|
1857
|
+
* leaves `options` `null`.
|
|
1869
1858
|
*/
|
|
1870
1859
|
export async function prepareDispatch(graph, routed, invocation) {
|
|
1871
1860
|
const result = routed.command.result;
|
|
1872
1861
|
const local = parseLocal(routed);
|
|
1873
1862
|
const preparation = { graph, invocation, routed };
|
|
1874
1863
|
const { globals, locals, sources } = await fillScope(preparation, local);
|
|
1875
|
-
const held = (fault) => ({
|
|
1876
|
-
fault,
|
|
1864
|
+
const held = (fault, options) => ({
|
|
1865
|
+
fault: local.kind === 'held' ? local.fault : fault,
|
|
1877
1866
|
globals,
|
|
1878
1867
|
kind: 'held',
|
|
1868
|
+
options,
|
|
1879
1869
|
request: null,
|
|
1880
1870
|
result,
|
|
1881
1871
|
});
|
|
1882
|
-
if (local.kind === 'held') {
|
|
1883
|
-
return held(local.fault);
|
|
1884
|
-
}
|
|
1885
1872
|
if (sources.fault) {
|
|
1886
|
-
return held(sources.fault);
|
|
1873
|
+
return held(sources.fault, null);
|
|
1887
1874
|
}
|
|
1888
1875
|
try {
|
|
1889
|
-
const
|
|
1876
|
+
const validation = await validateInvocation(preparation, {
|
|
1890
1877
|
local,
|
|
1878
|
+
locals,
|
|
1891
1879
|
sources,
|
|
1892
|
-
values:
|
|
1880
|
+
values: globals,
|
|
1893
1881
|
});
|
|
1894
|
-
|
|
1882
|
+
const options = routed.globalFault || rejectsGlobal(validation)
|
|
1883
|
+
? null
|
|
1884
|
+
: frozenValues(graph.globals.inputs, validation.values);
|
|
1885
|
+
if (local.kind === 'held' || validation.failure) {
|
|
1886
|
+
return held(validation.failure, options);
|
|
1887
|
+
}
|
|
1888
|
+
const ready = readyDispatch(preparation, local, validation.values);
|
|
1889
|
+
return { ...ready, globals, kind: 'ready', options, result };
|
|
1895
1890
|
}
|
|
1896
1891
|
catch (error) {
|
|
1897
1892
|
// The fault is held as a failure, so a throw from reading a validator's output is never offered.
|
|
1898
|
-
return held(toFailure(error));
|
|
1893
|
+
return held(toFailure(error), null);
|
|
1899
1894
|
}
|
|
1900
1895
|
}
|