@loomcli/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/application.d.ts +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +121 -55
- package/dist/plugin.js +496 -126
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +58 -0
- package/dist/sources.js +258 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
package/dist/command.js
CHANGED
|
@@ -1,117 +1,443 @@
|
|
|
1
|
-
var _a;
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
+
import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
|
|
4
|
+
import { quoteString, spelled } from './diagnostic-text.js';
|
|
5
|
+
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
|
|
6
|
+
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
7
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
|
|
8
|
+
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
9
|
+
import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
10
|
+
import { graphMismatch, nodeAt, resultNode, snapshot } from './inspect.js';
|
|
11
|
+
import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
|
|
12
|
+
import { isPlainObject } from './plain.js';
|
|
13
|
+
import { brokenAttachHook, notAnObject } from './plugin-rules.js';
|
|
14
|
+
import { fillInputs } from './sources.js';
|
|
15
|
+
import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
|
|
16
|
+
/**
|
|
17
|
+
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
18
|
+
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
19
|
+
*/
|
|
20
|
+
const nestingCap = { depth: 2, words: 'two' };
|
|
21
|
+
/**
|
|
22
|
+
* The shallowest level a parent can sit at: the root is level 0, and a named Command is attached
|
|
23
|
+
* somewhere below it, so it sits at level 1 or deeper.
|
|
24
|
+
*/
|
|
25
|
+
function parentLevel(name) {
|
|
26
|
+
return name === null ? 0 : 1;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Each authored value registers its handle here, so the public value holds no state to reach or
|
|
30
|
+
* replace.
|
|
31
|
+
*/
|
|
10
32
|
const nodes = new WeakMap();
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The handle one Command value registers, closed over its state. The value itself carries no
|
|
35
|
+
* member that reaches the state, so an attached Command stays final.
|
|
36
|
+
*/
|
|
37
|
+
function nodeHandle(name, state) {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
build: (context) => buildCommand(state, context),
|
|
40
|
+
declared: state,
|
|
41
|
+
hasAction: state.action !== undefined,
|
|
42
|
+
name,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** The handle behind a value an author built as a Command, or nothing for any other value. */
|
|
46
|
+
export function commandNode(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The path a finding opens with for a declaration call on one Command. The root's is empty. A named
|
|
51
|
+
* Command's call cannot know the parent it will join, so its path is its own name.
|
|
52
|
+
*/
|
|
53
|
+
function pathOf(name) {
|
|
54
|
+
return name === null ? [] : [name];
|
|
55
|
+
}
|
|
56
|
+
/** A call's arguments as its author wrote them: the trailing ones left undefined are dropped. */
|
|
57
|
+
export function callArguments(...values) {
|
|
58
|
+
let length = values.length;
|
|
59
|
+
while (length > 0 && values[length - 1] === undefined) {
|
|
60
|
+
length -= 1;
|
|
16
61
|
}
|
|
17
|
-
return
|
|
62
|
+
return values.slice(0, length);
|
|
63
|
+
}
|
|
64
|
+
/** A Command value as a finding prints it, which it would otherwise print as an ellipsis. */
|
|
65
|
+
export function commandCode(name) {
|
|
66
|
+
return spelled(`new Command(${quoteString(name)})`);
|
|
18
67
|
}
|
|
19
|
-
/**
|
|
68
|
+
/** The finding for one `command()` call that attached a child under the Command at `path`. */
|
|
69
|
+
export function commandPlacement(path, child) {
|
|
70
|
+
return { arguments: [commandCode(child)], call: 'command', mark: '0', path };
|
|
71
|
+
}
|
|
72
|
+
/** The finding for one `new Command()` call, which carries the name and the options slot. */
|
|
73
|
+
function constructorFinding(name, options, mark) {
|
|
74
|
+
return { arguments: callArguments(name, options), call: 'new Command', mark };
|
|
75
|
+
}
|
|
76
|
+
/** A named Command's options slot holds a plain options object, and never retired globals wiring. */
|
|
20
77
|
function checkCommandOptions(name, options) {
|
|
21
78
|
if (options !== undefined && !isPlainObject(options)) {
|
|
22
|
-
throw new DeclarationError(
|
|
79
|
+
throw new DeclarationError(notAnObject, {
|
|
80
|
+
correction: 'Supply a Command options object.',
|
|
81
|
+
findings: [constructorFinding(name, options, '1')],
|
|
82
|
+
sentence: `${commandSentence(name)} declares options that are not an object.`,
|
|
83
|
+
});
|
|
23
84
|
}
|
|
24
85
|
if (isPlainObject(options) && 'globals' in options) {
|
|
25
|
-
throw new DeclarationError(
|
|
86
|
+
throw new DeclarationError(commandGlobals, {
|
|
87
|
+
correction: 'Declare globals on the Application and register its environment.',
|
|
88
|
+
findings: [constructorFinding(name, options, '1.globals')],
|
|
89
|
+
sentence: `${commandSentence(name)} declares globals.`,
|
|
90
|
+
});
|
|
26
91
|
}
|
|
27
92
|
}
|
|
28
|
-
/** One name rule for
|
|
93
|
+
/** One name rule for an argument, option, or view name: a bare token the parser can read. */
|
|
29
94
|
function isDeclaredName(name) {
|
|
30
95
|
return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
31
96
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The portable name rule, for every name an operator types as a command at a shell prompt: the
|
|
99
|
+
* application name, every Command name, and every alias. The characters are the POSIX portable
|
|
100
|
+
* filename set, and a name starts with neither `-`, which reads as an option, nor `.`, which a
|
|
101
|
+
* shell hides.
|
|
102
|
+
*/
|
|
103
|
+
export function isPortableName(name) {
|
|
104
|
+
return typeof name === 'string' && /^[A-Za-z0-9_][A-Za-z0-9._-]*$/u.test(name);
|
|
36
105
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
106
|
+
/** The one correction every portable name diagnostic ends with. */
|
|
107
|
+
export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
|
|
108
|
+
/** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
|
|
109
|
+
function checkAliasNames(command, names) {
|
|
110
|
+
for (const [index, alias] of names.entries()) {
|
|
111
|
+
if (!isPortableName(alias)) {
|
|
112
|
+
throw new DeclarationError(portableName, {
|
|
113
|
+
correction: portableNameCorrection,
|
|
114
|
+
findings: [{ arguments: names, call: 'alias', mark: String(index), path: pathOf(command) }],
|
|
115
|
+
sentence: `${commandSentence(command)} declares an alias named ${quoted(alias)}.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
41
118
|
}
|
|
42
119
|
}
|
|
43
120
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
|
|
121
|
+
* The finding for the `alias()` call that declared one alias on the Command at `path`, rebuilt from
|
|
122
|
+
* every alias the Command holds.
|
|
123
|
+
*/
|
|
124
|
+
function aliasFinding(command, alias, note) {
|
|
125
|
+
const { aliases, path } = command;
|
|
126
|
+
return { arguments: aliases, call: 'alias', mark: String(aliases.indexOf(alias)), note, path };
|
|
127
|
+
}
|
|
128
|
+
/** How the extension diagnostics of one Command's own layers name it. */
|
|
129
|
+
export function layerOf(name) {
|
|
130
|
+
return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The state every declaration starts from. The unnamed root and each named Command share it. The
|
|
134
|
+
* declaration values arrive checked, because the constructor that read them threw for any fault.
|
|
47
135
|
*/
|
|
48
136
|
export function freshState(declaration) {
|
|
49
137
|
return {
|
|
50
|
-
|
|
138
|
+
action: undefined,
|
|
51
139
|
aliases: [],
|
|
52
140
|
bind: () => ({ args: {}, options: {} }),
|
|
53
141
|
children: [],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
hidden: declaration.hidden,
|
|
142
|
+
descriptors: declaration.descriptors,
|
|
143
|
+
extensions: declaration.extensions,
|
|
144
|
+
facts: declaration.facts,
|
|
58
145
|
inputs: [],
|
|
59
|
-
late: [],
|
|
60
146
|
name: declaration.name,
|
|
61
|
-
|
|
147
|
+
records: new Map(),
|
|
62
148
|
results: [],
|
|
63
149
|
};
|
|
64
150
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
151
|
+
/**
|
|
152
|
+
* A named Command's own declaration, checked before the value exists: the name, the options slot,
|
|
153
|
+
* the core facts, and the extension values the slot carries. A later change to the options object
|
|
154
|
+
* the author passed changes nothing the declaration holds.
|
|
155
|
+
*/
|
|
156
|
+
function namedState(name, options) {
|
|
157
|
+
if (!isPortableName(name)) {
|
|
158
|
+
throw new DeclarationError(portableName, {
|
|
159
|
+
correction: portableNameCorrection,
|
|
160
|
+
findings: [constructorFinding(name, options, '0')],
|
|
161
|
+
sentence: `Command name ${quoted(name)} is invalid.`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
checkCommandOptions(name, options);
|
|
165
|
+
const slot = isPlainObject(options) ? options : undefined;
|
|
166
|
+
const site = {
|
|
167
|
+
at: '1',
|
|
168
|
+
declaration: { arguments: [name, options], call: 'new Command' },
|
|
169
|
+
subject: commandSentence(name),
|
|
170
|
+
};
|
|
171
|
+
// The facts are read in the order their diagnostics have always ranked.
|
|
172
|
+
const description = checkDescription(site, slot?.description);
|
|
173
|
+
const hidden = checkHidden(site, slot?.hidden);
|
|
174
|
+
const deprecated = checkDeprecated(site, slot?.deprecated);
|
|
175
|
+
const descriptors = new Map();
|
|
176
|
+
const extensions = storeCommandLayers({
|
|
177
|
+
descriptors,
|
|
178
|
+
layers: [slot?.extensions],
|
|
179
|
+
site: { ...site, at: '1.extensions' },
|
|
180
|
+
subject: layerOf(name),
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
name,
|
|
184
|
+
state: freshState({
|
|
185
|
+
descriptors,
|
|
186
|
+
extensions,
|
|
187
|
+
facts: { deprecated, description, hidden },
|
|
188
|
+
name,
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A declaration call after `action()` is an order fault the receiver's own earlier call makes
|
|
194
|
+
* certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
|
|
195
|
+
*/
|
|
196
|
+
function checkOpen(state, late) {
|
|
197
|
+
if (state.action !== undefined) {
|
|
198
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
199
|
+
correction: late.remedy,
|
|
200
|
+
findings: [
|
|
201
|
+
{
|
|
202
|
+
arguments: late.arguments,
|
|
203
|
+
call: late.call,
|
|
204
|
+
mark: '0',
|
|
205
|
+
note: 'after action()',
|
|
206
|
+
path: pathOf(state.name),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
sentence: `${commandSentence(state.name)} ${late.clause} after its action.`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** The remedy a late argument or option earns. */
|
|
214
|
+
const inputRemedy = 'Declare arguments and options before action().';
|
|
215
|
+
/** Where one input was declared: the call on the Command at `path` that declared it. */
|
|
216
|
+
function inputSite(name, path, input) {
|
|
217
|
+
return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
|
|
218
|
+
}
|
|
219
|
+
/** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
|
|
220
|
+
function inputFinding(path, input, note) {
|
|
221
|
+
const site = declaringSite(input, inputPlace(input, { global: false, path }));
|
|
222
|
+
return siteFinding(site, site.named, note);
|
|
223
|
+
}
|
|
224
|
+
/** The scope one Command's own options compile under: its subject, and each option's call. */
|
|
225
|
+
function localScope(name, path) {
|
|
226
|
+
return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
|
|
227
|
+
}
|
|
228
|
+
/** One Command declares arguments or attaches children, whichever call came second. */
|
|
229
|
+
function placementFault(parent, argument, child) {
|
|
230
|
+
return new DeclarationError(argumentsBesideChildren, {
|
|
231
|
+
correction: 'Move the argument into a child Command or remove the children.',
|
|
232
|
+
findings: [
|
|
233
|
+
inputFinding(parent.path, argument, 'the argument'),
|
|
234
|
+
{ ...child.placement, note: 'the child' },
|
|
235
|
+
],
|
|
236
|
+
sentence: `${commandSentence(parent.name)} declares argument "${argument.name}" and attaches child "${child.name}".`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** The options one declaration list holds, in declaration order. */
|
|
240
|
+
function optionsOf(inputs) {
|
|
241
|
+
return inputs.filter((input) => input.kind === 'option');
|
|
242
|
+
}
|
|
243
|
+
/** The positional slot one argument fills. */
|
|
244
|
+
function slotOf(input) {
|
|
245
|
+
return {
|
|
246
|
+
input,
|
|
247
|
+
required: input.config.required === true,
|
|
248
|
+
variadic: input.config.variadic === true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* One input's extension values, validated at the call that declares it, and the descriptors they
|
|
253
|
+
* name joined to the declaration's own.
|
|
254
|
+
*/
|
|
255
|
+
function recordInput(state, input) {
|
|
256
|
+
const { name } = state;
|
|
257
|
+
const descriptors = new Map(state.descriptors);
|
|
258
|
+
const record = buildExtensions({
|
|
259
|
+
declared: input.config.extensions,
|
|
260
|
+
descriptors,
|
|
261
|
+
site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
|
|
262
|
+
subject: {
|
|
263
|
+
phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
|
|
264
|
+
sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
|
|
265
|
+
},
|
|
266
|
+
target: input.kind,
|
|
267
|
+
});
|
|
268
|
+
return { descriptors, records: new Map([...state.records, [input, record]]) };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Every rule one argument answers at its own call: its name, a repeated name, children beside it,
|
|
272
|
+
* its place after the arguments before it, its own facts, and its declaration rules.
|
|
273
|
+
*/
|
|
274
|
+
function checkArgument(state, input) {
|
|
275
|
+
const { name } = state;
|
|
276
|
+
const path = pathOf(name);
|
|
277
|
+
const command = { name, path, subject: commandSubject(name) };
|
|
278
|
+
const declared = state.inputs.filter((entry) => entry.kind === 'argument');
|
|
279
|
+
checkArgumentName(command, declared, input);
|
|
280
|
+
const child = state.children[0];
|
|
281
|
+
if (child) {
|
|
282
|
+
throw placementFault(command, input, child);
|
|
283
|
+
}
|
|
284
|
+
const previous = declared.at(-1);
|
|
285
|
+
if (previous) {
|
|
286
|
+
checkSlotOrder(slotOf(previous), slotOf(input), command);
|
|
287
|
+
}
|
|
288
|
+
const site = inputSite(name, path, input);
|
|
289
|
+
checkDescription(site, input.config.description);
|
|
290
|
+
checkNoListingFacts(site, input.config);
|
|
291
|
+
checkNoArgumentBinding(site, input.config);
|
|
292
|
+
checkDeclarations([{ input, site }]);
|
|
293
|
+
}
|
|
294
|
+
/** One argument's name answers the declared-name rule and names no argument declared before it. */
|
|
295
|
+
function checkArgumentName(command, declared, input) {
|
|
296
|
+
const { path } = command;
|
|
297
|
+
if (!isDeclaredName(input.name)) {
|
|
298
|
+
throw new DeclarationError(declaredName, {
|
|
299
|
+
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
300
|
+
findings: [inputFinding(path, input)],
|
|
301
|
+
sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const first = declared.find((entry) => entry.name === input.name);
|
|
305
|
+
if (first) {
|
|
306
|
+
throw new DeclarationError(argumentDeclaredTwice, {
|
|
307
|
+
correction: 'Remove or rename the duplicate.',
|
|
308
|
+
findings: [
|
|
309
|
+
inputFinding(path, first, 'the first declaration'),
|
|
310
|
+
inputFinding(path, input, 'the second declaration'),
|
|
311
|
+
],
|
|
312
|
+
sentence: `Argument "${input.name}" is declared more than once on ${command.subject}.`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
68
315
|
}
|
|
69
316
|
/** The declared value joins `args` under its literal name, typed by its own config. */
|
|
70
|
-
export function declareArgument(state,
|
|
317
|
+
export function declareArgument(state, declared) {
|
|
318
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
319
|
+
// A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
|
|
320
|
+
// The config is judged right after its name, and only then captured.
|
|
321
|
+
const { name } = state;
|
|
322
|
+
checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
|
|
323
|
+
checkInputConfig(declared, { call: 'argument', path: pathOf(name) });
|
|
324
|
+
const input = { ...declared, config: captureConfig(declared.config) };
|
|
325
|
+
checkOpen(state, {
|
|
326
|
+
arguments: [input.name, input.config],
|
|
327
|
+
call: 'argument',
|
|
328
|
+
clause: `declares argument ${quoted(input.name)}`,
|
|
329
|
+
remedy: inputRemedy,
|
|
330
|
+
});
|
|
331
|
+
checkArgument(state, input);
|
|
332
|
+
const recorded = recordInput(state, input);
|
|
71
333
|
const previous = state.bind;
|
|
72
334
|
return {
|
|
73
335
|
...state,
|
|
336
|
+
...recorded,
|
|
74
337
|
bind: (values) => {
|
|
75
338
|
const bound = previous(values);
|
|
76
339
|
return { ...bound, args: { ...bound.args, ...values.argument(input) } };
|
|
77
340
|
},
|
|
78
341
|
inputs: [...state.inputs, input],
|
|
79
|
-
late: recordLate(state, [{ input, kind: 'input' }]),
|
|
80
342
|
};
|
|
81
343
|
}
|
|
82
|
-
/** The
|
|
83
|
-
|
|
344
|
+
/** The table a named Command's options meet at its own calls, before any Application holds it. */
|
|
345
|
+
const noGlobals = { names: new Map(), options: new Map(), variables: new Map() };
|
|
346
|
+
/**
|
|
347
|
+
* The declared value joins `options` under its literal name, typed by its own config. The root's
|
|
348
|
+
* options also meet the Application's globals table, which a named Command meets when its subtree
|
|
349
|
+
* joins an Application.
|
|
350
|
+
*/
|
|
351
|
+
export function declareOption(state, declared, table = noGlobals) {
|
|
352
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
353
|
+
// The config is judged right after its name, and only then captured.
|
|
354
|
+
checkOptionName(declared.name, inputSite(state.name, pathOf(state.name), declared));
|
|
355
|
+
checkInputConfig(declared, { call: 'option', path: pathOf(state.name) });
|
|
356
|
+
const input = { ...declared, config: captureConfig(declared.config) };
|
|
357
|
+
const site = inputSite(state.name, pathOf(state.name), input);
|
|
358
|
+
checkOpen(state, {
|
|
359
|
+
arguments: [input.name, input.config],
|
|
360
|
+
call: 'option',
|
|
361
|
+
clause: `declares option ${quoted(input.name)}`,
|
|
362
|
+
remedy: inputRemedy,
|
|
363
|
+
});
|
|
364
|
+
checkDescription(site, input.config.description);
|
|
365
|
+
checkHidden(site, input.config.hidden);
|
|
366
|
+
checkDeprecated(site, input.config.deprecated);
|
|
367
|
+
checkEnvBinding(site, input.config);
|
|
368
|
+
const recorded = recordInput(state, input);
|
|
369
|
+
checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
|
|
370
|
+
checkDeclarations([{ input, site }]);
|
|
84
371
|
const previous = state.bind;
|
|
85
372
|
return {
|
|
86
373
|
...state,
|
|
374
|
+
...recorded,
|
|
87
375
|
bind: (values) => {
|
|
88
376
|
const bound = previous(values);
|
|
89
377
|
return { ...bound, options: { ...bound.options, ...values.option(input) } };
|
|
90
378
|
},
|
|
91
379
|
inputs: [...state.inputs, input],
|
|
92
|
-
late: recordLate(state, [{ input, kind: 'input' }]),
|
|
93
|
-
};
|
|
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
380
|
};
|
|
103
381
|
}
|
|
104
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* One call's names join the Command's aliases. A call that names none, which the types reject and
|
|
384
|
+
* a JavaScript author can still write, reports as the call it is.
|
|
385
|
+
*/
|
|
105
386
|
export function declareAlias(state, names) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
387
|
+
const { name } = state;
|
|
388
|
+
const path = pathOf(name);
|
|
389
|
+
const first = names[0];
|
|
390
|
+
if (first === undefined) {
|
|
391
|
+
throw new DeclarationError(aliasWithoutNames, {
|
|
392
|
+
correction: 'Supply at least one name.',
|
|
393
|
+
findings: [{ arguments: [], call: 'alias', path }],
|
|
394
|
+
sentence: `${commandSentence(name)} declares an alias with no names.`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// The call's own input is judged before the receiver's state.
|
|
398
|
+
// A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
|
|
399
|
+
checkAliasNames(name, names);
|
|
400
|
+
checkOpen(state, {
|
|
401
|
+
arguments: names,
|
|
402
|
+
call: 'alias',
|
|
403
|
+
clause: `declares alias "${first}"`,
|
|
404
|
+
remedy: 'Declare aliases before action().',
|
|
405
|
+
});
|
|
406
|
+
const aliases = [...state.aliases];
|
|
407
|
+
for (const [index, alias] of names.entries()) {
|
|
408
|
+
const repeated = { arguments: names, call: 'alias', mark: String(index), path };
|
|
409
|
+
if (alias === name) {
|
|
410
|
+
throw new DeclarationError(repeatedAlias, {
|
|
411
|
+
correction: 'Remove the alias.',
|
|
412
|
+
findings: [{ ...repeated, note: 'its own name' }],
|
|
413
|
+
sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (aliases.includes(alias)) {
|
|
417
|
+
throw new DeclarationError(repeatedAlias, {
|
|
418
|
+
correction: 'Remove the repeated alias.',
|
|
419
|
+
findings: [{ ...repeated, note: 'already an alias' }],
|
|
420
|
+
sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
aliases.push(alias);
|
|
424
|
+
}
|
|
425
|
+
return { ...state, aliases };
|
|
111
426
|
}
|
|
112
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* Extension layers remain open after inputs and the action have been fixed. Each layer is validated
|
|
429
|
+
* at its own call, against every descriptor the declaration already names.
|
|
430
|
+
*/
|
|
113
431
|
export function declareExtensions(state, values) {
|
|
114
|
-
|
|
432
|
+
const descriptors = new Map(state.descriptors);
|
|
433
|
+
const layer = validateLayer({
|
|
434
|
+
declared: values,
|
|
435
|
+
descriptors,
|
|
436
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
|
|
437
|
+
subject: layerOf(state.name),
|
|
438
|
+
target: 'command',
|
|
439
|
+
});
|
|
440
|
+
return { ...state, descriptors, extensions: extendStore(state.extensions, layer) };
|
|
115
441
|
}
|
|
116
442
|
/**
|
|
117
443
|
* The same channel, read as the result the handler's own declaration names. The value one call
|
|
@@ -123,217 +449,360 @@ function declaredChannel(out) {
|
|
|
123
449
|
}
|
|
124
450
|
/**
|
|
125
451
|
* The handler is typed against the result its own declaration carries, and the state holds one
|
|
126
|
-
*
|
|
452
|
+
* action for every declaration, so the context the handler receives is read back at the call.
|
|
127
453
|
*/
|
|
128
454
|
export function declareAction(state, handler) {
|
|
129
|
-
|
|
130
|
-
|
|
455
|
+
if (state.action !== undefined) {
|
|
456
|
+
throw new DeclarationError(multipleActions, {
|
|
457
|
+
correction: 'Register one action.',
|
|
458
|
+
findings: [
|
|
459
|
+
{
|
|
460
|
+
arguments: [handler],
|
|
461
|
+
call: 'action',
|
|
462
|
+
mark: '0',
|
|
463
|
+
note: 'the second action',
|
|
464
|
+
path: pathOf(state.name),
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
sentence: `${commandSentence(state.name)} has multiple actions.`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// The graph and the routed node stay getters, because a spread would build the graph on every run.
|
|
471
|
+
const stored = (context) => handler({
|
|
472
|
+
args: context.args,
|
|
473
|
+
get command() {
|
|
474
|
+
return context.command;
|
|
475
|
+
},
|
|
476
|
+
get graph() {
|
|
477
|
+
return context.graph;
|
|
478
|
+
},
|
|
479
|
+
host: context.host,
|
|
480
|
+
options: context.options,
|
|
481
|
+
out: declaredChannel(context.out),
|
|
482
|
+
passthrough: context.passthrough,
|
|
483
|
+
signal: context.signal,
|
|
484
|
+
style: context.style,
|
|
485
|
+
});
|
|
486
|
+
return { ...state, action: stored };
|
|
131
487
|
}
|
|
132
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* The result declaration, which closes both result calls. Every rule its own call can judge throws
|
|
490
|
+
* here; the empty record and the default wait for attach, because `views()` can still add keys.
|
|
491
|
+
*/
|
|
133
492
|
export function declareResult(state, kind, declaration) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
493
|
+
const call = {
|
|
494
|
+
arguments: callArguments(declaration),
|
|
495
|
+
kind,
|
|
496
|
+
views: recordOf(declaration, 'views'),
|
|
138
497
|
};
|
|
498
|
+
checkOpen(state, {
|
|
499
|
+
arguments: call.arguments,
|
|
500
|
+
call: resultCallName(call),
|
|
501
|
+
clause: 'declares its result',
|
|
502
|
+
remedy: 'Declare result() or rows() before action().',
|
|
503
|
+
});
|
|
504
|
+
const results = [...state.results, call];
|
|
505
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
506
|
+
return { ...state, results };
|
|
139
507
|
}
|
|
140
508
|
/** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
|
|
141
509
|
export function declareResultViews(state, replacements, options) {
|
|
510
|
+
const results = [...state.results, viewsCall(replacements, options)];
|
|
511
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
512
|
+
return { ...state, results };
|
|
513
|
+
}
|
|
514
|
+
/** One `views()` call as the results lane records it. */
|
|
515
|
+
function viewsCall(replacements, options) {
|
|
142
516
|
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
],
|
|
517
|
+
arguments: callArguments(replacements, options),
|
|
518
|
+
default: recordOf(options, 'default'),
|
|
519
|
+
kind: 'views',
|
|
520
|
+
views: replacements,
|
|
148
521
|
};
|
|
149
522
|
}
|
|
150
|
-
/** One property of an authoring argument, read defensively:
|
|
523
|
+
/** One property of an authoring argument, read defensively: the rules report whatever it holds. */
|
|
151
524
|
function recordOf(declaration, key) {
|
|
152
525
|
return isPlainObject(declaration) ? declaration[key] : undefined;
|
|
153
526
|
}
|
|
154
|
-
/**
|
|
155
|
-
|
|
527
|
+
/** The authoring call one results-lane call was made through. */
|
|
528
|
+
function resultCallName(call) {
|
|
529
|
+
return call.kind === 'value' ? 'result' : call.kind;
|
|
530
|
+
}
|
|
531
|
+
/** The finding for one results-lane call on the Command at `path`, marking one of its arguments. */
|
|
532
|
+
function resultFinding(path, call, mark) {
|
|
533
|
+
const finding = { arguments: call.arguments, call: resultCallName(call), mark: mark.at, path };
|
|
534
|
+
return mark.note === undefined ? finding : { ...finding, note: mark.note };
|
|
535
|
+
}
|
|
536
|
+
/** Where one views entry sits among its call's arguments: in `views` for a declaration, or first. */
|
|
537
|
+
function entryMark(call, key) {
|
|
538
|
+
return call.kind === 'views' ? `0.${key}` : `0.views.${key}`;
|
|
539
|
+
}
|
|
540
|
+
/** The parent a declaration is, as attach reads it. */
|
|
541
|
+
function parentOf(state) {
|
|
156
542
|
return {
|
|
157
|
-
|
|
158
|
-
children:
|
|
159
|
-
|
|
543
|
+
argument: state.inputs.find((input) => input.kind === 'argument'),
|
|
544
|
+
children: state.children,
|
|
545
|
+
hasAction: state.action !== undefined,
|
|
546
|
+
name: state.name,
|
|
547
|
+
path: pathOf(state.name),
|
|
160
548
|
};
|
|
161
549
|
}
|
|
162
|
-
/** The
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
550
|
+
/** The path one child sits at under its parent. */
|
|
551
|
+
function childPath(parent, child) {
|
|
552
|
+
return [...parent.path, child];
|
|
553
|
+
}
|
|
554
|
+
/** The call that attached one child, noted with the child's name. */
|
|
555
|
+
function childFinding(entry) {
|
|
556
|
+
return { ...entry.placement, note: `child "${entry.name}"` };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* One parent's namespace, which every canonical name and alias under it shares. The rule reads the
|
|
560
|
+
* same whichever sibling the author attached first.
|
|
561
|
+
*/
|
|
562
|
+
function checkSiblings(parent, child) {
|
|
563
|
+
const sentence = commandSentence(parent.name);
|
|
564
|
+
const { children } = parent;
|
|
565
|
+
const aliased = (entry, alias) => aliasFinding({ aliases: entry.node.declared.aliases, path: childPath(parent, entry.name) }, alias, `alias of child "${entry.name}"`);
|
|
566
|
+
const correction = 'Rename or remove one.';
|
|
567
|
+
const twin = children.find((entry) => entry.name === child.name);
|
|
568
|
+
if (twin) {
|
|
569
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
570
|
+
correction,
|
|
571
|
+
findings: [childFinding(twin), childFinding(child)],
|
|
572
|
+
sentence: `${sentence} attaches two children named "${child.name}".`,
|
|
573
|
+
});
|
|
168
574
|
}
|
|
169
|
-
|
|
170
|
-
|
|
575
|
+
for (const alias of child.node.declared.aliases) {
|
|
576
|
+
const holder = children.find((entry) => entry.name === alias);
|
|
577
|
+
if (holder) {
|
|
578
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
579
|
+
correction,
|
|
580
|
+
findings: [childFinding(holder), aliased(child, alias)],
|
|
581
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}".`,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
|
|
585
|
+
if (owner) {
|
|
586
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
587
|
+
correction,
|
|
588
|
+
findings: [aliased(owner, alias), aliased(child, alias)],
|
|
589
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}".`,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
171
592
|
}
|
|
172
|
-
|
|
173
|
-
|
|
593
|
+
const owner = children.find((entry) => entry.node.declared.aliases.includes(child.name));
|
|
594
|
+
if (owner) {
|
|
595
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
596
|
+
correction,
|
|
597
|
+
findings: [aliased(owner, child.name), childFinding(child)],
|
|
598
|
+
sentence: `${sentence} attaches child "${owner.name}" with alias "${child.name}", which is also the name of child "${child.name}".`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* A child at the cap holds no children. Attach reads the child at the shallowest level its parent
|
|
604
|
+
* can sit at, and the Application's join reads every node at its level below the root.
|
|
605
|
+
*/
|
|
606
|
+
function checkNesting(parent, child, level) {
|
|
607
|
+
if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
|
|
608
|
+
throw new DeclarationError(nestingDepth, {
|
|
609
|
+
correction: `Nest Commands at most ${nestingCap.words} levels below the root.`,
|
|
610
|
+
findings: [{ ...child.placement, note: 'has children of its own' }],
|
|
611
|
+
sentence: `${commandSentence(parent)} attaches child "${child.name}", which has children of its own.`,
|
|
612
|
+
});
|
|
174
613
|
}
|
|
175
|
-
|
|
176
|
-
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A Command without an action is a group, and routing sends an invocation on to one of its
|
|
617
|
+
* children. A group with no children receives an invocation no handler can answer, and a local
|
|
618
|
+
* option on a group reaches no handler either, because locals never inherit.
|
|
619
|
+
*/
|
|
620
|
+
function checkGroup(state, place) {
|
|
621
|
+
const { name } = state;
|
|
622
|
+
if (state.children.length === 0) {
|
|
623
|
+
const { placement } = place;
|
|
624
|
+
throw new DeclarationError(commandWithoutAction, {
|
|
625
|
+
correction: 'Register an action.',
|
|
626
|
+
findings: placement === undefined ? [] : [{ ...placement, note: 'no action and no children' }],
|
|
627
|
+
sentence: `${commandSentence(name)} has no action.`,
|
|
628
|
+
});
|
|
177
629
|
}
|
|
178
|
-
|
|
179
|
-
|
|
630
|
+
const option = state.inputs.find((input) => input.kind === 'option');
|
|
631
|
+
if (option) {
|
|
632
|
+
throw new DeclarationError(groupOption, {
|
|
633
|
+
correction: 'Register an action or remove the option.',
|
|
634
|
+
findings: [inputFinding(place.path, option, 'no action reads it')],
|
|
635
|
+
sentence: `${commandSentence(name)} declares option "${option.name}" but registers no action to receive it.`,
|
|
636
|
+
});
|
|
180
637
|
}
|
|
181
|
-
// Child identity and names are settled before this call, so the node and its name are valid.
|
|
182
|
-
throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
|
|
183
638
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw new DeclarationError(`${commandSentence(state.name)} attaches two children named "${name}". Rename or remove one.`);
|
|
194
|
-
}
|
|
195
|
-
seen.add(name);
|
|
196
|
-
attached.push([name, node]);
|
|
639
|
+
/**
|
|
640
|
+
* The rules a finished Command answers, which attach applies to a child and build to the root: a
|
|
641
|
+
* result needs an action, its merged views record names at least one view and its default, and a
|
|
642
|
+
* Command without an action is a group. It answers with the resolved result.
|
|
643
|
+
*/
|
|
644
|
+
function checkFinished(declared, hasAction, place) {
|
|
645
|
+
const result = buildResult(declared, hasAction, place.path);
|
|
646
|
+
if (!hasAction) {
|
|
647
|
+
checkGroup(declared, place);
|
|
197
648
|
}
|
|
198
|
-
return
|
|
649
|
+
return result;
|
|
199
650
|
}
|
|
200
651
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
652
|
+
* The one attach operation `Command.command()`, `Application.command()`, and a plugin's
|
|
653
|
+
* `commands` list share. A Command is an immutable value, so the child is final here: it is checked
|
|
654
|
+
* as a finished Command, against the parent's current children, and against the nesting cap from
|
|
655
|
+
* the shallowest level the parent can sit at. The placement is the call that attached it, which a
|
|
656
|
+
* finding about the child as a whole marks.
|
|
203
657
|
*/
|
|
204
|
-
function
|
|
205
|
-
const { name } =
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
checkAliasName(name, alias);
|
|
214
|
-
if (alias === name) {
|
|
215
|
-
throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
|
|
216
|
-
}
|
|
217
|
-
if (seen.has(alias)) {
|
|
218
|
-
throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
|
|
219
|
-
}
|
|
220
|
-
seen.add(alias);
|
|
221
|
-
aliases.push(alias);
|
|
222
|
-
}
|
|
658
|
+
export function attach(parent, node, placement) {
|
|
659
|
+
const { name } = node;
|
|
660
|
+
const child = { name, node, placement };
|
|
661
|
+
if (parent.hasAction) {
|
|
662
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
663
|
+
correction: 'Attach children before action().',
|
|
664
|
+
findings: [{ ...placement, note: 'after action()' }],
|
|
665
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}" after its action.`,
|
|
666
|
+
});
|
|
223
667
|
}
|
|
224
|
-
|
|
668
|
+
if (parent.argument !== undefined) {
|
|
669
|
+
throw placementFault(parent, parent.argument, child);
|
|
670
|
+
}
|
|
671
|
+
checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
|
|
672
|
+
checkSiblings(parent, child);
|
|
673
|
+
checkNesting(parent.name, child, parentLevel(parent.name) + 1);
|
|
674
|
+
return child;
|
|
675
|
+
}
|
|
676
|
+
/** The handle behind the value one `command()` call received; anything else is a declaration error. */
|
|
677
|
+
export function childNode(parent, child) {
|
|
678
|
+
const node = commandNode(child);
|
|
679
|
+
if (!node) {
|
|
680
|
+
throw new DeclarationError(notACommand, {
|
|
681
|
+
correction: 'Attach the value returned by new Command(name).',
|
|
682
|
+
findings: [{ arguments: [child], call: 'command', mark: '0', path: pathOf(parent) }],
|
|
683
|
+
sentence: `${commandSentence(parent)} attaches a value that is not a Command.`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return node;
|
|
687
|
+
}
|
|
688
|
+
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
689
|
+
function attachChild(state, child) {
|
|
690
|
+
const node = childNode(state.name, child);
|
|
691
|
+
const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
|
|
692
|
+
return { ...state, children: [...state.children, attached] };
|
|
225
693
|
}
|
|
226
694
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
695
|
+
* Walks one subtree joining an Application, once, for the rules only the Application can judge: one
|
|
696
|
+
* Command value reached through two paths, two distinct descriptors under one identity, a local
|
|
697
|
+
* option that meets a global or plugin option's key, spelling, or variable, and the nesting cap
|
|
698
|
+
* measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
|
|
699
|
+
* At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
|
|
700
|
+
* here fires only once the cap rises: attach reads one level, and only this walk knows each level.
|
|
230
701
|
*/
|
|
231
|
-
function
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
702
|
+
function joinSubtree(scope, child, { level, parent }) {
|
|
703
|
+
const { name, node, placement } = child;
|
|
704
|
+
checkNesting(parent.name, child, level);
|
|
705
|
+
const owner = scope.owners.get(node);
|
|
706
|
+
if (owner) {
|
|
707
|
+
throw new DeclarationError(commandAttachedTwice, {
|
|
708
|
+
correction: 'Attach a Command value at one point; create a new Command for each placement.',
|
|
709
|
+
findings: [
|
|
710
|
+
{ ...owner.placement, note: 'the first placement' },
|
|
711
|
+
{ ...placement, note: 'the second placement' },
|
|
712
|
+
],
|
|
713
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}", which ${commandSubject(owner.name)} also attaches.`,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
scope.owners.set(node, { name: parent.name, placement });
|
|
717
|
+
for (const [key, descriptor] of node.declared.descriptors) {
|
|
718
|
+
registerDescriptor(scope.descriptors, descriptor, { identity: key });
|
|
719
|
+
}
|
|
720
|
+
const path = childPath(parent, name);
|
|
721
|
+
checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
|
|
722
|
+
for (const entry of node.declared.children) {
|
|
723
|
+
// A nested child's own placement knew its parent alone, so the walk places it under the root.
|
|
724
|
+
const rooted = { ...entry, placement: commandPlacement(path, entry.name) };
|
|
725
|
+
joinSubtree(scope, rooted, { level: level + 1, parent: { name, path } });
|
|
726
|
+
}
|
|
243
727
|
}
|
|
244
728
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* attaching a value twice. Parents may share a name, so the claim is by node identity and the name
|
|
248
|
-
* serves the diagnostic alone.
|
|
729
|
+
* Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
|
|
730
|
+
* registers are copies: the caller commits the owners, and the returned root holds the descriptors.
|
|
249
731
|
*/
|
|
250
|
-
function
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
732
|
+
export function attachToRoot(state, child, scope) {
|
|
733
|
+
const parent = parentOf(state);
|
|
734
|
+
const attached = attach(parent, child.node, child.placement);
|
|
735
|
+
joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent });
|
|
736
|
+
return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
|
|
737
|
+
}
|
|
738
|
+
/** Every attached Command's local options against a globals table that has just grown. */
|
|
739
|
+
function checkAttachedOptions(table, parent, children) {
|
|
740
|
+
for (const { name, node } of children) {
|
|
741
|
+
const path = childPath(parent, name);
|
|
742
|
+
checkLocalOptions(optionsOf(node.declared.inputs), table, localScope(name, path));
|
|
743
|
+
checkAttachedOptions(table, { name, path }, node.declared.children);
|
|
254
744
|
}
|
|
255
|
-
|
|
256
|
-
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* A declaration's own options and every attached Command's, against a globals table that has just
|
|
748
|
+
* grown. The table's new option reads as the other side of any collision.
|
|
749
|
+
*/
|
|
750
|
+
export function checkDeclaredOptions(state, table) {
|
|
751
|
+
const place = { name: state.name, path: pathOf(state.name) };
|
|
752
|
+
checkLocalOptions(optionsOf(state.inputs), table, localScope(place.name, place.path));
|
|
753
|
+
checkAttachedOptions(table, place, state.children);
|
|
257
754
|
}
|
|
258
755
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
259
|
-
function checkSlotOrder(slot, next,
|
|
756
|
+
function checkSlotOrder(slot, next, command) {
|
|
757
|
+
const { path, subject } = command;
|
|
758
|
+
const after = inputFinding(path, next.input, 'the argument after it');
|
|
260
759
|
if (slot.variadic) {
|
|
261
|
-
throw new DeclarationError(
|
|
760
|
+
throw new DeclarationError(variadicArgumentLast, {
|
|
761
|
+
correction: 'Declare the variadic argument last.',
|
|
762
|
+
findings: [inputFinding(path, slot.input, 'the variadic argument'), after],
|
|
763
|
+
sentence: `Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}.`,
|
|
764
|
+
});
|
|
262
765
|
}
|
|
263
766
|
if (!slot.required) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
767
|
+
const findings = [inputFinding(path, slot.input, 'the optional argument'), after];
|
|
768
|
+
throw new DeclarationError(optionalArgumentLast, next.required
|
|
769
|
+
? {
|
|
770
|
+
correction: 'Declare optional arguments after required ones.',
|
|
771
|
+
findings,
|
|
772
|
+
sentence: `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}.`,
|
|
773
|
+
}
|
|
774
|
+
: {
|
|
775
|
+
correction: 'Declare an optional argument last.',
|
|
776
|
+
findings,
|
|
777
|
+
sentence: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}.`,
|
|
778
|
+
});
|
|
267
779
|
}
|
|
268
780
|
}
|
|
269
|
-
|
|
781
|
+
/**
|
|
782
|
+
* The positional slots one declaration holds. Every authored argument answered these rules at its
|
|
783
|
+
* own call, so they throw here only for an argument a lifecycle hook declared.
|
|
784
|
+
*/
|
|
785
|
+
function collectArguments(state, command) {
|
|
270
786
|
const slots = [];
|
|
271
|
-
const
|
|
787
|
+
const declared = [];
|
|
788
|
+
const judged = { ...command, subject: commandSubject(command.name) };
|
|
272
789
|
for (const input of state.inputs.filter((entry) => entry.kind === 'argument')) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (seen.has(input.name)) {
|
|
277
|
-
throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
278
|
-
}
|
|
279
|
-
seen.add(input.name);
|
|
280
|
-
slots.push({
|
|
281
|
-
input,
|
|
282
|
-
required: input.config.required === true,
|
|
283
|
-
variadic: input.config.variadic === true,
|
|
284
|
-
});
|
|
790
|
+
checkArgumentName(judged, declared, input);
|
|
791
|
+
declared.push(input);
|
|
792
|
+
slots.push(slotOf(input));
|
|
285
793
|
}
|
|
286
794
|
for (let index = 0; index + 1 < slots.length; index += 1) {
|
|
287
795
|
const slot = slots[index];
|
|
288
796
|
const next = slots[index + 1];
|
|
289
797
|
if (slot && next) {
|
|
290
|
-
checkSlotOrder(slot, next,
|
|
798
|
+
checkSlotOrder(slot, next, judged);
|
|
291
799
|
}
|
|
292
800
|
}
|
|
293
801
|
return slots;
|
|
294
802
|
}
|
|
295
803
|
/**
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* the other side claimed the name or the spelling.
|
|
299
|
-
*/
|
|
300
|
-
function compileLocalOptions(state, globals, subject) {
|
|
301
|
-
const declarations = state.inputs.filter((input) => input.kind === 'option');
|
|
302
|
-
const local = { kind: 'local', subject };
|
|
303
|
-
const application = { kind: 'application' };
|
|
304
|
-
for (const declaration of declarations) {
|
|
305
|
-
const claimed = globals.names.get(declaration.name);
|
|
306
|
-
if (claimed) {
|
|
307
|
-
throw keyCollision(declaration.name, claimed, local);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
const options = compileOptions(declarations, subject);
|
|
311
|
-
for (const [spelling, option] of options) {
|
|
312
|
-
const global = globals.options.get(spelling);
|
|
313
|
-
if (global) {
|
|
314
|
-
throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return options;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* A Command without an action is a group, and routing sends an invocation on to one of its
|
|
321
|
-
* children. A group with no children receives an invocation no handler can answer, and a local
|
|
322
|
-
* option on a group reaches no handler either, because locals never inherit.
|
|
323
|
-
*/
|
|
324
|
-
function checkGroup(state, children) {
|
|
325
|
-
const { name } = state;
|
|
326
|
-
if (children.length === 0) {
|
|
327
|
-
throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
|
|
328
|
-
}
|
|
329
|
-
const option = state.inputs.find((input) => input.kind === 'option');
|
|
330
|
-
if (option) {
|
|
331
|
-
throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
|
|
332
|
-
}
|
|
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.
|
|
804
|
+
* A view name is a bare token the way an argument or option name is, and never an array index,
|
|
805
|
+
* because an integer-like key does not keep the position the author gave it.
|
|
337
806
|
*/
|
|
338
807
|
function isViewName(name) {
|
|
339
808
|
return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
|
|
@@ -367,71 +836,126 @@ function recordEntries(record) {
|
|
|
367
836
|
* One `views` entry under the unit its declaration named, read back as the shape its own functions
|
|
368
837
|
* name. The two shapes are exclusive, and a row view answers a rows declaration alone.
|
|
369
838
|
*/
|
|
370
|
-
function resultView(
|
|
371
|
-
const {
|
|
839
|
+
function resultView(kind, site, entry) {
|
|
840
|
+
const { call, key, path, sentence } = site;
|
|
841
|
+
const findings = [resultFinding(path, call, { at: entryMark(call, key) })];
|
|
372
842
|
const whole = isWholeView(entry);
|
|
373
843
|
const row = isRowView(entry);
|
|
374
844
|
if (whole && row) {
|
|
375
|
-
throw new DeclarationError(
|
|
845
|
+
throw new DeclarationError(viewShape, {
|
|
846
|
+
correction: 'Supply one of the two.',
|
|
847
|
+
findings,
|
|
848
|
+
sentence: `${sentence} names view ${quoted(key)} with render and row.`,
|
|
849
|
+
});
|
|
376
850
|
}
|
|
377
851
|
if (row) {
|
|
378
852
|
if (kind === 'value') {
|
|
379
|
-
throw new DeclarationError(
|
|
853
|
+
throw new DeclarationError(rowViewOnValue, {
|
|
854
|
+
correction: 'Supply a view with render, or declare the result with rows().',
|
|
855
|
+
findings,
|
|
856
|
+
sentence: `${sentence} names row view ${quoted(key)} on a value result.`,
|
|
857
|
+
});
|
|
380
858
|
}
|
|
381
859
|
return entry;
|
|
382
860
|
}
|
|
383
861
|
if (whole) {
|
|
384
862
|
return entry;
|
|
385
863
|
}
|
|
386
|
-
throw new DeclarationError(
|
|
864
|
+
throw new DeclarationError(viewShape, {
|
|
865
|
+
correction: 'Supply a view with render or a row view with row.',
|
|
866
|
+
findings,
|
|
867
|
+
sentence: `${sentence} names view ${quoted(key)} with a value that is not a view.`,
|
|
868
|
+
});
|
|
387
869
|
}
|
|
388
870
|
/**
|
|
389
|
-
* Every rule
|
|
390
|
-
* The merged record is what the rules read: a later `views()` call replaces a key in
|
|
391
|
-
* appends a new one, so each name keeps the position the call that first named it gave
|
|
392
|
-
* `default` once named persists through later calls that name none
|
|
393
|
-
* until one is named.
|
|
871
|
+
* Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
|
|
872
|
+
* it made them. The merged record is what the rules read: a later `views()` call replaces a key in
|
|
873
|
+
* place and appends a new one, so each name keeps the position the call that first named it gave
|
|
874
|
+
* it. A `default` once named persists through later calls that name none.
|
|
394
875
|
*/
|
|
395
|
-
function
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
876
|
+
function mergeResult(command, results) {
|
|
877
|
+
const { path } = command;
|
|
878
|
+
const sentence = commandSentence(command.name);
|
|
879
|
+
const declarations = results.filter((call) => call.kind !== 'views');
|
|
880
|
+
const [declaration, second] = declarations;
|
|
881
|
+
if (declaration && second) {
|
|
882
|
+
throw new DeclarationError(multipleResults, {
|
|
883
|
+
correction: 'Declare one result() or rows() call.',
|
|
884
|
+
findings: [
|
|
885
|
+
resultFinding(path, declaration, { at: '0', note: 'the first result' }),
|
|
886
|
+
resultFinding(path, second, { at: '0', note: 'the second result' }),
|
|
887
|
+
],
|
|
888
|
+
sentence: `${sentence} declares two results.`,
|
|
889
|
+
});
|
|
400
890
|
}
|
|
401
|
-
const declaration = declarations[0];
|
|
402
891
|
// A `views()` call reshapes a result's views, so one with no result reshapes nothing.
|
|
403
892
|
// The types publish the call where a result is carried, so this reaches a JavaScript author.
|
|
404
893
|
if (!declaration) {
|
|
405
|
-
|
|
406
|
-
|
|
894
|
+
const [reshape] = results;
|
|
895
|
+
if (reshape) {
|
|
896
|
+
throw new DeclarationError(viewsWithoutResult, {
|
|
897
|
+
correction: 'Declare result() or rows() before action().',
|
|
898
|
+
findings: [resultFinding(path, reshape, { at: '0' })],
|
|
899
|
+
sentence: `${sentence} reshapes its views and declares no result.`,
|
|
900
|
+
});
|
|
407
901
|
}
|
|
408
902
|
return undefined;
|
|
409
903
|
}
|
|
410
|
-
if (!hasAction) {
|
|
411
|
-
throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
|
|
412
|
-
}
|
|
413
904
|
const views = new Map();
|
|
414
905
|
let selected = undefined;
|
|
415
|
-
for (const call of
|
|
416
|
-
for (const [
|
|
417
|
-
const view = resultView(
|
|
418
|
-
if (!isViewName(
|
|
419
|
-
throw new DeclarationError(
|
|
906
|
+
for (const call of results) {
|
|
907
|
+
for (const [key, entry] of recordEntries(call.views)) {
|
|
908
|
+
const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
|
|
909
|
+
if (!isViewName(key)) {
|
|
910
|
+
throw new DeclarationError(viewName, {
|
|
911
|
+
correction: 'Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.',
|
|
912
|
+
findings: [resultFinding(path, call, { at: entryMark(call, key) })],
|
|
913
|
+
sentence: `${sentence} names view ${quoted(key)}.`,
|
|
914
|
+
});
|
|
420
915
|
}
|
|
421
|
-
views.set(
|
|
916
|
+
views.set(key, view);
|
|
422
917
|
}
|
|
423
|
-
|
|
424
|
-
|
|
918
|
+
const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
|
|
919
|
+
if (key !== undefined) {
|
|
920
|
+
selected = { call, key };
|
|
425
921
|
}
|
|
426
922
|
}
|
|
923
|
+
return { declaration, kind: declaration.kind, selected, views };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* The finished result: the merged record, which needs an action, at least one view, and a default
|
|
927
|
+
* that names one of them. The first key answers until one is named.
|
|
928
|
+
*/
|
|
929
|
+
function buildResult(declared, hasAction, path) {
|
|
930
|
+
const merged = mergeResult({ name: declared.name, path }, declared.results);
|
|
931
|
+
if (!merged) {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
const sentence = commandSentence(declared.name);
|
|
935
|
+
const { declaration, selected, views } = merged;
|
|
936
|
+
if (!hasAction) {
|
|
937
|
+
throw new DeclarationError(resultWithoutAction, {
|
|
938
|
+
correction: 'Register an action or remove the result.',
|
|
939
|
+
findings: [resultFinding(path, declaration, { at: '0' })],
|
|
940
|
+
sentence: `${sentence} declares a result and no action.`,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
427
943
|
const first = views.keys().next();
|
|
428
944
|
if (first.done === true) {
|
|
429
|
-
throw new DeclarationError(
|
|
945
|
+
throw new DeclarationError(resultWithoutViews, {
|
|
946
|
+
correction: 'Name at least one view.',
|
|
947
|
+
findings: [resultFinding(path, declaration, { at: '0.views' })],
|
|
948
|
+
sentence: `${sentence} declares a result with no views.`,
|
|
949
|
+
});
|
|
430
950
|
}
|
|
431
|
-
if (selected !== undefined && !views.has(selected)) {
|
|
432
|
-
throw new DeclarationError(
|
|
951
|
+
if (selected !== undefined && !views.has(selected.key)) {
|
|
952
|
+
throw new DeclarationError(unknownDefaultView, {
|
|
953
|
+
correction: 'Name the view or select a named one.',
|
|
954
|
+
findings: [resultFinding(path, selected.call, { at: '1.default' })],
|
|
955
|
+
sentence: `${sentence} selects default view ${quoted(selected.key)}, which it does not name.`,
|
|
956
|
+
});
|
|
433
957
|
}
|
|
434
|
-
return { default: selected ?? first.value, kind:
|
|
958
|
+
return { default: selected?.key ?? first.value, kind: merged.kind, views };
|
|
435
959
|
}
|
|
436
960
|
/** The values one build made, so a value a hook returns is one of them and never a forged shape. */
|
|
437
961
|
const attachments = new WeakMap();
|
|
@@ -454,7 +978,7 @@ class AttachedCommandValue {
|
|
|
454
978
|
#result;
|
|
455
979
|
constructor(state) {
|
|
456
980
|
this.#state = state;
|
|
457
|
-
this.#result = resultNode(buildResult(state.declared, state.hasAction));
|
|
981
|
+
this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
|
|
458
982
|
attachments.set(this, state);
|
|
459
983
|
Object.freeze(this);
|
|
460
984
|
}
|
|
@@ -481,17 +1005,13 @@ class AttachedCommandValue {
|
|
|
481
1005
|
return publishStore(this.#state.extensions);
|
|
482
1006
|
}
|
|
483
1007
|
argument(name, config) {
|
|
484
|
-
return this.#declare({ config
|
|
1008
|
+
return this.#declare({ config, kind: 'argument', name });
|
|
485
1009
|
}
|
|
486
1010
|
option(name, config) {
|
|
487
|
-
return this.#declare({ config
|
|
1011
|
+
return this.#declare({ config, kind: 'option', name });
|
|
488
1012
|
}
|
|
489
1013
|
views(replacements, options) {
|
|
490
|
-
const call =
|
|
491
|
-
default: recordOf(options, 'default'),
|
|
492
|
-
kind: 'views',
|
|
493
|
-
views: replacements,
|
|
494
|
-
};
|
|
1014
|
+
const call = viewsCall(replacements, options);
|
|
495
1015
|
const { declared } = this.#state;
|
|
496
1016
|
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
497
1017
|
}
|
|
@@ -505,6 +1025,7 @@ class AttachedCommandValue {
|
|
|
505
1025
|
const layer = validateLayer({
|
|
506
1026
|
declared: values,
|
|
507
1027
|
descriptors: registry,
|
|
1028
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
|
|
508
1029
|
subject: state.subject,
|
|
509
1030
|
target: 'command',
|
|
510
1031
|
});
|
|
@@ -515,8 +1036,22 @@ class AttachedCommandValue {
|
|
|
515
1036
|
});
|
|
516
1037
|
}
|
|
517
1038
|
/** One input the running hook declared, which the Command's own names now hold. */
|
|
518
|
-
#declare(
|
|
1039
|
+
#declare(raw) {
|
|
519
1040
|
const { declared, identity } = this.#state;
|
|
1041
|
+
const { path } = this.#state;
|
|
1042
|
+
// The name is judged before the config, as Command and Application judge it.
|
|
1043
|
+
if (raw.kind === 'argument') {
|
|
1044
|
+
const { name } = declared;
|
|
1045
|
+
checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
checkOptionName(raw.name, inputSite(declared.name, path, raw));
|
|
1049
|
+
}
|
|
1050
|
+
checkInputConfig(raw, { call: raw.kind, path });
|
|
1051
|
+
// Each kind captures its own config, so the input keeps the pairing its kind declares.
|
|
1052
|
+
const input = raw.kind === 'argument'
|
|
1053
|
+
? { ...raw, config: captureConfig(raw.config) }
|
|
1054
|
+
: { ...raw, config: captureConfig(raw.config) };
|
|
520
1055
|
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
521
1056
|
}
|
|
522
1057
|
#derive(call, declared) {
|
|
@@ -525,9 +1060,13 @@ class AttachedCommandValue {
|
|
|
525
1060
|
}
|
|
526
1061
|
}
|
|
527
1062
|
_a = AttachedCommandValue;
|
|
528
|
-
/** The
|
|
529
|
-
function
|
|
530
|
-
return
|
|
1063
|
+
/** The finding for the hook one plugin declared, as `plugin(identity, { onCommandAttach })`. */
|
|
1064
|
+
function hookFinding(identity, hook) {
|
|
1065
|
+
return {
|
|
1066
|
+
arguments: [identity, { onCommandAttach: hook }],
|
|
1067
|
+
call: 'plugin',
|
|
1068
|
+
mark: '1.onCommandAttach',
|
|
1069
|
+
};
|
|
531
1070
|
}
|
|
532
1071
|
/** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
|
|
533
1072
|
function callHook(value, hook, named) {
|
|
@@ -539,7 +1078,11 @@ function callHook(value, hook, named) {
|
|
|
539
1078
|
if (error instanceof DeclarationError) {
|
|
540
1079
|
throw error;
|
|
541
1080
|
}
|
|
542
|
-
throw new DeclarationError(
|
|
1081
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1082
|
+
correction: 'Return the value the hook received or a value derived from it, and throw only a DeclarationError from the hook.',
|
|
1083
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1084
|
+
sentence: `Plugin ${quoted(named.identity)} failed in onCommandAttach for ${named.subject}: ${asSentence(reasonOf(error))}`,
|
|
1085
|
+
}, { cause: error });
|
|
543
1086
|
}
|
|
544
1087
|
}
|
|
545
1088
|
/**
|
|
@@ -550,7 +1093,11 @@ function callHook(value, hook, named) {
|
|
|
550
1093
|
function attachOnce(value, hook, named) {
|
|
551
1094
|
const state = attachments.get(callHook(value, hook, named));
|
|
552
1095
|
if (!state || state.lineage !== named.lineage) {
|
|
553
|
-
throw new DeclarationError(
|
|
1096
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1097
|
+
correction: 'Return the value it received or a value derived from it.',
|
|
1098
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1099
|
+
sentence: `Plugin ${quoted(named.identity)} returned a value that is not the attached Command from onCommandAttach for ${named.subject}.`,
|
|
1100
|
+
});
|
|
554
1101
|
}
|
|
555
1102
|
return state;
|
|
556
1103
|
}
|
|
@@ -581,8 +1128,8 @@ function runAttachHooks(start, facts, plugins) {
|
|
|
581
1128
|
}
|
|
582
1129
|
/**
|
|
583
1130
|
* One input a hook declared. It joins the values an action reads at run time and the declaration's
|
|
584
|
-
* types not at all, and it
|
|
585
|
-
* closures `action()` applies.
|
|
1131
|
+
* types not at all, and it skips the order checks an authoring call makes, because a hook's calls
|
|
1132
|
+
* are exempt from the closures `action()` applies.
|
|
586
1133
|
*/
|
|
587
1134
|
function declareHookInput(state, input) {
|
|
588
1135
|
const previous = state.bind;
|
|
@@ -608,10 +1155,28 @@ function applyAttachCalls(state, calls) {
|
|
|
608
1155
|
}
|
|
609
1156
|
return next;
|
|
610
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* The reason rule one name collision breaks.
|
|
1160
|
+
* Two options or two arguments break the rule the application's own collision of the pair breaks.
|
|
1161
|
+
* An argument and an option under one name break name-shared-across-kinds, which only a hook reaches.
|
|
1162
|
+
*/
|
|
1163
|
+
function nameCollisionRule(declared, held) {
|
|
1164
|
+
if (declared !== held) {
|
|
1165
|
+
return nameSharedAcrossKinds;
|
|
1166
|
+
}
|
|
1167
|
+
return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
|
|
1168
|
+
}
|
|
1169
|
+
/** The note a finding for one hook-declared input carries. */
|
|
1170
|
+
function hookNote(identity) {
|
|
1171
|
+
return `declared by plugin ${quoted(identity)}`;
|
|
1172
|
+
}
|
|
611
1173
|
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
612
|
-
function hookClause(
|
|
1174
|
+
function hookClause(earlier, path) {
|
|
1175
|
+
const { identity, input } = earlier;
|
|
613
1176
|
return {
|
|
614
|
-
clause: `an ${kind} plugin
|
|
1177
|
+
clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
|
|
1178
|
+
held: inputFinding(path, input, hookNote(identity)),
|
|
1179
|
+
kind: input.kind,
|
|
615
1180
|
remedy: 'Install one of them.',
|
|
616
1181
|
};
|
|
617
1182
|
}
|
|
@@ -633,6 +1198,25 @@ function attachedRemedy(target) {
|
|
|
633
1198
|
}
|
|
634
1199
|
return 'Install one of them.';
|
|
635
1200
|
}
|
|
1201
|
+
/** The collision with one option the globals table holds, a global or a plugin's own option. */
|
|
1202
|
+
function tableCollision(entry) {
|
|
1203
|
+
const { owner, site } = entry;
|
|
1204
|
+
if (owner.kind === 'plugin') {
|
|
1205
|
+
const clause = `an option of plugin ${quoted(owner.identity)}`;
|
|
1206
|
+
return {
|
|
1207
|
+
clause,
|
|
1208
|
+
held: siteFinding(site, site.named, clause),
|
|
1209
|
+
kind: 'option',
|
|
1210
|
+
remedy: attachedRemedy('plugin'),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
clause: 'a global option',
|
|
1215
|
+
held: siteFinding(site, site.named, 'the global option'),
|
|
1216
|
+
kind: 'option',
|
|
1217
|
+
remedy: attachedRemedy('global'),
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
636
1220
|
/**
|
|
637
1221
|
* What one name a hook-declared input of either kind collides with, in the order the scopes are
|
|
638
1222
|
* reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
|
|
@@ -641,32 +1225,36 @@ function attachedRemedy(target) {
|
|
|
641
1225
|
*/
|
|
642
1226
|
function attachedCollision(input, held) {
|
|
643
1227
|
const { name } = input;
|
|
644
|
-
const
|
|
1228
|
+
const { path } = held;
|
|
1229
|
+
const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
|
|
645
1230
|
if (hook !== undefined) {
|
|
646
|
-
return hookClause(
|
|
1231
|
+
return hookClause(hook, path);
|
|
647
1232
|
}
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1233
|
+
const local = held.locals.get(name);
|
|
1234
|
+
if (local !== undefined) {
|
|
1235
|
+
return {
|
|
1236
|
+
clause: 'a local option',
|
|
1237
|
+
held: inputFinding(path, local, 'the local option'),
|
|
1238
|
+
kind: 'option',
|
|
1239
|
+
remedy: attachedRemedy('local'),
|
|
1240
|
+
};
|
|
654
1241
|
}
|
|
655
|
-
const
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
|
|
659
|
-
return { clause, remedy: attachedRemedy(target) };
|
|
1242
|
+
const entry = held.globals.names.get(name);
|
|
1243
|
+
if (entry) {
|
|
1244
|
+
return tableCollision(entry);
|
|
660
1245
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1246
|
+
const argument = held.arguments.get(name);
|
|
1247
|
+
return argument === undefined
|
|
1248
|
+
? undefined
|
|
1249
|
+
: {
|
|
1250
|
+
clause: 'an argument',
|
|
1251
|
+
held: inputFinding(path, argument, 'the argument'),
|
|
1252
|
+
kind: 'argument',
|
|
1253
|
+
remedy: attachedRemedy('argument'),
|
|
1254
|
+
};
|
|
664
1255
|
}
|
|
665
|
-
/**
|
|
666
|
-
|
|
667
|
-
* its long form where it declares one and the colliding spelling itself where it declares none.
|
|
668
|
-
*/
|
|
669
|
-
function readSpellings(table, claimed) {
|
|
1256
|
+
/** The spellings one compiled table holds, each with its form and the place that declared it. */
|
|
1257
|
+
function readSpellings(table, claimed, placeOf) {
|
|
670
1258
|
const longs = new Map();
|
|
671
1259
|
for (const [spelling, option] of table) {
|
|
672
1260
|
if (option.role === 'long') {
|
|
@@ -675,69 +1263,129 @@ function readSpellings(table, claimed) {
|
|
|
675
1263
|
}
|
|
676
1264
|
for (const [spelling, option] of table) {
|
|
677
1265
|
if (!claimed.has(spelling)) {
|
|
678
|
-
|
|
1266
|
+
const form = longs.get(option.name) ?? spelling;
|
|
1267
|
+
claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
|
|
679
1268
|
}
|
|
680
1269
|
}
|
|
681
1270
|
}
|
|
1271
|
+
/** The place one input's spelling sits: the key of its call that yields the spelling. */
|
|
1272
|
+
function spellingPlace(site, role, note) {
|
|
1273
|
+
return siteFinding(site, spellingMark(site, role), note);
|
|
1274
|
+
}
|
|
682
1275
|
/** One hook-declared option's spellings, against every spelling the table already claims. */
|
|
683
1276
|
function checkAttachedSpelling(declared, named) {
|
|
684
|
-
const { claimed,
|
|
685
|
-
const
|
|
686
|
-
|
|
1277
|
+
const { claimed, scope } = named;
|
|
1278
|
+
const { identity, input } = declared;
|
|
1279
|
+
const { subject } = scope;
|
|
1280
|
+
const site = scope.siteOf(input);
|
|
1281
|
+
const table = compileOptions([input], scope);
|
|
1282
|
+
for (const [spelling, option] of table) {
|
|
687
1283
|
const used = claimed.get(spelling);
|
|
688
1284
|
if (used !== undefined) {
|
|
689
|
-
throw new DeclarationError(
|
|
1285
|
+
throw new DeclarationError(spellingTaken, {
|
|
1286
|
+
correction: 'Change one of the two spellings or omit the plugin.',
|
|
1287
|
+
findings: [
|
|
1288
|
+
spellingPlace(site, option.role, hookNote(identity)),
|
|
1289
|
+
...(used.finding === undefined ? [] : [used.finding]),
|
|
1290
|
+
],
|
|
1291
|
+
sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
|
|
1292
|
+
});
|
|
690
1293
|
}
|
|
691
1294
|
}
|
|
692
|
-
readSpellings(table, claimed);
|
|
1295
|
+
readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, hookNote(identity)));
|
|
1296
|
+
}
|
|
1297
|
+
/** The declarations of one kind, keyed by name, the first of each name winning. */
|
|
1298
|
+
function byName(inputs) {
|
|
1299
|
+
const named = new Map();
|
|
1300
|
+
for (const input of inputs) {
|
|
1301
|
+
if (!named.has(input.name)) {
|
|
1302
|
+
named.set(input.name, input);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return named;
|
|
1306
|
+
}
|
|
1307
|
+
/** Where the globals table's options declared their spellings, a global or a plugin's option. */
|
|
1308
|
+
function tableSpellings(globals) {
|
|
1309
|
+
return (name, role) => {
|
|
1310
|
+
const entry = globals.names.get(name);
|
|
1311
|
+
if (!entry) {
|
|
1312
|
+
return undefined;
|
|
1313
|
+
}
|
|
1314
|
+
const { owner, site } = entry;
|
|
1315
|
+
const note = owner.kind === 'plugin'
|
|
1316
|
+
? `an option of plugin ${quoted(owner.identity)}`
|
|
1317
|
+
: `the global option "${name}"`;
|
|
1318
|
+
return spellingPlace(site, role, note);
|
|
1319
|
+
};
|
|
693
1320
|
}
|
|
694
1321
|
/**
|
|
695
1322
|
* Every input a hook declared, against the names and spellings the Command, the globals table, and
|
|
696
1323
|
* an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
|
|
697
|
-
* input reports as the plugin's fault and never as the author's.
|
|
1324
|
+
* input reports as the plugin's fault and never as the author's. A collision carries a finding for
|
|
1325
|
+
* the hook's input and one for the declaration that already holds the name or spelling.
|
|
698
1326
|
*/
|
|
699
|
-
function checkAttachedInputs(declared, attached,
|
|
700
|
-
const
|
|
1327
|
+
function checkAttachedInputs(declared, attached, place) {
|
|
1328
|
+
const { globals, path } = place;
|
|
1329
|
+
const scope = localScope(declared.name, path);
|
|
1330
|
+
const { subject } = scope;
|
|
701
1331
|
const hooked = new Set(attached.map((entry) => entry.input));
|
|
702
1332
|
const authored = declared.inputs.filter((input) => !hooked.has(input));
|
|
703
1333
|
const options = authored.filter((input) => input.kind === 'option');
|
|
1334
|
+
const locals = byName(options);
|
|
704
1335
|
const held = {
|
|
705
|
-
arguments:
|
|
1336
|
+
arguments: byName(authored.filter((input) => input.kind === 'argument')),
|
|
706
1337
|
globals,
|
|
707
1338
|
hookArguments: new Map(),
|
|
708
1339
|
hooks: new Map(),
|
|
709
|
-
locals
|
|
1340
|
+
locals,
|
|
1341
|
+
path,
|
|
710
1342
|
};
|
|
711
1343
|
const claimed = new Map();
|
|
712
|
-
readSpellings(globals.options, claimed);
|
|
713
|
-
readSpellings(compileOptions(options,
|
|
714
|
-
|
|
1344
|
+
readSpellings(globals.options, claimed, tableSpellings(globals));
|
|
1345
|
+
readSpellings(compileOptions(options, scope), claimed, (name, role) => {
|
|
1346
|
+
const local = locals.get(name);
|
|
1347
|
+
return local === undefined || local.kind !== 'option'
|
|
1348
|
+
? undefined
|
|
1349
|
+
: spellingPlace(scope.siteOf(local), role, `the local option "${name}"`);
|
|
1350
|
+
});
|
|
1351
|
+
for (const entry of attached) {
|
|
1352
|
+
const { identity, input } = entry;
|
|
715
1353
|
const collision = attachedCollision(input, held);
|
|
716
1354
|
if (collision) {
|
|
717
|
-
throw new DeclarationError(
|
|
1355
|
+
throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
|
|
1356
|
+
correction: collision.remedy,
|
|
1357
|
+
findings: [inputFinding(path, input, hookNote(identity)), collision.held],
|
|
1358
|
+
sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
|
|
1359
|
+
});
|
|
718
1360
|
}
|
|
719
1361
|
if (input.kind === 'option') {
|
|
720
|
-
checkAttachedSpelling({ identity, input }, { claimed,
|
|
721
|
-
held.hooks.set(input.name,
|
|
1362
|
+
checkAttachedSpelling({ identity, input }, { claimed, scope });
|
|
1363
|
+
held.hooks.set(input.name, entry);
|
|
722
1364
|
}
|
|
723
1365
|
else {
|
|
724
|
-
held.hookArguments.set(input.name,
|
|
1366
|
+
held.hookArguments.set(input.name, entry);
|
|
725
1367
|
}
|
|
726
1368
|
}
|
|
727
1369
|
}
|
|
728
1370
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
729
1371
|
function bindDispatch(state, action, globals) {
|
|
730
|
-
return ({ host, out, passthrough, signal, style, values }) => {
|
|
1372
|
+
return ({ command, graph, host, out, passthrough, signal, style, values }) => {
|
|
731
1373
|
const bound = state.bind(values);
|
|
732
1374
|
// Last resort: no typed path exists.
|
|
733
1375
|
// The graph erases the binder's generic relationship.
|
|
734
1376
|
// It holds because attachment checks the global output requirement.
|
|
735
|
-
//
|
|
1377
|
+
// The call or the attach that met both options rejected a collision between a global and a local.
|
|
736
1378
|
// This binder returns the Application's validated globals alone.
|
|
737
1379
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
738
1380
|
const globalOptions = globals.bind(values);
|
|
739
1381
|
return action({
|
|
740
1382
|
args: bound.args,
|
|
1383
|
+
get command() {
|
|
1384
|
+
return command();
|
|
1385
|
+
},
|
|
1386
|
+
get graph() {
|
|
1387
|
+
return graph();
|
|
1388
|
+
},
|
|
741
1389
|
host,
|
|
742
1390
|
options: { ...globalOptions, ...bound.options },
|
|
743
1391
|
out,
|
|
@@ -747,69 +1395,64 @@ function bindDispatch(state, action, globals) {
|
|
|
747
1395
|
});
|
|
748
1396
|
};
|
|
749
1397
|
}
|
|
750
|
-
/**
|
|
1398
|
+
/**
|
|
1399
|
+
* The own facts and extension values of each input a lifecycle hook declared, which the calls that
|
|
1400
|
+
* declared the author's inputs already checked.
|
|
1401
|
+
*/
|
|
751
1402
|
function checkInputFacts(inputs, command, context) {
|
|
752
1403
|
const { name, subject } = command;
|
|
753
1404
|
for (const input of inputs) {
|
|
754
|
-
const
|
|
755
|
-
|
|
1405
|
+
const site = inputSite(name, context.path, input);
|
|
1406
|
+
const sentence = site.subject;
|
|
1407
|
+
checkDescription(site, input.config.description);
|
|
756
1408
|
if (input.kind === 'argument') {
|
|
757
|
-
checkNoListingFacts(
|
|
1409
|
+
checkNoListingFacts(site, input.config);
|
|
1410
|
+
checkNoArgumentBinding(site, input.config);
|
|
758
1411
|
}
|
|
759
1412
|
else {
|
|
760
|
-
checkHidden(
|
|
761
|
-
checkDeprecated(
|
|
1413
|
+
checkHidden(site, input.config.hidden);
|
|
1414
|
+
checkDeprecated(site, input.config.deprecated);
|
|
1415
|
+
checkEnvBinding(site, input.config);
|
|
762
1416
|
}
|
|
763
1417
|
context.extensions.set(input, buildExtensions({
|
|
764
1418
|
declared: input.config.extensions,
|
|
765
1419
|
descriptors: context.descriptors,
|
|
1420
|
+
site: { ...site, at: '1.extensions' },
|
|
766
1421
|
subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
|
|
767
1422
|
target: input.kind,
|
|
768
1423
|
}));
|
|
769
1424
|
}
|
|
770
1425
|
}
|
|
771
1426
|
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
772
|
-
function checkArgumentPlacement(
|
|
1427
|
+
function checkArgumentPlacement(command, slot, child) {
|
|
773
1428
|
if (slot && child) {
|
|
774
|
-
throw
|
|
1429
|
+
throw placementFault(command, slot.input, child);
|
|
775
1430
|
}
|
|
776
1431
|
}
|
|
777
|
-
/**
|
|
1432
|
+
/**
|
|
1433
|
+
* Compiles one declaration for dispatch against the shared globals table. Every authored rule threw
|
|
1434
|
+
* at the call or the attach that first held its data, so what can still fail here is the root's
|
|
1435
|
+
* finished-Command rules, the root never being attached, and whatever the lifecycle hooks
|
|
1436
|
+
* contributed.
|
|
1437
|
+
*/
|
|
778
1438
|
export function buildCommand(state, context) {
|
|
779
|
-
const {
|
|
1439
|
+
const { action, facts, name } = state;
|
|
780
1440
|
const { globals } = context;
|
|
781
1441
|
const subject = commandSubject(name);
|
|
782
|
-
const layer =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
const declaredExtensions = storeCommandLayers({
|
|
789
|
-
descriptors: context.descriptors,
|
|
790
|
-
layers: state.extensions,
|
|
791
|
-
subject: layer,
|
|
792
|
-
});
|
|
793
|
-
// Each declaration's own facts, in authoring order, before the rules that pair declarations.
|
|
794
|
-
checkInputFacts(state.inputs, { name, subject }, context);
|
|
795
|
-
const attached = collectChildren(state);
|
|
796
|
-
checkDeclarationOrder(state);
|
|
797
|
-
const aliases = collectAliases(state);
|
|
798
|
-
const declaredSlots = collectArguments(state, subject);
|
|
799
|
-
checkArgumentPlacement(name, declaredSlots[0], attached[0]);
|
|
800
|
-
if (actions.length > 1) {
|
|
801
|
-
throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
|
|
802
|
-
}
|
|
803
|
-
const action = actions[0];
|
|
1442
|
+
const layer = layerOf(name);
|
|
1443
|
+
for (const [input, record] of state.records) {
|
|
1444
|
+
context.extensions.set(input, record);
|
|
1445
|
+
}
|
|
1446
|
+
const place = { name, path: context.path };
|
|
1447
|
+
const declaredSlots = collectArguments(state, place);
|
|
804
1448
|
const hasAction = action !== undefined;
|
|
805
|
-
const declaredResult = buildResult(state, hasAction);
|
|
806
1449
|
/**
|
|
807
|
-
* The hooks run once the author's declaration is complete and
|
|
808
|
-
* reads an exact record
|
|
1450
|
+
* The hooks run once the author's declaration is complete, and each value a hook receives resolves
|
|
1451
|
+
* its result, so a hook reads an exact record. Every rule below reads what the hooks returned.
|
|
809
1452
|
*/
|
|
810
1453
|
// One token per Command per build, which binds a hook's return to the value it received.
|
|
811
1454
|
const lineage = {};
|
|
812
|
-
const progress = runAttachHooks({ declared: state, extensions:
|
|
1455
|
+
const progress = runAttachHooks({ declared: state, extensions: state.extensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
|
|
813
1456
|
const { calls } = progress;
|
|
814
1457
|
// The descriptors the returned value's extensions registered join the build's registry.
|
|
815
1458
|
for (const [identity, descriptor] of progress.registry) {
|
|
@@ -820,38 +1463,37 @@ export function buildCommand(state, context) {
|
|
|
820
1463
|
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
821
1464
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
822
1465
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
823
|
-
checkAttachedInputs(hooked, hookInputs, globals);
|
|
824
|
-
// Every value was validated once, the author's
|
|
1466
|
+
checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
|
|
1467
|
+
// Every value was validated once, the author's at each call and each hook's at its `extend()`.
|
|
825
1468
|
const extensions = publishStore(progress.extensions);
|
|
826
|
-
const slots = hookInputs.length > 0 ? collectArguments(hooked,
|
|
827
|
-
checkArgumentPlacement(
|
|
828
|
-
const result =
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
const options = compileLocalOptions(hooked, globals, subject);
|
|
1469
|
+
const slots = hookInputs.length > 0 ? collectArguments(hooked, place) : declaredSlots;
|
|
1470
|
+
checkArgumentPlacement(place, slots[0], state.children[0]);
|
|
1471
|
+
const result = checkFinished(hooked, hasAction, { path: context.path });
|
|
1472
|
+
const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
|
|
1473
|
+
// A hook's erased calls answer the declaration rules an authored call answers at the call.
|
|
1474
|
+
checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
|
|
833
1475
|
const children = new Map();
|
|
834
1476
|
const routes = new Map();
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1477
|
+
for (const child of state.children) {
|
|
1478
|
+
const routed = {
|
|
1479
|
+
command: child.node.build({ ...context, path: [...context.path, child.name] }),
|
|
1480
|
+
name: child.name,
|
|
1481
|
+
};
|
|
839
1482
|
children.set(routed.name, routed.command);
|
|
840
1483
|
routes.set(routed.name, routed);
|
|
841
1484
|
for (const alias of routed.command.aliases) {
|
|
842
|
-
claim(routed.name, alias);
|
|
843
1485
|
routes.set(alias, routed);
|
|
844
1486
|
}
|
|
845
1487
|
}
|
|
846
1488
|
return {
|
|
847
|
-
aliases,
|
|
1489
|
+
aliases: state.aliases,
|
|
848
1490
|
arguments: slots,
|
|
849
1491
|
children,
|
|
850
|
-
deprecated,
|
|
851
|
-
description,
|
|
1492
|
+
deprecated: facts.deprecated,
|
|
1493
|
+
description: facts.description,
|
|
852
1494
|
dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
|
|
853
1495
|
extensions,
|
|
854
|
-
hidden,
|
|
1496
|
+
hidden: facts.hidden,
|
|
855
1497
|
inputs: hooked.inputs,
|
|
856
1498
|
name,
|
|
857
1499
|
options,
|
|
@@ -859,105 +1501,101 @@ export function buildCommand(state, context) {
|
|
|
859
1501
|
routes,
|
|
860
1502
|
};
|
|
861
1503
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* The globals table compiles once per build and the whole graph shares it. The root's registry
|
|
1506
|
+
* holds every descriptor the Application names, so a hook's `extend()` call meets all of them.
|
|
1507
|
+
*/
|
|
1508
|
+
export function buildGraph(root, globals, plugins) {
|
|
1509
|
+
const extensions = new Map([
|
|
1510
|
+
...globals.records,
|
|
1511
|
+
...plugins.flatMap((installed) => [...installed.records]),
|
|
1512
|
+
]);
|
|
864
1513
|
const context = {
|
|
865
|
-
descriptors:
|
|
866
|
-
extensions
|
|
867
|
-
globals: buildGlobals(globals,
|
|
868
|
-
owners: new Map(),
|
|
1514
|
+
descriptors: new Map(root.descriptors),
|
|
1515
|
+
extensions,
|
|
1516
|
+
globals: buildGlobals(globals, plugins),
|
|
869
1517
|
path: [],
|
|
870
|
-
plugins
|
|
871
|
-
};
|
|
872
|
-
return {
|
|
873
|
-
extensions: context.extensions,
|
|
874
|
-
globals: context.globals,
|
|
875
|
-
root: buildCommand(root, context),
|
|
1518
|
+
plugins,
|
|
876
1519
|
};
|
|
1520
|
+
return { extensions, globals: context.globals, root: buildCommand(root, context) };
|
|
877
1521
|
}
|
|
878
1522
|
export class CommandBuilder {
|
|
1523
|
+
#name;
|
|
879
1524
|
#state;
|
|
880
|
-
constructor(state) {
|
|
1525
|
+
constructor(name, state) {
|
|
1526
|
+
this.#name = name;
|
|
881
1527
|
this.#state = state;
|
|
882
|
-
nodes.set(this,
|
|
883
|
-
}
|
|
884
|
-
get name() {
|
|
885
|
-
return this.#state.name;
|
|
1528
|
+
nodes.set(this, nodeHandle(name, state));
|
|
886
1529
|
}
|
|
887
1530
|
argument(name, config) {
|
|
888
1531
|
const input = {
|
|
889
|
-
config
|
|
1532
|
+
config,
|
|
890
1533
|
kind: 'argument',
|
|
891
1534
|
name,
|
|
892
1535
|
};
|
|
893
|
-
return this
|
|
1536
|
+
return this.#derive(declareArgument(this.#state, input));
|
|
894
1537
|
}
|
|
895
1538
|
option(name, config) {
|
|
896
1539
|
const input = {
|
|
897
|
-
config
|
|
1540
|
+
config,
|
|
898
1541
|
kind: 'option',
|
|
899
1542
|
name,
|
|
900
1543
|
};
|
|
901
|
-
return this
|
|
1544
|
+
return this.#derive(declareOption(this.#state, input));
|
|
902
1545
|
}
|
|
903
1546
|
/**
|
|
904
|
-
* Aliases are other
|
|
1547
|
+
* Aliases are other portable names that route to this Command. They invalidate no call, and the
|
|
905
1548
|
* tuple rest parameter rejects a call that names none.
|
|
906
1549
|
*/
|
|
907
1550
|
alias(...names) {
|
|
908
|
-
return this
|
|
1551
|
+
return this.#derive(declareAlias(this.#state, names));
|
|
909
1552
|
}
|
|
910
1553
|
/** A child arrives in any type state, because its own action is the call that finished it. */
|
|
911
1554
|
command(child) {
|
|
912
|
-
return this
|
|
1555
|
+
return this.#derive(attachChild(this.#state, child));
|
|
913
1556
|
}
|
|
914
1557
|
/**
|
|
915
1558
|
* The value this Command produces for its consumer. The type argument is stated by the author,
|
|
916
1559
|
* so the views record states no type of its own and an omitted argument names none either.
|
|
917
1560
|
*/
|
|
918
1561
|
result(declaration) {
|
|
919
|
-
return this
|
|
1562
|
+
return this.#derive(declareResult(this.#state, 'value', declaration));
|
|
920
1563
|
}
|
|
921
1564
|
/** The same declaration over a sequence, whose type argument is one row. */
|
|
922
1565
|
rows(declaration) {
|
|
923
|
-
return this
|
|
1566
|
+
return this.#derive(declareResult(this.#state, 'rows', declaration));
|
|
924
1567
|
}
|
|
925
1568
|
/**
|
|
926
1569
|
* Views after the fact. It merges by key, so an existing name is replaced in place and a new one
|
|
927
1570
|
* is appended, and `default` names the key core renders when nothing selects another.
|
|
928
1571
|
*/
|
|
929
1572
|
views(replacements, options) {
|
|
930
|
-
return this
|
|
1573
|
+
return this.#derive(declareResultViews(this.#state, replacements, options));
|
|
931
1574
|
}
|
|
932
1575
|
/** The action closes input authoring; `extend()` remains outside this state transition. */
|
|
933
1576
|
action(handler) {
|
|
934
|
-
return this
|
|
1577
|
+
return this.#derive(declareAction(this.#state, handler));
|
|
935
1578
|
}
|
|
936
1579
|
extend(...values) {
|
|
937
|
-
return this
|
|
938
|
-
}
|
|
939
|
-
build(context) {
|
|
940
|
-
return buildCommand(this.#state, context);
|
|
1580
|
+
return this.#derive(declareExtensions(this.#state, values));
|
|
941
1581
|
}
|
|
942
1582
|
/**
|
|
943
1583
|
* The same runtime value in the state the calling method's return type names. Each call states
|
|
944
1584
|
* its own transition, and the declared result travels with it unless the call replaces it.
|
|
945
1585
|
*/
|
|
946
|
-
derive(state) {
|
|
947
|
-
return new
|
|
1586
|
+
#derive(state) {
|
|
1587
|
+
return new _b(this.#name, state);
|
|
948
1588
|
}
|
|
949
1589
|
}
|
|
950
|
-
|
|
1590
|
+
_b = CommandBuilder;
|
|
1591
|
+
/**
|
|
1592
|
+
* The constructor uses the Application registration; public type defaults stay library-neutral.
|
|
1593
|
+
* Every rule on the name and the options slot throws before the value exists.
|
|
1594
|
+
*/
|
|
951
1595
|
class CommandDeclaration extends CommandBuilder {
|
|
952
1596
|
constructor(name, options) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
description: options?.description,
|
|
956
|
-
extensions: options?.extensions,
|
|
957
|
-
hidden: options?.hidden,
|
|
958
|
-
name,
|
|
959
|
-
options,
|
|
960
|
-
}));
|
|
1597
|
+
const declared = namedState(name, options);
|
|
1598
|
+
super(declared.name, declared.state);
|
|
961
1599
|
}
|
|
962
1600
|
}
|
|
963
1601
|
/** The public constructor takes a name and one options object, as the Application does. */
|
|
@@ -970,21 +1608,54 @@ export function collectInputs(command) {
|
|
|
970
1608
|
];
|
|
971
1609
|
}
|
|
972
1610
|
/**
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
|
|
1611
|
+
* Where every input of one graph was declared: each global option at its `globalOption()` call,
|
|
1612
|
+
* and each Command's own inputs at their calls on the Command, under its path from the root.
|
|
1613
|
+
*/
|
|
1614
|
+
export function inputPlaces(graph) {
|
|
1615
|
+
const places = new Map();
|
|
1616
|
+
for (const input of graph.globals.inputs) {
|
|
1617
|
+
places.set(input, inputPlace(input, { global: true, path: [] }));
|
|
1618
|
+
}
|
|
1619
|
+
const walk = (command, path) => {
|
|
1620
|
+
for (const input of command.inputs) {
|
|
1621
|
+
places.set(input, inputPlace(input, { global: false, path }));
|
|
1622
|
+
}
|
|
1623
|
+
for (const [name, child] of command.children) {
|
|
1624
|
+
walk(child, [...path, name]);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
walk(graph.root, []);
|
|
1628
|
+
return places;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* The names a routing failure offers: the canonical names of the visible, current children, in
|
|
1632
|
+
* authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
|
|
1633
|
+
* it, as completion leaves them out, and a parent whose children are all hidden or deprecated
|
|
1634
|
+
* offers none. A deprecated child typed in full still routes.
|
|
976
1635
|
*/
|
|
977
1636
|
function candidatesOf(command) {
|
|
978
|
-
return [...command.children]
|
|
1637
|
+
return [...command.children]
|
|
1638
|
+
.filter(([, child]) => !child.hidden && child.deprecated === undefined)
|
|
1639
|
+
.map(([name]) => name);
|
|
979
1640
|
}
|
|
980
|
-
/**
|
|
981
|
-
|
|
1641
|
+
/**
|
|
1642
|
+
* Whether a token names one of the Command's children: the Command has children and the token is
|
|
1643
|
+
* no option token. Routing and `locate` read a bare word through this rule.
|
|
1644
|
+
*/
|
|
1645
|
+
export function readsAsChild(command, token) {
|
|
1646
|
+
return command.children.size > 0 && !isOptionToken(token);
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
|
|
1650
|
+
* `walked` receives the path after each name routes, so an unknown Command leaves its caller
|
|
1651
|
+
* holding the partial path walked before it.
|
|
1652
|
+
*/
|
|
1653
|
+
export function route(root, tokens, walked) {
|
|
982
1654
|
let command = root;
|
|
983
1655
|
const path = [];
|
|
984
1656
|
let index = 0;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (token === undefined || token === '--' || token.startsWith('-')) {
|
|
1657
|
+
for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
|
|
1658
|
+
if (!readsAsChild(command, token)) {
|
|
988
1659
|
break;
|
|
989
1660
|
}
|
|
990
1661
|
const child = command.routes.get(token);
|
|
@@ -994,6 +1665,7 @@ export function route(root, tokens) {
|
|
|
994
1665
|
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
995
1666
|
command = child.command;
|
|
996
1667
|
path.push(child.name);
|
|
1668
|
+
walked?.(Object.freeze([...path]));
|
|
997
1669
|
index += 1;
|
|
998
1670
|
}
|
|
999
1671
|
return { command, path, tokens: tokens.slice(index) };
|
|
@@ -1005,33 +1677,40 @@ export function route(root, tokens) {
|
|
|
1005
1677
|
*/
|
|
1006
1678
|
function bindArguments(command, path, positionals) {
|
|
1007
1679
|
const values = new Map();
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (slot
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1680
|
+
for (const [position, token] of positionals.entries()) {
|
|
1681
|
+
const slot = argumentSlot(command.arguments, position);
|
|
1682
|
+
if (!slot) {
|
|
1683
|
+
throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
|
|
1684
|
+
}
|
|
1685
|
+
// An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
|
|
1686
|
+
const bound = values.get(slot.input);
|
|
1687
|
+
if (!slot.variadic) {
|
|
1688
|
+
values.set(slot.input, token);
|
|
1689
|
+
}
|
|
1690
|
+
else if (Array.isArray(bound)) {
|
|
1691
|
+
bound.push(token);
|
|
1017
1692
|
}
|
|
1018
1693
|
else {
|
|
1019
|
-
|
|
1020
|
-
if (value !== undefined) {
|
|
1021
|
-
values.set(slot.input, value);
|
|
1022
|
-
index += 1;
|
|
1023
|
-
}
|
|
1694
|
+
values.set(slot.input, [token]);
|
|
1024
1695
|
}
|
|
1025
1696
|
}
|
|
1026
|
-
if (index < positionals.length) {
|
|
1027
|
-
throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
|
|
1028
|
-
}
|
|
1029
1697
|
return values;
|
|
1030
1698
|
}
|
|
1031
|
-
/**
|
|
1032
|
-
|
|
1699
|
+
/**
|
|
1700
|
+
* The slot the positional at one index fills: the slot at that index, else a variadic last slot,
|
|
1701
|
+
* which accepts every later positional, else none. Binding and `locate` read positions through it.
|
|
1702
|
+
*/
|
|
1703
|
+
export function argumentSlot(slots, position) {
|
|
1704
|
+
const last = slots.at(-1);
|
|
1705
|
+
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
|
|
1709
|
+
* receives the path as routing extends it, the partial path of an unknown Command included.
|
|
1710
|
+
*/
|
|
1711
|
+
export function routeInvocation(graph, argv, walked) {
|
|
1033
1712
|
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
1034
|
-
const routed = route(graph.root, scan.rest);
|
|
1713
|
+
const routed = route(graph.root, scan.rest, walked);
|
|
1035
1714
|
return {
|
|
1036
1715
|
command: routed.command,
|
|
1037
1716
|
path: routed.path,
|
|
@@ -1060,37 +1739,104 @@ function requestOf(command, values, passthrough) {
|
|
|
1060
1739
|
});
|
|
1061
1740
|
}
|
|
1062
1741
|
/**
|
|
1063
|
-
* The
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1742
|
+
* The callable check and local parsing. A group answers no invocation of its own, so it holds the
|
|
1743
|
+
* missing-subcommand error with the rank the routing errors have and parses no token; a token
|
|
1744
|
+
* fault holds the same way.
|
|
1066
1745
|
*/
|
|
1067
|
-
|
|
1068
|
-
const { command, path
|
|
1746
|
+
function parseLocal(routed) {
|
|
1747
|
+
const { command, path } = routed;
|
|
1069
1748
|
const { dispatch } = command;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1749
|
+
try {
|
|
1750
|
+
if (!dispatch) {
|
|
1751
|
+
throw new NonCallableCommandError(path, candidatesOf(command));
|
|
1752
|
+
}
|
|
1753
|
+
const parsed = parseInputs(command.options, routed.tokens);
|
|
1754
|
+
const args = bindArguments(command, path, parsed.positionals);
|
|
1755
|
+
return {
|
|
1756
|
+
args,
|
|
1757
|
+
dispatch,
|
|
1758
|
+
kind: 'parsed',
|
|
1759
|
+
options: parsed.options,
|
|
1760
|
+
passthrough: parsed.passthrough,
|
|
1761
|
+
};
|
|
1073
1762
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1763
|
+
catch (error) {
|
|
1764
|
+
return { fault: error, kind: 'held' };
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* The node of one option a configuration source is asked about: in the graph's globals, or among
|
|
1769
|
+
* the routed Command's own options. Build published every declaration, so a missing node means the
|
|
1770
|
+
* two readings of one graph disagree.
|
|
1771
|
+
*/
|
|
1772
|
+
function requestNode(graph, path, place) {
|
|
1773
|
+
const options = place.global ? graph.globals : nodeAt(graph, path).options;
|
|
1774
|
+
const node = options.find((option) => option.name === place.name);
|
|
1775
|
+
if (!node) {
|
|
1776
|
+
throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
|
|
1777
|
+
}
|
|
1778
|
+
return node;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* The input-source stage over this run's own copies of the parsed values. The global and plugin
|
|
1782
|
+
* options fill whatever local parsing held; the routed Command's own options fill only when local
|
|
1783
|
+
* parsing held no fault, because the request is `null` otherwise.
|
|
1784
|
+
*/
|
|
1785
|
+
async function fillScope({ graph, invocation, routed }, local) {
|
|
1786
|
+
const globals = copyValues(routed.scan);
|
|
1787
|
+
const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
|
|
1788
|
+
const sources = await fillInputs({
|
|
1789
|
+
extensions: graph.extensions,
|
|
1790
|
+
globals: {
|
|
1791
|
+
global: true,
|
|
1792
|
+
inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
|
|
1793
|
+
values: globals,
|
|
1794
|
+
},
|
|
1795
|
+
host: invocation.host,
|
|
1796
|
+
inspected: invocation.inspected,
|
|
1797
|
+
locals: local.kind === 'parsed'
|
|
1798
|
+
? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
|
|
1799
|
+
: undefined,
|
|
1800
|
+
offer: invocation.offer,
|
|
1801
|
+
out: invocation.sourceOut,
|
|
1802
|
+
plugins: graph.globals.plugins,
|
|
1803
|
+
request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
|
|
1804
|
+
signal: invocation.signal,
|
|
1805
|
+
style: invocation.style,
|
|
1806
|
+
});
|
|
1807
|
+
return { globals, locals, sources };
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Validation and the dispatch it prepares, over the values the input-source stage left. It answers
|
|
1811
|
+
* with the call that dispatches, so the caller records that the action was invoked at the moment
|
|
1812
|
+
* it invokes it and no earlier failure reads as a dispatch.
|
|
1813
|
+
*/
|
|
1814
|
+
async function readyDispatch({ graph, invocation, routed }, filled) {
|
|
1815
|
+
const { command, path } = routed;
|
|
1816
|
+
const { local } = filled;
|
|
1076
1817
|
const values = await validateValues({
|
|
1077
1818
|
command: path,
|
|
1078
1819
|
defaults: invocation.defaults,
|
|
1079
1820
|
host: invocation.host,
|
|
1080
1821
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
1081
|
-
passthrough:
|
|
1822
|
+
passthrough: local.passthrough,
|
|
1823
|
+
plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
|
|
1082
1824
|
signal: invocation.signal,
|
|
1083
|
-
|
|
1825
|
+
sources: filled.sources,
|
|
1826
|
+
supplied: { args: local.args, options: filled.values },
|
|
1084
1827
|
});
|
|
1085
1828
|
return {
|
|
1086
1829
|
dispatch: async (view) => {
|
|
1087
1830
|
// The channel is built at the boundary, because the view a middleware selected is read there.
|
|
1088
1831
|
const channel = invocation.channel({ path, result: command.result, view });
|
|
1089
1832
|
try {
|
|
1090
|
-
await dispatch({
|
|
1833
|
+
await local.dispatch({
|
|
1834
|
+
// The run builds its graph once, so every read answers the same node.
|
|
1835
|
+
command: () => nodeAt(invocation.inspected(), path),
|
|
1836
|
+
graph: invocation.inspected,
|
|
1091
1837
|
host: invocation.host,
|
|
1092
1838
|
out: channel.out,
|
|
1093
|
-
passthrough:
|
|
1839
|
+
passthrough: local.passthrough,
|
|
1094
1840
|
signal: invocation.signal,
|
|
1095
1841
|
style: invocation.style,
|
|
1096
1842
|
values,
|
|
@@ -1112,19 +1858,43 @@ async function readyDispatch(graph, routed, invocation) {
|
|
|
1112
1858
|
throw error;
|
|
1113
1859
|
}
|
|
1114
1860
|
},
|
|
1115
|
-
request: requestOf(command, values,
|
|
1861
|
+
request: requestOf(command, values, local.passthrough),
|
|
1116
1862
|
};
|
|
1117
1863
|
}
|
|
1118
1864
|
/**
|
|
1119
1865
|
* Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
|
|
1120
1866
|
* middleware reads the request before the action runs and a takeover never observes the fault.
|
|
1867
|
+
* The phases run in order: local parsing, the input-source stage, and validation. A local fault
|
|
1868
|
+
* outranks a configuration source's fault, and after either one core runs no validation.
|
|
1121
1869
|
*/
|
|
1122
1870
|
export async function prepareDispatch(graph, routed, invocation) {
|
|
1123
1871
|
const result = routed.command.result;
|
|
1872
|
+
const local = parseLocal(routed);
|
|
1873
|
+
const preparation = { graph, invocation, routed };
|
|
1874
|
+
const { globals, locals, sources } = await fillScope(preparation, local);
|
|
1875
|
+
const held = (fault) => ({
|
|
1876
|
+
fault,
|
|
1877
|
+
globals,
|
|
1878
|
+
kind: 'held',
|
|
1879
|
+
request: null,
|
|
1880
|
+
result,
|
|
1881
|
+
});
|
|
1882
|
+
if (local.kind === 'held') {
|
|
1883
|
+
return held(local.fault);
|
|
1884
|
+
}
|
|
1885
|
+
if (sources.fault) {
|
|
1886
|
+
return held(sources.fault);
|
|
1887
|
+
}
|
|
1124
1888
|
try {
|
|
1125
|
-
|
|
1889
|
+
const ready = await readyDispatch(preparation, {
|
|
1890
|
+
local,
|
|
1891
|
+
sources,
|
|
1892
|
+
values: mergeValues(globals, locals),
|
|
1893
|
+
});
|
|
1894
|
+
return { ...ready, globals, kind: 'ready', result };
|
|
1126
1895
|
}
|
|
1127
1896
|
catch (error) {
|
|
1128
|
-
|
|
1897
|
+
// The fault is held as a failure, so a throw from reading a validator's output is never offered.
|
|
1898
|
+
return held(toFailure(error));
|
|
1129
1899
|
}
|
|
1130
1900
|
}
|