reslop 0.1.8 → 0.2.0

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 (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
@@ -0,0 +1,712 @@
1
+ 'use strict';
2
+
3
+ const { jsonParse } = require('metautil');
4
+ const { itemPath } = require('./files.js');
5
+ const {
6
+ countSides,
7
+ formatHunkHeader,
8
+ formatPatch,
9
+ } = require('./diff/patch.js');
10
+ const { isCtxType } = require('./diff/diff.js');
11
+ const manifest = require('./manifest.js');
12
+ const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
13
+ const { listFilePath, readSections, emptySectionMaps } = manifest;
14
+ const { lockRootSections, lockEntries, lockPackageCount } = manifest;
15
+
16
+ const SECTION_ORDER = [...SECTION_KEYS, 'resolved', 'field'];
17
+ const ACTION_ORDER = ['added', 'removed', 'changed', 'vulnerable'];
18
+ const SECTION_NAMES = SECTION_KEYS.join('|');
19
+ const SECTION_OPEN = new RegExp(`^\\s*"(${SECTION_NAMES})"\\s*:\\s*\\{\\s*$`);
20
+ const SECTION_END = /^\s*\},?\s*$/;
21
+ const DEP_ENTRY = /^\s*"([^"]+)"\s*:\s*"([^"]*)"\s*,?\s*$/;
22
+
23
+ const diffMaps = (oldMap, newMap, section) => {
24
+ const keys = [...oldMap.keys(), ...newMap.keys()];
25
+ const names = [...new Set(keys)];
26
+ names.sort();
27
+ const changes = [];
28
+ for (const name of names) {
29
+ const from = oldMap.has(name) ? oldMap.get(name) : '';
30
+ const to = newMap.has(name) ? newMap.get(name) : '';
31
+ if (from === to) continue;
32
+ let action = 'changed';
33
+ if (!oldMap.has(name)) action = 'added';
34
+ else if (!newMap.has(name)) action = 'removed';
35
+ changes.push({ name, section, action, from, to });
36
+ }
37
+ return changes;
38
+ };
39
+
40
+ const diffSections = (oldMaps, newMaps) => {
41
+ const changes = [];
42
+ for (const key of SECTION_KEYS) {
43
+ const found = diffMaps(oldMaps[key], newMaps[key], key);
44
+ for (const change of found) changes.push(change);
45
+ }
46
+ return changes;
47
+ };
48
+
49
+ const fieldText = (value) => {
50
+ if (value === undefined) return '';
51
+ if (typeof value === 'string') return value;
52
+ return JSON.stringify(value);
53
+ };
54
+
55
+ const diffOtherFields = (oldPkg, newPkg) => {
56
+ if (!oldPkg && !newPkg) return [];
57
+ const oldObj = oldPkg ?? {};
58
+ const newObj = newPkg ?? {};
59
+ const keys = [...Object.keys(oldObj), ...Object.keys(newObj)];
60
+ const names = [...new Set(keys)];
61
+ names.sort();
62
+ const changes = [];
63
+ for (const name of names) {
64
+ if (SECTION_KEYS.includes(name)) continue;
65
+ const prev = oldObj[name];
66
+ const next = newObj[name];
67
+ if (JSON.stringify(prev) === JSON.stringify(next)) continue;
68
+ let action = 'changed';
69
+ if (!Object.hasOwn(oldObj, name)) action = 'added';
70
+ else if (!Object.hasOwn(newObj, name)) action = 'removed';
71
+ const from = fieldText(prev);
72
+ const to = fieldText(next);
73
+ const section = 'field';
74
+ changes.push({ name, section, action, from, to });
75
+ }
76
+ return changes;
77
+ };
78
+
79
+ const foldedDepNames = (changes) => {
80
+ const names = new Set();
81
+ for (const change of changes) {
82
+ if (change.section === 'field') continue;
83
+ names.add(change.name);
84
+ }
85
+ return names;
86
+ };
87
+
88
+ const lineEntryName = (text) => {
89
+ const entry = DEP_ENTRY.exec(text);
90
+ return entry ? entry[1] : '';
91
+ };
92
+
93
+ const lineInBlock = (item, line) => {
94
+ const blockId = item.blockId;
95
+ if (typeof blockId !== 'number') return true;
96
+ return line.blockId === blockId;
97
+ };
98
+
99
+ const hunkOnlyFoldedDeps = (item, names) => {
100
+ if (!item.hunk || !names.size) return false;
101
+ let folded = 0;
102
+ for (const line of item.hunk.lines) {
103
+ if (line.type === 'ctx') continue;
104
+ if (!lineInBlock(item, line)) continue;
105
+ const name = lineEntryName(line.text);
106
+ if (name && names.has(name)) {
107
+ folded += 1;
108
+ continue;
109
+ }
110
+ if (SECTION_OPEN.test(line.text) || SECTION_END.test(line.text)) continue;
111
+ return false;
112
+ }
113
+ return folded > 0;
114
+ };
115
+
116
+ const stripFoldedDepItem = (item, names) => {
117
+ if (!item.hunk || !names.size) return item;
118
+ const lines = [];
119
+ let removed = false;
120
+ for (const line of item.hunk.lines) {
121
+ if (line.type !== 'ctx') {
122
+ const name = lineEntryName(line.text);
123
+ if (name && names.has(name)) {
124
+ removed = true;
125
+ continue;
126
+ }
127
+ }
128
+ lines.push(line);
129
+ }
130
+ if (!removed) return item;
131
+ let hasEdit = false;
132
+ for (const line of lines) {
133
+ if (line.type === 'ctx') continue;
134
+ if (!lineInBlock(item, line)) continue;
135
+ hasEdit = true;
136
+ break;
137
+ }
138
+ if (!hasEdit) return null;
139
+ const counts = countSides(lines);
140
+ const hunk = {
141
+ ...item.hunk,
142
+ oldCount: counts.oldCount,
143
+ newCount: counts.newCount,
144
+ header: formatHunkHeader(
145
+ item.hunk.oldStart,
146
+ counts.oldCount,
147
+ item.hunk.newStart,
148
+ counts.newCount,
149
+ ),
150
+ lines,
151
+ };
152
+ const patchAdd = formatPatch(item.file, hunk, item.blockId, 'old');
153
+ const patchRevert = formatPatch(item.file, hunk, item.blockId, 'new');
154
+ return { ...item, hunk, patchAdd, patchRevert };
155
+ };
156
+
157
+ const onlyDepEdits = (items) => {
158
+ if (!items.length) return false;
159
+ for (const item of items) {
160
+ if (!item.hunk) return false;
161
+ const blockId = item.blockId;
162
+ const scoped = typeof blockId === 'number';
163
+ let section = '';
164
+ for (const line of item.hunk.lines) {
165
+ const open = SECTION_OPEN.exec(line.text);
166
+ let nextSection = section;
167
+ if (open) nextSection = open[1];
168
+ else if (section && SECTION_END.test(line.text)) nextSection = '';
169
+ const inBlock = !scoped || line.blockId === blockId;
170
+ if (inBlock && line.type !== 'ctx') {
171
+ const inDep = !!(open || section);
172
+ if (!inDep) return false;
173
+ const isEntry = DEP_ENTRY.test(line.text);
174
+ const isClose = SECTION_END.test(line.text);
175
+ if (!open && !isEntry && !isClose) return false;
176
+ }
177
+ section = nextSection;
178
+ }
179
+ }
180
+ return true;
181
+ };
182
+
183
+ const mapsFromHunks = (items) => {
184
+ const oldMaps = emptySectionMaps();
185
+ const newMaps = emptySectionMaps();
186
+ let section = '';
187
+ for (const item of items) {
188
+ if (!item.hunk) continue;
189
+ for (const line of item.hunk.lines) {
190
+ const open = SECTION_OPEN.exec(line.text);
191
+ if (open) {
192
+ section = open[1];
193
+ continue;
194
+ }
195
+ if (section && SECTION_END.test(line.text)) {
196
+ section = '';
197
+ continue;
198
+ }
199
+ if (!section) continue;
200
+ const entry = DEP_ENTRY.exec(line.text);
201
+ if (!entry) continue;
202
+ const name = entry[1];
203
+ const version = entry[2];
204
+ if (line.type === 'del' || line.type === 'ctx') {
205
+ oldMaps[section].set(name, version);
206
+ }
207
+ if (line.type === 'add' || line.type === 'ctx') {
208
+ newMaps[section].set(name, version);
209
+ }
210
+ }
211
+ }
212
+ return { oldMaps, newMaps };
213
+ };
214
+
215
+ const directNames = (oldMaps, newMaps) => {
216
+ const names = new Set();
217
+ for (const key of SECTION_KEYS) {
218
+ for (const name of oldMaps[key].keys()) names.add(name);
219
+ for (const name of newMaps[key].keys()) names.add(name);
220
+ }
221
+ return names;
222
+ };
223
+
224
+ const mergeResolved = (changes, oldResolved, newResolved, oldMaps, newMaps) => {
225
+ const seen = new Set();
226
+ const { length } = changes;
227
+ const merged = new Array(length);
228
+ for (let i = 0; i < length; i++) {
229
+ const change = changes[i];
230
+ seen.add(change.name);
231
+ const resolvedFrom = oldResolved.get(change.name) ?? '';
232
+ const resolvedTo = newResolved.get(change.name) ?? '';
233
+ merged[i] = { ...change, resolvedFrom, resolvedTo };
234
+ }
235
+ const names = [...directNames(oldMaps, newMaps)];
236
+ names.sort();
237
+ for (const name of names) {
238
+ if (seen.has(name)) continue;
239
+ const from = oldResolved.get(name) ?? '';
240
+ const to = newResolved.get(name) ?? '';
241
+ if (from === to) continue;
242
+ if (!from && !to) continue;
243
+ let action = 'changed';
244
+ if (!from) action = 'added';
245
+ else if (!to) action = 'removed';
246
+ const section = 'resolved';
247
+ const resolvedFrom = from;
248
+ const resolvedTo = to;
249
+ merged.push({ name, section, action, from, to, resolvedFrom, resolvedTo });
250
+ }
251
+ return merged;
252
+ };
253
+
254
+ const compareChangeOrder = (left, right) => {
255
+ const section = SECTION_ORDER.indexOf(left.section);
256
+ const other = SECTION_ORDER.indexOf(right.section);
257
+ if (section !== other) return section - other;
258
+ const leftAction = ACTION_ORDER.indexOf(left.action);
259
+ const rightAction = ACTION_ORDER.indexOf(right.action);
260
+ if (leftAction !== rightAction) return leftAction - rightAction;
261
+ return left.name.localeCompare(right.name);
262
+ };
263
+
264
+ const orderedChanges = (changes) => [...changes].sort(compareChangeOrder);
265
+
266
+ const kindItems = (items, kind) => {
267
+ const matched = [];
268
+ for (const item of items) {
269
+ const rel = itemPath(item);
270
+ const meta = depFileMeta(rel);
271
+ if (meta && meta.kind === kind) matched.push(item);
272
+ }
273
+ return matched;
274
+ };
275
+
276
+ const readGroupSnapshot = (group, readSides) => {
277
+ const origin = group.origin;
278
+ const dir = group.dir;
279
+ const groupItems = group.items;
280
+ const manifestItems = kindItems(groupItems, 'manifest');
281
+ const lockItems = kindItems(groupItems, 'lockfile');
282
+ const manifestRel = relOf(dir, MANIFEST);
283
+ const lockRel = relOf(dir, LOCKFILE);
284
+ const manifestSides = readSides(origin, manifestRel);
285
+ const lockSides = readSides(origin, lockRel);
286
+ const oldPkg = jsonParse(manifestSides.oldText ?? '');
287
+ const newPkg = jsonParse(manifestSides.newText ?? '');
288
+ const oldLock = jsonParse(lockSides.oldText ?? '');
289
+ const newLock = jsonParse(lockSides.newText ?? '');
290
+ const parsedManifest = !!(oldPkg || newPkg);
291
+ return {
292
+ origin,
293
+ dir,
294
+ groupItems,
295
+ manifestItems,
296
+ lockItems,
297
+ oldPkg,
298
+ newPkg,
299
+ oldLock,
300
+ newLock,
301
+ parsedManifest,
302
+ };
303
+ };
304
+
305
+ const sectionMapsOf = (snapshot) => {
306
+ if (snapshot.parsedManifest) {
307
+ const oldMaps = readSections(snapshot.oldPkg);
308
+ const newMaps = readSections(snapshot.newPkg);
309
+ return { oldMaps, newMaps };
310
+ }
311
+ if (snapshot.manifestItems.length) {
312
+ return mapsFromHunks(snapshot.manifestItems);
313
+ }
314
+ const oldMaps = lockRootSections(snapshot.oldLock);
315
+ const newMaps = lockRootSections(snapshot.newLock);
316
+ return { oldMaps, newMaps };
317
+ };
318
+
319
+ const comparedChanges = (snapshot) => {
320
+ const maps = sectionMapsOf(snapshot);
321
+ const oldMaps = maps.oldMaps;
322
+ const newMaps = maps.newMaps;
323
+ const manifestChanges = diffSections(oldMaps, newMaps);
324
+ const parsed = snapshot.parsedManifest;
325
+ let otherChanges = [];
326
+ if (parsed) {
327
+ otherChanges = diffOtherFields(snapshot.oldPkg, snapshot.newPkg);
328
+ }
329
+ const oldResolved = lockEntries(snapshot.oldLock);
330
+ const newResolved = lockEntries(snapshot.newLock);
331
+ const changes = mergeResolved(
332
+ manifestChanges,
333
+ oldResolved,
334
+ newResolved,
335
+ oldMaps,
336
+ newMaps,
337
+ );
338
+ const oldCount = lockPackageCount(snapshot.oldLock);
339
+ const newCount = lockPackageCount(snapshot.newLock);
340
+ return { changes, otherChanges, oldCount, newCount };
341
+ };
342
+
343
+ const foldingPolicy = (snapshot, compared) => {
344
+ const foldLock = snapshot.lockItems.length > 0;
345
+ const hasDepChanges = compared.changes.length > 0;
346
+ if (!snapshot.manifestItems.length) {
347
+ const foldManifest = false;
348
+ const takeAllManifest = false;
349
+ return { foldLock, foldManifest, takeAllManifest };
350
+ }
351
+ if (snapshot.parsedManifest) {
352
+ const foldManifest = hasDepChanges;
353
+ const takeAllManifest = hasDepChanges && compared.otherChanges.length === 0;
354
+ return { foldLock, foldManifest, takeAllManifest };
355
+ }
356
+ const foldManifest = hasDepChanges && onlyDepEdits(snapshot.manifestItems);
357
+ const takeAllManifest = foldManifest;
358
+ return { foldLock, foldManifest, takeAllManifest };
359
+ };
360
+
361
+ const summarizeGroup = (group, readSides) => {
362
+ const snapshot = readGroupSnapshot(group, readSides);
363
+ const compared = comparedChanges(snapshot);
364
+ const policy = foldingPolicy(snapshot, compared);
365
+ return {
366
+ changes: compared.changes,
367
+ oldCount: compared.oldCount,
368
+ newCount: compared.newCount,
369
+ foldLock: policy.foldLock,
370
+ foldManifest: policy.foldManifest,
371
+ takeAllManifest: policy.takeAllManifest,
372
+ manifestItems: snapshot.manifestItems,
373
+ lockItems: snapshot.lockItems,
374
+ pkg: snapshot.newPkg || snapshot.oldPkg,
375
+ };
376
+ };
377
+
378
+ const collectGroups = (items) => {
379
+ const groups = new Map();
380
+ for (const item of items) {
381
+ const rel = itemPath(item);
382
+ const meta = depFileMeta(rel);
383
+ if (!meta) continue;
384
+ const key = `${item.origin}\0${meta.dir}`;
385
+ let group = groups.get(key);
386
+ if (!group) {
387
+ const dir = meta.dir;
388
+ const origin = item.origin;
389
+ group = { dir, origin, items: [] };
390
+ groups.set(key, group);
391
+ }
392
+ group.items.push(item);
393
+ }
394
+ return groups;
395
+ };
396
+
397
+ const takeManifestItems = (summary) => {
398
+ if (!summary.foldManifest) return [];
399
+ if (summary.takeAllManifest) return [...summary.manifestItems];
400
+ const names = foldedDepNames(summary.changes);
401
+ const taken = [];
402
+ for (const item of summary.manifestItems) {
403
+ if (onlyDepEdits([item]) || hunkOnlyFoldedDeps(item, names)) {
404
+ taken.push(item);
405
+ }
406
+ }
407
+ return taken;
408
+ };
409
+
410
+ const DEP_CAPTION = 'Dependencies in package.json & package-lock.json';
411
+
412
+ const changeMarks = (change) => {
413
+ const marks = [];
414
+ if (change.unused) marks.push('unused');
415
+ if (change.outdated && !change.propose) marks.push('outdated');
416
+ if (change.audit) marks.push(change.audit.severity);
417
+ return marks;
418
+ };
419
+
420
+ const changeSources = (change) => {
421
+ const sources = [];
422
+ if (change.unused) {
423
+ sources.push('npm uninstall');
424
+ return sources;
425
+ }
426
+ if (change.propose && change.outdated) sources.push('npm outdated');
427
+ if (change.audit) sources.push('npm audit');
428
+ return sources;
429
+ };
430
+
431
+ const sectionTitle = (section, action, marks, sources) => {
432
+ const title = action === 'changed' ? 'version changed' : action;
433
+ const kind = section.replace(/ies$/, 'y');
434
+ let label = `${kind} ${title}`;
435
+ if (section === 'field') label = `package.json ${title}`;
436
+ if (section === 'resolved' && action === 'vulnerable') {
437
+ label = 'lockfile vulnerable';
438
+ } else if (section === 'resolved') {
439
+ label = `lockfile resolved ${title}`;
440
+ }
441
+ if (marks.includes('unused')) {
442
+ label = `${kind} unused`;
443
+ const extra = [];
444
+ for (const mark of marks) {
445
+ if (mark !== 'unused') extra.push(mark);
446
+ }
447
+ if (extra.length) label = `${label}, ${extra.join(', ')}`;
448
+ } else if (marks.length) {
449
+ label = `${label}, ${marks.join(', ')}`;
450
+ }
451
+ if (!sources.length) return label;
452
+ return `${sources.join(', ')}: ${label}`;
453
+ };
454
+
455
+ const entryText = (change, side) => {
456
+ const version = side === 'old' ? change.from : change.to;
457
+ return `"${change.name}": "${version}"`;
458
+ };
459
+
460
+ const ctxLine = (text) => {
461
+ const type = 'ctx';
462
+ return { type, text };
463
+ };
464
+
465
+ const warnLine = (text) => {
466
+ const type = 'warn';
467
+ return { type, text };
468
+ };
469
+
470
+ const noteLine = (text) => {
471
+ const type = 'note';
472
+ return { type, text };
473
+ };
474
+
475
+ const noteSepLine = () => {
476
+ const type = 'noteSep';
477
+ const text = '';
478
+ return { type, text };
479
+ };
480
+
481
+ const changeLine = (type, change, side) => {
482
+ const text = entryText(change, side);
483
+ return { type, text };
484
+ };
485
+
486
+ const pushChangeLines = (rows, change) => {
487
+ if (change.unused) {
488
+ const side = change.action === 'added' ? 'new' : 'old';
489
+ rows.push(changeLine('del', change, side));
490
+ return;
491
+ }
492
+ if (change.action === 'added') {
493
+ rows.push(changeLine('add', change, 'new'));
494
+ return;
495
+ }
496
+ if (change.action === 'removed') {
497
+ rows.push(changeLine('del', change, 'old'));
498
+ return;
499
+ }
500
+ if (change.from === change.to) {
501
+ rows.push(changeLine('ctx', change, 'old'));
502
+ return;
503
+ }
504
+ rows.push(changeLine('del', change, 'old'));
505
+ rows.push(changeLine('add', change, 'new'));
506
+ };
507
+
508
+ const lockCountText = (oldCount, newCount) => {
509
+ if (oldCount === newCount) return '';
510
+ return `lockfile packages ${oldCount} → ${newCount}`;
511
+ };
512
+
513
+ const auditNoteTexts = (audit) => {
514
+ if (!audit) return [];
515
+ const notes = [];
516
+ if (Array.isArray(audit.titles)) {
517
+ for (const note of audit.titles) {
518
+ if (note && !notes.includes(note)) notes.push(note);
519
+ }
520
+ } else if (audit.title) {
521
+ notes.push(audit.title);
522
+ }
523
+ const severity = audit.severity;
524
+ if (!notes.length) return [`npm audit ${severity}`];
525
+ const { length } = notes;
526
+ const texts = new Array(length);
527
+ for (let i = 0; i < length; i++) {
528
+ texts[i] = `npm audit ${severity} ${notes[i]}`;
529
+ }
530
+ return texts;
531
+ };
532
+
533
+ const pushAuditNotes = (rows, audit) => {
534
+ const texts = auditNoteTexts(audit);
535
+ if (!texts.length) return;
536
+ rows.push(ctxLine(''));
537
+ let first = true;
538
+ for (const text of texts) {
539
+ if (!first) rows.push(noteSepLine());
540
+ first = false;
541
+ rows.push(noteLine(text));
542
+ }
543
+ };
544
+
545
+ const buildRows = (change, oldCount, newCount) => {
546
+ const rows = [ctxLine(DEP_CAPTION), ctxLine('')];
547
+ if (change) {
548
+ const marks = changeMarks(change);
549
+ const sources = changeSources(change);
550
+ const title = sectionTitle(change.section, change.action, marks, sources);
551
+ const audited = sources.includes('npm audit');
552
+ rows.push(audited ? warnLine(title) : ctxLine(title));
553
+ rows.push(ctxLine(''));
554
+ pushChangeLines(rows, change);
555
+ pushAuditNotes(rows, change.audit);
556
+ }
557
+ const countLine = lockCountText(oldCount, newCount);
558
+ if (countLine) {
559
+ if (rows.at(-1).text !== '') rows.push(ctxLine(''));
560
+ rows.push(ctxLine(countLine));
561
+ }
562
+ return rows;
563
+ };
564
+
565
+ const toHunkLine = (row, blockId) => {
566
+ const type = row.type;
567
+ const text = row.text;
568
+ const noNl = false;
569
+ const id = isCtxType(type) ? null : blockId;
570
+ return { type, text, noNl, blockId: id };
571
+ };
572
+
573
+ const makeHunk = (rows, blockId) => {
574
+ const lines = rows.map((row) => toHunkLine(row, blockId));
575
+ const counts = countSides(lines);
576
+ const oldCount = counts.oldCount;
577
+ const newCount = counts.newCount;
578
+ const header = `@@ -1,${oldCount} +1,${newCount} @@`;
579
+ return { oldStart: 1, oldCount, newStart: 1, newCount, header, lines };
580
+ };
581
+
582
+ const uniquePaths = (items) => {
583
+ const files = [];
584
+ const seen = new Set();
585
+ for (const item of items) {
586
+ const rel = itemPath(item);
587
+ if (!rel || seen.has(rel)) continue;
588
+ seen.add(rel);
589
+ files.push(rel);
590
+ }
591
+ return files;
592
+ };
593
+
594
+ const filesForChange = (change, files) => {
595
+ const manifest = [];
596
+ const lockfile = [];
597
+ for (const rel of files) {
598
+ const meta = depFileMeta(rel);
599
+ if (!meta) continue;
600
+ if (meta.kind === 'manifest') manifest.push(rel);
601
+ else lockfile.push(rel);
602
+ }
603
+ if (change.section === 'field') {
604
+ if (manifest.length) return manifest;
605
+ return files;
606
+ }
607
+ if (change.section === 'resolved') {
608
+ if (lockfile.length) return lockfile;
609
+ return files;
610
+ }
611
+ if (change.propose) {
612
+ if (manifest.length) return manifest;
613
+ return files;
614
+ }
615
+ const both = [...manifest, ...lockfile];
616
+ if (both.length) return both;
617
+ return files;
618
+ };
619
+
620
+ const pickPrimary = (items) => {
621
+ for (const item of items) {
622
+ const rel = itemPath(item);
623
+ const meta = depFileMeta(rel);
624
+ if (meta && meta.kind === 'manifest') return item;
625
+ }
626
+ return items[0];
627
+ };
628
+
629
+ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
630
+ const allFiles = uniquePaths(fileItems);
631
+ const files = change ? filesForChange(change, allFiles) : allFiles;
632
+ const primary = pickPrimary(fileItems);
633
+ const oldPath = listFilePath(itemPath(primary));
634
+ const newPath = oldPath;
635
+ const isNew = fileItems.some((item) => item.file && item.file.isNew);
636
+ const isDeleted = fileItems.every((item) => item.file && item.file.isDeleted);
637
+ let oldCount = 0;
638
+ let newCount = 0;
639
+ if (!change) {
640
+ oldCount = extra.oldCount ?? 0;
641
+ newCount = extra.newCount ?? 0;
642
+ }
643
+ const rows = buildRows(change, oldCount, newCount);
644
+ const hunk = makeHunk(rows, blockId);
645
+ const isBinary = false;
646
+ const preamble = [];
647
+ const hunks = [hunk];
648
+ const file = {
649
+ oldPath,
650
+ newPath,
651
+ isNew,
652
+ isDeleted,
653
+ isBinary,
654
+ preamble,
655
+ hunks,
656
+ };
657
+ const patchAdd = '';
658
+ const patchRevert = '';
659
+ const items = hidden;
660
+ const dep = { files, items, change };
661
+ const reload = !!(change && change.propose);
662
+ return { origin, file, hunk, blockId, patchAdd, patchRevert, dep, reload };
663
+ };
664
+
665
+ const makeDepItems = (origin, fileItems, hidden, summary) => {
666
+ const ordered = orderedChanges(summary.changes);
667
+ if (!ordered.length) {
668
+ const oldCount = summary.oldCount ?? 0;
669
+ const newCount = summary.newCount ?? 0;
670
+ if (oldCount === newCount) return [];
671
+ return [makeDepItem(origin, fileItems, hidden, null, summary, 0)];
672
+ }
673
+ const { length } = ordered;
674
+ const items = new Array(length);
675
+ for (let i = 0; i < length; i++) {
676
+ items[i] = makeDepItem(origin, fileItems, hidden, ordered[i], summary, i);
677
+ }
678
+ return items;
679
+ };
680
+
681
+ const stubFileItem = (rel, origin) => {
682
+ const file = {
683
+ oldPath: rel,
684
+ newPath: rel,
685
+ isNew: false,
686
+ isDeleted: false,
687
+ isBinary: false,
688
+ preamble: [],
689
+ hunks: [],
690
+ };
691
+ const hunk = null;
692
+ const blockId = 0;
693
+ const patchAdd = '';
694
+ const patchRevert = '';
695
+ return { origin, file, hunk, blockId, patchAdd, patchRevert };
696
+ };
697
+
698
+ module.exports = {
699
+ DEP_CAPTION,
700
+ diffSections,
701
+ mergeResolved,
702
+ orderedChanges,
703
+ summarizeGroup,
704
+ collectGroups,
705
+ takeManifestItems,
706
+ foldedDepNames,
707
+ stripFoldedDepItem,
708
+ buildRows,
709
+ makeHunk,
710
+ makeDepItems,
711
+ stubFileItem,
712
+ };