@loomcli/core 0.3.0 → 0.5.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 +44 -31
- package/dist/application.js +122 -110
- package/dist/bindings.d.ts +26 -0
- package/dist/bindings.js +45 -0
- package/dist/chain.d.ts +11 -6
- package/dist/chain.js +28 -81
- package/dist/command.d.ts +161 -84
- package/dist/command.js +692 -369
- package/dist/errors.d.ts +7 -2
- package/dist/errors.js +9 -1
- package/dist/extension.d.ts +81 -23
- package/dist/extension.js +119 -54
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +10 -3
- package/dist/globals.d.ts +45 -12
- package/dist/globals.js +74 -18
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +37 -3
- package/dist/inspect.js +93 -6
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +73 -0
- package/dist/options.js +124 -27
- package/dist/output.d.ts +2 -0
- package/dist/output.js +5 -1
- package/dist/plugin.d.ts +107 -53
- package/dist/plugin.js +204 -66
- package/dist/sources.d.ts +56 -0
- package/dist/sources.js +249 -0
- package/dist/types.d.ts +70 -20
- package/dist/validation.d.ts +22 -8
- package/dist/validation.js +184 -77
- package/dist/view.d.ts +1 -1
- package/dist/view.js +2 -2
- package/package.json +3 -2
package/dist/command.js
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
var _a;
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
+
import { commandSentence, commandSubject, DeclarationError, InternalError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
|
|
4
|
+
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
4
5
|
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
|
|
5
|
-
import { buildGlobals,
|
|
6
|
-
import { resultNode, snapshot } from './inspect.js';
|
|
7
|
-
import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
6
|
+
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
7
|
+
import { nodeAt, resultNode, snapshot } from './inspect.js';
|
|
8
|
+
import { compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, } from './options.js';
|
|
9
|
+
import { fillInputs } from './sources.js';
|
|
10
|
+
import { captureConfig, checkDeclarations, validateValues } from './validation.js';
|
|
11
|
+
/**
|
|
12
|
+
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
13
|
+
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
14
|
+
*/
|
|
15
|
+
const nestingCap = { depth: 2, words: 'two' };
|
|
16
|
+
/**
|
|
17
|
+
* The shallowest level a parent can sit at: the root is level 0, and a named Command is attached
|
|
18
|
+
* somewhere below it, so it sits at level 1 or deeper.
|
|
19
|
+
*/
|
|
20
|
+
function parentLevel(name) {
|
|
21
|
+
return name === null ? 0 : 1;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Each authored value registers its handle here, so the public value holds no state to reach or
|
|
25
|
+
* replace.
|
|
26
|
+
*/
|
|
10
27
|
const nodes = new WeakMap();
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The handle one Command value registers, closed over its state. The value itself carries no
|
|
30
|
+
* member that reaches the state, so an attached Command stays final.
|
|
31
|
+
*/
|
|
32
|
+
function nodeHandle(name, state) {
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
build: (context) => buildCommand(state, context),
|
|
35
|
+
declared: state,
|
|
36
|
+
hasAction: state.action !== undefined,
|
|
37
|
+
name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** The handle behind a value an author built as a Command, or nothing for any other value. */
|
|
41
|
+
export function commandNode(value) {
|
|
42
|
+
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
18
43
|
}
|
|
19
|
-
/**
|
|
44
|
+
/** A named Command's options slot holds a plain options object, and never retired globals wiring. */
|
|
20
45
|
function checkCommandOptions(name, options) {
|
|
21
46
|
if (options !== undefined && !isPlainObject(options)) {
|
|
22
47
|
throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
|
|
@@ -25,93 +50,240 @@ function checkCommandOptions(name, options) {
|
|
|
25
50
|
throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
|
|
26
51
|
}
|
|
27
52
|
}
|
|
28
|
-
/** One name rule for
|
|
53
|
+
/** One name rule for an argument, option, or view name: a bare token the parser can read. */
|
|
29
54
|
function isDeclaredName(name) {
|
|
30
55
|
return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
31
56
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The portable name rule, for every name an operator types as a command at a shell prompt: the
|
|
59
|
+
* application name, every Command name, and every alias. The characters are the POSIX portable
|
|
60
|
+
* filename set, and a name starts with neither `-`, which reads as an option, nor `.`, which a
|
|
61
|
+
* shell hides.
|
|
62
|
+
*/
|
|
63
|
+
export function isPortableName(name) {
|
|
64
|
+
return typeof name === 'string' && /^[A-Za-z0-9_][A-Za-z0-9._-]*$/u.test(name);
|
|
36
65
|
}
|
|
37
|
-
/**
|
|
66
|
+
/** The one correction every portable name diagnostic ends with. */
|
|
67
|
+
export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
|
|
68
|
+
/** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
|
|
38
69
|
function checkAliasName(command, alias) {
|
|
39
|
-
if (!
|
|
40
|
-
throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}".
|
|
70
|
+
if (!isPortableName(alias)) {
|
|
71
|
+
throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". ${portableNameCorrection}`);
|
|
41
72
|
}
|
|
42
73
|
}
|
|
74
|
+
/** How the extension diagnostics of one Command's own layers name it. */
|
|
75
|
+
export function layerOf(name) {
|
|
76
|
+
return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
|
|
77
|
+
}
|
|
43
78
|
/**
|
|
44
|
-
* The state every declaration starts from. The unnamed root and each named Command share it.
|
|
45
|
-
*
|
|
46
|
-
* passed changes nothing the declaration holds.
|
|
79
|
+
* The state every declaration starts from. The unnamed root and each named Command share it. The
|
|
80
|
+
* declaration values arrive checked, because the constructor that read them threw for any fault.
|
|
47
81
|
*/
|
|
48
82
|
export function freshState(declaration) {
|
|
49
83
|
return {
|
|
50
|
-
|
|
84
|
+
action: undefined,
|
|
51
85
|
aliases: [],
|
|
52
86
|
bind: () => ({ args: {}, options: {} }),
|
|
53
87
|
children: [],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
hidden: declaration.hidden,
|
|
88
|
+
descriptors: declaration.descriptors,
|
|
89
|
+
extensions: declaration.extensions,
|
|
90
|
+
facts: declaration.facts,
|
|
58
91
|
inputs: [],
|
|
59
|
-
late: [],
|
|
60
92
|
name: declaration.name,
|
|
61
|
-
|
|
93
|
+
records: new Map(),
|
|
62
94
|
results: [],
|
|
63
95
|
};
|
|
64
96
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
/**
|
|
98
|
+
* A named Command's own declaration, checked before the value exists: the name, the options slot,
|
|
99
|
+
* the core facts, and the extension values the slot carries. A later change to the options object
|
|
100
|
+
* the author passed changes nothing the declaration holds.
|
|
101
|
+
*/
|
|
102
|
+
function namedState(name, options) {
|
|
103
|
+
if (!isPortableName(name)) {
|
|
104
|
+
throw new DeclarationError(`Command name "${String(name)}" is invalid. ${portableNameCorrection}`);
|
|
105
|
+
}
|
|
106
|
+
checkCommandOptions(name, options);
|
|
107
|
+
const slot = isPlainObject(options) ? options : undefined;
|
|
108
|
+
const sentence = commandSentence(name);
|
|
109
|
+
// The facts are read in the order their diagnostics have always ranked.
|
|
110
|
+
const description = checkDescription(sentence, slot?.description);
|
|
111
|
+
const hidden = checkHidden(sentence, slot?.hidden);
|
|
112
|
+
const deprecated = checkDeprecated(sentence, slot?.deprecated);
|
|
113
|
+
const descriptors = new Map();
|
|
114
|
+
const extensions = storeCommandLayers({
|
|
115
|
+
descriptors,
|
|
116
|
+
layers: [slot?.extensions],
|
|
117
|
+
subject: layerOf(name),
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
state: freshState({
|
|
122
|
+
descriptors,
|
|
123
|
+
extensions,
|
|
124
|
+
facts: { deprecated, description, hidden },
|
|
125
|
+
name,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A declaration call after `action()` is an order fault the receiver's own earlier call makes
|
|
131
|
+
* certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
|
|
132
|
+
*/
|
|
133
|
+
function checkOpen(state, clause, remedy) {
|
|
134
|
+
if (state.action !== undefined) {
|
|
135
|
+
throw new DeclarationError(`${commandSentence(state.name)} ${clause} after its action. ${remedy}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** The remedy a late argument or option earns. */
|
|
139
|
+
const inputRemedy = 'Declare arguments and options before action().';
|
|
140
|
+
/** One Command declares arguments or attaches children, whichever call came second. */
|
|
141
|
+
function placementFault(name, argument, child) {
|
|
142
|
+
return new DeclarationError(`${commandSentence(name)} declares argument "${argument}" and attaches child "${child}". Move the argument into a child Command or remove the children.`);
|
|
143
|
+
}
|
|
144
|
+
/** The options one declaration list holds, in declaration order. */
|
|
145
|
+
function optionsOf(inputs) {
|
|
146
|
+
return inputs.filter((input) => input.kind === 'option');
|
|
147
|
+
}
|
|
148
|
+
/** The positional slot one argument fills. */
|
|
149
|
+
function slotOf(input) {
|
|
150
|
+
return {
|
|
151
|
+
input,
|
|
152
|
+
required: input.config.required === true,
|
|
153
|
+
variadic: input.config.variadic === true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One input's extension values, validated at the call that declares it, and the descriptors they
|
|
158
|
+
* name joined to the declaration's own.
|
|
159
|
+
*/
|
|
160
|
+
function recordInput(state, input) {
|
|
161
|
+
const { name } = state;
|
|
162
|
+
const descriptors = new Map(state.descriptors);
|
|
163
|
+
const record = buildExtensions({
|
|
164
|
+
declared: input.config.extensions,
|
|
165
|
+
descriptors,
|
|
166
|
+
subject: {
|
|
167
|
+
phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
|
|
168
|
+
sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
|
|
169
|
+
},
|
|
170
|
+
target: input.kind,
|
|
171
|
+
});
|
|
172
|
+
return { descriptors, records: new Map([...state.records, [input, record]]) };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every rule one argument answers at its own call: its name, a repeated name, children beside it,
|
|
176
|
+
* its place after the arguments before it, its own facts, and its declaration rules.
|
|
177
|
+
*/
|
|
178
|
+
function checkArgument(state, input) {
|
|
179
|
+
const { name } = state;
|
|
180
|
+
const subject = commandSubject(name);
|
|
181
|
+
if (!isDeclaredName(input.name)) {
|
|
182
|
+
throw new DeclarationError(`${commandSentence(name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
|
|
183
|
+
}
|
|
184
|
+
const declared = state.inputs.filter((entry) => entry.kind === 'argument');
|
|
185
|
+
if (declared.some((entry) => entry.name === input.name)) {
|
|
186
|
+
throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
187
|
+
}
|
|
188
|
+
const child = state.children[0];
|
|
189
|
+
if (child) {
|
|
190
|
+
throw placementFault(name, input.name, child.name);
|
|
191
|
+
}
|
|
192
|
+
const previous = declared.at(-1);
|
|
193
|
+
if (previous) {
|
|
194
|
+
checkSlotOrder(slotOf(previous), slotOf(input), subject);
|
|
195
|
+
}
|
|
196
|
+
const sentence = `${commandSentence(name)} argument "${input.name}"`;
|
|
197
|
+
checkDescription(sentence, input.config.description);
|
|
198
|
+
checkNoListingFacts(sentence, input.config);
|
|
199
|
+
checkNoArgumentBinding(sentence, input.config);
|
|
200
|
+
checkDeclarations([input]);
|
|
68
201
|
}
|
|
69
202
|
/** The declared value joins `args` under its literal name, typed by its own config. */
|
|
70
203
|
export function declareArgument(state, input) {
|
|
204
|
+
checkOpen(state, `declares argument "${input.name}"`, inputRemedy);
|
|
205
|
+
checkArgument(state, input);
|
|
206
|
+
const recorded = recordInput(state, input);
|
|
71
207
|
const previous = state.bind;
|
|
72
208
|
return {
|
|
73
209
|
...state,
|
|
210
|
+
...recorded,
|
|
74
211
|
bind: (values) => {
|
|
75
212
|
const bound = previous(values);
|
|
76
213
|
return { ...bound, args: { ...bound.args, ...values.argument(input) } };
|
|
77
214
|
},
|
|
78
215
|
inputs: [...state.inputs, input],
|
|
79
|
-
late: recordLate(state, [{ input, kind: 'input' }]),
|
|
80
216
|
};
|
|
81
217
|
}
|
|
82
|
-
/** The
|
|
83
|
-
|
|
218
|
+
/** The table a named Command's options meet at its own calls, before any Application holds it. */
|
|
219
|
+
const noGlobals = { names: new Map(), options: new Map(), variables: new Map() };
|
|
220
|
+
/**
|
|
221
|
+
* The declared value joins `options` under its literal name, typed by its own config. The root's
|
|
222
|
+
* options also meet the Application's globals table, which a named Command meets when its subtree
|
|
223
|
+
* joins an Application.
|
|
224
|
+
*/
|
|
225
|
+
export function declareOption(state, input, table = noGlobals) {
|
|
226
|
+
checkOpen(state, `declares option "${input.name}"`, inputRemedy);
|
|
227
|
+
const sentence = `${commandSentence(state.name)} option "${input.name}"`;
|
|
228
|
+
checkDescription(sentence, input.config.description);
|
|
229
|
+
checkHidden(sentence, input.config.hidden);
|
|
230
|
+
checkDeprecated(sentence, input.config.deprecated);
|
|
231
|
+
checkEnvBinding(sentence, input.config);
|
|
232
|
+
const recorded = recordInput(state, input);
|
|
233
|
+
checkLocalOptions([...optionsOf(state.inputs), input], table, commandSubject(state.name));
|
|
234
|
+
checkDeclarations([input]);
|
|
84
235
|
const previous = state.bind;
|
|
85
236
|
return {
|
|
86
237
|
...state,
|
|
238
|
+
...recorded,
|
|
87
239
|
bind: (values) => {
|
|
88
240
|
const bound = previous(values);
|
|
89
241
|
return { ...bound, options: { ...bound.options, ...values.option(input) } };
|
|
90
242
|
},
|
|
91
243
|
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
244
|
};
|
|
103
245
|
}
|
|
104
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* One call's names join the Command's aliases. A call that names none, which the types reject and
|
|
248
|
+
* a JavaScript author can still write, reports as the call it is.
|
|
249
|
+
*/
|
|
105
250
|
export function declareAlias(state, names) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
251
|
+
const { name } = state;
|
|
252
|
+
const first = names[0];
|
|
253
|
+
if (first === undefined) {
|
|
254
|
+
throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
|
|
255
|
+
}
|
|
256
|
+
// The call's own input is judged before the receiver's state.
|
|
257
|
+
// A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
|
|
258
|
+
for (const alias of names) {
|
|
259
|
+
checkAliasName(name, alias);
|
|
260
|
+
}
|
|
261
|
+
checkOpen(state, `declares alias "${first}"`, 'Declare aliases before action().');
|
|
262
|
+
const aliases = [...state.aliases];
|
|
263
|
+
for (const alias of names) {
|
|
264
|
+
if (alias === name) {
|
|
265
|
+
throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
|
|
266
|
+
}
|
|
267
|
+
if (aliases.includes(alias)) {
|
|
268
|
+
throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
|
|
269
|
+
}
|
|
270
|
+
aliases.push(alias);
|
|
271
|
+
}
|
|
272
|
+
return { ...state, aliases };
|
|
111
273
|
}
|
|
112
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Extension layers remain open after inputs and the action have been fixed. Each layer is validated
|
|
276
|
+
* at its own call, against every descriptor the declaration already names.
|
|
277
|
+
*/
|
|
113
278
|
export function declareExtensions(state, values) {
|
|
114
|
-
|
|
279
|
+
const descriptors = new Map(state.descriptors);
|
|
280
|
+
const layer = validateLayer({
|
|
281
|
+
declared: values,
|
|
282
|
+
descriptors,
|
|
283
|
+
subject: layerOf(state.name),
|
|
284
|
+
target: 'command',
|
|
285
|
+
});
|
|
286
|
+
return { ...state, descriptors, extensions: extendStore(state.extensions, layer) };
|
|
115
287
|
}
|
|
116
288
|
/**
|
|
117
289
|
* The same channel, read as the result the handler's own declaration names. The value one call
|
|
@@ -123,137 +295,201 @@ function declaredChannel(out) {
|
|
|
123
295
|
}
|
|
124
296
|
/**
|
|
125
297
|
* The handler is typed against the result its own declaration carries, and the state holds one
|
|
126
|
-
*
|
|
298
|
+
* action for every declaration, so the context the handler receives is read back at the call.
|
|
127
299
|
*/
|
|
128
300
|
export function declareAction(state, handler) {
|
|
129
|
-
|
|
130
|
-
|
|
301
|
+
if (state.action !== undefined) {
|
|
302
|
+
throw new DeclarationError(`${commandSentence(state.name)} has multiple actions. Register one action.`);
|
|
303
|
+
}
|
|
304
|
+
// The graph and the routed node stay getters, because a spread would build the graph on every run.
|
|
305
|
+
const stored = (context) => handler({
|
|
306
|
+
args: context.args,
|
|
307
|
+
get command() {
|
|
308
|
+
return context.command;
|
|
309
|
+
},
|
|
310
|
+
get graph() {
|
|
311
|
+
return context.graph;
|
|
312
|
+
},
|
|
313
|
+
host: context.host,
|
|
314
|
+
options: context.options,
|
|
315
|
+
out: declaredChannel(context.out),
|
|
316
|
+
passthrough: context.passthrough,
|
|
317
|
+
signal: context.signal,
|
|
318
|
+
style: context.style,
|
|
319
|
+
});
|
|
320
|
+
return { ...state, action: stored };
|
|
131
321
|
}
|
|
132
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* The result declaration, which closes both result calls. Every rule its own call can judge throws
|
|
324
|
+
* here; the empty record and the default wait for attach, because `views()` can still add keys.
|
|
325
|
+
*/
|
|
133
326
|
export function declareResult(state, kind, declaration) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
};
|
|
327
|
+
checkOpen(state, 'declares its result', 'Declare result() or rows() before action().');
|
|
328
|
+
const results = [...state.results, { kind, views: recordOf(declaration, 'views') }];
|
|
329
|
+
mergeResult(state.name, results);
|
|
330
|
+
return { ...state, results };
|
|
139
331
|
}
|
|
140
332
|
/** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
|
|
141
333
|
export function declareResultViews(state, replacements, options) {
|
|
142
|
-
|
|
143
|
-
...state,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
334
|
+
const results = [
|
|
335
|
+
...state.results,
|
|
336
|
+
{ default: recordOf(options, 'default'), kind: 'views', views: replacements },
|
|
337
|
+
];
|
|
338
|
+
mergeResult(state.name, results);
|
|
339
|
+
return { ...state, results };
|
|
149
340
|
}
|
|
150
|
-
/** One property of an authoring argument, read defensively:
|
|
341
|
+
/** One property of an authoring argument, read defensively: the rules report whatever it holds. */
|
|
151
342
|
function recordOf(declaration, key) {
|
|
152
343
|
return isPlainObject(declaration) ? declaration[key] : undefined;
|
|
153
344
|
}
|
|
154
|
-
/**
|
|
155
|
-
|
|
345
|
+
/** The parent a declaration is, as attach reads it. */
|
|
346
|
+
function parentOf(state) {
|
|
156
347
|
return {
|
|
157
|
-
|
|
158
|
-
children:
|
|
159
|
-
|
|
348
|
+
argument: state.inputs.find((input) => input.kind === 'argument')?.name,
|
|
349
|
+
children: state.children,
|
|
350
|
+
hasAction: state.action !== undefined,
|
|
351
|
+
name: state.name,
|
|
160
352
|
};
|
|
161
353
|
}
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
354
|
+
/**
|
|
355
|
+
* One parent's namespace, which every canonical name and alias under it shares. The rule reads the
|
|
356
|
+
* same whichever sibling the author attached first.
|
|
357
|
+
*/
|
|
358
|
+
function checkSiblings(parent, child) {
|
|
359
|
+
const sentence = commandSentence(parent.name);
|
|
360
|
+
const { children } = parent;
|
|
361
|
+
if (children.some((entry) => entry.name === child.name)) {
|
|
362
|
+
throw new DeclarationError(`${sentence} attaches two children named "${child.name}". Rename or remove one.`);
|
|
363
|
+
}
|
|
364
|
+
for (const alias of child.node.declared.aliases) {
|
|
365
|
+
if (children.some((entry) => entry.name === alias)) {
|
|
366
|
+
throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
|
|
367
|
+
}
|
|
368
|
+
const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
|
|
369
|
+
if (owner) {
|
|
370
|
+
throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}". Rename or remove one.`);
|
|
371
|
+
}
|
|
168
372
|
}
|
|
169
|
-
|
|
170
|
-
|
|
373
|
+
const aliased = children.find((entry) => entry.node.declared.aliases.includes(child.name));
|
|
374
|
+
if (aliased) {
|
|
375
|
+
throw new DeclarationError(`${sentence} attaches child "${aliased.name}" with alias "${child.name}", which is also the name of child "${child.name}". Rename or remove one.`);
|
|
171
376
|
}
|
|
172
|
-
|
|
173
|
-
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* A child at the cap holds no children. Attach reads the child at the shallowest level its parent
|
|
380
|
+
* can sit at, and the Application's join reads every node at its level below the root.
|
|
381
|
+
*/
|
|
382
|
+
function checkNesting(parent, child, level) {
|
|
383
|
+
if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
|
|
384
|
+
throw new DeclarationError(`${commandSentence(parent)} attaches child "${child.name}", which has children of its own. Nest Commands at most ${nestingCap.words} levels below the root.`);
|
|
174
385
|
}
|
|
175
|
-
|
|
176
|
-
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A Command without an action is a group, and routing sends an invocation on to one of its
|
|
389
|
+
* children. A group with no children receives an invocation no handler can answer, and a local
|
|
390
|
+
* option on a group reaches no handler either, because locals never inherit.
|
|
391
|
+
*/
|
|
392
|
+
function checkGroup(state) {
|
|
393
|
+
const { name } = state;
|
|
394
|
+
if (state.children.length === 0) {
|
|
395
|
+
throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
|
|
177
396
|
}
|
|
178
|
-
|
|
179
|
-
|
|
397
|
+
const option = state.inputs.find((input) => input.kind === 'option');
|
|
398
|
+
if (option) {
|
|
399
|
+
throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
|
|
180
400
|
}
|
|
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
401
|
}
|
|
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]);
|
|
402
|
+
/**
|
|
403
|
+
* The rules a finished Command answers, which attach applies to a child and build to the root: a
|
|
404
|
+
* result needs an action, its merged views record names at least one view and its default, and a
|
|
405
|
+
* Command without an action is a group. It answers with the resolved result.
|
|
406
|
+
*/
|
|
407
|
+
function checkFinished(declared, hasAction) {
|
|
408
|
+
const result = buildResult(declared, hasAction);
|
|
409
|
+
if (!hasAction) {
|
|
410
|
+
checkGroup(declared);
|
|
197
411
|
}
|
|
198
|
-
return
|
|
412
|
+
return result;
|
|
199
413
|
}
|
|
200
414
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
415
|
+
* The one attach operation `Command.command()`, `Application.command()`, and a plugin's
|
|
416
|
+
* `commands` list share. A Command is an immutable value, so the child is final here: it is checked
|
|
417
|
+
* as a finished Command, against the parent's current children, and against the nesting cap from
|
|
418
|
+
* the shallowest level the parent can sit at.
|
|
203
419
|
*/
|
|
204
|
-
function
|
|
205
|
-
const { name } =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
420
|
+
export function attach(parent, node) {
|
|
421
|
+
const { name } = node;
|
|
422
|
+
if (parent.hasAction) {
|
|
423
|
+
throw new DeclarationError(`${commandSentence(parent.name)} attaches child "${name}" after its action. Attach children before action().`);
|
|
424
|
+
}
|
|
425
|
+
if (parent.argument !== undefined) {
|
|
426
|
+
throw placementFault(parent.name, parent.argument, name);
|
|
427
|
+
}
|
|
428
|
+
checkFinished(node.declared, node.hasAction);
|
|
429
|
+
const child = { name, node };
|
|
430
|
+
checkSiblings(parent, child);
|
|
431
|
+
checkNesting(parent.name, child, parentLevel(parent.name) + 1);
|
|
432
|
+
return child;
|
|
433
|
+
}
|
|
434
|
+
/** The handle behind the value one `command()` call received; anything else is a declaration error. */
|
|
435
|
+
export function childNode(parent, child) {
|
|
436
|
+
const node = commandNode(child);
|
|
437
|
+
if (!node) {
|
|
438
|
+
throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
|
|
223
439
|
}
|
|
224
|
-
return
|
|
440
|
+
return node;
|
|
441
|
+
}
|
|
442
|
+
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
443
|
+
function attachChild(state, child) {
|
|
444
|
+
const attached = attach(parentOf(state), childNode(state.name, child));
|
|
445
|
+
return { ...state, children: [...state.children, attached] };
|
|
225
446
|
}
|
|
226
447
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
448
|
+
* Walks one subtree joining an Application, once, for the rules only the Application can judge: one
|
|
449
|
+
* Command value reached through two paths, two distinct descriptors under one identity, a local
|
|
450
|
+
* option that meets a global or plugin option's key, spelling, or variable, and the nesting cap
|
|
451
|
+
* measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
|
|
452
|
+
* At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
|
|
453
|
+
* here fires only once the cap rises: attach reads one level, and only this walk knows each level.
|
|
230
454
|
*/
|
|
231
|
-
function
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
455
|
+
function joinSubtree(scope, child, { level, parent }) {
|
|
456
|
+
const { name, node } = child;
|
|
457
|
+
checkNesting(parent, child, level);
|
|
458
|
+
if (scope.owners.has(node)) {
|
|
459
|
+
throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(scope.owners.get(node) ?? null)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
|
|
460
|
+
}
|
|
461
|
+
scope.owners.set(node, parent);
|
|
462
|
+
for (const descriptor of node.declared.descriptors.values()) {
|
|
463
|
+
registerDescriptor(scope.descriptors, descriptor);
|
|
464
|
+
}
|
|
465
|
+
checkLocalOptions(optionsOf(node.declared.inputs), scope.table, commandSubject(name));
|
|
466
|
+
for (const entry of node.declared.children) {
|
|
467
|
+
joinSubtree(scope, entry, { level: level + 1, parent: name });
|
|
468
|
+
}
|
|
243
469
|
}
|
|
244
470
|
/**
|
|
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.
|
|
471
|
+
* Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
|
|
472
|
+
* registers are copies: the caller commits the owners, and the returned root holds the descriptors.
|
|
249
473
|
*/
|
|
250
|
-
function
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
474
|
+
export function attachToRoot(state, node, scope) {
|
|
475
|
+
const attached = attach(parentOf(state), node);
|
|
476
|
+
joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent: state.name });
|
|
477
|
+
return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
|
|
478
|
+
}
|
|
479
|
+
/** Every attached Command's local options against a globals table that has just grown. */
|
|
480
|
+
function checkAttachedOptions(table, children) {
|
|
481
|
+
for (const { name, node } of children) {
|
|
482
|
+
checkLocalOptions(optionsOf(node.declared.inputs), table, commandSubject(name));
|
|
483
|
+
checkAttachedOptions(table, node.declared.children);
|
|
254
484
|
}
|
|
255
|
-
|
|
256
|
-
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* A declaration's own options and every attached Command's, against a globals table that has just
|
|
488
|
+
* grown. The table's new option reads as the other side of any collision.
|
|
489
|
+
*/
|
|
490
|
+
export function checkDeclaredOptions(state, table) {
|
|
491
|
+
checkLocalOptions(optionsOf(state.inputs), table, commandSubject(state.name));
|
|
492
|
+
checkAttachedOptions(table, state.children);
|
|
257
493
|
}
|
|
258
494
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
259
495
|
function checkSlotOrder(slot, next, subject) {
|
|
@@ -266,6 +502,10 @@ function checkSlotOrder(slot, next, subject) {
|
|
|
266
502
|
: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}. Declare an optional argument last.`);
|
|
267
503
|
}
|
|
268
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* The positional slots one declaration holds. Every authored argument answered these rules at its
|
|
507
|
+
* own call, so they throw here only for an argument a lifecycle hook declared.
|
|
508
|
+
*/
|
|
269
509
|
function collectArguments(state, subject) {
|
|
270
510
|
const slots = [];
|
|
271
511
|
const seen = new Set();
|
|
@@ -277,11 +517,7 @@ function collectArguments(state, subject) {
|
|
|
277
517
|
throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
278
518
|
}
|
|
279
519
|
seen.add(input.name);
|
|
280
|
-
slots.push(
|
|
281
|
-
input,
|
|
282
|
-
required: input.config.required === true,
|
|
283
|
-
variadic: input.config.variadic === true,
|
|
284
|
-
});
|
|
520
|
+
slots.push(slotOf(input));
|
|
285
521
|
}
|
|
286
522
|
for (let index = 0; index + 1 < slots.length; index += 1) {
|
|
287
523
|
const slot = slots[index];
|
|
@@ -293,47 +529,8 @@ function collectArguments(state, subject) {
|
|
|
293
529
|
return slots;
|
|
294
530
|
}
|
|
295
531
|
/**
|
|
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.
|
|
532
|
+
* A view name is a bare token the way an argument or option name is, and never an array index,
|
|
533
|
+
* because an integer-like key does not keep the position the author gave it.
|
|
337
534
|
*/
|
|
338
535
|
function isViewName(name) {
|
|
339
536
|
return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
|
|
@@ -386,15 +583,14 @@ function resultView(declaration, name, entry) {
|
|
|
386
583
|
throw new DeclarationError(`${sentence} names view "${name}" with a value that is not a view. Supply a view with render or a row view with row.`);
|
|
387
584
|
}
|
|
388
585
|
/**
|
|
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.
|
|
586
|
+
* Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
|
|
587
|
+
* it made them. The merged record is what the rules read: a later `views()` call replaces a key in
|
|
588
|
+
* place and appends a new one, so each name keeps the position the call that first named it gave
|
|
589
|
+
* it. A `default` once named persists through later calls that name none.
|
|
394
590
|
*/
|
|
395
|
-
function
|
|
396
|
-
const sentence = commandSentence(
|
|
397
|
-
const declarations =
|
|
591
|
+
function mergeResult(name, results) {
|
|
592
|
+
const sentence = commandSentence(name);
|
|
593
|
+
const declarations = results.filter((call) => call.kind !== 'views');
|
|
398
594
|
if (declarations.length > 1) {
|
|
399
595
|
throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
|
|
400
596
|
}
|
|
@@ -402,28 +598,41 @@ function buildResult(state, hasAction) {
|
|
|
402
598
|
// A `views()` call reshapes a result's views, so one with no result reshapes nothing.
|
|
403
599
|
// The types publish the call where a result is carried, so this reaches a JavaScript author.
|
|
404
600
|
if (!declaration) {
|
|
405
|
-
if (
|
|
601
|
+
if (results.length > 0) {
|
|
406
602
|
throw new DeclarationError(`${sentence} reshapes its views and declares no result. Declare result() or rows() before action().`);
|
|
407
603
|
}
|
|
408
604
|
return undefined;
|
|
409
605
|
}
|
|
410
|
-
if (!hasAction) {
|
|
411
|
-
throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
|
|
412
|
-
}
|
|
413
606
|
const views = new Map();
|
|
414
607
|
let selected = undefined;
|
|
415
|
-
for (const call of
|
|
416
|
-
for (const [
|
|
417
|
-
const view = resultView({ kind: declaration.kind, sentence },
|
|
418
|
-
if (!isViewName(
|
|
419
|
-
throw new DeclarationError(`${sentence} names view "${
|
|
608
|
+
for (const call of results) {
|
|
609
|
+
for (const [key, entry] of recordEntries(call.views)) {
|
|
610
|
+
const view = resultView({ kind: declaration.kind, sentence }, key, entry);
|
|
611
|
+
if (!isViewName(key)) {
|
|
612
|
+
throw new DeclarationError(`${sentence} names view "${key}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
|
|
420
613
|
}
|
|
421
|
-
views.set(
|
|
614
|
+
views.set(key, view);
|
|
422
615
|
}
|
|
423
616
|
if (call.kind === 'views') {
|
|
424
617
|
selected = selectedKey(call.default) ?? selected;
|
|
425
618
|
}
|
|
426
619
|
}
|
|
620
|
+
return { kind: declaration.kind, selected, views };
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The finished result: the merged record, which needs an action, at least one view, and a default
|
|
624
|
+
* that names one of them. The first key answers until one is named.
|
|
625
|
+
*/
|
|
626
|
+
function buildResult(declared, hasAction) {
|
|
627
|
+
const merged = mergeResult(declared.name, declared.results);
|
|
628
|
+
if (!merged) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
const sentence = commandSentence(declared.name);
|
|
632
|
+
if (!hasAction) {
|
|
633
|
+
throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
|
|
634
|
+
}
|
|
635
|
+
const { selected, views } = merged;
|
|
427
636
|
const first = views.keys().next();
|
|
428
637
|
if (first.done === true) {
|
|
429
638
|
throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
|
|
@@ -431,7 +640,7 @@ function buildResult(state, hasAction) {
|
|
|
431
640
|
if (selected !== undefined && !views.has(selected)) {
|
|
432
641
|
throw new DeclarationError(`${sentence} selects default view "${selected}", which it does not name. Name the view or select a named one.`);
|
|
433
642
|
}
|
|
434
|
-
return { default: selected ?? first.value, kind:
|
|
643
|
+
return { default: selected ?? first.value, kind: merged.kind, views };
|
|
435
644
|
}
|
|
436
645
|
/** The values one build made, so a value a hook returns is one of them and never a forged shape. */
|
|
437
646
|
const attachments = new WeakMap();
|
|
@@ -476,6 +685,10 @@ class AttachedCommandValue {
|
|
|
476
685
|
get result() {
|
|
477
686
|
return this.#result;
|
|
478
687
|
}
|
|
688
|
+
/** Published on first read and shared by every value whose store is unchanged. */
|
|
689
|
+
get extensions() {
|
|
690
|
+
return publishStore(this.#state.extensions);
|
|
691
|
+
}
|
|
479
692
|
argument(name, config) {
|
|
480
693
|
return this.#declare({ config: captureConfig(config), kind: 'argument', name });
|
|
481
694
|
}
|
|
@@ -491,8 +704,24 @@ class AttachedCommandValue {
|
|
|
491
704
|
const { declared } = this.#state;
|
|
492
705
|
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
493
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* The values are validated here, at the call, so the next read of `extensions` holds them and
|
|
709
|
+
* build never validates them again. A rejected value throws from the call and adds nothing.
|
|
710
|
+
*/
|
|
494
711
|
extend(...values) {
|
|
495
|
-
|
|
712
|
+
const state = this.#state;
|
|
713
|
+
const registry = new Map(state.registry);
|
|
714
|
+
const layer = validateLayer({
|
|
715
|
+
declared: values,
|
|
716
|
+
descriptors: registry,
|
|
717
|
+
subject: state.subject,
|
|
718
|
+
target: 'command',
|
|
719
|
+
});
|
|
720
|
+
return new _a({
|
|
721
|
+
...state,
|
|
722
|
+
extensions: extendStore(state.extensions, layer),
|
|
723
|
+
registry,
|
|
724
|
+
});
|
|
496
725
|
}
|
|
497
726
|
/** One input the running hook declared, which the Command's own names now hold. */
|
|
498
727
|
#declare(input) {
|
|
@@ -538,25 +767,31 @@ function attachOnce(value, hook, named) {
|
|
|
538
767
|
* Every installed plugin's hook over one Command, in installation order, each receiving what the
|
|
539
768
|
* previous returned. The calls they made travel back to the declaration the remaining rules read.
|
|
540
769
|
*/
|
|
541
|
-
function runAttachHooks(
|
|
770
|
+
function runAttachHooks(start, facts, plugins) {
|
|
771
|
+
const { declared, extensions, layer, registry } = start;
|
|
542
772
|
const subject = commandSubject(declared.name);
|
|
543
773
|
const { lineage } = facts;
|
|
544
|
-
let progress = { calls: [], declared };
|
|
774
|
+
let progress = { calls: [], declared, extensions, registry };
|
|
545
775
|
for (const installed of plugins) {
|
|
546
776
|
const hook = installed.onCommandAttach;
|
|
547
777
|
if (hook) {
|
|
548
778
|
const identity = installed.identity;
|
|
549
|
-
const value = new AttachedCommandValue({ ...progress, ...facts, identity });
|
|
779
|
+
const value = new AttachedCommandValue({ ...progress, ...facts, identity, subject: layer });
|
|
550
780
|
const state = attachOnce(value, hook, { identity, lineage, subject });
|
|
551
|
-
progress = {
|
|
781
|
+
progress = {
|
|
782
|
+
calls: state.calls,
|
|
783
|
+
declared: state.declared,
|
|
784
|
+
extensions: state.extensions,
|
|
785
|
+
registry: state.registry,
|
|
786
|
+
};
|
|
552
787
|
}
|
|
553
788
|
}
|
|
554
|
-
return progress
|
|
789
|
+
return progress;
|
|
555
790
|
}
|
|
556
791
|
/**
|
|
557
792
|
* One input a hook declared. It joins the values an action reads at run time and the declaration's
|
|
558
|
-
* types not at all, and it
|
|
559
|
-
* closures `action()` applies.
|
|
793
|
+
* types not at all, and it skips the order checks an authoring call makes, because a hook's calls
|
|
794
|
+
* are exempt from the closures `action()` applies.
|
|
560
795
|
*/
|
|
561
796
|
function declareHookInput(state, input) {
|
|
562
797
|
const previous = state.bind;
|
|
@@ -575,15 +810,10 @@ function declareHookInput(state, input) {
|
|
|
575
810
|
function applyAttachCalls(state, calls) {
|
|
576
811
|
let next = state;
|
|
577
812
|
for (const call of calls) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
next = { ...next, results: [...next.results, call.call] };
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
next = declareExtensions(next, call.values);
|
|
586
|
-
}
|
|
813
|
+
next =
|
|
814
|
+
call.kind === 'input'
|
|
815
|
+
? declareHookInput(next, call.input)
|
|
816
|
+
: { ...next, results: [...next.results, call.call] };
|
|
587
817
|
}
|
|
588
818
|
return next;
|
|
589
819
|
}
|
|
@@ -706,17 +936,23 @@ function checkAttachedInputs(declared, attached, globals) {
|
|
|
706
936
|
}
|
|
707
937
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
708
938
|
function bindDispatch(state, action, globals) {
|
|
709
|
-
return ({ host, out, passthrough, signal, style, values }) => {
|
|
939
|
+
return ({ command, graph, host, out, passthrough, signal, style, values }) => {
|
|
710
940
|
const bound = state.bind(values);
|
|
711
941
|
// Last resort: no typed path exists.
|
|
712
942
|
// The graph erases the binder's generic relationship.
|
|
713
943
|
// It holds because attachment checks the global output requirement.
|
|
714
|
-
//
|
|
944
|
+
// The call or the attach that met both options rejected a collision between a global and a local.
|
|
715
945
|
// This binder returns the Application's validated globals alone.
|
|
716
946
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
717
947
|
const globalOptions = globals.bind(values);
|
|
718
948
|
return action({
|
|
719
949
|
args: bound.args,
|
|
950
|
+
get command() {
|
|
951
|
+
return command();
|
|
952
|
+
},
|
|
953
|
+
get graph() {
|
|
954
|
+
return graph();
|
|
955
|
+
},
|
|
720
956
|
host,
|
|
721
957
|
options: { ...globalOptions, ...bound.options },
|
|
722
958
|
out,
|
|
@@ -726,7 +962,10 @@ function bindDispatch(state, action, globals) {
|
|
|
726
962
|
});
|
|
727
963
|
};
|
|
728
964
|
}
|
|
729
|
-
/**
|
|
965
|
+
/**
|
|
966
|
+
* The own facts and extension values of each input a lifecycle hook declared, which the calls that
|
|
967
|
+
* declared the author's inputs already checked.
|
|
968
|
+
*/
|
|
730
969
|
function checkInputFacts(inputs, command, context) {
|
|
731
970
|
const { name, subject } = command;
|
|
732
971
|
for (const input of inputs) {
|
|
@@ -734,10 +973,12 @@ function checkInputFacts(inputs, command, context) {
|
|
|
734
973
|
checkDescription(sentence, input.config.description);
|
|
735
974
|
if (input.kind === 'argument') {
|
|
736
975
|
checkNoListingFacts(sentence, input.config);
|
|
976
|
+
checkNoArgumentBinding(sentence, input.config);
|
|
737
977
|
}
|
|
738
978
|
else {
|
|
739
979
|
checkHidden(sentence, input.config.hidden);
|
|
740
980
|
checkDeprecated(sentence, input.config.deprecated);
|
|
981
|
+
checkEnvBinding(sentence, input.config);
|
|
741
982
|
}
|
|
742
983
|
context.extensions.set(input, buildExtensions({
|
|
743
984
|
declared: input.config.extensions,
|
|
@@ -750,86 +991,73 @@ function checkInputFacts(inputs, command, context) {
|
|
|
750
991
|
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
751
992
|
function checkArgumentPlacement(name, slot, child) {
|
|
752
993
|
if (slot && child) {
|
|
753
|
-
throw
|
|
994
|
+
throw placementFault(name, slot.input.name, child.name);
|
|
754
995
|
}
|
|
755
996
|
}
|
|
756
|
-
/**
|
|
997
|
+
/**
|
|
998
|
+
* Compiles one declaration for dispatch against the shared globals table. Every authored rule threw
|
|
999
|
+
* at the call or the attach that first held its data, so what can still fail here is the root's
|
|
1000
|
+
* finished-Command rules, the root never being attached, and whatever the lifecycle hooks
|
|
1001
|
+
* contributed.
|
|
1002
|
+
*/
|
|
757
1003
|
export function buildCommand(state, context) {
|
|
758
|
-
const {
|
|
1004
|
+
const { action, facts, name } = state;
|
|
759
1005
|
const { globals } = context;
|
|
760
1006
|
const subject = commandSubject(name);
|
|
761
|
-
const layer =
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
765
|
-
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
766
|
-
const declaredExtensions = buildCommandExtensions({
|
|
767
|
-
descriptors: context.descriptors,
|
|
768
|
-
layers: state.extensions,
|
|
769
|
-
subject: layer,
|
|
770
|
-
});
|
|
771
|
-
// Each declaration's own facts, in authoring order, before the rules that pair declarations.
|
|
772
|
-
checkInputFacts(state.inputs, { name, subject }, context);
|
|
773
|
-
const attached = collectChildren(state);
|
|
774
|
-
checkDeclarationOrder(state);
|
|
775
|
-
const aliases = collectAliases(state);
|
|
776
|
-
const declaredSlots = collectArguments(state, subject);
|
|
777
|
-
checkArgumentPlacement(name, declaredSlots[0], attached[0]);
|
|
778
|
-
if (actions.length > 1) {
|
|
779
|
-
throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
|
|
1007
|
+
const layer = layerOf(name);
|
|
1008
|
+
for (const [input, record] of state.records) {
|
|
1009
|
+
context.extensions.set(input, record);
|
|
780
1010
|
}
|
|
781
|
-
const
|
|
1011
|
+
const declaredSlots = collectArguments(state, subject);
|
|
782
1012
|
const hasAction = action !== undefined;
|
|
783
|
-
const declaredResult = buildResult(state, hasAction);
|
|
784
1013
|
/**
|
|
785
|
-
* The hooks run once the author's declaration is complete and
|
|
786
|
-
* reads an exact record
|
|
1014
|
+
* The hooks run once the author's declaration is complete, and each value a hook receives resolves
|
|
1015
|
+
* its result, so a hook reads an exact record. Every rule below reads what the hooks returned.
|
|
787
1016
|
*/
|
|
788
1017
|
// One token per Command per build, which binds a hook's return to the value it received.
|
|
789
1018
|
const lineage = {};
|
|
790
|
-
const
|
|
1019
|
+
const progress = runAttachHooks({ declared: state, extensions: state.extensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
|
|
1020
|
+
const { calls } = progress;
|
|
1021
|
+
// The descriptors the returned value's extensions registered join the build's registry.
|
|
1022
|
+
for (const [identity, descriptor] of progress.registry) {
|
|
1023
|
+
context.descriptors.set(identity, descriptor);
|
|
1024
|
+
}
|
|
791
1025
|
const hooked = applyAttachCalls(state, calls);
|
|
792
1026
|
const hookInputs = calls.filter((call) => call.kind === 'input');
|
|
793
1027
|
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
794
1028
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
795
1029
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
796
1030
|
checkAttachedInputs(hooked, hookInputs, globals);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
descriptors: context.descriptors,
|
|
800
|
-
layers: hooked.extensions,
|
|
801
|
-
subject: layer,
|
|
802
|
-
})
|
|
803
|
-
: declaredExtensions;
|
|
1031
|
+
// Every value was validated once, the author's at each call and each hook's at its `extend()`.
|
|
1032
|
+
const extensions = publishStore(progress.extensions);
|
|
804
1033
|
const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
|
|
805
|
-
checkArgumentPlacement(name, slots[0],
|
|
806
|
-
const result =
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const options = compileLocalOptions(hooked, globals, subject);
|
|
1034
|
+
checkArgumentPlacement(name, slots[0], state.children[0]);
|
|
1035
|
+
const result = checkFinished(hooked, hasAction);
|
|
1036
|
+
const options = checkLocalOptions(optionsOf(hooked.inputs), globals, subject);
|
|
1037
|
+
// A hook's erased calls answer the declaration rules an authored call answers at the call.
|
|
1038
|
+
checkDeclarations(hookInputs.map((call) => call.input));
|
|
811
1039
|
const children = new Map();
|
|
812
1040
|
const routes = new Map();
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1041
|
+
for (const child of state.children) {
|
|
1042
|
+
const routed = {
|
|
1043
|
+
command: child.node.build({ ...context, path: [...context.path, child.name] }),
|
|
1044
|
+
name: child.name,
|
|
1045
|
+
};
|
|
817
1046
|
children.set(routed.name, routed.command);
|
|
818
1047
|
routes.set(routed.name, routed);
|
|
819
1048
|
for (const alias of routed.command.aliases) {
|
|
820
|
-
claim(routed.name, alias);
|
|
821
1049
|
routes.set(alias, routed);
|
|
822
1050
|
}
|
|
823
1051
|
}
|
|
824
1052
|
return {
|
|
825
|
-
aliases,
|
|
1053
|
+
aliases: state.aliases,
|
|
826
1054
|
arguments: slots,
|
|
827
1055
|
children,
|
|
828
|
-
deprecated,
|
|
829
|
-
description,
|
|
1056
|
+
deprecated: facts.deprecated,
|
|
1057
|
+
description: facts.description,
|
|
830
1058
|
dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
|
|
831
1059
|
extensions,
|
|
832
|
-
hidden,
|
|
1060
|
+
hidden: facts.hidden,
|
|
833
1061
|
inputs: hooked.inputs,
|
|
834
1062
|
name,
|
|
835
1063
|
options,
|
|
@@ -837,30 +1065,31 @@ export function buildCommand(state, context) {
|
|
|
837
1065
|
routes,
|
|
838
1066
|
};
|
|
839
1067
|
}
|
|
840
|
-
/**
|
|
841
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* The globals table compiles once per build and the whole graph shares it. The root's registry
|
|
1070
|
+
* holds every descriptor the Application names, so a hook's `extend()` call meets all of them.
|
|
1071
|
+
*/
|
|
1072
|
+
export function buildGraph(root, globals, plugins) {
|
|
1073
|
+
const extensions = new Map([
|
|
1074
|
+
...globals.records,
|
|
1075
|
+
...plugins.flatMap((installed) => [...installed.records]),
|
|
1076
|
+
]);
|
|
842
1077
|
const context = {
|
|
843
|
-
descriptors:
|
|
844
|
-
extensions
|
|
845
|
-
globals: buildGlobals(globals,
|
|
846
|
-
owners: new Map(),
|
|
1078
|
+
descriptors: new Map(root.descriptors),
|
|
1079
|
+
extensions,
|
|
1080
|
+
globals: buildGlobals(globals, plugins),
|
|
847
1081
|
path: [],
|
|
848
|
-
plugins
|
|
849
|
-
};
|
|
850
|
-
return {
|
|
851
|
-
extensions: context.extensions,
|
|
852
|
-
globals: context.globals,
|
|
853
|
-
root: buildCommand(root, context),
|
|
1082
|
+
plugins,
|
|
854
1083
|
};
|
|
1084
|
+
return { extensions, globals: context.globals, root: buildCommand(root, context) };
|
|
855
1085
|
}
|
|
856
1086
|
export class CommandBuilder {
|
|
1087
|
+
#name;
|
|
857
1088
|
#state;
|
|
858
|
-
constructor(state) {
|
|
1089
|
+
constructor(name, state) {
|
|
1090
|
+
this.#name = name;
|
|
859
1091
|
this.#state = state;
|
|
860
|
-
nodes.set(this,
|
|
861
|
-
}
|
|
862
|
-
get name() {
|
|
863
|
-
return this.#state.name;
|
|
1092
|
+
nodes.set(this, nodeHandle(name, state));
|
|
864
1093
|
}
|
|
865
1094
|
argument(name, config) {
|
|
866
1095
|
const input = {
|
|
@@ -868,7 +1097,7 @@ export class CommandBuilder {
|
|
|
868
1097
|
kind: 'argument',
|
|
869
1098
|
name,
|
|
870
1099
|
};
|
|
871
|
-
return this
|
|
1100
|
+
return this.#derive(declareArgument(this.#state, input));
|
|
872
1101
|
}
|
|
873
1102
|
option(name, config) {
|
|
874
1103
|
const input = {
|
|
@@ -876,66 +1105,61 @@ export class CommandBuilder {
|
|
|
876
1105
|
kind: 'option',
|
|
877
1106
|
name,
|
|
878
1107
|
};
|
|
879
|
-
return this
|
|
1108
|
+
return this.#derive(declareOption(this.#state, input));
|
|
880
1109
|
}
|
|
881
1110
|
/**
|
|
882
|
-
* Aliases are other
|
|
1111
|
+
* Aliases are other portable names that route to this Command. They invalidate no call, and the
|
|
883
1112
|
* tuple rest parameter rejects a call that names none.
|
|
884
1113
|
*/
|
|
885
1114
|
alias(...names) {
|
|
886
|
-
return this
|
|
1115
|
+
return this.#derive(declareAlias(this.#state, names));
|
|
887
1116
|
}
|
|
888
1117
|
/** A child arrives in any type state, because its own action is the call that finished it. */
|
|
889
1118
|
command(child) {
|
|
890
|
-
return this
|
|
1119
|
+
return this.#derive(attachChild(this.#state, child));
|
|
891
1120
|
}
|
|
892
1121
|
/**
|
|
893
1122
|
* The value this Command produces for its consumer. The type argument is stated by the author,
|
|
894
1123
|
* so the views record states no type of its own and an omitted argument names none either.
|
|
895
1124
|
*/
|
|
896
1125
|
result(declaration) {
|
|
897
|
-
return this
|
|
1126
|
+
return this.#derive(declareResult(this.#state, 'value', declaration));
|
|
898
1127
|
}
|
|
899
1128
|
/** The same declaration over a sequence, whose type argument is one row. */
|
|
900
1129
|
rows(declaration) {
|
|
901
|
-
return this
|
|
1130
|
+
return this.#derive(declareResult(this.#state, 'rows', declaration));
|
|
902
1131
|
}
|
|
903
1132
|
/**
|
|
904
1133
|
* Views after the fact. It merges by key, so an existing name is replaced in place and a new one
|
|
905
1134
|
* is appended, and `default` names the key core renders when nothing selects another.
|
|
906
1135
|
*/
|
|
907
1136
|
views(replacements, options) {
|
|
908
|
-
return this
|
|
1137
|
+
return this.#derive(declareResultViews(this.#state, replacements, options));
|
|
909
1138
|
}
|
|
910
1139
|
/** The action closes input authoring; `extend()` remains outside this state transition. */
|
|
911
1140
|
action(handler) {
|
|
912
|
-
return this
|
|
1141
|
+
return this.#derive(declareAction(this.#state, handler));
|
|
913
1142
|
}
|
|
914
1143
|
extend(...values) {
|
|
915
|
-
return this
|
|
916
|
-
}
|
|
917
|
-
build(context) {
|
|
918
|
-
return buildCommand(this.#state, context);
|
|
1144
|
+
return this.#derive(declareExtensions(this.#state, values));
|
|
919
1145
|
}
|
|
920
1146
|
/**
|
|
921
1147
|
* The same runtime value in the state the calling method's return type names. Each call states
|
|
922
1148
|
* its own transition, and the declared result travels with it unless the call replaces it.
|
|
923
1149
|
*/
|
|
924
|
-
derive(state) {
|
|
925
|
-
return new
|
|
1150
|
+
#derive(state) {
|
|
1151
|
+
return new _b(this.#name, state);
|
|
926
1152
|
}
|
|
927
1153
|
}
|
|
928
|
-
|
|
1154
|
+
_b = CommandBuilder;
|
|
1155
|
+
/**
|
|
1156
|
+
* The constructor uses the Application registration; public type defaults stay library-neutral.
|
|
1157
|
+
* Every rule on the name and the options slot throws before the value exists.
|
|
1158
|
+
*/
|
|
929
1159
|
class CommandDeclaration extends CommandBuilder {
|
|
930
1160
|
constructor(name, options) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
description: options?.description,
|
|
934
|
-
extensions: options?.extensions,
|
|
935
|
-
hidden: options?.hidden,
|
|
936
|
-
name,
|
|
937
|
-
options,
|
|
938
|
-
}));
|
|
1161
|
+
const declared = namedState(name, options);
|
|
1162
|
+
super(declared.name, declared.state);
|
|
939
1163
|
}
|
|
940
1164
|
}
|
|
941
1165
|
/** The public constructor takes a name and one options object, as the Application does. */
|
|
@@ -955,14 +1179,20 @@ export function collectInputs(command) {
|
|
|
955
1179
|
function candidatesOf(command) {
|
|
956
1180
|
return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
|
|
957
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Whether a token names one of the Command's children: the Command has children and the token is
|
|
1184
|
+
* no option token. Routing and `locate` read a bare word through this rule.
|
|
1185
|
+
*/
|
|
1186
|
+
export function readsAsChild(command, token) {
|
|
1187
|
+
return command.children.size > 0 && !isOptionToken(token);
|
|
1188
|
+
}
|
|
958
1189
|
/** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
|
|
959
1190
|
export function route(root, tokens) {
|
|
960
1191
|
let command = root;
|
|
961
1192
|
const path = [];
|
|
962
1193
|
let index = 0;
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (token === undefined || token === '--' || token.startsWith('-')) {
|
|
1194
|
+
for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
|
|
1195
|
+
if (!readsAsChild(command, token)) {
|
|
966
1196
|
break;
|
|
967
1197
|
}
|
|
968
1198
|
const child = command.routes.get(token);
|
|
@@ -983,29 +1213,33 @@ export function route(root, tokens) {
|
|
|
983
1213
|
*/
|
|
984
1214
|
function bindArguments(command, path, positionals) {
|
|
985
1215
|
const values = new Map();
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (slot
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1216
|
+
for (const [position, token] of positionals.entries()) {
|
|
1217
|
+
const slot = argumentSlot(command.arguments, position);
|
|
1218
|
+
if (!slot) {
|
|
1219
|
+
throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
|
|
1220
|
+
}
|
|
1221
|
+
// An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
|
|
1222
|
+
const bound = values.get(slot.input);
|
|
1223
|
+
if (!slot.variadic) {
|
|
1224
|
+
values.set(slot.input, token);
|
|
1225
|
+
}
|
|
1226
|
+
else if (Array.isArray(bound)) {
|
|
1227
|
+
bound.push(token);
|
|
995
1228
|
}
|
|
996
1229
|
else {
|
|
997
|
-
|
|
998
|
-
if (value !== undefined) {
|
|
999
|
-
values.set(slot.input, value);
|
|
1000
|
-
index += 1;
|
|
1001
|
-
}
|
|
1230
|
+
values.set(slot.input, [token]);
|
|
1002
1231
|
}
|
|
1003
1232
|
}
|
|
1004
|
-
if (index < positionals.length) {
|
|
1005
|
-
throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
|
|
1006
|
-
}
|
|
1007
1233
|
return values;
|
|
1008
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* The slot the positional at one index fills: the slot at that index, else a variadic last slot,
|
|
1237
|
+
* which accepts every later positional, else none. Binding and `locate` read positions through it.
|
|
1238
|
+
*/
|
|
1239
|
+
export function argumentSlot(slots, position) {
|
|
1240
|
+
const last = slots.at(-1);
|
|
1241
|
+
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
1242
|
+
}
|
|
1009
1243
|
/** Consumes the globals table, then routes the remaining bare tokens to a Command. */
|
|
1010
1244
|
export function routeInvocation(graph, argv) {
|
|
1011
1245
|
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
@@ -1038,37 +1272,103 @@ function requestOf(command, values, passthrough) {
|
|
|
1038
1272
|
});
|
|
1039
1273
|
}
|
|
1040
1274
|
/**
|
|
1041
|
-
* The
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1275
|
+
* The callable check and local parsing. A group answers no invocation of its own, so it holds the
|
|
1276
|
+
* missing-subcommand error with the rank the routing errors have and parses no token; a token
|
|
1277
|
+
* fault holds the same way.
|
|
1044
1278
|
*/
|
|
1045
|
-
|
|
1046
|
-
const { command, path
|
|
1279
|
+
function parseLocal(routed) {
|
|
1280
|
+
const { command, path } = routed;
|
|
1047
1281
|
const { dispatch } = command;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1282
|
+
try {
|
|
1283
|
+
if (!dispatch) {
|
|
1284
|
+
throw new NonCallableCommandError(path, candidatesOf(command));
|
|
1285
|
+
}
|
|
1286
|
+
const parsed = parseInputs(command.options, routed.tokens);
|
|
1287
|
+
const args = bindArguments(command, path, parsed.positionals);
|
|
1288
|
+
return {
|
|
1289
|
+
args,
|
|
1290
|
+
dispatch,
|
|
1291
|
+
kind: 'parsed',
|
|
1292
|
+
options: parsed.options,
|
|
1293
|
+
passthrough: parsed.passthrough,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
return { fault: error, kind: 'held' };
|
|
1051
1298
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* The node of one option a configuration source is asked about: in the graph's globals, or among
|
|
1302
|
+
* the routed Command's own options. Build published every declaration, so a missing node means the
|
|
1303
|
+
* two readings of one graph disagree.
|
|
1304
|
+
*/
|
|
1305
|
+
function requestNode(graph, path, place) {
|
|
1306
|
+
const options = place.global ? graph.globals : nodeAt(graph, path).options;
|
|
1307
|
+
const node = options.find((option) => option.name === place.name);
|
|
1308
|
+
if (!node) {
|
|
1309
|
+
throw new InternalError(`Option "${place.name}" is not in the inspected graph.`, undefined);
|
|
1310
|
+
}
|
|
1311
|
+
return node;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* The input-source stage over this run's own copies of the parsed values. The global and plugin
|
|
1315
|
+
* options fill whatever local parsing held; the routed Command's own options fill only when local
|
|
1316
|
+
* parsing held no fault, because the request is `null` otherwise.
|
|
1317
|
+
*/
|
|
1318
|
+
async function fillScope({ graph, invocation, routed }, local) {
|
|
1319
|
+
const globals = copyValues(routed.scan);
|
|
1320
|
+
const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
|
|
1321
|
+
const sources = await fillInputs({
|
|
1322
|
+
extensions: graph.extensions,
|
|
1323
|
+
globals: {
|
|
1324
|
+
global: true,
|
|
1325
|
+
inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
|
|
1326
|
+
values: globals,
|
|
1327
|
+
},
|
|
1328
|
+
host: invocation.host,
|
|
1329
|
+
inspected: invocation.inspected,
|
|
1330
|
+
locals: local.kind === 'parsed'
|
|
1331
|
+
? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
|
|
1332
|
+
: undefined,
|
|
1333
|
+
out: invocation.sourceOut,
|
|
1334
|
+
plugins: graph.globals.plugins,
|
|
1335
|
+
request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
|
|
1336
|
+
signal: invocation.signal,
|
|
1337
|
+
style: invocation.style,
|
|
1338
|
+
});
|
|
1339
|
+
return { globals, locals, sources };
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Validation and the dispatch it prepares, over the values the input-source stage left. It answers
|
|
1343
|
+
* with the call that dispatches, so the caller records that the action was invoked at the moment
|
|
1344
|
+
* it invokes it and no earlier failure reads as a dispatch.
|
|
1345
|
+
*/
|
|
1346
|
+
async function readyDispatch({ graph, invocation, routed }, filled) {
|
|
1347
|
+
const { command, path } = routed;
|
|
1348
|
+
const { local } = filled;
|
|
1054
1349
|
const values = await validateValues({
|
|
1055
1350
|
command: path,
|
|
1056
1351
|
defaults: invocation.defaults,
|
|
1057
1352
|
host: invocation.host,
|
|
1058
1353
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
1059
|
-
passthrough:
|
|
1354
|
+
passthrough: local.passthrough,
|
|
1355
|
+
plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
|
|
1060
1356
|
signal: invocation.signal,
|
|
1061
|
-
|
|
1357
|
+
sources: filled.sources,
|
|
1358
|
+
supplied: { args: local.args, options: filled.values },
|
|
1062
1359
|
});
|
|
1063
1360
|
return {
|
|
1064
1361
|
dispatch: async (view) => {
|
|
1065
1362
|
// The channel is built at the boundary, because the view a middleware selected is read there.
|
|
1066
1363
|
const channel = invocation.channel({ path, result: command.result, view });
|
|
1067
1364
|
try {
|
|
1068
|
-
await dispatch({
|
|
1365
|
+
await local.dispatch({
|
|
1366
|
+
// The run builds its graph once, so every read answers the same node.
|
|
1367
|
+
command: () => nodeAt(invocation.inspected(), path),
|
|
1368
|
+
graph: invocation.inspected,
|
|
1069
1369
|
host: invocation.host,
|
|
1070
1370
|
out: channel.out,
|
|
1071
|
-
passthrough:
|
|
1371
|
+
passthrough: local.passthrough,
|
|
1072
1372
|
signal: invocation.signal,
|
|
1073
1373
|
style: invocation.style,
|
|
1074
1374
|
values,
|
|
@@ -1090,19 +1390,42 @@ async function readyDispatch(graph, routed, invocation) {
|
|
|
1090
1390
|
throw error;
|
|
1091
1391
|
}
|
|
1092
1392
|
},
|
|
1093
|
-
request: requestOf(command, values,
|
|
1393
|
+
request: requestOf(command, values, local.passthrough),
|
|
1094
1394
|
};
|
|
1095
1395
|
}
|
|
1096
1396
|
/**
|
|
1097
1397
|
* Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
|
|
1098
1398
|
* middleware reads the request before the action runs and a takeover never observes the fault.
|
|
1399
|
+
* The phases run in order: local parsing, the input-source stage, and validation. A local fault
|
|
1400
|
+
* outranks a configuration source's fault, and after either one core runs no validation.
|
|
1099
1401
|
*/
|
|
1100
1402
|
export async function prepareDispatch(graph, routed, invocation) {
|
|
1101
1403
|
const result = routed.command.result;
|
|
1404
|
+
const local = parseLocal(routed);
|
|
1405
|
+
const preparation = { graph, invocation, routed };
|
|
1406
|
+
const { globals, locals, sources } = await fillScope(preparation, local);
|
|
1407
|
+
const held = (fault) => ({
|
|
1408
|
+
fault,
|
|
1409
|
+
globals,
|
|
1410
|
+
kind: 'held',
|
|
1411
|
+
request: null,
|
|
1412
|
+
result,
|
|
1413
|
+
});
|
|
1414
|
+
if (local.kind === 'held') {
|
|
1415
|
+
return held(local.fault);
|
|
1416
|
+
}
|
|
1417
|
+
if (sources.fault) {
|
|
1418
|
+
return held(sources.fault);
|
|
1419
|
+
}
|
|
1102
1420
|
try {
|
|
1103
|
-
|
|
1421
|
+
const ready = await readyDispatch(preparation, {
|
|
1422
|
+
local,
|
|
1423
|
+
sources,
|
|
1424
|
+
values: mergeValues(globals, locals),
|
|
1425
|
+
});
|
|
1426
|
+
return { ...ready, globals, kind: 'ready', result };
|
|
1104
1427
|
}
|
|
1105
1428
|
catch (error) {
|
|
1106
|
-
return
|
|
1429
|
+
return held(error);
|
|
1107
1430
|
}
|
|
1108
1431
|
}
|