@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.
Files changed (77) hide show
  1. package/dist/application.d.ts +18 -2
  2. package/dist/application.js +266 -71
  3. package/dist/bindings.d.ts +15 -10
  4. package/dist/bindings.js +34 -15
  5. package/dist/chain.d.ts +15 -6
  6. package/dist/chain.js +40 -20
  7. package/dist/command-rules.d.ts +55 -0
  8. package/dist/command-rules.js +142 -0
  9. package/dist/command.d.ts +65 -23
  10. package/dist/command.js +695 -226
  11. package/dist/controls.d.ts +8 -0
  12. package/dist/controls.js +23 -0
  13. package/dist/defect.d.ts +18 -0
  14. package/dist/defect.js +272 -0
  15. package/dist/developer.d.ts +24 -0
  16. package/dist/developer.js +52 -0
  17. package/dist/diagnostic-text.d.ts +81 -0
  18. package/dist/diagnostic-text.js +283 -0
  19. package/dist/diagnostic.d.ts +11 -0
  20. package/dist/diagnostic.js +70 -0
  21. package/dist/errors.d.ts +108 -24
  22. package/dist/errors.js +324 -53
  23. package/dist/exit-codes.d.ts +45 -0
  24. package/dist/exit-codes.js +46 -0
  25. package/dist/extension.d.ts +41 -8
  26. package/dist/extension.js +142 -57
  27. package/dist/facts.d.ts +66 -11
  28. package/dist/facts.js +98 -22
  29. package/dist/globals.d.ts +29 -21
  30. package/dist/globals.js +115 -46
  31. package/dist/hints.d.ts +79 -0
  32. package/dist/hints.js +247 -0
  33. package/dist/host.d.ts +13 -0
  34. package/dist/host.js +43 -1
  35. package/dist/identity.d.ts +19 -0
  36. package/dist/identity.js +72 -0
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.js +5 -0
  39. package/dist/input-rules.d.ts +64 -0
  40. package/dist/input-rules.js +145 -0
  41. package/dist/inspect.d.ts +12 -4
  42. package/dist/inspect.js +88 -28
  43. package/dist/lanes.js +1 -1
  44. package/dist/locate.js +4 -4
  45. package/dist/options.d.ts +23 -2
  46. package/dist/options.js +135 -44
  47. package/dist/output.d.ts +9 -2
  48. package/dist/output.js +18 -2
  49. package/dist/plain.d.ts +6 -0
  50. package/dist/plain.js +12 -0
  51. package/dist/plugin-rules.d.ts +62 -0
  52. package/dist/plugin-rules.js +155 -0
  53. package/dist/plugin.d.ts +22 -10
  54. package/dist/plugin.js +348 -116
  55. package/dist/prototypes.d.ts +7 -0
  56. package/dist/prototypes.js +29 -0
  57. package/dist/rendering.d.ts +6 -1
  58. package/dist/rendering.js +23 -5
  59. package/dist/rules.d.ts +51 -0
  60. package/dist/rules.js +115 -0
  61. package/dist/sequence.js +6 -1
  62. package/dist/sources.d.ts +6 -4
  63. package/dist/sources.js +22 -13
  64. package/dist/style-wire.js +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/theme.d.ts +4 -0
  67. package/dist/theme.js +25 -5
  68. package/dist/thenable.d.ts +15 -0
  69. package/dist/thenable.js +29 -0
  70. package/dist/translators.d.ts +69 -0
  71. package/dist/translators.js +253 -0
  72. package/dist/types.d.ts +11 -1
  73. package/dist/validation.d.ts +44 -3
  74. package/dist/validation.js +156 -57
  75. package/dist/view.d.ts +48 -14
  76. package/dist/view.js +155 -77
  77. package/package.json +1 -1
package/dist/view.js CHANGED
@@ -1,4 +1,10 @@
1
- import { DeclarationError, defaultText, FatalError, notTextReason, reasonOf } from './errors.js';
1
+ import { viewShape } from './command-rules.js';
2
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
3
+ import { DeclarationError, defaultText, FatalError, notTextReason, quoted, reasonOf, } from './errors.js';
4
+ import { partFinding, slotSite } from './facts.js';
5
+ import { checkIdentity } from './identity.js';
6
+ import { foreignValue, notAList, overrideKey, overrideTwice, twoPackageCopies, } from './plugin-rules.js';
7
+ import { prototypeChain } from './prototypes.js';
2
8
  import { escapeText } from './style.js';
3
9
  /** Authored declarations register here, so a hand-built object with an identity is a bare view. */
4
10
  const declarations = new WeakMap();
@@ -48,12 +54,22 @@ function shapeOf(value) {
48
54
  return render ? 'render' : 'neither';
49
55
  }
50
56
  function view(identity, definition) {
57
+ checkIdentity('view', identity);
51
58
  const shape = shapeOf(definition);
59
+ const findings = [{ arguments: [identity, definition], call: 'view', mark: '1' }];
52
60
  if (shape === 'both') {
53
- throw new DeclarationError(`View "${identity}" carries render and row. Supply one of the two.`);
61
+ throw new DeclarationError(viewShape, {
62
+ correction: 'Supply one of the two.',
63
+ findings,
64
+ sentence: `View ${quoted(identity)} carries render and row.`,
65
+ });
54
66
  }
55
67
  if (shape === 'neither') {
56
- throw new DeclarationError(`View "${identity}" carries neither render nor row. Supply a view with render or a row view with row.`);
68
+ throw new DeclarationError(viewShape, {
69
+ correction: 'Supply a view with render or a row view with row.',
70
+ findings,
71
+ sentence: `View ${quoted(identity)} carries neither render nor row.`,
72
+ });
57
73
  }
58
74
  return typeof definition.row === 'function'
59
75
  ? new RowViewDeclaration(identity, definition)
@@ -97,7 +113,7 @@ function failureKey(key) {
97
113
  const name = 'name' in key && typeof key.name === 'string' && key.name !== '' ? key.name : 'a failure class';
98
114
  return { kind: 'failure', name, prototype };
99
115
  }
100
- /** The identity register one build starts from, holding the views core itself declares. */
116
+ /** The identity one build starts from, holding the views core itself declares. */
101
117
  function viewIdentities(declared) {
102
118
  const identities = new Map();
103
119
  for (const value of declared) {
@@ -105,22 +121,63 @@ function viewIdentities(declared) {
105
121
  }
106
122
  return identities;
107
123
  }
108
- /** One identity means one declared view, wherever on the graph that view appears. */
109
- function registerIdentity(identities, declared) {
124
+ /**
125
+ * One identity means one declared view, wherever on the graph that view appears. `place` marks the
126
+ * list entry that names the view, when one does.
127
+ */
128
+ function registerIdentity(identities, declared, place) {
110
129
  const known = identities.get(declared.identity);
111
130
  if (known === undefined) {
112
131
  identities.set(declared.identity, declared);
113
132
  return;
114
133
  }
115
134
  if (known !== declared) {
116
- throw new DeclarationError(`View "${declared.identity}" is declared by two distinct objects. Install one copy of the package that declares it.`);
135
+ throw new DeclarationError(twoPackageCopies, {
136
+ correction: 'Install one copy of the package that declares it.',
137
+ findings: place === undefined ? [] : [{ ...place, note: `another ${quoted(declared.identity)}` }],
138
+ sentence: `View ${quoted(declared.identity)} is declared by two distinct objects.`,
139
+ });
117
140
  }
118
141
  }
119
- /** The sentence one contributor's list reports for a value it cannot read. */
120
- function entryFault(subject) {
142
+ /** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
143
+ const identifier = /^[A-Za-z_$][\w$]*$/u;
144
+ /** How a finding prints an override's key: a failure class by its name, anything else elided. */
145
+ function keyCode(key) {
146
+ return key.kind === 'failure' && identifier.test(key.name) ? key.name : elided;
147
+ }
148
+ /**
149
+ * One list entry as a finding prints it: a declared view as the `view()` call that made it, an
150
+ * override as its `override()` call, and any other value as it is.
151
+ */
152
+ function entryCode(entry) {
153
+ if (typeof entry !== 'object' || entry === null) {
154
+ return entry;
155
+ }
156
+ const listed = declarations.get(entry);
157
+ if (listed) {
158
+ return spelled(`view(${quoteString(listed.identity)}, ${elided})`);
159
+ }
160
+ const record = overrides.get(entry);
161
+ return record ? spelled(`override(${keyCode(record.key)}, ${elided})`) : entry;
162
+ }
163
+ /** Where one contributor's `views` list sits, with each entry printed as the call that made it. */
164
+ function listSite(subject, declared) {
165
+ const views = Array.isArray(declared) ? Array.from(declared, entryCode) : declared;
166
+ return slotSite({ ...subject.owner, subject: subject.sentence }, 'views', views);
167
+ }
168
+ /** The fault of one list entry that is not a value this contributor's list may hold. */
169
+ function entryFault(subject, place) {
121
170
  return subject.declares
122
- ? `${subject.sentence} holds a value that is not a view. Supply the value returned by view(identity, definition) or override(key, view).`
123
- : `${subject.sentence} holds a value that is not a view override. Supply the value returned by override(key, view).`;
171
+ ? new DeclarationError(foreignValue, {
172
+ correction: 'Supply the value returned by view(identity, definition) or override(key, view).',
173
+ findings: [place],
174
+ sentence: `${subject.sentence} holds a value that is not a view.`,
175
+ })
176
+ : new DeclarationError(foreignValue, {
177
+ correction: 'Supply the value returned by override(key, view).',
178
+ findings: [place],
179
+ sentence: `${subject.sentence} holds a value that is not a view override.`,
180
+ });
124
181
  }
125
182
  /** A `views` slot holds a list, so anything else is the same declaration fault. */
126
183
  function readContributions(subject, declared) {
@@ -128,50 +185,80 @@ function readContributions(subject, declared) {
128
185
  return [];
129
186
  }
130
187
  if (!Array.isArray(declared)) {
131
- throw new DeclarationError(subject.declares
132
- ? `${subject.sentence} declares views that are not an array. Supply a list of declared views and override values.`
133
- : entryFault(subject));
188
+ throw new DeclarationError(notAList, {
189
+ correction: subject.declares
190
+ ? 'Supply a list of declared views and override values.'
191
+ : 'Supply a list of override values.',
192
+ findings: [partFinding(listSite(subject, declared), [])],
193
+ sentence: `${subject.sentence} declares views that are not an array.`,
194
+ });
134
195
  }
135
196
  return declared;
136
197
  }
137
- /** The override one entry carries; anything else is a declaration fault of the slot. */
138
- function overrideOf(subject, entry) {
139
- const record = typeof entry === 'object' && entry !== null ? overrides.get(entry) : undefined;
140
- if (!record) {
141
- throw new DeclarationError(entryFault(subject));
198
+ /**
199
+ * The entry a key's first override sits at, recording this one's when it is the first. One key
200
+ * answers to one override inside one contributor, so a second override marks both entries.
201
+ */
202
+ function claimKey(build, key, index) {
203
+ const slot = key.kind === 'failure' ? key.prototype : key.view;
204
+ const first = build.positions.get(slot);
205
+ if (first === undefined) {
206
+ build.positions.set(slot, index);
207
+ return;
142
208
  }
143
- return record;
209
+ const clause = key.kind === 'failure'
210
+ ? `the view for ${quoted(key.name)}`
211
+ : `view ${quoted(key.view.identity)}`;
212
+ throw new DeclarationError(overrideTwice, {
213
+ correction: 'Remove one override.',
214
+ findings: [
215
+ partFinding(build.site, [first], 'the first override'),
216
+ partFinding(build.site, [index], 'the second override'),
217
+ ],
218
+ sentence: `${build.subject.sentence} overrides ${clause} twice.`,
219
+ });
144
220
  }
145
221
  /**
146
- * One override recorded under the key it answers. One key answers to one override inside one
147
- * contributor, so a second override for it is a declaration fault; the same key overridden by two
148
- * contributors resolves first-in-wins. A key that is neither a declared view nor a failure class
149
- * is the entry fault of the list that holds it, reported here rather than at the `override()` call.
222
+ * One override recorded under the key it answers. The same key overridden by two contributors
223
+ * resolves first-in-wins. A key that is neither a declared view nor a failure class is a fault of
224
+ * the list that holds it, reported here rather than at the `override()` call.
150
225
  */
151
- function recordOverride(build, { key, replacement }) {
226
+ function recordOverride(build, { key, replacement }, index) {
152
227
  if (key.kind === 'invalid') {
153
- throw new DeclarationError(entryFault(build.subject));
228
+ throw new DeclarationError(overrideKey, {
229
+ correction: 'Key the override on a value view(identity, definition) returned, or on a failure class.',
230
+ findings: [partFinding(build.site, [index])],
231
+ sentence: `${build.subject.sentence} overrides a key that is neither a declared view nor a failure class.`,
232
+ });
154
233
  }
234
+ claimKey(build, key, index);
155
235
  if (key.kind === 'failure') {
156
- recordFailureOverride(build, key, replacement);
236
+ build.contributions.failures.set(key.prototype, replacement);
157
237
  return;
158
238
  }
159
- recordViewOverride(build, key.view, replacement);
239
+ // Naming a declared view as a key registers its identity, as listing the declaration does.
240
+ registerIdentity(build.identities, key.view, partFinding(build.site, [index]));
241
+ build.contributions.views.set(key.view, replacement);
160
242
  }
161
- /** One failure class answers to one override inside one contributor, keyed by its prototype. */
162
- function recordFailureOverride({ contributions, subject }, key, replacement) {
163
- if (contributions.failures.has(key.prototype)) {
164
- throw new DeclarationError(`${subject.sentence} overrides the view for "${key.name}" twice. Remove one override.`);
243
+ /**
244
+ * One entry of a contributor's list: a declared view, which only a plugin may list, or an
245
+ * override. Any other value is the list's entry fault.
246
+ */
247
+ function recordEntry(build, entry, index) {
248
+ const { identities, site, subject } = build;
249
+ const place = partFinding(site, [index]);
250
+ const object = typeof entry === 'object' && entry !== null ? entry : undefined;
251
+ const listed = object === undefined ? undefined : declarations.get(object);
252
+ const record = object === undefined ? undefined : overrides.get(object);
253
+ if (listed && subject.declares) {
254
+ registerIdentity(identities, listed, place);
165
255
  }
166
- contributions.failures.set(key.prototype, replacement);
167
- }
168
- /** Naming a declared view as a key registers its identity, as listing the declaration does. */
169
- function recordViewOverride({ contributions, identities, subject }, key, replacement) {
170
- registerIdentity(identities, key);
171
- if (contributions.views.has(key)) {
172
- throw new DeclarationError(`${subject.sentence} overrides view "${key.identity}" twice. Remove one override.`);
256
+ else if (record) {
257
+ recordOverride(build, record, index);
258
+ }
259
+ else {
260
+ throw entryFault(subject, place);
173
261
  }
174
- contributions.views.set(key, replacement);
175
262
  }
176
263
  /**
177
264
  * One contributor's own overrides, with the identity of every declared view it lists or names as a
@@ -180,17 +267,10 @@ function recordViewOverride({ contributions, identities, subject }, key, replace
180
267
  */
181
268
  function buildViews(subject, declared, identities) {
182
269
  const contributions = { failures: new Map(), views: new Map() };
183
- for (const entry of readContributions(subject, declared)) {
184
- const listed = typeof entry === 'object' && entry !== null ? declarations.get(entry) : undefined;
185
- if (listed && !subject.declares) {
186
- throw new DeclarationError(entryFault(subject));
187
- }
188
- if (listed) {
189
- registerIdentity(identities, listed);
190
- }
191
- else {
192
- recordOverride({ contributions, identities, subject }, overrideOf(subject, entry));
193
- }
270
+ const site = listSite(subject, declared);
271
+ const build = { contributions, identities, positions: new Map(), site, subject };
272
+ for (const [index, entry] of readContributions(subject, declared).entries()) {
273
+ recordEntry(build, entry, index);
194
274
  }
195
275
  return contributions;
196
276
  }
@@ -198,9 +278,10 @@ function buildViews(subject, declared, identities) {
198
278
  function readStored(stored) {
199
279
  // Last resort: no typed path exists.
200
280
  // A registry holds one entry per key and cannot carry a type parameter per entry.
201
- // A stored view value therefore reads back with its data type erased.
281
+ // A stored view value therefore reads back with its data type and its context erased.
202
282
  // It holds because `override(key, replacement)` typed the replacement against its key's data.
203
283
  // Resolution reaches a stored value through that key alone.
284
+ // A failure key is resolved by `describeFailure` alone, which passes the failure view context.
204
285
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
205
286
  return stored;
206
287
  }
@@ -217,16 +298,6 @@ function missing(name, key) {
217
298
  function callView(stored, key) {
218
299
  return readStored(stored).render ?? missing('render', key);
219
300
  }
220
- /** Every prototype in a failure's chain, most derived first, so one walk reads one contributor. */
221
- function chainOf(failure) {
222
- const chain = [];
223
- let prototype = Object.getPrototypeOf(failure);
224
- while (prototype !== null) {
225
- chain.push(prototype);
226
- prototype = Object.getPrototypeOf(prototype);
227
- }
228
- return chain;
229
- }
230
301
  /**
231
302
  * The view function one declared view resolves to: the first contributor that overrides it, then
232
303
  * its own default. A bare view is never overridden, so it resolves to its own function.
@@ -267,7 +338,8 @@ function resolveRowView(registry, value) {
267
338
  * for a base class beats a plugin's override for a subclass.
268
339
  */
269
340
  function resolveFailure(registry, failure) {
270
- const chain = chainOf(failure);
341
+ // A failure core reports has a readable chain, so the empty fallback only keeps the walk total.
342
+ const chain = prototypeChain(failure) ?? [];
271
343
  for (const contributor of registry) {
272
344
  for (const prototype of chain) {
273
345
  const replacement = contributor.failures.get(prototype);
@@ -278,30 +350,36 @@ function resolveFailure(registry, failure) {
278
350
  }
279
351
  return undefined;
280
352
  }
353
+ /**
354
+ * Core's own default text for one failure, escaped unless it is the authored marked message of a
355
+ * `FatalError`, with each hint on its own line under it as marked text it does not escape.
356
+ */
357
+ function coreText(failure, text, hints) {
358
+ const sentence = failure instanceof FatalError ? text : escapeText(text);
359
+ return `${sentence}${hints.map((hint) => `${hint}\n`).join('')}`;
360
+ }
281
361
  /**
282
362
  * The text core writes for one failure. Resolution walks the registry as `resolveFailure` defines
283
- * it and falls to core's own default text, which escapes the raw facts it interpolates. A
284
- * `FatalError` keeps the authored marked message it was given.
363
+ * it and falls to core's own default text, which opens a usage failure with the context's
364
+ * application name and escapes the raw facts it interpolates. A `FatalError` keeps the authored
365
+ * marked message it was given. The default text writes each hint on its own line under the
366
+ * sentence, as marked text it does not escape; an override decides for itself. An unrendered report
367
+ * carries the same default text without hints, which the plain fallback path writes.
285
368
  */
286
369
  function describeFailure(registry, failure, context) {
370
+ const text = defaultText(failure, context.application);
287
371
  const replacement = resolveFailure(registry, failure);
288
372
  if (!replacement) {
289
- return {
290
- kind: 'rendered',
291
- text: failure instanceof FatalError ? defaultText(failure) : escapeText(defaultText(failure)),
292
- };
373
+ return { core: true, kind: 'rendered', text: coreText(failure, text, context.hints) };
293
374
  }
294
375
  try {
295
- if (context === undefined) {
296
- throw new Error('Missing rendering context.');
297
- }
298
- const text = callView(replacement, failure.name)(failure, context);
299
- return typeof text === 'string'
300
- ? { kind: 'rendered', text }
301
- : { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
376
+ const rendered = callView(replacement, failure.name)(failure, context);
377
+ return typeof rendered === 'string'
378
+ ? { core: false, kind: 'rendered', text: rendered }
379
+ : { cause: undefined, kind: 'unrendered', reason: notTextReason(rendered), text };
302
380
  }
303
381
  catch (error) {
304
- return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
382
+ return { cause: error, kind: 'unrendered', reason: reasonOf(error), text };
305
383
  }
306
384
  }
307
385
  export { buildViews, describeFailure, override, resolveRowView, resolveView, shapeOf, view, viewIdentities, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcli/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "The Loom CLI core package. Provides the scaffolding for creating new Loom CLI applications.",
5
5
  "license": "MIT",
6
6
  "repository": {