@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.
Files changed (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
@@ -1,18 +1,28 @@
1
+ import { captureDeclaration, unreadableArgument } from './capture.js';
1
2
  import { runInvocation } from './chain.js';
2
- import { attachToRoot, childNode, buildGraph, checkDeclaredOptions, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
3
- import { DeclarationError, InternalError, reasonOf, toFailure } from './errors.js';
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, isPlainObject } from './facts.js';
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 { installPlugins, ownedSignals, pluginSentence } from './plugin.js';
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 { captureConfig, checkDeclarations, prepareInputs } from './validation.js';
15
- import { buildViews, describeFailure, viewIdentities } from './view.js';
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('run() received a signal that is not an AbortSignal. Supply the signal of an AbortController.', undefined);
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: captureConfig(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: captureConfig(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(`The Application declares global option "${name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
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: captureConfig(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 root = attachToRoot(this.#root, childNode(null, child), scope);
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, built.facts);
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
- policy = { ...declared, ...renderingPolicy(options?.rendering) };
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
- name: this.#name,
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(`Rendering output failed: ${reasonOf(fault.cause)}`, fault.cause);
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.exitCode;
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
- const report = describeFailure(registry ?? noViews, failure, output.context('stderr'));
283
- if (report.kind === 'rendered') {
284
- // The view owns the trailing newline; output resolves its marked text.
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
- faults.push(toFailure(cause));
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
- code = code === 0 ? 1 : code;
430
+ const own = deferred.has(fault) ? exitCodeOf(fault) : 1;
431
+ code = code === 0 ? own : code;
315
432
  try {
316
- const report = describeFailure(registry ?? noViews, fault, output?.context('stderr'));
317
- if (report.kind === 'rendered') {
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
- await reportPlainly(stderr, 'Internal error: Could not write invocation output.\n');
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 changing it. The
344
- * signal decides the code whatever the action did afterward, so this reading comes last.
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
- /** Reject obsolete wiring before silently losing options that invocations depend on. */
356
- function checkOptions(options) {
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
- if ('failures' in options) {
367
- throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
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('The Application', options);
517
+ checkNoListingFacts(site, options);
372
518
  return {
373
- description: checkDescription('The Application', options.description),
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 its facts, the
380
- * installed list and every rule between two plugins, the root's extension values, and then each
381
- * plugin's Commands, which attach to the root first, in installation order and list order.
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(options) {
384
- const slot = isPlainObject(options) ? options : undefined;
588
+ function declareApplication(name, declared) {
589
+ const slot = captureOptions(name, declared);
385
590
  const identities = viewIdentities(coreViews);
386
- const views = buildViews({ declares: false, sentence: 'The Application' }, slot?.views, identities);
387
- const rendering = renderingPolicy(slot?.rendering);
388
- const facts = checkOptions(options);
389
- const installed = installPlugins(slot?.plugins ?? []);
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({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
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.node, {
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: { composed: false, contributors, facts, owners, plugins, rendering, views },
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(`Application name "${String(name)}" is invalid. ${portableNameCorrection}`);
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
- super(checkApplicationName(name), declareApplication(options));
658
+ const checked = checkApplicationName(name);
659
+ super(checked, declaring(() => declareApplication(checked, options)));
433
660
  }
434
661
  }
435
662
  export const Application = ApplicationDeclaration;
@@ -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. `sentence` names the declaration at the start of the diagnostic.
10
+ * a variable name. The site names the declaration and marks its `env`.
10
11
  */
11
- export declare function checkEnvBinding(sentence: string, config: BindingConfig): string | undefined;
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(sentence: string, config: object): void;
14
- /** One option bound to a variable, with the phrase a duplicate-variable diagnostic names it by. */
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 site: string;
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 phrase of the option that binds it. Within one
21
- * invocation's scope a variable binds one option, so a second binder is a declaration error that
22
- * names the first binder, in scope order, and then the second. `held` is what an enclosing scope
23
- * already binds, such as the globals table under a Command's own options.
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, string>): ReadonlyMap<string, string>;
30
+ export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string, BoundOption>): ReadonlyMap<string, BoundOption>;
26
31
  export {};