@loomcli/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +20 -0
- package/dist/application.js +207 -75
- package/dist/chain.d.ts +49 -0
- package/dist/chain.js +288 -0
- package/dist/command.d.ts +72 -21
- package/dist/command.js +142 -28
- package/dist/errors.d.ts +10 -2
- package/dist/errors.js +33 -8
- package/dist/extension.d.ts +95 -0
- package/dist/extension.js +313 -0
- package/dist/facts.d.ts +39 -0
- package/dist/facts.js +95 -0
- package/dist/globals.d.ts +39 -7
- package/dist/globals.js +109 -12
- package/dist/index.d.ts +7 -1
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +44 -10
- package/dist/inspect.js +64 -21
- package/dist/options.d.ts +7 -0
- package/dist/options.js +9 -0
- package/dist/plugin.d.ts +116 -0
- package/dist/plugin.js +250 -0
- package/dist/signals.d.ts +52 -0
- package/dist/signals.js +85 -0
- package/dist/types.d.ts +55 -7
- package/dist/validation.d.ts +4 -2
- package/dist/validation.js +17 -15
- package/package.json +1 -1
package/dist/command.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { buildExtensions } from './extension.js';
|
|
3
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
|
|
4
|
+
import { bindGlobals, buildGlobals, isGlobalOptions, keyCollision, spellingCollision, } from './globals.js';
|
|
3
5
|
import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
|
|
4
6
|
import { captureConfig, validateValues } from './validation.js';
|
|
5
7
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
@@ -12,6 +14,22 @@ function nodeOf(parent, child) {
|
|
|
12
14
|
}
|
|
13
15
|
return node;
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The shape of a Command's second argument, read where it is supplied. The retired positional form
|
|
19
|
+
* declares its globals on a value that holds no `globals` key, so without this rule the globals
|
|
20
|
+
* vanish silently and the operator, not the author, meets the consequence as an unknown-option
|
|
21
|
+
* error. It answers before every other rule, because a Command that lost its globals this way would
|
|
22
|
+
* otherwise report the mismatch with its Application instead of the slot that caused it.
|
|
23
|
+
* The facts the slot carried were captured at construction, so this reads the slot's shape alone.
|
|
24
|
+
*/
|
|
25
|
+
function checkCommandOptions(name, options) {
|
|
26
|
+
if (isGlobalOptions(options)) {
|
|
27
|
+
throw new DeclarationError(`${commandSentence(name)} takes an options object. Supply { globals } instead of a positional GlobalOptions value.`);
|
|
28
|
+
}
|
|
29
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
30
|
+
throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply { globals }.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
15
33
|
/** One name rule for every declared name in the graph, so a child and an argument read alike. */
|
|
16
34
|
function isDeclaredName(name) {
|
|
17
35
|
return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
@@ -27,17 +45,26 @@ function checkAliasName(command, alias) {
|
|
|
27
45
|
throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The state every declaration starts from. The unnamed root and each named Command share it.
|
|
50
|
+
* The declaration values arrive captured, because a later change to the options object the author
|
|
51
|
+
* passed changes nothing the declaration holds.
|
|
52
|
+
*/
|
|
53
|
+
export function freshState(declaration) {
|
|
32
54
|
return {
|
|
33
55
|
actions: [],
|
|
34
56
|
aliases: [],
|
|
35
57
|
bind: () => ({ args: {}, options: {} }),
|
|
36
58
|
children: [],
|
|
37
|
-
|
|
59
|
+
deprecated: declaration.deprecated,
|
|
60
|
+
description: declaration.description,
|
|
61
|
+
extensions: declaration.extensions,
|
|
62
|
+
globals: declaration.globals,
|
|
63
|
+
hidden: declaration.hidden,
|
|
38
64
|
inputs: [],
|
|
39
65
|
late: [],
|
|
40
|
-
name,
|
|
66
|
+
name: declaration.name,
|
|
67
|
+
options: declaration.options,
|
|
41
68
|
};
|
|
42
69
|
}
|
|
43
70
|
/** A declaration after the action is an order fault; build reports the first one recorded. */
|
|
@@ -216,18 +243,26 @@ function collectArguments(state, subject) {
|
|
|
216
243
|
}
|
|
217
244
|
return slots;
|
|
218
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* One Command's own options against the shared globals table. The table holds the application's
|
|
248
|
+
* globals and every plugin option, so a local collision reads the same sentence whichever scope on
|
|
249
|
+
* the other side claimed the name or the spelling.
|
|
250
|
+
*/
|
|
219
251
|
function compileLocalOptions(state, globals, subject) {
|
|
220
252
|
const declarations = state.inputs.filter((input) => input.kind === 'option');
|
|
253
|
+
const local = { kind: 'local', subject };
|
|
254
|
+
const application = { kind: 'application' };
|
|
221
255
|
for (const declaration of declarations) {
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
const claimed = globals.names.get(declaration.name);
|
|
257
|
+
if (claimed) {
|
|
258
|
+
throw keyCollision(declaration.name, claimed, local);
|
|
224
259
|
}
|
|
225
260
|
}
|
|
226
261
|
const options = compileOptions(declarations, subject);
|
|
227
262
|
for (const [spelling, option] of options) {
|
|
228
263
|
const global = globals.options.get(spelling);
|
|
229
264
|
if (global) {
|
|
230
|
-
throw
|
|
265
|
+
throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
|
|
231
266
|
}
|
|
232
267
|
}
|
|
233
268
|
return options;
|
|
@@ -249,7 +284,7 @@ function checkGroup(state, children) {
|
|
|
249
284
|
}
|
|
250
285
|
/** Binds one Command's declarations to its action, so an action reads only validated values. */
|
|
251
286
|
function bindDispatch(state, action) {
|
|
252
|
-
return ({ host, out, passthrough, values }) => {
|
|
287
|
+
return ({ host, out, passthrough, signal, values }) => {
|
|
253
288
|
const bound = state.bind(values);
|
|
254
289
|
return action({
|
|
255
290
|
args: bound.args,
|
|
@@ -257,6 +292,7 @@ function bindDispatch(state, action) {
|
|
|
257
292
|
options: { ...bindGlobals(state.globals, values), ...bound.options },
|
|
258
293
|
out,
|
|
259
294
|
passthrough,
|
|
295
|
+
signal,
|
|
260
296
|
});
|
|
261
297
|
};
|
|
262
298
|
}
|
|
@@ -265,6 +301,34 @@ export function buildCommand(state, context) {
|
|
|
265
301
|
const { actions, name } = state;
|
|
266
302
|
const { globals } = context;
|
|
267
303
|
const subject = commandSubject(name);
|
|
304
|
+
checkCommandOptions(name, state.options);
|
|
305
|
+
const description = checkDescription(commandSentence(name), state.description);
|
|
306
|
+
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
307
|
+
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
308
|
+
const extensions = buildExtensions({
|
|
309
|
+
declared: state.extensions,
|
|
310
|
+
descriptors: context.descriptors,
|
|
311
|
+
subject: { phrase: `on ${subject}`, sentence: commandSentence(name) },
|
|
312
|
+
target: 'command',
|
|
313
|
+
});
|
|
314
|
+
// Each declaration's own facts, in authoring order, before the rules that pair declarations.
|
|
315
|
+
for (const input of state.inputs) {
|
|
316
|
+
const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
|
|
317
|
+
checkDescription(sentence, input.config.description);
|
|
318
|
+
if (input.kind === 'argument') {
|
|
319
|
+
checkNoListingFacts(sentence, input.config);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
checkHidden(sentence, input.config.hidden);
|
|
323
|
+
checkDeprecated(sentence, input.config.deprecated);
|
|
324
|
+
}
|
|
325
|
+
context.extensions.set(input, buildExtensions({
|
|
326
|
+
declared: input.config.extensions,
|
|
327
|
+
descriptors: context.descriptors,
|
|
328
|
+
subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
|
|
329
|
+
target: input.kind,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
268
332
|
if (state.globals !== globals.source) {
|
|
269
333
|
throw new DeclarationError(`${commandSentence(name)} holds a different GlobalOptions value than its Application. Share one GlobalOptions value across the declarations.`);
|
|
270
334
|
}
|
|
@@ -302,7 +366,11 @@ export function buildCommand(state, context) {
|
|
|
302
366
|
aliases,
|
|
303
367
|
arguments: slots,
|
|
304
368
|
children,
|
|
369
|
+
deprecated,
|
|
370
|
+
description,
|
|
305
371
|
dispatch: action ? bindDispatch(state, action) : undefined,
|
|
372
|
+
extensions,
|
|
373
|
+
hidden,
|
|
306
374
|
inputs: state.inputs,
|
|
307
375
|
name,
|
|
308
376
|
options,
|
|
@@ -310,9 +378,18 @@ export function buildCommand(state, context) {
|
|
|
310
378
|
};
|
|
311
379
|
}
|
|
312
380
|
/** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
|
|
313
|
-
export function buildGraph(root) {
|
|
314
|
-
const context = {
|
|
315
|
-
|
|
381
|
+
export function buildGraph(root, install) {
|
|
382
|
+
const context = {
|
|
383
|
+
descriptors: install.descriptors,
|
|
384
|
+
extensions: install.extensions,
|
|
385
|
+
globals: buildGlobals(root.globals, install.plugins, install),
|
|
386
|
+
owners: new Map(),
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
extensions: context.extensions,
|
|
390
|
+
globals: context.globals,
|
|
391
|
+
root: buildCommand(root, context),
|
|
392
|
+
};
|
|
316
393
|
}
|
|
317
394
|
export class CommandBuilder {
|
|
318
395
|
#state;
|
|
@@ -340,8 +417,8 @@ export class CommandBuilder {
|
|
|
340
417
|
return new CommandBuilder(declareOption(this.#state, input));
|
|
341
418
|
}
|
|
342
419
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
420
|
+
* Aliases are other bare tokens that route to this Command. They invalidate no call, and the
|
|
421
|
+
* tuple rest parameter rejects a call that names none.
|
|
345
422
|
*/
|
|
346
423
|
alias(...names) {
|
|
347
424
|
return new CommandBuilder(declareAlias(this.#state, names));
|
|
@@ -364,11 +441,21 @@ export class CommandBuilder {
|
|
|
364
441
|
* signature of the constructor interface publishes.
|
|
365
442
|
*/
|
|
366
443
|
class CommandDeclaration extends CommandBuilder {
|
|
367
|
-
constructor(name,
|
|
368
|
-
|
|
444
|
+
constructor(name, options) {
|
|
445
|
+
// The options slot is read defensively, never inspected: an invalid value still yields
|
|
446
|
+
// `globals`, the core facts, and `extensions` of some kind, and `buildCommand` reports it.
|
|
447
|
+
super(freshState({
|
|
448
|
+
deprecated: options?.deprecated,
|
|
449
|
+
description: options?.description,
|
|
450
|
+
extensions: options?.extensions,
|
|
451
|
+
globals: options?.globals,
|
|
452
|
+
hidden: options?.hidden,
|
|
453
|
+
name,
|
|
454
|
+
options,
|
|
455
|
+
}));
|
|
369
456
|
}
|
|
370
457
|
}
|
|
371
|
-
/** The public constructor
|
|
458
|
+
/** The public constructor takes a name and one options object, as the Application does. */
|
|
372
459
|
export const Command = CommandDeclaration;
|
|
373
460
|
/** Every declaration in the graph, so defaults are validated before any token is read. */
|
|
374
461
|
export function collectInputs(command) {
|
|
@@ -377,6 +464,14 @@ export function collectInputs(command) {
|
|
|
377
464
|
...[...command.children.values()].flatMap((child) => collectInputs(child)),
|
|
378
465
|
];
|
|
379
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* The names a routing failure offers: the canonical names of the visible children, in authoring
|
|
469
|
+
* order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
|
|
470
|
+
* children are all hidden offers none.
|
|
471
|
+
*/
|
|
472
|
+
function candidatesOf(command) {
|
|
473
|
+
return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
|
|
474
|
+
}
|
|
380
475
|
/** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
|
|
381
476
|
export function route(root, tokens) {
|
|
382
477
|
let command = root;
|
|
@@ -389,7 +484,7 @@ export function route(root, tokens) {
|
|
|
389
484
|
}
|
|
390
485
|
const child = command.routes.get(token);
|
|
391
486
|
if (!child) {
|
|
392
|
-
throw new UnknownCommandError(token,
|
|
487
|
+
throw new UnknownCommandError(token, candidatesOf(command));
|
|
393
488
|
}
|
|
394
489
|
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
395
490
|
command = child.command;
|
|
@@ -428,25 +523,44 @@ function bindArguments(command, path, positionals) {
|
|
|
428
523
|
}
|
|
429
524
|
return values;
|
|
430
525
|
}
|
|
431
|
-
/** Consumes globals
|
|
432
|
-
export
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
526
|
+
/** Consumes the globals table, then routes the remaining bare tokens to a Command. */
|
|
527
|
+
export function routeInvocation(graph, argv) {
|
|
528
|
+
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
529
|
+
const routed = route(graph.root, scan.rest);
|
|
530
|
+
return {
|
|
531
|
+
command: routed.command,
|
|
532
|
+
path: routed.path,
|
|
533
|
+
scan: scan.values,
|
|
534
|
+
tokens: routed.tokens,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* The phases the middleware chain terminates in: the callable check, local parsing, validation, and
|
|
539
|
+
* the action. It answers with the call that dispatches, so the caller records that the action was
|
|
540
|
+
* invoked at the moment it invokes it and no earlier failure reads as a dispatch.
|
|
541
|
+
*/
|
|
542
|
+
export async function prepareDispatch(graph, routed, invocation) {
|
|
543
|
+
const { command, path, scan } = routed;
|
|
436
544
|
const { dispatch } = command;
|
|
437
545
|
// A group answers no invocation of its own, so it fails with the routing errors above it.
|
|
438
546
|
if (!dispatch) {
|
|
439
|
-
throw new NonCallableCommandError(path,
|
|
547
|
+
throw new NonCallableCommandError(path, candidatesOf(command));
|
|
440
548
|
}
|
|
441
|
-
const parsed = parseInputs(command.options,
|
|
549
|
+
const parsed = parseInputs(command.options, routed.tokens);
|
|
442
550
|
const args = bindArguments(command, path, parsed.positionals);
|
|
443
551
|
const values = await validateValues({
|
|
444
552
|
command: path,
|
|
445
553
|
defaults: invocation.defaults,
|
|
446
|
-
host,
|
|
554
|
+
host: invocation.host,
|
|
447
555
|
inputs: { globals: graph.globals.inputs, locals: command.inputs },
|
|
448
556
|
passthrough: parsed.passthrough,
|
|
449
|
-
supplied: { args, options: mergeValues(scan
|
|
557
|
+
supplied: { args, options: mergeValues(scan, parsed.options) },
|
|
558
|
+
});
|
|
559
|
+
return () => dispatch({
|
|
560
|
+
host: invocation.host,
|
|
561
|
+
out: invocation.out,
|
|
562
|
+
passthrough: parsed.passthrough,
|
|
563
|
+
signal: invocation.signal,
|
|
564
|
+
values,
|
|
450
565
|
});
|
|
451
|
-
return { dispatch, passthrough: parsed.passthrough, values };
|
|
452
566
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -134,8 +134,16 @@ export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration
|
|
|
134
134
|
export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
|
|
135
135
|
/** The renderers one application registered, keyed by the class each one names. */
|
|
136
136
|
export type FailureRegistry = ReadonlyMap<unknown, Registration>;
|
|
137
|
-
/**
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* One class answers to one renderer inside one contributor, so a second registration for it is a
|
|
139
|
+
* declaration fault. The subject names the contributor: the Application, or an installed plugin.
|
|
140
|
+
*/
|
|
141
|
+
export declare function buildFailures(failures: readonly FailureRenderer[], subject?: string): FailureRegistry;
|
|
142
|
+
/**
|
|
143
|
+
* One registry from every contributor's own, resolving first-in-wins: the application's
|
|
144
|
+
* registrations, then each installed plugin's in installation order, then core's text.
|
|
145
|
+
*/
|
|
146
|
+
export declare function mergeFailures(registries: readonly FailureRegistry[]): FailureRegistry;
|
|
139
147
|
/**
|
|
140
148
|
* The report of one failure: the text core writes, and whether a registered renderer produced it.
|
|
141
149
|
* An unrendered report carries core's own text, which the plain fallback path writes beside the
|
package/dist/errors.js
CHANGED
|
@@ -3,6 +3,13 @@ function routedSentence(command) {
|
|
|
3
3
|
const name = command.at(-1);
|
|
4
4
|
return name === undefined ? 'The root Command' : commandSentence(name);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The clause that offers the candidates, which is absent when there are none: every child of the
|
|
8
|
+
* Command is hidden, so the diagnostic ends after the sentence that names the fault.
|
|
9
|
+
*/
|
|
10
|
+
function offering(candidates) {
|
|
11
|
+
return candidates.length === 0 ? '' : ` Use one of: ${candidates.join(', ')}.`;
|
|
12
|
+
}
|
|
6
13
|
function shortGroupMessage(fault) {
|
|
7
14
|
return fault.reason === 'value-position'
|
|
8
15
|
? `Value option "${fault.token}" must be last in its short group. Supply its value in the next token.`
|
|
@@ -46,10 +53,10 @@ class RegisteredFailure {
|
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
/** Reads the pair behind a registered value; anything else is a declaration error. */
|
|
49
|
-
function nodeOf(value) {
|
|
56
|
+
function nodeOf(value, subject) {
|
|
50
57
|
const registration = nodes.get(value);
|
|
51
58
|
if (!registration) {
|
|
52
|
-
throw new DeclarationError(
|
|
59
|
+
throw new DeclarationError(`${subject} holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).`);
|
|
53
60
|
}
|
|
54
61
|
return registration;
|
|
55
62
|
}
|
|
@@ -96,7 +103,7 @@ export class UnknownCommandError extends UsageError {
|
|
|
96
103
|
token;
|
|
97
104
|
candidates;
|
|
98
105
|
constructor(token, candidates) {
|
|
99
|
-
super(`Unknown command "${token}"
|
|
106
|
+
super(`Unknown command "${token}".${offering(candidates)}`);
|
|
100
107
|
this.candidates = candidates;
|
|
101
108
|
this.name = 'UnknownCommandError';
|
|
102
109
|
this.token = token;
|
|
@@ -107,7 +114,7 @@ export class NonCallableCommandError extends UsageError {
|
|
|
107
114
|
command;
|
|
108
115
|
candidates;
|
|
109
116
|
constructor(command, candidates) {
|
|
110
|
-
super(`${routedSentence(command)} requires a subcommand
|
|
117
|
+
super(`${routedSentence(command)} requires a subcommand.${offering(candidates)}`);
|
|
111
118
|
this.candidates = candidates;
|
|
112
119
|
this.command = command;
|
|
113
120
|
this.name = 'NonCallableCommandError';
|
|
@@ -225,18 +232,36 @@ export function renderFailure(type, renderer) {
|
|
|
225
232
|
render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
|
|
226
233
|
});
|
|
227
234
|
}
|
|
228
|
-
/**
|
|
229
|
-
|
|
235
|
+
/**
|
|
236
|
+
* One class answers to one renderer inside one contributor, so a second registration for it is a
|
|
237
|
+
* declaration fault. The subject names the contributor: the Application, or an installed plugin.
|
|
238
|
+
*/
|
|
239
|
+
export function buildFailures(failures, subject = 'The Application') {
|
|
230
240
|
const registry = new Map();
|
|
231
241
|
for (const failure of failures) {
|
|
232
|
-
const registration = nodeOf(failure);
|
|
242
|
+
const registration = nodeOf(failure, subject);
|
|
233
243
|
if (registry.has(registration.prototype)) {
|
|
234
|
-
throw new DeclarationError(
|
|
244
|
+
throw new DeclarationError(`${subject} registers two failure renderers for "${registration.name}". Remove one registration.`);
|
|
235
245
|
}
|
|
236
246
|
registry.set(registration.prototype, registration);
|
|
237
247
|
}
|
|
238
248
|
return registry;
|
|
239
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* One registry from every contributor's own, resolving first-in-wins: the application's
|
|
252
|
+
* registrations, then each installed plugin's in installation order, then core's text.
|
|
253
|
+
*/
|
|
254
|
+
export function mergeFailures(registries) {
|
|
255
|
+
const merged = new Map();
|
|
256
|
+
for (const registry of registries) {
|
|
257
|
+
for (const [type, registration] of registry) {
|
|
258
|
+
if (!merged.has(type)) {
|
|
259
|
+
merged.set(type, registration);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return merged;
|
|
264
|
+
}
|
|
240
265
|
/**
|
|
241
266
|
* The text core writes for one failure. Resolution walks the failure's prototype chain most
|
|
242
267
|
* derived first through the application's registrations, then falls to core's own text, so a
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { ArgumentNode, CommandNode, OptionNode } from './inspect.js';
|
|
3
|
+
/** The three declaration kinds an extension can name, each with its own node in the graph. */
|
|
4
|
+
type ExtensionTarget = 'argument' | 'command' | 'option';
|
|
5
|
+
/**
|
|
6
|
+
* The descriptor supertype a plugin's `extensions` list uses. It publishes the identity and the
|
|
7
|
+
* target and erases both the schema and the factory call signature, because a schema-typed call
|
|
8
|
+
* signature relates only by schema identity and a list cannot name one schema per element. A
|
|
9
|
+
* descriptor is assignable to it; an extension value, which publishes its brand alone, is not.
|
|
10
|
+
*/
|
|
11
|
+
interface AnyExtension {
|
|
12
|
+
readonly identity: string;
|
|
13
|
+
readonly target: ExtensionTarget;
|
|
14
|
+
}
|
|
15
|
+
/** One carried value: the input its author supplied and the descriptor that produced it. */
|
|
16
|
+
interface CarriedValue {
|
|
17
|
+
descriptor: AnyExtension;
|
|
18
|
+
input: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** Phantom key. It brands an extension value with its target and holds no runtime value. */
|
|
21
|
+
declare const extensionTarget: unique symbol;
|
|
22
|
+
/**
|
|
23
|
+
* The value one extension produces, branded with its target so a value on the wrong declaration is
|
|
24
|
+
* a compile error. It publishes the brand alone, so a value is never mistaken for the descriptor
|
|
25
|
+
* that produced it, and the input it carries stays private to this package.
|
|
26
|
+
*/
|
|
27
|
+
declare class ExtensionCarrier<Target extends ExtensionTarget> {
|
|
28
|
+
readonly [extensionTarget]: (target: Target) => Target;
|
|
29
|
+
constructor(record: CarriedValue);
|
|
30
|
+
}
|
|
31
|
+
/** A branded extension value, keyed by its extension's identity and typed by its target. */
|
|
32
|
+
type ExtensionValue<Target extends ExtensionTarget> = Pick<ExtensionCarrier<Target>, typeof extensionTarget>;
|
|
33
|
+
/**
|
|
34
|
+
* A descriptor that is also a factory: calling it with the schema's input returns the branded value
|
|
35
|
+
* a declaration carries. The schema is public so a typed read recovers the output type.
|
|
36
|
+
*/
|
|
37
|
+
interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1> extends AnyExtension {
|
|
38
|
+
(input: StandardSchemaV1.InferInput<Schema>): ExtensionValue<Target>;
|
|
39
|
+
readonly schema: Schema;
|
|
40
|
+
readonly target: Target;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* One typed fact a plugin defines for one target. The descriptor is compared by reference wherever
|
|
44
|
+
* it appears, so one identity means one descriptor and a duplicated package copy is visible.
|
|
45
|
+
*/
|
|
46
|
+
declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
|
|
47
|
+
schema: Schema;
|
|
48
|
+
target: Target;
|
|
49
|
+
}): Extension<Target, Schema>;
|
|
50
|
+
/** The node kind one descriptor's target names, so a read against another kind cannot compile. */
|
|
51
|
+
type NodeFor<Target extends ExtensionTarget> = Target extends 'command' ? CommandNode : Target extends 'option' ? OptionNode : ArgumentNode;
|
|
52
|
+
/** Stored output is plain data the graph froze, so every read of it is read-only to any depth. */
|
|
53
|
+
type DeepReadonly<Value> = Value extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : Value extends object ? {
|
|
54
|
+
readonly [Key in keyof Value]: DeepReadonly<Value[Key]>;
|
|
55
|
+
} : Value;
|
|
56
|
+
/**
|
|
57
|
+
* The typed read of one extension value. It takes the node kind the descriptor targets, returns the
|
|
58
|
+
* stored output or `undefined`, compares the descriptor by reference with the one that produced the
|
|
59
|
+
* value, and runs no schema.
|
|
60
|
+
*/
|
|
61
|
+
declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(node: NodeFor<Target>, descriptor: Extension<Target, Schema>): DeepReadonly<StandardSchemaV1.InferOutput<Schema>> | undefined;
|
|
62
|
+
/** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
|
|
63
|
+
interface ExtensionSubject {
|
|
64
|
+
/** The subject after a preposition, such as `on Command "get"`. */
|
|
65
|
+
phrase: string;
|
|
66
|
+
/** The subject at the start of a sentence, such as `Command "get"`. */
|
|
67
|
+
sentence: string;
|
|
68
|
+
}
|
|
69
|
+
/** Every descriptor one build has met, so a second descriptor under one identity is visible. */
|
|
70
|
+
type DescriptorRegistry = Map<string, AnyExtension>;
|
|
71
|
+
/**
|
|
72
|
+
* The record each declaration published during one build, keyed by the declaration itself. The
|
|
73
|
+
* records live here rather than on the declaration, because one build's outputs belong to that
|
|
74
|
+
* build alone and inspection reads them back with the nodes it renders.
|
|
75
|
+
*/
|
|
76
|
+
type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
|
|
77
|
+
/** Whether a value is a descriptor: a callable object carrying an identity and a declared target. */
|
|
78
|
+
declare function isDescriptor(value: unknown): value is AnyExtension;
|
|
79
|
+
/** One identity means one descriptor, wherever on the graph that descriptor appears. */
|
|
80
|
+
declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: AnyExtension): void;
|
|
81
|
+
/** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
|
|
82
|
+
interface ExtensionSlot {
|
|
83
|
+
declared: unknown;
|
|
84
|
+
descriptors: DescriptorRegistry;
|
|
85
|
+
subject: ExtensionSubject;
|
|
86
|
+
target: ExtensionTarget;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The frozen record one declaration publishes: each carried value validated once, synchronously,
|
|
90
|
+
* and stored under its extension's identity. The descriptors that produced them are recorded beside
|
|
91
|
+
* the record, so a typed read compares by reference without the graph carrying a reference.
|
|
92
|
+
*/
|
|
93
|
+
declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
|
|
94
|
+
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
95
|
+
export { buildExtensions, extension, isDescriptor, readExtension, registerDescriptor };
|