@loomcli/core 0.2.0 → 0.3.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 +620 -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 +5 -1
- package/dist/extension.js +18 -1
- 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 +14 -5
- package/dist/index.js +5 -2
- package/dist/inspect.d.ts +26 -3
- package/dist/inspect.js +19 -5
- 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 +169 -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
|
@@ -91,5 +91,9 @@ interface ExtensionSlot {
|
|
|
91
91
|
* the record, so a typed read compares by reference without the graph carrying a reference.
|
|
92
92
|
*/
|
|
93
93
|
declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
|
|
94
|
+
/** Layers validate in authoring order; replacement updates existing keys without reinsertion. */
|
|
95
|
+
declare function buildCommandExtensions(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
|
|
96
|
+
layers: readonly unknown[];
|
|
97
|
+
}): Readonly<Record<string, unknown>>;
|
|
94
98
|
export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
|
|
95
|
-
export { buildExtensions, extension, isDescriptor, readExtension, registerDescriptor };
|
|
99
|
+
export { buildCommandExtensions, buildExtensions, extension, isDescriptor, readExtension, registerDescriptor, };
|
package/dist/extension.js
CHANGED
|
@@ -310,4 +310,21 @@ function buildExtensions(slot) {
|
|
|
310
310
|
owners.set(frozen, defined);
|
|
311
311
|
return frozen;
|
|
312
312
|
}
|
|
313
|
-
|
|
313
|
+
/** Layers validate in authoring order; replacement updates existing keys without reinsertion. */
|
|
314
|
+
function buildCommandExtensions(slot) {
|
|
315
|
+
const stored = new Map();
|
|
316
|
+
const defined = new Map();
|
|
317
|
+
for (const declared of slot.layers) {
|
|
318
|
+
const layer = buildExtensions({ ...slot, declared, target: 'command' });
|
|
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
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const frozen = Object.freeze(Object.fromEntries(stored));
|
|
327
|
+
owners.set(frozen, defined);
|
|
328
|
+
return frozen;
|
|
329
|
+
}
|
|
330
|
+
export { buildCommandExtensions, buildExtensions, extension, isDescriptor, readExtension, registerDescriptor, };
|
package/dist/globals.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { DeclarationError } from './errors.js';
|
|
2
2
|
import { compileOptions } from './options.js';
|
|
3
3
|
import type { BuiltPlugin, PluginBuild } from './plugin.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { OptionConfig, OptionValue } from './types.js';
|
|
5
5
|
import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
|
|
6
|
-
/** Phantom key. It keeps the declared option types exact and holds no runtime value. */
|
|
7
|
-
declare const declaredTypes: unique symbol;
|
|
8
6
|
/**
|
|
9
7
|
* Who declared one option that shares the globals table, or the Command that declares a local
|
|
10
8
|
* option colliding with it. Every collision sentence is derived from a pair of these, so the
|
|
@@ -36,33 +34,24 @@ declare function spellingCollision(spelling: string, first: OptionSite, second:
|
|
|
36
34
|
* validated and never reaches an action.
|
|
37
35
|
*/
|
|
38
36
|
interface BuiltGlobals {
|
|
37
|
+
bind: (values: ValidatedInputs) => unknown;
|
|
39
38
|
inputs: readonly InputDeclaration[];
|
|
40
39
|
names: ReadonlyMap<string, OptionOwner>;
|
|
41
40
|
options: ReturnType<typeof compileOptions>;
|
|
42
41
|
plugins: readonly BuiltPlugin[];
|
|
43
|
-
source: unknown;
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): GlobalOptions<Options & Record<Name, OptionValue<Config>>>;
|
|
43
|
+
/** The Application's private global declarations and their schema-derived value binder. */
|
|
44
|
+
interface GlobalsState<Globals = unknown> {
|
|
45
|
+
bind: (values: ValidatedInputs) => Globals;
|
|
46
|
+
inputs: readonly OptionInput[];
|
|
50
47
|
}
|
|
48
|
+
declare function emptyGlobals(): GlobalsState<{}>;
|
|
49
|
+
declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* The one table the pre-scan reads: the application's global options in authoring order, then each
|
|
52
|
+
* installed plugin's options in installation order. Every collision between the two scopes, by key
|
|
53
|
+
* or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
|
|
55
54
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* registry is the test, because `option()` returns a new declaration of its own and the exported
|
|
60
|
-
* constructor is only the first of them.
|
|
61
|
-
*/
|
|
62
|
-
declare function isGlobalOptions(value: unknown): boolean;
|
|
63
|
-
/** Absent globals compile to an empty table whose source is `undefined`, like the declarations. */
|
|
64
|
-
declare function buildGlobals(globals: object | undefined, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
|
|
65
|
-
declare function bindGlobals<Options>(globals: GlobalOptions<Options> | undefined, values: ValidatedInputs): Options;
|
|
66
|
-
declare const GlobalOptions: new () => GlobalOptions;
|
|
67
|
-
export type { BuiltGlobals, OptionOwner, OptionSite };
|
|
68
|
-
export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions, keyCollision, spellingCollision, };
|
|
55
|
+
declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
|
|
56
|
+
export type { BuiltGlobals, GlobalsState, OptionOwner, OptionSite };
|
|
57
|
+
export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
|
package/dist/globals.js
CHANGED
|
@@ -2,7 +2,6 @@ import { DeclarationError } from './errors.js';
|
|
|
2
2
|
import { buildExtensions } from './extension.js';
|
|
3
3
|
import { checkDeprecated, checkDescription, checkHidden } from './facts.js';
|
|
4
4
|
import { compileOptions } from './options.js';
|
|
5
|
-
import { captureConfig } from './validation.js';
|
|
6
5
|
const globalSubject = 'the global options';
|
|
7
6
|
/** A collision sentence names a plugin first, then the application's globals, then a local. */
|
|
8
7
|
const ranks = {
|
|
@@ -57,51 +56,21 @@ function spellingCollision(spelling, first, second) {
|
|
|
57
56
|
const [leading, trailing] = ordered(first, second);
|
|
58
57
|
return new DeclarationError(`Option spelling "${spelling}" is used by ${usedBy(leading)} and ${usedBy(trailing)}. Change one declaration.`);
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class GlobalOptionsBuilder {
|
|
63
|
-
#bind;
|
|
64
|
-
#inputs;
|
|
65
|
-
constructor(inputs, bind) {
|
|
66
|
-
this.#bind = bind;
|
|
67
|
-
this.#inputs = inputs;
|
|
68
|
-
nodes.set(this, { bind, inputs, source: this });
|
|
69
|
-
}
|
|
70
|
-
option(name, config) {
|
|
71
|
-
const input = {
|
|
72
|
-
config: captureConfig(config),
|
|
73
|
-
kind: 'option',
|
|
74
|
-
name,
|
|
75
|
-
};
|
|
76
|
-
const previous = this.#bind;
|
|
77
|
-
return new GlobalOptionsBuilder([...this.#inputs, input], (values) => ({
|
|
78
|
-
...previous(values),
|
|
79
|
-
...values.option(input),
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
59
|
+
function emptyGlobals() {
|
|
60
|
+
return { bind: () => ({}), inputs: [] };
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
return node;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
|
|
93
|
-
* registry is the test, because `option()` returns a new declaration of its own and the exported
|
|
94
|
-
* constructor is only the first of them.
|
|
95
|
-
*/
|
|
96
|
-
function isGlobalOptions(value) {
|
|
97
|
-
return typeof value === 'object' && value !== null && nodes.has(value);
|
|
62
|
+
function declareGlobalOption(state, input) {
|
|
63
|
+
return {
|
|
64
|
+
bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
|
|
65
|
+
inputs: [...state.inputs, input],
|
|
66
|
+
};
|
|
98
67
|
}
|
|
99
68
|
/**
|
|
100
69
|
* The one table the pre-scan reads: the application's global options in authoring order, then each
|
|
101
70
|
* installed plugin's options in installation order. Every collision between the two scopes, by key
|
|
102
71
|
* or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
|
|
103
72
|
*/
|
|
104
|
-
function
|
|
73
|
+
function buildGlobals(node, plugins, build) {
|
|
105
74
|
const names = new Map();
|
|
106
75
|
const application = { kind: 'application' };
|
|
107
76
|
// A global option belongs to the application, not to one Command, so its facts read that way.
|
|
@@ -125,7 +94,7 @@ function compileTable(node, plugins, build) {
|
|
|
125
94
|
options,
|
|
126
95
|
});
|
|
127
96
|
});
|
|
128
|
-
return { inputs: node.inputs, names, options, plugins
|
|
97
|
+
return { bind: node.bind, inputs: node.inputs, names, options, plugins };
|
|
129
98
|
}
|
|
130
99
|
/** One plugin's options joining the table the application's globals already hold. */
|
|
131
100
|
function join(owner, inputs, table) {
|
|
@@ -145,24 +114,4 @@ function join(owner, inputs, table) {
|
|
|
145
114
|
options.set(spelling, option);
|
|
146
115
|
}
|
|
147
116
|
}
|
|
148
|
-
|
|
149
|
-
function buildGlobals(globals, plugins, build) {
|
|
150
|
-
const node = globals === undefined ? { bind: () => ({}), inputs: [], source: undefined } : nodeOf(globals);
|
|
151
|
-
return compileTable(node, plugins, build);
|
|
152
|
-
}
|
|
153
|
-
function bindGlobals(globals, values) {
|
|
154
|
-
const bound = globals === undefined ? {} : nodeOf(globals).bind(values);
|
|
155
|
-
// Last resort: no typed path exists. The public GlobalOptions type hides its declarations.
|
|
156
|
-
// The registry is the only bridge from a value to its binder, and a WeakMap cannot carry the
|
|
157
|
-
// Options type of its key. It holds because the registered binder belongs to this value alone.
|
|
158
|
-
// Its record composes exactly the declarations that its Options type records.
|
|
159
|
-
// A declaration without globals publishes `Options = {}`, and the empty record is exactly that.
|
|
160
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
161
|
-
return bound;
|
|
162
|
-
}
|
|
163
|
-
const GlobalOptions = class extends GlobalOptionsBuilder {
|
|
164
|
-
constructor() {
|
|
165
|
-
super([], () => ({}));
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions, keyCollision, spellingCollision, };
|
|
117
|
+
export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
|