@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,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text with every control character, line separator, and bidirectional control replaced by its
|
|
3
|
+
* four-digit lowercase `\uXXXX` escape, so the text stays on one line, no control character
|
|
4
|
+
* reaches a terminal, and no bidirectional control reorders the rest of the line. Raw text a
|
|
5
|
+
* diagnostic quotes, such as a file path, a typed token, or a reason a plugin threw, is escaped
|
|
6
|
+
* with this before it is written.
|
|
7
|
+
*/
|
|
8
|
+
export declare function escapeControlCharacters(text: string): string;
|
package/dist/controls.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The radix and digit count a `\uXXXX` escape always uses. */
|
|
2
|
+
const hexRadix = 16;
|
|
3
|
+
const hexDigitCount = 4;
|
|
4
|
+
/** Each replaced character is exactly one UTF-16 code unit, so its code always sits at index 0. */
|
|
5
|
+
const soleCodeUnit = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Every character a quoted diagnostic escapes: the control characters U+0000 through U+001F and
|
|
8
|
+
* U+007F through U+009F, the separators U+2028 and U+2029, the bidirectional embedding, override,
|
|
9
|
+
* and isolate controls U+202A through U+202E and U+2066 through U+2069, and the marks U+200E,
|
|
10
|
+
* U+200F, and U+061C. Other format characters, such as a zero-width joiner inside an emoji or a
|
|
11
|
+
* soft hyphen, are ordinary text and stay.
|
|
12
|
+
*/
|
|
13
|
+
const escaped = /[\p{Cc}\p{Zl}\p{Zp}\u{202a}-\u{202e}\u{2066}-\u{2069}\u{200e}\u{200f}\u{61c}]/gu;
|
|
14
|
+
/**
|
|
15
|
+
* The text with every control character, line separator, and bidirectional control replaced by its
|
|
16
|
+
* four-digit lowercase `\uXXXX` escape, so the text stays on one line, no control character
|
|
17
|
+
* reaches a terminal, and no bidirectional control reorders the rest of the line. Raw text a
|
|
18
|
+
* diagnostic quotes, such as a file path, a typed token, or a reason a plugin threw, is escaped
|
|
19
|
+
* with this before it is written.
|
|
20
|
+
*/
|
|
21
|
+
export function escapeControlCharacters(text) {
|
|
22
|
+
return text.replaceAll(escaped, (character) => `\\u${character.charCodeAt(soleCodeUnit).toString(hexRadix).padStart(hexDigitCount, '0')}`);
|
|
23
|
+
}
|
package/dist/defect.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a development build reads a defect's source through: the working directory, the roots a
|
|
3
|
+
* frame may lie under, and a reader. The roots are `cwd` and the path it resolves to through
|
|
4
|
+
* symbolic links, because a runtime names a module by its resolved path.
|
|
5
|
+
*/
|
|
6
|
+
interface SourceAccess {
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly roots: readonly string[];
|
|
9
|
+
readonly readSource: ((path: string, cwd: string) => string | undefined) | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A defect's findings in a development build: the author's source around the failing frame, read
|
|
13
|
+
* only here, and the cause chain. A thrown value that is not an Error prints as a value, and a
|
|
14
|
+
* defect with no cause shows none.
|
|
15
|
+
*/
|
|
16
|
+
declare function defectEvidence(cause: unknown, access: SourceAccess): string[];
|
|
17
|
+
export type { SourceAccess };
|
|
18
|
+
export { defectEvidence };
|
package/dist/defect.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { escapeControlCharacters } from './controls.js';
|
|
4
|
+
import { valueCode } from './diagnostic-text.js';
|
|
5
|
+
import { DeclarationError } from './errors.js';
|
|
6
|
+
/** How many causes a chain prints before it stops, so a chain a getter grows cannot run forever. */
|
|
7
|
+
const causeLimit = 8;
|
|
8
|
+
/** How many lines a source excerpt shows on each side of the failing line. */
|
|
9
|
+
const context = 2;
|
|
10
|
+
/** A frame's location, `file:line:column`, where the file may hold spaces and parentheses. */
|
|
11
|
+
const frameLocation = /^(?<file>.+):(?<line>\d+):(?<column>\d+)$/u;
|
|
12
|
+
/** Every line terminator a JavaScript engine counts, in a message, a stack, or a source file. */
|
|
13
|
+
const lineBreak = /\r\n|[\n\r\u2028\u2029]/u;
|
|
14
|
+
/** A value read through a property, or `undefined` when reading it throws. */
|
|
15
|
+
function readField(value, key) {
|
|
16
|
+
try {
|
|
17
|
+
return Reflect.get(value, key);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** A field that must be a string, read defensively. */
|
|
24
|
+
function readText(value, key) {
|
|
25
|
+
const field = readField(value, key);
|
|
26
|
+
return typeof field === 'string' ? field : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Whether a value is an Error, read defensively, because a proxy's prototype trap can throw. */
|
|
29
|
+
function isError(value) {
|
|
30
|
+
try {
|
|
31
|
+
return value instanceof Error;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What one cause says about itself on its heading line. A `DeclarationError` says its sentence,
|
|
39
|
+
* because its message holds its whole diagnostic.
|
|
40
|
+
*/
|
|
41
|
+
function causeReason(cause) {
|
|
42
|
+
try {
|
|
43
|
+
return cause instanceof DeclarationError ? cause.sentence : readText(cause, 'message');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return readText(cause, 'message');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The part of a stack after its header. A runtime opens a stack with the error's name and message,
|
|
51
|
+
* and a message can hold a line that reads as a frame, so the header is cut off whole: by its text
|
|
52
|
+
* when the stack opens with it, and otherwise by as many lines as the message holds.
|
|
53
|
+
*/
|
|
54
|
+
function stackBody(cause) {
|
|
55
|
+
const stack = readText(cause, 'stack') ?? '';
|
|
56
|
+
const name = readText(cause, 'name') ?? 'Error';
|
|
57
|
+
const message = readText(cause, 'message') ?? '';
|
|
58
|
+
const header = message === '' ? name : `${name}: ${message}`;
|
|
59
|
+
if (stack.startsWith(header)) {
|
|
60
|
+
return stack.slice(header.length).split(lineBreak);
|
|
61
|
+
}
|
|
62
|
+
return stack.split(lineBreak).slice(message.split(lineBreak).length);
|
|
63
|
+
}
|
|
64
|
+
/** The frame lines of one cause's stack: each line after the header that opens with `at`, trimmed. */
|
|
65
|
+
function frameLines(cause) {
|
|
66
|
+
return stackBody(cause)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter((line) => line.startsWith('at '));
|
|
69
|
+
}
|
|
70
|
+
/** The file a frame names as a path, converting a `file:` URL, or `undefined` for one that fails. */
|
|
71
|
+
function framePath(file) {
|
|
72
|
+
if (!file.startsWith('file:')) {
|
|
73
|
+
return file;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return fileURLToPath(file);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The location one frame line names: the text between the first ` (` and the closing parenthesis
|
|
84
|
+
* when the line has both, and otherwise everything after `at` and an optional `async`.
|
|
85
|
+
*/
|
|
86
|
+
function frameText(line) {
|
|
87
|
+
const rest = line.replace(/^at (?:async )?/u, '');
|
|
88
|
+
const open = rest.indexOf(' (');
|
|
89
|
+
return open !== -1 && rest.endsWith(')') ? rest.slice(open + 2, -1) : rest;
|
|
90
|
+
}
|
|
91
|
+
/** One frame line's location, or `undefined` for a line that names none. */
|
|
92
|
+
function parseFrame(line) {
|
|
93
|
+
const groups = frameLocation.exec(frameText(line))?.groups;
|
|
94
|
+
const file = groups?.file === undefined ? undefined : framePath(groups.file);
|
|
95
|
+
if (file === undefined || groups?.line === undefined || groups.column === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return { column: Number(groups.column), file, line: Number(groups.line) };
|
|
99
|
+
}
|
|
100
|
+
/** A frame's file relative to one root, or `undefined` when the file does not lie under it. */
|
|
101
|
+
function within(file, root) {
|
|
102
|
+
const inside = relative(resolve(root), resolve(file));
|
|
103
|
+
return inside === '' || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)
|
|
104
|
+
? undefined
|
|
105
|
+
: inside;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A frame's file relative to the first root it lies under, when it may be read: an absolute path
|
|
109
|
+
* whose normalized form lies under a root and outside any `node_modules` directory. A thrown
|
|
110
|
+
* value's stack can be forged, so this is checked before any read, and the reader checks it again
|
|
111
|
+
* past symbolic links. It answers `undefined` for a file that does not qualify.
|
|
112
|
+
*/
|
|
113
|
+
function qualified(file, roots) {
|
|
114
|
+
if (!isAbsolute(file) || resolve(file).split(sep).includes('node_modules')) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
for (const root of roots) {
|
|
118
|
+
const inside = within(file, root);
|
|
119
|
+
if (inside !== undefined) {
|
|
120
|
+
return inside;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/** A frame's location as the diagnostic prints it: relative to the working directory when under it. */
|
|
126
|
+
function locationText(frame, roots) {
|
|
127
|
+
const file = qualified(frame.file, roots) ?? frame.file;
|
|
128
|
+
return escapeControlCharacters(`${file}:${String(frame.line)}:${String(frame.column)}`);
|
|
129
|
+
}
|
|
130
|
+
/** The source one reader answers for a file, or `undefined` when it answers none or throws. */
|
|
131
|
+
function readFile(access, file) {
|
|
132
|
+
try {
|
|
133
|
+
const text = access.readSource?.(resolve(file), access.cwd);
|
|
134
|
+
return typeof text === 'string' ? text : undefined;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One numbered source line, marked when it is the failing line, with a caret under the frame's
|
|
142
|
+
* column below it. A tab reads as one space, so the caret stays under the column the frame names.
|
|
143
|
+
*/
|
|
144
|
+
function numberedLine(source, number, frame) {
|
|
145
|
+
const text = escapeControlCharacters(source.replaceAll('\t', ' '));
|
|
146
|
+
const failing = number === frame.line;
|
|
147
|
+
const line = `${failing ? '>' : ' '} ${String(number).padStart(frame.digits)} | ${text}`.trimEnd();
|
|
148
|
+
if (!failing) {
|
|
149
|
+
return [line];
|
|
150
|
+
}
|
|
151
|
+
return [line, ` ${' '.repeat(frame.digits)} | ${' '.repeat(Math.max(0, frame.column - 1))}^`];
|
|
152
|
+
}
|
|
153
|
+
/** The lines around one frame, numbered, with the failing line marked, or none past the file's end. */
|
|
154
|
+
function excerpt(text, frame) {
|
|
155
|
+
// A JavaScript engine counts every line terminator, so a frame's line number counts them all.
|
|
156
|
+
const lines = text.split(lineBreak);
|
|
157
|
+
if (frame.line < 1 || frame.line > lines.length) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const first = Math.max(1, frame.line - context);
|
|
161
|
+
const shown = lines.slice(first - 1, Math.min(lines.length, frame.line + context));
|
|
162
|
+
const digits = String(first + shown.length - 1).length;
|
|
163
|
+
return shown
|
|
164
|
+
.flatMap((source, offset) => numberedLine(source, first + offset, { ...frame, digits }))
|
|
165
|
+
.join('\n');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The errors an `AggregateError` holds, in order, read defensively, or none for any other Error. A
|
|
169
|
+
* broken translator's defect holds the translator's throw and then the original throw this way.
|
|
170
|
+
*/
|
|
171
|
+
function heldErrors(cause) {
|
|
172
|
+
try {
|
|
173
|
+
if (!(cause instanceof AggregateError)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const errors = readField(cause, 'errors');
|
|
181
|
+
return Array.isArray(errors) ? errors : [];
|
|
182
|
+
}
|
|
183
|
+
/** Every frame one cause's stack names, in order, skipping a line that names no location. */
|
|
184
|
+
function framesOf(cause) {
|
|
185
|
+
return frameLines(cause).flatMap((line) => parseFrame(line) ?? []);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The author's source for a defect: the first frame that lies under the working directory and
|
|
189
|
+
* outside `node_modules`, read from the outermost cause's stack, or, for an `AggregateError`, from
|
|
190
|
+
* the stacks of the errors it holds in order and then its own, because what broke is what it
|
|
191
|
+
* holds. A read that answers nothing, and stacks with no qualifying frame, leave the outermost
|
|
192
|
+
* cause's first frame's location alone.
|
|
193
|
+
*/
|
|
194
|
+
function sourceSection(cause, access) {
|
|
195
|
+
const stacks = [...heldErrors(cause).filter(isError), cause];
|
|
196
|
+
const frame = stacks
|
|
197
|
+
.flatMap((stack) => framesOf(stack))
|
|
198
|
+
.find((candidate) => qualified(candidate.file, access.roots) !== undefined);
|
|
199
|
+
if (frame === undefined) {
|
|
200
|
+
const [located] = framesOf(cause);
|
|
201
|
+
return located === undefined ? undefined : locationText(located, access.roots);
|
|
202
|
+
}
|
|
203
|
+
const text = readFile(access, frame.file);
|
|
204
|
+
const lines = text === undefined ? undefined : excerpt(text, frame);
|
|
205
|
+
const location = locationText(frame, access.roots);
|
|
206
|
+
return lines === undefined ? location : `${location}\n\n${lines}`;
|
|
207
|
+
}
|
|
208
|
+
/** One cause as its name, its escaped message, or a declaration fault's sentence, and its frames. */
|
|
209
|
+
function causeLines(cause) {
|
|
210
|
+
const name = readText(cause, 'name') ?? 'Error';
|
|
211
|
+
const reason = causeReason(cause);
|
|
212
|
+
const heading = reason === undefined || reason === '' ? name : `${name}: ${reason}`;
|
|
213
|
+
return [
|
|
214
|
+
escapeControlCharacters(heading),
|
|
215
|
+
...frameLines(cause).map((line) => ` ${escapeControlCharacters(line)}`),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* One link of a cause chain under `prefix`, then each error it holds as an `AggregateError`, each
|
|
220
|
+
* with its own chain under `Holds`. It answers the link's own cause, or `undefined` for a link that
|
|
221
|
+
* is not an Error, which prints as a value.
|
|
222
|
+
*/
|
|
223
|
+
function appendLink(cause, prefix, walk) {
|
|
224
|
+
walk.seen.add(cause);
|
|
225
|
+
if (!isError(cause)) {
|
|
226
|
+
walk.lines.push(`${prefix}${valueCode(cause)}`);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const [heading = '', ...frames] = causeLines(cause);
|
|
230
|
+
walk.lines.push(`${prefix}${heading}`, ...frames);
|
|
231
|
+
for (const held of heldErrors(cause)) {
|
|
232
|
+
appendChain(held, 'Holds ', walk);
|
|
233
|
+
}
|
|
234
|
+
return readField(cause, 'cause');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* One chain from `first`, the first link under `opening` and each later one under `Caused by`,
|
|
238
|
+
* following `cause` links until one is absent, repeats, or passes the limit.
|
|
239
|
+
*/
|
|
240
|
+
function appendChain(first, opening, walk) {
|
|
241
|
+
let cause = first;
|
|
242
|
+
let prefix = opening;
|
|
243
|
+
while (cause !== undefined && !walk.seen.has(cause) && walk.seen.size < causeLimit) {
|
|
244
|
+
cause = appendLink(cause, prefix, walk);
|
|
245
|
+
prefix = 'Caused by ';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The cause chain: each cause's name and escaped message and its stack, and each error an
|
|
250
|
+
* `AggregateError` holds, until the chain ends, repeats, or passes the limit.
|
|
251
|
+
*/
|
|
252
|
+
function causeChain(outermost) {
|
|
253
|
+
const walk = { lines: [], seen: new Set() };
|
|
254
|
+
appendChain(outermost, '', walk);
|
|
255
|
+
return walk.lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* A defect's findings in a development build: the author's source around the failing frame, read
|
|
259
|
+
* only here, and the cause chain. A thrown value that is not an Error prints as a value, and a
|
|
260
|
+
* defect with no cause shows none.
|
|
261
|
+
*/
|
|
262
|
+
function defectEvidence(cause, access) {
|
|
263
|
+
if (cause === undefined) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
if (!isError(cause)) {
|
|
267
|
+
return [`Thrown value: ${valueCode(cause)}`];
|
|
268
|
+
}
|
|
269
|
+
const source = sourceSection(cause, access);
|
|
270
|
+
return [...(source === undefined ? [] : [source]), causeChain(cause)];
|
|
271
|
+
}
|
|
272
|
+
export { defectEvidence };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DeclarationError } from './errors.js';
|
|
2
|
+
import type { InternalError } from './errors.js';
|
|
3
|
+
import type { ContextualStyle } from './style.js';
|
|
4
|
+
import type { Host } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Where a development build's diagnostics print: the application name a finding's path opens with,
|
|
7
|
+
* the stderr width, and the working directory and reader a defect's source comes through.
|
|
8
|
+
*/
|
|
9
|
+
interface DeveloperScene {
|
|
10
|
+
readonly application: string;
|
|
11
|
+
readonly host: Pick<Host, 'cwd' | 'readSource' | 'terminal'>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One fault's Developer Diagnostic as marked text for stderr, the banner in the error style, and
|
|
15
|
+
* the hints printed under it after one blank line. It is not a view, so no override replaces it.
|
|
16
|
+
*/
|
|
17
|
+
declare function developerText(fault: DeclarationError | InternalError, scene: DeveloperScene & {
|
|
18
|
+
readonly hints: readonly string[];
|
|
19
|
+
readonly style: ContextualStyle;
|
|
20
|
+
}): string;
|
|
21
|
+
/** One fault's Developer Diagnostic as plain text, for the plain fallback path. */
|
|
22
|
+
declare function developerPlainText(fault: DeclarationError | InternalError, scene: DeveloperScene): string;
|
|
23
|
+
export type { DeveloperScene };
|
|
24
|
+
export { developerPlainText, developerText };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defectEvidence } from './defect.js';
|
|
2
|
+
import { diagnosticSections, messageWidth } from './diagnostic-text.js';
|
|
3
|
+
import { DeclarationError } from './errors.js';
|
|
4
|
+
import { sourceRoots } from './host.js';
|
|
5
|
+
/** What a defect's source is read through: the host's working directory, its roots, and reader. */
|
|
6
|
+
function sourceAccess({ cwd, readSource }) {
|
|
7
|
+
return { cwd, readSource, roots: sourceRoots(cwd) };
|
|
8
|
+
}
|
|
9
|
+
/** The parts one fault renders, with a defect's source and causes read here, in development alone. */
|
|
10
|
+
function anatomyOf(fault, host) {
|
|
11
|
+
if (fault instanceof DeclarationError) {
|
|
12
|
+
return {
|
|
13
|
+
correction: fault.correction,
|
|
14
|
+
evidence: [],
|
|
15
|
+
fallback: 'INVALID DECLARATION',
|
|
16
|
+
findings: fault.findings,
|
|
17
|
+
rule: fault.rule,
|
|
18
|
+
sentence: fault.sentence,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
correction: fault.correction,
|
|
23
|
+
evidence: defectEvidence(fault.cause, sourceAccess(host)),
|
|
24
|
+
fallback: 'DEFECT',
|
|
25
|
+
findings: [],
|
|
26
|
+
rule: fault.rule,
|
|
27
|
+
sentence: fault.sentence,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The banner and body of one fault's diagnostic, laid out for this run's stderr. */
|
|
31
|
+
function sections(fault, scene) {
|
|
32
|
+
return diagnosticSections(anatomyOf(fault, scene.host), {
|
|
33
|
+
application: scene.application,
|
|
34
|
+
width: scene.host.terminal.stderr.columns ?? messageWidth,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One fault's Developer Diagnostic as marked text for stderr, the banner in the error style, and
|
|
39
|
+
* the hints printed under it after one blank line. It is not a view, so no override replaces it.
|
|
40
|
+
*/
|
|
41
|
+
function developerText(fault, scene) {
|
|
42
|
+
const { banner, body } = sections(fault, scene);
|
|
43
|
+
const { hints, style } = scene;
|
|
44
|
+
const hinted = hints.length === 0 ? '' : `\n${hints.map((hint) => `${hint}\n`).join('')}`;
|
|
45
|
+
return `${style.error(style.escape(banner))}\n\n${style.escape(body)}\n${hinted}`;
|
|
46
|
+
}
|
|
47
|
+
/** One fault's Developer Diagnostic as plain text, for the plain fallback path. */
|
|
48
|
+
function developerPlainText(fault, scene) {
|
|
49
|
+
const { banner, body } = sections(fault, scene);
|
|
50
|
+
return `${banner}\n\n${body}\n`;
|
|
51
|
+
}
|
|
52
|
+
export { developerPlainText, developerText };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reason a declaration can be wrong, or one kind of defect: the parts that hold at every site
|
|
3
|
+
* that raises it. `diagnosticRule()` builds and freezes it.
|
|
4
|
+
*/
|
|
5
|
+
interface DiagnosticRule {
|
|
6
|
+
readonly identity: string;
|
|
7
|
+
readonly headline: string;
|
|
8
|
+
readonly explanation: string;
|
|
9
|
+
readonly docs: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One declaration a fault points at, rebuilt from the facts core holds: the Command it sits on, the
|
|
13
|
+
* authoring call, and the arguments that call received. `mark` is a dotted path into `arguments`
|
|
14
|
+
* that the diagnostic underlines, and `note` prints beside the marks.
|
|
15
|
+
*/
|
|
16
|
+
interface Finding {
|
|
17
|
+
readonly path?: readonly string[];
|
|
18
|
+
readonly call: string;
|
|
19
|
+
readonly arguments: readonly unknown[];
|
|
20
|
+
readonly mark?: string;
|
|
21
|
+
readonly note?: string;
|
|
22
|
+
}
|
|
23
|
+
/** The parts of one fault that differ at each site that raises its rule. */
|
|
24
|
+
interface DiagnosticParts {
|
|
25
|
+
readonly sentence: string;
|
|
26
|
+
readonly findings?: readonly Finding[];
|
|
27
|
+
readonly correction?: string | readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/** Everything one Developer Diagnostic prints, whichever class carries it. */
|
|
30
|
+
interface Anatomy {
|
|
31
|
+
readonly rule: DiagnosticRule | undefined;
|
|
32
|
+
/** The banner a fault with no rule prints. */
|
|
33
|
+
readonly fallback: 'INVALID DECLARATION' | 'DEFECT';
|
|
34
|
+
readonly sentence: string;
|
|
35
|
+
readonly findings: readonly Finding[];
|
|
36
|
+
/** A defect's own findings, already rendered: the author's source and the cause chain. */
|
|
37
|
+
readonly evidence: readonly string[];
|
|
38
|
+
readonly correction: string | readonly string[] | undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Where one diagnostic is printed: the column count its banner fills and its prose wraps to, and
|
|
42
|
+
* the application name a finding's path opens with when `run()` reports the fault.
|
|
43
|
+
*/
|
|
44
|
+
interface Layout {
|
|
45
|
+
readonly width: number;
|
|
46
|
+
readonly application?: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The width a diagnostic takes in a thrown error's message, or when the stderr width is unknown. */
|
|
49
|
+
declare const messageWidth = 80;
|
|
50
|
+
/** The stand-in for a value a finding does not spell out: a function, a validator, or a cycle. */
|
|
51
|
+
declare const elided = "\u2026";
|
|
52
|
+
/** A string as a single-quoted JavaScript literal, with every control escaped. */
|
|
53
|
+
declare function quoteString(text: string): string;
|
|
54
|
+
/** A finding argument that prints as the code given, which the caller has already escaped. */
|
|
55
|
+
declare function spelled(code: string): object;
|
|
56
|
+
/** One value as the JavaScript a finding prints for it. */
|
|
57
|
+
declare function valueCode(value: unknown): string;
|
|
58
|
+
/** Prose wrapped to the width at spaces. A word wider than the width keeps a line of its own. */
|
|
59
|
+
declare function wrap(text: string, width: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* One Developer Diagnostic as plain text with no trailing newline: the banner, the sentence, the
|
|
62
|
+
* findings, the explanation, the correction, and the docs link, separated by one blank line, each
|
|
63
|
+
* left out when the fault does not carry it. The banner is returned apart, so a caller that styles
|
|
64
|
+
* it can.
|
|
65
|
+
*/
|
|
66
|
+
declare function diagnosticSections(anatomy: Anatomy, layout: Layout): {
|
|
67
|
+
banner: string;
|
|
68
|
+
body: string;
|
|
69
|
+
};
|
|
70
|
+
/** The whole diagnostic as the plain text a thrown fault's message holds. */
|
|
71
|
+
declare function diagnosticText(anatomy: Anatomy, layout?: Layout): string;
|
|
72
|
+
/** One frozen descriptor from parts already checked, which every site raising it shares. */
|
|
73
|
+
declare function registerRule(identity: string, definition: {
|
|
74
|
+
readonly headline: string;
|
|
75
|
+
readonly explanation: string;
|
|
76
|
+
readonly docs?: string;
|
|
77
|
+
}): DiagnosticRule;
|
|
78
|
+
/** Whether a value is a descriptor `diagnosticRule()` built. */
|
|
79
|
+
declare function isDiagnosticRule(value: unknown): value is DiagnosticRule;
|
|
80
|
+
export type { Anatomy, DiagnosticParts, DiagnosticRule, Finding, Layout };
|
|
81
|
+
export { diagnosticSections, diagnosticText, elided, isDiagnosticRule, messageWidth, quoteString, registerRule, spelled, valueCode, wrap, };
|