@loomcli/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/application.d.ts +44 -31
- package/dist/application.js +122 -110
- package/dist/bindings.d.ts +26 -0
- package/dist/bindings.js +45 -0
- package/dist/chain.d.ts +11 -6
- package/dist/chain.js +28 -81
- package/dist/command.d.ts +161 -84
- package/dist/command.js +692 -369
- package/dist/errors.d.ts +7 -2
- package/dist/errors.js +9 -1
- package/dist/extension.d.ts +81 -23
- package/dist/extension.js +119 -54
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +10 -3
- package/dist/globals.d.ts +45 -12
- package/dist/globals.js +74 -18
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +37 -3
- package/dist/inspect.js +93 -6
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +73 -0
- package/dist/options.js +124 -27
- package/dist/output.d.ts +2 -0
- package/dist/output.js +5 -1
- package/dist/plugin.d.ts +107 -53
- package/dist/plugin.js +204 -66
- package/dist/sources.d.ts +56 -0
- package/dist/sources.js +249 -0
- package/dist/types.d.ts +70 -20
- package/dist/validation.d.ts +22 -8
- package/dist/validation.js +184 -77
- package/dist/view.d.ts +1 -1
- package/dist/view.js +2 -2
- package/package.json +3 -2
package/dist/errors.d.ts
CHANGED
|
@@ -111,8 +111,8 @@ export declare class InternalError extends LoomError {
|
|
|
111
111
|
readonly cause: unknown;
|
|
112
112
|
constructor(message: string, cause: unknown);
|
|
113
113
|
}
|
|
114
|
-
/** The
|
|
115
|
-
export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware';
|
|
114
|
+
/** The five ways the results lane is broken, each named where core meets it. */
|
|
115
|
+
export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware' | 'source';
|
|
116
116
|
/**
|
|
117
117
|
* Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
|
|
118
118
|
* `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
|
|
@@ -125,6 +125,11 @@ export declare class ResultError extends InternalError {
|
|
|
125
125
|
readonly cause: undefined;
|
|
126
126
|
constructor(kind: ResultFault, path: readonly string[]);
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* One message someone else wrote, as a sentence of its own. A schema or a plugin writes its message
|
|
130
|
+
* with or without a full stop, so a diagnostic supplies one only where the message carries none.
|
|
131
|
+
*/
|
|
132
|
+
export declare function asSentence(text: string): string;
|
|
128
133
|
/** What a diagnostic says about an unexpected value, whether or not it was an Error. */
|
|
129
134
|
export declare function reasonOf(thrown: unknown): string;
|
|
130
135
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -14,7 +14,8 @@ function resultMessage(kind, command) {
|
|
|
14
14
|
if (kind === 'undeclared') {
|
|
15
15
|
return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
const caller = kind === 'source' ? 'A configuration source' : 'A middleware';
|
|
18
|
+
return `${caller} called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* The clause that offers the candidates, which is absent when there are none: every child of the
|
|
@@ -210,6 +211,13 @@ export class ResultError extends InternalError {
|
|
|
210
211
|
this.path = path;
|
|
211
212
|
}
|
|
212
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* One message someone else wrote, as a sentence of its own. A schema or a plugin writes its message
|
|
216
|
+
* with or without a full stop, so a diagnostic supplies one only where the message carries none.
|
|
217
|
+
*/
|
|
218
|
+
export function asSentence(text) {
|
|
219
|
+
return text.endsWith('.') ? text : `${text}.`;
|
|
220
|
+
}
|
|
213
221
|
/** What a diagnostic says about an unexpected value, whether or not it was an Error. */
|
|
214
222
|
export function reasonOf(thrown) {
|
|
215
223
|
return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
|
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,54 @@ 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>;
|
|
86
|
+
/** How a diagnostic names the declarations one target covers, such as "Commands". */
|
|
87
|
+
declare function appliesTo(target: ExtensionTarget): string;
|
|
62
88
|
/** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
|
|
63
89
|
interface ExtensionSubject {
|
|
64
90
|
/** The subject after a preposition, such as `on Command "get"`. */
|
|
@@ -74,10 +100,13 @@ type DescriptorRegistry = Map<string, AnyExtension>;
|
|
|
74
100
|
* build alone and inspection reads them back with the nodes it renders.
|
|
75
101
|
*/
|
|
76
102
|
type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
|
|
105
|
+
* target. Its `collect` flag is checked where a build registers it.
|
|
106
|
+
*/
|
|
107
|
+
declare function isDescriptor(value: unknown): value is DescriptorShape;
|
|
108
|
+
/** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
|
|
109
|
+
declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: DescriptorShape): void;
|
|
81
110
|
/** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
|
|
82
111
|
interface ExtensionSlot {
|
|
83
112
|
declared: unknown;
|
|
@@ -85,15 +114,44 @@ interface ExtensionSlot {
|
|
|
85
114
|
subject: ExtensionSubject;
|
|
86
115
|
target: ExtensionTarget;
|
|
87
116
|
}
|
|
117
|
+
/** One carried value, validated against its descriptor's schema and ready to store. */
|
|
118
|
+
interface ValidatedValue {
|
|
119
|
+
descriptor: AnyExtension;
|
|
120
|
+
output: unknown;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* One layer's values, each validated once, synchronously, in authoring order. A layer holds at
|
|
124
|
+
* most one value of an extension, collecting or not, so a second one is a declaration fault.
|
|
125
|
+
*/
|
|
126
|
+
declare function validateLayer(slot: ExtensionSlot): readonly ValidatedValue[];
|
|
127
|
+
/**
|
|
128
|
+
* The values one declaration has validated so far, by identity in the order each first appeared:
|
|
129
|
+
* an ordinary extension's latest output alone, and a collecting extension's every output in
|
|
130
|
+
* collection order. A store is never changed; adding a layer answers a new one.
|
|
131
|
+
*/
|
|
132
|
+
interface ExtensionStore {
|
|
133
|
+
readonly entries: ReadonlyMap<string, {
|
|
134
|
+
readonly descriptor: AnyExtension;
|
|
135
|
+
readonly outputs: readonly unknown[];
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The store with one more validated layer. An ordinary value replaces the earlier one in place, so
|
|
140
|
+
* a key keeps its first position, and a collecting value joins the values before it.
|
|
141
|
+
*/
|
|
142
|
+
declare function extendStore(store: ExtensionStore, layer: readonly ValidatedValue[]): ExtensionStore;
|
|
88
143
|
/**
|
|
89
|
-
* The frozen record
|
|
90
|
-
*
|
|
91
|
-
* the record, so a typed read compares by reference without the graph carrying a
|
|
144
|
+
* The frozen record a store publishes: each ordinary extension's output, and each collecting
|
|
145
|
+
* extension's frozen list of outputs, under its identity. The descriptors that produced them are
|
|
146
|
+
* recorded beside the record, so a typed read compares by reference without the graph carrying a
|
|
147
|
+
* reference.
|
|
92
148
|
*/
|
|
149
|
+
declare function publishStore(store: ExtensionStore): Readonly<Record<string, unknown>>;
|
|
150
|
+
/** The frozen record of one `extensions` slot, which is one layer. */
|
|
93
151
|
declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
|
|
94
|
-
/**
|
|
95
|
-
declare function
|
|
152
|
+
/** A Command's author layers, validated in authoring order into the store its hooks extend. */
|
|
153
|
+
declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
|
|
96
154
|
layers: readonly unknown[];
|
|
97
|
-
}):
|
|
98
|
-
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
99
|
-
export {
|
|
155
|
+
}): ExtensionStore;
|
|
156
|
+
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
157
|
+
export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
|
package/dist/extension.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeclarationError, reasonOf } from './errors.js';
|
|
1
|
+
import { asSentence, DeclarationError, reasonOf } from './errors.js';
|
|
2
2
|
import { isPlainObject } from './facts.js';
|
|
3
3
|
/** Authored values register here, so the public brand publishes no state to reach or replace. */
|
|
4
4
|
const values = new WeakMap();
|
|
@@ -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 = {
|
|
@@ -63,6 +67,10 @@ const applies = {
|
|
|
63
67
|
command: 'Commands',
|
|
64
68
|
option: 'options',
|
|
65
69
|
};
|
|
70
|
+
/** How a diagnostic names the declarations one target covers, such as "Commands". */
|
|
71
|
+
function appliesTo(target) {
|
|
72
|
+
return applies[target];
|
|
73
|
+
}
|
|
66
74
|
/**
|
|
67
75
|
* The children one container contributes, or nothing when its own shape is not plain data. An
|
|
68
76
|
* accessor, a symbol key, and a non-enumerable own property are not plain data, and an `undefined`
|
|
@@ -172,7 +180,10 @@ function plainData(value) {
|
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
|
|
185
|
+
* target. Its `collect` flag is checked where a build registers it.
|
|
186
|
+
*/
|
|
176
187
|
function isDescriptor(value) {
|
|
177
188
|
return (value !== null &&
|
|
178
189
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
@@ -181,16 +192,37 @@ function isDescriptor(value) {
|
|
|
181
192
|
'target' in value &&
|
|
182
193
|
(value.target === 'argument' || value.target === 'command' || value.target === 'option'));
|
|
183
194
|
}
|
|
184
|
-
/**
|
|
185
|
-
function
|
|
195
|
+
/** Whether a descriptor carries the `collect` flag every descriptor publishes, as a Boolean. */
|
|
196
|
+
function hasCollectFlag(descriptor) {
|
|
197
|
+
return 'collect' in descriptor && typeof descriptor.collect === 'boolean';
|
|
198
|
+
}
|
|
199
|
+
/** Whether one registered descriptor collects, so its values accumulate instead of replacing. */
|
|
200
|
+
function collects(descriptor) {
|
|
201
|
+
return descriptor.collect;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The descriptor a registry holds for one identity once this one is admitted. One identity means
|
|
205
|
+
* one descriptor, wherever on the graph that descriptor appears, and a descriptor is checked when
|
|
206
|
+
* a build first meets it: its `collect` flag is `true` or `false`, which the factory always
|
|
207
|
+
* publishes and a hand-built descriptor may not. It reads the registry and changes nothing.
|
|
208
|
+
*/
|
|
209
|
+
function admitDescriptor(descriptors, descriptor) {
|
|
186
210
|
const known = descriptors.get(descriptor.identity);
|
|
187
211
|
if (known === undefined) {
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
if (!hasCollectFlag(descriptor)) {
|
|
213
|
+
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).`);
|
|
214
|
+
}
|
|
215
|
+
return descriptor;
|
|
190
216
|
}
|
|
191
217
|
if (known !== descriptor) {
|
|
192
218
|
throw new DeclarationError(`Extension "${descriptor.identity}" is defined twice. Install one copy of the package that defines it.`);
|
|
193
219
|
}
|
|
220
|
+
return known;
|
|
221
|
+
}
|
|
222
|
+
/** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
|
|
223
|
+
function registerDescriptor(descriptors, descriptor) {
|
|
224
|
+
const admitted = admitDescriptor(descriptors, descriptor);
|
|
225
|
+
descriptors.set(admitted.identity, admitted);
|
|
194
226
|
}
|
|
195
227
|
/** Whether a value answers the Standard Schema v1 contract this build calls synchronously. */
|
|
196
228
|
function isSchema(value) {
|
|
@@ -203,13 +235,6 @@ function isSchema(value) {
|
|
|
203
235
|
'validate' in standard &&
|
|
204
236
|
typeof standard.validate === 'function');
|
|
205
237
|
}
|
|
206
|
-
/**
|
|
207
|
-
* One schema message as a sentence of its own. A schema author writes the message with or without a
|
|
208
|
-
* full stop, so the diagnostic supplies one only where the message carries none.
|
|
209
|
-
*/
|
|
210
|
-
function sentence(text) {
|
|
211
|
-
return text.endsWith('.') ? text : `${text}.`;
|
|
212
|
-
}
|
|
213
238
|
/** The message one rejected value reports, with the placeholder a silent schema earns. */
|
|
214
239
|
function issueText(issues) {
|
|
215
240
|
const first = Array.isArray(issues) ? issues[0] : undefined;
|
|
@@ -244,7 +269,7 @@ function validated(subject, carried, schema) {
|
|
|
244
269
|
}
|
|
245
270
|
catch (error) {
|
|
246
271
|
// A schema that throws rejected the value the only way it could, so it reads as a rejection.
|
|
247
|
-
throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${
|
|
272
|
+
throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${asSentence(reasonOf(error))} Correct the value.`);
|
|
248
273
|
}
|
|
249
274
|
}
|
|
250
275
|
/** Validates one carried value and answers the plain-data output the node stores under it. */
|
|
@@ -256,7 +281,7 @@ function validateValue(subject, carried) {
|
|
|
256
281
|
}
|
|
257
282
|
const issues = 'issues' in result ? result.issues : undefined;
|
|
258
283
|
if (issues !== undefined) {
|
|
259
|
-
throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${
|
|
284
|
+
throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${asSentence(issueText(issues))} Correct the value.`);
|
|
260
285
|
}
|
|
261
286
|
const output = plainData('value' in result ? result.value : undefined);
|
|
262
287
|
if (!output) {
|
|
@@ -286,45 +311,85 @@ function carriedValue(subject, target, entry) {
|
|
|
286
311
|
return carried;
|
|
287
312
|
}
|
|
288
313
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* the record, so a typed read compares by reference without the graph carrying a reference.
|
|
314
|
+
* One layer's values, each validated once, synchronously, in authoring order. A layer holds at
|
|
315
|
+
* most one value of an extension, collecting or not, so a second one is a declaration fault.
|
|
292
316
|
*/
|
|
293
|
-
function
|
|
317
|
+
function validateLayer(slot) {
|
|
294
318
|
const { subject } = slot;
|
|
295
|
-
const
|
|
296
|
-
const
|
|
319
|
+
const layer = [];
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
// The layer's descriptors register together once every value is valid.
|
|
322
|
+
// A rejected layer, such as a hook's `extend()` call the hook catches, leaves the registry as it was.
|
|
323
|
+
const staged = new Map(slot.descriptors);
|
|
297
324
|
for (const entry of readList(subject, slot.declared)) {
|
|
298
325
|
const carried = carriedValue(subject, slot.target, entry);
|
|
299
326
|
const { descriptor } = carried;
|
|
300
|
-
registerDescriptor(
|
|
301
|
-
if (
|
|
327
|
+
registerDescriptor(staged, descriptor);
|
|
328
|
+
if (seen.has(descriptor.identity)) {
|
|
302
329
|
throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}" twice. Supply one value.`);
|
|
303
330
|
}
|
|
304
|
-
|
|
305
|
-
|
|
331
|
+
seen.add(descriptor.identity);
|
|
332
|
+
layer.push({ descriptor, output: validateValue(subject, carried) });
|
|
333
|
+
}
|
|
334
|
+
for (const [identity, descriptor] of staged) {
|
|
335
|
+
slot.descriptors.set(identity, descriptor);
|
|
336
|
+
}
|
|
337
|
+
return layer;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The record each store published, so a store a hook left unchanged publishes the same record on
|
|
341
|
+
* every read and build does no second walk of it.
|
|
342
|
+
*/
|
|
343
|
+
const published = new WeakMap();
|
|
344
|
+
/** The store of a declaration that carries no value yet. */
|
|
345
|
+
const emptyStore = { entries: new Map() };
|
|
346
|
+
/**
|
|
347
|
+
* The store with one more validated layer. An ordinary value replaces the earlier one in place, so
|
|
348
|
+
* a key keeps its first position, and a collecting value joins the values before it.
|
|
349
|
+
*/
|
|
350
|
+
function extendStore(store, layer) {
|
|
351
|
+
const entries = new Map(store.entries);
|
|
352
|
+
for (const { descriptor, output } of layer) {
|
|
353
|
+
const earlier = entries.get(descriptor.identity)?.outputs ?? [];
|
|
354
|
+
const outputs = collects(descriptor) ? [...earlier, output] : [output];
|
|
355
|
+
entries.set(descriptor.identity, { descriptor, outputs });
|
|
356
|
+
}
|
|
357
|
+
return { entries };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The frozen record a store publishes: each ordinary extension's output, and each collecting
|
|
361
|
+
* extension's frozen list of outputs, under its identity. The descriptors that produced them are
|
|
362
|
+
* recorded beside the record, so a typed read compares by reference without the graph carrying a
|
|
363
|
+
* reference.
|
|
364
|
+
*/
|
|
365
|
+
function publishStore(store) {
|
|
366
|
+
const cached = published.get(store);
|
|
367
|
+
if (cached) {
|
|
368
|
+
return cached;
|
|
369
|
+
}
|
|
370
|
+
const stored = [];
|
|
371
|
+
const defined = new Map();
|
|
372
|
+
for (const [identity, { descriptor, outputs }] of store.entries) {
|
|
373
|
+
stored.push([identity, collects(descriptor) ? Object.freeze([...outputs]) : outputs[0]]);
|
|
374
|
+
defined.set(identity, descriptor);
|
|
306
375
|
}
|
|
307
376
|
// `Object.fromEntries` defines each identity as an own data property, so an identity of
|
|
308
377
|
// `__proto__` is a key of the record and the record keeps `Object.prototype`.
|
|
309
378
|
const frozen = Object.freeze(Object.fromEntries(stored));
|
|
310
379
|
owners.set(frozen, defined);
|
|
380
|
+
published.set(store, frozen);
|
|
311
381
|
return frozen;
|
|
312
382
|
}
|
|
313
|
-
/**
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
383
|
+
/** The frozen record of one `extensions` slot, which is one layer. */
|
|
384
|
+
function buildExtensions(slot) {
|
|
385
|
+
return publishStore(extendStore(emptyStore, validateLayer(slot)));
|
|
386
|
+
}
|
|
387
|
+
/** A Command's author layers, validated in authoring order into the store its hooks extend. */
|
|
388
|
+
function storeCommandLayers(slot) {
|
|
389
|
+
let store = emptyStore;
|
|
317
390
|
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
|
-
}
|
|
391
|
+
store = extendStore(store, validateLayer({ ...slot, declared, target: 'command' }));
|
|
325
392
|
}
|
|
326
|
-
|
|
327
|
-
owners.set(frozen, defined);
|
|
328
|
-
return frozen;
|
|
393
|
+
return store;
|
|
329
394
|
}
|
|
330
|
-
export {
|
|
395
|
+
export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
|
package/dist/facts.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line of prose: a string that holds a character other than whitespace and no line terminator.
|
|
3
|
+
* Every one-line fact reads this rule, and so does the label a configuration source answers with.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isProseLine(value: unknown): value is string;
|
|
1
6
|
/**
|
|
2
7
|
* The `description` core fact: one line of prose every projection reads. A value that is not a
|
|
3
8
|
* string fails the same way a blank one does, because the author reads one rule for one fact.
|
package/dist/facts.js
CHANGED
|
@@ -10,6 +10,13 @@ const prose = /\P{White_Space}/u;
|
|
|
10
10
|
* The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
|
|
11
11
|
*/
|
|
12
12
|
const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
|
|
13
|
+
/**
|
|
14
|
+
* One line of prose: a string that holds a character other than whitespace and no line terminator.
|
|
15
|
+
* Every one-line fact reads this rule, and so does the label a configuration source answers with.
|
|
16
|
+
*/
|
|
17
|
+
export function isProseLine(value) {
|
|
18
|
+
return typeof value === 'string' && prose.test(value) && !lineTerminator.test(value);
|
|
19
|
+
}
|
|
13
20
|
/**
|
|
14
21
|
* The `description` core fact: one line of prose every projection reads. A value that is not a
|
|
15
22
|
* string fails the same way a blank one does, because the author reads one rule for one fact.
|
|
@@ -19,7 +26,7 @@ export function checkDescription(subject, value) {
|
|
|
19
26
|
if (value === undefined) {
|
|
20
27
|
return undefined;
|
|
21
28
|
}
|
|
22
|
-
if (
|
|
29
|
+
if (!isProseLine(value)) {
|
|
23
30
|
throw new DeclarationError(`${subject} description must hold a character other than whitespace and no line terminator. Supply a one-line summary.`);
|
|
24
31
|
}
|
|
25
32
|
return value;
|
|
@@ -48,7 +55,7 @@ export function checkDeprecated(subject, value) {
|
|
|
48
55
|
if (value === undefined) {
|
|
49
56
|
return undefined;
|
|
50
57
|
}
|
|
51
|
-
if (
|
|
58
|
+
if (!isProseLine(value)) {
|
|
52
59
|
throw new DeclarationError(`${subject} deprecated message must hold a character other than whitespace and no line terminator. Supply a one-line migration path, such as "Use get instead.".`);
|
|
53
60
|
}
|
|
54
61
|
return value;
|
|
@@ -76,7 +83,7 @@ export function checkVersion(value) {
|
|
|
76
83
|
if (value === undefined) {
|
|
77
84
|
return '0.0.0';
|
|
78
85
|
}
|
|
79
|
-
if (
|
|
86
|
+
if (!isProseLine(value)) {
|
|
80
87
|
throw new DeclarationError('The Application version must be a string that holds a character other than whitespace and no line terminator. Supply a string such as "1.2.0".');
|
|
81
88
|
}
|
|
82
89
|
return value;
|
package/dist/globals.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { BoundOption } from './bindings.js';
|
|
1
2
|
import { DeclarationError } from './errors.js';
|
|
3
|
+
import type { DescriptorRegistry } from './extension.js';
|
|
2
4
|
import { compileOptions } from './options.js';
|
|
3
|
-
import type { BuiltPlugin
|
|
5
|
+
import type { BuiltPlugin } from './plugin.js';
|
|
4
6
|
import type { OptionConfig, OptionValue } from './types.js';
|
|
5
7
|
import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
|
|
6
8
|
/**
|
|
@@ -28,30 +30,61 @@ declare function keyCollision(name: string, first: OptionOwner, second: OptionOw
|
|
|
28
30
|
/** One spelling claimed twice, whichever two scopes claimed it. */
|
|
29
31
|
declare function spellingCollision(spelling: string, first: OptionSite, second: OptionSite): DeclarationError;
|
|
30
32
|
/**
|
|
31
|
-
* The
|
|
33
|
+
* The globals table the pre-scan reads, with every rule that pairs two of its options settled: one
|
|
34
|
+
* spelling map, the scope that owns each key, and the variable each option binds. It holds the
|
|
32
35
|
* application's global options and every installed plugin's options, because the pre-scan reads one
|
|
33
|
-
* table.
|
|
34
|
-
* validated and never reaches an action.
|
|
36
|
+
* table.
|
|
35
37
|
*/
|
|
36
|
-
interface
|
|
37
|
-
bind: (values: ValidatedInputs) => unknown;
|
|
38
|
-
inputs: readonly InputDeclaration[];
|
|
38
|
+
interface GlobalTable {
|
|
39
39
|
names: ReadonlyMap<string, OptionOwner>;
|
|
40
40
|
options: ReturnType<typeof compileOptions>;
|
|
41
|
+
/** The variable each option in the table binds, under the phrase a duplicate names it by. */
|
|
42
|
+
variables: ReadonlyMap<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The compiled table one graph build shares. `inputs` holds the application's declarations alone,
|
|
46
|
+
* because a plugin option is never validated and never reaches an action.
|
|
47
|
+
*/
|
|
48
|
+
interface BuiltGlobals extends GlobalTable {
|
|
49
|
+
bind: (values: ValidatedInputs) => unknown;
|
|
50
|
+
inputs: readonly OptionInput[];
|
|
41
51
|
plugins: readonly BuiltPlugin[];
|
|
42
52
|
}
|
|
43
|
-
/** The
|
|
53
|
+
/** The validated extension record of each declaration, keyed by the declaration itself. */
|
|
54
|
+
type InputRecords = ReadonlyMap<InputDeclaration, Readonly<Record<string, unknown>>>;
|
|
55
|
+
/**
|
|
56
|
+
* The Application's private global declarations, their schema-derived value binder, and the
|
|
57
|
+
* extension record each declaration's own call validated.
|
|
58
|
+
*/
|
|
44
59
|
interface GlobalsState<Globals = unknown> {
|
|
45
60
|
bind: (values: ValidatedInputs) => Globals;
|
|
46
61
|
inputs: readonly OptionInput[];
|
|
62
|
+
records: InputRecords;
|
|
47
63
|
}
|
|
48
64
|
declare function emptyGlobals(): GlobalsState<{}>;
|
|
49
|
-
|
|
65
|
+
/**
|
|
66
|
+
* One global option's own facts, binding, and extension values, checked at its `globalOption()`
|
|
67
|
+
* call against the Application's descriptors. The rules that pair it with another option belong to
|
|
68
|
+
* the table, which the caller rebuilds with it.
|
|
69
|
+
*/
|
|
70
|
+
declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>, descriptors: DescriptorRegistry): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
|
|
50
71
|
/**
|
|
51
72
|
* The one table the pre-scan reads: the application's global options in authoring order, then each
|
|
52
73
|
* installed plugin's options in installation order. Every collision between the two scopes, by key
|
|
53
74
|
* or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
|
|
54
75
|
*/
|
|
55
|
-
declare function
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
declare function globalTable(inputs: readonly OptionInput[], plugins: readonly BuiltPlugin[]): GlobalTable;
|
|
77
|
+
/** The table one graph build shares, which every earlier call already proved free of collisions. */
|
|
78
|
+
declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[]): BuiltGlobals;
|
|
79
|
+
/**
|
|
80
|
+
* One Command's own options against the globals table, compiled for dispatch. The table holds the
|
|
81
|
+
* application's globals and every plugin option, so a local collision reads the same sentence
|
|
82
|
+
* whichever scope on the other side claimed the name, the spelling, or the variable. One
|
|
83
|
+
* invocation's scope is this Command's own options and the table, so a variable binds one option
|
|
84
|
+
* there, while a sibling Command may bind it again.
|
|
85
|
+
*/
|
|
86
|
+
declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable, subject: string): ReturnType<typeof compileOptions>;
|
|
87
|
+
/** The options in one list that bind a variable, each named by the phrase its scope gives it. */
|
|
88
|
+
declare function boundOptions(inputs: readonly OptionInput[], site: (name: string) => string): BoundOption[];
|
|
89
|
+
export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner, OptionSite };
|
|
90
|
+
export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalTable, keyCollision, spellingCollision, };
|