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