@loomcli/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +18 -2
- package/dist/application.js +266 -71
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/chain.d.ts +15 -6
- package/dist/chain.js +40 -20
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +65 -23
- package/dist/command.js +695 -226
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +108 -24
- package/dist/errors.js +324 -53
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +41 -8
- package/dist/extension.js +142 -57
- package/dist/facts.d.ts +66 -11
- package/dist/facts.js +98 -22
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +115 -46
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +12 -4
- package/dist/inspect.js +88 -28
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +23 -2
- package/dist/options.js +135 -44
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +22 -10
- package/dist/plugin.js +348 -116
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +6 -4
- package/dist/sources.js +22 -13
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +11 -1
- package/dist/validation.d.ts +44 -3
- package/dist/validation.js +156 -57
- package/dist/view.d.ts +48 -14
- package/dist/view.js +155 -77
- package/package.json +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { escapeControlCharacters } from './controls.js';
|
|
2
|
+
import { isPlainObject } from './plain.js';
|
|
3
|
+
/** The width a diagnostic takes in a thrown error's message, or when the stderr width is unknown. */
|
|
4
|
+
const messageWidth = 80;
|
|
5
|
+
/** How far a finding's code sits from the left edge. */
|
|
6
|
+
const findingIndent = ' ';
|
|
7
|
+
/** How deep a finding's argument is printed before the rest reads as an ellipsis. */
|
|
8
|
+
const depthLimit = 4;
|
|
9
|
+
/** The stand-in for a value a finding does not spell out: a function, a validator, or a cycle. */
|
|
10
|
+
const elided = '…';
|
|
11
|
+
/** A key an object literal can spell without quotes. */
|
|
12
|
+
const identifierKey = /^[A-Za-z_$][\w$]*$/u;
|
|
13
|
+
/** A string as a single-quoted JavaScript literal, with every control escaped. */
|
|
14
|
+
function quoteString(text) {
|
|
15
|
+
return `'${escapeControlCharacters(text.replaceAll('\\', String.raw `\\`).replaceAll("'", String.raw `\'`))}'`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Stand-ins a finding prints as the code they name, such as `new Command('get')` for the Command
|
|
19
|
+
* value an attach received, which a finding would otherwise print as an ellipsis.
|
|
20
|
+
*/
|
|
21
|
+
const spellings = new WeakMap();
|
|
22
|
+
/** A finding argument that prints as the code given, which the caller has already escaped. */
|
|
23
|
+
function spelled(code) {
|
|
24
|
+
const value = Object.freeze({});
|
|
25
|
+
spellings.set(value, code);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** The code a stand-in prints as, or `undefined` for any other value. */
|
|
29
|
+
function spellingOf(value) {
|
|
30
|
+
return typeof value === 'object' && value !== null ? spellings.get(value) : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Whether a value is a Standard Schema, which a finding prints as an ellipsis like a function. */
|
|
33
|
+
function isValidator(value) {
|
|
34
|
+
return '~standard' in value;
|
|
35
|
+
}
|
|
36
|
+
/** A primitive as JavaScript source, or `undefined` for an object or a function. */
|
|
37
|
+
function primitiveCode(value) {
|
|
38
|
+
switch (typeof value) {
|
|
39
|
+
case 'string': {
|
|
40
|
+
return quoteString(value);
|
|
41
|
+
}
|
|
42
|
+
case 'number': {
|
|
43
|
+
return Object.is(value, -0) ? '-0' : String(value);
|
|
44
|
+
}
|
|
45
|
+
case 'bigint': {
|
|
46
|
+
return `${String(value)}n`;
|
|
47
|
+
}
|
|
48
|
+
case 'symbol': {
|
|
49
|
+
return escapeControlCharacters(String(value));
|
|
50
|
+
}
|
|
51
|
+
case 'boolean':
|
|
52
|
+
case 'undefined': {
|
|
53
|
+
return String(value);
|
|
54
|
+
}
|
|
55
|
+
default: {
|
|
56
|
+
return value === null ? 'null' : undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** One object key as an object literal spells it. */
|
|
61
|
+
function keyCode(key) {
|
|
62
|
+
return identifierKey.test(key) ? key : quoteString(key);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Printed code in progress, and the span each argument path and key covers, which a mark
|
|
66
|
+
* underlines. Reading a value runs no author code beyond property reads, and a read that throws
|
|
67
|
+
* prints an ellipsis.
|
|
68
|
+
*/
|
|
69
|
+
class CodePrinter {
|
|
70
|
+
text;
|
|
71
|
+
spans = new Map();
|
|
72
|
+
constructor(text) {
|
|
73
|
+
this.text = text;
|
|
74
|
+
}
|
|
75
|
+
/** Appends one value and records the span it covers under its path. */
|
|
76
|
+
value(value, place) {
|
|
77
|
+
const start = this.text.length;
|
|
78
|
+
try {
|
|
79
|
+
// Nested members append while the composite is read, so its tail appends after them.
|
|
80
|
+
const tail = this.composite(value, place);
|
|
81
|
+
this.text += tail;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
this.text = `${this.text.slice(0, start)}${elided}`;
|
|
85
|
+
}
|
|
86
|
+
this.spans.set(place.path, { end: this.text.length, start });
|
|
87
|
+
}
|
|
88
|
+
/** Appends each item with a comma between, as a list of arguments or members is written. */
|
|
89
|
+
separated(items, write) {
|
|
90
|
+
for (const [index, item] of items.entries()) {
|
|
91
|
+
if (index > 0) {
|
|
92
|
+
this.text += ', ';
|
|
93
|
+
}
|
|
94
|
+
write(item, index);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The code for one value, appending nested members as it goes and returning what is left to
|
|
99
|
+
* append. A function, a validator, a class instance, a cycle, and a value deeper than the limit
|
|
100
|
+
* print an ellipsis.
|
|
101
|
+
*/
|
|
102
|
+
composite(value, { path, seen }) {
|
|
103
|
+
const known = primitiveCode(value) ?? spellingOf(value);
|
|
104
|
+
if (known !== undefined) {
|
|
105
|
+
return known;
|
|
106
|
+
}
|
|
107
|
+
if (typeof value !== 'object' || value === null || seen.has(value) || seen.size >= depthLimit) {
|
|
108
|
+
return elided;
|
|
109
|
+
}
|
|
110
|
+
const inner = { path, seen: new Set([...seen, value]) };
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return this.list(value, inner);
|
|
113
|
+
}
|
|
114
|
+
return isValidator(value) || !isPlainObject(value) ? elided : this.record(value, inner);
|
|
115
|
+
}
|
|
116
|
+
/** One array literal. Its members append in place, so each records its own span. */
|
|
117
|
+
list(list, { path, seen }) {
|
|
118
|
+
this.text += '[';
|
|
119
|
+
// A hole reads as undefined, so the list is copied before it is walked.
|
|
120
|
+
this.separated([...list], (member, index) => {
|
|
121
|
+
this.value(member, { path: `${path}.${String(index)}`, seen });
|
|
122
|
+
});
|
|
123
|
+
return ']';
|
|
124
|
+
}
|
|
125
|
+
/** One object literal. A key's span covers the whole `key: value` pair, which a mark underlines. */
|
|
126
|
+
record(record, place) {
|
|
127
|
+
const keys = Object.keys(record);
|
|
128
|
+
if (keys.length === 0) {
|
|
129
|
+
return '{}';
|
|
130
|
+
}
|
|
131
|
+
this.text += '{ ';
|
|
132
|
+
this.separated(keys, (key) => {
|
|
133
|
+
this.member(key, Reflect.get(record, key), place);
|
|
134
|
+
});
|
|
135
|
+
return ' }';
|
|
136
|
+
}
|
|
137
|
+
/** One `key: value` pair of an object literal. */
|
|
138
|
+
member(key, value, { path, seen }) {
|
|
139
|
+
const start = this.text.length;
|
|
140
|
+
const member = `${path}.${key}`;
|
|
141
|
+
this.text += `${keyCode(key)}: `;
|
|
142
|
+
this.value(value, { path: member, seen });
|
|
143
|
+
this.spans.set(member, { end: this.text.length, start });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** One call with its arguments, and the span each argument and key covers inside the line. */
|
|
147
|
+
function printCall(prefix, finding) {
|
|
148
|
+
const printer = new CodePrinter(`${prefix}${escapeControlCharacters(finding.call)}(`);
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
printer.separated(finding.arguments, (argument, index) => {
|
|
151
|
+
printer.value(argument, { path: String(index), seen });
|
|
152
|
+
});
|
|
153
|
+
printer.text += ')';
|
|
154
|
+
return printer;
|
|
155
|
+
}
|
|
156
|
+
/** One value as the JavaScript a finding prints for it. */
|
|
157
|
+
function valueCode(value) {
|
|
158
|
+
const printer = new CodePrinter('');
|
|
159
|
+
printer.value(value, { path: '', seen: new Set() });
|
|
160
|
+
return printer.text;
|
|
161
|
+
}
|
|
162
|
+
/** The line of carets under a finding's mark, with its note beside it, or none. */
|
|
163
|
+
function markLine(printed, finding) {
|
|
164
|
+
const span = finding.mark === undefined ? undefined : printed.spans.get(finding.mark);
|
|
165
|
+
if (span === undefined) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const carets = '^'.repeat(Math.max(1, span.end - span.start));
|
|
169
|
+
const note = finding.note === undefined ? '' : ` ${escapeControlCharacters(finding.note)}`;
|
|
170
|
+
return [`${' '.repeat(span.start)}${carets}${note}`];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The receiver a finding's call sits on: the Command its path names, or the Application for the
|
|
174
|
+
* root, and the comment above it that opens with the path, after the application name when known.
|
|
175
|
+
*/
|
|
176
|
+
function receiverLines(path, application) {
|
|
177
|
+
const words = application === undefined ? path : [application, ...path];
|
|
178
|
+
const comment = words.length === 0 ? [] : [`// ${escapeControlCharacters(words.join(' '))}`];
|
|
179
|
+
const last = path.at(-1);
|
|
180
|
+
if (last !== undefined) {
|
|
181
|
+
return [...comment, `new Command(${quoteString(last)})`];
|
|
182
|
+
}
|
|
183
|
+
return [
|
|
184
|
+
...comment,
|
|
185
|
+
`new Application(${application === undefined ? elided : quoteString(application)})`,
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
/** One finding as indented code: the rebuilt call, and the marks under the part at fault. */
|
|
189
|
+
function findingSection(finding, application) {
|
|
190
|
+
const receiver = finding.path === undefined ? [] : receiverLines(finding.path, application);
|
|
191
|
+
const printed = printCall(finding.path === undefined ? '' : ' .', finding);
|
|
192
|
+
return [...receiver, printed.text, ...markLine(printed, finding)]
|
|
193
|
+
.map((line) => `${findingIndent}${line}`.trimEnd())
|
|
194
|
+
.join('\n');
|
|
195
|
+
}
|
|
196
|
+
/** Prose wrapped to the width at spaces. A word wider than the width keeps a line of its own. */
|
|
197
|
+
function wrap(text, width) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
let line = '';
|
|
200
|
+
for (const word of text.split(' ')) {
|
|
201
|
+
if (line !== '' && line.length + 1 + word.length > width) {
|
|
202
|
+
lines.push(line);
|
|
203
|
+
line = word;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
line = line === '' ? word : `${line} ${word}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
lines.push(line);
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The sentence with every control escaped. A declaration fault's sentence is written by the author
|
|
214
|
+
* of a rule, so its line breaks, such as the issue lines under a rejected default, stay breaks; a
|
|
215
|
+
* defect's sentence can carry a thrown value's reason, so it stays on one line.
|
|
216
|
+
*/
|
|
217
|
+
function sentenceText(anatomy) {
|
|
218
|
+
return anatomy.fallback === 'DEFECT'
|
|
219
|
+
? escapeControlCharacters(anatomy.sentence)
|
|
220
|
+
: anatomy.sentence.split('\n').map(escapeControlCharacters).join('\n');
|
|
221
|
+
}
|
|
222
|
+
/** The banner: two hyphens, the headline in capitals, a run of hyphens, and the rule's identity. */
|
|
223
|
+
function bannerLine(anatomy, width) {
|
|
224
|
+
const { rule } = anatomy;
|
|
225
|
+
const title = rule === undefined ? anatomy.fallback : rule.headline.toUpperCase();
|
|
226
|
+
const left = `-- ${escapeControlCharacters(title)} `;
|
|
227
|
+
const right = rule === undefined ? '' : ` ${escapeControlCharacters(rule.identity)}`;
|
|
228
|
+
const fill = Math.max(2, width - left.length - right.length);
|
|
229
|
+
return `${left}${'-'.repeat(fill)}${right}`;
|
|
230
|
+
}
|
|
231
|
+
/** The correction: one imperative sentence, or one fix per line when several fit. */
|
|
232
|
+
function correctionSection(correction) {
|
|
233
|
+
if (correction === undefined) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
if (typeof correction === 'string') {
|
|
237
|
+
return [escapeControlCharacters(correction)];
|
|
238
|
+
}
|
|
239
|
+
return correction.length === 0
|
|
240
|
+
? []
|
|
241
|
+
: [correction.map((fix) => `- ${escapeControlCharacters(fix)}`).join('\n')];
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* One Developer Diagnostic as plain text with no trailing newline: the banner, the sentence, the
|
|
245
|
+
* findings, the explanation, the correction, and the docs link, separated by one blank line, each
|
|
246
|
+
* left out when the fault does not carry it. The banner is returned apart, so a caller that styles
|
|
247
|
+
* it can.
|
|
248
|
+
*/
|
|
249
|
+
function diagnosticSections(anatomy, layout) {
|
|
250
|
+
const { rule } = anatomy;
|
|
251
|
+
const sections = [
|
|
252
|
+
sentenceText(anatomy),
|
|
253
|
+
...anatomy.findings.map((finding) => findingSection(finding, layout.application)),
|
|
254
|
+
...anatomy.evidence,
|
|
255
|
+
...(rule === undefined ? [] : [wrap(escapeControlCharacters(rule.explanation), layout.width)]),
|
|
256
|
+
...correctionSection(anatomy.correction),
|
|
257
|
+
...(rule?.docs === undefined ? [] : [`See ${escapeControlCharacters(rule.docs)}`]),
|
|
258
|
+
];
|
|
259
|
+
return { banner: bannerLine(anatomy, layout.width), body: sections.join('\n\n') };
|
|
260
|
+
}
|
|
261
|
+
/** The whole diagnostic as the plain text a thrown fault's message holds. */
|
|
262
|
+
function diagnosticText(anatomy, layout = { width: messageWidth }) {
|
|
263
|
+
const { banner, body } = diagnosticSections(anatomy, layout);
|
|
264
|
+
return `${banner}\n\n${body}`;
|
|
265
|
+
}
|
|
266
|
+
/** Every descriptor built through `registerRule`, so a hand-built object is not a rule. */
|
|
267
|
+
const rules = new WeakSet();
|
|
268
|
+
/** One frozen descriptor from parts already checked, which every site raising it shares. */
|
|
269
|
+
function registerRule(identity, definition) {
|
|
270
|
+
const rule = Object.freeze({
|
|
271
|
+
docs: definition.docs,
|
|
272
|
+
explanation: definition.explanation,
|
|
273
|
+
headline: definition.headline,
|
|
274
|
+
identity,
|
|
275
|
+
});
|
|
276
|
+
rules.add(rule);
|
|
277
|
+
return rule;
|
|
278
|
+
}
|
|
279
|
+
/** Whether a value is a descriptor `diagnosticRule()` built. */
|
|
280
|
+
function isDiagnosticRule(value) {
|
|
281
|
+
return typeof value === 'object' && value !== null && rules.has(value);
|
|
282
|
+
}
|
|
283
|
+
export { diagnosticSections, diagnosticText, elided, isDiagnosticRule, messageWidth, quoteString, registerRule, spelled, valueCode, wrap, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DiagnosticRule } from './diagnostic-text.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declares one Developer Diagnostic rule: its identity, the headline its banner prints, the
|
|
4
|
+
* explanation that says why the rule exists, and an optional docs link. It returns a frozen
|
|
5
|
+
* descriptor that every site raising the rule shares, as `extension()` and `issueCode()` do.
|
|
6
|
+
*/
|
|
7
|
+
export declare function diagnosticRule(identity: string, definition: {
|
|
8
|
+
readonly headline: string;
|
|
9
|
+
readonly explanation: string;
|
|
10
|
+
readonly docs?: string;
|
|
11
|
+
}): DiagnosticRule;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { registerRule } from './diagnostic-text.js';
|
|
2
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
3
|
+
import { isRuleIdentity } from './identity.js';
|
|
4
|
+
import { ruleDocs, ruleIdentity, ruleProse } from './rules.js';
|
|
5
|
+
/** Whether a text holds a character other than whitespace. */
|
|
6
|
+
function isFilled(value) {
|
|
7
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
8
|
+
}
|
|
9
|
+
/** Whether a docs value is an absolute web address. */
|
|
10
|
+
function isWebAddress(value) {
|
|
11
|
+
if (!URL.canParse(value)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const { protocol } = new URL(value);
|
|
15
|
+
return protocol === 'https:' || protocol === 'http:';
|
|
16
|
+
}
|
|
17
|
+
/** The definition's own fields, read by shape, because a JavaScript caller reaches the call. */
|
|
18
|
+
function definitionOf(definition) {
|
|
19
|
+
return typeof definition === 'object' && definition !== null ? { ...definition } : {};
|
|
20
|
+
}
|
|
21
|
+
/** The finding for one `diagnosticRule()` call, marking the argument or key at fault. */
|
|
22
|
+
function callFinding(identity, definition, mark) {
|
|
23
|
+
return { arguments: [identity, definition], call: 'diagnosticRule', mark };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The definition's headline, explanation, and docs, checked. A fault marks the key at fault, or the
|
|
27
|
+
* definition itself when the author left the key out.
|
|
28
|
+
*/
|
|
29
|
+
function checkDefinition(identity, definition) {
|
|
30
|
+
const fields = definitionOf(definition);
|
|
31
|
+
const { docs, explanation, headline } = fields;
|
|
32
|
+
const part = (key) => callFinding(identity, definition, key in fields ? `1.${key}` : '1');
|
|
33
|
+
if (!isFilled(headline)) {
|
|
34
|
+
throw new DeclarationError(ruleProse, {
|
|
35
|
+
correction: 'Supply a short noun phrase.',
|
|
36
|
+
findings: [part('headline')],
|
|
37
|
+
sentence: `Diagnostic rule ${quoted(identity)} declares an empty headline.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!isFilled(explanation)) {
|
|
41
|
+
throw new DeclarationError(ruleProse, {
|
|
42
|
+
correction: 'Supply prose that says why the rule exists.',
|
|
43
|
+
findings: [part('explanation')],
|
|
44
|
+
sentence: `Diagnostic rule ${quoted(identity)} declares an empty explanation.`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (docs !== undefined && (typeof docs !== 'string' || !isWebAddress(docs))) {
|
|
48
|
+
throw new DeclarationError(ruleDocs, {
|
|
49
|
+
correction: 'Supply an absolute https URL, or omit docs.',
|
|
50
|
+
findings: [part('docs')],
|
|
51
|
+
sentence: `Diagnostic rule ${quoted(identity)} declares docs that are not a URL.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return docs === undefined ? { explanation, headline } : { docs, explanation, headline };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Declares one Developer Diagnostic rule: its identity, the headline its banner prints, the
|
|
58
|
+
* explanation that says why the rule exists, and an optional docs link. It returns a frozen
|
|
59
|
+
* descriptor that every site raising the rule shares, as `extension()` and `issueCode()` do.
|
|
60
|
+
*/
|
|
61
|
+
export function diagnosticRule(identity, definition) {
|
|
62
|
+
if (!isRuleIdentity(identity)) {
|
|
63
|
+
throw new DeclarationError(ruleIdentity, {
|
|
64
|
+
correction: 'Name it <package>[/<subpath>...]/<kebab-case-rule>, such as "@acme/retry/retry-limit".',
|
|
65
|
+
findings: [callFinding(identity, definition, '0')],
|
|
66
|
+
sentence: `Diagnostic rule ${quoted(identity)} has no package part, or a subpath or rule name that is not kebab-case.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return registerRule(identity, checkDefinition(identity, definition));
|
|
70
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { DiagnosticParts, DiagnosticRule, Finding } from './diagnostic-text.js';
|
|
3
|
+
import type { FailureExitCode } from './exit-codes.js';
|
|
2
4
|
import type { InputIdentity } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* A value a sentence quotes: raw text an operator typed, or a name or identity an author declared.
|
|
7
|
+
* Text is escaped, so a control character or a bidirectional control in it cannot reorder or break
|
|
8
|
+
* the line. A value of any other kind prints as the code a finding prints for it, because a rule
|
|
9
|
+
* that rejects a value that is not a string quotes that value too. The failure's public field
|
|
10
|
+
* keeps the raw value.
|
|
11
|
+
*/
|
|
12
|
+
export declare function quoted(value: unknown): string;
|
|
3
13
|
/**
|
|
4
14
|
* The two short-group faults. A value option that is not last in its group names that option's
|
|
5
|
-
* spelling; a group that mixes scopes names
|
|
6
|
-
*
|
|
15
|
+
* spelling; a group that mixes scopes names only the two letters that disagree, because the rest
|
|
16
|
+
* of the group may hold an inline value. `token` keeps the whole group as the reported fact.
|
|
7
17
|
*/
|
|
8
18
|
type ShortGroupFault = {
|
|
9
19
|
reason: 'value-position';
|
|
@@ -15,32 +25,66 @@ type ShortGroupFault = {
|
|
|
15
25
|
other: string;
|
|
16
26
|
};
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
* The one message a distributed build shows an operator for a defect or a declaration fault: the
|
|
29
|
+
* application name and a fixed phrase, with no reason, class name, code, or path.
|
|
30
|
+
*/
|
|
31
|
+
export declare function genericDefectText(application: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether one failure is only the author's to fix: a declaration fault or a defect. A development
|
|
34
|
+
* build shows the author its Developer Diagnostic; a distributed one shows the generic message.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isAuthorFault(failure: LoomError): failure is DeclarationError | InternalError;
|
|
37
|
+
/**
|
|
38
|
+
* Core's own text for one failure, with the trailing newline every view's text carries. The
|
|
39
|
+
* application name opens every line of a usage failure's message, one line for each problem it
|
|
40
|
+
* reports, so the operator reads who is speaking on each. A declaration fault and a defect read
|
|
41
|
+
* the generic defect message, because only the author can act on their detail, and a development
|
|
42
|
+
* build shows that detail ahead of every view. Every other class writes its message alone, even
|
|
43
|
+
* one that declares a usage error's exit code. It is the default view of every failure class and
|
|
44
|
+
* the text the plain fallback path writes, so it runs no application code and nothing downstream
|
|
45
|
+
* composes its newline.
|
|
23
46
|
*/
|
|
24
|
-
export declare function defaultText(failure: LoomError): string;
|
|
47
|
+
export declare function defaultText(failure: LoomError, application: string): string;
|
|
25
48
|
/** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
|
|
26
49
|
export declare function commandSubject(name: string | null): string;
|
|
27
50
|
/** The routed path names the Command a sentence speaks of; an empty path is the root. */
|
|
28
51
|
export declare function routedSubject(command: readonly string[]): string;
|
|
29
52
|
/** The same subject at the start of a sentence. */
|
|
30
53
|
export declare function commandSentence(name: string | null): string;
|
|
54
|
+
/**
|
|
55
|
+
* The code a failure exits with. A value that inherits from a failure class without having been
|
|
56
|
+
* constructed holds none, and `toFailure` reports it as an internal error, so it reads 1.
|
|
57
|
+
*/
|
|
58
|
+
export declare function exitCodeOf(failure: LoomError): FailureExitCode;
|
|
31
59
|
/**
|
|
32
60
|
* Every failure `run()` reports is an instance of a public class. Each class carries the facts its
|
|
33
|
-
* sentence interpolates, so a view reads them instead of parsing prose
|
|
34
|
-
*
|
|
35
|
-
*
|
|
61
|
+
* sentence interpolates, so a view reads them instead of parsing prose. The exit code is a static
|
|
62
|
+
* field the class declares, read from the nearest ancestor that declares one and captured at the
|
|
63
|
+
* class's first construction, so one class exits with one code and a projection reads it without
|
|
64
|
+
* an instance. The instance reports the same value through a read-only accessor, and no subclass
|
|
65
|
+
* property or assignment changes the code `run()` resolves. `message` never carries a category
|
|
66
|
+
* prefix; the default views add it.
|
|
36
67
|
*/
|
|
37
68
|
export declare abstract class LoomError extends Error {
|
|
38
|
-
readonly exitCode:
|
|
39
|
-
|
|
69
|
+
static readonly exitCode: FailureExitCode;
|
|
70
|
+
/**
|
|
71
|
+
* Reads the constructed class's code, captured at its first construction. A code outside 1
|
|
72
|
+
* through 125 throws a `DeclarationError` in place of the failure and captures nothing, because
|
|
73
|
+
* core never clamps or replaces a code. `options` is the platform's own, so a failure that
|
|
74
|
+
* replaces another error keeps it as `cause` only when its author passes one.
|
|
75
|
+
*/
|
|
76
|
+
constructor(message: string, options?: ErrorOptions);
|
|
77
|
+
/**
|
|
78
|
+
* The code this failure exits with. An accessor without a setter, so a TypeScript subclass cannot
|
|
79
|
+
* declare it as a property, and an assignment throws in strict mode code and is ignored in sloppy
|
|
80
|
+
* mode code.
|
|
81
|
+
*/
|
|
82
|
+
get exitCode(): FailureExitCode;
|
|
40
83
|
}
|
|
41
84
|
/** Exit 2: the invocation, not the application, is wrong. */
|
|
42
85
|
export declare abstract class UsageError extends LoomError {
|
|
43
|
-
|
|
86
|
+
static readonly exitCode: FailureExitCode;
|
|
87
|
+
constructor(message: string, options?: ErrorOptions);
|
|
44
88
|
}
|
|
45
89
|
/** One input the validation phase rejected: an omission, or a value its schema refused. */
|
|
46
90
|
export type InputProblem = {
|
|
@@ -56,7 +100,7 @@ export type InputProblem = {
|
|
|
56
100
|
/** The whole validation phase in authoring order, so one failure reports every rejected input. */
|
|
57
101
|
export declare class InputError extends UsageError {
|
|
58
102
|
readonly problems: readonly InputProblem[];
|
|
59
|
-
constructor(message: string, problems: readonly InputProblem[]);
|
|
103
|
+
constructor(message: string, problems: readonly InputProblem[], options?: ErrorOptions);
|
|
60
104
|
}
|
|
61
105
|
export declare class UnknownCommandError extends UsageError {
|
|
62
106
|
readonly token: string;
|
|
@@ -98,17 +142,41 @@ export declare class ShortGroupError extends UsageError {
|
|
|
98
142
|
readonly reason: 'value-position' | 'mixed-scope';
|
|
99
143
|
constructor(fault: ShortGroupFault);
|
|
100
144
|
}
|
|
101
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Exit 1: the declaration is wrong, so the author reads its Developer Diagnostic. A rule and the
|
|
147
|
+
* fault's own parts build it, or a sentence alone does for a fault with no rule. `message` holds
|
|
148
|
+
* the whole diagnostic as plain text at 80 columns, so a fault thrown at an authoring call prints
|
|
149
|
+
* it through the runtime's own uncaught-error output, and `sentence` holds the sentence alone.
|
|
150
|
+
*/
|
|
102
151
|
export declare class DeclarationError extends LoomError {
|
|
103
|
-
|
|
152
|
+
readonly rule: DiagnosticRule | undefined;
|
|
153
|
+
readonly sentence: string;
|
|
154
|
+
readonly findings: readonly Finding[];
|
|
155
|
+
readonly correction: string | readonly string[] | undefined;
|
|
156
|
+
constructor(rule: DiagnosticRule, parts: DiagnosticParts, options?: ErrorOptions);
|
|
157
|
+
constructor(sentence: string, options?: ErrorOptions);
|
|
104
158
|
}
|
|
105
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Exit 1: the application ended the invocation itself. An application may subclass it, and the
|
|
161
|
+
* subclass may declare its own exit code.
|
|
162
|
+
*/
|
|
106
163
|
export declare class FatalError extends LoomError {
|
|
107
|
-
constructor(message: string);
|
|
164
|
+
constructor(message: string, options?: ErrorOptions);
|
|
108
165
|
}
|
|
109
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Exit 1: a defect, such as an unexpected exception, a non-error throw, or a view that could not
|
|
168
|
+
* answer. A rule and the defect's own parts build it, or a sentence and the thrown value do for a
|
|
169
|
+
* defect with no rule. `message` stays the sentence, because only `run()` reports a defect, and a
|
|
170
|
+
* development build renders its Developer Diagnostic from the parts.
|
|
171
|
+
*/
|
|
110
172
|
export declare class InternalError extends LoomError {
|
|
111
173
|
readonly cause: unknown;
|
|
174
|
+
readonly rule: DiagnosticRule | undefined;
|
|
175
|
+
readonly sentence: string;
|
|
176
|
+
readonly correction: string | readonly string[] | undefined;
|
|
177
|
+
constructor(rule: DiagnosticRule, parts: Omit<DiagnosticParts, 'findings'> & {
|
|
178
|
+
readonly cause: unknown;
|
|
179
|
+
});
|
|
112
180
|
constructor(message: string, cause: unknown);
|
|
113
181
|
}
|
|
114
182
|
/** The five ways the results lane is broken, each named where core meets it. */
|
|
@@ -130,14 +198,30 @@ export declare class ResultError extends InternalError {
|
|
|
130
198
|
* with or without a full stop, so a diagnostic supplies one only where the message carries none.
|
|
131
199
|
*/
|
|
132
200
|
export declare function asSentence(text: string): string;
|
|
133
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* What a diagnostic says about an unexpected value, whether or not it was an Error, with every
|
|
203
|
+
* control character escaped. A `DeclarationError` answers its sentence, because its message holds
|
|
204
|
+
* its whole diagnostic. Every sentence that quotes a thrown value reads it here, so a reason stays
|
|
205
|
+
* on one line and no bidirectional control reaches a terminal, whichever rule's sentence carries
|
|
206
|
+
* it, while the author's words around it keep their line breaks. Reading it never throws: an Error
|
|
207
|
+
* whose message is not a string or cannot be read, and a value whose prototype cannot be read, such
|
|
208
|
+
* as a proxy whose trap throws, answer one fixed sentence.
|
|
209
|
+
*/
|
|
134
210
|
export declare function reasonOf(thrown: unknown): string;
|
|
135
211
|
/**
|
|
136
212
|
* Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
|
|
137
|
-
* a non-string return like any other
|
|
138
|
-
* unobserved rejection would end the process before the invocation could report anything.
|
|
213
|
+
* a non-string return like any other: it receives a rejection handler and is otherwise ignored.
|
|
139
214
|
*/
|
|
140
215
|
export declare function notTextReason(value: unknown): string;
|
|
141
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Every thrown value reaches reporting as a failure class; anything else is internal. A value that
|
|
218
|
+
* inherits from a failure class without having been constructed holds no code, so it is internal
|
|
219
|
+
* too.
|
|
220
|
+
*/
|
|
142
221
|
export declare function toFailure(thrown: unknown): LoomError;
|
|
222
|
+
/**
|
|
223
|
+
* The defect a foreign throw reports: its reason as the sentence, and the thrown value as the
|
|
224
|
+
* cause a development build's diagnostic shows.
|
|
225
|
+
*/
|
|
226
|
+
export declare function foreignFailure(thrown: unknown): InternalError;
|
|
143
227
|
export {};
|