@loomcli/core 0.2.0 → 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 +59 -34
- package/dist/application.js +162 -59
- package/dist/chain.d.ts +25 -6
- package/dist/chain.js +99 -15
- package/dist/command.d.ts +146 -42
- package/dist/command.js +620 -78
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -59
- package/dist/errors.js +49 -106
- package/dist/extension.d.ts +5 -1
- package/dist/extension.js +18 -1
- package/dist/globals.d.ts +14 -25
- package/dist/globals.js +10 -61
- 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 +14 -5
- package/dist/index.js +5 -2
- package/dist/inspect.d.ts +26 -3
- package/dist/inspect.js +19 -5
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +32 -16
- package/dist/plugin.js +46 -18
- 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/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 +169 -21
- package/dist/validation.d.ts +8 -1
- package/dist/validation.js +17 -2
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
package/dist/command.js
CHANGED
|
@@ -1,7 +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';
|
|
3
4
|
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
|
|
4
|
-
import {
|
|
5
|
+
import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
|
|
6
|
+
import { resultNode, snapshot } from './inspect.js';
|
|
5
7
|
import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
|
|
6
8
|
import { captureConfig, validateValues } from './validation.js';
|
|
7
9
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
@@ -14,20 +16,13 @@ function nodeOf(parent, child) {
|
|
|
14
16
|
}
|
|
15
17
|
return node;
|
|
16
18
|
}
|
|
17
|
-
/**
|
|
18
|
-
* The shape of a Command's second argument, read where it is supplied. The retired positional form
|
|
19
|
-
* declares its globals on a value that holds no `globals` key, so without this rule the globals
|
|
20
|
-
* vanish silently and the operator, not the author, meets the consequence as an unknown-option
|
|
21
|
-
* error. It answers before every other rule, because a Command that lost its globals this way would
|
|
22
|
-
* otherwise report the mismatch with its Application instead of the slot that caused it.
|
|
23
|
-
* The facts the slot carried were captured at construction, so this reads the slot's shape alone.
|
|
24
|
-
*/
|
|
19
|
+
/** Reject retired globals wiring at graph build, before any invocation reads the options. */
|
|
25
20
|
function checkCommandOptions(name, options) {
|
|
26
|
-
if (isGlobalOptions(options)) {
|
|
27
|
-
throw new DeclarationError(`${commandSentence(name)} takes an options object. Supply { globals } instead of a positional GlobalOptions value.`);
|
|
28
|
-
}
|
|
29
21
|
if (options !== undefined && !isPlainObject(options)) {
|
|
30
|
-
throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply
|
|
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.`);
|
|
31
26
|
}
|
|
32
27
|
}
|
|
33
28
|
/** One name rule for every declared name in the graph, so a child and an argument read alike. */
|
|
@@ -58,13 +53,13 @@ export function freshState(declaration) {
|
|
|
58
53
|
children: [],
|
|
59
54
|
deprecated: declaration.deprecated,
|
|
60
55
|
description: declaration.description,
|
|
61
|
-
extensions: declaration.extensions,
|
|
62
|
-
globals: declaration.globals,
|
|
56
|
+
extensions: [declaration.extensions],
|
|
63
57
|
hidden: declaration.hidden,
|
|
64
58
|
inputs: [],
|
|
65
59
|
late: [],
|
|
66
60
|
name: declaration.name,
|
|
67
61
|
options: declaration.options,
|
|
62
|
+
results: [],
|
|
68
63
|
};
|
|
69
64
|
}
|
|
70
65
|
/** A declaration after the action is an order fault; build reports the first one recorded. */
|
|
@@ -97,6 +92,15 @@ export function declareOption(state, input) {
|
|
|
97
92
|
late: recordLate(state, [{ input, kind: 'input' }]),
|
|
98
93
|
};
|
|
99
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
|
+
}
|
|
100
104
|
/** One call's names stay one group, so the empty call the types reject still reports as one. */
|
|
101
105
|
export function declareAlias(state, names) {
|
|
102
106
|
return {
|
|
@@ -105,8 +109,47 @@ export function declareAlias(state, names) {
|
|
|
105
109
|
late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
|
|
106
110
|
};
|
|
107
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
|
+
*/
|
|
108
128
|
export function declareAction(state, handler) {
|
|
109
|
-
|
|
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;
|
|
110
153
|
}
|
|
111
154
|
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
112
155
|
export function attachChild(state, child) {
|
|
@@ -123,12 +166,18 @@ function checkDeclarationOrder(state) {
|
|
|
123
166
|
if (!late) {
|
|
124
167
|
return;
|
|
125
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
|
+
}
|
|
126
172
|
if (late.kind === 'input') {
|
|
127
173
|
throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
|
|
128
174
|
}
|
|
129
175
|
if (late.kind === 'alias') {
|
|
130
176
|
throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
|
|
131
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
|
+
}
|
|
132
181
|
// Child identity and names are settled before this call, so the node and its name are valid.
|
|
133
182
|
throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
|
|
134
183
|
}
|
|
@@ -204,7 +253,7 @@ function buildChild(parent, [name, node], context) {
|
|
|
204
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.`);
|
|
205
254
|
}
|
|
206
255
|
context.owners.set(node, parent);
|
|
207
|
-
return node.build(context);
|
|
256
|
+
return node.build({ ...context, path: [...context.path, name] });
|
|
208
257
|
}
|
|
209
258
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
210
259
|
function checkSlotOrder(slot, next, subject) {
|
|
@@ -282,37 +331,405 @@ function checkGroup(state, children) {
|
|
|
282
331
|
throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
|
|
283
332
|
}
|
|
284
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
|
+
}
|
|
285
707
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
286
|
-
function bindDispatch(state, action) {
|
|
287
|
-
return ({ host, out, passthrough, signal, values }) => {
|
|
708
|
+
function bindDispatch(state, action, globals) {
|
|
709
|
+
return ({ host, out, passthrough, signal, style, values }) => {
|
|
288
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);
|
|
289
718
|
return action({
|
|
290
719
|
args: bound.args,
|
|
291
720
|
host,
|
|
292
|
-
options: { ...
|
|
721
|
+
options: { ...globalOptions, ...bound.options },
|
|
293
722
|
out,
|
|
294
723
|
passthrough,
|
|
295
724
|
signal,
|
|
725
|
+
style,
|
|
296
726
|
});
|
|
297
727
|
};
|
|
298
728
|
}
|
|
299
|
-
/**
|
|
300
|
-
|
|
301
|
-
const {
|
|
302
|
-
const
|
|
303
|
-
const subject = commandSubject(name);
|
|
304
|
-
checkCommandOptions(name, state.options);
|
|
305
|
-
const description = checkDescription(commandSentence(name), state.description);
|
|
306
|
-
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
307
|
-
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
308
|
-
const extensions = buildExtensions({
|
|
309
|
-
declared: state.extensions,
|
|
310
|
-
descriptors: context.descriptors,
|
|
311
|
-
subject: { phrase: `on ${subject}`, sentence: commandSentence(name) },
|
|
312
|
-
target: 'command',
|
|
313
|
-
});
|
|
314
|
-
// Each declaration's own facts, in authoring order, before the rules that pair declarations.
|
|
315
|
-
for (const input of state.inputs) {
|
|
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) {
|
|
316
733
|
const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
|
|
317
734
|
checkDescription(sentence, input.config.description);
|
|
318
735
|
if (input.kind === 'argument') {
|
|
@@ -329,26 +746,68 @@ export function buildCommand(state, context) {
|
|
|
329
746
|
target: input.kind,
|
|
330
747
|
}));
|
|
331
748
|
}
|
|
332
|
-
|
|
333
|
-
|
|
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.`);
|
|
334
754
|
}
|
|
755
|
+
}
|
|
756
|
+
/** Validates one declaration against the shared globals table and compiles it for dispatch. */
|
|
757
|
+
export function buildCommand(state, context) {
|
|
758
|
+
const { actions, name } = state;
|
|
759
|
+
const { globals } = context;
|
|
760
|
+
const subject = commandSubject(name);
|
|
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);
|
|
335
773
|
const attached = collectChildren(state);
|
|
336
774
|
checkDeclarationOrder(state);
|
|
337
775
|
const aliases = collectAliases(state);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const child = attached[0];
|
|
341
|
-
if (first && child) {
|
|
342
|
-
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.`);
|
|
343
|
-
}
|
|
776
|
+
const declaredSlots = collectArguments(state, subject);
|
|
777
|
+
checkArgumentPlacement(name, declaredSlots[0], attached[0]);
|
|
344
778
|
if (actions.length > 1) {
|
|
345
779
|
throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
|
|
346
780
|
}
|
|
347
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;
|
|
348
807
|
if (!action) {
|
|
349
|
-
checkGroup(
|
|
808
|
+
checkGroup(hooked, attached);
|
|
350
809
|
}
|
|
351
|
-
const options = compileLocalOptions(
|
|
810
|
+
const options = compileLocalOptions(hooked, globals, subject);
|
|
352
811
|
const children = new Map();
|
|
353
812
|
const routes = new Map();
|
|
354
813
|
const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
|
|
@@ -368,22 +827,25 @@ export function buildCommand(state, context) {
|
|
|
368
827
|
children,
|
|
369
828
|
deprecated,
|
|
370
829
|
description,
|
|
371
|
-
dispatch: action ? bindDispatch(
|
|
830
|
+
dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
|
|
372
831
|
extensions,
|
|
373
832
|
hidden,
|
|
374
|
-
inputs:
|
|
833
|
+
inputs: hooked.inputs,
|
|
375
834
|
name,
|
|
376
835
|
options,
|
|
836
|
+
result,
|
|
377
837
|
routes,
|
|
378
838
|
};
|
|
379
839
|
}
|
|
380
840
|
/** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
|
|
381
|
-
export function buildGraph(root, install) {
|
|
841
|
+
export function buildGraph(root, globals, install) {
|
|
382
842
|
const context = {
|
|
383
843
|
descriptors: install.descriptors,
|
|
384
844
|
extensions: install.extensions,
|
|
385
|
-
globals: buildGlobals(
|
|
845
|
+
globals: buildGlobals(globals, install.plugins, install),
|
|
386
846
|
owners: new Map(),
|
|
847
|
+
path: [],
|
|
848
|
+
plugins: install.plugins,
|
|
387
849
|
};
|
|
388
850
|
return {
|
|
389
851
|
extensions: context.extensions,
|
|
@@ -406,7 +868,7 @@ export class CommandBuilder {
|
|
|
406
868
|
kind: 'argument',
|
|
407
869
|
name,
|
|
408
870
|
};
|
|
409
|
-
return
|
|
871
|
+
return this.derive(declareArgument(this.#state, input));
|
|
410
872
|
}
|
|
411
873
|
option(name, config) {
|
|
412
874
|
const input = {
|
|
@@ -414,41 +876,62 @@ export class CommandBuilder {
|
|
|
414
876
|
kind: 'option',
|
|
415
877
|
name,
|
|
416
878
|
};
|
|
417
|
-
return
|
|
879
|
+
return this.derive(declareOption(this.#state, input));
|
|
418
880
|
}
|
|
419
881
|
/**
|
|
420
882
|
* Aliases are other bare tokens that route to this Command. They invalidate no call, and the
|
|
421
883
|
* tuple rest parameter rejects a call that names none.
|
|
422
884
|
*/
|
|
423
885
|
alias(...names) {
|
|
424
|
-
return
|
|
886
|
+
return this.derive(declareAlias(this.#state, names));
|
|
425
887
|
}
|
|
426
888
|
/** A child arrives in any type state, because its own action is the call that finished it. */
|
|
427
889
|
command(child) {
|
|
428
|
-
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));
|
|
429
898
|
}
|
|
430
|
-
/** 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. */
|
|
431
911
|
action(handler) {
|
|
432
|
-
return
|
|
912
|
+
return this.derive(declareAction(this.#state, handler));
|
|
913
|
+
}
|
|
914
|
+
extend(...values) {
|
|
915
|
+
return this.derive(declareExtensions(this.#state, values));
|
|
433
916
|
}
|
|
434
917
|
build(context) {
|
|
435
918
|
return buildCommand(this.#state, context);
|
|
436
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
|
+
}
|
|
437
927
|
}
|
|
438
|
-
/**
|
|
439
|
-
* The runtime class behind the public constructor. It is generic so that an instance's `Globals`
|
|
440
|
-
* is the type of the value it holds, with `{}` standing in when there is none, which is what each
|
|
441
|
-
* signature of the constructor interface publishes.
|
|
442
|
-
*/
|
|
928
|
+
/** The constructor uses the Application registration; public type defaults stay library-neutral. */
|
|
443
929
|
class CommandDeclaration extends CommandBuilder {
|
|
444
930
|
constructor(name, options) {
|
|
445
|
-
// The options slot is read defensively, never inspected: an invalid value still yields
|
|
446
|
-
// `globals`, the core facts, and `extensions` of some kind, and `buildCommand` reports it.
|
|
447
931
|
super(freshState({
|
|
448
932
|
deprecated: options?.deprecated,
|
|
449
933
|
description: options?.description,
|
|
450
934
|
extensions: options?.extensions,
|
|
451
|
-
globals: options?.globals,
|
|
452
935
|
hidden: options?.hidden,
|
|
453
936
|
name,
|
|
454
937
|
options,
|
|
@@ -535,11 +1018,31 @@ export function routeInvocation(graph, argv) {
|
|
|
535
1018
|
};
|
|
536
1019
|
}
|
|
537
1020
|
/**
|
|
538
|
-
* The
|
|
539
|
-
*
|
|
540
|
-
*
|
|
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.
|
|
541
1026
|
*/
|
|
542
|
-
|
|
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) {
|
|
543
1046
|
const { command, path, scan } = routed;
|
|
544
1047
|
const { dispatch } = command;
|
|
545
1048
|
// A group answers no invocation of its own, so it fails with the routing errors above it.
|
|
@@ -554,13 +1057,52 @@ export async function prepareDispatch(graph, routed, invocation) {
|
|
|
554
1057
|
host: invocation.host,
|
|
555
1058
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
556
1059
|
passthrough: parsed.passthrough,
|
|
557
|
-
supplied: { args, options: mergeValues(scan, parsed.options) },
|
|
558
|
-
});
|
|
559
|
-
return () => dispatch({
|
|
560
|
-
host: invocation.host,
|
|
561
|
-
out: invocation.out,
|
|
562
|
-
passthrough: parsed.passthrough,
|
|
563
1060
|
signal: invocation.signal,
|
|
564
|
-
|
|
1061
|
+
supplied: { args, options: mergeValues(scan, parsed.options) },
|
|
565
1062
|
});
|
|
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
|
+
}
|
|
566
1108
|
}
|