reslop 0.2.5 → 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
@@ -6,7 +6,7 @@ const { isPassingTest } = npmCommands;
6
6
  const utilities = require('./utilities.js');
7
7
  const { unquote } = utilities;
8
8
 
9
- const { summary, problem, report, userTrace } = model;
9
+ const { summary, problem, report, userTrace, errorKey } = model;
10
10
 
11
11
  const KNOWN = [
12
12
  'message',
@@ -22,8 +22,38 @@ const KNOWN = [
22
22
  'location',
23
23
  'severity',
24
24
  'type',
25
+ 'at',
26
+ 'found',
27
+ 'wanted',
28
+ 'got',
29
+ 'expect',
25
30
  ];
26
31
 
32
+ const HIDDEN = ['generatedMessage', 'diff'];
33
+
34
+ const ERROR_HEAD = /^([\w.]*Error)(?: \[([^\]]+)\])?:\s*/;
35
+
36
+ const exceptionFields = (lines) => {
37
+ const rows = lines.map((line) => line.replace(/^\s*# ?/, ''));
38
+ const header = rows.findIndex((line) => ERROR_HEAD.test(line.trim()));
39
+ const frame = rows.findIndex((line) => /^\s*at\s/.test(line));
40
+ const start = header < 0 ? 0 : header;
41
+ const end = frame < 0 ? rows.length : frame;
42
+ const text = rows
43
+ .slice(start, end)
44
+ .map((line) => line.trim())
45
+ .join('\n');
46
+ const match = ERROR_HEAD.exec(text);
47
+ const message = match ? text.slice(match[0].length).trim() : text.trim();
48
+ const stack = frame < 0 ? '' : rows.slice(frame).join('\n');
49
+ return [
50
+ ['message', message],
51
+ ['name', match ? match[1] : ''],
52
+ ['code', match ? match[2] || '' : ''],
53
+ ['stack', stack],
54
+ ];
55
+ };
56
+
27
57
  const show = (value) => {
28
58
  if (value === null || value === undefined) return '';
29
59
  if (typeof value === 'string') return value;
@@ -36,7 +66,7 @@ const show = (value) => {
36
66
  const extraFrom = (fields) => {
37
67
  const extra = [];
38
68
  for (const field of fields) {
39
- if (KNOWN.includes(field[0])) continue;
69
+ if (KNOWN.includes(field[0]) || HIDDEN.includes(field[0])) continue;
40
70
  const value = show(field[1]);
41
71
  if (!value) continue;
42
72
  const shown = value.length > 500 ? `${value.slice(0, 500)}…` : value;
@@ -45,30 +75,70 @@ const extraFrom = (fields) => {
45
75
  return extra;
46
76
  };
47
77
 
48
- const fieldMap = (fields) => {
49
- const map = new Map();
50
- for (const field of fields) map.set(field[0], field[1]);
51
- return map;
52
- };
53
-
54
- const problemFromFields = (name, fields, root) => {
55
- const map = fieldMap(fields);
56
- const message = show(map.get('error') || map.get('message'));
78
+ const problemFromFields = (name, fields, root, fallback = '') => {
79
+ const map = new Map(fields);
57
80
  const stack = show(map.get('stack'));
58
- const location = show(map.get('location'));
59
- const trace = userTrace(stack || location, root);
81
+ const parsed = new Map(exceptionFields(stack.split(/\r?\n/)));
82
+ const text = show(
83
+ map.get('error') || map.get('message') || parsed.get('message'),
84
+ );
85
+ const header = ERROR_HEAD.exec(text);
86
+ const message = header ? text.slice(header[0].length).trim() : text;
87
+ const location = show(map.get('at') || map.get('location'));
60
88
  const duration = show(map.get('duration_ms') || map.get('duration'));
61
- return problem({
89
+ const details = {
62
90
  name,
63
91
  message,
64
- trace,
65
92
  duration,
66
- code: show(map.get('code')),
93
+ code: show(map.get('code') || (header && header[2]) || parsed.get('code')),
67
94
  operator: show(map.get('operator')),
68
- expected: show(map.get('expected')),
69
- actual: show(map.get('actual')),
95
+ expected: show(
96
+ map.get('expected') ?? map.get('wanted') ?? map.get('expect'),
97
+ ),
98
+ actual: show(map.get('actual') ?? map.get('found') ?? map.get('got')),
70
99
  extra: extraFrom(fields),
71
- });
100
+ };
101
+ const type = show(
102
+ map.get('name') || (header && header[1]) || parsed.get('name'),
103
+ );
104
+ if (type) details.extra.unshift(['type', type]);
105
+ const known =
106
+ message ||
107
+ stack ||
108
+ location ||
109
+ details.operator ||
110
+ details.code ||
111
+ details.expected ||
112
+ details.actual;
113
+ if (!known) details.message = fallback;
114
+ const source = show(map.get('at')) || stack || location;
115
+ const key = errorKey({ ...details, type }, source, root);
116
+ const trace = userTrace(parsed.get('stack') || stack || location, root);
117
+ return problem({ ...details, key, trace });
118
+ };
119
+
120
+ const quoted = (value) => value.startsWith('"') || value.startsWith('\x27');
121
+
122
+ const scalarText = (value) => {
123
+ if (value.startsWith('"')) {
124
+ try {
125
+ return JSON.parse(value.replace(/\r?\n/g, '\\n'));
126
+ } catch {
127
+ return unquote(value);
128
+ }
129
+ }
130
+ return unquote(value);
131
+ };
132
+
133
+ const blockText = (lines) => {
134
+ let indent = Infinity;
135
+ for (const line of lines) {
136
+ if (line.trim()) indent = Math.min(indent, line.match(/^\s*/)[0].length);
137
+ }
138
+ return lines
139
+ .map((line) => line.slice(indent))
140
+ .join('\n')
141
+ .trimEnd();
72
142
  };
73
143
 
74
144
  const yamlFields = (lines) => {
@@ -81,38 +151,53 @@ const yamlFields = (lines) => {
81
151
  continue;
82
152
  }
83
153
  const key = match[2];
84
- const value = match[3].trim().replace(/,$/, '');
154
+ let value = match[3].trim().replace(/,$/, '');
85
155
  const block = value === '|-' || value === '|' || value === '>';
86
- if (!block) {
87
- fields.push([key, unquote(value)]);
88
- index += 1;
156
+ index += 1;
157
+ if (key === 'error' && !value) continue;
158
+ if (quoted(value)) {
159
+ const quote = value[0];
160
+ while (
161
+ index < lines.length &&
162
+ (value.length < 2 || value.at(-1) !== quote)
163
+ ) {
164
+ value = `${value}\n${lines[index]}`;
165
+ index += 1;
166
+ }
167
+ fields.push([key, scalarText(value)]);
89
168
  continue;
90
169
  }
91
170
  const rows = [];
92
- index += 1;
93
- while (index < lines.length && /^\s+\S/.test(lines[index])) {
94
- rows.push(lines[index].trim());
171
+ const indent = match[1].length;
172
+ while (index < lines.length) {
173
+ const line = lines[index];
174
+ if (line.trim() && line.match(/^\s*/)[0].length <= indent) break;
175
+ rows.push(lines[index]);
95
176
  index += 1;
96
177
  }
97
- fields.push([key, rows.join('\n')]);
178
+ if (block || rows.some((line) => line.trim())) {
179
+ const text = blockText(rows);
180
+ fields.push([key, block ? text : `${value}\n${text}`.trim()]);
181
+ } else {
182
+ fields.push([key, scalarText(value)]);
183
+ }
98
184
  }
99
185
  return fields;
100
186
  };
101
187
 
102
- const readYaml = (lines, start) => {
103
- const body = [];
104
- let index = start + 1;
105
- for (; index < lines.length; index++) {
106
- if (lines[index].trim() === '...') break;
107
- body.push(lines[index]);
108
- }
109
- return { fields: yamlFields(body), next: index + 1 };
110
- };
111
-
112
- const tapName = (line) => {
113
- const match = /^(?:not )?ok\s+\d+\s+-?\s*(.*)$/.exec(line.trim());
114
- if (!match) return line.trim();
115
- return match[1].trim();
188
+ const tapPoint = (line) => {
189
+ const match = /^\s*(not ok|ok)\b(?:\s+\d+)?(?:\s+-)?(?:\s+(.*))?$/.exec(line);
190
+ if (!match) return null;
191
+ const text = match[2] || '';
192
+ return {
193
+ passed: match[1] === 'ok',
194
+ name: text
195
+ .split(/\s+#/)[0]
196
+ .replace(/\s*\{\s*$/, '')
197
+ .trim(),
198
+ skipped: /#\s*(SKIP|TODO)\b/i.test(text),
199
+ container: /\{\s*$/.test(text),
200
+ };
116
201
  };
117
202
 
118
203
  const STAT_FIELD = {
@@ -137,21 +222,57 @@ const applyTapComment = (stats, line) => {
137
222
  const isTapLine = (line) => {
138
223
  const text = line.trim();
139
224
  if (text.startsWith('TAP version')) return true;
140
- if (/^(not )?ok\s+\d+\b/.test(text)) return true;
225
+ if (tapPoint(text)) return true;
141
226
  if (/^1\.\.\d+/.test(text)) return true;
142
227
  if (text.startsWith('Bail out!')) return true;
143
228
  return false;
144
229
  };
145
230
 
231
+ const tapBoundary = (line) => {
232
+ if (isTapLine(line) || line.trim() === '}') return true;
233
+ const stats =
234
+ /^\s*#\s+(?:Subtest:|(?:tests|pass|fail|skipped|duration_ms)\s+\d)/;
235
+ return stats.test(line);
236
+ };
237
+
238
+ const readYaml = (lines, start) => {
239
+ const body = [];
240
+ const indent = lines[start].match(/^\s*/)[0].length;
241
+ let index = start + 1;
242
+ for (; index < lines.length; index++) {
243
+ if (lines[index].trim() === '...') break;
244
+ const lead = lines[index].match(/^\s*/)[0].length;
245
+ if (lead <= indent && tapBoundary(lines[index])) break;
246
+ body.push(lines[index]);
247
+ }
248
+ const end = lines[index] && lines[index].trim() === '...';
249
+ return { fields: yamlFields(body), next: end ? index + 1 : index };
250
+ };
251
+
252
+ const readTapFields = (lines, start) => {
253
+ const fields = [];
254
+ const body = [];
255
+ let index = start;
256
+ while (index < lines.length && !tapBoundary(lines[index])) {
257
+ if (lines[index].trim() === '---') {
258
+ const yaml = readYaml(lines, index);
259
+ fields.push(...yaml.fields);
260
+ index = yaml.next;
261
+ } else {
262
+ body.push(lines[index]);
263
+ index += 1;
264
+ }
265
+ }
266
+ if (!fields.length) fields.push(...exceptionFields(body));
267
+ return { fields, next: index };
268
+ };
269
+
146
270
  const parseTap = (text, root) => {
147
271
  const lines = `${text ?? ''}`.split(/\r?\n/);
148
- let seen = false;
149
- for (const line of lines) {
150
- if (isTapLine(line)) seen = true;
151
- }
152
- if (!seen) return null;
272
+ if (!lines.some(isTapLine)) return null;
153
273
  const stats = summary();
154
274
  const problems = [];
275
+ const counts = { tests: 0, passed: 0, failed: 0, skipped: 0 };
155
276
  let index = 0;
156
277
  while (index < lines.length) {
157
278
  const line = lines[index];
@@ -160,73 +281,67 @@ const parseTap = (text, root) => {
160
281
  if (trimmed.startsWith('Bail out!')) {
161
282
  problems.push(problem({ name: 'Bail out!', message: trimmed }));
162
283
  }
163
- const failed = /^not ok\s+\d+\b/.test(trimmed);
164
- const passed = /^ok\s+\d+\b/.test(trimmed);
165
- if (!failed && !passed) {
284
+ const point = tapPoint(line);
285
+ if (!point || point.container) {
166
286
  index += 1;
167
287
  continue;
168
288
  }
169
- const name = tapName(trimmed);
170
- let fields = [];
171
- if (lines[index + 1] && lines[index + 1].trim() === '---') {
172
- const yaml = readYaml(lines, index + 1);
173
- fields = yaml.fields;
174
- index = yaml.next;
289
+ const body = readTapFields(lines, index + 1);
290
+ index = body.next;
291
+ const failure = new Map(body.fields).get('failureType');
292
+ if (failure === 'subtestsFailed') continue;
293
+ counts.tests += 1;
294
+ if (point.skipped) {
295
+ counts.skipped += 1;
296
+ } else if (point.passed) {
297
+ counts.passed += 1;
175
298
  } else {
176
- index += 1;
299
+ counts.failed += 1;
300
+ problems.push(
301
+ problemFromFields(point.name, body.fields, root, point.name),
302
+ );
177
303
  }
178
- if (failed) problems.push(problemFromFields(name, fields, root));
179
304
  }
180
- return report('test', { summary: stats, problems });
181
- };
182
-
183
- const countStatus = (stats, status) => {
184
- if (status === 'passed' || status === 'pass') {
185
- stats.passed = (stats.passed || 0) + 1;
186
- return;
187
- }
188
- if (status === 'failed' || status === 'fail' || status === 'timedOut') {
189
- stats.failed = (stats.failed || 0) + 1;
190
- return;
191
- }
192
- if (status === 'skipped' || status === 'pending' || status === 'todo') {
193
- stats.skipped = (stats.skipped || 0) + 1;
305
+ for (const key of Object.keys(counts)) {
306
+ if (stats[key] === null) stats[key] = counts[key];
194
307
  }
308
+ return report('test', { summary: stats, problems });
195
309
  };
196
310
 
197
311
  const nodeEventProblem = (event, root) => {
198
312
  const data = event.data || {};
199
313
  const details = data.details || {};
200
314
  const error = details.error || {};
201
- const fields = [];
202
- for (const key of Object.keys(error)) fields.push([key, error[key]]);
315
+ const fields = Object.entries(error);
203
316
  if (details.duration_ms !== undefined) {
204
317
  fields.push(['duration_ms', details.duration_ms]);
205
318
  }
206
- const name = data.name || '';
207
- return problemFromFields(name, fields, root);
319
+ return problemFromFields(data.name || '', fields, root);
208
320
  };
209
321
 
210
322
  const parseNodeEvents = (events, root) => {
211
- const stats = summary();
212
323
  const problems = [];
213
324
  let seen = false;
325
+ let passed = 0;
326
+ let failed = 0;
327
+ let skipped = 0;
214
328
  for (const event of events) {
215
329
  const type = event && event.type;
216
330
  if (typeof type !== 'string' || !type.startsWith('test:')) continue;
217
331
  seen = true;
218
- if (type === 'test:pass') countStatus(stats, 'passed');
332
+ if (type === 'test:pass') passed += 1;
333
+ if (type === 'test:skip' || type === 'test:todo') skipped += 1;
219
334
  if (type === 'test:fail') {
220
- countStatus(stats, 'failed');
335
+ failed += 1;
221
336
  problems.push(nodeEventProblem(event, root));
222
337
  }
223
- if (type === 'test:skip' || type === 'test:todo') {
224
- countStatus(stats, 'skipped');
225
- }
226
338
  }
227
339
  if (!seen) return null;
228
- const skipped = stats.skipped || 0;
229
- stats.tests = (stats.passed || 0) + (stats.failed || 0) + skipped;
340
+ const stats = summary();
341
+ stats.tests = passed + failed + skipped;
342
+ stats.passed = passed || null;
343
+ stats.failed = failed || null;
344
+ stats.skipped = skipped || null;
230
345
  return report('test', { summary: stats, problems });
231
346
  };
232
347
 
@@ -384,19 +499,17 @@ const readSpecObject = (lines, start) => {
384
499
 
385
500
  const pushSpec = (problems, current, root) => {
386
501
  if (!current) return;
387
- const fields = current.fields;
388
- const item = problemFromFields(current.name, fields, root);
389
- if (current.message && !item.message) item.message = current.message;
390
- const trace = userTrace(current.trace.join('\n'), root);
391
- if (trace) item.trace = trace;
392
- if (current.expected) item.expected = current.expected;
393
- if (current.actual) item.actual = current.actual;
394
- problems.push(item);
502
+ const fields = [...current.fields];
503
+ if (current.message) fields.push(['error', current.message.trimEnd()]);
504
+ if (current.trace.length) fields.push(['stack', current.trace.join('\n')]);
505
+ if (current.expected) fields.push(['expected', current.expected]);
506
+ if (current.actual) fields.push(['actual', current.actual]);
507
+ problems.push(problemFromFields(current.name, fields, root));
395
508
  };
396
509
 
397
510
  const takeSpecLine = (current, line) => {
398
511
  const text = line.trim();
399
- if (/^(AssertionError|Error)\b/.test(text)) {
512
+ if (/^[\w.]*Error(?: \[[^\]]+\])?:/.test(text)) {
400
513
  current.message = text;
401
514
  return;
402
515
  }
@@ -404,6 +517,9 @@ const takeSpecLine = (current, line) => {
404
517
  if (expected) current.expected = expected[1];
405
518
  const received = /^Received:\s*(.*)$/.exec(text);
406
519
  if (received) current.actual = received[1];
520
+ if (current.message && !current.trace.length) {
521
+ current.message = `${current.message}\n${text}`;
522
+ }
407
523
  };
408
524
 
409
525
  const takeSpecFrame = (lines, index, current) => {
@@ -433,12 +549,8 @@ const dropPreviews = (problems) => {
433
549
  for (const item of problems) {
434
550
  if (item.name && hasDetail(item)) named.add(item.name);
435
551
  }
436
- const kept = [];
437
- for (const item of problems) {
438
- if (item.name && named.has(item.name) && !hasDetail(item)) continue;
439
- kept.push(item);
440
- }
441
- return kept;
552
+ const isPreview = (item) => named.has(item.name) && !hasDetail(item);
553
+ return problems.filter((item) => !isPreview(item));
442
554
  };
443
555
 
444
556
  const parseSpec = (text, root) => {
@@ -447,9 +559,19 @@ const parseSpec = (text, root) => {
447
559
  const problems = [];
448
560
  let current = null;
449
561
  let marked = false;
562
+ let summarized = false;
450
563
  let index = 0;
451
564
  while (index < lines.length) {
452
565
  const line = lines[index];
566
+ if (specHeader(line)) {
567
+ pushSpec(problems, current, root);
568
+ current = null;
569
+ if (!summarized) problems.length = 0;
570
+ summarized = true;
571
+ marked = true;
572
+ index += 1;
573
+ continue;
574
+ }
453
575
  if (specStats(stats, line)) marked = true;
454
576
  if (isPassingTest(line)) {
455
577
  marked = true;