@loomcli/core 0.3.0 → 0.4.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/command.js +47 -25
- package/dist/extension.d.ts +79 -23
- package/dist/extension.js +112 -44
- package/dist/index.d.ts +1 -1
- package/dist/inspect.d.ts +12 -1
- package/dist/inspect.js +50 -1
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/dist/command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { buildExtensions, extendStore, publishStore, storeCommandLayers, validateLayer, } from './extension.js';
|
|
4
4
|
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
|
|
5
5
|
import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
|
|
6
6
|
import { resultNode, snapshot } from './inspect.js';
|
|
@@ -476,6 +476,10 @@ class AttachedCommandValue {
|
|
|
476
476
|
get result() {
|
|
477
477
|
return this.#result;
|
|
478
478
|
}
|
|
479
|
+
/** Published on first read and shared by every value whose store is unchanged. */
|
|
480
|
+
get extensions() {
|
|
481
|
+
return publishStore(this.#state.extensions);
|
|
482
|
+
}
|
|
479
483
|
argument(name, config) {
|
|
480
484
|
return this.#declare({ config: captureConfig(config), kind: 'argument', name });
|
|
481
485
|
}
|
|
@@ -491,8 +495,24 @@ class AttachedCommandValue {
|
|
|
491
495
|
const { declared } = this.#state;
|
|
492
496
|
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
493
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* The values are validated here, at the call, so the next read of `extensions` holds them and
|
|
500
|
+
* build never validates them again. A rejected value throws from the call and adds nothing.
|
|
501
|
+
*/
|
|
494
502
|
extend(...values) {
|
|
495
|
-
|
|
503
|
+
const state = this.#state;
|
|
504
|
+
const registry = new Map(state.registry);
|
|
505
|
+
const layer = validateLayer({
|
|
506
|
+
declared: values,
|
|
507
|
+
descriptors: registry,
|
|
508
|
+
subject: state.subject,
|
|
509
|
+
target: 'command',
|
|
510
|
+
});
|
|
511
|
+
return new _a({
|
|
512
|
+
...state,
|
|
513
|
+
extensions: extendStore(state.extensions, layer),
|
|
514
|
+
registry,
|
|
515
|
+
});
|
|
496
516
|
}
|
|
497
517
|
/** One input the running hook declared, which the Command's own names now hold. */
|
|
498
518
|
#declare(input) {
|
|
@@ -538,20 +558,26 @@ function attachOnce(value, hook, named) {
|
|
|
538
558
|
* Every installed plugin's hook over one Command, in installation order, each receiving what the
|
|
539
559
|
* previous returned. The calls they made travel back to the declaration the remaining rules read.
|
|
540
560
|
*/
|
|
541
|
-
function runAttachHooks(
|
|
561
|
+
function runAttachHooks(start, facts, plugins) {
|
|
562
|
+
const { declared, extensions, layer, registry } = start;
|
|
542
563
|
const subject = commandSubject(declared.name);
|
|
543
564
|
const { lineage } = facts;
|
|
544
|
-
let progress = { calls: [], declared };
|
|
565
|
+
let progress = { calls: [], declared, extensions, registry };
|
|
545
566
|
for (const installed of plugins) {
|
|
546
567
|
const hook = installed.onCommandAttach;
|
|
547
568
|
if (hook) {
|
|
548
569
|
const identity = installed.identity;
|
|
549
|
-
const value = new AttachedCommandValue({ ...progress, ...facts, identity });
|
|
570
|
+
const value = new AttachedCommandValue({ ...progress, ...facts, identity, subject: layer });
|
|
550
571
|
const state = attachOnce(value, hook, { identity, lineage, subject });
|
|
551
|
-
progress = {
|
|
572
|
+
progress = {
|
|
573
|
+
calls: state.calls,
|
|
574
|
+
declared: state.declared,
|
|
575
|
+
extensions: state.extensions,
|
|
576
|
+
registry: state.registry,
|
|
577
|
+
};
|
|
552
578
|
}
|
|
553
579
|
}
|
|
554
|
-
return progress
|
|
580
|
+
return progress;
|
|
555
581
|
}
|
|
556
582
|
/**
|
|
557
583
|
* One input a hook declared. It joins the values an action reads at run time and the declaration's
|
|
@@ -575,15 +601,10 @@ function declareHookInput(state, input) {
|
|
|
575
601
|
function applyAttachCalls(state, calls) {
|
|
576
602
|
let next = state;
|
|
577
603
|
for (const call of calls) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
next = { ...next, results: [...next.results, call.call] };
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
next = declareExtensions(next, call.values);
|
|
586
|
-
}
|
|
604
|
+
next =
|
|
605
|
+
call.kind === 'input'
|
|
606
|
+
? declareHookInput(next, call.input)
|
|
607
|
+
: { ...next, results: [...next.results, call.call] };
|
|
587
608
|
}
|
|
588
609
|
return next;
|
|
589
610
|
}
|
|
@@ -763,7 +784,8 @@ export function buildCommand(state, context) {
|
|
|
763
784
|
const description = checkDescription(commandSentence(name), state.description);
|
|
764
785
|
const hidden = checkHidden(commandSentence(name), state.hidden);
|
|
765
786
|
const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
|
|
766
|
-
|
|
787
|
+
// The author's layers validate before any hook runs on this Command, so a hook reads them.
|
|
788
|
+
const declaredExtensions = storeCommandLayers({
|
|
767
789
|
descriptors: context.descriptors,
|
|
768
790
|
layers: state.extensions,
|
|
769
791
|
subject: layer,
|
|
@@ -787,20 +809,20 @@ export function buildCommand(state, context) {
|
|
|
787
809
|
*/
|
|
788
810
|
// One token per Command per build, which binds a hook's return to the value it received.
|
|
789
811
|
const lineage = {};
|
|
790
|
-
const
|
|
812
|
+
const progress = runAttachHooks({ declared: state, extensions: declaredExtensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
|
|
813
|
+
const { calls } = progress;
|
|
814
|
+
// The descriptors the returned value's extensions registered join the build's registry.
|
|
815
|
+
for (const [identity, descriptor] of progress.registry) {
|
|
816
|
+
context.descriptors.set(identity, descriptor);
|
|
817
|
+
}
|
|
791
818
|
const hooked = applyAttachCalls(state, calls);
|
|
792
819
|
const hookInputs = calls.filter((call) => call.kind === 'input');
|
|
793
820
|
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
794
821
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
795
822
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
796
823
|
checkAttachedInputs(hooked, hookInputs, globals);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
descriptors: context.descriptors,
|
|
800
|
-
layers: hooked.extensions,
|
|
801
|
-
subject: layer,
|
|
802
|
-
})
|
|
803
|
-
: declaredExtensions;
|
|
824
|
+
// Every value was validated once, the author's above and each hook's at its `extend()` call.
|
|
825
|
+
const extensions = publishStore(progress.extensions);
|
|
804
826
|
const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
|
|
805
827
|
checkArgumentPlacement(name, slots[0], attached[0]);
|
|
806
828
|
const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
2
|
import type { ArgumentNode, CommandNode, OptionNode } from './inspect.js';
|
|
3
|
+
import type { AttachedCommand } from './types.js';
|
|
3
4
|
/** The three declaration kinds an extension can name, each with its own node in the graph. */
|
|
4
5
|
type ExtensionTarget = 'argument' | 'command' | 'option';
|
|
5
6
|
/**
|
|
6
|
-
* The descriptor supertype a plugin's `extensions` list uses. It publishes the identity
|
|
7
|
-
* target and erases both the schema and the factory call
|
|
8
|
-
* signature relates only by schema identity and a list
|
|
9
|
-
* descriptor is assignable to it; an extension value, which
|
|
7
|
+
* The descriptor supertype a plugin's `extensions` list uses. It publishes the identity, the
|
|
8
|
+
* target, and whether the extension collects, and erases both the schema and the factory call
|
|
9
|
+
* signature, because a schema-typed call signature relates only by schema identity and a list
|
|
10
|
+
* cannot name one schema per element. A descriptor is assignable to it; an extension value, which
|
|
11
|
+
* publishes its brand alone, is not.
|
|
10
12
|
*/
|
|
11
13
|
interface AnyExtension {
|
|
12
14
|
readonly identity: string;
|
|
13
15
|
readonly target: ExtensionTarget;
|
|
16
|
+
readonly collect: boolean;
|
|
14
17
|
}
|
|
18
|
+
/** What a value must show to be taken for a descriptor before its `collect` flag is checked. */
|
|
19
|
+
type DescriptorShape = Pick<AnyExtension, 'identity' | 'target'>;
|
|
15
20
|
/** One carried value: the input its author supplied and the descriptor that produced it. */
|
|
16
21
|
interface CarriedValue {
|
|
17
22
|
descriptor: AnyExtension;
|
|
@@ -32,33 +37,52 @@ declare class ExtensionCarrier<Target extends ExtensionTarget> {
|
|
|
32
37
|
type ExtensionValue<Target extends ExtensionTarget> = Pick<ExtensionCarrier<Target>, typeof extensionTarget>;
|
|
33
38
|
/**
|
|
34
39
|
* 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
|
|
40
|
+
* a declaration carries. The schema is public so a typed read recovers the output type, and
|
|
41
|
+
* `collect` says whether the values a declaration carries accumulate or replace each other.
|
|
36
42
|
*/
|
|
37
|
-
interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1> extends AnyExtension {
|
|
43
|
+
interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1, Collect extends boolean = false> extends AnyExtension {
|
|
38
44
|
(input: StandardSchemaV1.InferInput<Schema>): ExtensionValue<Target>;
|
|
39
45
|
readonly schema: Schema;
|
|
40
46
|
readonly target: Target;
|
|
47
|
+
readonly collect: Collect;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* 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.
|
|
51
|
+
* it appears, so one identity means one descriptor and a duplicated package copy is visible. With
|
|
52
|
+
* `collect: true` it is a collecting extension, whose values accumulate on a declaration in order.
|
|
45
53
|
*/
|
|
46
54
|
declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
|
|
47
55
|
schema: Schema;
|
|
48
56
|
target: Target;
|
|
57
|
+
collect?: false | undefined;
|
|
49
58
|
}): Extension<Target, Schema>;
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
|
|
60
|
+
schema: Schema;
|
|
61
|
+
target: Target;
|
|
62
|
+
collect: true;
|
|
63
|
+
}): Extension<Target, Schema, true>;
|
|
64
|
+
/**
|
|
65
|
+
* The node kind one descriptor's target names, so a read against another kind cannot compile. A
|
|
66
|
+
* Command-target read also takes the value a lifecycle hook receives, which publishes the record
|
|
67
|
+
* as it stands at that hook.
|
|
68
|
+
*/
|
|
69
|
+
type NodeFor<Target extends ExtensionTarget> = Target extends 'command' ? CommandNode | AttachedCommand : Target extends 'option' ? OptionNode : ArgumentNode;
|
|
52
70
|
/** Stored output is plain data the graph froze, so every read of it is read-only to any depth. */
|
|
53
71
|
type DeepReadonly<Value> = Value extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : Value extends object ? {
|
|
54
72
|
readonly [Key in keyof Value]: DeepReadonly<Value[Key]>;
|
|
55
73
|
} : Value;
|
|
74
|
+
/**
|
|
75
|
+
* What one read answers, decided by the descriptor's own `collect` type: a collecting extension's
|
|
76
|
+
* outputs as a read-only list, or an ordinary extension's output or nothing.
|
|
77
|
+
*/
|
|
78
|
+
type ExtensionRead<Schema extends StandardSchemaV1, Collect extends boolean> = Collect extends true ? readonly DeepReadonly<StandardSchemaV1.InferOutput<Schema>>[] : DeepReadonly<StandardSchemaV1.InferOutput<Schema>> | undefined;
|
|
56
79
|
/**
|
|
57
80
|
* The typed read of one extension value. It takes the node kind the descriptor targets, returns the
|
|
58
81
|
* stored output or `undefined`, compares the descriptor by reference with the one that produced the
|
|
59
|
-
* value, and runs no schema.
|
|
82
|
+
* value, and runs no schema. Through a collecting descriptor it returns every collected output in
|
|
83
|
+
* collection order, and an empty list where the node carries none.
|
|
60
84
|
*/
|
|
61
|
-
declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(node: NodeFor<Target>, descriptor: Extension<Target, Schema>):
|
|
85
|
+
declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1, Collect extends boolean = false>(node: NodeFor<Target>, descriptor: Extension<Target, Schema, Collect>): ExtensionRead<Schema, Collect>;
|
|
62
86
|
/** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
|
|
63
87
|
interface ExtensionSubject {
|
|
64
88
|
/** The subject after a preposition, such as `on Command "get"`. */
|
|
@@ -74,10 +98,13 @@ type DescriptorRegistry = Map<string, AnyExtension>;
|
|
|
74
98
|
* build alone and inspection reads them back with the nodes it renders.
|
|
75
99
|
*/
|
|
76
100
|
type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
|
|
103
|
+
* target. Its `collect` flag is checked where a build registers it.
|
|
104
|
+
*/
|
|
105
|
+
declare function isDescriptor(value: unknown): value is DescriptorShape;
|
|
106
|
+
/** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
|
|
107
|
+
declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: DescriptorShape): void;
|
|
81
108
|
/** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
|
|
82
109
|
interface ExtensionSlot {
|
|
83
110
|
declared: unknown;
|
|
@@ -85,15 +112,44 @@ interface ExtensionSlot {
|
|
|
85
112
|
subject: ExtensionSubject;
|
|
86
113
|
target: ExtensionTarget;
|
|
87
114
|
}
|
|
115
|
+
/** One carried value, validated against its descriptor's schema and ready to store. */
|
|
116
|
+
interface ValidatedValue {
|
|
117
|
+
descriptor: AnyExtension;
|
|
118
|
+
output: unknown;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One layer's values, each validated once, synchronously, in authoring order. A layer holds at
|
|
122
|
+
* most one value of an extension, collecting or not, so a second one is a declaration fault.
|
|
123
|
+
*/
|
|
124
|
+
declare function validateLayer(slot: ExtensionSlot): readonly ValidatedValue[];
|
|
125
|
+
/**
|
|
126
|
+
* The values one declaration has validated so far, by identity in the order each first appeared:
|
|
127
|
+
* an ordinary extension's latest output alone, and a collecting extension's every output in
|
|
128
|
+
* collection order. A store is never changed; adding a layer answers a new one.
|
|
129
|
+
*/
|
|
130
|
+
interface ExtensionStore {
|
|
131
|
+
readonly entries: ReadonlyMap<string, {
|
|
132
|
+
readonly descriptor: AnyExtension;
|
|
133
|
+
readonly outputs: readonly unknown[];
|
|
134
|
+
}>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The store with one more validated layer. An ordinary value replaces the earlier one in place, so
|
|
138
|
+
* a key keeps its first position, and a collecting value joins the values before it.
|
|
139
|
+
*/
|
|
140
|
+
declare function extendStore(store: ExtensionStore, layer: readonly ValidatedValue[]): ExtensionStore;
|
|
88
141
|
/**
|
|
89
|
-
* The frozen record
|
|
90
|
-
*
|
|
91
|
-
* the record, so a typed read compares by reference without the graph carrying a
|
|
142
|
+
* The frozen record a store publishes: each ordinary extension's output, and each collecting
|
|
143
|
+
* extension's frozen list of outputs, under its identity. The descriptors that produced them are
|
|
144
|
+
* recorded beside the record, so a typed read compares by reference without the graph carrying a
|
|
145
|
+
* reference.
|
|
92
146
|
*/
|
|
147
|
+
declare function publishStore(store: ExtensionStore): Readonly<Record<string, unknown>>;
|
|
148
|
+
/** The frozen record of one `extensions` slot, which is one layer. */
|
|
93
149
|
declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
|
|
94
|
-
/**
|
|
95
|
-
declare function
|
|
150
|
+
/** A Command's author layers, validated in authoring order into the store its hooks extend. */
|
|
151
|
+
declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
|
|
96
152
|
layers: readonly unknown[];
|
|
97
|
-
}):
|
|
98
|
-
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
99
|
-
export {
|
|
153
|
+
}): ExtensionStore;
|
|
154
|
+
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
155
|
+
export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
|
package/dist/extension.js
CHANGED
|
@@ -19,16 +19,15 @@ class ExtensionCarrier {
|
|
|
19
19
|
Object.freeze(this);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* One typed fact a plugin defines for one target. The descriptor is compared by reference wherever
|
|
24
|
-
* it appears, so one identity means one descriptor and a duplicated package copy is visible.
|
|
25
|
-
*/
|
|
26
22
|
function extension(identity, config) {
|
|
27
23
|
// `Object.assign` returns the same function object, so the value carries the descriptor itself.
|
|
28
24
|
function create(input) {
|
|
29
25
|
return new ExtensionCarrier({ descriptor, input });
|
|
30
26
|
}
|
|
27
|
+
// An omitted or undefined `collect` publishes as false, and any other value as given.
|
|
28
|
+
// Build then rejects a JavaScript author's value that is neither Boolean.
|
|
31
29
|
const descriptor = Object.assign(create, {
|
|
30
|
+
collect: config.collect === undefined ? false : config.collect,
|
|
32
31
|
identity,
|
|
33
32
|
schema: config.schema,
|
|
34
33
|
target: config.target,
|
|
@@ -36,26 +35,31 @@ function extension(identity, config) {
|
|
|
36
35
|
Object.freeze(descriptor);
|
|
37
36
|
return descriptor;
|
|
38
37
|
}
|
|
38
|
+
/** The read of a collecting extension a declaration carries no value of, shared and frozen. */
|
|
39
|
+
const noValues = Object.freeze([]);
|
|
39
40
|
/**
|
|
40
41
|
* The typed read of one extension value. It takes the node kind the descriptor targets, returns the
|
|
41
42
|
* stored output or `undefined`, compares the descriptor by reference with the one that produced the
|
|
42
|
-
* value, and runs no schema.
|
|
43
|
+
* value, and runs no schema. Through a collecting descriptor it returns every collected output in
|
|
44
|
+
* collection order, and an empty list where the node carries none.
|
|
43
45
|
*/
|
|
44
46
|
function readExtension(node, descriptor) {
|
|
45
47
|
const record = node.extensions;
|
|
46
48
|
const owner = owners.get(record)?.get(descriptor.identity);
|
|
47
|
-
if (owner
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
if (owner !== descriptor) {
|
|
49
|
+
if (owner !== undefined && owner !== descriptor) {
|
|
51
50
|
throw new DeclarationError(`Extension "${descriptor.identity}" was read through a descriptor that did not define the stored value. Install one copy of the package that defines it.`);
|
|
52
51
|
}
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
52
|
+
// A collecting extension a declaration carries no value of reads as an empty list.
|
|
53
|
+
const absent = descriptor.collect ? noValues : undefined;
|
|
54
|
+
const stored = owner === undefined ? absent : record[descriptor.identity];
|
|
55
|
+
// Last resort: no typed path exists.
|
|
56
|
+
// The graph stores every output under a string identity, so the record reads back as `unknown`.
|
|
57
|
+
// No key relates one entry to a descriptor's schema or to its runtime `collect` flag.
|
|
58
|
+
// It holds because the reference test above proves this descriptor produced this entry.
|
|
59
|
+
// Build stored exactly what its schema returned, a list when the descriptor collects.
|
|
60
|
+
// `Collect` is the literal `extension()` published as `collect`, which registration enforces.
|
|
57
61
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
58
|
-
return
|
|
62
|
+
return stored;
|
|
59
63
|
}
|
|
60
64
|
/** How a diagnostic names the declarations one target covers. */
|
|
61
65
|
const applies = {
|
|
@@ -172,7 +176,10 @@ function plainData(value) {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
|
|
181
|
+
* target. Its `collect` flag is checked where a build registers it.
|
|
182
|
+
*/
|
|
176
183
|
function isDescriptor(value) {
|
|
177
184
|
return (value !== null &&
|
|
178
185
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
@@ -181,16 +188,37 @@ function isDescriptor(value) {
|
|
|
181
188
|
'target' in value &&
|
|
182
189
|
(value.target === 'argument' || value.target === 'command' || value.target === 'option'));
|
|
183
190
|
}
|
|
184
|
-
/**
|
|
185
|
-
function
|
|
191
|
+
/** Whether a descriptor carries the `collect` flag every descriptor publishes, as a Boolean. */
|
|
192
|
+
function hasCollectFlag(descriptor) {
|
|
193
|
+
return 'collect' in descriptor && typeof descriptor.collect === 'boolean';
|
|
194
|
+
}
|
|
195
|
+
/** Whether one registered descriptor collects, so its values accumulate instead of replacing. */
|
|
196
|
+
function collects(descriptor) {
|
|
197
|
+
return descriptor.collect;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The descriptor a registry holds for one identity once this one is admitted. One identity means
|
|
201
|
+
* one descriptor, wherever on the graph that descriptor appears, and a descriptor is checked when
|
|
202
|
+
* a build first meets it: its `collect` flag is `true` or `false`, which the factory always
|
|
203
|
+
* publishes and a hand-built descriptor may not. It reads the registry and changes nothing.
|
|
204
|
+
*/
|
|
205
|
+
function admitDescriptor(descriptors, descriptor) {
|
|
186
206
|
const known = descriptors.get(descriptor.identity);
|
|
187
207
|
if (known === undefined) {
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
if (!hasCollectFlag(descriptor)) {
|
|
209
|
+
throw new DeclarationError(`Extension "${descriptor.identity}" declares collect that is not a Boolean. Supply true or false, or build the descriptor with extension(identity, config).`);
|
|
210
|
+
}
|
|
211
|
+
return descriptor;
|
|
190
212
|
}
|
|
191
213
|
if (known !== descriptor) {
|
|
192
214
|
throw new DeclarationError(`Extension "${descriptor.identity}" is defined twice. Install one copy of the package that defines it.`);
|
|
193
215
|
}
|
|
216
|
+
return known;
|
|
217
|
+
}
|
|
218
|
+
/** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
|
|
219
|
+
function registerDescriptor(descriptors, descriptor) {
|
|
220
|
+
const admitted = admitDescriptor(descriptors, descriptor);
|
|
221
|
+
descriptors.set(admitted.identity, admitted);
|
|
194
222
|
}
|
|
195
223
|
/** Whether a value answers the Standard Schema v1 contract this build calls synchronously. */
|
|
196
224
|
function isSchema(value) {
|
|
@@ -286,45 +314,85 @@ function carriedValue(subject, target, entry) {
|
|
|
286
314
|
return carried;
|
|
287
315
|
}
|
|
288
316
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* the record, so a typed read compares by reference without the graph carrying a reference.
|
|
317
|
+
* One layer's values, each validated once, synchronously, in authoring order. A layer holds at
|
|
318
|
+
* most one value of an extension, collecting or not, so a second one is a declaration fault.
|
|
292
319
|
*/
|
|
293
|
-
function
|
|
320
|
+
function validateLayer(slot) {
|
|
294
321
|
const { subject } = slot;
|
|
295
|
-
const
|
|
296
|
-
const
|
|
322
|
+
const layer = [];
|
|
323
|
+
const seen = new Set();
|
|
324
|
+
// The layer's descriptors register together once every value is valid.
|
|
325
|
+
// A rejected layer, such as a hook's `extend()` call the hook catches, leaves the registry as it was.
|
|
326
|
+
const staged = new Map(slot.descriptors);
|
|
297
327
|
for (const entry of readList(subject, slot.declared)) {
|
|
298
328
|
const carried = carriedValue(subject, slot.target, entry);
|
|
299
329
|
const { descriptor } = carried;
|
|
300
|
-
registerDescriptor(
|
|
301
|
-
if (
|
|
330
|
+
registerDescriptor(staged, descriptor);
|
|
331
|
+
if (seen.has(descriptor.identity)) {
|
|
302
332
|
throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}" twice. Supply one value.`);
|
|
303
333
|
}
|
|
304
|
-
|
|
305
|
-
|
|
334
|
+
seen.add(descriptor.identity);
|
|
335
|
+
layer.push({ descriptor, output: validateValue(subject, carried) });
|
|
336
|
+
}
|
|
337
|
+
for (const [identity, descriptor] of staged) {
|
|
338
|
+
slot.descriptors.set(identity, descriptor);
|
|
339
|
+
}
|
|
340
|
+
return layer;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The record each store published, so a store a hook left unchanged publishes the same record on
|
|
344
|
+
* every read and build does no second walk of it.
|
|
345
|
+
*/
|
|
346
|
+
const published = new WeakMap();
|
|
347
|
+
/** The store of a declaration that carries no value yet. */
|
|
348
|
+
const emptyStore = { entries: new Map() };
|
|
349
|
+
/**
|
|
350
|
+
* The store with one more validated layer. An ordinary value replaces the earlier one in place, so
|
|
351
|
+
* a key keeps its first position, and a collecting value joins the values before it.
|
|
352
|
+
*/
|
|
353
|
+
function extendStore(store, layer) {
|
|
354
|
+
const entries = new Map(store.entries);
|
|
355
|
+
for (const { descriptor, output } of layer) {
|
|
356
|
+
const earlier = entries.get(descriptor.identity)?.outputs ?? [];
|
|
357
|
+
const outputs = collects(descriptor) ? [...earlier, output] : [output];
|
|
358
|
+
entries.set(descriptor.identity, { descriptor, outputs });
|
|
359
|
+
}
|
|
360
|
+
return { entries };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The frozen record a store publishes: each ordinary extension's output, and each collecting
|
|
364
|
+
* extension's frozen list of outputs, under its identity. The descriptors that produced them are
|
|
365
|
+
* recorded beside the record, so a typed read compares by reference without the graph carrying a
|
|
366
|
+
* reference.
|
|
367
|
+
*/
|
|
368
|
+
function publishStore(store) {
|
|
369
|
+
const cached = published.get(store);
|
|
370
|
+
if (cached) {
|
|
371
|
+
return cached;
|
|
372
|
+
}
|
|
373
|
+
const stored = [];
|
|
374
|
+
const defined = new Map();
|
|
375
|
+
for (const [identity, { descriptor, outputs }] of store.entries) {
|
|
376
|
+
stored.push([identity, collects(descriptor) ? Object.freeze([...outputs]) : outputs[0]]);
|
|
377
|
+
defined.set(identity, descriptor);
|
|
306
378
|
}
|
|
307
379
|
// `Object.fromEntries` defines each identity as an own data property, so an identity of
|
|
308
380
|
// `__proto__` is a key of the record and the record keeps `Object.prototype`.
|
|
309
381
|
const frozen = Object.freeze(Object.fromEntries(stored));
|
|
310
382
|
owners.set(frozen, defined);
|
|
383
|
+
published.set(store, frozen);
|
|
311
384
|
return frozen;
|
|
312
385
|
}
|
|
313
|
-
/**
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
386
|
+
/** The frozen record of one `extensions` slot, which is one layer. */
|
|
387
|
+
function buildExtensions(slot) {
|
|
388
|
+
return publishStore(extendStore(emptyStore, validateLayer(slot)));
|
|
389
|
+
}
|
|
390
|
+
/** A Command's author layers, validated in authoring order into the store its hooks extend. */
|
|
391
|
+
function storeCommandLayers(slot) {
|
|
392
|
+
let store = emptyStore;
|
|
317
393
|
for (const declared of slot.layers) {
|
|
318
|
-
|
|
319
|
-
for (const [identity, value] of Object.entries(layer)) {
|
|
320
|
-
stored.set(identity, value);
|
|
321
|
-
}
|
|
322
|
-
for (const [identity, descriptor] of owners.get(layer) ?? []) {
|
|
323
|
-
defined.set(identity, descriptor);
|
|
324
|
-
}
|
|
394
|
+
store = extendStore(store, validateLayer({ ...slot, declared, target: 'command' }));
|
|
325
395
|
}
|
|
326
|
-
|
|
327
|
-
owners.set(frozen, defined);
|
|
328
|
-
return frozen;
|
|
396
|
+
return store;
|
|
329
397
|
}
|
|
330
|
-
export {
|
|
398
|
+
export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type { RenderingPolicy } from './rendering.js';
|
|
|
11
11
|
export type { ViewContext } from './types.js';
|
|
12
12
|
export { pad, style } from './style.js';
|
|
13
13
|
export { issuePath } from './validation.js';
|
|
14
|
-
export type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
14
|
+
export type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
|
|
15
15
|
export type { ApplicationMethod, ApplicationOptions } from './application.js';
|
|
16
16
|
export type { ChainOutcome, MiddlewareContext } from './chain.js';
|
|
17
17
|
export type { InputProblem, ResultFault } from './errors.js';
|
package/dist/inspect.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { BuiltGraph } from './command.js';
|
|
2
2
|
import type { DeclaredResult } from './types.js';
|
|
3
|
-
/**
|
|
3
|
+
/** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
|
|
4
|
+
type InputSchema = Readonly<Record<string, unknown>> | null;
|
|
5
|
+
/**
|
|
6
|
+
* One declared argument. `default` wraps the declared value, so an explicit `undefined` shows, and
|
|
7
|
+
* `schema` is the input schema its validator publishes through the Standard JSON Schema converter.
|
|
8
|
+
*/
|
|
4
9
|
interface ArgumentNode {
|
|
5
10
|
readonly name: string;
|
|
6
11
|
readonly description: string | undefined;
|
|
@@ -8,6 +13,7 @@ interface ArgumentNode {
|
|
|
8
13
|
readonly variadic: boolean;
|
|
9
14
|
readonly validated: boolean;
|
|
10
15
|
readonly validateOmitted: boolean;
|
|
16
|
+
readonly schema: InputSchema;
|
|
11
17
|
readonly default: {
|
|
12
18
|
readonly value: unknown;
|
|
13
19
|
} | undefined;
|
|
@@ -21,6 +27,9 @@ interface ArgumentNode {
|
|
|
21
27
|
* `hidden` is `false` unless the declaration says `true`, and `deprecated` is the declared
|
|
22
28
|
* migration message or `undefined`. A listing projection omits a hidden node and marks a
|
|
23
29
|
* deprecated one; parsing binds without reading either.
|
|
30
|
+
* `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
|
|
31
|
+
* variant carries the field at `null`, and every projection built on the node holds if a later
|
|
32
|
+
* contract lets it validate.
|
|
24
33
|
*/
|
|
25
34
|
type OptionNode = {
|
|
26
35
|
readonly type: 'string';
|
|
@@ -35,6 +44,7 @@ type OptionNode = {
|
|
|
35
44
|
readonly multiple: boolean;
|
|
36
45
|
readonly validated: boolean;
|
|
37
46
|
readonly validateOmitted: boolean;
|
|
47
|
+
readonly schema: InputSchema;
|
|
38
48
|
readonly default: {
|
|
39
49
|
readonly value: unknown;
|
|
40
50
|
} | undefined;
|
|
@@ -50,6 +60,7 @@ type OptionNode = {
|
|
|
50
60
|
readonly short: string | null;
|
|
51
61
|
readonly negative: string | null;
|
|
52
62
|
readonly polarity: 'positive' | 'negative' | 'both';
|
|
63
|
+
readonly schema: InputSchema;
|
|
53
64
|
readonly extensions: Readonly<Record<string, unknown>>;
|
|
54
65
|
};
|
|
55
66
|
/**
|
package/dist/inspect.js
CHANGED
|
@@ -28,14 +28,60 @@ export function snapshot(value) {
|
|
|
28
28
|
return Object.freeze(value.map((entry) => snapshot(entry)));
|
|
29
29
|
}
|
|
30
30
|
if (isPlainObject(value)) {
|
|
31
|
-
return
|
|
31
|
+
return snapshotRecord(value);
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
/** The snapshot of one plain object, under the record type the caller already established. */
|
|
36
|
+
function snapshotRecord(value) {
|
|
37
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
|
|
38
|
+
}
|
|
35
39
|
/** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
|
|
36
40
|
function declaredDefault(config) {
|
|
37
41
|
return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
|
|
38
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The JSON Schema draft build asks every converter for, with no library options. Every converter
|
|
45
|
+
* receives this one object, so it is frozen against a library that writes to its argument.
|
|
46
|
+
*/
|
|
47
|
+
const schemaTarget = Object.freeze({ target: 'draft-2020-12' });
|
|
48
|
+
/** Whether a validator declares the Standard JSON Schema converter, both sides, beside `validate`. */
|
|
49
|
+
function publishesSchema(schema) {
|
|
50
|
+
const props = schema['~standard'];
|
|
51
|
+
return ('jsonSchema' in props &&
|
|
52
|
+
typeof props.jsonSchema === 'object' &&
|
|
53
|
+
props.jsonSchema !== null &&
|
|
54
|
+
'input' in props.jsonSchema &&
|
|
55
|
+
typeof props.jsonSchema.input === 'function' &&
|
|
56
|
+
'output' in props.jsonSchema &&
|
|
57
|
+
typeof props.jsonSchema.output === 'function');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The input-side schema a declaration's validator publishes, snapshotted the way a declared
|
|
61
|
+
* default is, or `null` where the graph holds no published shape: no validator, a validator with
|
|
62
|
+
* no converter, or a converter that throws or returns anything but a plain object. The contract of
|
|
63
|
+
* 2026-09-19 made that last case a declaration error `inspect()` alone reports; that diagnostic is
|
|
64
|
+
* held while the question of how a run tells development from a distributed application is
|
|
65
|
+
* decided, so it reads `null` on both paths.
|
|
66
|
+
*/
|
|
67
|
+
function inputSchema(config) {
|
|
68
|
+
const schema = 'validate' in config ? config.validate : undefined;
|
|
69
|
+
if (schema === undefined) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// The converter is the library's code from the first property read.
|
|
73
|
+
// A throw on reaching it and a throw on calling it are one failure.
|
|
74
|
+
try {
|
|
75
|
+
if (!publishesSchema(schema)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const published = schema['~standard'].jsonSchema.input(schemaTarget);
|
|
79
|
+
return isPlainObject(published) ? snapshotRecord(published) : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
39
85
|
/** One declaration's extension record, which is the shared empty one when it carries no value. */
|
|
40
86
|
function extensionsOf(records, declaration) {
|
|
41
87
|
return records.get(declaration) ?? noExtensions;
|
|
@@ -54,6 +100,7 @@ function optionNode(input, { records, scope, table }) {
|
|
|
54
100
|
name,
|
|
55
101
|
negative,
|
|
56
102
|
polarity: config.polarity ?? 'positive',
|
|
103
|
+
schema: null,
|
|
57
104
|
scope,
|
|
58
105
|
short,
|
|
59
106
|
type: 'boolean',
|
|
@@ -69,6 +116,7 @@ function optionNode(input, { records, scope, table }) {
|
|
|
69
116
|
multiple: config.multiple === true,
|
|
70
117
|
name,
|
|
71
118
|
required: config.required === true,
|
|
119
|
+
schema: inputSchema(config),
|
|
72
120
|
scope,
|
|
73
121
|
short,
|
|
74
122
|
type: 'string',
|
|
@@ -86,6 +134,7 @@ function argumentNode(slot, records) {
|
|
|
86
134
|
extensions: extensionsOf(records, slot.input),
|
|
87
135
|
name,
|
|
88
136
|
required: slot.required,
|
|
137
|
+
schema: inputSchema(config),
|
|
89
138
|
validateOmitted: validatesOmission(slot.input),
|
|
90
139
|
validated: config.validate !== undefined,
|
|
91
140
|
variadic: slot.variadic,
|
package/dist/types.d.ts
CHANGED
|
@@ -211,6 +211,11 @@ export interface AttachedCommand {
|
|
|
211
211
|
/** The declared local option names, in declaration order. */
|
|
212
212
|
readonly options: readonly string[];
|
|
213
213
|
readonly result: ResultNode | null;
|
|
214
|
+
/**
|
|
215
|
+
* The extension record `inspect()` would publish for this Command at this hook: the author's
|
|
216
|
+
* layers, then every value an earlier hook or this hook's earlier `extend()` calls added.
|
|
217
|
+
*/
|
|
218
|
+
readonly extensions: Readonly<Record<string, unknown>>;
|
|
214
219
|
argument(name: string, config: ArgumentConfig): AttachedCommand;
|
|
215
220
|
option(name: string, config: OptionConfig): AttachedCommand;
|
|
216
221
|
views(replacements: Readonly<Record<string, ResultView>>, options?: {
|