@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
package/dist/plugin.js CHANGED
@@ -1,15 +1,25 @@
1
1
  import { checkEnvBinding, claimVariables } from './bindings.js';
2
- import { attach, commandNode } from './command.js';
3
- import { DeclarationError, InternalError, reasonOf } from './errors.js';
2
+ import { captureDeclaration, elidedRead, unreadableArgument, unreadableFault } from './capture.js';
3
+ import { notACommand } from './command-rules.js';
4
+ import { attach, commandCode, commandNode } from './command.js';
5
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
6
+ import { DeclarationError, InternalError, quoted, reasonOf } from './errors.js';
4
7
  import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
5
- import { checkDeprecated, checkDescription, checkHidden, isPlainObject } from './facts.js';
6
- import { boundOptions } from './globals.js';
8
+ import { checkDeprecated, checkDescription, checkHidden, factFault, partFinding, pluginOptionSite, slotSite, } from './facts.js';
9
+ import { boundOptions, pluginSites } from './globals.js';
10
+ import { checkIdentity } from './identity.js';
7
11
  import { coreViews } from './lanes.js';
8
12
  import { booleanValue, compileOptions } from './options.js';
13
+ import { copyOwnKeys, decidePlain, declaring, isPlainObject, shallowList, shallowRecord, } from './plain.js';
14
+ import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice, pluginOptionRule, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, unknownSignal, } from './plugin-rules.js';
15
+ import { pluginLoaderFailed } from './rules.js';
9
16
  import { isProcessSignal } from './signals.js';
10
17
  import { buildTheme } from './theme.js';
11
- import { captureConfig, checkDeclarations } from './validation.js';
18
+ import { readTranslations } from './translators.js';
19
+ import { captureConfig, checkDeclarations, defaultDepthFault } from './validation.js';
12
20
  import { buildViews, viewIdentities } from './view.js';
21
+ /** The slots of a definition that hold a list, each copied with its entries as they were built. */
22
+ const definitionLists = ['extensions', 'signals', 'commands', 'views', 'translators'];
13
23
  /** Authored values register here, so the public type publishes no state to reach or replace. */
14
24
  const nodes = new WeakMap();
15
25
  /**
@@ -25,110 +35,219 @@ class PluginDeclaration {
25
35
  }
26
36
  /**
27
37
  * One plugin: an identity and the contributions it carries. Creating and installing the value runs
28
- * none of its code: a hook runs at graph build, and the middleware runs inside an invocation, so an
29
- * installed plugin an invocation never reaches costs that invocation its hooks alone. Every rule
30
- * that one definition carries on its own throws here, before the value exists.
38
+ * none of its code: `onCommandAttach` runs at graph build, `onFailure` runs when `run()` renders a
39
+ * failure, and the middleware runs inside an invocation, so an installed plugin an invocation never
40
+ * reaches costs that invocation its hooks alone. The definition is read once, after the identity,
41
+ * and every rule that one definition carries on its own throws here, before the value exists.
31
42
  */
32
43
  function plugin(identity, definition) {
33
- const captured = {
34
- ...definition,
35
- ...(definition?.theme === undefined
36
- ? {}
37
- : { theme: isPlainObject(definition.theme) ? { ...definition.theme } : definition.theme }),
38
- };
39
- return new PluginDeclaration(readPlugin(identity, isPlainObject(definition) ? captured : definition));
44
+ return new PluginDeclaration(declaring(() => readPlugin(identity, definition)));
45
+ }
46
+ /**
47
+ * The one copy of a definition that `plugin()` reads: the definition, its theme mapping, options
48
+ * record, middleware with its activation list, and source, and each list it holds. An entry of a
49
+ * list is what its factory built and is not copied, and each option's config is captured where its
50
+ * own rules read it, so a fault about it names the option. A read that throws is the unreadable
51
+ * fault of the definition, and a definition that is not a plain object is the not-an-object fault.
52
+ */
53
+ function captureDefinition(identity, definition) {
54
+ return captureDeclaration(definition, (copy, read) => {
55
+ read.nested(copy, 'theme', shallowRecord);
56
+ read.nested(copy, 'options', shallowRecord);
57
+ read.nested(copy, 'middleware', copyMiddleware);
58
+ read.nested(copy, 'source', shallowRecord);
59
+ for (const list of definitionLists) {
60
+ read.nested(copy, list, shallowList);
61
+ }
62
+ }, {
63
+ notAnObject: () => new DeclarationError(notAnObject, {
64
+ correction: 'Supply { options, middleware, extensions, views }.',
65
+ findings: [{ arguments: [identity, definition], call: 'plugin', mark: '1' }],
66
+ sentence: `${pluginSentence(identity)} declares a definition that is not an object.`,
67
+ }),
68
+ unreadable: unreadableArgument({ call: 'plugin', named: identity, subject: pluginSentence(identity) }, 'definition'),
69
+ });
70
+ }
71
+ /** A middleware object's copy, its activation list copied too, or any other value as declared. */
72
+ function copyMiddleware(middleware) {
73
+ if (!decidePlain(middleware)) {
74
+ return middleware;
75
+ }
76
+ const copy = copyOwnKeys(middleware);
77
+ if (Object.hasOwn(copy, 'activate')) {
78
+ copy.activate = shallowList(copy.activate);
79
+ }
80
+ return copy;
40
81
  }
41
82
  /** How every plugin diagnostic names one plugin at the start of a sentence. */
42
83
  function pluginSentence(identity) {
43
- return `Plugin "${identity}"`;
84
+ return `Plugin ${quoted(identity)}`;
85
+ }
86
+ /** Where one slot of a plugin's definition sits, rebuilt as `plugin(identity, { slot })`. */
87
+ function pluginSlot(identity, slot, value) {
88
+ return slotSite({ call: 'plugin', named: identity, subject: pluginSentence(identity) }, slot, value);
89
+ }
90
+ /** The subject a plugin's `views` list reports under, and the call that declared it. */
91
+ function pluginViews(identity) {
92
+ return {
93
+ declares: true,
94
+ owner: { call: 'plugin', named: identity },
95
+ sentence: pluginSentence(identity),
96
+ };
44
97
  }
45
- /** Reads the declarations behind an installed value; anything else is a declaration error. */
98
+ /** The declarations behind one installed value, or `undefined` for a value `plugin()` did not make. */
46
99
  function nodeOf(value) {
47
- const node = typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
48
- if (!node) {
49
- throw new DeclarationError('The Application holds a value that is not a plugin. Supply the value returned by plugin(identity, definition).');
50
- }
51
- return node;
100
+ return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
52
101
  }
53
- /** The identity one plugin declares, which is a nonempty string. */
54
- function readIdentity(identity) {
55
- if (typeof identity !== 'string') {
56
- throw new DeclarationError('A plugin declares an identity that is not a string. Supply a nonempty string, such as the package name.');
57
- }
58
- if (identity === '') {
59
- throw new DeclarationError('A plugin declares an empty identity. Supply a nonempty string, such as the package name.');
60
- }
61
- return identity;
102
+ /** One `plugins` entry as a finding prints it: a plugin as its `plugin()` call, elided. */
103
+ function entryCode(value) {
104
+ const node = nodeOf(value);
105
+ return node ? spelled(`plugin(${quoteString(node.identity)}, ${elided})`) : value;
62
106
  }
63
- /** The declarations one plugin value carries, which a JavaScript author reaches as any value. */
64
- function definitionOf(identity, definition) {
65
- if (!isPlainObject(definition)) {
66
- throw new DeclarationError(`${pluginSentence(identity)} declares a definition that is not an object. Supply { options, middleware, extensions, views }.`);
67
- }
68
- return definition;
107
+ /** How a second claim on one slot reads: its clause, the note on the owner's entry, and the fix. */
108
+ const slotWords = {
109
+ signals: {
110
+ clause: (owner) => `claims the signals slot, which plugin ${quoted(owner)} already holds.`,
111
+ correction: 'Install one owner.',
112
+ held: 'holds the signals slot',
113
+ },
114
+ source: {
115
+ clause: (owner) => `declares a configuration source, which plugin ${quoted(owner)} already declares.`,
116
+ correction: 'Install one source.',
117
+ held: 'declares the configuration source',
118
+ },
119
+ theme: {
120
+ clause: (owner) => `claims the theme slot, which plugin ${quoted(owner)} already holds.`,
121
+ correction: 'Install one owner.',
122
+ held: 'holds the theme slot',
123
+ },
124
+ };
125
+ /** A second claim on one slot, which marks the owner's entry and the claimant's in `plugins`. */
126
+ function slotFault(site, slot, claim) {
127
+ const { claimant, installed, owner } = claim;
128
+ const words = slotWords[slot];
129
+ return new DeclarationError(slotTaken, {
130
+ correction: words.correction,
131
+ findings: [
132
+ partFinding(site, [owner], words.held),
133
+ partFinding(site, [claimant], 'claims it again'),
134
+ ],
135
+ sentence: `${pluginSentence(installed[claimant]?.identity ?? '')} ${words.clause(installed[owner]?.identity ?? '')}`,
136
+ });
69
137
  }
70
138
  /**
71
139
  * The installed list in composition order, with every rule that reads two plugins together: an
72
140
  * identity installed twice, a second claim on the theme slot, the signals slot, or the
73
- * configuration source, and two distinct descriptors under one identity. The slot is read
74
- * defensively, because a JavaScript author reaches it with any value. Each plugin's own rules
75
- * already ran at its `plugin()` call.
141
+ * configuration source, and two distinct descriptors under one identity. The slot is the copy the
142
+ * Application's constructor took, which a JavaScript author fills with any value. Each plugin's own
143
+ * rules already ran at its `plugin()` call.
76
144
  */
77
- function installPlugins(plugins) {
145
+ function installPlugins(application, plugins) {
146
+ const printed = Array.isArray(plugins) ? Array.from(plugins, entryCode) : plugins;
147
+ const site = slotSite({ call: 'new Application', named: application, subject: 'The Application' }, 'plugins', printed);
78
148
  if (!Array.isArray(plugins)) {
79
- throw new DeclarationError('The Application plugins must be an array. Supply a list of plugin values.');
149
+ throw new DeclarationError(notAList, {
150
+ correction: 'Supply a list of plugin values.',
151
+ findings: [partFinding(site, [])],
152
+ sentence: 'The Application declares plugins that are not an array.',
153
+ });
80
154
  }
81
155
  const list = plugins;
82
- const installed = list.map((value) => nodeOf(value));
83
- const identities = new Set();
156
+ const installed = list.map((value, index) => {
157
+ const node = nodeOf(value);
158
+ if (!node) {
159
+ throw new DeclarationError(foreignValue, {
160
+ correction: 'Supply the value returned by plugin(identity, definition).',
161
+ findings: [partFinding(site, [index])],
162
+ sentence: 'The Application holds a value that is not a plugin.',
163
+ });
164
+ }
165
+ return node;
166
+ });
167
+ const positions = new Map();
84
168
  const descriptors = new Map();
85
169
  // Each slot has one owner, so the first plugin to claim it names the second claimant's diagnostic.
86
- const owners = {};
87
- for (const entry of installed) {
170
+ const owners = new Map();
171
+ const claim = (slot, index) => {
172
+ const owner = owners.get(slot);
173
+ if (owner !== undefined) {
174
+ throw slotFault(site, slot, { claimant: index, installed, owner });
175
+ }
176
+ owners.set(slot, index);
177
+ };
178
+ for (const [index, entry] of installed.entries()) {
88
179
  const { identity } = entry;
89
- if (identities.has(identity)) {
90
- throw new DeclarationError(`The Application installs plugin "${identity}" twice. Install each plugin once.`);
180
+ const first = positions.get(identity);
181
+ if (first !== undefined) {
182
+ throw new DeclarationError(pluginInstalledTwice, {
183
+ correction: 'Install each plugin once.',
184
+ findings: [
185
+ partFinding(site, [first], 'the first installation'),
186
+ partFinding(site, [index], 'the second installation'),
187
+ ],
188
+ sentence: `The Application installs plugin ${quoted(identity)} twice.`,
189
+ });
91
190
  }
92
- identities.add(identity);
191
+ positions.set(identity, index);
93
192
  if (entry.theme !== undefined) {
94
- if (owners.theme !== undefined) {
95
- throw new DeclarationError(`${pluginSentence(identity)} claims the theme slot, which plugin "${owners.theme}" already holds. Install one owner.`);
96
- }
97
- owners.theme = identity;
193
+ claim('theme', index);
98
194
  }
99
- for (const descriptor of entry.descriptors.values()) {
100
- registerDescriptor(descriptors, descriptor);
195
+ for (const [key, descriptor] of entry.descriptors) {
196
+ registerDescriptor(descriptors, descriptor, {
197
+ identity: key,
198
+ place: partFinding(site, [index]),
199
+ });
101
200
  }
102
201
  // An empty claim leaves the signals slot free.
103
202
  if (entry.signals.length > 0) {
104
- if (owners.signals !== undefined) {
105
- throw new DeclarationError(`${pluginSentence(identity)} claims the signals slot, which plugin "${owners.signals}" already holds. Install one owner.`);
106
- }
107
- owners.signals = identity;
203
+ claim('signals', index);
108
204
  }
109
205
  if (entry.source) {
110
- if (owners.source !== undefined) {
111
- throw new DeclarationError(`${pluginSentence(identity)} declares a configuration source, which plugin "${owners.source}" already declares. Install one source.`);
112
- }
113
- owners.source = identity;
206
+ claim('source', index);
114
207
  }
115
208
  }
116
209
  return { descriptors, plugins: installed };
117
210
  }
118
211
  /** The keys a plugin option may not declare, in the order its diagnostic names them. */
119
212
  const forbidden = ['validate', 'validateOmitted', 'required'];
120
- /** The rules a plugin option answers before every rule an ordinary declaration carries. */
121
- function checkPluginOption(sentence, config) {
122
- if (!isPlainObject(config)) {
123
- throw new DeclarationError(`${sentence} is not an option declaration. Supply { type, ... }.`);
124
- }
213
+ /**
214
+ * The rules a plugin option answers before every rule an ordinary declaration carries, read from
215
+ * the one copy of its config that `captureConfig` takes, which it answers with for every later read.
216
+ * `siteOf` places the option with the value its entry prints as: the config as declared for a value
217
+ * that is not one, elided for a config whose read threw, and the copy for every later rule.
218
+ */
219
+ function checkPluginOption(siteOf, declared) {
220
+ const sentence = siteOf(declared).subject;
221
+ const config = captureConfig(declared, {
222
+ notAnObject: () => new DeclarationError(notAnObject, {
223
+ correction: 'Supply { type, ... }.',
224
+ findings: [partFinding(siteOf(declared), [])],
225
+ sentence: `${sentence} is not an option declaration.`,
226
+ }),
227
+ // The default's walk stopped at the limit, so the finding prints it elided.
228
+ tooDeep: () => {
229
+ const { keys, shown } = elidedRead('default');
230
+ return defaultDepthFault(sentence, [partFinding(siteOf(shown), keys)]);
231
+ },
232
+ // A part whose read threw is never read again, so the finding prints it elided.
233
+ unreadable: (thrown, slot) => {
234
+ const { keys, shown } = elidedRead(slot);
235
+ return unreadableFault({ declared: 'config', findings: [partFinding(siteOf(shown), keys)], subject: sentence }, thrown);
236
+ },
237
+ });
238
+ const site = siteOf(config);
125
239
  const rejected = forbidden.find((key) => key in config);
126
240
  if (rejected !== undefined) {
127
- throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; a plugin option carries no validator or presence rule, and the middleware interprets the value.`);
128
- }
129
- checkDescription(sentence, config.description);
130
- checkHidden(sentence, config.hidden);
131
- checkDeprecated(sentence, config.deprecated);
241
+ throw factFault(pluginOptionRule, site, {
242
+ correction: 'Remove it; a plugin option carries no validator or presence rule, and the middleware interprets the value.',
243
+ fact: rejected,
244
+ sentence: `${sentence} declares ${rejected}.`,
245
+ });
246
+ }
247
+ checkDescription(site, config.description);
248
+ checkHidden(site, config.hidden);
249
+ checkDeprecated(site, config.deprecated);
250
+ return config;
132
251
  }
133
252
  /**
134
253
  * One plugin's option declarations, in declaration order. They join the globals table, so the rules
@@ -136,19 +255,36 @@ function checkPluginOption(sentence, config) {
136
255
  */
137
256
  function readOptions(identity, declared, build) {
138
257
  if (declared !== undefined && !isPlainObject(declared)) {
139
- throw new DeclarationError(`${pluginSentence(identity)} declares options that are not an object. Supply a record of option declarations.`);
258
+ throw new DeclarationError(notAnObject, {
259
+ correction: 'Supply a record of option declarations.',
260
+ findings: [partFinding(pluginSlot(identity, 'options', declared), [])],
261
+ sentence: `${pluginSentence(identity)} declares options that are not an object.`,
262
+ });
140
263
  }
141
264
  const inputs = [];
142
- for (const [name, config] of Object.entries(declared ?? {})) {
143
- const sentence = `${pluginSentence(identity)} option "${name}"`;
144
- checkPluginOption(sentence, config);
145
- checkEnvBinding(sentence, config);
146
- const input = { config: captureConfig(config), kind: 'option', name };
265
+ // A finding prints each option's captured config once it is taken, and each later one elided.
266
+ const shown = Object.fromEntries(Object.keys(declared ?? {}).map((name) => [name, spelled(elided)]));
267
+ for (const [name, entry] of Object.entries(declared ?? {})) {
268
+ const sentence = `${pluginSentence(identity)} option ${quoted(name)}`;
269
+ // A computed key defines its own property even for `__proto__`, where assignment would not.
270
+ const siteOf = (value) => pluginOptionSite({ identity, options: { ...shown, [name]: value } }, name, sentence);
271
+ const config = checkPluginOption(siteOf, entry);
272
+ // Defining the key keeps its place, and holds even for a key of `__proto__`.
273
+ Object.defineProperty(shown, name, {
274
+ configurable: true,
275
+ enumerable: true,
276
+ value: config,
277
+ writable: true,
278
+ });
279
+ const site = siteOf(config);
280
+ checkEnvBinding(site, config);
281
+ const input = { config, kind: 'option', name };
147
282
  // The shared rules name the plugin and the option, so a fault reads with its contributor.
148
- checkDeclarations([input], sentence);
283
+ checkDeclarations([{ input, site }], sentence);
149
284
  build.records.set(input, buildExtensions({
150
285
  declared: config.extensions,
151
286
  descriptors: build.descriptors,
287
+ site: { ...site, at: `${site.at}.extensions` },
152
288
  subject: {
153
289
  phrase: `on ${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
154
290
  sentence,
@@ -158,30 +294,49 @@ function readOptions(identity, declared, build) {
158
294
  inputs.push(input);
159
295
  }
160
296
  // Two options of one plugin meet in the one table the pre-scan reads, so they share its rules.
161
- compileOptions(inputs, `plugin "${identity}"`);
162
- claimVariables(boundOptions(inputs, (name) => `plugin "${identity}" option "${name}"`));
297
+ const siteOf = pluginSites(identity, inputs);
298
+ compileOptions(inputs, { siteOf, subject: `plugin ${quoted(identity)}` });
299
+ claimVariables(boundOptions(inputs, (input) => ({
300
+ phrase: `plugin ${quoted(identity)} option ${quoted(input.name)}`,
301
+ site: siteOf(input),
302
+ })));
163
303
  return inputs;
164
304
  }
165
- /** One activation name, which must be one of the plugin's own declared options. */
166
- function readActivationName(identity, name, names) {
167
- if (typeof name !== 'string' || !names.has(name)) {
168
- throw new DeclarationError(`${pluginSentence(identity)} activates middleware on option "${String(name)}", which it does not declare. Name one of the plugin's own options.`);
169
- }
170
- return name;
171
- }
172
- /** The activation a middleware declares, checked against the options its own plugin declares. */
173
- function readActivation(identity, declared, names) {
174
- if (declared === 'always') {
305
+ /**
306
+ * The activation a middleware declares, checked against the options its own plugin declares. `site`
307
+ * holds the middleware object, and a fault marks its `activate` key, or the object itself when the
308
+ * key is absent.
309
+ */
310
+ function readActivation(site, declared, names) {
311
+ const { activate } = declared;
312
+ if (activate === 'always') {
175
313
  return 'always';
176
314
  }
177
- if (!Array.isArray(declared)) {
178
- throw new DeclarationError(`${pluginSentence(identity)} declares middleware with no activation. Supply activate: 'always' or a list of the plugin's own option names.`);
315
+ if (!Array.isArray(activate)) {
316
+ throw new DeclarationError(middlewareActivation, {
317
+ correction: "Supply activate: 'always' or a list of the plugin's own option names.",
318
+ findings: [partFinding(site, 'activate' in declared ? ['activate'] : [])],
319
+ sentence: `${site.subject} declares middleware with no activation.`,
320
+ });
179
321
  }
180
- const list = declared;
322
+ const list = activate;
181
323
  if (list.length === 0) {
182
- throw new DeclarationError(`${pluginSentence(identity)} declares middleware with an empty activation list. Name at least one of the plugin's options or use 'always'.`);
183
- }
184
- return list.map((name) => readActivationName(identity, name, names));
324
+ throw new DeclarationError(middlewareActivation, {
325
+ correction: "Name at least one of the plugin's options or use 'always'.",
326
+ findings: [partFinding(site, ['activate'])],
327
+ sentence: `${site.subject} declares middleware with an empty activation list.`,
328
+ });
329
+ }
330
+ return list.map((name, index) => {
331
+ if (typeof name !== 'string' || !names.has(name)) {
332
+ throw new DeclarationError(middlewareActivation, {
333
+ correction: "Name one of the plugin's own options.",
334
+ findings: [partFinding(site, ['activate', index])],
335
+ sentence: `${site.subject} activates middleware on option ${quoted(name)}, which it does not declare.`,
336
+ });
337
+ }
338
+ return name;
339
+ });
185
340
  }
186
341
  /**
187
342
  * The loader a middleware declares. Core calls it with no arguments and reads whatever it resolves
@@ -201,28 +356,47 @@ async function loadDefault(identity, load, owed) {
201
356
  module = await load();
202
357
  }
203
358
  catch (error) {
204
- throw new InternalError(`Loading plugin "${identity}" failed: ${reasonOf(error)}`, error);
359
+ throw new InternalError(pluginLoaderFailed, {
360
+ cause: error,
361
+ correction: loaderCorrection,
362
+ sentence: `Loading plugin ${quoted(identity)} failed: ${reasonOf(error)}`,
363
+ });
205
364
  }
206
365
  const exported = module !== null && typeof module === 'object' && 'default' in module
207
366
  ? module.default
208
367
  : undefined;
209
368
  if (!owed.guard(exported)) {
210
- throw new InternalError(`Loading plugin "${identity}" failed: the module exports no default ${owed.noun} function.`, undefined);
369
+ throw new InternalError(pluginLoaderFailed, {
370
+ cause: undefined,
371
+ correction: loaderCorrection,
372
+ sentence: `Loading plugin ${quoted(identity)} failed: the module exports no default ${owed.noun} function.`,
373
+ });
211
374
  }
212
375
  return exported;
213
376
  }
377
+ /** The fix every plugin loader fault shares. */
378
+ const loaderCorrection = "Make load() resolve to a module whose default export is the plugin's function, such as () => import('./middleware.js').";
214
379
  /** One plugin's middleware, or `undefined` for a plugin that declares none. */
215
380
  function readMiddleware(identity, declared, names) {
216
381
  if (declared === undefined) {
217
382
  return undefined;
218
383
  }
384
+ const site = pluginSlot(identity, 'middleware', declared);
219
385
  if (!isPlainObject(declared)) {
220
- throw new DeclarationError(`${pluginSentence(identity)} declares middleware that is not an object. Supply { activate, load }.`);
386
+ throw new DeclarationError(notAnObject, {
387
+ correction: 'Supply { activate, load }.',
388
+ findings: [partFinding(site, [])],
389
+ sentence: `${pluginSentence(identity)} declares middleware that is not an object.`,
390
+ });
221
391
  }
222
- const activate = readActivation(identity, declared.activate, names);
392
+ const activate = readActivation(site, declared, names);
223
393
  const { load } = declared;
224
394
  if (!isLoader(load)) {
225
- throw new DeclarationError(`${pluginSentence(identity)} declares middleware with no load function. Supply load: () => import('./middleware.js').`);
395
+ throw new DeclarationError(notAFunction, {
396
+ correction: "Supply load: () => import('./middleware.js').",
397
+ findings: [partFinding(site, 'load' in declared ? ['load'] : [])],
398
+ sentence: `${pluginSentence(identity)} declares middleware with no load function.`,
399
+ });
226
400
  }
227
401
  return { activate, load };
228
402
  }
@@ -236,17 +410,42 @@ function readCommands(identity, declared) {
236
410
  return [];
237
411
  }
238
412
  if (!Array.isArray(declared)) {
239
- throw new DeclarationError(`${pluginSentence(identity)} declares commands that are not an array. Supply a list of Command values.`);
413
+ throw new DeclarationError(notAList, {
414
+ correction: 'Supply a list of Command values.',
415
+ findings: [partFinding(pluginSlot(identity, 'commands', declared), [])],
416
+ sentence: `${pluginSentence(identity)} declares commands that are not an array.`,
417
+ });
240
418
  }
241
419
  const list = declared;
242
420
  const commands = [];
421
+ // Each entry prints as the Command it is, so a finding rebuilds the list the plugin declared.
422
+ const entries = Array.from(list, (value) => {
423
+ const node = commandNode(value);
424
+ return node ? commandCode(node.name) : value;
425
+ });
243
426
  // A for...of walk reads a hole as undefined, which the entry rule rejects, where map would skip it.
244
- for (const value of list) {
427
+ for (const [index, value] of list.entries()) {
245
428
  const node = commandNode(value);
429
+ const placement = {
430
+ arguments: [identity, { commands: entries }],
431
+ call: 'plugin',
432
+ mark: `1.commands.${String(index)}`,
433
+ };
246
434
  if (!node) {
247
- throw new DeclarationError(`${pluginSentence(identity)} holds a value that is not a Command. Supply the value returned by new Command(name).`);
435
+ throw new DeclarationError(notACommand, {
436
+ correction: 'Supply the value returned by new Command(name).',
437
+ findings: [placement],
438
+ sentence: `${pluginSentence(identity)} holds a value that is not a Command.`,
439
+ });
248
440
  }
249
- commands.push(attach({ argument: undefined, children: commands, hasAction: false, name: null }, node));
441
+ const parent = {
442
+ argument: undefined,
443
+ children: commands,
444
+ hasAction: false,
445
+ name: null,
446
+ path: [],
447
+ };
448
+ commands.push(attach(parent, node, placement));
250
449
  }
251
450
  return commands;
252
451
  }
@@ -260,21 +459,39 @@ function readSignals(identity, declared) {
260
459
  if (declared === undefined) {
261
460
  return [];
262
461
  }
462
+ const site = pluginSlot(identity, 'signals', declared);
263
463
  if (!Array.isArray(declared)) {
264
- throw new DeclarationError(`${pluginSentence(identity)} declares signals that are not an array. Supply a list of signal names.`);
464
+ throw new DeclarationError(notAList, {
465
+ correction: 'Supply a list of signal names.',
466
+ findings: [partFinding(site, [])],
467
+ sentence: `${pluginSentence(identity)} declares signals that are not an array.`,
468
+ });
265
469
  }
266
470
  const list = declared;
267
- const claimed = new Set();
268
- for (const value of list) {
471
+ // Each claimed signal's position, so a repeat marks both claims.
472
+ const claimed = new Map();
473
+ for (const [index, value] of list.entries()) {
269
474
  if (!isProcessSignal(value)) {
270
- throw new DeclarationError(`${pluginSentence(identity)} claims signal "${String(value)}". Claim SIGINT or SIGTERM.`);
475
+ throw new DeclarationError(unknownSignal, {
476
+ correction: 'Claim SIGINT or SIGTERM.',
477
+ findings: [partFinding(site, [index])],
478
+ sentence: `${pluginSentence(identity)} claims signal ${quoted(value)}.`,
479
+ });
271
480
  }
272
- if (claimed.has(value)) {
273
- throw new DeclarationError(`${pluginSentence(identity)} claims signal "${value}" twice. Claim each signal once.`);
481
+ const first = claimed.get(value);
482
+ if (first !== undefined) {
483
+ throw new DeclarationError(signalClaimedTwice, {
484
+ correction: 'Claim each signal once.',
485
+ findings: [
486
+ partFinding(site, [first], 'the first claim'),
487
+ partFinding(site, [index], 'the second claim'),
488
+ ],
489
+ sentence: `${pluginSentence(identity)} claims signal ${quoted(value)} twice.`,
490
+ });
274
491
  }
275
- claimed.add(value);
492
+ claimed.set(value, index);
276
493
  }
277
- return [...claimed];
494
+ return [...claimed.keys()];
278
495
  }
279
496
  /**
280
497
  * A lifecycle hook is a function core calls at one named point, so being callable is the whole
@@ -289,7 +506,29 @@ function readHook(identity, declared) {
289
506
  return undefined;
290
507
  }
291
508
  if (!isHook(declared)) {
292
- throw new DeclarationError(`${pluginSentence(identity)} declares onCommandAttach that is not a function. Supply a function of the Command.`);
509
+ throw new DeclarationError(notAFunction, {
510
+ correction: 'Supply a function of the Command.',
511
+ findings: [partFinding(pluginSlot(identity, 'onCommandAttach', declared), [])],
512
+ sentence: `${pluginSentence(identity)} declares onCommandAttach that is not a function.`,
513
+ });
514
+ }
515
+ return declared;
516
+ }
517
+ /** Being callable is the whole claim, as it is for `onCommandAttach`; `run()` checks what it returns. */
518
+ function isFailureHook(value) {
519
+ return typeof value === 'function';
520
+ }
521
+ /** One plugin's `onFailure` hook, or `undefined` for a plugin that declares none. */
522
+ function readFailureHook(identity, declared) {
523
+ if (declared === undefined) {
524
+ return undefined;
525
+ }
526
+ if (!isFailureHook(declared)) {
527
+ throw new DeclarationError(notAFunction, {
528
+ correction: 'Supply a function of the failure and its context.',
529
+ findings: [partFinding(pluginSlot(identity, 'onFailure', declared), [])],
530
+ sentence: `${pluginSentence(identity)} declares onFailure that is not a function.`,
531
+ });
293
532
  }
294
533
  return declared;
295
534
  }
@@ -305,48 +544,90 @@ function readSource(identity, declaration, own) {
305
544
  return undefined;
306
545
  }
307
546
  const sentence = pluginSentence(identity);
547
+ const site = pluginSlot(identity, 'source', declared);
308
548
  if (!isPlainObject(declared)) {
309
- throw new DeclarationError(`${sentence} declares a source that is not an object. Supply { binding, load }.`);
549
+ throw new DeclarationError(notAnObject, {
550
+ correction: 'Supply { binding, load }.',
551
+ findings: [partFinding(site, [])],
552
+ sentence: `${sentence} declares a source that is not an object.`,
553
+ });
310
554
  }
311
555
  const { binding, load } = declared;
312
- const listed = (declaration.extensions ?? []).some((descriptor) => descriptor === binding);
313
- if (!listed || !isDescriptor(binding)) {
314
- throw new DeclarationError(`${sentence} declares a source binding that is not one of its extensions. Supply a descriptor the plugin lists under extensions.`);
315
- }
316
- if (binding.target !== 'option') {
317
- throw new DeclarationError(`${sentence} declares source binding "${binding.identity}", which applies to ${appliesTo(binding.target)}. Supply an extension that applies to options.`);
556
+ // A fault about one key marks the key, or the source itself when the key is absent.
557
+ const keyFinding = (key) => partFinding(site, key in declared ? [key] : []);
558
+ // The plugin's own extensions were admitted first, so a listed binding is in the registry.
559
+ // Its identity is read from there, where admission read it once, and never from the binding again.
560
+ const bound = [...own.build.descriptors].find(([, descriptor]) => descriptor === binding);
561
+ if (bound === undefined) {
562
+ throw new DeclarationError(sourceBinding, {
563
+ correction: 'Supply a descriptor the plugin lists under extensions.',
564
+ findings: [keyFinding('binding')],
565
+ sentence: `${sentence} declares a source binding that is not one of its extensions.`,
566
+ });
567
+ }
568
+ const [bindingIdentity, { target }] = bound;
569
+ if (target !== 'option') {
570
+ throw new DeclarationError(sourceBinding, {
571
+ correction: 'Supply an extension that applies to options.',
572
+ findings: [keyFinding('binding')],
573
+ sentence: `${sentence} declares source binding ${quoted(bindingIdentity)}, which applies to ${appliesTo(target)}.`,
574
+ });
318
575
  }
319
576
  if (!isLoader(load)) {
320
- throw new DeclarationError(`${sentence} declares a source with no load function. Supply load: () => import('./source.js').`);
577
+ throw new DeclarationError(notAFunction, {
578
+ correction: "Supply load: () => import('./source.js').",
579
+ findings: [keyFinding('load')],
580
+ sentence: `${sentence} declares a source with no load function.`,
581
+ });
321
582
  }
322
- const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, binding.identity));
583
+ const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, bindingIdentity));
323
584
  if (carrier) {
324
- throw new DeclarationError(`${sentence} option "${carrier.name}" carries its own source binding. Remove the value; the source's own options resolve before it loads.`);
325
- }
326
- return { binding: binding.identity, load };
585
+ // The record prints each option's captured config, which is what its rules read.
586
+ const option = pluginSites(identity, own.inputs)(carrier);
587
+ throw new DeclarationError(sourceBoundOwnOption, {
588
+ correction: "Remove the value; the source's own options resolve before it loads.",
589
+ findings: [partFinding(option, ['extensions'])],
590
+ sentence: `${option.subject} carries its own source binding.`,
591
+ });
592
+ }
593
+ return { binding: bindingIdentity, load };
327
594
  }
328
595
  /** A plugin's own list names the extensions it defines, before any declaration carries one. */
329
596
  function defineExtensions(identity, declaration, build) {
330
597
  const { extensions } = declaration;
598
+ const site = pluginSlot(identity, 'extensions', extensions);
331
599
  if (extensions !== undefined && !Array.isArray(extensions)) {
332
- throw new DeclarationError(`${pluginSentence(identity)} declares extensions that are not an array. Supply a list of extension descriptors.`);
333
- }
334
- for (const descriptor of extensions ?? []) {
600
+ throw new DeclarationError(notAList, {
601
+ correction: 'Supply a list of extension descriptors.',
602
+ findings: [partFinding(site, [])],
603
+ sentence: `${pluginSentence(identity)} declares extensions that are not an array.`,
604
+ });
605
+ }
606
+ const list = Array.isArray(extensions) ? extensions : [];
607
+ for (const [index, descriptor] of list.entries()) {
335
608
  if (!isDescriptor(descriptor)) {
336
- throw new DeclarationError(`${pluginSentence(identity)} holds a value that is not an extension. Supply the value returned by extension(identity, config).`);
609
+ throw new DeclarationError(foreignValue, {
610
+ correction: 'Supply the value returned by extension(identity, config).',
611
+ findings: [partFinding(site, [index])],
612
+ sentence: `${pluginSentence(identity)} holds a value that is not an extension.`,
613
+ });
337
614
  }
338
- registerDescriptor(build.descriptors, descriptor);
615
+ registerDescriptor(build.descriptors, descriptor, {
616
+ holder: pluginSentence(identity),
617
+ place: partFinding(site, [index]),
618
+ });
339
619
  }
340
620
  }
341
621
  /**
342
- * Every rule one definition carries on its own, in the order the definition's slots are read. The
343
- * plugin's own extensions register before any declaration carries a value, so a duplicated package
344
- * copy is reported from the list that defines it. The views list is read against core's view
345
- * identities, and the Application reads it again against every other contributor's.
622
+ * Every rule one definition carries on its own, in the order the definition's slots are read, each
623
+ * from the one copy `captureDefinition` takes once the identity is judged. The plugin's own
624
+ * extensions register before any declaration carries a value, so a duplicated package copy is
625
+ * reported from the list that defines it. The views list is read against core's view identities,
626
+ * and the Application reads the same copy again against every other contributor's.
346
627
  */
347
- function readPlugin(identity, definition) {
348
- const named = readIdentity(identity);
349
- const declaration = definitionOf(named, definition);
628
+ function readPlugin(named, definition) {
629
+ checkIdentity('plugin', named);
630
+ const declaration = captureDefinition(named, definition);
350
631
  const theme = declaration.theme === undefined ? undefined : buildTheme(declaration.theme, named);
351
632
  const build = { descriptors: new Map(), records: new Map() };
352
633
  defineExtensions(named, declaration, build);
@@ -357,7 +638,9 @@ function readPlugin(identity, definition) {
357
638
  const commands = readCommands(named, declaration.commands);
358
639
  const middleware = readMiddleware(named, declaration.middleware, names);
359
640
  const onCommandAttach = readHook(named, declaration.onCommandAttach);
360
- buildViews({ declares: true, sentence: pluginSentence(named) }, declaration.views, viewIdentities(coreViews));
641
+ const onFailure = readFailureHook(named, declaration.onFailure);
642
+ buildViews(pluginViews(named), declaration.views, viewIdentities(coreViews));
643
+ const translators = readTranslations(pluginSlot(named, 'translators', declaration.translators), declaration.translators);
361
644
  return {
362
645
  commands,
363
646
  descriptors: build.descriptors,
@@ -365,10 +648,12 @@ function readPlugin(identity, definition) {
365
648
  inputs,
366
649
  middleware,
367
650
  onCommandAttach,
651
+ onFailure,
368
652
  records: build.records,
369
653
  signals,
370
654
  source,
371
655
  theme,
656
+ translators,
372
657
  views: declaration.views,
373
658
  };
374
659
  }
@@ -407,10 +692,10 @@ function pluginValues(inputs, values) {
407
692
  /** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
408
693
  function pluginSpellings(inputs, values) {
409
694
  // Entries become own keys even for a name such as `__proto__`, which assignment would not.
410
- const spelled = inputs.flatMap(({ name }) => {
695
+ const supplied = inputs.flatMap(({ name }) => {
411
696
  const spelling = values.spellings.get(name);
412
697
  return spelling === undefined ? [] : [[name, spelling]];
413
698
  });
414
- return Object.freeze(Object.fromEntries(spelled));
699
+ return Object.freeze(Object.fromEntries(supplied));
415
700
  }
416
- export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginValues, };
701
+ export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, pluginValues, };