@loomcli/core 0.1.1 → 0.3.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/dist/application.d.ts +73 -28
- package/dist/application.js +334 -99
- package/dist/chain.d.ts +68 -0
- package/dist/chain.js +372 -0
- package/dist/command.d.ts +201 -46
- package/dist/command.js +713 -57
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -51
- package/dist/errors.js +58 -90
- package/dist/extension.d.ts +99 -0
- package/dist/extension.js +330 -0
- package/dist/facts.d.ts +39 -0
- package/dist/facts.js +95 -0
- package/dist/globals.d.ts +49 -28
- package/dist/globals.js +104 -58
- package/dist/glyphs.generated.d.ts +464 -0
- package/dist/glyphs.generated.js +491 -0
- package/dist/host.js +2 -1
- package/dist/index.d.ts +21 -6
- package/dist/index.js +7 -2
- package/dist/inspect.d.ts +69 -12
- package/dist/inspect.js +83 -26
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/options.d.ts +7 -0
- package/dist/options.js +9 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +132 -0
- package/dist/plugin.js +278 -0
- package/dist/rendering.d.ts +21 -0
- package/dist/rendering.js +72 -0
- package/dist/sequence.d.ts +41 -0
- package/dist/sequence.js +225 -0
- package/dist/signals.d.ts +52 -0
- package/dist/signals.js +85 -0
- package/dist/style-ansi.d.ts +13 -0
- package/dist/style-ansi.js +306 -0
- package/dist/style-layout.d.ts +29 -0
- package/dist/style-layout.js +228 -0
- package/dist/style-resolve.d.ts +6 -0
- package/dist/style-resolve.js +26 -0
- package/dist/style-state.d.ts +14 -0
- package/dist/style-state.js +179 -0
- package/dist/style-wire.d.ts +31 -0
- package/dist/style-wire.js +201 -0
- package/dist/style.d.ts +86 -0
- package/dist/style.js +201 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +22 -0
- package/dist/types.d.ts +222 -26
- package/dist/validation.d.ts +12 -3
- package/dist/validation.js +34 -17
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
package/dist/command.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
var _a;
|
|
2
|
+
import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
|
|
3
|
+
import { buildCommandExtensions, buildExtensions } from './extension.js';
|
|
4
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
|
|
5
|
+
import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
|
|
6
|
+
import { resultNode, snapshot } from './inspect.js';
|
|
3
7
|
import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
|
|
4
8
|
import { captureConfig, validateValues } from './validation.js';
|
|
5
9
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
@@ -12,6 +16,15 @@ function nodeOf(parent, child) {
|
|
|
12
16
|
}
|
|
13
17
|
return node;
|
|
14
18
|
}
|
|
19
|
+
/** Reject retired globals wiring at graph build, before any invocation reads the options. */
|
|
20
|
+
function checkCommandOptions(name, options) {
|
|
21
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
22
|
+
throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
|
|
23
|
+
}
|
|
24
|
+
if (isPlainObject(options) && 'globals' in options) {
|
|
25
|
+
throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
/** One name rule for every declared name in the graph, so a child and an argument read alike. */
|
|
16
29
|
function isDeclaredName(name) {
|
|
17
30
|
return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
@@ -27,17 +40,26 @@ function checkAliasName(command, alias) {
|
|
|
27
40
|
throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
|
|
28
41
|
}
|
|
29
42
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The state every declaration starts from. The unnamed root and each named Command share it.
|
|
45
|
+
* The declaration values arrive captured, because a later change to the options object the author
|
|
46
|
+
* passed changes nothing the declaration holds.
|
|
47
|
+
*/
|
|
48
|
+
export function freshState(declaration) {
|
|
32
49
|
return {
|
|
33
50
|
actions: [],
|
|
34
51
|
aliases: [],
|
|
35
52
|
bind: () => ({ args: {}, options: {} }),
|
|
36
53
|
children: [],
|
|
37
|
-
|
|
54
|
+
deprecated: declaration.deprecated,
|
|
55
|
+
description: declaration.description,
|
|
56
|
+
extensions: [declaration.extensions],
|
|
57
|
+
hidden: declaration.hidden,
|
|
38
58
|
inputs: [],
|
|
39
59
|
late: [],
|
|
40
|
-
name,
|
|
60
|
+
name: declaration.name,
|
|
61
|
+
options: declaration.options,
|
|
62
|
+
results: [],
|
|
41
63
|
};
|
|
42
64
|
}
|
|
43
65
|
/** A declaration after the action is an order fault; build reports the first one recorded. */
|
|
@@ -70,6 +92,15 @@ export function declareOption(state, input) {
|
|
|
70
92
|
late: recordLate(state, [{ input, kind: 'input' }]),
|
|
71
93
|
};
|
|
72
94
|
}
|
|
95
|
+
/** Globals close when composition starts; retain late calls for the shared build-order check. */
|
|
96
|
+
export function recordGlobalOption(state, name) {
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
late: state.actions.length > 0 || state.children.length > 0
|
|
100
|
+
? [...state.late, { kind: 'global', name }]
|
|
101
|
+
: state.late,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
73
104
|
/** One call's names stay one group, so the empty call the types reject still reports as one. */
|
|
74
105
|
export function declareAlias(state, names) {
|
|
75
106
|
return {
|
|
@@ -78,8 +109,47 @@ export function declareAlias(state, names) {
|
|
|
78
109
|
late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
|
|
79
110
|
};
|
|
80
111
|
}
|
|
112
|
+
/** Extension layers remain open after inputs and the action have been fixed. */
|
|
113
|
+
export function declareExtensions(state, values) {
|
|
114
|
+
return { ...state, extensions: [...state.extensions, values] };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The same channel, read as the result the handler's own declaration names. The value one call
|
|
118
|
+
* emits is checked where the action was authored, and the channel core hands an action accepts
|
|
119
|
+
* whatever the routed declaration named, so this reading adds no promise the run does not keep.
|
|
120
|
+
*/
|
|
121
|
+
function declaredChannel(out) {
|
|
122
|
+
return { ...out, results: (value) => out.results(value) };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The handler is typed against the result its own declaration carries, and the state holds one
|
|
126
|
+
* list for every declaration, so the context each handler receives is read back at the call.
|
|
127
|
+
*/
|
|
81
128
|
export function declareAction(state, handler) {
|
|
82
|
-
|
|
129
|
+
const stored = (context) => handler({ ...context, out: declaredChannel(context.out) });
|
|
130
|
+
return { ...state, actions: [...state.actions, stored] };
|
|
131
|
+
}
|
|
132
|
+
/** The result declaration, which closes both result calls and reports lateness like the rest. */
|
|
133
|
+
export function declareResult(state, kind, declaration) {
|
|
134
|
+
return {
|
|
135
|
+
...state,
|
|
136
|
+
late: recordLate(state, [{ kind: 'result' }]),
|
|
137
|
+
results: [...state.results, { kind, views: recordOf(declaration, 'views') }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
|
|
141
|
+
export function declareResultViews(state, replacements, options) {
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
results: [
|
|
145
|
+
...state.results,
|
|
146
|
+
{ default: recordOf(options, 'default'), kind: 'views', views: replacements },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** One property of an authoring argument, read defensively: build reports whatever it holds. */
|
|
151
|
+
function recordOf(declaration, key) {
|
|
152
|
+
return isPlainObject(declaration) ? declaration[key] : undefined;
|
|
83
153
|
}
|
|
84
154
|
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
85
155
|
export function attachChild(state, child) {
|
|
@@ -96,12 +166,18 @@ function checkDeclarationOrder(state) {
|
|
|
96
166
|
if (!late) {
|
|
97
167
|
return;
|
|
98
168
|
}
|
|
169
|
+
if (late.kind === 'global') {
|
|
170
|
+
throw new DeclarationError(`The Application declares global option "${late.name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
|
|
171
|
+
}
|
|
99
172
|
if (late.kind === 'input') {
|
|
100
173
|
throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
|
|
101
174
|
}
|
|
102
175
|
if (late.kind === 'alias') {
|
|
103
176
|
throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
|
|
104
177
|
}
|
|
178
|
+
if (late.kind === 'result') {
|
|
179
|
+
throw new DeclarationError(`${commandSentence(name)} declares its result after its action. Declare result() or rows() before action().`);
|
|
180
|
+
}
|
|
105
181
|
// Child identity and names are settled before this call, so the node and its name are valid.
|
|
106
182
|
throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
|
|
107
183
|
}
|
|
@@ -177,7 +253,7 @@ function buildChild(parent, [name, node], context) {
|
|
|
177
253
|
throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(owner)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
|
|
178
254
|
}
|
|
179
255
|
context.owners.set(node, parent);
|
|
180
|
-
return node.build(context);
|
|
256
|
+
return node.build({ ...context, path: [...context.path, name] });
|
|
181
257
|
}
|
|
182
258
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
183
259
|
function checkSlotOrder(slot, next, subject) {
|
|
@@ -216,18 +292,26 @@ function collectArguments(state, subject) {
|
|
|
216
292
|
}
|
|
217
293
|
return slots;
|
|
218
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* One Command's own options against the shared globals table. The table holds the application's
|
|
297
|
+
* globals and every plugin option, so a local collision reads the same sentence whichever scope on
|
|
298
|
+
* the other side claimed the name or the spelling.
|
|
299
|
+
*/
|
|
219
300
|
function compileLocalOptions(state, globals, subject) {
|
|
220
301
|
const declarations = state.inputs.filter((input) => input.kind === 'option');
|
|
302
|
+
const local = { kind: 'local', subject };
|
|
303
|
+
const application = { kind: 'application' };
|
|
221
304
|
for (const declaration of declarations) {
|
|
222
|
-
|
|
223
|
-
|
|
305
|
+
const claimed = globals.names.get(declaration.name);
|
|
306
|
+
if (claimed) {
|
|
307
|
+
throw keyCollision(declaration.name, claimed, local);
|
|
224
308
|
}
|
|
225
309
|
}
|
|
226
310
|
const options = compileOptions(declarations, subject);
|
|
227
311
|
for (const [spelling, option] of options) {
|
|
228
312
|
const global = globals.options.get(spelling);
|
|
229
313
|
if (global) {
|
|
230
|
-
throw
|
|
314
|
+
throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
|
|
231
315
|
}
|
|
232
316
|
}
|
|
233
317
|
return options;
|
|
@@ -247,44 +331,483 @@ function checkGroup(state, children) {
|
|
|
247
331
|
throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
|
|
248
332
|
}
|
|
249
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* A view name is a bare token the way a child name is, and never an array index, because an
|
|
336
|
+
* integer-like key does not keep the position the author gave it.
|
|
337
|
+
*/
|
|
338
|
+
function isViewName(name) {
|
|
339
|
+
return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
|
|
340
|
+
}
|
|
341
|
+
/** One entry read back as the whole view its own `render` function names. */
|
|
342
|
+
function isWholeView(entry) {
|
|
343
|
+
return (typeof entry === 'object' &&
|
|
344
|
+
entry !== null &&
|
|
345
|
+
'render' in entry &&
|
|
346
|
+
typeof entry.render === 'function');
|
|
347
|
+
}
|
|
348
|
+
/** The same reading for the row shape, which core feeds one row at a time. */
|
|
349
|
+
function isRowView(entry) {
|
|
350
|
+
return (typeof entry === 'object' && entry !== null && 'row' in entry && typeof entry.row === 'function');
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The key one `views()` call selected, spelled the way its own diagnostic names it. A call that
|
|
354
|
+
* names none keeps whichever key an earlier call named.
|
|
355
|
+
*/
|
|
356
|
+
function selectedKey(value) {
|
|
357
|
+
if (value === undefined) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return typeof value === 'string' ? value : (JSON.stringify(value) ?? 'undefined');
|
|
361
|
+
}
|
|
362
|
+
/** The entries one authored `views` record holds, in record order; anything else holds none. */
|
|
363
|
+
function recordEntries(record) {
|
|
364
|
+
return isPlainObject(record) ? Object.entries(record) : [];
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* One `views` entry under the unit its declaration named, read back as the shape its own functions
|
|
368
|
+
* name. The two shapes are exclusive, and a row view answers a rows declaration alone.
|
|
369
|
+
*/
|
|
370
|
+
function resultView(declaration, name, entry) {
|
|
371
|
+
const { kind, sentence } = declaration;
|
|
372
|
+
const whole = isWholeView(entry);
|
|
373
|
+
const row = isRowView(entry);
|
|
374
|
+
if (whole && row) {
|
|
375
|
+
throw new DeclarationError(`${sentence} names view "${name}" with render and row. Supply one of the two.`);
|
|
376
|
+
}
|
|
377
|
+
if (row) {
|
|
378
|
+
if (kind === 'value') {
|
|
379
|
+
throw new DeclarationError(`${sentence} names row view "${name}" on a value result. Supply a view with render, or declare the result with rows().`);
|
|
380
|
+
}
|
|
381
|
+
return entry;
|
|
382
|
+
}
|
|
383
|
+
if (whole) {
|
|
384
|
+
return entry;
|
|
385
|
+
}
|
|
386
|
+
throw new DeclarationError(`${sentence} names view "${name}" with a value that is not a view. Supply a view with render or a row view with row.`);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Every rule the results lane carries, applied to one Command's calls in the order it made them.
|
|
390
|
+
* The merged record is what the rules read: a later `views()` call replaces a key in place and
|
|
391
|
+
* appends a new one, so each name keeps the position the call that first named it gave it. A
|
|
392
|
+
* `default` once named persists through later calls that name none, and the first key answers
|
|
393
|
+
* until one is named.
|
|
394
|
+
*/
|
|
395
|
+
function buildResult(state, hasAction) {
|
|
396
|
+
const sentence = commandSentence(state.name);
|
|
397
|
+
const declarations = state.results.filter((call) => call.kind !== 'views');
|
|
398
|
+
if (declarations.length > 1) {
|
|
399
|
+
throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
|
|
400
|
+
}
|
|
401
|
+
const declaration = declarations[0];
|
|
402
|
+
// A `views()` call reshapes a result's views, so one with no result reshapes nothing.
|
|
403
|
+
// The types publish the call where a result is carried, so this reaches a JavaScript author.
|
|
404
|
+
if (!declaration) {
|
|
405
|
+
if (state.results.length > 0) {
|
|
406
|
+
throw new DeclarationError(`${sentence} reshapes its views and declares no result. Declare result() or rows() before action().`);
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
if (!hasAction) {
|
|
411
|
+
throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
|
|
412
|
+
}
|
|
413
|
+
const views = new Map();
|
|
414
|
+
let selected = undefined;
|
|
415
|
+
for (const call of state.results) {
|
|
416
|
+
for (const [name, entry] of recordEntries(call.views)) {
|
|
417
|
+
const view = resultView({ kind: declaration.kind, sentence }, name, entry);
|
|
418
|
+
if (!isViewName(name)) {
|
|
419
|
+
throw new DeclarationError(`${sentence} names view "${name}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
|
|
420
|
+
}
|
|
421
|
+
views.set(name, view);
|
|
422
|
+
}
|
|
423
|
+
if (call.kind === 'views') {
|
|
424
|
+
selected = selectedKey(call.default) ?? selected;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const first = views.keys().next();
|
|
428
|
+
if (first.done === true) {
|
|
429
|
+
throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
|
|
430
|
+
}
|
|
431
|
+
if (selected !== undefined && !views.has(selected)) {
|
|
432
|
+
throw new DeclarationError(`${sentence} selects default view "${selected}", which it does not name. Name the view or select a named one.`);
|
|
433
|
+
}
|
|
434
|
+
return { default: selected ?? first.value, kind: declaration.kind, views };
|
|
435
|
+
}
|
|
436
|
+
/** The values one build made, so a value a hook returns is one of them and never a forged shape. */
|
|
437
|
+
const attachments = new WeakMap();
|
|
438
|
+
/**
|
|
439
|
+
* The declared names of one kind, in declaration order, as a hook reads them. The list is frozen,
|
|
440
|
+
* because the surface publishes it read-only and a hook reshapes a Command through its calls alone.
|
|
441
|
+
*/
|
|
442
|
+
function declaredNames(declared, kind) {
|
|
443
|
+
return Object.freeze(declared.inputs.filter((input) => input.kind === kind).map((input) => input.name));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* One Command unlocked for a hook. Every call returns a new value and leaves its receiver
|
|
447
|
+
* unchanged, and records what it added twice over: on the erased declaration the next value reads
|
|
448
|
+
* its facts from, and on the call list the declaration reads back once every hook has returned.
|
|
449
|
+
* The result is resolved for each value, so a `views()` call reports its own fault where it is made
|
|
450
|
+
* and a later call reads the record that call left behind.
|
|
451
|
+
*/
|
|
452
|
+
class AttachedCommandValue {
|
|
453
|
+
#state;
|
|
454
|
+
#result;
|
|
455
|
+
constructor(state) {
|
|
456
|
+
this.#state = state;
|
|
457
|
+
this.#result = resultNode(buildResult(state.declared, state.hasAction));
|
|
458
|
+
attachments.set(this, state);
|
|
459
|
+
Object.freeze(this);
|
|
460
|
+
}
|
|
461
|
+
get name() {
|
|
462
|
+
return this.#state.declared.name;
|
|
463
|
+
}
|
|
464
|
+
get path() {
|
|
465
|
+
return this.#state.path;
|
|
466
|
+
}
|
|
467
|
+
get hasAction() {
|
|
468
|
+
return this.#state.hasAction;
|
|
469
|
+
}
|
|
470
|
+
get arguments() {
|
|
471
|
+
return declaredNames(this.#state.declared, 'argument');
|
|
472
|
+
}
|
|
473
|
+
get options() {
|
|
474
|
+
return declaredNames(this.#state.declared, 'option');
|
|
475
|
+
}
|
|
476
|
+
get result() {
|
|
477
|
+
return this.#result;
|
|
478
|
+
}
|
|
479
|
+
argument(name, config) {
|
|
480
|
+
return this.#declare({ config: captureConfig(config), kind: 'argument', name });
|
|
481
|
+
}
|
|
482
|
+
option(name, config) {
|
|
483
|
+
return this.#declare({ config: captureConfig(config), kind: 'option', name });
|
|
484
|
+
}
|
|
485
|
+
views(replacements, options) {
|
|
486
|
+
const call = {
|
|
487
|
+
default: recordOf(options, 'default'),
|
|
488
|
+
kind: 'views',
|
|
489
|
+
views: replacements,
|
|
490
|
+
};
|
|
491
|
+
const { declared } = this.#state;
|
|
492
|
+
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
493
|
+
}
|
|
494
|
+
extend(...values) {
|
|
495
|
+
return this.#derive({ kind: 'extend', values }, this.#state.declared);
|
|
496
|
+
}
|
|
497
|
+
/** One input the running hook declared, which the Command's own names now hold. */
|
|
498
|
+
#declare(input) {
|
|
499
|
+
const { declared, identity } = this.#state;
|
|
500
|
+
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
501
|
+
}
|
|
502
|
+
#derive(call, declared) {
|
|
503
|
+
const state = this.#state;
|
|
504
|
+
return new _a({ ...state, calls: [...state.calls, call], declared });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
_a = AttachedCommandValue;
|
|
508
|
+
/** The reason one failed hook reports, which is the thrown value's own message. */
|
|
509
|
+
function attachReason(error) {
|
|
510
|
+
return error instanceof Error ? error.message : String(error);
|
|
511
|
+
}
|
|
512
|
+
/** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
|
|
513
|
+
function callHook(value, hook, named) {
|
|
514
|
+
try {
|
|
515
|
+
return hook(value);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
// A hook that reports a declaration fault of its own reports as itself.
|
|
519
|
+
if (error instanceof DeclarationError) {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
throw new DeclarationError(`Plugin "${named.identity}" failed in onCommandAttach for ${named.subject}: ${attachReason(error)}.`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* One plugin's hook over one Command, which answers with the declaration the hook returned. The
|
|
527
|
+
* returned value has to carry the lineage token of the value the hook received, so the surface of
|
|
528
|
+
* another Command, or of one an earlier build made, never replays its calls onto this Command.
|
|
529
|
+
*/
|
|
530
|
+
function attachOnce(value, hook, named) {
|
|
531
|
+
const state = attachments.get(callHook(value, hook, named));
|
|
532
|
+
if (!state || state.lineage !== named.lineage) {
|
|
533
|
+
throw new DeclarationError(`Plugin "${named.identity}" returned a value that is not the attached Command from onCommandAttach for ${named.subject}. Return the value it received or a value derived from it.`);
|
|
534
|
+
}
|
|
535
|
+
return state;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Every installed plugin's hook over one Command, in installation order, each receiving what the
|
|
539
|
+
* previous returned. The calls they made travel back to the declaration the remaining rules read.
|
|
540
|
+
*/
|
|
541
|
+
function runAttachHooks(declared, facts, plugins) {
|
|
542
|
+
const subject = commandSubject(declared.name);
|
|
543
|
+
const { lineage } = facts;
|
|
544
|
+
let progress = { calls: [], declared };
|
|
545
|
+
for (const installed of plugins) {
|
|
546
|
+
const hook = installed.onCommandAttach;
|
|
547
|
+
if (hook) {
|
|
548
|
+
const identity = installed.identity;
|
|
549
|
+
const value = new AttachedCommandValue({ ...progress, ...facts, identity });
|
|
550
|
+
const state = attachOnce(value, hook, { identity, lineage, subject });
|
|
551
|
+
progress = { calls: state.calls, declared: state.declared };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return progress.calls;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* One input a hook declared. It joins the values an action reads at run time and the declaration's
|
|
558
|
+
* types not at all, and it records no late declaration, because a hook's calls are exempt from the
|
|
559
|
+
* closures `action()` applies.
|
|
560
|
+
*/
|
|
561
|
+
function declareHookInput(state, input) {
|
|
562
|
+
const previous = state.bind;
|
|
563
|
+
return {
|
|
564
|
+
...state,
|
|
565
|
+
bind: (values) => {
|
|
566
|
+
const bound = previous(values);
|
|
567
|
+
return input.kind === 'argument'
|
|
568
|
+
? { ...bound, args: { ...bound.args, ...values.argument(input) } }
|
|
569
|
+
: { ...bound, options: { ...bound.options, ...values.option(input) } };
|
|
570
|
+
},
|
|
571
|
+
inputs: [...state.inputs, input],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/** The declaration the hooks left behind, which every remaining build rule reads. */
|
|
575
|
+
function applyAttachCalls(state, calls) {
|
|
576
|
+
let next = state;
|
|
577
|
+
for (const call of calls) {
|
|
578
|
+
if (call.kind === 'input') {
|
|
579
|
+
next = declareHookInput(next, call.input);
|
|
580
|
+
}
|
|
581
|
+
else if (call.kind === 'views') {
|
|
582
|
+
next = { ...next, results: [...next.results, call.call] };
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
next = declareExtensions(next, call.values);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return next;
|
|
589
|
+
}
|
|
590
|
+
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
591
|
+
function hookClause(kind, identity) {
|
|
592
|
+
return {
|
|
593
|
+
clause: `an ${kind} plugin "${identity}" declared through onCommandAttach`,
|
|
594
|
+
remedy: 'Install one of them.',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* The remedy a collision against the Command's own declarations, the globals table, or another
|
|
599
|
+
* plugin's option earns. The remedy follows the target alone, whichever kind the hook declared:
|
|
600
|
+
* the author renames their own local option or argument, the author renames the colliding global
|
|
601
|
+
* option, or, when the target is another plugin's option, the two plugins install one of them.
|
|
602
|
+
*/
|
|
603
|
+
function attachedRemedy(target) {
|
|
604
|
+
if (target === 'local') {
|
|
605
|
+
return "Rename the Command's option or omit the plugin.";
|
|
606
|
+
}
|
|
607
|
+
if (target === 'argument') {
|
|
608
|
+
return "Rename the Command's argument or omit the plugin.";
|
|
609
|
+
}
|
|
610
|
+
if (target === 'global') {
|
|
611
|
+
return 'Rename the global option or omit the plugin.';
|
|
612
|
+
}
|
|
613
|
+
return 'Install one of them.';
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* What one name a hook-declared input of either kind collides with, in the order the scopes are
|
|
617
|
+
* reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
|
|
618
|
+
* plugin-owned option, or an argument the Command declares. The remedy follows the target alone,
|
|
619
|
+
* not which kind the hook declared.
|
|
620
|
+
*/
|
|
621
|
+
function attachedCollision(input, held) {
|
|
622
|
+
const { name } = input;
|
|
623
|
+
const hook = held.hooks.get(name);
|
|
624
|
+
if (hook !== undefined) {
|
|
625
|
+
return hookClause('option', hook);
|
|
626
|
+
}
|
|
627
|
+
const hooked = held.hookArguments.get(name);
|
|
628
|
+
if (hooked !== undefined) {
|
|
629
|
+
return hookClause('argument', hooked);
|
|
630
|
+
}
|
|
631
|
+
if (held.locals.has(name)) {
|
|
632
|
+
return { clause: 'a local option', remedy: attachedRemedy('local') };
|
|
633
|
+
}
|
|
634
|
+
const claimed = held.globals.names.get(name);
|
|
635
|
+
if (claimed) {
|
|
636
|
+
const target = claimed.kind === 'plugin' ? 'plugin' : 'global';
|
|
637
|
+
const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
|
|
638
|
+
return { clause, remedy: attachedRemedy(target) };
|
|
639
|
+
}
|
|
640
|
+
return held.arguments.has(name)
|
|
641
|
+
? { clause: 'an argument', remedy: attachedRemedy('argument') }
|
|
642
|
+
: undefined;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* The spellings one compiled table holds, each under the form its own option is named by, which is
|
|
646
|
+
* its long form where it declares one and the colliding spelling itself where it declares none.
|
|
647
|
+
*/
|
|
648
|
+
function readSpellings(table, claimed) {
|
|
649
|
+
const longs = new Map();
|
|
650
|
+
for (const [spelling, option] of table) {
|
|
651
|
+
if (option.role === 'long') {
|
|
652
|
+
longs.set(option.name, spelling);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
for (const [spelling, option] of table) {
|
|
656
|
+
if (!claimed.has(spelling)) {
|
|
657
|
+
claimed.set(spelling, longs.get(option.name) ?? spelling);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/** One hook-declared option's spellings, against every spelling the table already claims. */
|
|
662
|
+
function checkAttachedSpelling(declared, named) {
|
|
663
|
+
const { claimed, subject } = named;
|
|
664
|
+
const table = compileOptions([declared.input], subject);
|
|
665
|
+
for (const [spelling] of table) {
|
|
666
|
+
const used = claimed.get(spelling);
|
|
667
|
+
if (used !== undefined) {
|
|
668
|
+
throw new DeclarationError(`Plugin "${declared.identity}" declares option "${declared.input.name}" with spelling "${spelling}" on ${subject}, which "${used}" already uses.`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
readSpellings(table, claimed);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Every input a hook declared, against the names and spellings the Command, the globals table, and
|
|
675
|
+
* an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
|
|
676
|
+
* input reports as the plugin's fault and never as the author's.
|
|
677
|
+
*/
|
|
678
|
+
function checkAttachedInputs(declared, attached, globals) {
|
|
679
|
+
const subject = commandSubject(declared.name);
|
|
680
|
+
const hooked = new Set(attached.map((entry) => entry.input));
|
|
681
|
+
const authored = declared.inputs.filter((input) => !hooked.has(input));
|
|
682
|
+
const options = authored.filter((input) => input.kind === 'option');
|
|
683
|
+
const held = {
|
|
684
|
+
arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
|
|
685
|
+
globals,
|
|
686
|
+
hookArguments: new Map(),
|
|
687
|
+
hooks: new Map(),
|
|
688
|
+
locals: new Set(options.map((input) => input.name)),
|
|
689
|
+
};
|
|
690
|
+
const claimed = new Map();
|
|
691
|
+
readSpellings(globals.options, claimed);
|
|
692
|
+
readSpellings(compileOptions(options, subject), claimed);
|
|
693
|
+
for (const { identity, input } of attached) {
|
|
694
|
+
const collision = attachedCollision(input, held);
|
|
695
|
+
if (collision) {
|
|
696
|
+
throw new DeclarationError(`Plugin "${identity}" declares ${input.kind} "${input.name}" on ${subject}, which is already declared as ${collision.clause}. ${collision.remedy}`);
|
|
697
|
+
}
|
|
698
|
+
if (input.kind === 'option') {
|
|
699
|
+
checkAttachedSpelling({ identity, input }, { claimed, subject });
|
|
700
|
+
held.hooks.set(input.name, identity);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
held.hookArguments.set(input.name, identity);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
250
707
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
251
|
-
function bindDispatch(state, action) {
|
|
252
|
-
return ({ host, out, passthrough, values }) => {
|
|
708
|
+
function bindDispatch(state, action, globals) {
|
|
709
|
+
return ({ host, out, passthrough, signal, style, values }) => {
|
|
253
710
|
const bound = state.bind(values);
|
|
711
|
+
// Last resort: no typed path exists.
|
|
712
|
+
// The graph erases the binder's generic relationship.
|
|
713
|
+
// It holds because attachment checks the global output requirement.
|
|
714
|
+
// Graph build rejects a collision between a global and a local.
|
|
715
|
+
// This binder returns the Application's validated globals alone.
|
|
716
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
717
|
+
const globalOptions = globals.bind(values);
|
|
254
718
|
return action({
|
|
255
719
|
args: bound.args,
|
|
256
720
|
host,
|
|
257
|
-
options: { ...
|
|
721
|
+
options: { ...globalOptions, ...bound.options },
|
|
258
722
|
out,
|
|
259
723
|
passthrough,
|
|
724
|
+
signal,
|
|
725
|
+
style,
|
|
260
726
|
});
|
|
261
727
|
};
|
|
262
728
|
}
|
|
729
|
+
/** Each declaration's own facts and extension values, whichever scope declared it. */
|
|
730
|
+
function checkInputFacts(inputs, command, context) {
|
|
731
|
+
const { name, subject } = command;
|
|
732
|
+
for (const input of inputs) {
|
|
733
|
+
const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
|
|
734
|
+
checkDescription(sentence, input.config.description);
|
|
735
|
+
if (input.kind === 'argument') {
|
|
736
|
+
checkNoListingFacts(sentence, input.config);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
checkHidden(sentence, input.config.hidden);
|
|
740
|
+
checkDeprecated(sentence, input.config.deprecated);
|
|
741
|
+
}
|
|
742
|
+
context.extensions.set(input, buildExtensions({
|
|
743
|
+
declared: input.config.extensions,
|
|
744
|
+
descriptors: context.descriptors,
|
|
745
|
+
subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
|
|
746
|
+
target: input.kind,
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
751
|
+
function checkArgumentPlacement(name, slot, child) {
|
|
752
|
+
if (slot && child) {
|
|
753
|
+
throw new DeclarationError(`${commandSentence(name)} declares argument "${slot.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
263
756
|
/** Validates one declaration against the shared globals table and compiles it for dispatch. */
|
|
264
757
|
export function buildCommand(state, context) {
|
|
265
758
|
const { actions, name } = state;
|
|
266
759
|
const { globals } = context;
|
|
267
760
|
const subject = commandSubject(name);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
761
|
+
const layer = { phrase: `on ${subject}`, sentence: commandSentence(name) };
|
|
762
|
+
checkCommandOptions(name, state.options);
|
|
763
|
+
const description = checkDescription(commandSentence(name), state.description);
|
|
764
|
+
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
765
|
+
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
766
|
+
const declaredExtensions = buildCommandExtensions({
|
|
767
|
+
descriptors: context.descriptors,
|
|
768
|
+
layers: state.extensions,
|
|
769
|
+
subject: layer,
|
|
770
|
+
});
|
|
771
|
+
// Each declaration's own facts, in authoring order, before the rules that pair declarations.
|
|
772
|
+
checkInputFacts(state.inputs, { name, subject }, context);
|
|
271
773
|
const attached = collectChildren(state);
|
|
272
774
|
checkDeclarationOrder(state);
|
|
273
775
|
const aliases = collectAliases(state);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const child = attached[0];
|
|
277
|
-
if (first && child) {
|
|
278
|
-
throw new DeclarationError(`${commandSentence(name)} declares argument "${first.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
|
|
279
|
-
}
|
|
776
|
+
const declaredSlots = collectArguments(state, subject);
|
|
777
|
+
checkArgumentPlacement(name, declaredSlots[0], attached[0]);
|
|
280
778
|
if (actions.length > 1) {
|
|
281
779
|
throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
|
|
282
780
|
}
|
|
283
781
|
const action = actions[0];
|
|
782
|
+
const hasAction = action !== undefined;
|
|
783
|
+
const declaredResult = buildResult(state, hasAction);
|
|
784
|
+
/**
|
|
785
|
+
* The hooks run once the author's declaration is complete and its result is resolved, so a hook
|
|
786
|
+
* reads an exact record, and every rule below reads what the hooks returned.
|
|
787
|
+
*/
|
|
788
|
+
// One token per Command per build, which binds a hook's return to the value it received.
|
|
789
|
+
const lineage = {};
|
|
790
|
+
const calls = runAttachHooks(state, { hasAction, lineage, path: context.path }, context.plugins);
|
|
791
|
+
const hooked = applyAttachCalls(state, calls);
|
|
792
|
+
const hookInputs = calls.filter((call) => call.kind === 'input');
|
|
793
|
+
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
794
|
+
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
795
|
+
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
796
|
+
checkAttachedInputs(hooked, hookInputs, globals);
|
|
797
|
+
const extensions = calls.some((call) => call.kind === 'extend')
|
|
798
|
+
? buildCommandExtensions({
|
|
799
|
+
descriptors: context.descriptors,
|
|
800
|
+
layers: hooked.extensions,
|
|
801
|
+
subject: layer,
|
|
802
|
+
})
|
|
803
|
+
: declaredExtensions;
|
|
804
|
+
const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
|
|
805
|
+
checkArgumentPlacement(name, slots[0], attached[0]);
|
|
806
|
+
const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
|
|
284
807
|
if (!action) {
|
|
285
|
-
checkGroup(
|
|
808
|
+
checkGroup(hooked, attached);
|
|
286
809
|
}
|
|
287
|
-
const options = compileLocalOptions(
|
|
810
|
+
const options = compileLocalOptions(hooked, globals, subject);
|
|
288
811
|
const children = new Map();
|
|
289
812
|
const routes = new Map();
|
|
290
813
|
const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
|
|
@@ -302,17 +825,33 @@ export function buildCommand(state, context) {
|
|
|
302
825
|
aliases,
|
|
303
826
|
arguments: slots,
|
|
304
827
|
children,
|
|
305
|
-
|
|
306
|
-
|
|
828
|
+
deprecated,
|
|
829
|
+
description,
|
|
830
|
+
dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
|
|
831
|
+
extensions,
|
|
832
|
+
hidden,
|
|
833
|
+
inputs: hooked.inputs,
|
|
307
834
|
name,
|
|
308
835
|
options,
|
|
836
|
+
result,
|
|
309
837
|
routes,
|
|
310
838
|
};
|
|
311
839
|
}
|
|
312
840
|
/** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
|
|
313
|
-
export function buildGraph(root) {
|
|
314
|
-
const context = {
|
|
315
|
-
|
|
841
|
+
export function buildGraph(root, globals, install) {
|
|
842
|
+
const context = {
|
|
843
|
+
descriptors: install.descriptors,
|
|
844
|
+
extensions: install.extensions,
|
|
845
|
+
globals: buildGlobals(globals, install.plugins, install),
|
|
846
|
+
owners: new Map(),
|
|
847
|
+
path: [],
|
|
848
|
+
plugins: install.plugins,
|
|
849
|
+
};
|
|
850
|
+
return {
|
|
851
|
+
extensions: context.extensions,
|
|
852
|
+
globals: context.globals,
|
|
853
|
+
root: buildCommand(root, context),
|
|
854
|
+
};
|
|
316
855
|
}
|
|
317
856
|
export class CommandBuilder {
|
|
318
857
|
#state;
|
|
@@ -329,7 +868,7 @@ export class CommandBuilder {
|
|
|
329
868
|
kind: 'argument',
|
|
330
869
|
name,
|
|
331
870
|
};
|
|
332
|
-
return
|
|
871
|
+
return this.derive(declareArgument(this.#state, input));
|
|
333
872
|
}
|
|
334
873
|
option(name, config) {
|
|
335
874
|
const input = {
|
|
@@ -337,38 +876,69 @@ export class CommandBuilder {
|
|
|
337
876
|
kind: 'option',
|
|
338
877
|
name,
|
|
339
878
|
};
|
|
340
|
-
return
|
|
879
|
+
return this.derive(declareOption(this.#state, input));
|
|
341
880
|
}
|
|
342
881
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
882
|
+
* Aliases are other bare tokens that route to this Command. They invalidate no call, and the
|
|
883
|
+
* tuple rest parameter rejects a call that names none.
|
|
345
884
|
*/
|
|
346
885
|
alias(...names) {
|
|
347
|
-
return
|
|
886
|
+
return this.derive(declareAlias(this.#state, names));
|
|
348
887
|
}
|
|
349
888
|
/** A child arrives in any type state, because its own action is the call that finished it. */
|
|
350
889
|
command(child) {
|
|
351
|
-
return
|
|
890
|
+
return this.derive(attachChild(this.#state, child));
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* The value this Command produces for its consumer. The type argument is stated by the author,
|
|
894
|
+
* so the views record states no type of its own and an omitted argument names none either.
|
|
895
|
+
*/
|
|
896
|
+
result(declaration) {
|
|
897
|
+
return this.derive(declareResult(this.#state, 'value', declaration));
|
|
352
898
|
}
|
|
353
|
-
/** The
|
|
899
|
+
/** The same declaration over a sequence, whose type argument is one row. */
|
|
900
|
+
rows(declaration) {
|
|
901
|
+
return this.derive(declareResult(this.#state, 'rows', declaration));
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Views after the fact. It merges by key, so an existing name is replaced in place and a new one
|
|
905
|
+
* is appended, and `default` names the key core renders when nothing selects another.
|
|
906
|
+
*/
|
|
907
|
+
views(replacements, options) {
|
|
908
|
+
return this.derive(declareResultViews(this.#state, replacements, options));
|
|
909
|
+
}
|
|
910
|
+
/** The action closes input authoring; `extend()` remains outside this state transition. */
|
|
354
911
|
action(handler) {
|
|
355
|
-
return
|
|
912
|
+
return this.derive(declareAction(this.#state, handler));
|
|
913
|
+
}
|
|
914
|
+
extend(...values) {
|
|
915
|
+
return this.derive(declareExtensions(this.#state, values));
|
|
356
916
|
}
|
|
357
917
|
build(context) {
|
|
358
918
|
return buildCommand(this.#state, context);
|
|
359
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* The same runtime value in the state the calling method's return type names. Each call states
|
|
922
|
+
* its own transition, and the declared result travels with it unless the call replaces it.
|
|
923
|
+
*/
|
|
924
|
+
derive(state) {
|
|
925
|
+
return new CommandBuilder(state);
|
|
926
|
+
}
|
|
360
927
|
}
|
|
361
|
-
/**
|
|
362
|
-
* The runtime class behind the public constructor. It is generic so that an instance's `Globals`
|
|
363
|
-
* is the type of the value it holds, with `{}` standing in when there is none, which is what each
|
|
364
|
-
* signature of the constructor interface publishes.
|
|
365
|
-
*/
|
|
928
|
+
/** The constructor uses the Application registration; public type defaults stay library-neutral. */
|
|
366
929
|
class CommandDeclaration extends CommandBuilder {
|
|
367
|
-
constructor(name,
|
|
368
|
-
super(freshState(
|
|
930
|
+
constructor(name, options) {
|
|
931
|
+
super(freshState({
|
|
932
|
+
deprecated: options?.deprecated,
|
|
933
|
+
description: options?.description,
|
|
934
|
+
extensions: options?.extensions,
|
|
935
|
+
hidden: options?.hidden,
|
|
936
|
+
name,
|
|
937
|
+
options,
|
|
938
|
+
}));
|
|
369
939
|
}
|
|
370
940
|
}
|
|
371
|
-
/** The public constructor
|
|
941
|
+
/** The public constructor takes a name and one options object, as the Application does. */
|
|
372
942
|
export const Command = CommandDeclaration;
|
|
373
943
|
/** Every declaration in the graph, so defaults are validated before any token is read. */
|
|
374
944
|
export function collectInputs(command) {
|
|
@@ -377,6 +947,14 @@ export function collectInputs(command) {
|
|
|
377
947
|
...[...command.children.values()].flatMap((child) => collectInputs(child)),
|
|
378
948
|
];
|
|
379
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* The names a routing failure offers: the canonical names of the visible children, in authoring
|
|
952
|
+
* order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
|
|
953
|
+
* children are all hidden offers none.
|
|
954
|
+
*/
|
|
955
|
+
function candidatesOf(command) {
|
|
956
|
+
return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
|
|
957
|
+
}
|
|
380
958
|
/** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
|
|
381
959
|
export function route(root, tokens) {
|
|
382
960
|
let command = root;
|
|
@@ -389,7 +967,7 @@ export function route(root, tokens) {
|
|
|
389
967
|
}
|
|
390
968
|
const child = command.routes.get(token);
|
|
391
969
|
if (!child) {
|
|
392
|
-
throw new UnknownCommandError(token,
|
|
970
|
+
throw new UnknownCommandError(token, candidatesOf(command));
|
|
393
971
|
}
|
|
394
972
|
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
395
973
|
command = child.command;
|
|
@@ -428,25 +1006,103 @@ function bindArguments(command, path, positionals) {
|
|
|
428
1006
|
}
|
|
429
1007
|
return values;
|
|
430
1008
|
}
|
|
431
|
-
/** Consumes globals
|
|
432
|
-
export
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
1009
|
+
/** Consumes the globals table, then routes the remaining bare tokens to a Command. */
|
|
1010
|
+
export function routeInvocation(graph, argv) {
|
|
1011
|
+
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
1012
|
+
const routed = route(graph.root, scan.rest);
|
|
1013
|
+
return {
|
|
1014
|
+
command: routed.command,
|
|
1015
|
+
path: routed.path,
|
|
1016
|
+
scan: scan.values,
|
|
1017
|
+
tokens: routed.tokens,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
|
|
1022
|
+
* value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
|
|
1023
|
+
* schema's output object reaches its own copy and contributes nothing to what the action receives.
|
|
1024
|
+
* A value that is neither an array nor a plain object, a class instance or a `Date` a schema
|
|
1025
|
+
* produced, is shared by reference, because core cannot copy it meaningfully.
|
|
1026
|
+
*/
|
|
1027
|
+
function requestOf(command, values, passthrough) {
|
|
1028
|
+
const args = {};
|
|
1029
|
+
const options = {};
|
|
1030
|
+
for (const input of command.inputs) {
|
|
1031
|
+
const target = input.kind === 'argument' ? args : options;
|
|
1032
|
+
target[input.name] = snapshot(values.read(input));
|
|
1033
|
+
}
|
|
1034
|
+
return Object.freeze({
|
|
1035
|
+
args: Object.freeze(args),
|
|
1036
|
+
options: Object.freeze(options),
|
|
1037
|
+
passthrough: Object.freeze([...passthrough]),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* The phases that run ahead of the chain: the callable check, local parsing, and validation. It
|
|
1042
|
+
* answers with the call that dispatches, so the caller records that the action was invoked at the
|
|
1043
|
+
* moment it invokes it and no earlier failure reads as a dispatch.
|
|
1044
|
+
*/
|
|
1045
|
+
async function readyDispatch(graph, routed, invocation) {
|
|
1046
|
+
const { command, path, scan } = routed;
|
|
436
1047
|
const { dispatch } = command;
|
|
437
1048
|
// A group answers no invocation of its own, so it fails with the routing errors above it.
|
|
438
1049
|
if (!dispatch) {
|
|
439
|
-
throw new NonCallableCommandError(path,
|
|
1050
|
+
throw new NonCallableCommandError(path, candidatesOf(command));
|
|
440
1051
|
}
|
|
441
|
-
const parsed = parseInputs(command.options,
|
|
1052
|
+
const parsed = parseInputs(command.options, routed.tokens);
|
|
442
1053
|
const args = bindArguments(command, path, parsed.positionals);
|
|
443
1054
|
const values = await validateValues({
|
|
444
1055
|
command: path,
|
|
445
1056
|
defaults: invocation.defaults,
|
|
446
|
-
host,
|
|
1057
|
+
host: invocation.host,
|
|
447
1058
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
448
1059
|
passthrough: parsed.passthrough,
|
|
449
|
-
|
|
1060
|
+
signal: invocation.signal,
|
|
1061
|
+
supplied: { args, options: mergeValues(scan, parsed.options) },
|
|
450
1062
|
});
|
|
451
|
-
return {
|
|
1063
|
+
return {
|
|
1064
|
+
dispatch: async (view) => {
|
|
1065
|
+
// The channel is built at the boundary, because the view a middleware selected is read there.
|
|
1066
|
+
const channel = invocation.channel({ path, result: command.result, view });
|
|
1067
|
+
try {
|
|
1068
|
+
await dispatch({
|
|
1069
|
+
host: invocation.host,
|
|
1070
|
+
out: channel.out,
|
|
1071
|
+
passthrough: parsed.passthrough,
|
|
1072
|
+
signal: invocation.signal,
|
|
1073
|
+
style: invocation.style,
|
|
1074
|
+
values,
|
|
1075
|
+
});
|
|
1076
|
+
/**
|
|
1077
|
+
* A declared result is a promise the Command makes, so an action that returned normally
|
|
1078
|
+
* without emitting one broke it. A failure raised before the call is that failure, and a
|
|
1079
|
+
* cancelled run raises none, because an action that reads its signal and returns is the
|
|
1080
|
+
* sanctioned path.
|
|
1081
|
+
*/
|
|
1082
|
+
if (command.result && !channel.emitted() && !invocation.signal.aborted) {
|
|
1083
|
+
throw new ResultError('missing', path);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
// The action's failure is the invocation's outcome.
|
|
1088
|
+
// A sequence it left pending is stopped where it stands rather than drained to its end.
|
|
1089
|
+
channel.stop();
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
request: requestOf(command, values, parsed.passthrough),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
|
|
1098
|
+
* middleware reads the request before the action runs and a takeover never observes the fault.
|
|
1099
|
+
*/
|
|
1100
|
+
export async function prepareDispatch(graph, routed, invocation) {
|
|
1101
|
+
const result = routed.command.result;
|
|
1102
|
+
try {
|
|
1103
|
+
return { ...(await readyDispatch(graph, routed, invocation)), kind: 'ready', result };
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
return { fault: error, kind: 'held', request: null, result };
|
|
1107
|
+
}
|
|
452
1108
|
}
|