reslop 0.2.4 → 0.2.6
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/CHANGELOG.md +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +196 -404
- package/lib/deps.js +240 -399
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +138 -141
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +157 -132
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +16 -26
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../ansi.js');
|
|
4
|
+
const { THEME, paint, visibleWidth, truncateVisible } = ansi;
|
|
5
|
+
|
|
6
|
+
const GAP_X = 2;
|
|
7
|
+
const GAP_Y = 1;
|
|
8
|
+
const MIN_W = 34;
|
|
9
|
+
const MIN_H = 6;
|
|
10
|
+
const MAX_COLS = 4;
|
|
11
|
+
const RULE = '─';
|
|
12
|
+
const ELLIPSIS = '…';
|
|
13
|
+
const BAR_ON = '█';
|
|
14
|
+
const BAR_OFF = '░';
|
|
15
|
+
const SPARK = '▁▂▃▄▅▆▇█';
|
|
16
|
+
const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
17
|
+
|
|
18
|
+
const seg = (text, tone = 'text', bold = false) => ({
|
|
19
|
+
text: `${text}`,
|
|
20
|
+
tone,
|
|
21
|
+
bold,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const toneColors = () => ({
|
|
25
|
+
text: THEME.chromeFg,
|
|
26
|
+
muted: THEME.mutedFg,
|
|
27
|
+
hot: THEME.buttonHotFg,
|
|
28
|
+
add: THEME.addLineFg,
|
|
29
|
+
del: THEME.delLineFg,
|
|
30
|
+
warn: THEME.warnFg,
|
|
31
|
+
error: THEME.errorFg,
|
|
32
|
+
sha: THEME.shaFg,
|
|
33
|
+
trend: THEME.dashTrendFg,
|
|
34
|
+
key: THEME.dashKeyFg,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const headColors = () => ({
|
|
38
|
+
...toneColors(),
|
|
39
|
+
muted: THEME.dashHeadMuted,
|
|
40
|
+
del: THEME.dashDelFg,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const segmentsWidth = (segs) => {
|
|
44
|
+
let width = 0;
|
|
45
|
+
for (const item of segs) width += visibleWidth(item.text);
|
|
46
|
+
return width;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const clipSegments = (segs, width) => {
|
|
50
|
+
if (segmentsWidth(segs) <= width) {
|
|
51
|
+
return segs.map((item) => ({ ...item }));
|
|
52
|
+
}
|
|
53
|
+
const markW = visibleWidth(ELLIPSIS);
|
|
54
|
+
const budget = markW <= width ? width - markW : width;
|
|
55
|
+
const out = [];
|
|
56
|
+
let used = 0;
|
|
57
|
+
for (const item of segs) {
|
|
58
|
+
const room = budget - used;
|
|
59
|
+
if (room <= 0) break;
|
|
60
|
+
const text = truncateVisible(item.text, room);
|
|
61
|
+
if (!text) continue;
|
|
62
|
+
used += visibleWidth(text);
|
|
63
|
+
out.push({ ...item, text });
|
|
64
|
+
if (visibleWidth(text) < visibleWidth(item.text)) break;
|
|
65
|
+
}
|
|
66
|
+
if (markW > width) return out;
|
|
67
|
+
if (!out.length) return [seg(ELLIPSIS)];
|
|
68
|
+
out[out.length - 1].text += ELLIPSIS;
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const paintSegments = (segs, width, bg, color, fill = ' ', colors = null) => {
|
|
73
|
+
const clipped = clipSegments(segs, width);
|
|
74
|
+
const pad = Math.max(0, width - segmentsWidth(clipped));
|
|
75
|
+
const ink = colors ?? toneColors();
|
|
76
|
+
if (!color) {
|
|
77
|
+
return clipped.map((item) => item.text).join('') + fill.repeat(pad);
|
|
78
|
+
}
|
|
79
|
+
let out = '';
|
|
80
|
+
for (const item of clipped) {
|
|
81
|
+
out += paint(item.text, ink[item.tone], bg, color, item.bold);
|
|
82
|
+
}
|
|
83
|
+
return out + paint(fill.repeat(pad), ink.muted, bg, color);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const barSegments = (ratio, width, tone = 'add') => {
|
|
87
|
+
const clamped = Math.min(1, Math.max(0, Number.isFinite(ratio) ? ratio : 0));
|
|
88
|
+
const on = Math.round(clamped * width);
|
|
89
|
+
return [
|
|
90
|
+
seg(BAR_ON.repeat(on), tone),
|
|
91
|
+
seg(BAR_OFF.repeat(width - on), 'muted'),
|
|
92
|
+
];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const sparkline = (values, width) => {
|
|
96
|
+
const count = values.length;
|
|
97
|
+
if (!count || width < 1) return '';
|
|
98
|
+
const max = Math.max(...values);
|
|
99
|
+
const top = SPARK.length - 1;
|
|
100
|
+
let out = '';
|
|
101
|
+
for (let i = 0; i < width; i++) {
|
|
102
|
+
const span = width <= 1 ? count - 1 : (i * (count - 1)) / (width - 1);
|
|
103
|
+
const at = Math.round(span);
|
|
104
|
+
const level = max > 0 ? Math.round((values[at] / max) * top) : 0;
|
|
105
|
+
out += SPARK[level];
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const spinner = (frame) => SPINNER[Math.abs(frame ?? 0) % SPINNER.length];
|
|
111
|
+
|
|
112
|
+
const spread = (total, parts) => {
|
|
113
|
+
const base = Math.floor(total / parts);
|
|
114
|
+
const extra = total % parts;
|
|
115
|
+
return Array.from({ length: parts }, (_, i) => base + (i < extra ? 1 : 0));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const placeBand = (shown, from, size, width) => {
|
|
119
|
+
const room = Math.max(0, width - (size - 1) * GAP_X);
|
|
120
|
+
const widths = spread(room, size);
|
|
121
|
+
const tiles = [];
|
|
122
|
+
let x = 0;
|
|
123
|
+
for (let i = 0; i < size; i++) {
|
|
124
|
+
tiles.push({ id: shown[from + i].id, x, w: widths[i] });
|
|
125
|
+
x += widths[i] + GAP_X;
|
|
126
|
+
}
|
|
127
|
+
return tiles;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const placeTiles = (shown, cols, width, height) => {
|
|
131
|
+
const rows = Math.ceil(shown.length / cols);
|
|
132
|
+
const sizes = spread(shown.length, rows);
|
|
133
|
+
const heights = spread(height - (rows - 1) * GAP_Y, rows);
|
|
134
|
+
const bands = [];
|
|
135
|
+
let from = 0;
|
|
136
|
+
let y = 0;
|
|
137
|
+
for (let row = 0; row < rows; row++) {
|
|
138
|
+
const tiles = placeBand(shown, from, sizes[row], width);
|
|
139
|
+
bands.push({ y, h: Math.max(1, heights[row]), tiles });
|
|
140
|
+
from += sizes[row];
|
|
141
|
+
y += heights[row] + GAP_Y;
|
|
142
|
+
}
|
|
143
|
+
return bands;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const layoutTiles = (width, height, tiles) => {
|
|
147
|
+
const cap = Math.floor((width + GAP_X) / (MIN_W + GAP_X));
|
|
148
|
+
const maxCols = Math.max(1, Math.min(MAX_COLS, cap));
|
|
149
|
+
const ranked = [...tiles].sort((left, right) => left.rank - right.rank);
|
|
150
|
+
for (let count = tiles.length; count >= 1; count--) {
|
|
151
|
+
const keep = new Set(ranked.slice(0, count).map((tile) => tile.id));
|
|
152
|
+
const shown = tiles.filter((tile) => keep.has(tile.id));
|
|
153
|
+
const cols = Math.min(maxCols, count);
|
|
154
|
+
const rows = Math.ceil(count / cols);
|
|
155
|
+
const rowH = Math.floor((height - (rows - 1) * GAP_Y) / rows);
|
|
156
|
+
if (rowH >= MIN_H || count === 1) {
|
|
157
|
+
return placeTiles(shown, cols, width, height);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const titleSegments = (tile, badge = []) => {
|
|
164
|
+
const keyed = tile.title.startsWith(tile.key);
|
|
165
|
+
const rest = keyed ? tile.title.slice(tile.key.length) : ` ${tile.title}`;
|
|
166
|
+
const segs = [seg(tile.key, 'key', true), seg(rest)];
|
|
167
|
+
return badge.length ? [...segs, seg(' '), ...badge] : segs;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const paintFramed = (line, width, bg, color, fill = ' ', colors = null) => {
|
|
171
|
+
const edge = paintSegments([seg(' ')], 1, bg, color);
|
|
172
|
+
const inner = Math.max(0, width - 2);
|
|
173
|
+
return edge + paintSegments(line, inner, bg, color, fill, colors) + edge;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const paintHead = (tile, content, width, color) => {
|
|
177
|
+
const fill = color ? ' ' : RULE;
|
|
178
|
+
const bg = THEME.dashHeadBg;
|
|
179
|
+
const colors = headColors();
|
|
180
|
+
if (content.titleLine) {
|
|
181
|
+
return paintFramed(content.titleLine, width, bg, color, fill, colors);
|
|
182
|
+
}
|
|
183
|
+
const segs = [seg(' '), ...titleSegments(tile, content.badge)];
|
|
184
|
+
return paintSegments(segs, width, bg, color, fill, colors);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const rowBackground = (index, filled) => {
|
|
188
|
+
if (filled && index % 2 === 1) return THEME.dashRowBg;
|
|
189
|
+
return THEME.dashTileBg;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const paintTile = (tile, content, width, height, color) => {
|
|
193
|
+
const { lines, footer } = content;
|
|
194
|
+
const rows = [paintHead(tile, content, width, color)];
|
|
195
|
+
const bodyRoom = footer ? height - 2 : height - 1;
|
|
196
|
+
for (let i = 0; i < bodyRoom; i++) {
|
|
197
|
+
const bg = rowBackground(i, i < lines.length);
|
|
198
|
+
rows.push(paintFramed(lines[i] ?? [], width, bg, color));
|
|
199
|
+
}
|
|
200
|
+
if (footer && height > 1) {
|
|
201
|
+
const bg = rowBackground(bodyRoom, true);
|
|
202
|
+
rows.push(paintFramed(footer, width, bg, color));
|
|
203
|
+
}
|
|
204
|
+
return rows;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const pageFill = (width, color) =>
|
|
208
|
+
paint(' '.repeat(Math.max(0, width)), THEME.chromeFg, THEME.dashBg, color);
|
|
209
|
+
|
|
210
|
+
const composeBands = (bands, painted, width, height, color) => {
|
|
211
|
+
const rows = [];
|
|
212
|
+
const blank = pageFill(width, color);
|
|
213
|
+
for (const band of bands) {
|
|
214
|
+
if (rows.length) {
|
|
215
|
+
for (let gap = 0; gap < GAP_Y; gap++) rows.push(blank);
|
|
216
|
+
}
|
|
217
|
+
const last = band.tiles.at(-1);
|
|
218
|
+
const rest = pageFill(width - last.x - last.w, color);
|
|
219
|
+
for (let line = 0; line < band.h; line++) {
|
|
220
|
+
let row = '';
|
|
221
|
+
for (let i = 0; i < band.tiles.length; i++) {
|
|
222
|
+
if (i) row += pageFill(GAP_X, color);
|
|
223
|
+
row += painted.get(band.tiles[i].id)[line];
|
|
224
|
+
}
|
|
225
|
+
rows.push(row + rest);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
while (rows.length < height) rows.push(blank);
|
|
229
|
+
return rows.slice(0, height);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const tileHits = (bands, headerLines) => {
|
|
233
|
+
const hits = [];
|
|
234
|
+
for (const band of bands) {
|
|
235
|
+
for (const tile of band.tiles) {
|
|
236
|
+
for (let line = 0; line < band.h; line++) {
|
|
237
|
+
const y = headerLines + band.y + line + 1;
|
|
238
|
+
hits.push({ y, x0: tile.x, x1: tile.x + tile.w, cursor: tile.id });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return hits;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
GAP_X,
|
|
247
|
+
GAP_Y,
|
|
248
|
+
seg,
|
|
249
|
+
segmentsWidth,
|
|
250
|
+
clipSegments,
|
|
251
|
+
barSegments,
|
|
252
|
+
sparkline,
|
|
253
|
+
spinner,
|
|
254
|
+
titleSegments,
|
|
255
|
+
layoutTiles,
|
|
256
|
+
paintTile,
|
|
257
|
+
composeBands,
|
|
258
|
+
tileHits,
|
|
259
|
+
pageFill,
|
|
260
|
+
};
|
package/lib/render/unit.js
CHANGED
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const detect = require('../detect.js');
|
|
4
|
-
const
|
|
4
|
+
const diffLib = require('../diff/diff.js');
|
|
5
5
|
const primitives = require('./primitives.js');
|
|
6
6
|
const diff = require('./diff.js');
|
|
7
7
|
|
|
8
|
-
const {
|
|
9
|
-
const { paintDiffLine, codeCursorInRows, expandSoftRows } = diff;
|
|
10
|
-
const diffLib = require('../diff/diff.js');
|
|
8
|
+
const { detectLang } = detect;
|
|
11
9
|
const { sameHunk } = diffLib;
|
|
10
|
+
const { paneResult } = primitives;
|
|
11
|
+
const { paintDiffLine, expandSoftRows, codeHits } = diff;
|
|
12
12
|
|
|
13
13
|
const paintBodyUnit = (view, width, color) => {
|
|
14
|
-
const
|
|
15
|
-
const item = view.item;
|
|
14
|
+
const { item, codeOverlay: overlay } = view;
|
|
16
15
|
const lang = detectLang(view.reviewPath ?? '');
|
|
17
|
-
const overlay = view.codeOverlay;
|
|
18
16
|
const editing =
|
|
19
|
-
view.mode === 'compose' &&
|
|
20
|
-
const
|
|
21
|
-
const
|
|
17
|
+
view.mode === 'compose' && typeof overlay?.cursor === 'number';
|
|
18
|
+
const scrollCol = (editing ? overlay.scrollCol : 0) ?? 0;
|
|
19
|
+
const lines = view.unitLines ?? [];
|
|
22
20
|
const source = editing ? lines : expandSoftRows(lines, width, false, true);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const row = { ...line };
|
|
29
|
-
if (editing) {
|
|
30
|
-
row.mark = ' ';
|
|
31
|
-
} else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
|
|
32
|
-
row.mark = ' ';
|
|
33
|
-
}
|
|
34
|
-
body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
|
|
35
|
-
}
|
|
36
|
-
const edit = overlay ? overlay.cursor : null;
|
|
37
|
-
const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
|
|
38
|
-
return paneResult({
|
|
39
|
-
body,
|
|
40
|
-
cursor,
|
|
21
|
+
const body = source.map((line) => {
|
|
22
|
+
const changed = line.type === 'add' || line.type === 'del';
|
|
23
|
+
const outside = changed && !(item && sameHunk(line.item, item));
|
|
24
|
+
const row = editing || outside ? { ...line, mark: ' ' } : line;
|
|
25
|
+
return paintDiffLine(row, width, color, row.origin, lang, scrollCol);
|
|
41
26
|
});
|
|
27
|
+
const hits = codeHits(source, false, width, overlay, editing, scrollCol);
|
|
28
|
+
return paneResult({ body, ...hits });
|
|
42
29
|
};
|
|
43
30
|
|
|
44
31
|
module.exports = { paintBodyUnit };
|
package/lib/report-model.js
CHANGED
|
@@ -4,7 +4,7 @@ const ansi = require('./ansi.js');
|
|
|
4
4
|
const { stripAnsi } = ansi;
|
|
5
5
|
const commands = require('./npm-commands.js');
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { traceLine, relativize } = commands;
|
|
8
8
|
|
|
9
9
|
const summary = () => ({
|
|
10
10
|
tests: null,
|
|
@@ -23,8 +23,10 @@ const problem = (fields = {}) => {
|
|
|
23
23
|
severity: fields.severity || 'error',
|
|
24
24
|
name: fields.name || '',
|
|
25
25
|
message: fields.message || '',
|
|
26
|
+
key: fields.key || '',
|
|
26
27
|
trace,
|
|
27
28
|
traces,
|
|
29
|
+
related: fields.related || [],
|
|
28
30
|
duration: fields.duration || '',
|
|
29
31
|
code: fields.code || '',
|
|
30
32
|
operator: fields.operator || '',
|
|
@@ -44,15 +46,16 @@ const report = (tool, fields = {}) => ({
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
const userTrace = (text, root) => {
|
|
47
|
-
const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
|
|
48
49
|
const kept = [];
|
|
49
|
-
for (const line of
|
|
50
|
-
const next =
|
|
51
|
-
if (next
|
|
52
|
-
kept.push(relativize(indentStack(next), root).trimEnd());
|
|
50
|
+
for (const line of stripAnsi(`${text ?? ''}`).split(/\r?\n/)) {
|
|
51
|
+
const next = traceLine(line, root);
|
|
52
|
+
if (next !== null) kept.push(next.trimEnd());
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
let start = 0;
|
|
55
|
+
while (start < kept.length && kept[start].trim() === '') start += 1;
|
|
56
|
+
let end = kept.length;
|
|
57
|
+
while (end > start && kept[end - 1] === '') end -= 1;
|
|
58
|
+
return kept.slice(start, end).join('\n');
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
const normalizeMessage = (message) => {
|
|
@@ -60,33 +63,106 @@ const normalizeMessage = (message) => {
|
|
|
60
63
|
return text.replace(/\s+\S+:\d+:\d+$/, '');
|
|
61
64
|
};
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
// Build identity from raw diagnostics, before userTrace removes Node frames.
|
|
67
|
+
const errorKey = (fields, stack, root) => {
|
|
68
|
+
const text = stripAnsi(fields.message || '');
|
|
69
|
+
const header = /^([\w.]*Error)(?: \[[^\]]+\])?:\s*/.exec(text);
|
|
70
|
+
const type = fields.type || (header ? header[1] : '');
|
|
71
|
+
const message = header ? text.slice(header[0].length) : text;
|
|
72
|
+
let origin = '';
|
|
73
|
+
for (const row of stripAnsi(stack).split(/\r?\n/)) {
|
|
74
|
+
const line = row.trim().replace(/^at\s+/, '');
|
|
75
|
+
if (/^[\w.]*Error(?: \[[^\]]+\])?:/.test(line)) continue;
|
|
76
|
+
if (line.includes('node:') || !/:\d+:\d+\)?$/.test(line)) continue;
|
|
77
|
+
const location = line.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
|
|
78
|
+
origin = relativize(location.replace(/file:\/\//g, ''), root).replace(
|
|
79
|
+
/\\/g,
|
|
80
|
+
'/',
|
|
81
|
+
);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return JSON.stringify([
|
|
85
|
+
fields.severity || 'error',
|
|
86
|
+
type,
|
|
87
|
+
fields.code || '',
|
|
88
|
+
fields.operator || '',
|
|
89
|
+
message.replace(/\r\n/g, '\n').trim(),
|
|
90
|
+
fields.expected || '',
|
|
91
|
+
fields.actual || '',
|
|
92
|
+
origin,
|
|
93
|
+
]);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const normalizeTrace = (trace) => {
|
|
97
|
+
if (!trace) return '';
|
|
98
|
+
return `${trace}`
|
|
99
|
+
.trim()
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
.map((line) => line.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((line) => line.replace(/\\/g, '/').replace(/:\d+(?::\d+)?/g, ':X:Y'))
|
|
104
|
+
.join('\n');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const groupKey = (item) => {
|
|
108
|
+
if (item.key) return item.key;
|
|
109
|
+
const trace = normalizeTrace(item.trace);
|
|
110
|
+
return [
|
|
65
111
|
item.severity,
|
|
66
112
|
item.code,
|
|
67
113
|
item.operator,
|
|
68
|
-
item.
|
|
69
|
-
item.actual,
|
|
70
|
-
normalizeMessage(item.message),
|
|
114
|
+
trace || normalizeMessage(item.message),
|
|
71
115
|
].join('\0');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const relatedProblem = (item, group) => {
|
|
119
|
+
const frames = item.trace.split(/\r?\n/).map((line) => line.trim());
|
|
120
|
+
const sample = (group.traces[0] || '')
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.map((line) => line.trim());
|
|
123
|
+
let index = 0;
|
|
124
|
+
while (index < frames.length && frames[index] === sample[index]) index++;
|
|
125
|
+
if (index === frames.length && item.name === group.name) return '';
|
|
126
|
+
const rows = [item.name];
|
|
127
|
+
const branch = frames[index];
|
|
128
|
+
const last = frames.at(-1) || '';
|
|
129
|
+
let location = '';
|
|
130
|
+
if (/:\d+:\d+\)?$/.test(last)) {
|
|
131
|
+
const inner = last.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
|
|
132
|
+
location = inner.replace(/^at\s+/, '');
|
|
133
|
+
}
|
|
134
|
+
if (branch) {
|
|
135
|
+
let frame = branch.replace(/^at\s+/, '');
|
|
136
|
+
if (index === frames.length - 1 && location) frame = location;
|
|
137
|
+
rows.push(frame);
|
|
138
|
+
}
|
|
139
|
+
if (index < frames.length - 1 && location) rows.push(location);
|
|
140
|
+
return rows.filter(Boolean).join(' | ');
|
|
141
|
+
};
|
|
72
142
|
|
|
73
143
|
const groupProblems = (problems) => {
|
|
74
144
|
const groups = new Map();
|
|
75
|
-
const order = [];
|
|
76
145
|
for (const item of problems) {
|
|
77
146
|
const key = groupKey(item);
|
|
78
147
|
let group = groups.get(key);
|
|
79
148
|
if (!group) {
|
|
80
|
-
group = problem({ ...item, count: 0, traces: [],
|
|
149
|
+
group = problem({ ...item, count: 0, traces: [], related: [] });
|
|
81
150
|
groups.set(key, group);
|
|
82
|
-
order.push(group);
|
|
83
151
|
}
|
|
84
152
|
group.count += 1;
|
|
85
|
-
if (item.trace && group.traces.length
|
|
153
|
+
if (item.trace && !group.traces.length) {
|
|
154
|
+
group.traces.push(item.trace);
|
|
155
|
+
} else if (group.count > 1 && group.related.length < 2) {
|
|
156
|
+
const related = relatedProblem(item, group);
|
|
157
|
+
if (related && !group.related.includes(related)) {
|
|
158
|
+
group.related.push(related);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
86
161
|
}
|
|
162
|
+
const order = [...groups.values()];
|
|
87
163
|
for (const group of order) {
|
|
88
164
|
group.trace = group.traces[0] || '';
|
|
89
|
-
group.omitted = Math.max(0, group.count - group.
|
|
165
|
+
group.omitted = Math.max(0, group.count - 1 - group.related.length);
|
|
90
166
|
}
|
|
91
167
|
return order;
|
|
92
168
|
};
|
|
@@ -98,23 +174,25 @@ const groupReport = (item) =>
|
|
|
98
174
|
problems: groupProblems(item.problems),
|
|
99
175
|
});
|
|
100
176
|
|
|
101
|
-
const
|
|
177
|
+
const toolFailed = (item) => {
|
|
178
|
+
const stats = item.summary;
|
|
179
|
+
if (stats.failed || stats.errors) return true;
|
|
180
|
+
return item.problems.some((entry) => entry.severity !== 'warning');
|
|
181
|
+
};
|
|
102
182
|
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
return
|
|
183
|
+
const keepReport = (item) => {
|
|
184
|
+
const stats = item.summary;
|
|
185
|
+
if (stats.tests || stats.passed || stats.failed || stats.skipped) return true;
|
|
186
|
+
return !!(stats.errors || stats.warnings) || item.problems.length > 0;
|
|
107
187
|
};
|
|
108
188
|
|
|
109
189
|
const envelope = (reports, exit, stderr) => {
|
|
110
|
-
const status = exit
|
|
111
|
-
const
|
|
112
|
-
const items =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
return { ok, exit: status, reports: items, stderr: stderr || '' };
|
|
190
|
+
const status = exit ?? 1;
|
|
191
|
+
const kept = reports.filter(keepReport);
|
|
192
|
+
const items = kept.map((item) =>
|
|
193
|
+
report(item.tool, { ...item, ok: !toolFailed(item) }),
|
|
194
|
+
);
|
|
195
|
+
return { ok: status === 0, exit: status, reports: items, stderr };
|
|
118
196
|
};
|
|
119
197
|
|
|
120
198
|
module.exports = {
|
|
@@ -122,8 +200,7 @@ module.exports = {
|
|
|
122
200
|
problem,
|
|
123
201
|
report,
|
|
124
202
|
userTrace,
|
|
203
|
+
errorKey,
|
|
125
204
|
groupReport,
|
|
126
|
-
reduceStderr,
|
|
127
|
-
shortTail,
|
|
128
205
|
envelope,
|
|
129
206
|
};
|
package/lib/report-parse.js
CHANGED
|
@@ -6,31 +6,26 @@ const model = require('./report-model.js');
|
|
|
6
6
|
const testParse = require('./report-test.js');
|
|
7
7
|
const toolParse = require('./report-tools.js');
|
|
8
8
|
|
|
9
|
-
const { problem, report, groupReport } = model;
|
|
10
|
-
const { reduceStderr, shortTail, envelope } = model;
|
|
9
|
+
const { problem, report, userTrace, groupReport, envelope } = model;
|
|
11
10
|
const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
|
|
12
|
-
const { parseToolJson, parseToolText } = toolParse;
|
|
11
|
+
const { parseToolJson, parseToolText, stderrRest } = toolParse;
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const COUNTS = ['tests', 'passed', 'failed', 'skipped', 'errors', 'warnings'];
|
|
15
14
|
|
|
16
15
|
const toolHint = (program, args) => {
|
|
17
|
-
const name =
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (name === 'eslint') return 'eslint';
|
|
21
|
-
if (name === 'tsc') return 'tsc';
|
|
22
|
-
if (name === 'prettier') return 'prettier';
|
|
16
|
+
const name = program.split(/[/\\]/).pop();
|
|
17
|
+
const joined = [name, ...args].join(' ');
|
|
18
|
+
if (name === 'eslint' || name === 'tsc' || name === 'prettier') return name;
|
|
23
19
|
if (/\baudit\b/.test(joined)) return 'audit';
|
|
24
20
|
if (/\boutdated\b/.test(joined)) return 'outdated';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (name === 'node' && (args || []).includes('--test')) return 'test';
|
|
21
|
+
const isNpm = name === 'npm' || name === 'pnpm' || name === 'npm.cmd';
|
|
22
|
+
if (isNpm && /\b(ls|list)\b/.test(joined)) return 'ls';
|
|
23
|
+
if (name === 'node' && args.includes('--test')) return 'test';
|
|
29
24
|
return '';
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
const parseJson = (text) => {
|
|
33
|
-
const trimmed =
|
|
28
|
+
const trimmed = text.trim();
|
|
34
29
|
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
|
|
35
30
|
try {
|
|
36
31
|
return JSON.parse(trimmed);
|
|
@@ -46,44 +41,67 @@ const parseJsonReports = (text, root) => {
|
|
|
46
41
|
if (!data) return null;
|
|
47
42
|
const test = parseTestJson(data, root);
|
|
48
43
|
if (test) return [test];
|
|
49
|
-
|
|
50
|
-
if (tools) return tools;
|
|
51
|
-
return [];
|
|
44
|
+
return parseToolJson(data, root) || [];
|
|
52
45
|
};
|
|
53
46
|
|
|
54
47
|
const parseReports = (text, root) => {
|
|
55
48
|
const json = parseJsonReports(text, root);
|
|
56
49
|
if (json) return json;
|
|
57
|
-
const reports = [];
|
|
58
50
|
const tap = parseTap(text, root);
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (!tap) {
|
|
63
|
-
const spec = parseSpec(text, root);
|
|
64
|
-
if (spec) reports.push(spec);
|
|
65
|
-
}
|
|
66
|
-
return reports;
|
|
51
|
+
const spec = tap ? null : parseSpec(text, root);
|
|
52
|
+
const reports = [tap, ...parseToolText(text, root), spec];
|
|
53
|
+
return reports.filter(Boolean);
|
|
67
54
|
};
|
|
68
55
|
|
|
56
|
+
const shortTail = (text) => text.trim().split(/\r?\n/).slice(-20).join('\n');
|
|
57
|
+
|
|
69
58
|
const withHint = (reports, text, exit, hint) => {
|
|
70
|
-
if (reports.length ||
|
|
71
|
-
const
|
|
72
|
-
return [report(
|
|
59
|
+
if (reports.length || !text.trim() || exit === 0) return reports;
|
|
60
|
+
const fallback = problem({ message: shortTail(text) });
|
|
61
|
+
return [report(hint || 'test', { problems: [fallback] })];
|
|
73
62
|
};
|
|
74
63
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
const mergeSummary = (left, right) => {
|
|
65
|
+
const stats = { ...left };
|
|
66
|
+
for (const key of COUNTS) {
|
|
67
|
+
if (right[key] === null) continue;
|
|
68
|
+
stats[key] = (stats[key] ?? 0) + right[key];
|
|
69
|
+
}
|
|
70
|
+
if (!stats.duration && right.duration) stats.duration = right.duration;
|
|
71
|
+
return stats;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mergeReports = (left, right) => {
|
|
75
|
+
const merged = left.slice();
|
|
76
|
+
for (const item of right) {
|
|
77
|
+
const at = merged.findIndex((entry) => entry.tool === item.tool);
|
|
78
|
+
if (at < 0) {
|
|
79
|
+
merged.push(item);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const prev = merged[at];
|
|
83
|
+
merged[at] = report(item.tool, {
|
|
84
|
+
summary: mergeSummary(prev.summary, item.summary),
|
|
85
|
+
problems: prev.problems.concat(item.problems),
|
|
86
|
+
});
|
|
81
87
|
}
|
|
82
|
-
return
|
|
88
|
+
return merged;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const buildDocument = (stdout, stderr, root, exit, hint) => {
|
|
92
|
+
const out = stripAnsi(stdout);
|
|
93
|
+
const err = stripAnsi(stderr);
|
|
94
|
+
const reports = mergeReports(
|
|
95
|
+
parseReports(out, root),
|
|
96
|
+
parseReports(err, root),
|
|
97
|
+
);
|
|
98
|
+
const text = out.trim() ? out : err;
|
|
99
|
+
const grouped = withHint(reports, text, exit, hint).map(groupReport);
|
|
100
|
+
const rest = stderrRest(err, grouped);
|
|
101
|
+
return envelope(grouped, exit, userTrace(rest, root));
|
|
83
102
|
};
|
|
84
103
|
|
|
85
104
|
module.exports = {
|
|
86
105
|
toolHint,
|
|
87
|
-
parseReports,
|
|
88
106
|
buildDocument,
|
|
89
107
|
};
|