@loomcli/core 0.1.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 +72 -0
- package/dist/application.js +188 -0
- package/dist/command.d.ts +176 -0
- package/dist/command.js +452 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +18 -0
- package/dist/errors.d.ts +158 -0
- package/dist/errors.js +261 -0
- package/dist/globals.d.ts +36 -0
- package/dist/globals.js +71 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.js +37 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.js +98 -0
- package/dist/options.d.ts +38 -0
- package/dist/options.js +219 -0
- package/dist/output.d.ts +52 -0
- package/dist/output.js +188 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +406 -0
- package/package.json +28 -0
package/dist/options.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { DeclarationError, MissingValueError, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
|
|
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() };
|
|
5
|
+
}
|
|
6
|
+
function validateDeclaration({ name, config }) {
|
|
7
|
+
if (typeof name !== 'string') {
|
|
8
|
+
throw new DeclarationError('Option names must be strings. Supply a string name.');
|
|
9
|
+
}
|
|
10
|
+
if (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
|
|
11
|
+
throw new DeclarationError(`Option name "${name}" is invalid. Use a nonempty name without a leading hyphen, whitespace, or "=".`);
|
|
12
|
+
}
|
|
13
|
+
if (!['string', 'boolean'].includes(config.type)) {
|
|
14
|
+
throw new DeclarationError(`Option "${name}" has an invalid type. Use "string" or "boolean".`);
|
|
15
|
+
}
|
|
16
|
+
if (config.short !== undefined &&
|
|
17
|
+
(typeof config.short !== 'string' || !/^[A-Za-z]$/u.test(config.short))) {
|
|
18
|
+
throw new DeclarationError(`Option "${name}" requires a short alias of one ASCII letter.`);
|
|
19
|
+
}
|
|
20
|
+
if (config.shortOnly !== undefined && typeof config.shortOnly !== 'boolean') {
|
|
21
|
+
throw new DeclarationError(`Option "${name}" shortOnly must be Boolean. Use true or false.`);
|
|
22
|
+
}
|
|
23
|
+
if (config.shortOnly && config.short === undefined) {
|
|
24
|
+
throw new DeclarationError(`Option "${name}" with shortOnly requires a short alias.`);
|
|
25
|
+
}
|
|
26
|
+
if (config.type === 'boolean' && config.multiple !== undefined) {
|
|
27
|
+
throw new DeclarationError(`Option "${name}" is a boolean option and declares multiple. Remove multiple or declare a string option.`);
|
|
28
|
+
}
|
|
29
|
+
if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
|
|
30
|
+
throw new DeclarationError(`Option "${name}" multiple must be Boolean. Use true or false.`);
|
|
31
|
+
}
|
|
32
|
+
if (config.polarity !== undefined) {
|
|
33
|
+
if (config.type !== 'boolean') {
|
|
34
|
+
throw new DeclarationError(`Option "${name}" declares polarity but is not Boolean. Remove polarity or use type "boolean".`);
|
|
35
|
+
}
|
|
36
|
+
if (!['positive', 'both', 'negative'].includes(config.polarity)) {
|
|
37
|
+
throw new DeclarationError(`Option "${name}" has an invalid polarity. Use "positive", "both", or "negative".`);
|
|
38
|
+
}
|
|
39
|
+
if (config.polarity === 'both' && config.shortOnly) {
|
|
40
|
+
throw new DeclarationError(`Option "${name}" cannot express both polarities with shortOnly. Enable long forms or select one polarity.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function addSpelling(spellings, spelling, option) {
|
|
45
|
+
const existing = spellings.get(spelling);
|
|
46
|
+
if (existing) {
|
|
47
|
+
throw new DeclarationError(`Option spelling "${spelling}" is used by both "${existing.name}" and "${option.name}". Change one declaration.`);
|
|
48
|
+
}
|
|
49
|
+
spellings.set(spelling, option);
|
|
50
|
+
}
|
|
51
|
+
export function compileOptions(declarations, subject) {
|
|
52
|
+
const spellings = new Map();
|
|
53
|
+
const names = new Set();
|
|
54
|
+
for (const { name, config } of declarations) {
|
|
55
|
+
validateDeclaration({ config, name });
|
|
56
|
+
if (names.has(name)) {
|
|
57
|
+
throw new DeclarationError(`Option "${name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
58
|
+
}
|
|
59
|
+
names.add(name);
|
|
60
|
+
const positive = config.type === 'string'
|
|
61
|
+
? { multiple: config.multiple === true, name, type: 'string' }
|
|
62
|
+
: { name, type: 'boolean', value: config.polarity !== 'negative' };
|
|
63
|
+
if (!config.shortOnly) {
|
|
64
|
+
if (config.type === 'string' || config.polarity !== 'negative') {
|
|
65
|
+
addSpelling(spellings, `--${name}`, { ...positive, role: 'long' });
|
|
66
|
+
}
|
|
67
|
+
if (config.type === 'boolean' &&
|
|
68
|
+
(config.polarity === 'both' || config.polarity === 'negative')) {
|
|
69
|
+
addSpelling(spellings, `--no-${name}`, {
|
|
70
|
+
name,
|
|
71
|
+
role: 'negative',
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
value: false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (config.short !== undefined) {
|
|
78
|
+
addSpelling(spellings, `-${config.short}`, { ...positive, role: 'short' });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return spellings;
|
|
82
|
+
}
|
|
83
|
+
function lookup(spellings, spelling) {
|
|
84
|
+
const option = spellings.get(spelling);
|
|
85
|
+
if (!option) {
|
|
86
|
+
throw new UnknownOptionError(spelling);
|
|
87
|
+
}
|
|
88
|
+
return option;
|
|
89
|
+
}
|
|
90
|
+
function acceptValue({ option, spelling, values, next, inline, }) {
|
|
91
|
+
const repeatable = option.type === 'string' && option.multiple;
|
|
92
|
+
if (!repeatable && (values.strings.has(option.name) || values.booleans.has(option.name))) {
|
|
93
|
+
throw new RepeatedOptionError(spelling);
|
|
94
|
+
}
|
|
95
|
+
if (option.type === 'boolean') {
|
|
96
|
+
if (inline !== undefined) {
|
|
97
|
+
throw new UnexpectedValueError(spelling, inline);
|
|
98
|
+
}
|
|
99
|
+
values.booleans.set(option.name, option.value);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const value = inline ?? next;
|
|
103
|
+
if (value === undefined || (inline === undefined && value.startsWith('-'))) {
|
|
104
|
+
throw new MissingValueError(spelling);
|
|
105
|
+
}
|
|
106
|
+
if (repeatable) {
|
|
107
|
+
const collected = values.lists.get(option.name) ?? [];
|
|
108
|
+
collected.push(value);
|
|
109
|
+
values.lists.set(option.name, collected);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
values.strings.set(option.name, value);
|
|
113
|
+
}
|
|
114
|
+
return inline === undefined;
|
|
115
|
+
}
|
|
116
|
+
function parseOption(spellings, input, values) {
|
|
117
|
+
const { token, next } = input;
|
|
118
|
+
if (token.startsWith('--')) {
|
|
119
|
+
const equals = token.indexOf('=');
|
|
120
|
+
const spelling = equals === -1 ? token : token.slice(0, equals);
|
|
121
|
+
const inline = equals === -1 ? undefined : token.slice(equals + 1);
|
|
122
|
+
return acceptValue({ inline, next, option: lookup(spellings, spelling), spelling, values });
|
|
123
|
+
}
|
|
124
|
+
if (token === '-') {
|
|
125
|
+
lookup(spellings, token);
|
|
126
|
+
}
|
|
127
|
+
for (let index = 1; index < token.length; index += 1) {
|
|
128
|
+
const spelling = `-${token[index]}`;
|
|
129
|
+
const option = lookup(spellings, spelling);
|
|
130
|
+
const suffix = token.slice(index + 1);
|
|
131
|
+
if (option.type === 'string' && suffix !== '') {
|
|
132
|
+
throw new ShortGroupError({ reason: 'value-position', token: spelling });
|
|
133
|
+
}
|
|
134
|
+
const inline = suffix.startsWith('=') ? suffix.slice(1) : undefined;
|
|
135
|
+
if (acceptValue({ inline, next, option, spelling, values })) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A hyphen token belongs to the globals when its long spelling or every short letter does. The
|
|
143
|
+
* pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
|
|
144
|
+
*/
|
|
145
|
+
function isGlobalToken(spellings, token) {
|
|
146
|
+
if (token.startsWith('--')) {
|
|
147
|
+
const equals = token.indexOf('=');
|
|
148
|
+
return spellings.has(equals === -1 ? token : token.slice(0, equals));
|
|
149
|
+
}
|
|
150
|
+
const group = token.slice(1).split('=')[0] ?? '';
|
|
151
|
+
let global = '';
|
|
152
|
+
let other = '';
|
|
153
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
154
|
+
const letter = group.charAt(index);
|
|
155
|
+
if (spellings.has(`-${letter}`)) {
|
|
156
|
+
global = global === '' ? letter : global;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
other = other === '' ? letter : other;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (global === '') {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (other !== '') {
|
|
166
|
+
throw new ShortGroupError({ global, other, reason: 'mixed-scope', token });
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
171
|
+
export function extractGlobals(spellings, tokens) {
|
|
172
|
+
const values = emptyValues();
|
|
173
|
+
const rest = [];
|
|
174
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
175
|
+
const token = tokens[index];
|
|
176
|
+
if (token === undefined) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (token === '--') {
|
|
180
|
+
rest.push(...tokens.slice(index));
|
|
181
|
+
return { rest, values };
|
|
182
|
+
}
|
|
183
|
+
if (!token.startsWith('-') || !isGlobalToken(spellings, token)) {
|
|
184
|
+
rest.push(token);
|
|
185
|
+
}
|
|
186
|
+
else if (parseOption(spellings, { next: tokens[index + 1], token }, values)) {
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { rest, values };
|
|
191
|
+
}
|
|
192
|
+
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
193
|
+
export function mergeValues(globals, locals) {
|
|
194
|
+
return {
|
|
195
|
+
booleans: new Map([...globals.booleans, ...locals.booleans]),
|
|
196
|
+
lists: new Map([...globals.lists, ...locals.lists]),
|
|
197
|
+
strings: new Map([...globals.strings, ...locals.strings]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function parseInputs(spellings, tokens) {
|
|
201
|
+
const options = emptyValues();
|
|
202
|
+
const positionals = [];
|
|
203
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
204
|
+
const token = tokens[index];
|
|
205
|
+
if (token === undefined) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (token === '--') {
|
|
209
|
+
return { options, passthrough: tokens.slice(index + 1), positionals };
|
|
210
|
+
}
|
|
211
|
+
if (!token.startsWith('-')) {
|
|
212
|
+
positionals.push(token);
|
|
213
|
+
}
|
|
214
|
+
else if (parseOption(spellings, { next: tokens[index + 1], token }, options)) {
|
|
215
|
+
index += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { options, passthrough: [], positionals };
|
|
219
|
+
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { Host, Out } from './types.js';
|
|
3
|
+
type WriteState = {
|
|
4
|
+
kind: 'ok';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'failed';
|
|
7
|
+
error: unknown;
|
|
8
|
+
};
|
|
9
|
+
/** The semantic calls, which choose a destination. A rendered value has no purpose of its own. */
|
|
10
|
+
type Purpose = 'print' | 'info' | 'success' | 'warn' | 'error';
|
|
11
|
+
export declare class Output {
|
|
12
|
+
readonly host: Pick<Host, 'stdout' | 'stderr'>;
|
|
13
|
+
private readonly destinations;
|
|
14
|
+
private renderFault;
|
|
15
|
+
readonly out: Out;
|
|
16
|
+
constructor(host: Pick<Host, 'stdout' | 'stderr'>);
|
|
17
|
+
/** A semantic message is one line on its destination; only `print` writes to stdout. */
|
|
18
|
+
emit(kind: Purpose, message: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* The failure report of one invocation. Like `render`, the text is queued on its destination
|
|
21
|
+
* exactly as given: the caller already carries its own trailing newline, whether that text came
|
|
22
|
+
* from a registered renderer or from core's own default text.
|
|
23
|
+
*/
|
|
24
|
+
report(text: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* The renderer owns every byte, so its text is queued on stdout exactly as returned. A throw or
|
|
27
|
+
* a non-string return rejects this call alone: nothing is written for it, later output still
|
|
28
|
+
* writes, and the recorded cause ends the invocation once the action has completed.
|
|
29
|
+
*/
|
|
30
|
+
private rendered;
|
|
31
|
+
/**
|
|
32
|
+
* The rejected call. The first renderer failure is the reported one, so a later one adds no
|
|
33
|
+
* second diagnostic, and the rejection is observed here as well, because an action that never
|
|
34
|
+
* awaits the call must not end the process with an unhandled rejection.
|
|
35
|
+
*/
|
|
36
|
+
private renderFailed;
|
|
37
|
+
/** What a renderer failed with during this invocation, if one did. */
|
|
38
|
+
get fault(): {
|
|
39
|
+
cause: unknown;
|
|
40
|
+
} | undefined;
|
|
41
|
+
private write;
|
|
42
|
+
settle(): Promise<WriteState>;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The plain fallback path: a fresh destination on stderr, outside the invocation's queues and
|
|
47
|
+
* outside every registration, so no application code runs on it. The caller composes the newlines
|
|
48
|
+
* between whatever it is reporting, then passes the one string this writes verbatim. A failed
|
|
49
|
+
* write ends reporting.
|
|
50
|
+
*/
|
|
51
|
+
export declare function reportPlainly(stderr: Writable, text: string): Promise<void>;
|
|
52
|
+
export {};
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { setImmediate } from 'node:timers/promises';
|
|
2
|
+
import { FatalError, notTextReason } from './errors.js';
|
|
3
|
+
class Destination {
|
|
4
|
+
stream;
|
|
5
|
+
tail = Promise.resolve();
|
|
6
|
+
state = { kind: 'ok' };
|
|
7
|
+
constructor(stream) {
|
|
8
|
+
this.stream = stream;
|
|
9
|
+
stream.on('error', this.onError);
|
|
10
|
+
}
|
|
11
|
+
onError = (error) => {
|
|
12
|
+
if (this.state.kind === 'ok') {
|
|
13
|
+
this.state = { error, kind: 'failed' };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
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
|
+
});
|
|
23
|
+
// Keep the returned rejection observable, while accounting for calls without await.
|
|
24
|
+
this.tail = pending.catch(this.onError);
|
|
25
|
+
return pending;
|
|
26
|
+
}
|
|
27
|
+
writeText(text) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (this.stream.destroyed || this.stream.writableEnded) {
|
|
30
|
+
reject(new Error('The output destination is closed.'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const onError = (error) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
reject(error);
|
|
36
|
+
};
|
|
37
|
+
const onClose = () => {
|
|
38
|
+
onError(new Error('The output destination closed before writing completed.'));
|
|
39
|
+
};
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
this.stream.off('error', onError);
|
|
42
|
+
this.stream.off('close', onClose);
|
|
43
|
+
};
|
|
44
|
+
this.stream.on('error', onError);
|
|
45
|
+
this.stream.on('close', onClose);
|
|
46
|
+
try {
|
|
47
|
+
this.stream.write(text, (error) => {
|
|
48
|
+
cleanup();
|
|
49
|
+
if (error) {
|
|
50
|
+
reject(error);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
onError(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
dispose() {
|
|
63
|
+
this.stream.off('error', this.onError);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The text a renderer produced, or the value that stands for its failure to produce text. */
|
|
67
|
+
function renderText(produce) {
|
|
68
|
+
try {
|
|
69
|
+
const text = produce();
|
|
70
|
+
return typeof text === 'string' ? { text } : { failed: new Error(notTextReason(text)) };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return { failed: error };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class Output {
|
|
77
|
+
host;
|
|
78
|
+
destinations = new Map();
|
|
79
|
+
renderFault = undefined;
|
|
80
|
+
out;
|
|
81
|
+
constructor(host) {
|
|
82
|
+
this.host = host;
|
|
83
|
+
this.out = {
|
|
84
|
+
error: (message) => this.emit('error', message),
|
|
85
|
+
fatal: (message) => {
|
|
86
|
+
throw new FatalError(message);
|
|
87
|
+
},
|
|
88
|
+
info: (message) => this.emit('info', message),
|
|
89
|
+
print: (message) => this.emit('print', message),
|
|
90
|
+
render: (data, renderer) => this.rendered(() => renderer.render(data)),
|
|
91
|
+
success: (message) => this.emit('success', message),
|
|
92
|
+
warn: (message) => this.emit('warn', message),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** A semantic message is one line on its destination; only `print` writes to stdout. */
|
|
96
|
+
emit(kind, message) {
|
|
97
|
+
return this.write(kind === 'print' ? this.host.stdout : this.host.stderr, `${message}\n`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The failure report of one invocation. Like `render`, the text is queued on its destination
|
|
101
|
+
* exactly as given: the caller already carries its own trailing newline, whether that text came
|
|
102
|
+
* from a registered renderer or from core's own default text.
|
|
103
|
+
*/
|
|
104
|
+
report(text) {
|
|
105
|
+
return this.write(this.host.stderr, text);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The renderer owns every byte, so its text is queued on stdout exactly as returned. A throw or
|
|
109
|
+
* a non-string return rejects this call alone: nothing is written for it, later output still
|
|
110
|
+
* writes, and the recorded cause ends the invocation once the action has completed.
|
|
111
|
+
*/
|
|
112
|
+
rendered(produce) {
|
|
113
|
+
const rendered = renderText(produce);
|
|
114
|
+
return 'text' in rendered
|
|
115
|
+
? this.write(this.host.stdout, rendered.text)
|
|
116
|
+
: this.renderFailed(rendered.failed);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The rejected call. The first renderer failure is the reported one, so a later one adds no
|
|
120
|
+
* second diagnostic, and the rejection is observed here as well, because an action that never
|
|
121
|
+
* awaits the call must not end the process with an unhandled rejection.
|
|
122
|
+
*/
|
|
123
|
+
renderFailed(cause) {
|
|
124
|
+
this.renderFault ??= { cause };
|
|
125
|
+
const rejection = Promise.reject(cause);
|
|
126
|
+
void rejection.catch(() => undefined);
|
|
127
|
+
return rejection;
|
|
128
|
+
}
|
|
129
|
+
/** What a renderer failed with during this invocation, if one did. */
|
|
130
|
+
get fault() {
|
|
131
|
+
return this.renderFault;
|
|
132
|
+
}
|
|
133
|
+
write(stream, text) {
|
|
134
|
+
let destination = this.destinations.get(stream);
|
|
135
|
+
if (!destination) {
|
|
136
|
+
destination = new Destination(stream);
|
|
137
|
+
this.destinations.set(stream, destination);
|
|
138
|
+
}
|
|
139
|
+
return destination.write(text);
|
|
140
|
+
}
|
|
141
|
+
async settle() {
|
|
142
|
+
let drained = false;
|
|
143
|
+
while (!drained) {
|
|
144
|
+
const pending = [...this.destinations.values()].map((destination) => ({
|
|
145
|
+
destination,
|
|
146
|
+
tail: destination.tail,
|
|
147
|
+
}));
|
|
148
|
+
await Promise.all(pending.map(({ tail }) => tail));
|
|
149
|
+
// Node reports write errors after callbacks. Those callbacks can also enqueue output.
|
|
150
|
+
await setImmediate();
|
|
151
|
+
drained =
|
|
152
|
+
pending.length === this.destinations.size &&
|
|
153
|
+
pending.every(({ destination, tail }) => destination.tail === tail);
|
|
154
|
+
}
|
|
155
|
+
for (const destination of this.destinations.values()) {
|
|
156
|
+
if (destination.state.kind === 'failed') {
|
|
157
|
+
return destination.state;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { kind: 'ok' };
|
|
161
|
+
}
|
|
162
|
+
dispose() {
|
|
163
|
+
for (const destination of this.destinations.values()) {
|
|
164
|
+
destination.dispose();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The plain fallback path: a fresh destination on stderr, outside the invocation's queues and
|
|
170
|
+
* outside every registration, so no application code runs on it. The caller composes the newlines
|
|
171
|
+
* between whatever it is reporting, then passes the one string this writes verbatim. A failed
|
|
172
|
+
* write ends reporting.
|
|
173
|
+
*/
|
|
174
|
+
export async function reportPlainly(stderr, text) {
|
|
175
|
+
try {
|
|
176
|
+
const destination = new Destination(stderr);
|
|
177
|
+
try {
|
|
178
|
+
await destination.write(text);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await setImmediate();
|
|
182
|
+
destination.dispose();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// A failed fallback ends reporting; it never re-enters rendering.
|
|
187
|
+
}
|
|
188
|
+
}
|