@loomcli/core 0.1.1 → 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 +73 -28
- package/dist/application.js +334 -99
- package/dist/chain.d.ts +68 -0
- package/dist/chain.js +372 -0
- package/dist/command.d.ts +201 -46
- package/dist/command.js +713 -57
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -51
- package/dist/errors.js +58 -90
- package/dist/extension.d.ts +99 -0
- package/dist/extension.js +330 -0
- package/dist/facts.d.ts +39 -0
- package/dist/facts.js +95 -0
- package/dist/globals.d.ts +49 -28
- package/dist/globals.js +104 -58
- 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 +21 -6
- package/dist/index.js +7 -2
- package/dist/inspect.d.ts +69 -12
- package/dist/inspect.js +83 -26
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/options.d.ts +7 -0
- package/dist/options.js +9 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +132 -0
- package/dist/plugin.js +278 -0
- 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/signals.d.ts +52 -0
- package/dist/signals.js +85 -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 +222 -26
- package/dist/validation.d.ts +12 -3
- package/dist/validation.js +34 -17
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
package/dist/output.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { setImmediate } from 'node:timers/promises';
|
|
2
|
-
import { FatalError, notTextReason } from './errors.js';
|
|
2
|
+
import { FatalError, notTextReason, ResultError } from './errors.js';
|
|
3
|
+
import { incompleteResult, lanes } from './lanes.js';
|
|
4
|
+
import { capabilities } from './rendering.js';
|
|
5
|
+
import { writeSequence } from './sequence.js';
|
|
6
|
+
import { resolveText, width } from './style-resolve.js';
|
|
7
|
+
import { createStyle, tokens } from './style.js';
|
|
8
|
+
import { resolveRowView, resolveView, shapeOf } from './view.js';
|
|
9
|
+
function stringValue(value) {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
throw new Error(notTextReason(value));
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
/** The opener a reservation holds until its own gate publishes the real one. */
|
|
16
|
+
const unopened = () => undefined;
|
|
3
17
|
class Destination {
|
|
4
18
|
stream;
|
|
5
19
|
tail = Promise.resolve();
|
|
@@ -14,16 +28,44 @@ class Destination {
|
|
|
14
28
|
}
|
|
15
29
|
};
|
|
16
30
|
write(text) {
|
|
17
|
-
const pending = this.tail.then(() =>
|
|
18
|
-
if (this.state.kind === 'failed') {
|
|
19
|
-
throw this.state.error;
|
|
20
|
-
}
|
|
21
|
-
return this.writeText(text);
|
|
22
|
-
});
|
|
31
|
+
const pending = this.tail.then(() => this.accept(text));
|
|
23
32
|
// Keep the returned rejection observable, while accounting for calls without await.
|
|
24
33
|
this.tail = pending.catch(this.onError);
|
|
25
34
|
return pending;
|
|
26
35
|
}
|
|
36
|
+
/** One text queued behind whatever preceded it, on a destination that has not failed. */
|
|
37
|
+
accept(text) {
|
|
38
|
+
if (this.state.kind === 'failed') {
|
|
39
|
+
throw this.state.error;
|
|
40
|
+
}
|
|
41
|
+
return this.writeText(text);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One place held in this destination's order from the moment a sequence is issued until it
|
|
45
|
+
* closes. Later calls queue behind the gate, so they write after the sequence's last piece
|
|
46
|
+
* whether or not their caller awaited the sequence, and the gate keeps the destination undrained
|
|
47
|
+
* while the sequence is live, which is what makes a pending sequence open output.
|
|
48
|
+
*/
|
|
49
|
+
reserve() {
|
|
50
|
+
const previous = this.tail;
|
|
51
|
+
let open = unopened;
|
|
52
|
+
const gate = new Promise((resolve) => {
|
|
53
|
+
open = resolve;
|
|
54
|
+
});
|
|
55
|
+
this.tail = previous.then(() => gate);
|
|
56
|
+
// The sequence's own pieces queue on each other, ahead of the gate that holds its place.
|
|
57
|
+
let pieces = previous;
|
|
58
|
+
return {
|
|
59
|
+
close: () => {
|
|
60
|
+
open(pieces);
|
|
61
|
+
},
|
|
62
|
+
write: (text) => {
|
|
63
|
+
const pending = pieces.then(() => this.accept(text));
|
|
64
|
+
pieces = pending.catch(this.onError);
|
|
65
|
+
return pending;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
27
69
|
writeText(text) {
|
|
28
70
|
return new Promise((resolve, reject) => {
|
|
29
71
|
if (this.stream.destroyed || this.stream.writableEnded) {
|
|
@@ -63,7 +105,18 @@ class Destination {
|
|
|
63
105
|
this.stream.off('error', this.onError);
|
|
64
106
|
}
|
|
65
107
|
}
|
|
66
|
-
/** The
|
|
108
|
+
/** The sentence one view value of the wrong shape reports, which is the fault of its call. */
|
|
109
|
+
function shapeReason(both) {
|
|
110
|
+
return both
|
|
111
|
+
? 'The view carries render and row. Supply one of the two.'
|
|
112
|
+
: 'The view carries neither render nor row. Supply a view with render or a row view with row.';
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* No contributor at all. A result's selected view is replaced by view name alone, so the
|
|
116
|
+
* Application's override list never reaches a result's views, which carry no identity.
|
|
117
|
+
*/
|
|
118
|
+
const bare = [];
|
|
119
|
+
/** The text a view produced, or the value that stands for its failure to produce text. */
|
|
67
120
|
function renderText(produce) {
|
|
68
121
|
try {
|
|
69
122
|
const text = produce();
|
|
@@ -73,50 +126,266 @@ function renderText(produce) {
|
|
|
73
126
|
return { failed: error };
|
|
74
127
|
}
|
|
75
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The data one view reads back through the key that resolved it. A result's views are stored with
|
|
131
|
+
* their data type erased, as the view registry erases a declared view's, so the write site hands
|
|
132
|
+
* each function the value its own declaration checked.
|
|
133
|
+
*/
|
|
134
|
+
function erased(value) {
|
|
135
|
+
// Last resort: no typed path exists.
|
|
136
|
+
// A views record holds one entry per view name and carries no type parameter per entry.
|
|
137
|
+
// Every view it stores reads its data as the erased type the registry uses.
|
|
138
|
+
// It holds because the authoring call checked the value against the declaration the record answers.
|
|
139
|
+
// Build proved every entry in that record renders the declared type.
|
|
140
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
76
143
|
export class Output {
|
|
77
144
|
host;
|
|
145
|
+
signal;
|
|
78
146
|
destinations = new Map();
|
|
79
147
|
renderFault = undefined;
|
|
148
|
+
// Every fault the output path raised beside its calls, reported after the primary outcome.
|
|
149
|
+
// A source a sequence stopped on and a results-lane fault are both such faults.
|
|
150
|
+
stops = [];
|
|
151
|
+
// The routed Command an incomplete sequence names, published once routing resolved it.
|
|
152
|
+
route = [];
|
|
153
|
+
/**
|
|
154
|
+
* The channel every caller writes through. It is typed with the result left open, because the
|
|
155
|
+
* declaration a call answers to is checked where the action was authored.
|
|
156
|
+
*/
|
|
80
157
|
out;
|
|
81
|
-
|
|
158
|
+
palette = new Map();
|
|
159
|
+
policy = {};
|
|
160
|
+
// The contributors this invocation resolves a declared view through, published once they build.
|
|
161
|
+
registry = [];
|
|
162
|
+
style = createStyle();
|
|
163
|
+
constructor(host, signal) {
|
|
82
164
|
this.host = host;
|
|
165
|
+
this.signal = signal;
|
|
83
166
|
this.out = {
|
|
84
|
-
error: (message) => this.emit('error', message),
|
|
167
|
+
error: (message) => this.emit('error', message, 'stderr'),
|
|
85
168
|
fatal: (message) => {
|
|
86
169
|
throw new FatalError(message);
|
|
87
170
|
},
|
|
88
|
-
info: (message) => this.emit('info', message),
|
|
89
|
-
print: (message) => this.emit('print', message),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
171
|
+
info: (message) => this.emit('info', message, 'stderr'),
|
|
172
|
+
print: (message) => this.emit('print', message, 'stdout'),
|
|
173
|
+
// The data type is erased here, as it is in the registry.
|
|
174
|
+
// One call dispatches on the shape of the view it was handed.
|
|
175
|
+
// Every view function reads its data back through its own key.
|
|
176
|
+
render: (data, value) => this.renderValue(data, value, { destination: 'stdout' }),
|
|
177
|
+
// Only the action emits a result, so this call is the middleware fault whatever was declared.
|
|
178
|
+
results: () => this.resultFault('middleware'),
|
|
179
|
+
success: (message) => this.emit('success', message, 'stderr'),
|
|
180
|
+
warn: (message) => this.emit('warn', message, 'stderr'),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The channel one action receives. On a Command that declares a result nothing the action writes
|
|
185
|
+
* but the result reaches stdout: `print` and `render` move to stderr, and they move the view
|
|
186
|
+
* context with the destination, so capability detection follows the stream the bytes reach. The
|
|
187
|
+
* destination is decided here, from the declaration, and never from the view a run selected.
|
|
188
|
+
*/
|
|
189
|
+
channel(binding) {
|
|
190
|
+
const destination = binding.result ? 'stderr' : 'stdout';
|
|
191
|
+
const emission = { calls: 0, live: new Set() };
|
|
192
|
+
const target = { destination, live: emission.live };
|
|
193
|
+
return {
|
|
194
|
+
emitted: () => emission.calls > 0,
|
|
195
|
+
out: {
|
|
196
|
+
...this.out,
|
|
197
|
+
print: (message) => this.emit('print', message, destination),
|
|
198
|
+
// The data type is erased here, as it is in the registry.
|
|
199
|
+
// One call dispatches on the shape of the view it was handed.
|
|
200
|
+
// Every view function reads its data back through its own key.
|
|
201
|
+
render: (data, value) => this.renderValue(data, value, target),
|
|
202
|
+
results: (value) => this.results(binding, emission, value),
|
|
203
|
+
},
|
|
204
|
+
stop: () => {
|
|
205
|
+
for (const stop of emission.live) {
|
|
206
|
+
stop();
|
|
207
|
+
}
|
|
208
|
+
},
|
|
93
209
|
};
|
|
94
210
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
211
|
+
/**
|
|
212
|
+
* One `out.results` call on the action's channel. The declaration decides the unit, the selected
|
|
213
|
+
* view decides how it renders, and stdout carries the result under either one. The selected
|
|
214
|
+
* view is the one a middleware named before the action dispatched, or the declaration's default
|
|
215
|
+
* when none did. A call the declaration does not answer for is a fault of the lane and writes
|
|
216
|
+
* nothing.
|
|
217
|
+
*/
|
|
218
|
+
results(binding, emission, value) {
|
|
219
|
+
const { path, result } = binding;
|
|
220
|
+
if (!result) {
|
|
221
|
+
return this.resultFault('undeclared', path);
|
|
222
|
+
}
|
|
223
|
+
if (emission.calls > 0) {
|
|
224
|
+
return this.resultFault('repeated', path);
|
|
225
|
+
}
|
|
226
|
+
emission.calls += 1;
|
|
227
|
+
const selected = binding.view ?? result.default;
|
|
228
|
+
const view = result.views.get(selected);
|
|
229
|
+
if (!view) {
|
|
230
|
+
// Build proved the default names a view the record holds.
|
|
231
|
+
// The boundary proved a selected name is one too, so this is core's own fault.
|
|
232
|
+
return this.renderFailed(new Error(`The view "${selected}" is not declared.`));
|
|
233
|
+
}
|
|
234
|
+
if (result.kind === 'rows') {
|
|
235
|
+
return this.sequence(erased(value), this.sequenceView(view, bare), {
|
|
236
|
+
destination: 'stdout',
|
|
237
|
+
live: emission.live,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (typeof view.row === 'function') {
|
|
241
|
+
// Build rejects a row view on a value result, so reaching one here is core's own fault.
|
|
242
|
+
return this.renderFailed(new Error(`The view "${selected}" renders rows, not a value.`));
|
|
243
|
+
}
|
|
244
|
+
return this.rendered(() => resolveView(bare, view)(erased(value), this.context('stdout')));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* One fault of the results lane: the call rejects, and the same failure is reported after this
|
|
248
|
+
* invocation's primary outcome, so a call the action never awaited still turns a would-be 0 into
|
|
249
|
+
* 1 and one the action let propagate is reported once.
|
|
250
|
+
*/
|
|
251
|
+
resultFault(kind, path = this.route) {
|
|
252
|
+
const fault = new ResultError(kind, path);
|
|
253
|
+
this.stops.push(fault);
|
|
254
|
+
const rejected = Promise.reject(fault);
|
|
255
|
+
void rejected.catch(() => undefined);
|
|
256
|
+
return rejected;
|
|
257
|
+
}
|
|
258
|
+
/** The registry one invocation resolves through, republished as each contributor is read. */
|
|
259
|
+
useViews(registry) {
|
|
260
|
+
this.registry = registry;
|
|
261
|
+
}
|
|
262
|
+
/** The routed path, published once routing resolved it, which an incomplete sequence names. */
|
|
263
|
+
useRoute(path) {
|
|
264
|
+
this.route = path;
|
|
265
|
+
}
|
|
266
|
+
/** What this invocation's output raised beside its calls, in the order it was raised. */
|
|
267
|
+
get stopped() {
|
|
268
|
+
return this.stops;
|
|
269
|
+
}
|
|
270
|
+
configure(policy, palette) {
|
|
271
|
+
this.policy = policy;
|
|
272
|
+
this.palette = palette;
|
|
273
|
+
this.style = createStyle(new Set([...tokens, ...palette.keys()]));
|
|
274
|
+
}
|
|
275
|
+
context(destination) {
|
|
276
|
+
const caps = capabilities(this.host, destination, this.policy);
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
style: this.style,
|
|
279
|
+
width: (text) => width(text, this.palette, caps),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* A semantic message is one line on its destination; only `print` writes to stdout. The message
|
|
284
|
+
* is checked before the lane view runs, and this call appends the one newline after it, so a
|
|
285
|
+
* lane view returns none and an override that returns the empty string still writes one.
|
|
286
|
+
*/
|
|
287
|
+
emit(kind, message, destination) {
|
|
288
|
+
return this.rendered(() => {
|
|
289
|
+
if (typeof message !== 'string') {
|
|
290
|
+
throw new TypeError('Output messages must be strings.');
|
|
291
|
+
}
|
|
292
|
+
const render = resolveView(this.registry, lanes[kind]);
|
|
293
|
+
return `${stringValue(render(message, this.context(destination)))}\n`;
|
|
294
|
+
}, destination);
|
|
98
295
|
}
|
|
99
296
|
/**
|
|
100
297
|
* The failure report of one invocation. Like `render`, the text is queued on its destination
|
|
101
|
-
*
|
|
102
|
-
* from a
|
|
298
|
+
* after style resolution: the caller already carries its own trailing newline, whether that text
|
|
299
|
+
* came from a resolved view or from core's own default text.
|
|
103
300
|
*/
|
|
104
301
|
report(text) {
|
|
105
|
-
return this.
|
|
302
|
+
return this.rendered(() => text, 'stderr');
|
|
106
303
|
}
|
|
107
304
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
305
|
+
* One `out.render` call, dispatched on the shape of the view it was handed. The two shapes are
|
|
306
|
+
* exclusive, so a JavaScript author's value that carries both, or neither, is the output-view
|
|
307
|
+
* fault of this call and nothing is written for it.
|
|
111
308
|
*/
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
309
|
+
renderValue(data, value, target) {
|
|
310
|
+
const shape = shapeOf(value);
|
|
311
|
+
if (shape === 'both' || shape === 'neither') {
|
|
312
|
+
return this.renderFailed(new Error(shapeReason(shape === 'both')));
|
|
313
|
+
}
|
|
314
|
+
// The shape was named above, so this second read narrows the value rather than deciding it.
|
|
315
|
+
if (typeof value.render === 'function') {
|
|
316
|
+
return this.rendered(() => resolveView(this.registry, value)(data, this.context(target.destination)), target.destination);
|
|
317
|
+
}
|
|
318
|
+
return this.sequence(data, this.sequenceView(value, this.registry), target);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The view one sequence writes through, in the shape its own view carries. The caller supplies
|
|
322
|
+
* the contributors the view resolves through: `out.render`'s call-site view resolves through
|
|
323
|
+
* this invocation's registry, as ADR-0021 requires, and a result's view resolves through none,
|
|
324
|
+
* because a result's selected view is replaced by view name alone.
|
|
325
|
+
*/
|
|
326
|
+
sequenceView(value, registry) {
|
|
327
|
+
if (typeof value.row === 'function') {
|
|
328
|
+
return { kind: 'rows', view: resolveRowView(registry, value) };
|
|
329
|
+
}
|
|
330
|
+
const render = resolveView(registry, value);
|
|
331
|
+
return { kind: 'whole', render: (rows, context) => render(erased(rows), context) };
|
|
117
332
|
}
|
|
118
333
|
/**
|
|
119
|
-
*
|
|
334
|
+
* One sequence, which holds its place on the destination from here until its last piece is
|
|
335
|
+
* written. The returned rejection is observed here as well, because an action that never awaits
|
|
336
|
+
* the call must not end the process with an unhandled rejection.
|
|
337
|
+
*/
|
|
338
|
+
sequence(source, view, target) {
|
|
339
|
+
const { destination } = target;
|
|
340
|
+
const place = this.destination(this.host[destination]).reserve();
|
|
341
|
+
const sequence = writeSequence({
|
|
342
|
+
close: place.close,
|
|
343
|
+
context: this.context(destination),
|
|
344
|
+
incomplete: (facts) => {
|
|
345
|
+
this.incomplete(facts);
|
|
346
|
+
},
|
|
347
|
+
path: this.route,
|
|
348
|
+
piece: (produce) => this.rendered(produce, destination, place.write),
|
|
349
|
+
signal: this.signal,
|
|
350
|
+
source,
|
|
351
|
+
stopped: (cause) => {
|
|
352
|
+
this.stops.push(cause);
|
|
353
|
+
},
|
|
354
|
+
view,
|
|
355
|
+
});
|
|
356
|
+
// A sequence its own channel can stop, so an action's failure ends it rather than draining it.
|
|
357
|
+
target.live?.add(sequence.stop);
|
|
358
|
+
void sequence.pending.catch(() => undefined);
|
|
359
|
+
return sequence.pending;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The line one incomplete sequence writes on stderr, ahead of the fault's own report. It resolves
|
|
363
|
+
* through the registry like any other rendered output, so an override that returns the empty
|
|
364
|
+
* string silences it and one that throws is a view fault. A stderr that has failed already takes
|
|
365
|
+
* the plain fallback path and no further.
|
|
366
|
+
*/
|
|
367
|
+
incomplete(facts) {
|
|
368
|
+
const context = this.context('stderr');
|
|
369
|
+
if (this.destinations.get(this.host.stderr)?.state.kind === 'failed') {
|
|
370
|
+
void reportPlainly(this.host.stderr, incompleteResult.render(facts, context));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
void this.rendered(() => resolveView(this.registry, incompleteResult)(facts, context), 'stderr').catch(() => undefined);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The write site owns its newline; core resolves the view's marked text before queuing it. A
|
|
377
|
+
* throw or a non-string return rejects this call alone: nothing is written for it, later output
|
|
378
|
+
* still writes, and the recorded cause ends the invocation once the action has completed.
|
|
379
|
+
*/
|
|
380
|
+
rendered(produce, destination = 'stdout', write) {
|
|
381
|
+
const rendered = renderText(() => resolveText(stringValue(produce()), this.palette, capabilities(this.host, destination, this.policy)));
|
|
382
|
+
if (!('text' in rendered)) {
|
|
383
|
+
return this.renderFailed(rendered.failed);
|
|
384
|
+
}
|
|
385
|
+
return write ? write(rendered.text) : this.write(this.host[destination], rendered.text);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The rejected call. The first view failure is the reported one, so a later one adds no
|
|
120
389
|
* second diagnostic, and the rejection is observed here as well, because an action that never
|
|
121
390
|
* awaits the call must not end the process with an unhandled rejection.
|
|
122
391
|
*/
|
|
@@ -126,17 +395,21 @@ export class Output {
|
|
|
126
395
|
void rejection.catch(() => undefined);
|
|
127
396
|
return rejection;
|
|
128
397
|
}
|
|
129
|
-
/** What a
|
|
398
|
+
/** What a view failed with during this invocation, if one did. */
|
|
130
399
|
get fault() {
|
|
131
400
|
return this.renderFault;
|
|
132
401
|
}
|
|
133
|
-
|
|
402
|
+
/** The queue one stream writes through, opened the first time this invocation reaches it. */
|
|
403
|
+
destination(stream) {
|
|
134
404
|
let destination = this.destinations.get(stream);
|
|
135
405
|
if (!destination) {
|
|
136
406
|
destination = new Destination(stream);
|
|
137
407
|
this.destinations.set(stream, destination);
|
|
138
408
|
}
|
|
139
|
-
return destination
|
|
409
|
+
return destination;
|
|
410
|
+
}
|
|
411
|
+
write(stream, text) {
|
|
412
|
+
return this.destination(stream).write(text);
|
|
140
413
|
}
|
|
141
414
|
async settle() {
|
|
142
415
|
let drained = false;
|
|
@@ -167,7 +440,7 @@ export class Output {
|
|
|
167
440
|
}
|
|
168
441
|
/**
|
|
169
442
|
* The plain fallback path: a fresh destination on stderr, outside the invocation's queues and
|
|
170
|
-
* outside every
|
|
443
|
+
* outside every override, so no application code runs on it. The caller composes the newlines
|
|
171
444
|
* between whatever it is reporting, then passes the one string this writes verbatim. A failed
|
|
172
445
|
* write ends reporting.
|
|
173
446
|
*/
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { MiddlewareContext } from './chain.js';
|
|
2
|
+
import type { AnyExtension, DescriptorRegistry, ExtensionRecords } from './extension.js';
|
|
3
|
+
import type { ProcessSignal } from './signals.js';
|
|
4
|
+
import type { Palette } from './style-state.js';
|
|
5
|
+
import type { ThemeConstraint, ThemeMapping } from './style.js';
|
|
6
|
+
import type { CommandAttachHook, OptionValue, PluginOptionConfig } from './types.js';
|
|
7
|
+
import type { OptionInput } from './validation.js';
|
|
8
|
+
import type { ViewContribution } from './view.js';
|
|
9
|
+
/**
|
|
10
|
+
* The declaration record a plugin contributes its options under: the parsing part of an option
|
|
11
|
+
* config, keyed by option name. A plugin option carries no schema and no presence rule, so the
|
|
12
|
+
* config type publishes neither, and build repeats the rule for a JavaScript author.
|
|
13
|
+
*/
|
|
14
|
+
type PluginOptions = Readonly<Record<string, PluginOptionConfig>>;
|
|
15
|
+
/** The values one plugin's own options take, read through the same rules an action's options are. */
|
|
16
|
+
type PluginOptionValues<Options extends PluginOptions> = {
|
|
17
|
+
readonly [Name in keyof Options]: OptionValue<Options[Name]>;
|
|
18
|
+
};
|
|
19
|
+
/** Phantom key. It carries a plugin's declared options in a read position and holds no value. */
|
|
20
|
+
declare const pluginOptions: unique symbol;
|
|
21
|
+
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
|
+
/**
|
|
45
|
+
* The runtime value `plugin()` returns. `Options` appears in a read position alone, which makes it
|
|
46
|
+
* covariant: a `plugins` list holds plugins with different options the way `views` holds
|
|
47
|
+
* overrides for different keys, and `Middleware` and `load` accept a narrower plugin.
|
|
48
|
+
*/
|
|
49
|
+
declare class PluginDeclaration<Options extends PluginOptions, Theme extends ThemeMapping> {
|
|
50
|
+
readonly [pluginTheme]: Theme;
|
|
51
|
+
readonly [pluginOptions]: () => Options;
|
|
52
|
+
constructor(node: PluginNode);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One plugin, as the opaque value `plugin()` returns. The declarations behind it stay private to
|
|
56
|
+
* this package, so no consumer can read or replace them.
|
|
57
|
+
*/
|
|
58
|
+
type Plugin<Options extends PluginOptions = PluginOptions, Theme extends ThemeMapping = {}> = Pick<PluginDeclaration<Options, Theme>, typeof pluginOptions | typeof pluginTheme>;
|
|
59
|
+
/** The declared options of a plugin, or of the factory that returns one. */
|
|
60
|
+
type OptionsOf<Contributor> = Contributor extends Plugin<infer Options> ? Options : Contributor extends (...args: never[]) => Plugin<infer Options> ? Options : PluginOptions;
|
|
61
|
+
/** A middleware reads its own plugin's options and either takes over or continues the chain. */
|
|
62
|
+
type Middleware<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: MiddlewareContext<OptionsOf<Contributor>>) => Promise<void> | void;
|
|
63
|
+
/**
|
|
64
|
+
* Everything a plugin declares. Creating and installing the value runs none of its code: a hook
|
|
65
|
+
* runs at graph build, and the middleware runs inside an invocation.
|
|
66
|
+
*/
|
|
67
|
+
interface PluginDefinition<Options extends PluginOptions = PluginOptions, Theme extends ThemeMapping = ThemeMapping> {
|
|
68
|
+
theme?: Theme & ThemeConstraint<Theme>;
|
|
69
|
+
options?: Options;
|
|
70
|
+
middleware?: {
|
|
71
|
+
activate: 'always' | readonly (keyof Options & string)[];
|
|
72
|
+
load: () => Promise<{
|
|
73
|
+
default: Middleware<Plugin<Options>>;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
onCommandAttach?: CommandAttachHook;
|
|
77
|
+
extensions?: readonly AnyExtension[];
|
|
78
|
+
views?: readonly ViewContribution[];
|
|
79
|
+
signals?: readonly ('SIGINT' | 'SIGTERM')[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
83
|
+
* 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.
|
|
85
|
+
*/
|
|
86
|
+
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
|
+
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
93
|
+
declare function pluginSentence(identity: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* The installed list in composition order, with the rules that read the list itself. The slot is
|
|
96
|
+
* read defensively, because a JavaScript author reaches it with any value. Each plugin's own
|
|
97
|
+
* declarations are read by the build steps that consume them, in the order those steps run.
|
|
98
|
+
*/
|
|
99
|
+
declare function installPlugins(plugins: unknown): readonly InstalledPlugin[];
|
|
100
|
+
/** One plugin's declared middleware: what wakes it, and the loader that fetches its module. */
|
|
101
|
+
interface BuiltMiddleware {
|
|
102
|
+
activate: 'always' | readonly string[];
|
|
103
|
+
load: () => unknown;
|
|
104
|
+
}
|
|
105
|
+
/** One installed plugin's declarations, read once per build in installation order. */
|
|
106
|
+
interface BuiltPlugin {
|
|
107
|
+
theme: Palette | undefined;
|
|
108
|
+
/** The hook core calls once per Command at graph build, or nothing where none is declared. */
|
|
109
|
+
onCommandAttach: CommandAttachHook | undefined;
|
|
110
|
+
/** The plugin's own `views` slot, read once the validated theme is in place. */
|
|
111
|
+
views: unknown;
|
|
112
|
+
identity: string;
|
|
113
|
+
inputs: readonly OptionInput[];
|
|
114
|
+
middleware: BuiltMiddleware | undefined;
|
|
115
|
+
signals: readonly ProcessSignal[];
|
|
116
|
+
}
|
|
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
|
+
/** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
|
|
129
|
+
declare function ownedSignals(plugins: readonly BuiltPlugin[]): readonly ProcessSignal[];
|
|
130
|
+
type ThemeOf<Contributor> = [Contributor] extends [never] ? {} : Contributor extends Plugin<PluginOptions, infer Theme> ? Theme : {};
|
|
131
|
+
export type { ThemeOf, BuiltPlugin, InstalledPlugin, Middleware, OptionsOf, Plugin, PluginBuild, PluginDefinition, PluginOptions, PluginOptionValues, };
|
|
132
|
+
export { buildPlugins, installPlugins, ownedSignals, plugin, pluginSentence };
|