reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. 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;