@loomcli/core 0.2.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/application.d.ts +59 -34
- package/dist/application.js +162 -59
- package/dist/chain.d.ts +25 -6
- package/dist/chain.js +99 -15
- package/dist/command.d.ts +146 -42
- package/dist/command.js +642 -78
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -59
- package/dist/errors.js +49 -106
- package/dist/extension.d.ts +80 -20
- package/dist/extension.js +115 -30
- package/dist/globals.d.ts +14 -25
- package/dist/globals.js +10 -61
- package/dist/glyphs.generated.d.ts +464 -0
- package/dist/glyphs.generated.js +491 -0
- package/dist/host.js +2 -1
- package/dist/index.d.ts +15 -6
- package/dist/index.js +5 -2
- package/dist/inspect.d.ts +38 -4
- package/dist/inspect.js +69 -6
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +32 -16
- package/dist/plugin.js +46 -18
- package/dist/rendering.d.ts +21 -0
- package/dist/rendering.js +72 -0
- package/dist/sequence.d.ts +41 -0
- package/dist/sequence.js +225 -0
- package/dist/style-ansi.d.ts +13 -0
- package/dist/style-ansi.js +306 -0
- package/dist/style-layout.d.ts +29 -0
- package/dist/style-layout.js +228 -0
- package/dist/style-resolve.d.ts +6 -0
- package/dist/style-resolve.js +26 -0
- package/dist/style-state.d.ts +14 -0
- package/dist/style-state.js +179 -0
- package/dist/style-wire.d.ts +31 -0
- package/dist/style-wire.js +201 -0
- package/dist/style.d.ts +86 -0
- package/dist/style.js +201 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +22 -0
- package/dist/types.d.ts +174 -21
- package/dist/validation.d.ts +8 -1
- package/dist/validation.js +17 -2
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Plugin } from './plugin.js';
|
|
2
|
+
/** The shallow type information an Application supplies before its Command graph is composed. */
|
|
3
|
+
interface ApplicationEnvironment<Globals = {}, Plugins extends readonly Plugin[] = readonly []> {
|
|
4
|
+
readonly globals: Globals;
|
|
5
|
+
readonly plugins: Plugins;
|
|
6
|
+
}
|
|
7
|
+
interface RegistrationShape {
|
|
8
|
+
environment?: ApplicationEnvironment<unknown, readonly Plugin[]>;
|
|
9
|
+
}
|
|
10
|
+
/** The Application augments this interface once in its own TypeScript compilation context. */
|
|
11
|
+
interface Register extends RegistrationShape {
|
|
12
|
+
}
|
|
13
|
+
type RegisteredEnvironment = Register extends {
|
|
14
|
+
environment: infer Environment;
|
|
15
|
+
} ? Environment extends ApplicationEnvironment<unknown, readonly Plugin[]> ? Environment : never : ApplicationEnvironment;
|
|
16
|
+
type RegisteredGlobals = RegisteredEnvironment['globals'];
|
|
17
|
+
/** A private type marker avoids pretending parsed global values exist before an invocation. */
|
|
18
|
+
declare const applicationEnvironment: unique symbol;
|
|
19
|
+
type EnvironmentOf<Value extends {
|
|
20
|
+
readonly [applicationEnvironment]: ApplicationEnvironment<unknown, readonly Plugin[]>;
|
|
21
|
+
}> = Value[typeof applicationEnvironment];
|
|
22
|
+
export type { ApplicationEnvironment, EnvironmentOf, Register, RegisteredEnvironment, RegisteredGlobals, applicationEnvironment, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
import type { InputIdentity
|
|
2
|
+
import type { InputIdentity } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* The two short-group faults. A value option that is not last in its group names that option's
|
|
5
5
|
* spelling; a group that mixes scopes names the whole group and the two letters that disagree.
|
|
@@ -14,28 +14,25 @@ type ShortGroupFault = {
|
|
|
14
14
|
global: string;
|
|
15
15
|
other: string;
|
|
16
16
|
};
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
declare
|
|
25
|
-
/** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
|
|
26
|
-
declare class RegisteredFailure {
|
|
27
|
-
readonly [failureRegistration]: true;
|
|
28
|
-
constructor(registration: Registration);
|
|
29
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* Core's own text for one failure: its message under the category prefix its class carries, with
|
|
19
|
+
* the trailing newline every view's text carries. The four categories are disjoint branches of the
|
|
20
|
+
* hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
|
|
21
|
+
* the sentence alone. It is the default view of every failure class and the text the plain
|
|
22
|
+
* fallback path writes, so it runs no application code and nothing downstream composes its newline.
|
|
23
|
+
*/
|
|
24
|
+
export declare function defaultText(failure: LoomError): string;
|
|
30
25
|
/** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
|
|
31
26
|
export declare function commandSubject(name: string | null): string;
|
|
27
|
+
/** The routed path names the Command a sentence speaks of; an empty path is the root. */
|
|
28
|
+
export declare function routedSubject(command: readonly string[]): string;
|
|
32
29
|
/** The same subject at the start of a sentence. */
|
|
33
30
|
export declare function commandSentence(name: string | null): string;
|
|
34
31
|
/**
|
|
35
32
|
* Every failure `run()` reports is an instance of a public class. Each class carries the facts its
|
|
36
|
-
* sentence interpolates, so a
|
|
33
|
+
* sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
|
|
37
34
|
* a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
|
|
38
|
-
* default
|
|
35
|
+
* default views add it.
|
|
39
36
|
*/
|
|
40
37
|
export declare abstract class LoomError extends Error {
|
|
41
38
|
readonly exitCode: 1 | 2;
|
|
@@ -109,58 +106,33 @@ export declare class DeclarationError extends LoomError {
|
|
|
109
106
|
export declare class FatalError extends LoomError {
|
|
110
107
|
constructor(message: string);
|
|
111
108
|
}
|
|
112
|
-
/** Exit 1: an unexpected exception, a non-error throw, or a
|
|
109
|
+
/** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
|
|
113
110
|
export declare class InternalError extends LoomError {
|
|
114
111
|
readonly cause: unknown;
|
|
115
112
|
constructor(message: string, cause: unknown);
|
|
116
113
|
}
|
|
114
|
+
/** The four ways the results lane is broken, each named where core meets it. */
|
|
115
|
+
export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware';
|
|
116
|
+
/**
|
|
117
|
+
* Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
|
|
118
|
+
* `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
|
|
119
|
+
* and its default text carries the same prefix, while an override keyed by this class reaches it
|
|
120
|
+
* alone.
|
|
121
|
+
*/
|
|
122
|
+
export declare class ResultError extends InternalError {
|
|
123
|
+
readonly path: readonly string[];
|
|
124
|
+
readonly kind: ResultFault;
|
|
125
|
+
readonly cause: undefined;
|
|
126
|
+
constructor(kind: ResultFault, path: readonly string[]);
|
|
127
|
+
}
|
|
117
128
|
/** What a diagnostic says about an unexpected value, whether or not it was an Error. */
|
|
118
129
|
export declare function reasonOf(thrown: unknown): string;
|
|
119
130
|
/**
|
|
120
|
-
* Why a returned value is not the text a
|
|
121
|
-
*
|
|
122
|
-
*
|
|
131
|
+
* Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
|
|
132
|
+
* a non-string return like any other, and its rejection is adopted and swallowed here: an
|
|
133
|
+
* unobserved rejection would end the process before the invocation could report anything.
|
|
123
134
|
*/
|
|
124
135
|
export declare function notTextReason(value: unknown): string;
|
|
125
136
|
/** Every thrown value reaches reporting as a failure class; anything else is internal. */
|
|
126
137
|
export declare function toFailure(thrown: unknown): LoomError;
|
|
127
|
-
/** An opaque registration pairing one failure class with a renderer for its instances. */
|
|
128
|
-
export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration>;
|
|
129
|
-
/**
|
|
130
|
-
* A registration pairing one failure class with a renderer for its instances. The helper is the
|
|
131
|
-
* typed path for a class-keyed list, because an array literal cannot carry a different type
|
|
132
|
-
* parameter per element.
|
|
133
|
-
*/
|
|
134
|
-
export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
|
|
135
|
-
/** The renderers one application registered, keyed by the class each one names. */
|
|
136
|
-
export type FailureRegistry = ReadonlyMap<unknown, Registration>;
|
|
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;
|
|
147
|
-
/**
|
|
148
|
-
* The report of one failure: the text core writes, and whether a registered renderer produced it.
|
|
149
|
-
* An unrendered report carries core's own text, which the plain fallback path writes beside the
|
|
150
|
-
* diagnostic naming the renderer that could not answer.
|
|
151
|
-
*/
|
|
152
|
-
export type FailureReport = {
|
|
153
|
-
kind: 'rendered';
|
|
154
|
-
text: string;
|
|
155
|
-
} | {
|
|
156
|
-
kind: 'unrendered';
|
|
157
|
-
text: string;
|
|
158
|
-
reason: string;
|
|
159
|
-
};
|
|
160
|
-
/**
|
|
161
|
-
* The text core writes for one failure. Resolution walks the failure's prototype chain most
|
|
162
|
-
* derived first through the application's registrations, then falls to core's own text, so a
|
|
163
|
-
* registration for a base class brands every failure below it.
|
|
164
|
-
*/
|
|
165
|
-
export declare function describeFailure(registry: FailureRegistry, failure: LoomError): FailureReport;
|
|
166
138
|
export {};
|
package/dist/errors.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
/** The
|
|
1
|
+
/** The same subject at the start of a sentence, where a token fault names its Command. */
|
|
2
2
|
function routedSentence(command) {
|
|
3
|
-
const
|
|
4
|
-
return
|
|
3
|
+
const subject = routedSubject(command);
|
|
4
|
+
return `${subject.slice(0, 1).toUpperCase()}${subject.slice(1)}`;
|
|
5
|
+
}
|
|
6
|
+
/** The sentence one results-lane fault reports, which names the Command that holds it. */
|
|
7
|
+
function resultMessage(kind, command) {
|
|
8
|
+
if (kind === 'missing') {
|
|
9
|
+
return `${routedSentence(command)} declares a result and its action returned without emitting one. Call out.results() once.`;
|
|
10
|
+
}
|
|
11
|
+
if (kind === 'repeated') {
|
|
12
|
+
return `${routedSentence(command)} emitted its result twice. Call out.results() once.`;
|
|
13
|
+
}
|
|
14
|
+
if (kind === 'undeclared') {
|
|
15
|
+
return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
|
|
16
|
+
}
|
|
17
|
+
return `A middleware called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
|
|
5
18
|
}
|
|
6
19
|
/**
|
|
7
20
|
* The clause that offers the candidates, which is absent when there are none: every child of the
|
|
@@ -17,12 +30,12 @@ function shortGroupMessage(fault) {
|
|
|
17
30
|
}
|
|
18
31
|
/**
|
|
19
32
|
* Core's own text for one failure: its message under the category prefix its class carries, with
|
|
20
|
-
* the trailing newline every
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* nothing downstream composes
|
|
33
|
+
* the trailing newline every view's text carries. The four categories are disjoint branches of the
|
|
34
|
+
* hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
|
|
35
|
+
* the sentence alone. It is the default view of every failure class and the text the plain
|
|
36
|
+
* fallback path writes, so it runs no application code and nothing downstream composes its newline.
|
|
24
37
|
*/
|
|
25
|
-
function defaultText(failure) {
|
|
38
|
+
export function defaultText(failure) {
|
|
26
39
|
if (failure instanceof UsageError) {
|
|
27
40
|
return `Invalid input: ${failure.message}\n`;
|
|
28
41
|
}
|
|
@@ -34,36 +47,15 @@ function defaultText(failure) {
|
|
|
34
47
|
}
|
|
35
48
|
return `${failure.message}\n`;
|
|
36
49
|
}
|
|
37
|
-
/** Every prototype in a failure's chain, most derived first, so one walk reads the registry. */
|
|
38
|
-
function chainOf(failure) {
|
|
39
|
-
const chain = [];
|
|
40
|
-
let prototype = Object.getPrototypeOf(failure);
|
|
41
|
-
while (prototype !== null) {
|
|
42
|
-
chain.push(prototype);
|
|
43
|
-
prototype = Object.getPrototypeOf(prototype);
|
|
44
|
-
}
|
|
45
|
-
return chain;
|
|
46
|
-
}
|
|
47
|
-
/** Authored registrations register here, so the public type publishes nothing to reach. */
|
|
48
|
-
const nodes = new WeakMap();
|
|
49
|
-
/** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
|
|
50
|
-
class RegisteredFailure {
|
|
51
|
-
constructor(registration) {
|
|
52
|
-
nodes.set(this, registration);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/** Reads the pair behind a registered value; anything else is a declaration error. */
|
|
56
|
-
function nodeOf(value, subject) {
|
|
57
|
-
const registration = nodes.get(value);
|
|
58
|
-
if (!registration) {
|
|
59
|
-
throw new DeclarationError(`${subject} holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).`);
|
|
60
|
-
}
|
|
61
|
-
return registration;
|
|
62
|
-
}
|
|
63
50
|
/** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
|
|
64
51
|
export function commandSubject(name) {
|
|
65
52
|
return name === null ? 'the root Command' : `Command "${name}"`;
|
|
66
53
|
}
|
|
54
|
+
/** The routed path names the Command a sentence speaks of; an empty path is the root. */
|
|
55
|
+
export function routedSubject(command) {
|
|
56
|
+
const name = command.at(-1);
|
|
57
|
+
return name === undefined ? 'the root Command' : commandSubject(name);
|
|
58
|
+
}
|
|
67
59
|
/** The same subject at the start of a sentence. */
|
|
68
60
|
export function commandSentence(name) {
|
|
69
61
|
const subject = commandSubject(name);
|
|
@@ -71,9 +63,9 @@ export function commandSentence(name) {
|
|
|
71
63
|
}
|
|
72
64
|
/**
|
|
73
65
|
* Every failure `run()` reports is an instance of a public class. Each class carries the facts its
|
|
74
|
-
* sentence interpolates, so a
|
|
66
|
+
* sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
|
|
75
67
|
* a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
|
|
76
|
-
* default
|
|
68
|
+
* default views add it.
|
|
77
69
|
*/
|
|
78
70
|
export class LoomError extends Error {
|
|
79
71
|
exitCode;
|
|
@@ -193,7 +185,7 @@ export class FatalError extends LoomError {
|
|
|
193
185
|
this.name = 'FatalError';
|
|
194
186
|
}
|
|
195
187
|
}
|
|
196
|
-
/** Exit 1: an unexpected exception, a non-error throw, or a
|
|
188
|
+
/** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
|
|
197
189
|
export class InternalError extends LoomError {
|
|
198
190
|
cause;
|
|
199
191
|
constructor(message, cause) {
|
|
@@ -202,85 +194,36 @@ export class InternalError extends LoomError {
|
|
|
202
194
|
this.name = 'InternalError';
|
|
203
195
|
}
|
|
204
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
|
|
199
|
+
* `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
|
|
200
|
+
* and its default text carries the same prefix, while an override keyed by this class reaches it
|
|
201
|
+
* alone.
|
|
202
|
+
*/
|
|
203
|
+
export class ResultError extends InternalError {
|
|
204
|
+
path;
|
|
205
|
+
kind;
|
|
206
|
+
constructor(kind, path) {
|
|
207
|
+
super(resultMessage(kind, path), undefined);
|
|
208
|
+
this.kind = kind;
|
|
209
|
+
this.name = 'ResultError';
|
|
210
|
+
this.path = path;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
205
213
|
/** What a diagnostic says about an unexpected value, whether or not it was an Error. */
|
|
206
214
|
export function reasonOf(thrown) {
|
|
207
215
|
return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
|
|
208
216
|
}
|
|
209
217
|
/**
|
|
210
|
-
* Why a returned value is not the text a
|
|
211
|
-
*
|
|
212
|
-
*
|
|
218
|
+
* Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
|
|
219
|
+
* a non-string return like any other, and its rejection is adopted and swallowed here: an
|
|
220
|
+
* unobserved rejection would end the process before the invocation could report anything.
|
|
213
221
|
*/
|
|
214
222
|
export function notTextReason(value) {
|
|
215
223
|
void Promise.resolve(value).catch(() => undefined);
|
|
216
|
-
return `The
|
|
224
|
+
return `The view returned ${typeof value} instead of a string.`;
|
|
217
225
|
}
|
|
218
226
|
/** Every thrown value reaches reporting as a failure class; anything else is internal. */
|
|
219
227
|
export function toFailure(thrown) {
|
|
220
228
|
return thrown instanceof LoomError ? thrown : new InternalError(reasonOf(thrown), thrown);
|
|
221
229
|
}
|
|
222
|
-
/**
|
|
223
|
-
* A registration pairing one failure class with a renderer for its instances. The helper is the
|
|
224
|
-
* typed path for a class-keyed list, because an array literal cannot carry a different type
|
|
225
|
-
* parameter per element.
|
|
226
|
-
*/
|
|
227
|
-
export function renderFailure(type, renderer) {
|
|
228
|
-
return new RegisteredFailure({
|
|
229
|
-
name: 'name' in type && typeof type.name === 'string' ? type.name : 'a failure class',
|
|
230
|
-
prototype: 'prototype' in type ? type.prototype : undefined,
|
|
231
|
-
// Resolution reaches this registration through the same class, so the test always holds.
|
|
232
|
-
render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
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') {
|
|
240
|
-
const registry = new Map();
|
|
241
|
-
for (const failure of failures) {
|
|
242
|
-
const registration = nodeOf(failure, subject);
|
|
243
|
-
if (registry.has(registration.prototype)) {
|
|
244
|
-
throw new DeclarationError(`${subject} registers two failure renderers for "${registration.name}". Remove one registration.`);
|
|
245
|
-
}
|
|
246
|
-
registry.set(registration.prototype, registration);
|
|
247
|
-
}
|
|
248
|
-
return registry;
|
|
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
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* The text core writes for one failure. Resolution walks the failure's prototype chain most
|
|
267
|
-
* derived first through the application's registrations, then falls to core's own text, so a
|
|
268
|
-
* registration for a base class brands every failure below it.
|
|
269
|
-
*/
|
|
270
|
-
export function describeFailure(registry, failure) {
|
|
271
|
-
const registration = chainOf(failure)
|
|
272
|
-
.map((prototype) => registry.get(prototype))
|
|
273
|
-
.find((entry) => entry !== undefined);
|
|
274
|
-
if (!registration) {
|
|
275
|
-
return { kind: 'rendered', text: defaultText(failure) };
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
const text = registration.render(failure);
|
|
279
|
-
return typeof text === 'string'
|
|
280
|
-
? { kind: 'rendered', text }
|
|
281
|
-
: { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
|
|
282
|
-
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
|
|
285
|
-
}
|
|
286
|
-
}
|
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,11 +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
|
-
|
|
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'> & {
|
|
152
|
+
layers: readonly unknown[];
|
|
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, };
|