@vmz/diagnostic 0.2.3 → 0.2.5
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/README.md +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -5
- package/dist/pretty.d.ts +5 -0
- package/dist/pretty.js +80 -0
- package/dist/utf8.d.ts +15 -0
- package/dist/utf8.js +56 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Diagnostic layout for structured `code + args + span` rows.
|
|
|
4
4
|
|
|
5
5
|
- Caller injects `t` / catalog (official tables live in `@vmz/vmz`)
|
|
6
6
|
- No built-in language packs; no separate i18n package
|
|
7
|
-
-
|
|
7
|
+
- Pretty printer: rustc/miette-style snippets when `sourceText` + `span` + `position` are wired
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
import { formatDiagnostic } from '@vmz/diagnostic';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@vmz/diagnostic` — catalog render + pretty printer
|
|
2
|
+
* `@vmz/diagnostic` — catalog render + pretty printer.
|
|
3
3
|
*
|
|
4
4
|
* Product path: N-API diagnostic wire → `t(code, args)` → layout.
|
|
5
5
|
*/
|
|
@@ -37,11 +37,14 @@ export type PositionContext = {
|
|
|
37
37
|
column: number;
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
|
+
export type PrettyStyle = 'compact' | 'pretty';
|
|
40
41
|
export type FormatOptions = {
|
|
41
42
|
locale: string;
|
|
42
43
|
catalog: LocaleCatalog | ((locale: string) => LocaleCatalog);
|
|
43
44
|
sourceText?: string;
|
|
44
45
|
position?: PositionContext;
|
|
46
|
+
/** `pretty` (default) renders rustc/miette-style snippets when span + source are available. */
|
|
47
|
+
style?: PrettyStyle;
|
|
45
48
|
};
|
|
46
49
|
/**
|
|
47
50
|
* Resolve a catalog template with `{arg}` substitution.
|
|
@@ -50,7 +53,6 @@ export type FormatOptions = {
|
|
|
50
53
|
export declare function t(code: string, args: Record<string, string> | undefined, catalog: LocaleCatalog): string;
|
|
51
54
|
/**
|
|
52
55
|
* Format one diagnostic for terminal / logs.
|
|
53
|
-
* Skeleton: single-line `path: severity[code]: message` — no snippet yet.
|
|
54
56
|
*/
|
|
55
57
|
export declare function formatDiagnostic(d: DiagnosticInput, opts: FormatOptions): string;
|
|
56
58
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@vmz/diagnostic` — catalog render + pretty printer
|
|
2
|
+
* `@vmz/diagnostic` — catalog render + pretty printer.
|
|
3
3
|
*
|
|
4
4
|
* Product path: N-API diagnostic wire → `t(code, args)` → layout.
|
|
5
5
|
*/
|
|
6
|
+
import { formatDiagnosticPretty } from './pretty.js';
|
|
6
7
|
/**
|
|
7
8
|
* Resolve a catalog template with `{arg}` substitution.
|
|
8
9
|
* Missing keys fall back to `{{code}}` so wire identity stays visible.
|
|
@@ -24,7 +25,6 @@ function resolveCatalog(locale, catalog) {
|
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Format one diagnostic for terminal / logs.
|
|
27
|
-
* Skeleton: single-line `path: severity[code]: message` — no snippet yet.
|
|
28
28
|
*/
|
|
29
29
|
export function formatDiagnostic(d, opts) {
|
|
30
30
|
if (!d.code || typeof d.code !== 'string') {
|
|
@@ -36,9 +36,12 @@ export function formatDiagnostic(d, opts) {
|
|
|
36
36
|
: d.message != null && String(d.message).length
|
|
37
37
|
? String(d.message)
|
|
38
38
|
: t(d.code, d.args, catalog);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if ((opts.style ?? 'pretty') === 'compact') {
|
|
40
|
+
const where = formatWhere(d, opts);
|
|
41
|
+
const head = where ? `${where}: ` : '';
|
|
42
|
+
return `${head}${d.severity}[${d.code}]: ${message}`;
|
|
43
|
+
}
|
|
44
|
+
return formatDiagnosticPretty(d, message, opts);
|
|
42
45
|
}
|
|
43
46
|
/**
|
|
44
47
|
* Format many diagnostics, one per line.
|
package/dist/pretty.d.ts
ADDED
package/dist/pretty.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { scalarColumnToIndex, utf8LineAt } from './utf8.js';
|
|
2
|
+
const ANSI = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bold: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
red: '\x1b[31m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
cyan: '\x1b[36m',
|
|
9
|
+
blue: '\x1b[34m',
|
|
10
|
+
};
|
|
11
|
+
function useColor() {
|
|
12
|
+
return !process.env.NO_COLOR && Boolean(process.stderr?.isTTY);
|
|
13
|
+
}
|
|
14
|
+
function paint(text, ...styles) {
|
|
15
|
+
if (!useColor() || styles.length === 0)
|
|
16
|
+
return text;
|
|
17
|
+
return `${styles.join('')}${text}${ANSI.reset}`;
|
|
18
|
+
}
|
|
19
|
+
function severityStyle(severity) {
|
|
20
|
+
if (severity === 'error')
|
|
21
|
+
return [ANSI.bold, ANSI.red];
|
|
22
|
+
if (severity === 'warning')
|
|
23
|
+
return [ANSI.bold, ANSI.yellow];
|
|
24
|
+
return [ANSI.cyan];
|
|
25
|
+
}
|
|
26
|
+
function severityLabel(severity) {
|
|
27
|
+
return severity === 'warning' ? 'warning' : severity;
|
|
28
|
+
}
|
|
29
|
+
function formatLocation(path, line, column) {
|
|
30
|
+
const where = path ? `${path}:${line}:${column}` : `${line}:${column}`;
|
|
31
|
+
return paint(` --> ${where}`, ANSI.bold, ANSI.blue);
|
|
32
|
+
}
|
|
33
|
+
function canRenderSnippet(d, opts) {
|
|
34
|
+
return (opts.style ?? 'pretty') === 'pretty' && Boolean(opts.sourceText && opts.position && d.span && d.span.end > d.span.start);
|
|
35
|
+
}
|
|
36
|
+
function renderSnippetLine(out, gutterWidth, lineNum, lineText, colStart, colEnd, severity) {
|
|
37
|
+
const gutter = paint(`${String(lineNum).padStart(gutterWidth)} | `, ANSI.dim);
|
|
38
|
+
out.push(`${gutter}${lineText}`);
|
|
39
|
+
const startIdx = scalarColumnToIndex(lineText, colStart);
|
|
40
|
+
const endIdx = Math.max(startIdx + 1, scalarColumnToIndex(lineText, colEnd + 1));
|
|
41
|
+
const marker = paint(`${' '.repeat(gutterWidth)} | `, ANSI.dim) +
|
|
42
|
+
' '.repeat(startIdx) +
|
|
43
|
+
paint('^'.repeat(endIdx - startIdx), ...severityStyle(severity));
|
|
44
|
+
out.push(marker);
|
|
45
|
+
}
|
|
46
|
+
export function formatDiagnosticPretty(d, message, opts) {
|
|
47
|
+
const head = paint(`${severityLabel(d.severity)}[${d.code}]`, ...severityStyle(d.severity));
|
|
48
|
+
const lines = [`${head}: ${message}`];
|
|
49
|
+
if (!canRenderSnippet(d, opts)) {
|
|
50
|
+
const path = d.path || '';
|
|
51
|
+
if (d.span && opts.position) {
|
|
52
|
+
const { line, column } = opts.position.lineCol(d.span.start);
|
|
53
|
+
lines.push(formatLocation(path, line, column));
|
|
54
|
+
}
|
|
55
|
+
else if (path) {
|
|
56
|
+
lines.push(paint(` at ${path}`, ANSI.dim));
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
const source = String(opts.sourceText);
|
|
61
|
+
const position = opts.position;
|
|
62
|
+
const span = d.span;
|
|
63
|
+
const path = d.path || '';
|
|
64
|
+
const startPos = position.lineCol(span.start);
|
|
65
|
+
const endPos = position.lineCol(Math.max(span.start, span.end - 1));
|
|
66
|
+
lines.push(formatLocation(path, startPos.line, startPos.column));
|
|
67
|
+
const startLine = utf8LineAt(source, span.start);
|
|
68
|
+
const endLine = utf8LineAt(source, Math.max(span.start, span.end - 1));
|
|
69
|
+
const gutterWidth = String(endLine.line).length;
|
|
70
|
+
if (startLine.line === endLine.line) {
|
|
71
|
+
renderSnippetLine(lines, gutterWidth, startLine.line, startLine.text, startPos.column, endPos.column, d.severity);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
renderSnippetLine(lines, gutterWidth, startLine.line, startLine.text, startPos.column, startLine.text.length + 1, d.severity);
|
|
75
|
+
if (endLine.line > startLine.line + 1) {
|
|
76
|
+
lines.push(paint(`${' '.repeat(gutterWidth + 3)}...`, ANSI.dim));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
package/dist/utf8.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** UTF-8 byte helpers for Rust wire spans in a JS string buffer. */
|
|
2
|
+
export declare function utf8ByteLength(source: string): number;
|
|
3
|
+
export declare function utf8ByteSlice(source: string, start: number, end: number): string;
|
|
4
|
+
export type Utf8LineSlice = {
|
|
5
|
+
line: number;
|
|
6
|
+
lineStart: number;
|
|
7
|
+
lineEnd: number;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
/** Line containing `offset` (UTF-8 bytes, 1-based line number). */
|
|
11
|
+
export declare function utf8LineAt(source: string, offset: number): Utf8LineSlice;
|
|
12
|
+
/** 1-based Unicode scalar column within a line slice (matches compiler OffsetIndex). */
|
|
13
|
+
export declare function unicodeScalarColumn(lineText: string, byteOffsetInLine: number): number;
|
|
14
|
+
/** Map 1-based scalar column to JS string index for underline rendering. */
|
|
15
|
+
export declare function scalarColumnToIndex(lineText: string, column: number): number;
|
package/dist/utf8.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** UTF-8 byte helpers for Rust wire spans in a JS string buffer. */
|
|
2
|
+
export function utf8ByteLength(source) {
|
|
3
|
+
return Buffer.byteLength(source, 'utf8');
|
|
4
|
+
}
|
|
5
|
+
export function utf8ByteSlice(source, start, end) {
|
|
6
|
+
const buf = Buffer.from(source, 'utf8');
|
|
7
|
+
const s = Math.max(0, Math.min(start, buf.length));
|
|
8
|
+
const e = Math.max(s, Math.min(end, buf.length));
|
|
9
|
+
return buf.subarray(s, e).toString('utf8');
|
|
10
|
+
}
|
|
11
|
+
/** Line containing `offset` (UTF-8 bytes, 1-based line number). */
|
|
12
|
+
export function utf8LineAt(source, offset) {
|
|
13
|
+
const buf = Buffer.from(source, 'utf8');
|
|
14
|
+
const clamped = Math.max(0, Math.min(offset, buf.length));
|
|
15
|
+
let lineStart = clamped;
|
|
16
|
+
while (lineStart > 0 && buf[lineStart - 1] !== 0x0a) {
|
|
17
|
+
lineStart -= 1;
|
|
18
|
+
}
|
|
19
|
+
let lineEnd = clamped;
|
|
20
|
+
while (lineEnd < buf.length && buf[lineEnd] !== 0x0a) {
|
|
21
|
+
lineEnd += 1;
|
|
22
|
+
}
|
|
23
|
+
let line = 1;
|
|
24
|
+
for (let i = 0; i < lineStart; i += 1) {
|
|
25
|
+
if (buf[i] === 0x0a)
|
|
26
|
+
line += 1;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
line,
|
|
30
|
+
lineStart,
|
|
31
|
+
lineEnd,
|
|
32
|
+
text: buf.subarray(lineStart, lineEnd).toString('utf8'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 1-based Unicode scalar column within a line slice (matches compiler OffsetIndex). */
|
|
36
|
+
export function unicodeScalarColumn(lineText, byteOffsetInLine) {
|
|
37
|
+
const buf = Buffer.from(lineText, 'utf8');
|
|
38
|
+
const end = Math.max(0, Math.min(byteOffsetInLine, buf.length));
|
|
39
|
+
if (end === 0)
|
|
40
|
+
return 1;
|
|
41
|
+
return buf.subarray(0, end).toString('utf8').length > 0 ? [...buf.subarray(0, end).toString('utf8')].length : 1;
|
|
42
|
+
}
|
|
43
|
+
/** Map 1-based scalar column to JS string index for underline rendering. */
|
|
44
|
+
export function scalarColumnToIndex(lineText, column) {
|
|
45
|
+
if (column <= 1)
|
|
46
|
+
return 0;
|
|
47
|
+
let pos = 1;
|
|
48
|
+
let index = 0;
|
|
49
|
+
for (const ch of lineText) {
|
|
50
|
+
if (pos >= column)
|
|
51
|
+
break;
|
|
52
|
+
index += ch.length;
|
|
53
|
+
pos += 1;
|
|
54
|
+
}
|
|
55
|
+
return index;
|
|
56
|
+
}
|