@loomcli/core 0.4.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 +650 -349
- package/dist/errors.d.ts +7 -2
- package/dist/errors.js +9 -1
- package/dist/extension.d.ts +3 -1
- package/dist/extension.js +8 -11
- 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 +4 -2
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +25 -2
- package/dist/inspect.js +43 -5
- 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 +65 -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
|
+
});
|
|
18
39
|
}
|
|
19
|
-
/**
|
|
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;
|
|
43
|
+
}
|
|
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
244
|
};
|
|
94
245
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
late: state.actions.length > 0 || state.children.length > 0
|
|
100
|
-
? [...state.late, { kind: 'global', name }]
|
|
101
|
-
: state.late,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/** One call's names stay one group, so the empty call the types reject still reports as one. */
|
|
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();
|
|
@@ -581,8 +790,8 @@ function runAttachHooks(start, facts, plugins) {
|
|
|
581
790
|
}
|
|
582
791
|
/**
|
|
583
792
|
* 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.
|
|
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.
|
|
586
795
|
*/
|
|
587
796
|
function declareHookInput(state, input) {
|
|
588
797
|
const previous = state.bind;
|
|
@@ -727,17 +936,23 @@ function checkAttachedInputs(declared, attached, globals) {
|
|
|
727
936
|
}
|
|
728
937
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
729
938
|
function bindDispatch(state, action, globals) {
|
|
730
|
-
return ({ host, out, passthrough, signal, style, values }) => {
|
|
939
|
+
return ({ command, graph, host, out, passthrough, signal, style, values }) => {
|
|
731
940
|
const bound = state.bind(values);
|
|
732
941
|
// Last resort: no typed path exists.
|
|
733
942
|
// The graph erases the binder's generic relationship.
|
|
734
943
|
// It holds because attachment checks the global output requirement.
|
|
735
|
-
//
|
|
944
|
+
// The call or the attach that met both options rejected a collision between a global and a local.
|
|
736
945
|
// This binder returns the Application's validated globals alone.
|
|
737
946
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
738
947
|
const globalOptions = globals.bind(values);
|
|
739
948
|
return action({
|
|
740
949
|
args: bound.args,
|
|
950
|
+
get command() {
|
|
951
|
+
return command();
|
|
952
|
+
},
|
|
953
|
+
get graph() {
|
|
954
|
+
return graph();
|
|
955
|
+
},
|
|
741
956
|
host,
|
|
742
957
|
options: { ...globalOptions, ...bound.options },
|
|
743
958
|
out,
|
|
@@ -747,7 +962,10 @@ function bindDispatch(state, action, globals) {
|
|
|
747
962
|
});
|
|
748
963
|
};
|
|
749
964
|
}
|
|
750
|
-
/**
|
|
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
|
+
*/
|
|
751
969
|
function checkInputFacts(inputs, command, context) {
|
|
752
970
|
const { name, subject } = command;
|
|
753
971
|
for (const input of inputs) {
|
|
@@ -755,10 +973,12 @@ function checkInputFacts(inputs, command, context) {
|
|
|
755
973
|
checkDescription(sentence, input.config.description);
|
|
756
974
|
if (input.kind === 'argument') {
|
|
757
975
|
checkNoListingFacts(sentence, input.config);
|
|
976
|
+
checkNoArgumentBinding(sentence, input.config);
|
|
758
977
|
}
|
|
759
978
|
else {
|
|
760
979
|
checkHidden(sentence, input.config.hidden);
|
|
761
980
|
checkDeprecated(sentence, input.config.deprecated);
|
|
981
|
+
checkEnvBinding(sentence, input.config);
|
|
762
982
|
}
|
|
763
983
|
context.extensions.set(input, buildExtensions({
|
|
764
984
|
declared: input.config.extensions,
|
|
@@ -771,45 +991,32 @@ function checkInputFacts(inputs, command, context) {
|
|
|
771
991
|
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
772
992
|
function checkArgumentPlacement(name, slot, child) {
|
|
773
993
|
if (slot && child) {
|
|
774
|
-
throw
|
|
994
|
+
throw placementFault(name, slot.input.name, child.name);
|
|
775
995
|
}
|
|
776
996
|
}
|
|
777
|
-
/**
|
|
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
|
+
*/
|
|
778
1003
|
export function buildCommand(state, context) {
|
|
779
|
-
const {
|
|
1004
|
+
const { action, facts, name } = state;
|
|
780
1005
|
const { globals } = context;
|
|
781
1006
|
const subject = commandSubject(name);
|
|
782
|
-
const layer =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
786
|
-
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
787
|
-
// The author's layers validate before any hook runs on this Command, so a hook reads them.
|
|
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.`);
|
|
1007
|
+
const layer = layerOf(name);
|
|
1008
|
+
for (const [input, record] of state.records) {
|
|
1009
|
+
context.extensions.set(input, record);
|
|
802
1010
|
}
|
|
803
|
-
const
|
|
1011
|
+
const declaredSlots = collectArguments(state, subject);
|
|
804
1012
|
const hasAction = action !== undefined;
|
|
805
|
-
const declaredResult = buildResult(state, hasAction);
|
|
806
1013
|
/**
|
|
807
|
-
* The hooks run once the author's declaration is complete and
|
|
808
|
-
* 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.
|
|
809
1016
|
*/
|
|
810
1017
|
// One token per Command per build, which binds a hook's return to the value it received.
|
|
811
1018
|
const lineage = {};
|
|
812
|
-
const progress = runAttachHooks({ declared: state, extensions:
|
|
1019
|
+
const progress = runAttachHooks({ declared: state, extensions: state.extensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
|
|
813
1020
|
const { calls } = progress;
|
|
814
1021
|
// The descriptors the returned value's extensions registered join the build's registry.
|
|
815
1022
|
for (const [identity, descriptor] of progress.registry) {
|
|
@@ -821,37 +1028,36 @@ export function buildCommand(state, context) {
|
|
|
821
1028
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
822
1029
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
823
1030
|
checkAttachedInputs(hooked, hookInputs, globals);
|
|
824
|
-
// Every value was validated once, the author's
|
|
1031
|
+
// Every value was validated once, the author's at each call and each hook's at its `extend()`.
|
|
825
1032
|
const extensions = publishStore(progress.extensions);
|
|
826
1033
|
const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
|
|
827
|
-
checkArgumentPlacement(name, slots[0],
|
|
828
|
-
const result =
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
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));
|
|
833
1039
|
const children = new Map();
|
|
834
1040
|
const routes = new Map();
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
};
|
|
839
1046
|
children.set(routed.name, routed.command);
|
|
840
1047
|
routes.set(routed.name, routed);
|
|
841
1048
|
for (const alias of routed.command.aliases) {
|
|
842
|
-
claim(routed.name, alias);
|
|
843
1049
|
routes.set(alias, routed);
|
|
844
1050
|
}
|
|
845
1051
|
}
|
|
846
1052
|
return {
|
|
847
|
-
aliases,
|
|
1053
|
+
aliases: state.aliases,
|
|
848
1054
|
arguments: slots,
|
|
849
1055
|
children,
|
|
850
|
-
deprecated,
|
|
851
|
-
description,
|
|
1056
|
+
deprecated: facts.deprecated,
|
|
1057
|
+
description: facts.description,
|
|
852
1058
|
dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
|
|
853
1059
|
extensions,
|
|
854
|
-
hidden,
|
|
1060
|
+
hidden: facts.hidden,
|
|
855
1061
|
inputs: hooked.inputs,
|
|
856
1062
|
name,
|
|
857
1063
|
options,
|
|
@@ -859,30 +1065,31 @@ export function buildCommand(state, context) {
|
|
|
859
1065
|
routes,
|
|
860
1066
|
};
|
|
861
1067
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
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
|
+
]);
|
|
864
1077
|
const context = {
|
|
865
|
-
descriptors:
|
|
866
|
-
extensions
|
|
867
|
-
globals: buildGlobals(globals,
|
|
868
|
-
owners: new Map(),
|
|
1078
|
+
descriptors: new Map(root.descriptors),
|
|
1079
|
+
extensions,
|
|
1080
|
+
globals: buildGlobals(globals, plugins),
|
|
869
1081
|
path: [],
|
|
870
|
-
plugins
|
|
871
|
-
};
|
|
872
|
-
return {
|
|
873
|
-
extensions: context.extensions,
|
|
874
|
-
globals: context.globals,
|
|
875
|
-
root: buildCommand(root, context),
|
|
1082
|
+
plugins,
|
|
876
1083
|
};
|
|
1084
|
+
return { extensions, globals: context.globals, root: buildCommand(root, context) };
|
|
877
1085
|
}
|
|
878
1086
|
export class CommandBuilder {
|
|
1087
|
+
#name;
|
|
879
1088
|
#state;
|
|
880
|
-
constructor(state) {
|
|
1089
|
+
constructor(name, state) {
|
|
1090
|
+
this.#name = name;
|
|
881
1091
|
this.#state = state;
|
|
882
|
-
nodes.set(this,
|
|
883
|
-
}
|
|
884
|
-
get name() {
|
|
885
|
-
return this.#state.name;
|
|
1092
|
+
nodes.set(this, nodeHandle(name, state));
|
|
886
1093
|
}
|
|
887
1094
|
argument(name, config) {
|
|
888
1095
|
const input = {
|
|
@@ -890,7 +1097,7 @@ export class CommandBuilder {
|
|
|
890
1097
|
kind: 'argument',
|
|
891
1098
|
name,
|
|
892
1099
|
};
|
|
893
|
-
return this
|
|
1100
|
+
return this.#derive(declareArgument(this.#state, input));
|
|
894
1101
|
}
|
|
895
1102
|
option(name, config) {
|
|
896
1103
|
const input = {
|
|
@@ -898,66 +1105,61 @@ export class CommandBuilder {
|
|
|
898
1105
|
kind: 'option',
|
|
899
1106
|
name,
|
|
900
1107
|
};
|
|
901
|
-
return this
|
|
1108
|
+
return this.#derive(declareOption(this.#state, input));
|
|
902
1109
|
}
|
|
903
1110
|
/**
|
|
904
|
-
* Aliases are other
|
|
1111
|
+
* Aliases are other portable names that route to this Command. They invalidate no call, and the
|
|
905
1112
|
* tuple rest parameter rejects a call that names none.
|
|
906
1113
|
*/
|
|
907
1114
|
alias(...names) {
|
|
908
|
-
return this
|
|
1115
|
+
return this.#derive(declareAlias(this.#state, names));
|
|
909
1116
|
}
|
|
910
1117
|
/** A child arrives in any type state, because its own action is the call that finished it. */
|
|
911
1118
|
command(child) {
|
|
912
|
-
return this
|
|
1119
|
+
return this.#derive(attachChild(this.#state, child));
|
|
913
1120
|
}
|
|
914
1121
|
/**
|
|
915
1122
|
* The value this Command produces for its consumer. The type argument is stated by the author,
|
|
916
1123
|
* so the views record states no type of its own and an omitted argument names none either.
|
|
917
1124
|
*/
|
|
918
1125
|
result(declaration) {
|
|
919
|
-
return this
|
|
1126
|
+
return this.#derive(declareResult(this.#state, 'value', declaration));
|
|
920
1127
|
}
|
|
921
1128
|
/** The same declaration over a sequence, whose type argument is one row. */
|
|
922
1129
|
rows(declaration) {
|
|
923
|
-
return this
|
|
1130
|
+
return this.#derive(declareResult(this.#state, 'rows', declaration));
|
|
924
1131
|
}
|
|
925
1132
|
/**
|
|
926
1133
|
* Views after the fact. It merges by key, so an existing name is replaced in place and a new one
|
|
927
1134
|
* is appended, and `default` names the key core renders when nothing selects another.
|
|
928
1135
|
*/
|
|
929
1136
|
views(replacements, options) {
|
|
930
|
-
return this
|
|
1137
|
+
return this.#derive(declareResultViews(this.#state, replacements, options));
|
|
931
1138
|
}
|
|
932
1139
|
/** The action closes input authoring; `extend()` remains outside this state transition. */
|
|
933
1140
|
action(handler) {
|
|
934
|
-
return this
|
|
1141
|
+
return this.#derive(declareAction(this.#state, handler));
|
|
935
1142
|
}
|
|
936
1143
|
extend(...values) {
|
|
937
|
-
return this
|
|
938
|
-
}
|
|
939
|
-
build(context) {
|
|
940
|
-
return buildCommand(this.#state, context);
|
|
1144
|
+
return this.#derive(declareExtensions(this.#state, values));
|
|
941
1145
|
}
|
|
942
1146
|
/**
|
|
943
1147
|
* The same runtime value in the state the calling method's return type names. Each call states
|
|
944
1148
|
* its own transition, and the declared result travels with it unless the call replaces it.
|
|
945
1149
|
*/
|
|
946
|
-
derive(state) {
|
|
947
|
-
return new
|
|
1150
|
+
#derive(state) {
|
|
1151
|
+
return new _b(this.#name, state);
|
|
948
1152
|
}
|
|
949
1153
|
}
|
|
950
|
-
|
|
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
|
+
*/
|
|
951
1159
|
class CommandDeclaration extends CommandBuilder {
|
|
952
1160
|
constructor(name, options) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
description: options?.description,
|
|
956
|
-
extensions: options?.extensions,
|
|
957
|
-
hidden: options?.hidden,
|
|
958
|
-
name,
|
|
959
|
-
options,
|
|
960
|
-
}));
|
|
1161
|
+
const declared = namedState(name, options);
|
|
1162
|
+
super(declared.name, declared.state);
|
|
961
1163
|
}
|
|
962
1164
|
}
|
|
963
1165
|
/** The public constructor takes a name and one options object, as the Application does. */
|
|
@@ -977,14 +1179,20 @@ export function collectInputs(command) {
|
|
|
977
1179
|
function candidatesOf(command) {
|
|
978
1180
|
return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
|
|
979
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
|
+
}
|
|
980
1189
|
/** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
|
|
981
1190
|
export function route(root, tokens) {
|
|
982
1191
|
let command = root;
|
|
983
1192
|
const path = [];
|
|
984
1193
|
let index = 0;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (token === undefined || token === '--' || token.startsWith('-')) {
|
|
1194
|
+
for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
|
|
1195
|
+
if (!readsAsChild(command, token)) {
|
|
988
1196
|
break;
|
|
989
1197
|
}
|
|
990
1198
|
const child = command.routes.get(token);
|
|
@@ -1005,29 +1213,33 @@ export function route(root, tokens) {
|
|
|
1005
1213
|
*/
|
|
1006
1214
|
function bindArguments(command, path, positionals) {
|
|
1007
1215
|
const values = new Map();
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (slot
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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);
|
|
1017
1228
|
}
|
|
1018
1229
|
else {
|
|
1019
|
-
|
|
1020
|
-
if (value !== undefined) {
|
|
1021
|
-
values.set(slot.input, value);
|
|
1022
|
-
index += 1;
|
|
1023
|
-
}
|
|
1230
|
+
values.set(slot.input, [token]);
|
|
1024
1231
|
}
|
|
1025
1232
|
}
|
|
1026
|
-
if (index < positionals.length) {
|
|
1027
|
-
throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
|
|
1028
|
-
}
|
|
1029
1233
|
return values;
|
|
1030
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
|
+
}
|
|
1031
1243
|
/** Consumes the globals table, then routes the remaining bare tokens to a Command. */
|
|
1032
1244
|
export function routeInvocation(graph, argv) {
|
|
1033
1245
|
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
@@ -1060,37 +1272,103 @@ function requestOf(command, values, passthrough) {
|
|
|
1060
1272
|
});
|
|
1061
1273
|
}
|
|
1062
1274
|
/**
|
|
1063
|
-
* The
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
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.
|
|
1066
1278
|
*/
|
|
1067
|
-
|
|
1068
|
-
const { command, path
|
|
1279
|
+
function parseLocal(routed) {
|
|
1280
|
+
const { command, path } = routed;
|
|
1069
1281
|
const { dispatch } = command;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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' };
|
|
1298
|
+
}
|
|
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);
|
|
1073
1310
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
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;
|
|
1076
1349
|
const values = await validateValues({
|
|
1077
1350
|
command: path,
|
|
1078
1351
|
defaults: invocation.defaults,
|
|
1079
1352
|
host: invocation.host,
|
|
1080
1353
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
1081
|
-
passthrough:
|
|
1354
|
+
passthrough: local.passthrough,
|
|
1355
|
+
plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
|
|
1082
1356
|
signal: invocation.signal,
|
|
1083
|
-
|
|
1357
|
+
sources: filled.sources,
|
|
1358
|
+
supplied: { args: local.args, options: filled.values },
|
|
1084
1359
|
});
|
|
1085
1360
|
return {
|
|
1086
1361
|
dispatch: async (view) => {
|
|
1087
1362
|
// The channel is built at the boundary, because the view a middleware selected is read there.
|
|
1088
1363
|
const channel = invocation.channel({ path, result: command.result, view });
|
|
1089
1364
|
try {
|
|
1090
|
-
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,
|
|
1091
1369
|
host: invocation.host,
|
|
1092
1370
|
out: channel.out,
|
|
1093
|
-
passthrough:
|
|
1371
|
+
passthrough: local.passthrough,
|
|
1094
1372
|
signal: invocation.signal,
|
|
1095
1373
|
style: invocation.style,
|
|
1096
1374
|
values,
|
|
@@ -1112,19 +1390,42 @@ async function readyDispatch(graph, routed, invocation) {
|
|
|
1112
1390
|
throw error;
|
|
1113
1391
|
}
|
|
1114
1392
|
},
|
|
1115
|
-
request: requestOf(command, values,
|
|
1393
|
+
request: requestOf(command, values, local.passthrough),
|
|
1116
1394
|
};
|
|
1117
1395
|
}
|
|
1118
1396
|
/**
|
|
1119
1397
|
* Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
|
|
1120
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.
|
|
1121
1401
|
*/
|
|
1122
1402
|
export async function prepareDispatch(graph, routed, invocation) {
|
|
1123
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
|
+
}
|
|
1124
1420
|
try {
|
|
1125
|
-
|
|
1421
|
+
const ready = await readyDispatch(preparation, {
|
|
1422
|
+
local,
|
|
1423
|
+
sources,
|
|
1424
|
+
values: mergeValues(globals, locals),
|
|
1425
|
+
});
|
|
1426
|
+
return { ...ready, globals, kind: 'ready', result };
|
|
1126
1427
|
}
|
|
1127
1428
|
catch (error) {
|
|
1128
|
-
return
|
|
1429
|
+
return held(error);
|
|
1129
1430
|
}
|
|
1130
1431
|
}
|