@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/options.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ export interface OptionValues {
|
|
|
7
7
|
strings: Map<string, string>;
|
|
8
8
|
lists: Map<string, string[]>;
|
|
9
9
|
booleans: Map<string, boolean>;
|
|
10
|
+
/** The spelling of the token that supplied each parsed option, which no input source writes. */
|
|
11
|
+
spellings: Map<string, string>;
|
|
10
12
|
}
|
|
13
|
+
/** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
|
|
14
|
+
export declare function emptyValues(): OptionValues;
|
|
11
15
|
/** Which accepted form a table entry is. The table owns the convention, so readers never re-derive it. */
|
|
12
16
|
type SpellingRole = 'long' | 'negative' | 'short';
|
|
13
17
|
type OptionForm = {
|
|
@@ -30,13 +34,82 @@ type OptionSpelling = OptionForm & {
|
|
|
30
34
|
*/
|
|
31
35
|
export declare function booleanValue(values: OptionValues, name: string, config: OptionConfig): boolean;
|
|
32
36
|
export declare function compileOptions(declarations: readonly OptionDeclaration[], subject: string): Map<string, OptionSpelling>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
39
|
+
* separate value is never one. The parser and `locate` read each token through this rule.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isOptionToken(token: string): boolean;
|
|
42
|
+
/** A long option token and the inline value it carries after its first `=`, if any. */
|
|
43
|
+
export interface LongToken {
|
|
44
|
+
spelling: string;
|
|
45
|
+
inline: string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A token that starts with `--` split at its first `=` into the spelling and the inline value, or
|
|
49
|
+
* `undefined` for any other token. The parser and `locate` split long tokens through this rule.
|
|
50
|
+
*/
|
|
51
|
+
export declare function longToken(token: string): LongToken | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The name of the string option one long spelling names in a table, which takes its value after
|
|
54
|
+
* `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
|
|
55
|
+
*/
|
|
56
|
+
export declare function longStringOption(spellings: ReadonlyMap<string, OptionSpelling>, spelling: string): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* A string option whose value the next token supplies, where the tokens ended first. A complete
|
|
59
|
+
* invocation reports it as a missing value; a partial one reads the next word as that value.
|
|
60
|
+
*/
|
|
61
|
+
export interface AwaitingValue {
|
|
62
|
+
name: string;
|
|
63
|
+
spelling: string;
|
|
64
|
+
}
|
|
65
|
+
/** One option name a token newly supplied, with the index of that token in the list read. */
|
|
66
|
+
export interface SuppliedOption {
|
|
67
|
+
name: string;
|
|
68
|
+
token: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The pre-scan's reading of a token list that may stop short. `positions` holds the index in
|
|
72
|
+
* `tokens` of each `rest` token, and `awaiting` is the global option the last token left without
|
|
73
|
+
* its value.
|
|
74
|
+
*/
|
|
75
|
+
export interface GlobalScan {
|
|
76
|
+
awaiting: AwaitingValue | undefined;
|
|
77
|
+
positions: number[];
|
|
78
|
+
rest: string[];
|
|
79
|
+
supplied: SuppliedOption[];
|
|
80
|
+
values: OptionValues;
|
|
81
|
+
}
|
|
33
82
|
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
83
|
+
export declare function scanGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): GlobalScan;
|
|
84
|
+
/** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
|
|
34
85
|
export declare function extractGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
35
86
|
rest: string[];
|
|
36
87
|
values: OptionValues;
|
|
37
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
|
|
91
|
+
* from an input source. A declared default is never in these maps, so it never counts.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isSupplied(values: OptionValues, name: string): boolean;
|
|
94
|
+
/** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
|
|
95
|
+
export declare function copyValues(values: OptionValues): OptionValues;
|
|
38
96
|
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
39
97
|
export declare function mergeValues(globals: OptionValues, locals: OptionValues): OptionValues;
|
|
98
|
+
/**
|
|
99
|
+
* One Command's reading of its own tokens, which may stop short. `delimited` says a bare `--` was
|
|
100
|
+
* read, and `awaiting` is the option the last token left without its value.
|
|
101
|
+
*/
|
|
102
|
+
export interface InputScan {
|
|
103
|
+
awaiting: AwaitingValue | undefined;
|
|
104
|
+
delimited: boolean;
|
|
105
|
+
options: OptionValues;
|
|
106
|
+
passthrough: string[];
|
|
107
|
+
positionals: string[];
|
|
108
|
+
supplied: SuppliedOption[];
|
|
109
|
+
}
|
|
110
|
+
/** Reads one Command's tokens into options, positionals, and the passthrough tail. */
|
|
111
|
+
export declare function scanInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): InputScan;
|
|
112
|
+
/** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
|
|
40
113
|
export declare function parseInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
41
114
|
options: OptionValues;
|
|
42
115
|
passthrough: string[];
|
package/dist/options.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DeclarationError, MissingValueError, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
|
|
2
2
|
/** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
|
|
3
|
-
function emptyValues() {
|
|
4
|
-
return { booleans: new Map(), lists: new Map(), strings: new Map() };
|
|
3
|
+
export function emptyValues() {
|
|
4
|
+
return { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
|
|
5
5
|
}
|
|
6
6
|
function validateDeclaration({ name, config }) {
|
|
7
7
|
if (typeof name !== 'string') {
|
|
@@ -89,6 +89,26 @@ export function compileOptions(declarations, subject) {
|
|
|
89
89
|
}
|
|
90
90
|
return spellings;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
94
|
+
* separate value is never one. The parser and `locate` read each token through this rule.
|
|
95
|
+
*/
|
|
96
|
+
export function isOptionToken(token) {
|
|
97
|
+
return token.startsWith('-');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A token that starts with `--` split at its first `=` into the spelling and the inline value, or
|
|
101
|
+
* `undefined` for any other token. The parser and `locate` split long tokens through this rule.
|
|
102
|
+
*/
|
|
103
|
+
export function longToken(token) {
|
|
104
|
+
if (!token.startsWith('--')) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const equals = token.indexOf('=');
|
|
108
|
+
return equals === -1
|
|
109
|
+
? { inline: undefined, spelling: token }
|
|
110
|
+
: { inline: token.slice(equals + 1), spelling: token.slice(0, equals) };
|
|
111
|
+
}
|
|
92
112
|
function lookup(spellings, spelling) {
|
|
93
113
|
const option = spellings.get(spelling);
|
|
94
114
|
if (!option) {
|
|
@@ -96,20 +116,33 @@ function lookup(spellings, spelling) {
|
|
|
96
116
|
}
|
|
97
117
|
return option;
|
|
98
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The name of the string option one long spelling names in a table, which takes its value after
|
|
121
|
+
* `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
|
|
122
|
+
*/
|
|
123
|
+
export function longStringOption(spellings, spelling) {
|
|
124
|
+
const option = spellings.get(spelling);
|
|
125
|
+
return option?.type === 'string' && option.role === 'long' ? option.name : undefined;
|
|
126
|
+
}
|
|
99
127
|
function acceptValue({ option, spelling, values, next, inline, }) {
|
|
100
128
|
const repeatable = option.type === 'string' && option.multiple;
|
|
101
129
|
if (!repeatable && (values.strings.has(option.name) || values.booleans.has(option.name))) {
|
|
102
130
|
throw new RepeatedOptionError(spelling);
|
|
103
131
|
}
|
|
132
|
+
// A repeatable option records its last occurrence, because each one overwrites the entry.
|
|
133
|
+
values.spellings.set(option.name, spelling);
|
|
104
134
|
if (option.type === 'boolean') {
|
|
105
135
|
if (inline !== undefined) {
|
|
106
136
|
throw new UnexpectedValueError(spelling, inline);
|
|
107
137
|
}
|
|
108
138
|
values.booleans.set(option.name, option.value);
|
|
109
|
-
return
|
|
139
|
+
return 'alone';
|
|
110
140
|
}
|
|
111
141
|
const value = inline ?? next;
|
|
112
|
-
if (value === undefined
|
|
142
|
+
if (value === undefined) {
|
|
143
|
+
return { name: option.name, spelling };
|
|
144
|
+
}
|
|
145
|
+
if (inline === undefined && isOptionToken(value)) {
|
|
113
146
|
throw new MissingValueError(spelling);
|
|
114
147
|
}
|
|
115
148
|
if (repeatable) {
|
|
@@ -120,14 +153,13 @@ function acceptValue({ option, spelling, values, next, inline, }) {
|
|
|
120
153
|
else {
|
|
121
154
|
values.strings.set(option.name, value);
|
|
122
155
|
}
|
|
123
|
-
return inline === undefined;
|
|
156
|
+
return inline === undefined ? 'next' : 'alone';
|
|
124
157
|
}
|
|
125
158
|
function parseOption(spellings, input, values) {
|
|
126
159
|
const { token, next } = input;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const inline = equals === -1 ? undefined : token.slice(equals + 1);
|
|
160
|
+
const long = longToken(token);
|
|
161
|
+
if (long) {
|
|
162
|
+
const { inline, spelling } = long;
|
|
131
163
|
return acceptValue({ inline, next, option: lookup(spellings, spelling), spelling, values });
|
|
132
164
|
}
|
|
133
165
|
if (token === '-') {
|
|
@@ -141,20 +173,37 @@ function parseOption(spellings, input, values) {
|
|
|
141
173
|
throw new ShortGroupError({ reason: 'value-position', token: spelling });
|
|
142
174
|
}
|
|
143
175
|
const inline = suffix.startsWith('=') ? suffix.slice(1) : undefined;
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
const reading = acceptValue({ inline, next, option, spelling, values });
|
|
177
|
+
if (reading !== 'alone') {
|
|
178
|
+
return reading;
|
|
146
179
|
}
|
|
147
180
|
}
|
|
148
|
-
return
|
|
181
|
+
return 'alone';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reads one option token into the scan and answers whether it took the next token too. The names
|
|
185
|
+
* it newly supplied join `supplied` in the order the values map first recorded them, so a repeated
|
|
186
|
+
* option keeps its first position.
|
|
187
|
+
*/
|
|
188
|
+
function readOption(spellings, input, state) {
|
|
189
|
+
const known = state.values.spellings.size;
|
|
190
|
+
const reading = parseOption(spellings, input, state.values);
|
|
191
|
+
for (const name of [...state.values.spellings.keys()].slice(known)) {
|
|
192
|
+
state.supplied.push({ name, token: input.index });
|
|
193
|
+
}
|
|
194
|
+
if (typeof reading === 'object') {
|
|
195
|
+
state.awaiting = reading;
|
|
196
|
+
}
|
|
197
|
+
return reading === 'next';
|
|
149
198
|
}
|
|
150
199
|
/**
|
|
151
200
|
* A hyphen token belongs to the globals when its long spelling or every short letter does. The
|
|
152
201
|
* pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
|
|
153
202
|
*/
|
|
154
203
|
function isGlobalToken(spellings, token) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return spellings.has(
|
|
204
|
+
const long = longToken(token);
|
|
205
|
+
if (long) {
|
|
206
|
+
return spellings.has(long.spelling);
|
|
158
207
|
}
|
|
159
208
|
const group = token.slice(1).split('=')[0] ?? '';
|
|
160
209
|
let global = '';
|
|
@@ -177,52 +226,100 @@ function isGlobalToken(spellings, token) {
|
|
|
177
226
|
return true;
|
|
178
227
|
}
|
|
179
228
|
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
180
|
-
export function
|
|
181
|
-
const
|
|
229
|
+
export function scanGlobals(spellings, tokens) {
|
|
230
|
+
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
182
231
|
const rest = [];
|
|
232
|
+
const positions = [];
|
|
183
233
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
184
234
|
const token = tokens[index];
|
|
185
235
|
if (token === undefined) {
|
|
186
236
|
break;
|
|
187
237
|
}
|
|
188
238
|
if (token === '--') {
|
|
189
|
-
|
|
190
|
-
|
|
239
|
+
// One push per token, because a spread call would overflow the stack on a long list.
|
|
240
|
+
for (const [offset, tail] of tokens.slice(index).entries()) {
|
|
241
|
+
rest.push(tail);
|
|
242
|
+
positions.push(index + offset);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
191
245
|
}
|
|
192
|
-
if (!token
|
|
246
|
+
if (!isOptionToken(token) || !isGlobalToken(spellings, token)) {
|
|
193
247
|
rest.push(token);
|
|
248
|
+
positions.push(index);
|
|
194
249
|
}
|
|
195
|
-
else if (
|
|
250
|
+
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
196
251
|
index += 1;
|
|
197
252
|
}
|
|
198
253
|
}
|
|
254
|
+
return { ...state, positions, rest };
|
|
255
|
+
}
|
|
256
|
+
/** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
|
|
257
|
+
export function extractGlobals(spellings, tokens) {
|
|
258
|
+
const { awaiting, rest, values } = scanGlobals(spellings, tokens);
|
|
259
|
+
if (awaiting) {
|
|
260
|
+
throw new MissingValueError(awaiting.spelling);
|
|
261
|
+
}
|
|
199
262
|
return { rest, values };
|
|
200
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
|
|
266
|
+
* from an input source. A declared default is never in these maps, so it never counts.
|
|
267
|
+
*/
|
|
268
|
+
export function isSupplied(values, name) {
|
|
269
|
+
return values.strings.has(name) || values.lists.has(name) || values.booleans.has(name);
|
|
270
|
+
}
|
|
271
|
+
/** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
|
|
272
|
+
export function copyValues(values) {
|
|
273
|
+
return {
|
|
274
|
+
booleans: new Map(values.booleans),
|
|
275
|
+
lists: new Map([...values.lists].map(([name, list]) => [name, [...list]])),
|
|
276
|
+
spellings: new Map(values.spellings),
|
|
277
|
+
strings: new Map(values.strings),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
201
280
|
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
202
281
|
export function mergeValues(globals, locals) {
|
|
203
282
|
return {
|
|
204
283
|
booleans: new Map([...globals.booleans, ...locals.booleans]),
|
|
205
284
|
lists: new Map([...globals.lists, ...locals.lists]),
|
|
285
|
+
spellings: new Map([...globals.spellings, ...locals.spellings]),
|
|
206
286
|
strings: new Map([...globals.strings, ...locals.strings]),
|
|
207
287
|
};
|
|
208
288
|
}
|
|
209
|
-
|
|
210
|
-
|
|
289
|
+
/** Reads one Command's tokens into options, positionals, and the passthrough tail. */
|
|
290
|
+
export function scanInputs(spellings, tokens) {
|
|
291
|
+
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
211
292
|
const positionals = [];
|
|
293
|
+
const read = (passthrough, delimited) => ({
|
|
294
|
+
awaiting: state.awaiting,
|
|
295
|
+
delimited,
|
|
296
|
+
options: state.values,
|
|
297
|
+
passthrough,
|
|
298
|
+
positionals,
|
|
299
|
+
supplied: state.supplied,
|
|
300
|
+
});
|
|
212
301
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
213
302
|
const token = tokens[index];
|
|
214
303
|
if (token === undefined) {
|
|
215
304
|
break;
|
|
216
305
|
}
|
|
217
306
|
if (token === '--') {
|
|
218
|
-
return
|
|
307
|
+
return read(tokens.slice(index + 1), true);
|
|
219
308
|
}
|
|
220
|
-
if (!token
|
|
309
|
+
if (!isOptionToken(token)) {
|
|
221
310
|
positionals.push(token);
|
|
222
311
|
}
|
|
223
|
-
else if (
|
|
312
|
+
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
224
313
|
index += 1;
|
|
225
314
|
}
|
|
226
315
|
}
|
|
227
|
-
return
|
|
316
|
+
return read([], false);
|
|
317
|
+
}
|
|
318
|
+
/** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
|
|
319
|
+
export function parseInputs(spellings, tokens) {
|
|
320
|
+
const { awaiting, options, passthrough, positionals } = scanInputs(spellings, tokens);
|
|
321
|
+
if (awaiting) {
|
|
322
|
+
throw new MissingValueError(awaiting.spelling);
|
|
323
|
+
}
|
|
324
|
+
return { options, passthrough, positionals };
|
|
228
325
|
}
|
package/dist/output.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare class Output {
|
|
|
26
26
|
* declaration a call answers to is checked where the action was authored.
|
|
27
27
|
*/
|
|
28
28
|
readonly out: Out<OpenResult>;
|
|
29
|
+
/** The channel a configuration source receives: `out` with the results call naming a source. */
|
|
30
|
+
readonly sourceOut: Out<OpenResult>;
|
|
29
31
|
private palette;
|
|
30
32
|
private policy;
|
|
31
33
|
private registry;
|
package/dist/output.js
CHANGED
|
@@ -155,6 +155,8 @@ export class Output {
|
|
|
155
155
|
* declaration a call answers to is checked where the action was authored.
|
|
156
156
|
*/
|
|
157
157
|
out;
|
|
158
|
+
/** The channel a configuration source receives: `out` with the results call naming a source. */
|
|
159
|
+
sourceOut;
|
|
158
160
|
palette = new Map();
|
|
159
161
|
policy = {};
|
|
160
162
|
// The contributors this invocation resolves a declared view through, published once they build.
|
|
@@ -179,6 +181,8 @@ export class Output {
|
|
|
179
181
|
success: (message) => this.emit('success', message, 'stderr'),
|
|
180
182
|
warn: (message) => this.emit('warn', message, 'stderr'),
|
|
181
183
|
};
|
|
184
|
+
// A configuration source writes through the same channel, and its results call names it.
|
|
185
|
+
this.sourceOut = { ...this.out, results: () => this.resultFault('source') };
|
|
182
186
|
}
|
|
183
187
|
/**
|
|
184
188
|
* The channel one action receives. On a Command that declares a result nothing the action writes
|
|
@@ -238,7 +242,7 @@ export class Output {
|
|
|
238
242
|
});
|
|
239
243
|
}
|
|
240
244
|
if (typeof view.row === 'function') {
|
|
241
|
-
//
|
|
245
|
+
// The result() call rejects a row view on a value result, so reaching one here is core's own fault.
|
|
242
246
|
return this.renderFailed(new Error(`The view "${selected}" renders rows, not a value.`));
|
|
243
247
|
}
|
|
244
248
|
return this.rendered(() => resolveView(bare, view)(erased(value), this.context('stdout')));
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
import type { MiddlewareContext } from './chain.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AttachedChild, Command } from './command.js';
|
|
3
|
+
import type { AnyExtension, DescriptorRegistry } from './extension.js';
|
|
4
|
+
import type { InputRecords } from './globals.js';
|
|
5
|
+
import type { CommandGraph, OptionNode } from './inspect.js';
|
|
6
|
+
import type { OptionValues } from './options.js';
|
|
3
7
|
import type { ProcessSignal } from './signals.js';
|
|
4
8
|
import type { Palette } from './style-state.js';
|
|
5
|
-
import type { ThemeConstraint, ThemeMapping } from './style.js';
|
|
6
|
-
import type { CommandAttachHook, OptionValue, PluginOptionConfig } from './types.js';
|
|
9
|
+
import type { ContextualStyle, ThemeConstraint, ThemeMapping } from './style.js';
|
|
10
|
+
import type { CommandAttachHook, Host, OptionValue, Out, PluginOptionConfig } from './types.js';
|
|
7
11
|
import type { OptionInput } from './validation.js';
|
|
8
12
|
import type { ViewContribution } from './view.js';
|
|
9
13
|
/**
|
|
10
14
|
* The declaration record a plugin contributes its options under: the parsing part of an option
|
|
11
15
|
* config, keyed by option name. A plugin option carries no schema and no presence rule, so the
|
|
12
|
-
* config type publishes neither, and
|
|
16
|
+
* config type publishes neither, and `plugin()` repeats the rule for a JavaScript author.
|
|
13
17
|
*/
|
|
14
18
|
type PluginOptions = Readonly<Record<string, PluginOptionConfig>>;
|
|
15
19
|
/** The values one plugin's own options take, read through the same rules an action's options are. */
|
|
16
20
|
type PluginOptionValues<Options extends PluginOptions> = {
|
|
17
21
|
readonly [Name in keyof Options]: OptionValue<Options[Name]>;
|
|
18
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* The spelling that supplied each of one plugin's own options given as a token, such as `-h`,
|
|
25
|
+
* `--help`, or `--no-total`. An option filled by an input source, defaulted, or not supplied has
|
|
26
|
+
* no entry.
|
|
27
|
+
*/
|
|
28
|
+
type PluginOptionSpellings<Options extends PluginOptions> = Readonly<Partial<Record<keyof Options & string, string>>>;
|
|
19
29
|
/** Phantom key. It carries a plugin's declared options in a read position and holds no value. */
|
|
20
30
|
declare const pluginOptions: unique symbol;
|
|
21
31
|
declare const pluginTheme: unique symbol;
|
|
22
|
-
/**
|
|
23
|
-
* One plugin's declarations as the registry holds them, with the generic parts erased. Build reads
|
|
24
|
-
* every one of them defensively, because a JavaScript author reaches the same slots, so the erased
|
|
25
|
-
* shape is what the rules below read and no declaration is claimed to be well formed here.
|
|
26
|
-
*/
|
|
27
|
-
interface DeclaredPlugin {
|
|
28
|
-
theme?: unknown;
|
|
29
|
-
options?: PluginOptions;
|
|
30
|
-
middleware?: {
|
|
31
|
-
activate?: unknown;
|
|
32
|
-
load?: unknown;
|
|
33
|
-
};
|
|
34
|
-
onCommandAttach?: unknown;
|
|
35
|
-
extensions?: readonly AnyExtension[];
|
|
36
|
-
views?: unknown;
|
|
37
|
-
signals?: unknown;
|
|
38
|
-
}
|
|
39
|
-
/** The declarations behind one plugin value, read by this package alone. */
|
|
40
|
-
interface PluginNode {
|
|
41
|
-
definition: DeclaredPlugin;
|
|
42
|
-
identity: unknown;
|
|
43
|
-
}
|
|
44
32
|
/**
|
|
45
33
|
* The runtime value `plugin()` returns. `Options` appears in a read position alone, which makes it
|
|
46
34
|
* covariant: a `plugins` list holds plugins with different options the way `views` holds
|
|
@@ -49,7 +37,7 @@ interface PluginNode {
|
|
|
49
37
|
declare class PluginDeclaration<Options extends PluginOptions, Theme extends ThemeMapping> {
|
|
50
38
|
readonly [pluginTheme]: Theme;
|
|
51
39
|
readonly [pluginOptions]: () => Options;
|
|
52
|
-
constructor(node:
|
|
40
|
+
constructor(node: BuiltPlugin);
|
|
53
41
|
}
|
|
54
42
|
/**
|
|
55
43
|
* One plugin, as the opaque value `plugin()` returns. The declarations behind it stay private to
|
|
@@ -61,8 +49,38 @@ type OptionsOf<Contributor> = Contributor extends Plugin<infer Options> ? Option
|
|
|
61
49
|
/** A middleware reads its own plugin's options and either takes over or continues the chain. */
|
|
62
50
|
type Middleware<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: MiddlewareContext<OptionsOf<Contributor>>) => Promise<void> | void;
|
|
63
51
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
52
|
+
* What a configuration source receives: the host, its own plugin's option values, resolved from
|
|
53
|
+
* argv, the environment, and their defaults, the `OptionNode` of every option core asks about, and
|
|
54
|
+
* the ordinary channels a middleware and an action already read. Each request is a node inside
|
|
55
|
+
* `graph`, the graph `inspect()` returns for the run. `out` is the channel a middleware receives,
|
|
56
|
+
* and `style` the contextual style an action receives, so a source warns and escapes as they do.
|
|
57
|
+
*/
|
|
58
|
+
interface SourceContext<Options extends PluginOptions = PluginOptions> {
|
|
59
|
+
readonly host: Host;
|
|
60
|
+
readonly options: PluginOptionValues<Options>;
|
|
61
|
+
readonly requests: readonly OptionNode[];
|
|
62
|
+
readonly graph: CommandGraph;
|
|
63
|
+
readonly out: Out;
|
|
64
|
+
readonly style: ContextualStyle;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One answer: a value of the option's raw type, a string, a Boolean, or a list of strings for a
|
|
68
|
+
* multiple option, and the one-line label core prints in a diagnostic about the value.
|
|
69
|
+
*/
|
|
70
|
+
interface SourceAnswer {
|
|
71
|
+
readonly value: string | boolean | readonly string[];
|
|
72
|
+
readonly label: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A configuration source answers the requested options by declared name. A requested option with
|
|
76
|
+
* no key in the record has no answer and falls through to its default.
|
|
77
|
+
*/
|
|
78
|
+
type SourceResolver<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: SourceContext<OptionsOf<Contributor>>) => Promise<Readonly<Record<string, SourceAnswer>>>;
|
|
79
|
+
/**
|
|
80
|
+
* Everything a plugin declares. `plugin()` checks every rule the definition carries on its own, and
|
|
81
|
+
* creating and installing the value runs none of its code: a hook runs at graph build, the
|
|
82
|
+
* middleware runs inside an invocation, and the configuration source runs in the input-source
|
|
83
|
+
* stage when an unfilled option carries its binding.
|
|
66
84
|
*/
|
|
67
85
|
interface PluginDefinition<Options extends PluginOptions = PluginOptions, Theme extends ThemeMapping = ThemeMapping> {
|
|
68
86
|
theme?: Theme & ThemeConstraint<Theme>;
|
|
@@ -77,32 +95,61 @@ interface PluginDefinition<Options extends PluginOptions = PluginOptions, Theme
|
|
|
77
95
|
extensions?: readonly AnyExtension[];
|
|
78
96
|
views?: readonly ViewContribution[];
|
|
79
97
|
signals?: readonly ('SIGINT' | 'SIGTERM')[];
|
|
98
|
+
source?: {
|
|
99
|
+
binding: AnyExtension & {
|
|
100
|
+
readonly target: 'option';
|
|
101
|
+
};
|
|
102
|
+
load: () => Promise<{
|
|
103
|
+
default: SourceResolver<Plugin<Options>>;
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
commands?: readonly Command<unknown, unknown>[];
|
|
80
107
|
}
|
|
81
108
|
/**
|
|
82
109
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
83
110
|
* none of its code: a hook runs at graph build, and the middleware runs inside an invocation, so an
|
|
84
|
-
* installed plugin an invocation never reaches costs that invocation its hooks alone.
|
|
111
|
+
* installed plugin an invocation never reaches costs that invocation its hooks alone. Every rule
|
|
112
|
+
* that one definition carries on its own throws here, before the value exists.
|
|
85
113
|
*/
|
|
86
114
|
declare function plugin<Options extends PluginOptions = {}, const Theme extends ThemeMapping = {}>(identity: string, definition: PluginDefinition<Options, Theme>): Plugin<NoInfer<Options>, NoInfer<Theme>>;
|
|
87
|
-
/** One installed plugin, with the declarations build reads out of it in installation order. */
|
|
88
|
-
interface InstalledPlugin {
|
|
89
|
-
declaration: DeclaredPlugin;
|
|
90
|
-
identity: string;
|
|
91
|
-
}
|
|
92
115
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
93
116
|
declare function pluginSentence(identity: string): string;
|
|
117
|
+
/** What the installed list resolves to: the plugins in order, and every descriptor they define. */
|
|
118
|
+
interface InstalledPlugins {
|
|
119
|
+
descriptors: DescriptorRegistry;
|
|
120
|
+
plugins: readonly BuiltPlugin[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
124
|
+
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
125
|
+
* configuration source, and two distinct descriptors under one identity. The slot is read
|
|
126
|
+
* defensively, because a JavaScript author reaches it with any value. Each plugin's own rules
|
|
127
|
+
* already ran at its `plugin()` call.
|
|
128
|
+
*/
|
|
129
|
+
declare function installPlugins(plugins: unknown): InstalledPlugins;
|
|
94
130
|
/**
|
|
95
|
-
* The
|
|
96
|
-
*
|
|
97
|
-
*
|
|
131
|
+
* The default export one plugin loader resolves to, checked by the guard its caller supplies. A
|
|
132
|
+
* loader that throws where it is called and one that rejects later are one failure, and a module
|
|
133
|
+
* without the export names the kind of function it owed, such as `middleware` or `source`.
|
|
98
134
|
*/
|
|
99
|
-
declare function
|
|
135
|
+
declare function loadDefault<Export>(identity: string, load: () => unknown, owed: {
|
|
136
|
+
guard: (value: unknown) => value is Export;
|
|
137
|
+
noun: string;
|
|
138
|
+
}): Promise<Export>;
|
|
100
139
|
/** One plugin's declared middleware: what wakes it, and the loader that fetches its module. */
|
|
101
140
|
interface BuiltMiddleware {
|
|
102
141
|
activate: 'always' | readonly string[];
|
|
103
142
|
load: () => unknown;
|
|
104
143
|
}
|
|
105
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* One plugin's configuration source: the identity of the binding that marks an option as
|
|
146
|
+
* configuration-bound, and the loader that fetches the resolver's module.
|
|
147
|
+
*/
|
|
148
|
+
interface BuiltSource {
|
|
149
|
+
binding: string;
|
|
150
|
+
load: () => unknown;
|
|
151
|
+
}
|
|
152
|
+
/** One plugin's declarations, read once at its `plugin()` call. */
|
|
106
153
|
interface BuiltPlugin {
|
|
107
154
|
theme: Palette | undefined;
|
|
108
155
|
/** The hook core calls once per Command at graph build, or nothing where none is declared. */
|
|
@@ -113,20 +160,27 @@ interface BuiltPlugin {
|
|
|
113
160
|
inputs: readonly OptionInput[];
|
|
114
161
|
middleware: BuiltMiddleware | undefined;
|
|
115
162
|
signals: readonly ProcessSignal[];
|
|
163
|
+
source: BuiltSource | undefined;
|
|
164
|
+
/** The Commands the plugin attaches to the root, in list order. */
|
|
165
|
+
commands: readonly AttachedChild[];
|
|
166
|
+
/** Every descriptor the plugin defines or its options' values name, by identity. */
|
|
167
|
+
descriptors: ReadonlyMap<string, AnyExtension>;
|
|
168
|
+
/** The extension record each of the plugin's own options carries. */
|
|
169
|
+
records: InputRecords;
|
|
116
170
|
}
|
|
117
|
-
/** The shared registers one build fills while it reads each plugin's contributions. */
|
|
118
|
-
interface PluginBuild {
|
|
119
|
-
descriptors: DescriptorRegistry;
|
|
120
|
-
extensions: ExtensionRecords;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Every installed plugin's declarations, in installation order. A plugin's own extensions register
|
|
124
|
-
* before any declaration carries a value, so a duplicated package copy is reported from the list
|
|
125
|
-
* that installed it.
|
|
126
|
-
*/
|
|
127
|
-
declare function buildPlugins(installed: readonly InstalledPlugin[], build: PluginBuild): readonly BuiltPlugin[];
|
|
128
171
|
/** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
|
|
129
172
|
declare function ownedSignals(plugins: readonly BuiltPlugin[]): readonly ProcessSignal[];
|
|
173
|
+
/** The value shape a plugin option takes, which is what `OptionValue` gives its declaration. */
|
|
174
|
+
type PluginValues = Record<string, string | string[] | boolean | undefined>;
|
|
175
|
+
/**
|
|
176
|
+
* One plugin's own option values for one run: what argv or an input source supplied, or the
|
|
177
|
+
* declared default, filled without validation. A collected value and an array default are copied,
|
|
178
|
+
* so a plugin that writes to what it received changes neither the declaration nor the next run.
|
|
179
|
+
* Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
180
|
+
*/
|
|
181
|
+
declare function pluginValues(inputs: readonly OptionInput[], values: OptionValues): PluginValues;
|
|
182
|
+
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
183
|
+
declare function pluginSpellings(inputs: readonly OptionInput[], values: OptionValues): Readonly<Record<string, string>>;
|
|
130
184
|
type ThemeOf<Contributor> = [Contributor] extends [never] ? {} : Contributor extends Plugin<PluginOptions, infer Theme> ? Theme : {};
|
|
131
|
-
export type { ThemeOf, BuiltPlugin,
|
|
132
|
-
export {
|
|
185
|
+
export type { ThemeOf, BuiltPlugin, BuiltSource, PluginValues, SourceAnswer, SourceContext, SourceResolver, Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, };
|
|
186
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginValues, };
|