@loomcli/core 0.5.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/dist/application.d.ts +18 -2
- package/dist/application.js +266 -71
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- 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 +695 -226
- 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 +66 -11
- package/dist/facts.js +98 -22
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +115 -46
- 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 +11 -2
- package/dist/index.js +5 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +12 -4
- package/dist/inspect.js +88 -28
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +23 -2
- package/dist/options.js +135 -44
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- 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 +22 -10
- package/dist/plugin.js +348 -116
- 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 +22 -13
- 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 +11 -1
- package/dist/validation.d.ts +44 -3
- package/dist/validation.js +156 -57
- package/dist/view.d.ts +48 -14
- package/dist/view.js +155 -77
- package/package.json +1 -1
package/dist/application.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, Command, CommandMethod, CommandNodeHandle, CommandState, ResultMethod } from './command.js';
|
|
1
|
+
import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, ChildOwner, Command, CommandMethod, CommandNodeHandle, CommandState, ResultMethod } from './command.js';
|
|
2
2
|
import type { ApplicationEnvironment, applicationEnvironment } from './environment.js';
|
|
3
3
|
import type { ExtensionValue } from './extension.js';
|
|
4
4
|
import type { GlobalsState } from './globals.js';
|
|
5
5
|
import type { CommandGraph } from './inspect.js';
|
|
6
6
|
import type { BuiltPlugin, Plugin } from './plugin.js';
|
|
7
7
|
import type { RenderingPolicy } from './rendering.js';
|
|
8
|
+
import type { Translation, TranslatorRegistry } from './translators.js';
|
|
8
9
|
import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, GlobalOmissionConstraint, PerValueConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
|
|
9
10
|
import type { ViewContributions, ViewOverride } from './view.js';
|
|
10
11
|
/**
|
|
@@ -14,9 +15,20 @@ import type { ViewContributions, ViewOverride } from './view.js';
|
|
|
14
15
|
* to no bare token, so it has no name to alias.
|
|
15
16
|
*/
|
|
16
17
|
export type ApplicationMethod = Exclude<CommandMethod, 'alias'> | 'globalOption';
|
|
18
|
+
/**
|
|
19
|
+
* The build fact file, `loom.packet.json`, that the entry imports and hands to the Application.
|
|
20
|
+
* `build` is typed `string`, because a JSON module types its members that way, and the Application
|
|
21
|
+
* constructor accepts `development` or `distributed` alone. Core ignores every other member.
|
|
22
|
+
*/
|
|
23
|
+
export interface Packet {
|
|
24
|
+
readonly build: string;
|
|
25
|
+
}
|
|
17
26
|
export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly Plugin[]> {
|
|
18
27
|
rendering?: RenderingPolicy;
|
|
28
|
+
/** The packet that says whether this is a development build. With none, it is distributed. */
|
|
29
|
+
packet?: Packet;
|
|
19
30
|
views?: readonly ViewOverride[];
|
|
31
|
+
translators?: readonly Translation[];
|
|
20
32
|
plugins?: Plugins;
|
|
21
33
|
extensions?: readonly ExtensionValue<'command'>[];
|
|
22
34
|
description?: string;
|
|
@@ -29,13 +41,17 @@ export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly
|
|
|
29
41
|
interface ApplicationConfig {
|
|
30
42
|
/** Whether the application's own `command()` or `action()` has run, which closes `globalOption()`. */
|
|
31
43
|
composed: boolean;
|
|
44
|
+
/** Whether the packet reads `development`, read once at construction. */
|
|
45
|
+
development: boolean;
|
|
32
46
|
/** Each installed plugin's view contributions, in installation order. */
|
|
33
47
|
contributors: readonly ViewContributions[];
|
|
34
48
|
facts: ApplicationFacts;
|
|
35
49
|
/** The parent that claimed each node the graph holds, so one value attaches at one point. */
|
|
36
|
-
owners: ReadonlyMap<CommandNodeHandle,
|
|
50
|
+
owners: ReadonlyMap<CommandNodeHandle, ChildOwner>;
|
|
37
51
|
plugins: readonly BuiltPlugin[];
|
|
38
52
|
rendering: RenderingPolicy;
|
|
53
|
+
/** The application's translations, then each installed plugin's, in resolution order. */
|
|
54
|
+
translators: TranslatorRegistry;
|
|
39
55
|
/** The application's own view overrides. */
|
|
40
56
|
views: ViewContributions;
|
|
41
57
|
}
|
package/dist/application.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { runInvocation } from './chain.js';
|
|
2
|
-
import {
|
|
3
|
-
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';
|
|
4
6
|
import { storeCommandLayers } from './extension.js';
|
|
5
|
-
import { checkDescription, checkNoListingFacts, checkVersion,
|
|
6
|
-
import { declareGlobalOption, emptyGlobals, globalTable } from './globals.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';
|
|
7
10
|
import { captureHost } from './host.js';
|
|
11
|
+
import { globalOptionAfterCommand } from './input-rules.js';
|
|
8
12
|
import { inspectGraph } from './inspect.js';
|
|
9
13
|
import { coreViews } from './lanes.js';
|
|
10
14
|
import { Output, reportPlainly } from './output.js';
|
|
11
|
-
import {
|
|
15
|
+
import { isPlainObject } from './plain.js';
|
|
16
|
+
import { invalidPacket, notAnObject, retiredApplicationOption } from './plugin-rules.js';
|
|
17
|
+
import { installPlugins, ownedSignals, pluginViews } from './plugin.js';
|
|
12
18
|
import { renderingPolicy } from './rendering.js';
|
|
19
|
+
import { brokenOutputView, runOptions, viewCorrection } from './rules.js';
|
|
13
20
|
import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
21
|
+
import { readTranslations, translateThrow } from './translators.js';
|
|
22
|
+
import { checkDeclarations, prepareInputs } from './validation.js';
|
|
23
|
+
import { buildViews, viewIdentities } from './view.js';
|
|
16
24
|
/** The registry a failure is reported through when the application's own could not be built. */
|
|
17
25
|
const noViews = [];
|
|
18
26
|
/**
|
|
@@ -27,6 +35,16 @@ function silenced(thrown, signal, cancelled) {
|
|
|
27
35
|
* `undefined` is itself throwable, so the absence is spelled here rather than borrowed from it.
|
|
28
36
|
*/
|
|
29
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
|
+
}
|
|
30
48
|
/**
|
|
31
49
|
* Whether the primary outcome carries one recorded cause already: the value itself, or a failure
|
|
32
50
|
* that wraps it at any depth, which an action that caught a source failure and rethrew its own
|
|
@@ -54,10 +72,31 @@ function checkSignal(signal) {
|
|
|
54
72
|
return undefined;
|
|
55
73
|
}
|
|
56
74
|
if (!(signal instanceof AbortSignal)) {
|
|
57
|
-
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
|
+
});
|
|
58
80
|
}
|
|
59
81
|
return signal;
|
|
60
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
|
+
}
|
|
61
100
|
/**
|
|
62
101
|
* The Application holds the unnamed root's declaration state and applies the same transitions a
|
|
63
102
|
* Command does, so each declaration call has one typed implementation and no builder to recover.
|
|
@@ -78,7 +117,7 @@ class ApplicationBuilder {
|
|
|
78
117
|
}
|
|
79
118
|
argument(name, config) {
|
|
80
119
|
const input = {
|
|
81
|
-
config
|
|
120
|
+
config,
|
|
82
121
|
kind: 'argument',
|
|
83
122
|
name,
|
|
84
123
|
};
|
|
@@ -86,7 +125,7 @@ class ApplicationBuilder {
|
|
|
86
125
|
}
|
|
87
126
|
option(name, config) {
|
|
88
127
|
const input = {
|
|
89
|
-
config
|
|
128
|
+
config,
|
|
90
129
|
kind: 'option',
|
|
91
130
|
name,
|
|
92
131
|
};
|
|
@@ -95,18 +134,24 @@ class ApplicationBuilder {
|
|
|
95
134
|
globalOption(name, config) {
|
|
96
135
|
// The plugins' Commands attach at construction, so only the application's own calls close it.
|
|
97
136
|
if (this.#config.composed) {
|
|
98
|
-
throw new DeclarationError(
|
|
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
|
+
});
|
|
99
144
|
}
|
|
100
145
|
const input = {
|
|
101
|
-
config
|
|
146
|
+
config,
|
|
102
147
|
kind: 'option',
|
|
103
148
|
name,
|
|
104
149
|
};
|
|
105
150
|
const descriptors = new Map(this.#root.descriptors);
|
|
106
|
-
const globals = declareGlobalOption(this.#globals, input, descriptors);
|
|
151
|
+
const { input: captured, state: globals } = declareGlobalOption(this.#globals, input, descriptors);
|
|
107
152
|
const root = { ...this.#root, descriptors };
|
|
108
153
|
checkDeclaredOptions(root, globalTable(globals.inputs, this.#config.plugins));
|
|
109
|
-
checkDeclarations([input]);
|
|
154
|
+
checkDeclarations([{ input: captured, site: globalSite(captured) }]);
|
|
110
155
|
return new ApplicationBuilder(this.#name, { config: this.#config, globals, root });
|
|
111
156
|
}
|
|
112
157
|
/** Registering the action closes input authoring; extension configuration remains available. */
|
|
@@ -120,7 +165,8 @@ class ApplicationBuilder {
|
|
|
120
165
|
owners: new Map(this.#config.owners),
|
|
121
166
|
table: this.table(),
|
|
122
167
|
};
|
|
123
|
-
const
|
|
168
|
+
const node = childNode(null, child);
|
|
169
|
+
const root = attachToRoot(this.#root, { node, placement: commandPlacement([], node.name) }, scope);
|
|
124
170
|
return this.derive(root, { composed: true, owners: scope.owners });
|
|
125
171
|
}
|
|
126
172
|
/**
|
|
@@ -180,7 +226,10 @@ class ApplicationBuilder {
|
|
|
180
226
|
rendering: () => undefined,
|
|
181
227
|
views: () => undefined,
|
|
182
228
|
});
|
|
183
|
-
return inspectGraph(this.#name, built.graph,
|
|
229
|
+
return inspectGraph(this.#name, built.graph, {
|
|
230
|
+
...built.facts,
|
|
231
|
+
development: this.#config.development,
|
|
232
|
+
});
|
|
184
233
|
}
|
|
185
234
|
async run(options) {
|
|
186
235
|
let stderr = process.stderr;
|
|
@@ -193,6 +242,27 @@ class ApplicationBuilder {
|
|
|
193
242
|
const faults = [];
|
|
194
243
|
// The failure this run reports as its primary outcome, so nothing reports it a second time.
|
|
195
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;
|
|
196
266
|
// One private controller per run, subscribed to the caller's signal at run entry.
|
|
197
267
|
const controller = new AbortController();
|
|
198
268
|
/**
|
|
@@ -207,6 +277,20 @@ class ApplicationBuilder {
|
|
|
207
277
|
const reason = graphBuilt ? signals?.reason() : undefined;
|
|
208
278
|
return reason ? cancellationCode(reason) : undefined;
|
|
209
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
|
+
};
|
|
210
294
|
/**
|
|
211
295
|
* Every exit path of the run leaves through the removal below, the one place it is written,
|
|
212
296
|
* so no listener this run installed outlives it however the run ends.
|
|
@@ -216,6 +300,7 @@ class ApplicationBuilder {
|
|
|
216
300
|
const overrides = options?.host;
|
|
217
301
|
stderr = overrides?.stderr ?? stderr;
|
|
218
302
|
const host = captureHost(overrides, stderr);
|
|
303
|
+
reportHost = host;
|
|
219
304
|
const invocationOutput = new Output(host, controller.signal);
|
|
220
305
|
output = invocationOutput;
|
|
221
306
|
// The constructor validated the declared policy, which the build hands over after the overrides.
|
|
@@ -226,7 +311,8 @@ class ApplicationBuilder {
|
|
|
226
311
|
invocationOutput.configure(policy, plugins.find((entry) => entry.theme !== undefined)?.theme ?? new Map());
|
|
227
312
|
},
|
|
228
313
|
rendering: (declared) => {
|
|
229
|
-
|
|
314
|
+
const rendering = options?.rendering;
|
|
315
|
+
policy = { ...declared, ...renderingPolicy(rendering, runRendering(rendering)) };
|
|
230
316
|
invocationOutput.configure(policy, new Map());
|
|
231
317
|
},
|
|
232
318
|
views: (value) => {
|
|
@@ -235,8 +321,17 @@ class ApplicationBuilder {
|
|
|
235
321
|
},
|
|
236
322
|
});
|
|
237
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
|
+
}
|
|
238
333
|
const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
|
|
239
|
-
const defaults = await prepareInputs(inputs, host);
|
|
334
|
+
const defaults = await prepareInputs(inputs, host, inputPlaces(graph));
|
|
240
335
|
graphBuilt = true;
|
|
241
336
|
if (!controller.signal.aborted) {
|
|
242
337
|
/**
|
|
@@ -247,14 +342,15 @@ class ApplicationBuilder {
|
|
|
247
342
|
await runInvocation({
|
|
248
343
|
channel: (binding) => invocationOutput.channel(binding),
|
|
249
344
|
defaults,
|
|
250
|
-
facts: built.facts,
|
|
251
345
|
graph,
|
|
252
346
|
host,
|
|
253
|
-
|
|
347
|
+
inspected,
|
|
348
|
+
offer,
|
|
254
349
|
out: output.out,
|
|
255
350
|
plugins: built.plugins,
|
|
256
351
|
report: (fault) => faults.push(fault),
|
|
257
352
|
route: (path) => {
|
|
353
|
+
walked = path;
|
|
258
354
|
invocationOutput.useRoute(path);
|
|
259
355
|
},
|
|
260
356
|
signal: controller.signal,
|
|
@@ -268,80 +364,96 @@ class ApplicationBuilder {
|
|
|
268
364
|
const fault = output.fault;
|
|
269
365
|
if (fault) {
|
|
270
366
|
// The action returned, so the view failure is this invocation's own failure.
|
|
271
|
-
throw new InternalError(
|
|
367
|
+
throw new InternalError(brokenOutputView, {
|
|
368
|
+
cause: fault.cause,
|
|
369
|
+
correction: viewCorrection,
|
|
370
|
+
sentence: `Rendering output failed: ${reasonOf(fault.cause)}`,
|
|
371
|
+
});
|
|
272
372
|
}
|
|
273
373
|
}
|
|
274
374
|
catch (error) {
|
|
275
375
|
primary = error;
|
|
276
376
|
try {
|
|
277
377
|
const failure = toFailure(error);
|
|
278
|
-
code = failure
|
|
378
|
+
code = exitCodeOf(failure);
|
|
279
379
|
output ??= new Output(captureHost(undefined, stderr), controller.signal);
|
|
280
|
-
const writes = await output.settle();
|
|
380
|
+
const writes = answeredWrite(await output.settle(), translatedFrom.get(failure));
|
|
281
381
|
if (writes.kind === 'ok' && !silenced(error, controller.signal, cancellation())) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await output.report(report.text);
|
|
286
|
-
}
|
|
287
|
-
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())) {
|
|
288
385
|
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
386
|
}
|
|
292
387
|
}
|
|
293
388
|
}
|
|
294
|
-
catch {
|
|
389
|
+
catch (reportError) {
|
|
295
390
|
code = 1;
|
|
296
391
|
reportingFailed = true;
|
|
392
|
+
reportingCause = reportError;
|
|
297
393
|
}
|
|
298
394
|
}
|
|
299
395
|
/**
|
|
300
396
|
* A sequence that stopped on its own source reports the same way: the call the action never
|
|
301
397
|
* 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.
|
|
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.
|
|
303
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();
|
|
304
406
|
for (const cause of output?.stopped ?? []) {
|
|
305
|
-
if (!carried(primary, cause)) {
|
|
306
|
-
|
|
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));
|
|
307
414
|
}
|
|
308
415
|
}
|
|
309
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.
|
|
310
418
|
// The primary outcome keeps its code, the way a view failure leaves it alone.
|
|
311
419
|
// It is reported the way the primary failure is, so an override answers its class.
|
|
312
420
|
for (const fault of faults) {
|
|
313
421
|
if (!silenced(fault, controller.signal, cancellation())) {
|
|
314
|
-
|
|
422
|
+
const own = deferred.has(fault) ? exitCodeOf(fault) : 1;
|
|
423
|
+
code = code === 0 ? own : code;
|
|
315
424
|
try {
|
|
316
|
-
const
|
|
317
|
-
if (
|
|
318
|
-
await output?.report(report.text);
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
425
|
+
const sink = output && { build, output, registry: registry ?? noViews, stderr };
|
|
426
|
+
if (sink && (await reportFailure(sink, fault, scene()))) {
|
|
321
427
|
code = 1;
|
|
322
|
-
await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
|
|
323
428
|
}
|
|
324
429
|
}
|
|
325
|
-
catch {
|
|
430
|
+
catch (reportError) {
|
|
326
431
|
reportingFailed = true;
|
|
432
|
+
reportingCause ??= reportError;
|
|
327
433
|
}
|
|
328
434
|
}
|
|
329
435
|
}
|
|
330
436
|
if (output) {
|
|
331
|
-
const writes = await output.settle();
|
|
437
|
+
const writes = answeredWrite(await output.settle(), translatedFrom.get(primary));
|
|
332
438
|
if (writes.kind === 'failed') {
|
|
333
439
|
code = 1;
|
|
334
440
|
reportingFailed = true;
|
|
441
|
+
reportingCause ??= writes.error;
|
|
335
442
|
}
|
|
336
443
|
output.dispose();
|
|
337
444
|
}
|
|
338
445
|
if (reportingFailed) {
|
|
339
|
-
|
|
446
|
+
const { application, host } = scene();
|
|
447
|
+
const text = destinationReport(build, reportingCause, { application, host });
|
|
448
|
+
if (text !== '') {
|
|
449
|
+
await reportPlainly(stderr, text);
|
|
450
|
+
}
|
|
340
451
|
}
|
|
341
452
|
/**
|
|
342
453
|
* 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
|
|
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.
|
|
345
457
|
*/
|
|
346
458
|
code = cancellation() ?? code;
|
|
347
459
|
process.exitCode = code;
|
|
@@ -352,48 +464,116 @@ class ApplicationBuilder {
|
|
|
352
464
|
}
|
|
353
465
|
}
|
|
354
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
|
+
}
|
|
355
484
|
/** Reject obsolete wiring before silently losing options that invocations depend on. */
|
|
356
|
-
function checkOptions(options) {
|
|
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
|
+
};
|
|
357
491
|
if (options === undefined) {
|
|
358
|
-
return { description: undefined, version: checkVersion(undefined) };
|
|
492
|
+
return { description: undefined, version: checkVersion(site, undefined) };
|
|
359
493
|
}
|
|
360
494
|
if (!isPlainObject(options)) {
|
|
361
|
-
throw new DeclarationError(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
});
|
|
365
500
|
}
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
});
|
|
509
|
+
}
|
|
368
510
|
}
|
|
369
511
|
// The root is every page's entry point, so it carries neither listing fact.
|
|
370
512
|
// A key that may not be there is a fault of the slot, so it answers with the slot's shape.
|
|
371
|
-
checkNoListingFacts(
|
|
513
|
+
checkNoListingFacts(site, options);
|
|
372
514
|
return {
|
|
373
|
-
description: checkDescription(
|
|
374
|
-
version: checkVersion(options.version),
|
|
515
|
+
description: checkDescription(site, options.description),
|
|
516
|
+
version: checkVersion(site, options.version),
|
|
375
517
|
};
|
|
376
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
|
+
}
|
|
377
549
|
/**
|
|
378
550
|
* 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
|
|
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.
|
|
382
555
|
*/
|
|
383
|
-
function declareApplication(options) {
|
|
556
|
+
function declareApplication(name, options) {
|
|
384
557
|
const slot = isPlainObject(options) ? options : undefined;
|
|
385
558
|
const identities = viewIdentities(coreViews);
|
|
386
|
-
const views = buildViews({
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 ?? []);
|
|
390
569
|
const { plugins } = installed;
|
|
391
|
-
const contributors = plugins.map((entry) => buildViews(
|
|
570
|
+
const contributors = plugins.map((entry) => buildViews(pluginViews(entry.identity), entry.views, identities));
|
|
392
571
|
const table = globalTable([], plugins);
|
|
393
572
|
const descriptors = installed.descriptors;
|
|
394
573
|
const extensions = storeCommandLayers({
|
|
395
574
|
descriptors,
|
|
396
575
|
layers: [slot?.extensions],
|
|
576
|
+
site: optionSite(name, 'extensions', slot?.extensions),
|
|
397
577
|
subject: layerOf(null),
|
|
398
578
|
});
|
|
399
579
|
// The Application checks its own facts, so the root carries none.
|
|
@@ -406,14 +586,24 @@ function declareApplication(options) {
|
|
|
406
586
|
});
|
|
407
587
|
const owners = new Map();
|
|
408
588
|
for (const command of plugins.flatMap((entry) => entry.commands)) {
|
|
409
|
-
root = attachToRoot(root, command
|
|
589
|
+
root = attachToRoot(root, command, {
|
|
410
590
|
descriptors: new Map(root.descriptors),
|
|
411
591
|
owners,
|
|
412
592
|
table,
|
|
413
593
|
});
|
|
414
594
|
}
|
|
415
595
|
return {
|
|
416
|
-
config: {
|
|
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
|
+
},
|
|
417
607
|
globals: emptyGlobals(),
|
|
418
608
|
root,
|
|
419
609
|
};
|
|
@@ -421,7 +611,11 @@ function declareApplication(options) {
|
|
|
421
611
|
/** The application name is typed as a command at the prompt, so it answers to the portable rule. */
|
|
422
612
|
function checkApplicationName(name) {
|
|
423
613
|
if (!isPortableName(name)) {
|
|
424
|
-
throw new DeclarationError(
|
|
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
|
+
});
|
|
425
619
|
}
|
|
426
620
|
return name;
|
|
427
621
|
}
|
|
@@ -429,7 +623,8 @@ function checkApplicationName(name) {
|
|
|
429
623
|
class ApplicationDeclaration extends ApplicationBuilder {
|
|
430
624
|
constructor(name, options) {
|
|
431
625
|
// The arguments evaluate in order, so the name is checked before any option is read.
|
|
432
|
-
|
|
626
|
+
const checked = checkApplicationName(name);
|
|
627
|
+
super(checked, declareApplication(checked, options));
|
|
433
628
|
}
|
|
434
629
|
}
|
|
435
630
|
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 {};
|