@loomcli/core 0.5.0 → 0.7.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/NOTICE +34 -0
- package/dist/application.d.ts +18 -2
- package/dist/application.js +302 -75
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/chain.d.ts +15 -6
- package/dist/chain.js +40 -20
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +65 -23
- package/dist/command.js +734 -236
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +108 -24
- package/dist/errors.js +324 -53
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +41 -8
- package/dist/extension.js +142 -57
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +106 -23
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +117 -46
- package/dist/glyphs.generated.js +1 -1
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +68 -0
- package/dist/input-rules.js +152 -0
- package/dist/inspect.d.ts +12 -12
- package/dist/inspect.js +92 -48
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +30 -2
- package/dist/options.js +143 -46
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +56 -0
- package/dist/plain.js +238 -0
- package/dist/plugin-rules.d.ts +68 -0
- package/dist/plugin-rules.js +164 -0
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +27 -15
- package/dist/plugin.js +429 -144
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +6 -4
- package/dist/sources.js +25 -16
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +13 -3
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +69 -7
- package/dist/validation.js +211 -67
- package/dist/view.d.ts +61 -22
- package/dist/view.js +168 -81
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/application.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
import { captureDeclaration, unreadableArgument } from './capture.js';
|
|
1
2
|
import { runInvocation } from './chain.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { portableName } from './command-rules.js';
|
|
4
|
+
import { attachToRoot, callArguments, childNode, buildGraph, checkDeclaredOptions, collectInputs, commandPlacement, inputPlaces, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
|
|
5
|
+
import { escapeControlCharacters } from './controls.js';
|
|
6
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
7
|
+
import { DeclarationError, exitCodeOf, InternalError, quoted, reasonOf, toFailure, } from './errors.js';
|
|
4
8
|
import { storeCommandLayers } from './extension.js';
|
|
5
|
-
import { checkDescription, checkNoListingFacts, checkVersion,
|
|
6
|
-
import { declareGlobalOption, emptyGlobals, globalTable } from './globals.js';
|
|
9
|
+
import { checkDescription, checkNoListingFacts, checkVersion, partFinding, slotSite, } from './facts.js';
|
|
10
|
+
import { declareGlobalOption, emptyGlobals, globalSite, globalTable } from './globals.js';
|
|
11
|
+
import { destinationReport, reportFailure } from './hints.js';
|
|
7
12
|
import { captureHost } from './host.js';
|
|
13
|
+
import { globalOptionAfterCommand } from './input-rules.js';
|
|
8
14
|
import { inspectGraph } from './inspect.js';
|
|
9
15
|
import { coreViews } from './lanes.js';
|
|
10
16
|
import { Output, reportPlainly } from './output.js';
|
|
11
|
-
import {
|
|
17
|
+
import { declaring, isPlainObject, shallowList, shallowRecord } from './plain.js';
|
|
18
|
+
import { invalidPacket, notAnObject, retiredApplicationOption } from './plugin-rules.js';
|
|
19
|
+
import { installPlugins, ownedSignals, pluginViews } from './plugin.js';
|
|
12
20
|
import { renderingPolicy } from './rendering.js';
|
|
21
|
+
import { brokenOutputView, runOptions, viewCorrection } from './rules.js';
|
|
13
22
|
import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
23
|
+
import { readTranslations, translateThrow } from './translators.js';
|
|
24
|
+
import { checkDeclarations, prepareInputs } from './validation.js';
|
|
25
|
+
import { buildViews, viewIdentities } from './view.js';
|
|
16
26
|
/** The registry a failure is reported through when the application's own could not be built. */
|
|
17
27
|
const noViews = [];
|
|
18
28
|
/**
|
|
@@ -27,6 +37,16 @@ function silenced(thrown, signal, cancelled) {
|
|
|
27
37
|
* `undefined` is itself throwable, so the absence is spelled here rather than borrowed from it.
|
|
28
38
|
*/
|
|
29
39
|
const noPrimary = Symbol('no primary');
|
|
40
|
+
/**
|
|
41
|
+
* The write state a run reports by. A destination whose write failure the action let propagate,
|
|
42
|
+
* and a translator answered, is reported as that translated failure, so its failure is no longer
|
|
43
|
+
* the destination fault that forces 1.
|
|
44
|
+
*/
|
|
45
|
+
function answeredWrite(writes, answered) {
|
|
46
|
+
return writes.kind === 'failed' && answered !== undefined && writes.error === answered
|
|
47
|
+
? { kind: 'ok' }
|
|
48
|
+
: writes;
|
|
49
|
+
}
|
|
30
50
|
/**
|
|
31
51
|
* Whether the primary outcome carries one recorded cause already: the value itself, or a failure
|
|
32
52
|
* that wraps it at any depth, which an action that caught a source failure and rethrew its own
|
|
@@ -54,10 +74,31 @@ function checkSignal(signal) {
|
|
|
54
74
|
return undefined;
|
|
55
75
|
}
|
|
56
76
|
if (!(signal instanceof AbortSignal)) {
|
|
57
|
-
throw new InternalError(
|
|
77
|
+
throw new InternalError(runOptions, {
|
|
78
|
+
cause: undefined,
|
|
79
|
+
correction: 'Supply the signal of an AbortController.',
|
|
80
|
+
sentence: 'run() received a signal that is not an AbortSignal.',
|
|
81
|
+
});
|
|
58
82
|
}
|
|
59
83
|
return signal;
|
|
60
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a throw in a cancelled run echoes its cancellation, so no translator is offered it. A
|
|
87
|
+
* value that cannot be read, such as an Error whose `name` getter throws, counts as an echo, so
|
|
88
|
+
* it keeps its cancellation code and no translator replaces it.
|
|
89
|
+
*/
|
|
90
|
+
function echoesCancellation(thrown, controller) {
|
|
91
|
+
const { signal } = controller;
|
|
92
|
+
if (!signal.aborted) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return isCancellationEcho(thrown, signal.reason);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
61
102
|
/**
|
|
62
103
|
* The Application holds the unnamed root's declaration state and applies the same transitions a
|
|
63
104
|
* Command does, so each declaration call has one typed implementation and no builder to recover.
|
|
@@ -78,7 +119,7 @@ class ApplicationBuilder {
|
|
|
78
119
|
}
|
|
79
120
|
argument(name, config) {
|
|
80
121
|
const input = {
|
|
81
|
-
config
|
|
122
|
+
config,
|
|
82
123
|
kind: 'argument',
|
|
83
124
|
name,
|
|
84
125
|
};
|
|
@@ -86,7 +127,7 @@ class ApplicationBuilder {
|
|
|
86
127
|
}
|
|
87
128
|
option(name, config) {
|
|
88
129
|
const input = {
|
|
89
|
-
config
|
|
130
|
+
config,
|
|
90
131
|
kind: 'option',
|
|
91
132
|
name,
|
|
92
133
|
};
|
|
@@ -94,19 +135,31 @@ class ApplicationBuilder {
|
|
|
94
135
|
}
|
|
95
136
|
globalOption(name, config) {
|
|
96
137
|
// The plugins' Commands attach at construction, so only the application's own calls close it.
|
|
138
|
+
// The config is judged after the order, so the finding prints it elided and reads none of it.
|
|
97
139
|
if (this.#config.composed) {
|
|
98
|
-
throw new DeclarationError(
|
|
140
|
+
throw new DeclarationError(globalOptionAfterCommand, {
|
|
141
|
+
correction: 'Declare global options before attaching Commands or registering an action.',
|
|
142
|
+
findings: [
|
|
143
|
+
{
|
|
144
|
+
arguments: callArguments(name, config === undefined ? undefined : spelled(elided)),
|
|
145
|
+
call: 'globalOption',
|
|
146
|
+
mark: '0',
|
|
147
|
+
path: [],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
sentence: `The Application declares global option ${quoted(name)} after command() or action().`,
|
|
151
|
+
});
|
|
99
152
|
}
|
|
100
153
|
const input = {
|
|
101
|
-
config
|
|
154
|
+
config,
|
|
102
155
|
kind: 'option',
|
|
103
156
|
name,
|
|
104
157
|
};
|
|
105
158
|
const descriptors = new Map(this.#root.descriptors);
|
|
106
|
-
const globals = declareGlobalOption(this.#globals, input, descriptors);
|
|
159
|
+
const { input: captured, state: globals } = declareGlobalOption(this.#globals, input, descriptors);
|
|
107
160
|
const root = { ...this.#root, descriptors };
|
|
108
161
|
checkDeclaredOptions(root, globalTable(globals.inputs, this.#config.plugins));
|
|
109
|
-
checkDeclarations([input]);
|
|
162
|
+
checkDeclarations([{ input: captured, site: globalSite(captured) }]);
|
|
110
163
|
return new ApplicationBuilder(this.#name, { config: this.#config, globals, root });
|
|
111
164
|
}
|
|
112
165
|
/** Registering the action closes input authoring; extension configuration remains available. */
|
|
@@ -120,7 +173,8 @@ class ApplicationBuilder {
|
|
|
120
173
|
owners: new Map(this.#config.owners),
|
|
121
174
|
table: this.table(),
|
|
122
175
|
};
|
|
123
|
-
const
|
|
176
|
+
const node = childNode(null, child);
|
|
177
|
+
const root = attachToRoot(this.#root, { node, placement: commandPlacement([], node.name) }, scope);
|
|
124
178
|
return this.derive(root, { composed: true, owners: scope.owners });
|
|
125
179
|
}
|
|
126
180
|
/**
|
|
@@ -180,7 +234,10 @@ class ApplicationBuilder {
|
|
|
180
234
|
rendering: () => undefined,
|
|
181
235
|
views: () => undefined,
|
|
182
236
|
});
|
|
183
|
-
return inspectGraph(this.#name, built.graph,
|
|
237
|
+
return inspectGraph(this.#name, built.graph, {
|
|
238
|
+
...built.facts,
|
|
239
|
+
development: this.#config.development,
|
|
240
|
+
});
|
|
184
241
|
}
|
|
185
242
|
async run(options) {
|
|
186
243
|
let stderr = process.stderr;
|
|
@@ -193,6 +250,27 @@ class ApplicationBuilder {
|
|
|
193
250
|
const faults = [];
|
|
194
251
|
// The failure this run reports as its primary outcome, so nothing reports it a second time.
|
|
195
252
|
let primary = noPrimary;
|
|
253
|
+
// Each failure a translator answered, keyed to the foreign throw it replaced.
|
|
254
|
+
const translatedFrom = new Map();
|
|
255
|
+
// Where a failure happened: the path routing walked, and what the hooks read once the graph built.
|
|
256
|
+
let walked = Object.freeze([]);
|
|
257
|
+
let reached = undefined;
|
|
258
|
+
// The host a failure's report reads, once it is captured; before that, the process's own.
|
|
259
|
+
let reportHost = undefined;
|
|
260
|
+
const scene = () => ({
|
|
261
|
+
application: this.#name,
|
|
262
|
+
built: reached,
|
|
263
|
+
host: (reportHost ??= captureHost(undefined, stderr)),
|
|
264
|
+
path: walked,
|
|
265
|
+
});
|
|
266
|
+
// What this run's build decides about its reports, shared by every report the run writes.
|
|
267
|
+
const build = {
|
|
268
|
+
development: this.#config.development,
|
|
269
|
+
generic: false,
|
|
270
|
+
reported: false,
|
|
271
|
+
};
|
|
272
|
+
// What broke the run's reporting: a destination's write error, or a throw while reporting.
|
|
273
|
+
let reportingCause = undefined;
|
|
196
274
|
// One private controller per run, subscribed to the caller's signal at run entry.
|
|
197
275
|
const controller = new AbortController();
|
|
198
276
|
/**
|
|
@@ -207,6 +285,20 @@ class ApplicationBuilder {
|
|
|
207
285
|
const reason = graphBuilt ? signals?.reason() : undefined;
|
|
208
286
|
return reason ? cancellationCode(reason) : undefined;
|
|
209
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* Offers one throw from the application's work to the translators. A view's failure and a
|
|
290
|
+
* cancellation echo are never offered, because each already names what it is.
|
|
291
|
+
*/
|
|
292
|
+
const offer = (thrown) => {
|
|
293
|
+
if (output?.raisedByView(thrown) === true || echoesCancellation(thrown, controller)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const failure = translateThrow(this.#config.translators, thrown);
|
|
297
|
+
if (failure !== undefined) {
|
|
298
|
+
translatedFrom.set(failure, thrown);
|
|
299
|
+
}
|
|
300
|
+
return failure;
|
|
301
|
+
};
|
|
210
302
|
/**
|
|
211
303
|
* Every exit path of the run leaves through the removal below, the one place it is written,
|
|
212
304
|
* so no listener this run installed outlives it however the run ends.
|
|
@@ -216,6 +308,7 @@ class ApplicationBuilder {
|
|
|
216
308
|
const overrides = options?.host;
|
|
217
309
|
stderr = overrides?.stderr ?? stderr;
|
|
218
310
|
const host = captureHost(overrides, stderr);
|
|
311
|
+
reportHost = host;
|
|
219
312
|
const invocationOutput = new Output(host, controller.signal);
|
|
220
313
|
output = invocationOutput;
|
|
221
314
|
// The constructor validated the declared policy, which the build hands over after the overrides.
|
|
@@ -226,7 +319,8 @@ class ApplicationBuilder {
|
|
|
226
319
|
invocationOutput.configure(policy, plugins.find((entry) => entry.theme !== undefined)?.theme ?? new Map());
|
|
227
320
|
},
|
|
228
321
|
rendering: (declared) => {
|
|
229
|
-
|
|
322
|
+
const rendering = options?.rendering;
|
|
323
|
+
policy = { ...declared, ...renderingPolicy(rendering, runRendering(rendering)) };
|
|
230
324
|
invocationOutput.configure(policy, new Map());
|
|
231
325
|
},
|
|
232
326
|
views: (value) => {
|
|
@@ -235,8 +329,17 @@ class ApplicationBuilder {
|
|
|
235
329
|
},
|
|
236
330
|
});
|
|
237
331
|
const { graph } = built;
|
|
332
|
+
// The graph `inspect()` returns, built at most once for the run, whoever reads it first.
|
|
333
|
+
let inspectedGraph = undefined;
|
|
334
|
+
const { development } = this.#config;
|
|
335
|
+
const inspected = () => (inspectedGraph ??= inspectGraph(this.#name, graph, { ...built.facts, development }));
|
|
336
|
+
reached = { inspected, plugins: built.plugins };
|
|
337
|
+
if (development) {
|
|
338
|
+
// A development build asks every converter at build, so its check runs on every run.
|
|
339
|
+
inspected();
|
|
340
|
+
}
|
|
238
341
|
const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
|
|
239
|
-
const defaults = await prepareInputs(inputs, host);
|
|
342
|
+
const defaults = await prepareInputs(inputs, host, inputPlaces(graph));
|
|
240
343
|
graphBuilt = true;
|
|
241
344
|
if (!controller.signal.aborted) {
|
|
242
345
|
/**
|
|
@@ -247,14 +350,15 @@ class ApplicationBuilder {
|
|
|
247
350
|
await runInvocation({
|
|
248
351
|
channel: (binding) => invocationOutput.channel(binding),
|
|
249
352
|
defaults,
|
|
250
|
-
facts: built.facts,
|
|
251
353
|
graph,
|
|
252
354
|
host,
|
|
253
|
-
|
|
355
|
+
inspected,
|
|
356
|
+
offer,
|
|
254
357
|
out: output.out,
|
|
255
358
|
plugins: built.plugins,
|
|
256
359
|
report: (fault) => faults.push(fault),
|
|
257
360
|
route: (path) => {
|
|
361
|
+
walked = path;
|
|
258
362
|
invocationOutput.useRoute(path);
|
|
259
363
|
},
|
|
260
364
|
signal: controller.signal,
|
|
@@ -268,80 +372,96 @@ class ApplicationBuilder {
|
|
|
268
372
|
const fault = output.fault;
|
|
269
373
|
if (fault) {
|
|
270
374
|
// The action returned, so the view failure is this invocation's own failure.
|
|
271
|
-
throw new InternalError(
|
|
375
|
+
throw new InternalError(brokenOutputView, {
|
|
376
|
+
cause: fault.cause,
|
|
377
|
+
correction: viewCorrection,
|
|
378
|
+
sentence: `Rendering output failed: ${reasonOf(fault.cause)}`,
|
|
379
|
+
});
|
|
272
380
|
}
|
|
273
381
|
}
|
|
274
382
|
catch (error) {
|
|
275
383
|
primary = error;
|
|
276
384
|
try {
|
|
277
385
|
const failure = toFailure(error);
|
|
278
|
-
code = failure
|
|
386
|
+
code = exitCodeOf(failure);
|
|
279
387
|
output ??= new Output(captureHost(undefined, stderr), controller.signal);
|
|
280
|
-
const writes = await output.settle();
|
|
388
|
+
const writes = answeredWrite(await output.settle(), translatedFrom.get(failure));
|
|
281
389
|
if (writes.kind === 'ok' && !silenced(error, controller.signal, cancellation())) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await output.report(report.text);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
390
|
+
// A broken failure view or onFailure hook forces 1 over the failure's own code.
|
|
391
|
+
const sink = { build, output, registry: registry ?? noViews, stderr };
|
|
392
|
+
if (await reportFailure(sink, failure, scene())) {
|
|
288
393
|
code = 1;
|
|
289
|
-
// `report.text` is core's default text, which already ends in `\n`.
|
|
290
|
-
await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
|
|
291
394
|
}
|
|
292
395
|
}
|
|
293
396
|
}
|
|
294
|
-
catch {
|
|
397
|
+
catch (reportError) {
|
|
295
398
|
code = 1;
|
|
296
399
|
reportingFailed = true;
|
|
400
|
+
reportingCause = reportError;
|
|
297
401
|
}
|
|
298
402
|
}
|
|
299
403
|
/**
|
|
300
404
|
* A sequence that stopped on its own source reports the same way: the call the action never
|
|
301
405
|
* awaited observed nothing, and a failure the action let propagate is the primary outcome
|
|
302
|
-
* already, so the one it raised is not reported twice.
|
|
406
|
+
* already, so the one it raised is not reported twice. A throw a translator replaced is
|
|
407
|
+
* carried by the failure it became, whether or not that failure keeps it as its cause. A
|
|
408
|
+
* foreign throw that is not carried is a deferred fault, offered to the translators here,
|
|
409
|
+
* where core would otherwise wrap it as an internal error.
|
|
303
410
|
*/
|
|
411
|
+
// A primary no translator answered replaced nothing, so even a thrown `undefined` is reported.
|
|
412
|
+
const replaced = translatedFrom.get(primary) ?? noPrimary;
|
|
413
|
+
const deferred = new Set();
|
|
304
414
|
for (const cause of output?.stopped ?? []) {
|
|
305
|
-
if (!carried(primary, cause)) {
|
|
306
|
-
|
|
415
|
+
if (!carried(primary, cause) && cause !== replaced) {
|
|
416
|
+
// A failure is never offered, so only a foreign throw can be translated here.
|
|
417
|
+
const translated = offer(cause);
|
|
418
|
+
if (translated !== undefined) {
|
|
419
|
+
deferred.add(translated);
|
|
420
|
+
}
|
|
421
|
+
faults.push(translated ?? toFailure(cause));
|
|
307
422
|
}
|
|
308
423
|
}
|
|
309
424
|
// A plugin's own fault is reported after the primary outcome and turns a would-be 0 into 1.
|
|
425
|
+
// A deferred fault a translator answered turns it into that failure's own code instead.
|
|
310
426
|
// The primary outcome keeps its code, the way a view failure leaves it alone.
|
|
311
427
|
// It is reported the way the primary failure is, so an override answers its class.
|
|
312
428
|
for (const fault of faults) {
|
|
313
429
|
if (!silenced(fault, controller.signal, cancellation())) {
|
|
314
|
-
|
|
430
|
+
const own = deferred.has(fault) ? exitCodeOf(fault) : 1;
|
|
431
|
+
code = code === 0 ? own : code;
|
|
315
432
|
try {
|
|
316
|
-
const
|
|
317
|
-
if (
|
|
318
|
-
await output?.report(report.text);
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
433
|
+
const sink = output && { build, output, registry: registry ?? noViews, stderr };
|
|
434
|
+
if (sink && (await reportFailure(sink, fault, scene()))) {
|
|
321
435
|
code = 1;
|
|
322
|
-
await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
|
|
323
436
|
}
|
|
324
437
|
}
|
|
325
|
-
catch {
|
|
438
|
+
catch (reportError) {
|
|
326
439
|
reportingFailed = true;
|
|
440
|
+
reportingCause ??= reportError;
|
|
327
441
|
}
|
|
328
442
|
}
|
|
329
443
|
}
|
|
330
444
|
if (output) {
|
|
331
|
-
const writes = await output.settle();
|
|
445
|
+
const writes = answeredWrite(await output.settle(), translatedFrom.get(primary));
|
|
332
446
|
if (writes.kind === 'failed') {
|
|
333
447
|
code = 1;
|
|
334
448
|
reportingFailed = true;
|
|
449
|
+
reportingCause ??= writes.error;
|
|
335
450
|
}
|
|
336
451
|
output.dispose();
|
|
337
452
|
}
|
|
338
453
|
if (reportingFailed) {
|
|
339
|
-
|
|
454
|
+
const { application, host } = scene();
|
|
455
|
+
const text = destinationReport(build, reportingCause, { application, host });
|
|
456
|
+
if (text !== '') {
|
|
457
|
+
await reportPlainly(stderr, text);
|
|
458
|
+
}
|
|
340
459
|
}
|
|
341
460
|
/**
|
|
342
461
|
* One rule orders every code: a cancelled run resolves its signal's code, and a broken
|
|
343
|
-
* failure view or destination in that run is reported as text without
|
|
344
|
-
* signal decides the code whatever the action did afterward, so this
|
|
462
|
+
* failure view, onFailure hook, or destination in that run is reported as text without
|
|
463
|
+
* changing it. The signal decides the code whatever the action did afterward, so this
|
|
464
|
+
* reading comes last.
|
|
345
465
|
*/
|
|
346
466
|
code = cancellation() ?? code;
|
|
347
467
|
process.exitCode = code;
|
|
@@ -352,48 +472,140 @@ class ApplicationBuilder {
|
|
|
352
472
|
}
|
|
353
473
|
}
|
|
354
474
|
}
|
|
355
|
-
/**
|
|
356
|
-
|
|
475
|
+
/** The retired Application options, in the order they are rejected, each with its fix. */
|
|
476
|
+
const retired = [
|
|
477
|
+
['globals', 'Declare them with globalOption(name, config).'],
|
|
478
|
+
['failures', 'Declare view overrides under views with override(key, view).'],
|
|
479
|
+
];
|
|
480
|
+
/** Where one Application option sits, rebuilt as `new Application(name, { key })`. */
|
|
481
|
+
function optionSite(name, key, value) {
|
|
482
|
+
return slotSite({ call: 'new Application', named: name, subject: 'The Application' }, key, value);
|
|
483
|
+
}
|
|
484
|
+
/** Where `run()`'s own rendering policy sits: the options object of the call on the Application. */
|
|
485
|
+
function runRendering(rendering) {
|
|
486
|
+
return {
|
|
487
|
+
at: '0.rendering',
|
|
488
|
+
declaration: { arguments: [{ rendering }], call: 'run', path: [] },
|
|
489
|
+
subject: 'The run',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Reject obsolete wiring before silently losing options that invocations depend on. `options` is the
|
|
494
|
+
* copy `captureOptions` took, or `undefined` for an Application declared without options.
|
|
495
|
+
*/
|
|
496
|
+
function checkOptions(name, options) {
|
|
497
|
+
const site = {
|
|
498
|
+
at: '1',
|
|
499
|
+
declaration: { arguments: callArguments(name, options), call: 'new Application' },
|
|
500
|
+
subject: 'The Application',
|
|
501
|
+
};
|
|
357
502
|
if (options === undefined) {
|
|
358
|
-
return { description: undefined, version: checkVersion(undefined) };
|
|
359
|
-
}
|
|
360
|
-
if (!isPlainObject(options)) {
|
|
361
|
-
throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
|
|
362
|
-
}
|
|
363
|
-
if ('globals' in options) {
|
|
364
|
-
throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
|
|
503
|
+
return { description: undefined, version: checkVersion(site, undefined) };
|
|
365
504
|
}
|
|
366
|
-
|
|
367
|
-
|
|
505
|
+
for (const [key, correction] of retired) {
|
|
506
|
+
if (key in options) {
|
|
507
|
+
const retiredSite = optionSite(name, key, Reflect.get(options, key));
|
|
508
|
+
throw new DeclarationError(retiredApplicationOption, {
|
|
509
|
+
correction,
|
|
510
|
+
findings: [partFinding(retiredSite, [])],
|
|
511
|
+
sentence: `The Application options contain ${key}.`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
368
514
|
}
|
|
369
515
|
// The root is every page's entry point, so it carries neither listing fact.
|
|
370
516
|
// A key that may not be there is a fault of the slot, so it answers with the slot's shape.
|
|
371
|
-
checkNoListingFacts(
|
|
517
|
+
checkNoListingFacts(site, options);
|
|
372
518
|
return {
|
|
373
|
-
description: checkDescription(
|
|
374
|
-
version: checkVersion(options.version),
|
|
519
|
+
description: checkDescription(site, options.description),
|
|
520
|
+
version: checkVersion(site, options.version),
|
|
375
521
|
};
|
|
376
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Whether the packet an Application received reads `development`. No packet is distributed, so an
|
|
525
|
+
* application that never opted in cannot show an operator the author's detail. The build is read
|
|
526
|
+
* once, here, so a later change to the imported object changes no run.
|
|
527
|
+
*/
|
|
528
|
+
function readPacket(name, packet) {
|
|
529
|
+
if (packet === undefined) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
const site = optionSite(name, 'packet', packet);
|
|
533
|
+
if (!isPlainObject(packet)) {
|
|
534
|
+
throw new DeclarationError(invalidPacket, {
|
|
535
|
+
correction: 'Import loom.packet.json and pass it as packet.',
|
|
536
|
+
findings: [partFinding(site, [])],
|
|
537
|
+
sentence: 'The Application packet must be an object.',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const { build } = packet;
|
|
541
|
+
if (build === 'development' || build === 'distributed') {
|
|
542
|
+
return build === 'development';
|
|
543
|
+
}
|
|
544
|
+
const found = build === undefined
|
|
545
|
+
? 'The packet has no build.'
|
|
546
|
+
: `The packet's build is ${typeof build === 'string' ? `"${escapeControlCharacters(build)}"` : 'not a string'}.`;
|
|
547
|
+
throw new DeclarationError(invalidPacket, {
|
|
548
|
+
correction: 'Set build to "development" or "distributed".',
|
|
549
|
+
findings: [partFinding(site, 'build' in packet ? ['build'] : [])],
|
|
550
|
+
sentence: found,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/** The parts of the Application options that are lists, each copied with its entries as built. */
|
|
554
|
+
const optionLists = ['plugins', 'views', 'translators', 'extensions'];
|
|
555
|
+
/**
|
|
556
|
+
* The one copy of the options that `new Application(name, options)` reads: the options object, its
|
|
557
|
+
* packet and rendering policy, and each list it holds. An entry of a list is what its factory built
|
|
558
|
+
* and is not copied. A read that throws is the unreadable fault of the options, and a value that is
|
|
559
|
+
* not a plain object is the not-an-object fault. An Application declared without options has none
|
|
560
|
+
* to copy.
|
|
561
|
+
*/
|
|
562
|
+
function captureOptions(name, options) {
|
|
563
|
+
if (options === undefined) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
return captureDeclaration(options, (copy, read) => {
|
|
567
|
+
read.nested(copy, 'packet', shallowRecord);
|
|
568
|
+
read.nested(copy, 'rendering', shallowRecord);
|
|
569
|
+
for (const list of optionLists) {
|
|
570
|
+
read.nested(copy, list, shallowList);
|
|
571
|
+
}
|
|
572
|
+
}, {
|
|
573
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
574
|
+
correction: 'Supply an Application options object.',
|
|
575
|
+
findings: [{ arguments: [name, options], call: 'new Application', mark: '1' }],
|
|
576
|
+
sentence: 'The Application declares options that are not an object.',
|
|
577
|
+
}),
|
|
578
|
+
unreadable: unreadableArgument({ call: 'new Application', named: name, subject: 'The Application' }, 'options'),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
377
581
|
/**
|
|
378
582
|
* Every rule `new Application(name, options)` applies, in the order it reads the slot: the
|
|
379
|
-
* application's own view overrides, the rendering policy, the options slot and
|
|
380
|
-
* installed list and every rule between two plugins, the root's extension values,
|
|
381
|
-
* plugin's Commands, which attach to the root first, in installation order and list
|
|
583
|
+
* application's own view overrides, its translations, the rendering policy, the options slot and
|
|
584
|
+
* its facts, the installed list and every rule between two plugins, the root's extension values,
|
|
585
|
+
* and then each plugin's Commands, which attach to the root first, in installation order and list
|
|
586
|
+
* order. Each reads the one copy of the options `captureOptions` takes.
|
|
382
587
|
*/
|
|
383
|
-
function declareApplication(
|
|
384
|
-
const slot =
|
|
588
|
+
function declareApplication(name, declared) {
|
|
589
|
+
const slot = captureOptions(name, declared);
|
|
385
590
|
const identities = viewIdentities(coreViews);
|
|
386
|
-
const views = buildViews({
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
591
|
+
const views = buildViews({
|
|
592
|
+
declares: false,
|
|
593
|
+
owner: { call: 'new Application', named: name },
|
|
594
|
+
sentence: 'The Application',
|
|
595
|
+
}, slot?.views, identities);
|
|
596
|
+
const translations = readTranslations(optionSite(name, 'translators', slot?.translators), slot?.translators);
|
|
597
|
+
const rendering = renderingPolicy(slot?.rendering, optionSite(name, 'rendering', slot?.rendering));
|
|
598
|
+
const facts = checkOptions(name, slot);
|
|
599
|
+
const development = readPacket(name, slot?.packet);
|
|
600
|
+
const installed = installPlugins(name, slot?.plugins ?? []);
|
|
390
601
|
const { plugins } = installed;
|
|
391
|
-
const contributors = plugins.map((entry) => buildViews(
|
|
602
|
+
const contributors = plugins.map((entry) => buildViews(pluginViews(entry.identity), entry.views, identities));
|
|
392
603
|
const table = globalTable([], plugins);
|
|
393
604
|
const descriptors = installed.descriptors;
|
|
394
605
|
const extensions = storeCommandLayers({
|
|
395
606
|
descriptors,
|
|
396
607
|
layers: [slot?.extensions],
|
|
608
|
+
site: optionSite(name, 'extensions', slot?.extensions),
|
|
397
609
|
subject: layerOf(null),
|
|
398
610
|
});
|
|
399
611
|
// The Application checks its own facts, so the root carries none.
|
|
@@ -406,14 +618,24 @@ function declareApplication(options) {
|
|
|
406
618
|
});
|
|
407
619
|
const owners = new Map();
|
|
408
620
|
for (const command of plugins.flatMap((entry) => entry.commands)) {
|
|
409
|
-
root = attachToRoot(root, command
|
|
621
|
+
root = attachToRoot(root, command, {
|
|
410
622
|
descriptors: new Map(root.descriptors),
|
|
411
623
|
owners,
|
|
412
624
|
table,
|
|
413
625
|
});
|
|
414
626
|
}
|
|
415
627
|
return {
|
|
416
|
-
config: {
|
|
628
|
+
config: {
|
|
629
|
+
composed: false,
|
|
630
|
+
contributors,
|
|
631
|
+
development,
|
|
632
|
+
facts,
|
|
633
|
+
owners,
|
|
634
|
+
plugins,
|
|
635
|
+
rendering,
|
|
636
|
+
translators: [translations, ...plugins.map((entry) => entry.translators)],
|
|
637
|
+
views,
|
|
638
|
+
},
|
|
417
639
|
globals: emptyGlobals(),
|
|
418
640
|
root,
|
|
419
641
|
};
|
|
@@ -421,7 +643,11 @@ function declareApplication(options) {
|
|
|
421
643
|
/** The application name is typed as a command at the prompt, so it answers to the portable rule. */
|
|
422
644
|
function checkApplicationName(name) {
|
|
423
645
|
if (!isPortableName(name)) {
|
|
424
|
-
throw new DeclarationError(
|
|
646
|
+
throw new DeclarationError(portableName, {
|
|
647
|
+
correction: portableNameCorrection,
|
|
648
|
+
findings: [{ arguments: [name], call: 'new Application', mark: '0' }],
|
|
649
|
+
sentence: `Application name ${quoted(name)} is invalid.`,
|
|
650
|
+
});
|
|
425
651
|
}
|
|
426
652
|
return name;
|
|
427
653
|
}
|
|
@@ -429,7 +655,8 @@ function checkApplicationName(name) {
|
|
|
429
655
|
class ApplicationDeclaration extends ApplicationBuilder {
|
|
430
656
|
constructor(name, options) {
|
|
431
657
|
// The arguments evaluate in order, so the name is checked before any option is read.
|
|
432
|
-
|
|
658
|
+
const checked = checkApplicationName(name);
|
|
659
|
+
super(checked, declaring(() => declareApplication(checked, options)));
|
|
433
660
|
}
|
|
434
661
|
}
|
|
435
662
|
export const Application = ApplicationDeclaration;
|
package/dist/bindings.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FactSite } from './facts.js';
|
|
1
2
|
/** The two keys the binding rules read, whichever scope declared the option. */
|
|
2
3
|
interface BindingConfig {
|
|
3
4
|
readonly env?: unknown;
|
|
@@ -6,21 +7,25 @@ interface BindingConfig {
|
|
|
6
7
|
/**
|
|
7
8
|
* The environment binding one option declares, or `undefined` when it declares none. A multiple
|
|
8
9
|
* option takes its list from the configuration source, so it cannot bind, and a bound name must be
|
|
9
|
-
* a variable name.
|
|
10
|
+
* a variable name. The site names the declaration and marks its `env`.
|
|
10
11
|
*/
|
|
11
|
-
export declare function checkEnvBinding(
|
|
12
|
+
export declare function checkEnvBinding(site: FactSite, config: BindingConfig): string | undefined;
|
|
12
13
|
/** An argument is identified by its place among bare tokens, so no variable can stand in for it. */
|
|
13
|
-
export declare function checkNoArgumentBinding(
|
|
14
|
-
/**
|
|
14
|
+
export declare function checkNoArgumentBinding(site: FactSite, config: object): void;
|
|
15
|
+
/**
|
|
16
|
+
* One option bound to a variable: the phrase a duplicate-variable diagnostic names it by, and the
|
|
17
|
+
* site whose `env` its finding marks.
|
|
18
|
+
*/
|
|
15
19
|
export interface BoundOption {
|
|
16
|
-
readonly
|
|
20
|
+
readonly phrase: string;
|
|
21
|
+
readonly site: FactSite;
|
|
17
22
|
readonly variable: string;
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
|
-
* The variables one scope binds, each to the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* The variables one scope binds, each to the option that binds it. Within one invocation's scope a
|
|
26
|
+
* variable binds one option, so a second binder is a declaration error that names the first
|
|
27
|
+
* binder, in scope order, and then the second. `held` is what an enclosing scope already binds,
|
|
28
|
+
* such as the globals table under a Command's own options.
|
|
24
29
|
*/
|
|
25
|
-
export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string,
|
|
30
|
+
export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string, BoundOption>): ReadonlyMap<string, BoundOption>;
|
|
26
31
|
export {};
|