@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
@@ -0,0 +1,253 @@
1
+ import { spelled } from './diagnostic-text.js';
2
+ import { asSentence, DeclarationError, InternalError, LoomError, quoted, reasonOf, } from './errors.js';
3
+ import { partFinding } from './facts.js';
4
+ import { foreignValue, notAFunction, notAList, translationKey } from './plugin-rules.js';
5
+ import { prototypeChain } from './prototypes.js';
6
+ import { brokenTranslator, brokenTranslatorCorrection } from './rules.js';
7
+ import { ignoreRejection, isThenable } from './thenable.js';
8
+ /** Authored translations register here, so the public type publishes nothing to reach. */
9
+ const records = new WeakMap();
10
+ /** The runtime value `translate()` returns. Its record lives in the registry above. */
11
+ class TranslationDeclaration {
12
+ constructor(record) {
13
+ records.set(this, record);
14
+ Object.freeze(this);
15
+ }
16
+ }
17
+ /**
18
+ * The prototype a class key carries, or `undefined` for a value that is not a class. A class is a
19
+ * function whose `prototype` is the object its instances' chains hold, so an arrow function, a
20
+ * bound function, and every non-function are no key at all. A key whose `prototype` cannot be
21
+ * read, such as a proxy whose trap throws, is no key either.
22
+ */
23
+ function keyPrototype(key) {
24
+ let prototype = undefined;
25
+ try {
26
+ if (typeof key !== 'function' || !('prototype' in key)) {
27
+ return undefined;
28
+ }
29
+ prototype = key.prototype;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ return typeof prototype === 'object' && prototype !== null ? prototype : undefined;
35
+ }
36
+ /** A key's own name, or `undefined` when it has none that can be read. */
37
+ function nameOf(key) {
38
+ let name = undefined;
39
+ try {
40
+ name = 'name' in key ? key.name : undefined;
41
+ }
42
+ catch {
43
+ name = undefined;
44
+ }
45
+ return typeof name === 'string' && name !== '' ? name : undefined;
46
+ }
47
+ /**
48
+ * How a diagnostic names one key class: its name, quoted and escaped, or what it is when it has no
49
+ * name that can be read.
50
+ */
51
+ function keyName(key) {
52
+ const name = nameOf(key);
53
+ return name === undefined ? 'an anonymous class' : quoted(name);
54
+ }
55
+ /** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
56
+ const identifier = /^[A-Za-z_$][\w$]*$/u;
57
+ /**
58
+ * The finding for one `translate()` call, marking the argument at `mark`. A class key prints as
59
+ * its name, as its author wrote it, where any other function prints as an ellipsis.
60
+ */
61
+ function translateFinding(key, translator, mark) {
62
+ const name = typeof key === 'function' ? nameOf(key) : undefined;
63
+ const code = name !== undefined && identifier.test(name) ? spelled(name) : key;
64
+ return { arguments: [code, translator], call: 'translate', mark };
65
+ }
66
+ /**
67
+ * Whether a key claims a thrown value as its instance. A value whose chain cannot be read is not
68
+ * claimed, so its translator is never called and is not blamed for the failed read.
69
+ */
70
+ function claims(thrown, key) {
71
+ try {
72
+ return thrown instanceof key;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Pairs an error class with the translator that turns its instances into a failure. The
80
+ * translator receives the thrown instance typed from the class, and a `translators` list on the
81
+ * Application or a plugin registers the pair.
82
+ */
83
+ function translate(key, translator) {
84
+ const prototype = keyPrototype(key);
85
+ // A key whose prototype's chain cannot be read, such as through a proxy whose trap throws, is no
86
+ // Key either, so it is reported as a non-class before any failure-class check.
87
+ const chain = prototype === undefined ? undefined : prototypeChain(prototype);
88
+ if (prototype === undefined || chain === undefined) {
89
+ throw new DeclarationError(translationKey, {
90
+ correction: 'Supply an error class, such as SyntaxError.',
91
+ findings: [translateFinding(key, translator, '0')],
92
+ sentence: 'translate() received a key that is not a class.',
93
+ });
94
+ }
95
+ // Core never offers a failure to translators, so a translation keyed on a failure class never runs.
96
+ if (prototype === LoomError.prototype || chain.includes(LoomError.prototype)) {
97
+ throw new DeclarationError(translationKey, {
98
+ correction: 'Key the translation on the foreign class it replaces.',
99
+ findings: [translateFinding(key, translator, '0')],
100
+ sentence: 'translate() received a failure class as its key.',
101
+ });
102
+ }
103
+ if (typeof translator !== 'function') {
104
+ throw new DeclarationError(notAFunction, {
105
+ correction: 'Supply a function that returns a failure or undefined.',
106
+ findings: [translateFinding(key, translator, '1')],
107
+ sentence: 'translate() received a translator that is not a function.',
108
+ });
109
+ }
110
+ return new TranslationDeclaration({
111
+ name: keyName(key),
112
+ offer: (thrown) => (claims(thrown, key) ? translator(thrown) : undefined),
113
+ prototype,
114
+ });
115
+ }
116
+ /**
117
+ * One contributor's `translators` list. `site` names the contributor at the start of a sentence,
118
+ * `The Application` or `Plugin "@acme/http"`, and holds the call that declared the list, which a
119
+ * fault marks. The slot is read defensively, because a JavaScript author reaches it with any value.
120
+ */
121
+ function readTranslations(site, declared) {
122
+ const sentence = site.subject;
123
+ const byPrototype = new Map();
124
+ for (const record of translationRecords(site, declared)) {
125
+ const listed = byPrototype.get(record.prototype) ?? [];
126
+ listed.push(record);
127
+ byPrototype.set(record.prototype, listed);
128
+ }
129
+ return {
130
+ byPrototype,
131
+ subject: `${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
132
+ };
133
+ }
134
+ /** The fix a `translators` slot's list fault and entry fault share. */
135
+ const translationSupply = 'translate(ErrorClass, translator)';
136
+ /** The record behind each entry of one `translators` slot, whose every other value is its fault. */
137
+ function translationRecords(site, declared) {
138
+ if (declared !== undefined && !Array.isArray(declared)) {
139
+ throw new DeclarationError(notAList, {
140
+ correction: `Supply a list of values returned by ${translationSupply}.`,
141
+ findings: [partFinding(site, [])],
142
+ sentence: `${site.subject} declares translators that are not an array.`,
143
+ });
144
+ }
145
+ const list = declared ?? [];
146
+ // Array.from visits a hole as undefined, where map would skip it and leave the hole in place.
147
+ return Array.from(list, (entry, index) => {
148
+ const record = typeof entry === 'object' && entry !== null ? records.get(entry) : undefined;
149
+ if (!record) {
150
+ throw new DeclarationError(foreignValue, {
151
+ correction: `Supply the value returned by ${translationSupply}.`,
152
+ findings: [partFinding(site, [index])],
153
+ sentence: `${site.subject} holds a translator entry that is not a translation.`,
154
+ });
155
+ }
156
+ return record;
157
+ });
158
+ }
159
+ /**
160
+ * Every prototype in one thrown value's chain, most derived first, or `undefined` when the value is
161
+ * never offered: a value whose chain holds a failure class, and a value whose chain cannot be read,
162
+ * such as a proxy whose trap throws, or whose chain repeats a link or never ends. A value with a
163
+ * `null` prototype has an empty chain, which no key matches.
164
+ */
165
+ function offeredChain(thrown) {
166
+ const chain = prototypeChain(thrown);
167
+ return chain?.includes(LoomError.prototype) ? undefined : chain;
168
+ }
169
+ /** How a broken translator's diagnostic names a value that is not a failure. */
170
+ function returnedKind(value) {
171
+ if (value === null) {
172
+ return 'null';
173
+ }
174
+ if (isThenable(value)) {
175
+ return 'a promise';
176
+ }
177
+ const kind = typeof value;
178
+ return /^[aeiou]/u.test(kind) ? `an ${kind}` : `a ${kind}`;
179
+ }
180
+ /** Whether a translator's answer is a failure, read without letting a hostile value throw. */
181
+ function isFailure(value) {
182
+ try {
183
+ return value instanceof LoomError;
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ /** The defect of a broken translator under its rule, with the sentence and cause each shape gives. */
190
+ function brokenDefect(sentence, cause) {
191
+ return new InternalError(brokenTranslator, {
192
+ cause,
193
+ correction: brokenTranslatorCorrection,
194
+ sentence,
195
+ });
196
+ }
197
+ /**
198
+ * The defect of a translator that threw. Its cause is a new `AggregateError` that holds the
199
+ * translator's throw and then the original throw, so neither is lost and neither is mutated. The
200
+ * reason is read through `reasonOf`, which escapes it onto one line.
201
+ */
202
+ function threwDefect(who, error, thrown) {
203
+ const reason = reasonOf(error);
204
+ const cause = new AggregateError([error, thrown], 'The translator threw while translating the original throw.');
205
+ return brokenDefect(`${who} threw: ${asSentence(reason)}`, cause);
206
+ }
207
+ /** The one translator call, whose throw or non-failure answer is the defect of that translator. */
208
+ function consult(contributor, record, thrown) {
209
+ const who = `The translator ${contributor.subject} registered for ${record.name}`;
210
+ let answer = undefined;
211
+ try {
212
+ answer = record.offer(thrown);
213
+ }
214
+ catch (error) {
215
+ return threwDefect(who, error, thrown);
216
+ }
217
+ if (answer === undefined || isFailure(answer)) {
218
+ return answer;
219
+ }
220
+ // A translator is synchronous, so a returned promise is ignored once its rejection is observed.
221
+ if (isThenable(answer)) {
222
+ ignoreRejection(answer);
223
+ }
224
+ return brokenDefect(`${who} returned ${returnedKind(answer)} instead of a failure.`, thrown);
225
+ }
226
+ /**
227
+ * The failure the translators answer for one foreign throw, or `undefined` when the throw is never
228
+ * offered or every translator passed. Resolution follows the override walk: each contributor in
229
+ * order, the thrown value's chain walked in full at each, most derived first, and within one
230
+ * class the list order. A broken translator ends the walk with its defect, so no later translator
231
+ * hides it.
232
+ */
233
+ function translateThrow(registry, thrown) {
234
+ if ((typeof thrown !== 'object' && typeof thrown !== 'function') || thrown === null) {
235
+ return undefined;
236
+ }
237
+ // With no translation registered, the chain is never read.
238
+ const chain = registry.some((contributor) => contributor.byPrototype.size > 0)
239
+ ? (offeredChain(thrown) ?? [])
240
+ : [];
241
+ for (const contributor of registry) {
242
+ for (const prototype of chain) {
243
+ for (const record of contributor.byPrototype.get(prototype) ?? []) {
244
+ const answer = consult(contributor, record, thrown);
245
+ if (answer !== undefined) {
246
+ return answer;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ return undefined;
252
+ }
253
+ export { readTranslations, translate, translateThrow };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="node" preserve="true" />
2
2
  import type { Readable, Writable } from 'node:stream';
3
3
  import type { StandardSchemaV1 } from '@standard-schema/spec';
4
+ import type { FailureExitCode } from './exit-codes.js';
4
5
  import type { ExtensionValue } from './extension.js';
5
6
  import type { CommandGraph, CommandNode, ResultNode } from './inspect.js';
6
7
  import type { RenderingPolicy } from './rendering.js';
@@ -101,7 +102,8 @@ export type GlobalNameConstraint<Name extends string, Globals> = Name extends ke
101
102
  } : unknown;
102
103
  /** A required record key excludes open strings; distribution rejects each union member. */
103
104
  export type NameConstraint<Name extends string, Whole extends string = Name> = {} extends Record<Name, unknown> ? LiteralNameFault : Name extends Whole ? [Whole] extends [Name] ? unknown : LiteralNameFault : LiteralNameFault;
104
- export type ExitCode = 0 | 1 | 2 | 130 | 143;
105
+ /** The status `run()` resolves: success, a failure class's code, or a cancellation code. */
106
+ export type ExitCode = 0 | FailureExitCode | 130 | 143;
105
107
  export interface InputTerminal {
106
108
  isTTY: boolean;
107
109
  }
@@ -122,6 +124,14 @@ export interface Host {
122
124
  stdin: Readable;
123
125
  stdout: Writable;
124
126
  stderr: Writable;
127
+ /**
128
+ * Reads one source file for a defect's Developer Diagnostic, or answers `undefined`. Core calls
129
+ * it only in a development build, only while it reports a defect, and only for a path under the
130
+ * working directory. Process capture supplies a reader that refuses a file outside `cwd` after
131
+ * resolving symbolic links, and answers `undefined` for a path that is not a regular file or
132
+ * that exceeds 1 MiB.
133
+ */
134
+ readSource?: (path: string, cwd: string) => string | undefined;
125
135
  }
126
136
  export interface RunOptions {
127
137
  rendering?: RenderingPolicy;
@@ -1,4 +1,6 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { Finding } from './diagnostic-text.js';
3
+ import type { InputSite } from './facts.js';
2
4
  import type { OptionValues } from './options.js';
3
5
  import type { ArgumentConfig, ArgumentValue, Host, OptionConfig, OptionValue } from './types.js';
4
6
  /**
@@ -73,12 +75,43 @@ declare class ValidatedInputs {
73
75
  read(input: InputDeclaration): unknown;
74
76
  }
75
77
  export type { ValidatedInputs };
78
+ /**
79
+ * The one check every `argument()`, `option()`, and `globalOption()` call makes before it reads its
80
+ * config, so a call in JavaScript that supplies none, or a value of another kind, reports a
81
+ * declaration fault and not a TypeError. `place` is where the call sits.
82
+ */
83
+ export declare function checkInputConfig(declared: {
84
+ readonly config: unknown;
85
+ readonly kind: InputDeclaration['kind'];
86
+ readonly name: string;
87
+ }, place: InputPlace): void;
76
88
  /**
77
89
  * Authoring's snapshot of one config. An array default is the one declared value core hands to an
78
90
  * action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
79
91
  * other property is captured as declared, because core clones no library object.
80
92
  */
81
93
  export declare function captureConfig<Config extends ArgumentConfig | OptionConfig>(config: Config): Config;
94
+ /**
95
+ * A declaration error names the declaration, because the author reads the declaration to fix it.
96
+ * An argument declares and reads under one name, so the two namings differ for options alone.
97
+ */
98
+ export declare function declarationSubject(input: InputDeclaration): string;
99
+ /** Where the call that declared one input sits: the call's name and the Command it is on. */
100
+ export type InputPlace = Pick<Finding, 'call' | 'path'>;
101
+ /**
102
+ * Where one input was declared: a global option at its `globalOption()` call, and every other input
103
+ * at its own `argument()` or `option()` call on the Command at `path`.
104
+ */
105
+ export declare function inputPlace(input: InputDeclaration, scope: {
106
+ readonly global: boolean;
107
+ readonly path: readonly string[];
108
+ }): InputPlace;
109
+ /**
110
+ * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
111
+ * finding for an input's own call starts from it, so each marks the call the same way. `subject`
112
+ * is the sentence's name for the input.
113
+ */
114
+ export declare function declaringSite(input: InputDeclaration, place: InputPlace, subject?: string): InputSite;
82
115
  /**
83
116
  * The declaration flag that sends an omitted value to its own validator. Every declaration reads it
84
117
  * here, and the declaration rules below reject it wherever another rule already decides absence.
@@ -90,6 +123,11 @@ export declare function validatesOmission(input: InputDeclaration): boolean;
90
123
  * helper, so a rejected item reads alike wherever its diagnostic is written.
91
124
  */
92
125
  export declare function issuePath(issue: StandardSchemaV1.Issue): string | undefined;
126
+ /** One declaration and where it was declared, which its faults' findings rebuild. */
127
+ export interface SitedInput {
128
+ readonly input: InputDeclaration;
129
+ readonly site: InputSite;
130
+ }
93
131
  /**
94
132
  * Every declaration rule that reads the declaration alone. It is synchronous, so the call that
95
133
  * declares an input applies it, and build applies it to an input a lifecycle hook declared; only
@@ -97,10 +135,13 @@ export declare function issuePath(issue: StandardSchemaV1.Issue): string | undef
97
135
  * contributor that declares under its own name, such as a plugin, supplies the subject its
98
136
  * diagnostics read with; every other caller is named by the declaration itself.
99
137
  */
100
- export declare function checkDeclarations(inputs: readonly InputDeclaration[], named?: string): void;
138
+ export declare function checkDeclarations(inputs: readonly SitedInput[], named?: string): void;
139
+ /** The call that declared one input and the Command it sits on, which a default's fault rebuilds. */
140
+ export type InputPlaces = ReadonlyMap<InputDeclaration, InputPlace>;
101
141
  /**
102
142
  * Every declared default, validated before any token is read. The host is captured by then, so a
103
- * default's validator reads the same Host its action will, under the `default` phase.
143
+ * default's validator reads the same Host its action will, under the `default` phase. `places`
144
+ * says where each declaration sits, which a rejected default's finding rebuilds.
104
145
  */
105
- export declare function prepareInputs(inputs: ScopedInputs, host: Host): Promise<DefaultValues>;
146
+ export declare function prepareInputs(inputs: ScopedInputs, host: Host, places: InputPlaces): Promise<DefaultValues>;
106
147
  export declare function validateValues(invocation: Invocation): Promise<ValidatedInputs>;