reslop 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const chrome = require('./chrome.js');
5
+ const primitives = require('./primitives.js');
6
+ const npmCommands = require('../npm-commands.js');
7
+
8
+ const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
9
+ const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
10
+ const { formatBusyStatus } = chrome;
11
+ const { TABLE_KEY, TABLE_VAL } = npmCommands;
12
+ const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
13
+ const { paintListEdit } = primitives;
14
+
15
+ const EDGE = 2;
16
+ const TAIL = 1;
17
+ const LOG_PAD_Y = 1;
18
+ const LOG_PAD_X = 2;
19
+ const NPM_NAME_MAX = 24;
20
+
21
+ const logViewRows = (bodyH) => {
22
+ const height = Math.max(1, bodyH);
23
+ if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
24
+ return height;
25
+ };
26
+
27
+ const rowFg = (selected) => (selected ? THEME.chromeFg : THEME.mutedFg);
28
+
29
+ const npmNameWidth = (commands) => {
30
+ let width = 0;
31
+ for (const entry of commands) {
32
+ width = Math.max(width, visibleWidth(`${entry.name ?? ''}`));
33
+ }
34
+ return Math.min(NPM_NAME_MAX, width);
35
+ };
36
+
37
+ const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
38
+ const inner = Math.max(1, width - EDGE - TAIL);
39
+ const mark = selected ? FILE_MARK : ' ';
40
+ const prefix = ` ${mark} `;
41
+ const prefixW = visibleWidth(prefix);
42
+ const rest = Math.max(1, inner - prefixW);
43
+ const detail = entry.kind === 'bin' ? 'bin' : entry.command;
44
+ const nameW = Math.min(Math.max(0, nameCol), rest);
45
+ const name = truncateVisible(entry.name, nameW);
46
+ const gap = rest > nameW + 2 ? ' ' : '';
47
+ const detailW = Math.max(0, rest - nameW - visibleWidth(gap));
48
+ const shown = truncateVisible(detail, detailW);
49
+ const mid = `${name}${' '.repeat(Math.max(0, nameW - visibleWidth(name)))}`;
50
+ const after = `${gap}${shown}`;
51
+ const pad = Math.max(0, rest - visibleWidth(mid + after));
52
+ const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
53
+ const fgRgb = rowFg(selected);
54
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
55
+ const tail = ' '.repeat(TAIL);
56
+ const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
57
+ if (!color) return `${left}${tail}${edge}`;
58
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
59
+ out += paint(prefix, fgRgb, bgRgb, color);
60
+ const bin = entry.kind === 'bin';
61
+ const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
62
+ out += paint(mid, nameFg, bgRgb, color, !bin);
63
+ out += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
64
+ return out + edge;
65
+ };
66
+
67
+ const paintNpmList = (view, width, color, bodyH, headerLines) => {
68
+ const commands = view.npmCommands ?? [];
69
+ const nameW = npmNameWidth(commands);
70
+ const cursor = view.npmCursor ?? 0;
71
+ const compose = view.compose;
72
+ const editing = compose && compose.kind === 'npm';
73
+ const paintRow = (entry, rowWidth, rowColor, selected) =>
74
+ paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
75
+ let extra = null;
76
+ if (editing) {
77
+ const painted = paintListEdit(compose, width, color, EDGE);
78
+ const at = view.npmEditName ? 'replace' : 'end';
79
+ extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
80
+ }
81
+ return paintCursorList(
82
+ commands,
83
+ cursor,
84
+ width,
85
+ color,
86
+ bodyH,
87
+ headerLines,
88
+ paintRow,
89
+ extra,
90
+ view.listScroll,
91
+ );
92
+ };
93
+
94
+ const logStart = (view, count, rows) => {
95
+ const maxStart = Math.max(0, count - rows);
96
+ let start = maxStart;
97
+ if (!view.npmFollow) start = view.npmScroll ?? 0;
98
+ if (start > maxStart) start = maxStart;
99
+ if (start < 0) start = 0;
100
+ return start;
101
+ };
102
+
103
+ const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
104
+
105
+ const mixRgb = (from, to, part) => {
106
+ const out = [0, 0, 0];
107
+ for (let i = 0; i < 3; i++) {
108
+ const next = from[i] + (to[i] - from[i]) * part;
109
+ out[i] = Math.round(next);
110
+ }
111
+ return out;
112
+ };
113
+
114
+ const tableTone = (index) => {
115
+ const lift = index % 2 === 0 ? 0.035 : 0.07;
116
+ const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
117
+ const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
118
+ const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
119
+ return { valueBg, keyBg, frame };
120
+ };
121
+
122
+ const tableIndex = (lines, index) => {
123
+ let at = index;
124
+ let row = 0;
125
+ while (at > 0 && isTableLine(lines[at - 1])) {
126
+ at -= 1;
127
+ row += 1;
128
+ }
129
+ return row;
130
+ };
131
+
132
+ const paintTableLine = (line, width, index) => {
133
+ const raw = line.slice(TABLE_KEY.length);
134
+ const at = raw.indexOf(TABLE_VAL);
135
+ const key = at < 0 ? raw : raw.slice(0, at);
136
+ const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
137
+ const gap = rest.startsWith(' ') ? ' ' : '';
138
+ const value = rest.slice(gap.length);
139
+ const edge = 1;
140
+ const inner = Math.max(1, width - edge * 2);
141
+ const keyW = visibleWidth(key);
142
+ const gapW = visibleWidth(gap);
143
+ const valueW = Math.max(0, inner - keyW - gapW);
144
+ const shown = truncateVisible(value, valueW);
145
+ const pad = ' '.repeat(Math.max(0, valueW - visibleWidth(shown)));
146
+ const tone = tableTone(index);
147
+ const ink = THEME.ctxFg;
148
+ let out = fill(edge, ink, tone.frame, true);
149
+ out += paint(key, ink, tone.keyBg, true);
150
+ out += paint(gap, ink, tone.frame, true);
151
+ out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
152
+ return `${out}${fill(edge, ink, tone.frame, true)}`;
153
+ };
154
+
155
+ const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
156
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
157
+ const source = color ? `${line ?? ''}` : stripAnsi(line);
158
+ const clip = (value) =>
159
+ color ? clipAnsi(value, inner) : truncateVisible(value, inner);
160
+ const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
161
+ const lead = ' '.repeat(LOG_PAD_X);
162
+ const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
163
+ const tail = ' '.repeat(pad);
164
+ if (!color) return `${lead}${body}${tail}`;
165
+ const base = seq(fgRgb, THEME.ctxBg);
166
+ let out = `${base}${EL}${lead}${body}`;
167
+ if (pad) out += `${RESET}${base}${tail}`;
168
+ return `${out}${RESET}`;
169
+ };
170
+
171
+ const paintRunBar = (width, color, frame) => {
172
+ const label = formatBusyStatus('running', frame);
173
+ return paintLogLine(label, width, color, THEME.chromeFg);
174
+ };
175
+
176
+ const paintNpmLog = (view, width, color, bodyH) => {
177
+ const text = view.npmOutput ?? '';
178
+ const lines = text.split('\n');
179
+ if (lines.length && lines.at(-1) === '') lines.pop();
180
+ const running = view.npmRunning === true;
181
+ const count = lines.length + (running ? 1 : 0);
182
+ const rows = logViewRows(bodyH);
183
+ const padY = bodyH - rows;
184
+ const top = padY ? 1 : 0;
185
+ const start = logStart(view, count, rows);
186
+ const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
187
+ const body = [];
188
+ for (let i = 0; i < top; i++) body.push(blank);
189
+ for (let i = 0; i < rows; i++) {
190
+ const index = start + i;
191
+ if (index < lines.length) {
192
+ const line = lines[index];
193
+ if (color && isTableLine(line)) {
194
+ const row = tableIndex(lines, index);
195
+ body.push(paintTableLine(line, width, row));
196
+ } else {
197
+ body.push(paintLogLine(line, width, color));
198
+ }
199
+ } else if (running && index === lines.length) {
200
+ body.push(paintRunBar(width, color, view.progressFrame ?? 0));
201
+ } else {
202
+ body.push(blank);
203
+ }
204
+ }
205
+ for (let i = 0; i < padY - top; i++) body.push(blank);
206
+ return paneResult({
207
+ body,
208
+ });
209
+ };
210
+
211
+ const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
212
+ if (view.npmView) return paintNpmLog(view, width, color, bodyH);
213
+ return paintNpmList(view, width, color, bodyH, headerLines);
214
+ };
215
+
216
+ module.exports = { paintBodyNpm, logViewRows };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { ESC } = require('../ansi.js');
3
+ const ansi = require('../ansi.js');
4
+ const { ESC } = ansi;
4
5
 
5
6
  const presentCursor = (cursor) => {
6
7
  if (!cursor || cursor.x <= 0 || cursor.y <= 0) return `${ESC}[?25l`;
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
+ const actions = require('../session/actions.js');
5
+ const { LIST_PANES } = actions;
4
6
 
5
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
6
8
 
@@ -8,7 +10,6 @@ const GUTTER_W = 2;
8
10
  const FILE_MARK = '▶';
9
11
  const FILE_LIST_PAD = 1;
10
12
  const CHECK_DONE = 'xX';
11
- const LIST_PANES = ['files', 'branches', 'commits'];
12
13
  const CHECK_TOKEN = /\[([ xX])\]/;
13
14
  const CHECK_LEAD = /^\[[ xX]\] /;
14
15
 
@@ -34,14 +35,21 @@ const splitCheck = (text) => {
34
35
  return { before, mark, token, after };
35
36
  };
36
37
 
37
- const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
38
+ const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
38
39
  const clipped = truncateVisible(text, width);
39
40
  const parts = color ? splitCheck(clipped) : null;
40
41
  if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
41
42
  const pad = Math.max(0, width - visibleWidth(clipped));
42
43
  const done = CHECK_DONE.includes(parts.mark);
43
- const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
44
- const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
44
+ let chipFg = THEME.checkFg;
45
+ let chipBg = THEME.checkBg;
46
+ if (plain) {
47
+ chipFg = fgRgb;
48
+ chipBg = bgRgb;
49
+ } else if (done) {
50
+ chipFg = THEME.checkDoneFg;
51
+ chipBg = THEME.checkDoneBg;
52
+ }
45
53
  const after = `${parts.after}${' '.repeat(pad)}`;
46
54
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
47
55
  if (parts.before) out += paint(parts.before, fgRgb, bgRgb, color);
@@ -111,6 +119,16 @@ const extraIdx = (extraRow, cursor, count) => {
111
119
  return count;
112
120
  };
113
121
 
122
+ const paneResult = (fields = {}) => ({
123
+ body: fields.body ?? [],
124
+ fileHits: fields.fileHits ?? [],
125
+ todoOwners: fields.todoOwners ?? [],
126
+ todoChecks: fields.todoChecks ?? [],
127
+ cursor: fields.cursor ?? null,
128
+ splitBody: fields.splitBody === true,
129
+ listScroll: fields.listScroll ?? 0,
130
+ });
131
+
114
132
  const paintCursorList = (
115
133
  items,
116
134
  cursor,
@@ -153,41 +171,23 @@ const paintCursorList = (
153
171
  fileHits.push({ y, cursor: itemIdx });
154
172
  }
155
173
  if (pad) body.push(blank);
156
- return { body, fileHits, cursor: textCursor, offset: start };
157
- };
158
-
159
- const branchLabel = (name) => {
160
- const branch = `${name ?? ''}`.trim();
161
- if (!branch) return '';
162
- return `[${branch}]`;
163
- };
164
-
165
- const splitBracketName = (label) => {
166
- const text = `${label ?? ''}`;
167
- const open = text.startsWith('[') ? '[' : '';
168
- const rest = open ? text.slice(1) : text;
169
- const close = rest.endsWith(']') ? ']' : '';
170
- const name = close ? rest.slice(0, -1) : rest;
171
- return { open, name, close };
174
+ return paneResult({
175
+ body,
176
+ fileHits,
177
+ cursor: textCursor,
178
+ listScroll: start,
179
+ });
172
180
  };
173
181
 
174
- const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
175
- const { open, name, close } = splitBracketName(label);
176
- return (
177
- paint(open, wrapFg, bgRgb, color, true) +
178
- paint(name, nameFg, bgRgb, color, nameBold) +
179
- paint(close, wrapFg, bgRgb, color, true)
180
- );
181
- };
182
+ const branchLabel = (name) => `${name ?? ''}`.trim();
182
183
 
183
- const paintListEdit = (compose, width, color, edgeW) => {
184
+ const paintListEdit = (compose, width, color, edgeW = 0) => {
184
185
  const inner = Math.max(1, width - edgeW);
185
186
  const prefix = ` ${FILE_MARK} `;
186
187
  const prefixW = visibleWidth(prefix);
187
188
  const rest = Math.max(1, inner - prefixW);
188
189
  const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
189
- const label = `[${text}]`;
190
- const clipped = truncateVisible(label, rest);
190
+ const clipped = truncateVisible(text, rest);
191
191
  const pad = Math.max(0, rest - visibleWidth(clipped));
192
192
  const left = `${prefix}${clipped}${' '.repeat(pad)}`;
193
193
  const fgRgb = THEME.chromeFg;
@@ -198,30 +198,14 @@ const paintListEdit = (compose, width, color, edgeW) => {
198
198
  const gap = ' '.repeat(pad);
199
199
  row = `${seq(fgRgb, bgRgb)}${EL}`;
200
200
  row += paint(prefix, fgRgb, bgRgb, color);
201
- row += paintBracketName(
202
- clipped,
203
- THEME.buttonHotFg,
204
- THEME.mutedFg,
205
- bgRgb,
206
- color,
207
- );
201
+ row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
208
202
  row += paint(gap, fgRgb, bgRgb, color);
209
203
  row += edge;
210
204
  }
211
- const col = Math.min((compose.cursor ?? 0) + 1, visibleWidth(clipped));
205
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
212
206
  return { row, cursor: { x: prefixW + col + 1 } };
213
207
  };
214
208
 
215
- const paneResult = (fields) => ({
216
- body: fields.body,
217
- fileHits: fields.fileHits,
218
- todoOwners: fields.todoOwners,
219
- todoChecks: fields.todoChecks ?? [],
220
- cursor: fields.cursor ?? null,
221
- splitBody: fields.splitBody === true,
222
- listScroll: fields.listScroll ?? 0,
223
- });
224
-
225
209
  const measureColumns = (entries, rowFields, align) => {
226
210
  const keys = Object.keys(align);
227
211
  const widths = Object.fromEntries(keys.map((key) => [key, 0]));
@@ -280,9 +264,9 @@ module.exports = {
280
264
  paintSplitRule,
281
265
  paintBodyFill,
282
266
  windowRows,
267
+ listWindowStart,
283
268
  paintCursorList,
284
269
  paintListEdit,
285
270
  branchLabel,
286
- paintBracketName,
287
271
  paneResult,
288
272
  };
@@ -4,7 +4,8 @@ const chrome = require('./chrome.js');
4
4
  const notes = require('./notes.js');
5
5
  const diff = require('./diff.js');
6
6
  const present = require('./present.js');
7
- const { renderFrame } = require('./frame.js');
7
+ const frame = require('./frame.js');
8
+ const { renderFrame } = frame;
8
9
 
9
10
  const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
10
11
  const { updatePrompt, dropPrompt } = chrome;
@@ -1,23 +1,29 @@
1
1
  'use strict';
2
2
 
3
- const { detectLang } = require('../detect.js');
3
+ const detect = require('../detect.js');
4
+ const { detectLang } = detect;
4
5
  const primitives = require('./primitives.js');
5
6
  const diff = require('./diff.js');
6
7
 
7
8
  const { paneResult } = primitives;
8
- const { paintDiffLine, codeCursorInRows } = diff;
9
- const { sameHunk } = require('../diff/diff.js');
9
+ const { paintDiffLine, codeCursorInRows, expandSoftRows } = diff;
10
+ const diffLib = require('../diff/diff.js');
11
+ const { sameHunk } = diffLib;
10
12
 
11
13
  const paintBodyUnit = (view, width, color) => {
12
14
  const lines = view.unitLines ?? [];
13
15
  const item = view.item;
14
16
  const lang = detectLang(view.reviewPath ?? '');
15
17
  const overlay = view.codeOverlay;
16
- const editing = view.mode === 'compose' && overlay;
17
- const { length } = lines;
18
+ const editing =
19
+ view.mode === 'compose' && overlay && typeof overlay.cursor === 'number';
20
+ const picked = editing ? overlay.scrollCol : 0;
21
+ const scrollCol = picked ?? 0;
22
+ const source = editing ? lines : expandSoftRows(lines, width, false, true);
23
+ const { length } = source;
18
24
  const body = new Array(length);
19
25
  for (let i = 0; i < length; i++) {
20
- const line = lines[i];
26
+ const line = source[i];
21
27
  const isBlock = item && sameHunk(line.item, item);
22
28
  const row = { ...line };
23
29
  if (editing) {
@@ -25,16 +31,13 @@ const paintBodyUnit = (view, width, color) => {
25
31
  } else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
26
32
  row.mark = ' ';
27
33
  }
28
- body[i] = paintDiffLine(row, width, color, row.origin, lang);
34
+ body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
29
35
  }
30
36
  const edit = overlay ? overlay.cursor : null;
31
- const cursor = codeCursorInRows(lines, false, width, edit);
37
+ const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
32
38
  return paneResult({
33
39
  body,
34
- fileHits: [],
35
- todoOwners: [],
36
40
  cursor,
37
- splitBody: false,
38
41
  });
39
42
  };
40
43
 
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('./ansi.js');
4
+ const { stripAnsi } = ansi;
5
+ const commands = require('./npm-commands.js');
6
+
7
+ const { withoutNodeFrame, indentStack, relativize } = commands;
8
+
9
+ const summary = () => ({
10
+ tests: null,
11
+ passed: null,
12
+ failed: null,
13
+ skipped: null,
14
+ errors: null,
15
+ warnings: null,
16
+ duration: '',
17
+ });
18
+
19
+ const problem = (fields = {}) => {
20
+ const trace = fields.trace || '';
21
+ const traces = fields.traces || (trace ? [trace] : []);
22
+ return {
23
+ severity: fields.severity || 'error',
24
+ name: fields.name || '',
25
+ message: fields.message || '',
26
+ trace,
27
+ traces,
28
+ duration: fields.duration || '',
29
+ code: fields.code || '',
30
+ operator: fields.operator || '',
31
+ expected: fields.expected || '',
32
+ actual: fields.actual || '',
33
+ extra: fields.extra || [],
34
+ count: fields.count === undefined ? 1 : fields.count,
35
+ omitted: fields.omitted || 0,
36
+ };
37
+ };
38
+
39
+ const report = (tool, fields = {}) => ({
40
+ tool,
41
+ ok: fields.ok !== false,
42
+ summary: fields.summary || summary(),
43
+ problems: fields.problems || [],
44
+ });
45
+
46
+ const userTrace = (text, root) => {
47
+ const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
48
+ const kept = [];
49
+ for (const line of lines) {
50
+ const next = withoutNodeFrame(line);
51
+ if (next === null) continue;
52
+ kept.push(relativize(indentStack(next), root).trimEnd());
53
+ }
54
+ while (kept.length && kept[kept.length - 1] === '') kept.pop();
55
+ return kept.join('\n').trim();
56
+ };
57
+
58
+ const normalizeMessage = (message) => {
59
+ const text = `${message ?? ''}`.trim().replace(/\s+/g, ' ');
60
+ return text.replace(/\s+\S+:\d+:\d+$/, '');
61
+ };
62
+
63
+ const groupKey = (item) =>
64
+ [
65
+ item.severity,
66
+ item.code,
67
+ item.operator,
68
+ item.expected,
69
+ item.actual,
70
+ normalizeMessage(item.message),
71
+ ].join('\0');
72
+
73
+ const groupProblems = (problems) => {
74
+ const groups = new Map();
75
+ const order = [];
76
+ for (const item of problems) {
77
+ const key = groupKey(item);
78
+ let group = groups.get(key);
79
+ if (!group) {
80
+ group = problem({ ...item, count: 0, traces: [], omitted: 0 });
81
+ groups.set(key, group);
82
+ order.push(group);
83
+ }
84
+ group.count += 1;
85
+ if (item.trace && group.traces.length < 3) group.traces.push(item.trace);
86
+ }
87
+ for (const group of order) {
88
+ group.trace = group.traces[0] || '';
89
+ group.omitted = Math.max(0, group.count - group.traces.length);
90
+ }
91
+ return order;
92
+ };
93
+
94
+ const groupReport = (item) =>
95
+ report(item.tool, {
96
+ ok: item.ok,
97
+ summary: item.summary,
98
+ problems: groupProblems(item.problems),
99
+ });
100
+
101
+ const reduceStderr = (text, root) => userTrace(text, root);
102
+
103
+ const shortTail = (text) => {
104
+ const lines = `${text ?? ''}`.trim().split(/\r?\n/);
105
+ if (lines.length <= 20) return lines.join('\n');
106
+ return lines.slice(-20).join('\n');
107
+ };
108
+
109
+ const envelope = (reports, exit, stderr) => {
110
+ const status = exit === null || exit === undefined ? 1 : exit;
111
+ const ok = status === 0;
112
+ const items = new Array(reports.length);
113
+ for (let i = 0; i < reports.length; i++) {
114
+ const item = reports[i];
115
+ items[i] = report(item.tool, { ...item, ok });
116
+ }
117
+ return { ok, exit: status, reports: items, stderr: stderr || '' };
118
+ };
119
+
120
+ module.exports = {
121
+ summary,
122
+ problem,
123
+ report,
124
+ userTrace,
125
+ groupReport,
126
+ reduceStderr,
127
+ shortTail,
128
+ envelope,
129
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('./ansi.js');
4
+ const { stripAnsi } = ansi;
5
+ const model = require('./report-model.js');
6
+ const testParse = require('./report-test.js');
7
+ const toolParse = require('./report-tools.js');
8
+
9
+ const { problem, report, groupReport } = model;
10
+ const { reduceStderr, shortTail, envelope } = model;
11
+ const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
12
+ const { parseToolJson, parseToolText } = toolParse;
13
+
14
+ const KNOWN = ['test', 'tsc', 'eslint', 'prettier', 'audit', 'outdated', 'ls'];
15
+
16
+ const toolHint = (program, args) => {
17
+ const name = `${program ?? ''}`.split(/[/\\]/).pop();
18
+ const list = args || [];
19
+ const joined = [name, ...list].join(' ');
20
+ if (name === 'eslint') return 'eslint';
21
+ if (name === 'tsc') return 'tsc';
22
+ if (name === 'prettier') return 'prettier';
23
+ if (/\baudit\b/.test(joined)) return 'audit';
24
+ if (/\boutdated\b/.test(joined)) return 'outdated';
25
+ if (name === 'npm' || name === 'pnpm' || name === 'npm.cmd') {
26
+ if (/\b(ls|list)\b/.test(joined)) return 'ls';
27
+ }
28
+ if (name === 'node' && (args || []).includes('--test')) return 'test';
29
+ return '';
30
+ };
31
+
32
+ const parseJson = (text) => {
33
+ const trimmed = `${text ?? ''}`.trim();
34
+ if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
35
+ try {
36
+ return JSON.parse(trimmed);
37
+ } catch {
38
+ return null;
39
+ }
40
+ };
41
+
42
+ const parseJsonReports = (text, root) => {
43
+ const node = parseNodeJsonl(text, root);
44
+ if (node) return [node];
45
+ const data = parseJson(text);
46
+ if (!data) return null;
47
+ const test = parseTestJson(data, root);
48
+ if (test) return [test];
49
+ const tools = parseToolJson(data, root);
50
+ if (tools) return tools;
51
+ return [];
52
+ };
53
+
54
+ const parseReports = (text, root) => {
55
+ const json = parseJsonReports(text, root);
56
+ if (json) return json;
57
+ const reports = [];
58
+ const tap = parseTap(text, root);
59
+ if (tap) reports.push(tap);
60
+ const tools = parseToolText(text, root);
61
+ for (const item of tools) reports.push(item);
62
+ if (!tap) {
63
+ const spec = parseSpec(text, root);
64
+ if (spec) reports.push(spec);
65
+ }
66
+ return reports;
67
+ };
68
+
69
+ const withHint = (reports, text, exit, hint) => {
70
+ if (reports.length || !`${text ?? ''}`.trim() || exit === 0) return reports;
71
+ const tool = KNOWN.includes(hint) ? hint : 'test';
72
+ return [report(tool, { problems: [problem({ message: shortTail(text) })] })];
73
+ };
74
+
75
+ const buildDocument = (stdout, stderr, root, exit, hint) => {
76
+ const text = stripAnsi(stdout);
77
+ const reports = withHint(parseReports(text, root), text, exit, hint);
78
+ const grouped = new Array(reports.length);
79
+ for (let i = 0; i < reports.length; i++) {
80
+ grouped[i] = groupReport(reports[i]);
81
+ }
82
+ return envelope(grouped, exit, reduceStderr(stderr, root));
83
+ };
84
+
85
+ module.exports = {
86
+ toolHint,
87
+ parseReports,
88
+ buildDocument,
89
+ };