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
package/lib/deps-diff.js CHANGED
@@ -13,25 +13,29 @@ const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
13
13
  const { listFilePath, readSections, emptySectionMaps } = manifest;
14
14
  const { lockRootSections, lockEntries, lockPackageCount } = manifest;
15
15
 
16
- const SECTION_ORDER = [...SECTION_KEYS, 'resolved', 'field'];
16
+ const SECTION_ORDER = [...SECTION_KEYS, 'resolved'];
17
17
  const ACTION_ORDER = ['added', 'removed', 'changed', 'vulnerable'];
18
18
  const SECTION_NAMES = SECTION_KEYS.join('|');
19
19
  const SECTION_OPEN = new RegExp(`^\\s*"(${SECTION_NAMES})"\\s*:\\s*\\{\\s*$`);
20
20
  const SECTION_END = /^\s*\},?\s*$/;
21
21
  const DEP_ENTRY = /^\s*"([^"]+)"\s*:\s*"([^"]*)"\s*,?\s*$/;
22
+ const DEP_CAPTION = 'Dependencies in package.json & package-lock.json';
23
+
24
+ const changeAction = (hadOld, hasNew) => {
25
+ if (!hadOld) return 'added';
26
+ if (!hasNew) return 'removed';
27
+ return 'changed';
28
+ };
29
+
30
+ const sortedUnion = (left, right) => [...new Set([...left, ...right])].sort();
22
31
 
23
32
  const diffMaps = (oldMap, newMap, section) => {
24
- const keys = [...oldMap.keys(), ...newMap.keys()];
25
- const names = [...new Set(keys)];
26
- names.sort();
27
33
  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) : '';
34
+ for (const name of sortedUnion(oldMap.keys(), newMap.keys())) {
35
+ const from = oldMap.get(name) ?? '';
36
+ const to = newMap.get(name) ?? '';
31
37
  if (from === to) continue;
32
- let action = 'changed';
33
- if (!oldMap.has(name)) action = 'added';
34
- else if (!newMap.has(name)) action = 'removed';
38
+ const action = changeAction(oldMap.has(name), newMap.has(name));
35
39
  changes.push({ name, section, action, from, to });
36
40
  }
37
41
  return changes;
@@ -40,64 +44,37 @@ const diffMaps = (oldMap, newMap, section) => {
40
44
  const diffSections = (oldMaps, newMaps) =>
41
45
  SECTION_KEYS.flatMap((key) => diffMaps(oldMaps[key], newMaps[key], key));
42
46
 
43
- const fieldText = (value) => {
44
- if (value === undefined) return '';
45
- if (typeof value === 'string') return value;
46
- return JSON.stringify(value);
47
- };
48
-
49
- const diffOtherFields = (oldPkg, newPkg) => {
50
- if (!oldPkg && !newPkg) return [];
47
+ const hasFieldChanges = (oldPkg, newPkg) => {
51
48
  const oldObj = oldPkg ?? {};
52
49
  const newObj = newPkg ?? {};
53
- const keys = [...Object.keys(oldObj), ...Object.keys(newObj)];
54
- const names = [...new Set(keys)];
55
- names.sort();
56
- const changes = [];
57
- for (const name of names) {
50
+ for (const name of new Set([
51
+ ...Object.keys(oldObj),
52
+ ...Object.keys(newObj),
53
+ ])) {
58
54
  if (SECTION_KEYS.includes(name)) continue;
59
- const prev = oldObj[name];
60
- const next = newObj[name];
61
- if (JSON.stringify(prev) === JSON.stringify(next)) continue;
62
- let action = 'changed';
63
- if (!Object.hasOwn(oldObj, name)) action = 'added';
64
- else if (!Object.hasOwn(newObj, name)) action = 'removed';
65
- const from = fieldText(prev);
66
- const to = fieldText(next);
67
- const section = 'field';
68
- changes.push({ name, section, action, from, to });
55
+ const prev = JSON.stringify(oldObj[name]);
56
+ if (prev !== JSON.stringify(newObj[name])) return true;
69
57
  }
70
- return changes;
58
+ return false;
71
59
  };
72
60
 
73
- const foldedDepNames = (changes) => {
74
- const names = new Set();
75
- for (const change of changes) {
76
- if (change.section === 'field') continue;
77
- names.add(change.name);
78
- }
79
- return names;
80
- };
61
+ const foldedDepNames = (changes) =>
62
+ new Set(changes.map((change) => change.name));
81
63
 
82
64
  const lineEntryName = (text) => {
83
65
  const entry = DEP_ENTRY.exec(text);
84
66
  return entry ? entry[1] : '';
85
67
  };
86
68
 
87
- const lineInBlock = (item, line) => {
88
- const blockId = item.blockId;
89
- if (typeof blockId !== 'number') return true;
90
- return line.blockId === blockId;
91
- };
69
+ const lineInBlock = (item, line) =>
70
+ typeof item.blockId !== 'number' || line.blockId === item.blockId;
92
71
 
93
72
  const hunkOnlyFoldedDeps = (item, names) => {
94
73
  if (!item.hunk || !names.size) return false;
95
74
  let folded = 0;
96
75
  for (const line of item.hunk.lines) {
97
- if (line.type === 'ctx') continue;
98
- if (!lineInBlock(item, line)) continue;
99
- const name = lineEntryName(line.text);
100
- if (name && names.has(name)) {
76
+ if (line.type === 'ctx' || !lineInBlock(item, line)) continue;
77
+ if (names.has(lineEntryName(line.text))) {
101
78
  folded += 1;
102
79
  continue;
103
80
  }
@@ -109,67 +86,33 @@ const hunkOnlyFoldedDeps = (item, names) => {
109
86
 
110
87
  const stripFoldedDepItem = (item, names) => {
111
88
  if (!item.hunk || !names.size) return item;
112
- const lines = [];
113
- let removed = false;
114
- for (const line of item.hunk.lines) {
115
- if (line.type !== 'ctx') {
116
- const name = lineEntryName(line.text);
117
- if (name && names.has(name)) {
118
- removed = true;
119
- continue;
120
- }
121
- }
122
- lines.push(line);
123
- }
124
- if (!removed) return item;
125
- let hasEdit = false;
126
- for (const line of lines) {
127
- if (line.type === 'ctx') continue;
128
- if (!lineInBlock(item, line)) continue;
129
- hasEdit = true;
130
- break;
131
- }
132
- if (!hasEdit) return null;
133
- const counts = countSides(lines);
134
- const hunk = {
135
- ...item.hunk,
136
- oldCount: counts.oldCount,
137
- newCount: counts.newCount,
138
- header: formatHunkHeader(
139
- item.hunk.oldStart,
140
- counts.oldCount,
141
- item.hunk.newStart,
142
- counts.newCount,
143
- ),
144
- lines,
145
- };
89
+ const isFolded = (line) =>
90
+ line.type !== 'ctx' && names.has(lineEntryName(line.text));
91
+ const lines = item.hunk.lines.filter((line) => !isFolded(line));
92
+ if (lines.length === item.hunk.lines.length) return item;
93
+ const isEdit = (line) => line.type !== 'ctx' && lineInBlock(item, line);
94
+ if (!lines.some(isEdit)) return null;
95
+ const { oldStart, newStart } = item.hunk;
96
+ const { oldCount, newCount } = countSides(lines);
97
+ const header = formatHunkHeader(oldStart, oldCount, newStart, newCount);
98
+ const hunk = { ...item.hunk, oldCount, newCount, header, lines };
146
99
  const patchAdd = formatPatch(item.file, hunk, item.blockId, 'old');
147
100
  const patchRevert = formatPatch(item.file, hunk, item.blockId, 'new');
148
101
  return { ...item, hunk, patchAdd, patchRevert };
149
102
  };
150
103
 
151
- const onlyDepEdits = (items) => {
152
- if (!items.length) return false;
153
- for (const item of items) {
154
- if (!item.hunk) return false;
155
- const blockId = item.blockId;
156
- const scoped = typeof blockId === 'number';
157
- let section = '';
158
- for (const line of item.hunk.lines) {
159
- const open = SECTION_OPEN.exec(line.text);
160
- let nextSection = section;
161
- if (open) nextSection = open[1];
162
- else if (section && SECTION_END.test(line.text)) nextSection = '';
163
- const inBlock = !scoped || line.blockId === blockId;
164
- if (inBlock && line.type !== 'ctx') {
165
- const inDep = !!(open || section);
166
- if (!inDep) return false;
167
- const isEntry = DEP_ENTRY.test(line.text);
168
- const isClose = SECTION_END.test(line.text);
169
- if (!open && !isEntry && !isClose) return false;
170
- }
171
- section = nextSection;
104
+ const isDepOnlyItem = (item) => {
105
+ if (!item.hunk) return false;
106
+ let section = '';
107
+ for (const line of item.hunk.lines) {
108
+ const open = SECTION_OPEN.exec(line.text);
109
+ const close = SECTION_END.test(line.text);
110
+ if (line.type !== 'ctx' && lineInBlock(item, line) && !open) {
111
+ if (!section) return false;
112
+ if (!close && !DEP_ENTRY.test(line.text)) return false;
172
113
  }
114
+ if (open) section = open[1];
115
+ else if (close) section = '';
173
116
  }
174
117
  return true;
175
118
  };
@@ -190,56 +133,38 @@ const mapsFromHunks = (items) => {
190
133
  section = '';
191
134
  continue;
192
135
  }
193
- if (!section) continue;
194
- const entry = DEP_ENTRY.exec(line.text);
136
+ const entry = section ? DEP_ENTRY.exec(line.text) : null;
195
137
  if (!entry) continue;
196
138
  const name = entry[1];
197
139
  const version = entry[2];
198
- if (line.type === 'del' || line.type === 'ctx') {
199
- oldMaps[section].set(name, version);
200
- }
201
- if (line.type === 'add' || line.type === 'ctx') {
202
- newMaps[section].set(name, version);
203
- }
140
+ const isCtx = line.type === 'ctx';
141
+ if (isCtx || line.type === 'del') oldMaps[section].set(name, version);
142
+ if (isCtx || line.type === 'add') newMaps[section].set(name, version);
204
143
  }
205
144
  }
206
145
  return { oldMaps, newMaps };
207
146
  };
208
147
 
209
- const directNames = (oldMaps, newMaps) => {
210
- const names = new Set();
211
- for (const key of SECTION_KEYS) {
212
- for (const name of oldMaps[key].keys()) names.add(name);
213
- for (const name of newMaps[key].keys()) names.add(name);
214
- }
215
- return names;
216
- };
217
-
218
148
  const mergeResolved = (changes, oldResolved, newResolved, oldMaps, newMaps) => {
219
- const seen = new Set();
220
- const { length } = changes;
221
- const merged = new Array(length);
222
- for (let i = 0; i < length; i++) {
223
- const change = changes[i];
224
- seen.add(change.name);
225
- const resolvedFrom = oldResolved.get(change.name) ?? '';
226
- const resolvedTo = newResolved.get(change.name) ?? '';
227
- merged[i] = { ...change, resolvedFrom, resolvedTo };
228
- }
229
- const names = [...directNames(oldMaps, newMaps)];
230
- names.sort();
231
- for (const name of names) {
149
+ const merged = changes.map((change) => ({
150
+ ...change,
151
+ resolvedFrom: oldResolved.get(change.name) ?? '',
152
+ resolvedTo: newResolved.get(change.name) ?? '',
153
+ }));
154
+ const seen = new Set(changes.map((change) => change.name));
155
+ const direct = SECTION_KEYS.flatMap((key) => [
156
+ ...oldMaps[key].keys(),
157
+ ...newMaps[key].keys(),
158
+ ]);
159
+ for (const name of [...new Set(direct)].sort()) {
232
160
  if (seen.has(name)) continue;
233
161
  const from = oldResolved.get(name) ?? '';
234
162
  const to = newResolved.get(name) ?? '';
235
163
  if (from === to) continue;
236
- if (!from && !to) continue;
237
- let action = 'changed';
238
- if (!from) action = 'added';
239
- else if (!to) action = 'removed';
240
- const section = 'resolved';
164
+ const action = changeAction(from, to);
241
165
  const resolvedFrom = from;
242
166
  const resolvedTo = to;
167
+ const section = 'resolved';
243
168
  merged.push({ name, section, action, from, to, resolvedFrom, resolvedTo });
244
169
  }
245
170
  return merged;
@@ -255,128 +180,81 @@ const compareChangeOrder = (left, right) => {
255
180
  return left.name.localeCompare(right.name);
256
181
  };
257
182
 
258
- const orderedChanges = (changes) => [...changes].sort(compareChangeOrder);
183
+ const kindOf = (rel) => depFileMeta(rel)?.kind;
259
184
 
260
185
  const kindItems = (items, kind) =>
261
- items.filter((item) => depFileMeta(itemPath(item))?.kind === kind);
262
-
263
- const readGroupSnapshot = (group, readSides) => {
264
- const origin = group.origin;
265
- const dir = group.dir;
266
- const groupItems = group.items;
267
- const manifestItems = kindItems(groupItems, 'manifest');
268
- const lockItems = kindItems(groupItems, 'lockfile');
269
- const manifestRel = relOf(dir, MANIFEST);
270
- const lockRel = relOf(dir, LOCKFILE);
271
- const manifestSides = readSides(origin, manifestRel);
272
- const lockSides = readSides(origin, lockRel);
273
- const oldPkg = jsonParse(manifestSides.oldText ?? '');
274
- const newPkg = jsonParse(manifestSides.newText ?? '');
275
- const oldLock = jsonParse(lockSides.oldText ?? '');
276
- const newLock = jsonParse(lockSides.newText ?? '');
277
- const parsedManifest = !!(oldPkg || newPkg);
278
- return {
279
- origin,
280
- dir,
281
- groupItems,
282
- manifestItems,
283
- lockItems,
284
- oldPkg,
285
- newPkg,
286
- oldLock,
287
- newLock,
288
- parsedManifest,
289
- };
290
- };
186
+ items.filter((item) => kindOf(itemPath(item)) === kind);
291
187
 
292
- const sectionMapsOf = (snapshot) => {
293
- if (snapshot.parsedManifest) {
294
- const oldMaps = readSections(snapshot.oldPkg);
295
- const newMaps = readSections(snapshot.newPkg);
296
- return { oldMaps, newMaps };
297
- }
298
- if (snapshot.manifestItems.length) {
299
- return mapsFromHunks(snapshot.manifestItems);
300
- }
301
- const oldMaps = lockRootSections(snapshot.oldLock);
302
- const newMaps = lockRootSections(snapshot.newLock);
303
- return { oldMaps, newMaps };
188
+ const readJsonSides = (readSides, origin, rel) => {
189
+ const sides = readSides(origin, rel);
190
+ const before = jsonParse(sides.oldText ?? '');
191
+ const after = jsonParse(sides.newText ?? '');
192
+ return { before, after };
304
193
  };
305
194
 
306
- const comparedChanges = (snapshot) => {
307
- const maps = sectionMapsOf(snapshot);
308
- const oldMaps = maps.oldMaps;
309
- const newMaps = maps.newMaps;
310
- const manifestChanges = diffSections(oldMaps, newMaps);
311
- const parsed = snapshot.parsedManifest;
312
- let otherChanges = [];
313
- if (parsed) {
314
- otherChanges = diffOtherFields(snapshot.oldPkg, snapshot.newPkg);
195
+ const sectionMapsOf = (pkg, lock, manifestItems) => {
196
+ if (pkg.before || pkg.after) {
197
+ return {
198
+ oldMaps: readSections(pkg.before),
199
+ newMaps: readSections(pkg.after),
200
+ };
315
201
  }
316
- const oldResolved = lockEntries(snapshot.oldLock);
317
- const newResolved = lockEntries(snapshot.newLock);
318
- const changes = mergeResolved(
319
- manifestChanges,
320
- oldResolved,
321
- newResolved,
322
- oldMaps,
323
- newMaps,
324
- );
325
- const oldCount = lockPackageCount(snapshot.oldLock);
326
- const newCount = lockPackageCount(snapshot.newLock);
327
- return { changes, otherChanges, oldCount, newCount };
202
+ if (manifestItems.length) return mapsFromHunks(manifestItems);
203
+ const oldMaps = lockRootSections(lock.before);
204
+ return { oldMaps, newMaps: lockRootSections(lock.after) };
328
205
  };
329
206
 
330
- const foldingPolicy = (snapshot, compared) => {
331
- const foldLock = snapshot.lockItems.length > 0;
332
- const hasDepChanges = compared.changes.length > 0;
333
- if (!snapshot.manifestItems.length) {
334
- const foldManifest = false;
335
- const takeAllManifest = false;
336
- return { foldLock, foldManifest, takeAllManifest };
207
+ const manifestPolicy = (manifestItems, pkg, hasChanges) => {
208
+ if (!manifestItems.length) {
209
+ return { foldManifest: false, takeAllManifest: false };
337
210
  }
338
- if (snapshot.parsedManifest) {
339
- const foldManifest = hasDepChanges;
340
- const takeAllManifest = hasDepChanges && compared.otherChanges.length === 0;
341
- return { foldLock, foldManifest, takeAllManifest };
211
+ if (pkg.before || pkg.after) {
212
+ const onlyDeps = !hasFieldChanges(pkg.before, pkg.after);
213
+ return {
214
+ foldManifest: hasChanges,
215
+ takeAllManifest: hasChanges && onlyDeps,
216
+ };
342
217
  }
343
- const foldManifest = hasDepChanges && onlyDepEdits(snapshot.manifestItems);
344
- const takeAllManifest = foldManifest;
345
- return { foldLock, foldManifest, takeAllManifest };
218
+ const fold = hasChanges && manifestItems.every(isDepOnlyItem);
219
+ return { foldManifest: fold, takeAllManifest: fold };
346
220
  };
347
221
 
348
222
  const summarizeGroup = (group, readSides) => {
349
- const snapshot = readGroupSnapshot(group, readSides);
350
- const compared = comparedChanges(snapshot);
351
- const policy = foldingPolicy(snapshot, compared);
223
+ const { origin, dir } = group;
224
+ const manifestItems = kindItems(group.items, 'manifest');
225
+ const lockItems = kindItems(group.items, 'lockfile');
226
+ const pkg = readJsonSides(readSides, origin, relOf(dir, MANIFEST));
227
+ const lock = readJsonSides(readSides, origin, relOf(dir, LOCKFILE));
228
+ const { oldMaps, newMaps } = sectionMapsOf(pkg, lock, manifestItems);
229
+ const changes = mergeResolved(
230
+ diffSections(oldMaps, newMaps),
231
+ lockEntries(lock.before),
232
+ lockEntries(lock.after),
233
+ oldMaps,
234
+ newMaps,
235
+ );
352
236
  return {
353
- changes: compared.changes,
354
- oldCount: compared.oldCount,
355
- newCount: compared.newCount,
356
- foldLock: policy.foldLock,
357
- foldManifest: policy.foldManifest,
358
- takeAllManifest: policy.takeAllManifest,
359
- manifestItems: snapshot.manifestItems,
360
- lockItems: snapshot.lockItems,
361
- pkg: snapshot.newPkg || snapshot.oldPkg,
237
+ changes,
238
+ oldCount: lockPackageCount(lock.before),
239
+ newCount: lockPackageCount(lock.after),
240
+ foldLock: lockItems.length > 0,
241
+ ...manifestPolicy(manifestItems, pkg, changes.length > 0),
242
+ manifestItems,
243
+ lockItems,
244
+ pkg: pkg.after || pkg.before,
362
245
  };
363
246
  };
364
247
 
365
248
  const collectGroups = (items) => {
366
249
  const groups = new Map();
367
250
  for (const item of items) {
368
- const rel = itemPath(item);
369
- const meta = depFileMeta(rel);
251
+ const meta = depFileMeta(itemPath(item));
370
252
  if (!meta) continue;
371
253
  const key = `${item.origin}\0${meta.dir}`;
372
- let group = groups.get(key);
373
- if (!group) {
374
- const dir = meta.dir;
375
- const origin = item.origin;
376
- group = { dir, origin, items: [] };
377
- groups.set(key, group);
254
+ if (!groups.has(key)) {
255
+ groups.set(key, { dir: meta.dir, origin: item.origin, items: [] });
378
256
  }
379
- group.items.push(item);
257
+ groups.get(key).items.push(item);
380
258
  }
381
259
  return groups;
382
260
  };
@@ -385,17 +263,11 @@ const takeManifestItems = (summary) => {
385
263
  if (!summary.foldManifest) return [];
386
264
  if (summary.takeAllManifest) return [...summary.manifestItems];
387
265
  const names = foldedDepNames(summary.changes);
388
- const taken = [];
389
- for (const item of summary.manifestItems) {
390
- if (onlyDepEdits([item]) || hunkOnlyFoldedDeps(item, names)) {
391
- taken.push(item);
392
- }
393
- }
394
- return taken;
266
+ return summary.manifestItems.filter(
267
+ (item) => isDepOnlyItem(item) || hunkOnlyFoldedDeps(item, names),
268
+ );
395
269
  };
396
270
 
397
- const DEP_CAPTION = 'Dependencies in package.json & package-lock.json';
398
-
399
271
  const changeMarks = (change) => {
400
272
  const marks = [];
401
273
  if (change.unused) marks.push('unused');
@@ -405,191 +277,113 @@ const changeMarks = (change) => {
405
277
  };
406
278
 
407
279
  const changeSources = (change) => {
280
+ if (change.unused) return ['npm uninstall'];
408
281
  const sources = [];
409
- if (change.unused) {
410
- sources.push('npm uninstall');
411
- return sources;
412
- }
413
282
  if (change.propose && change.outdated) sources.push('npm outdated');
414
283
  if (change.audit) sources.push('npm audit');
415
284
  return sources;
416
285
  };
417
286
 
418
- const sectionTitle = (section, action, marks, sources) => {
287
+ const sectionTitle = (change, marks, sources) => {
288
+ const { section, action } = change;
419
289
  const title = action === 'changed' ? 'version changed' : action;
420
290
  const kind = section.replace(/ies$/, 'y');
421
291
  let label = `${kind} ${title}`;
422
- if (section === 'field') label = `package.json ${title}`;
423
292
  if (section === 'resolved' && action === 'vulnerable') {
424
293
  label = 'lockfile vulnerable';
425
294
  } else if (section === 'resolved') {
426
295
  label = `lockfile resolved ${title}`;
427
296
  }
428
- if (marks.includes('unused')) {
429
- label = `${kind} unused`;
430
- const extra = [];
431
- for (const mark of marks) {
432
- if (mark !== 'unused') extra.push(mark);
433
- }
434
- if (extra.length) label = `${label}, ${extra.join(', ')}`;
435
- } else if (marks.length) {
436
- label = `${label}, ${marks.join(', ')}`;
437
- }
297
+ if (marks.includes('unused')) label = `${kind} unused`;
298
+ const extra = marks.filter((mark) => mark !== 'unused');
299
+ if (extra.length) label = `${label}, ${extra.join(', ')}`;
438
300
  if (!sources.length) return label;
439
301
  return `${sources.join(', ')}: ${label}`;
440
302
  };
441
303
 
442
- const entryText = (change, side) => {
443
- const version = side === 'old' ? change.from : change.to;
444
- return `"${change.name}": "${version}"`;
445
- };
446
-
447
- const ctxLine = (text) => ({ type: 'ctx', text });
448
- const warnLine = (text) => ({ type: 'warn', text });
449
- const noteLine = (text) => ({ type: 'note', text });
450
- const noteSepLine = () => ({ type: 'noteSep', text: '' });
304
+ const row = (type, text) => ({ type, text });
451
305
 
452
- const changeLine = (type, change, side) => {
453
- const text = entryText(change, side);
454
- return { type, text };
306
+ const entryRow = (type, change, side) => {
307
+ const version = side === 'old' ? change.from : change.to;
308
+ return row(type, `"${change.name}": "${version}"`);
455
309
  };
456
310
 
457
- const pushChangeLines = (rows, change) => {
311
+ const changeLines = (change) => {
458
312
  if (change.unused) {
459
313
  const side = change.action === 'added' ? 'new' : 'old';
460
- rows.push(changeLine('del', change, side));
461
- return;
462
- }
463
- if (change.action === 'added') {
464
- rows.push(changeLine('add', change, 'new'));
465
- return;
466
- }
467
- if (change.action === 'removed') {
468
- rows.push(changeLine('del', change, 'old'));
469
- return;
314
+ return [entryRow('del', change, side)];
470
315
  }
471
- if (change.from === change.to) {
472
- rows.push(changeLine('ctx', change, 'old'));
473
- return;
474
- }
475
- rows.push(changeLine('del', change, 'old'));
476
- rows.push(changeLine('add', change, 'new'));
316
+ if (change.action === 'added') return [entryRow('add', change, 'new')];
317
+ if (change.action === 'removed') return [entryRow('del', change, 'old')];
318
+ if (change.from === change.to) return [entryRow('ctx', change, 'old')];
319
+ return [entryRow('del', change, 'old'), entryRow('add', change, 'new')];
477
320
  };
478
321
 
479
- const lockCountText = (oldCount, newCount) => {
480
- if (oldCount === newCount) return '';
481
- return `lockfile packages ${oldCount} → ${newCount}`;
482
- };
483
-
484
- const auditNoteTexts = (audit) => {
322
+ const auditRows = (audit) => {
485
323
  if (!audit) return [];
486
- const notes = [];
487
- if (Array.isArray(audit.titles)) {
488
- for (const note of audit.titles) {
489
- if (note && !notes.includes(note)) notes.push(note);
490
- }
491
- } else if (audit.title) {
492
- notes.push(audit.title);
324
+ const titles = Array.isArray(audit.titles) ? audit.titles : [audit.title];
325
+ const notes = [...new Set(titles.filter(Boolean))];
326
+ const prefix = `npm audit ${audit.severity}`;
327
+ if (!notes.length) return [row('ctx', ''), row('note', prefix)];
328
+ const rows = [row('ctx', '')];
329
+ for (const note of notes) {
330
+ if (rows.length > 1) rows.push(row('noteSep', ''));
331
+ rows.push(row('note', `${prefix} ${note}`));
493
332
  }
494
- const severity = audit.severity;
495
- if (!notes.length) return [`npm audit ${severity}`];
496
- return notes.map((note) => `npm audit ${severity} ${note}`);
333
+ return rows;
497
334
  };
498
335
 
499
- const pushAuditNotes = (rows, audit) => {
500
- const texts = auditNoteTexts(audit);
501
- if (!texts.length) return;
502
- rows.push(ctxLine(''));
503
- let first = true;
504
- for (const text of texts) {
505
- if (!first) rows.push(noteSepLine());
506
- first = false;
507
- rows.push(noteLine(text));
508
- }
336
+ const changeRows = (change) => {
337
+ const sources = changeSources(change);
338
+ const title = sectionTitle(change, changeMarks(change), sources);
339
+ const audited = sources.includes('npm audit');
340
+ return [
341
+ row('ctx', DEP_CAPTION),
342
+ row('ctx', ''),
343
+ row(audited ? 'warn' : 'ctx', title),
344
+ row('ctx', ''),
345
+ ...changeLines(change),
346
+ ...auditRows(change.audit),
347
+ ];
509
348
  };
510
349
 
511
- const buildRows = (change, oldCount, newCount) => {
512
- const rows = [ctxLine(DEP_CAPTION), ctxLine('')];
513
- if (change) {
514
- const marks = changeMarks(change);
515
- const sources = changeSources(change);
516
- const title = sectionTitle(change.section, change.action, marks, sources);
517
- const audited = sources.includes('npm audit');
518
- rows.push(audited ? warnLine(title) : ctxLine(title));
519
- rows.push(ctxLine(''));
520
- pushChangeLines(rows, change);
521
- pushAuditNotes(rows, change.audit);
522
- }
523
- const countLine = lockCountText(oldCount, newCount);
524
- if (countLine) {
525
- if (rows.at(-1).text !== '') rows.push(ctxLine(''));
526
- rows.push(ctxLine(countLine));
527
- }
528
- return rows;
529
- };
530
-
531
- const toHunkLine = (row, blockId) => ({
532
- type: row.type,
533
- text: row.text,
534
- noNl: false,
535
- blockId: isCtxType(row.type) ? null : blockId,
536
- });
350
+ const countRows = (oldCount, newCount) => [
351
+ row('ctx', DEP_CAPTION),
352
+ row('ctx', ''),
353
+ row('ctx', `lockfile packages ${oldCount} → ${newCount}`),
354
+ ];
537
355
 
538
356
  const makeHunk = (rows, blockId) => {
539
- const lines = rows.map((row) => toHunkLine(row, blockId));
540
- const counts = countSides(lines);
541
- const oldCount = counts.oldCount;
542
- const newCount = counts.newCount;
357
+ const lines = rows.map((entry) => ({
358
+ type: entry.type,
359
+ text: entry.text,
360
+ noNl: false,
361
+ blockId: isCtxType(entry.type) ? null : blockId,
362
+ }));
363
+ const { oldCount, newCount } = countSides(lines);
543
364
  const header = `@@ -1,${oldCount} +1,${newCount} @@`;
544
365
  return { oldStart: 1, oldCount, newStart: 1, newCount, header, lines };
545
366
  };
546
367
 
547
- const uniquePaths = (items) => {
548
- const files = [];
549
- const seen = new Set();
550
- for (const item of items) {
551
- const rel = itemPath(item);
552
- if (!rel || seen.has(rel)) continue;
553
- seen.add(rel);
554
- files.push(rel);
555
- }
556
- return files;
557
- };
558
-
559
- const filesForChange = (change, files) => {
560
- const manifest = [];
561
- const lockfile = [];
562
- for (const rel of files) {
563
- const meta = depFileMeta(rel);
564
- if (!meta) continue;
565
- if (meta.kind === 'manifest') manifest.push(rel);
566
- else lockfile.push(rel);
567
- }
568
- if (change.section === 'resolved') return lockfile.length ? lockfile : files;
569
- if (change.section === 'field' || change.propose) {
570
- return manifest.length ? manifest : files;
368
+ const filesForChange = (change, rels) => {
369
+ const manifests = rels.filter((rel) => kindOf(rel) === 'manifest');
370
+ const lockfiles = rels.filter((rel) => kindOf(rel) === 'lockfile');
371
+ if (change.section === 'resolved') {
372
+ return lockfiles.length ? lockfiles : rels;
571
373
  }
572
- return manifest.length || lockfile.length
573
- ? [...manifest, ...lockfile]
574
- : files;
374
+ if (change.propose) return manifests.length ? manifests : rels;
375
+ const both = [...manifests, ...lockfiles];
376
+ return both.length ? both : rels;
575
377
  };
576
378
 
577
- const pickPrimary = (items) => {
578
- for (const item of items) {
579
- const rel = itemPath(item);
580
- const meta = depFileMeta(rel);
581
- if (meta && meta.kind === 'manifest') return item;
582
- }
583
- return items[0];
584
- };
585
-
586
- const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
587
- const allFiles = uniquePaths(fileItems);
588
- const files = change ? filesForChange(change, allFiles) : allFiles;
589
- const primary = pickPrimary(fileItems);
379
+ const makeDepItem = (origin, fileItems, hidden, change, rows, blockId) => {
380
+ const paths = new Set(fileItems.map((item) => itemPath(item)));
381
+ const rels = [...paths].filter(Boolean);
382
+ const files = change ? filesForChange(change, rels) : rels;
383
+ const primary =
384
+ fileItems.find((item) => kindOf(itemPath(item)) === 'manifest') ??
385
+ fileItems[0];
590
386
  const rel = listFilePath(itemPath(primary));
591
- const counts = change ? {} : extra;
592
- const rows = buildRows(change, counts.oldCount ?? 0, counts.newCount ?? 0);
593
387
  const hunk = makeHunk(rows, blockId);
594
388
  return {
595
389
  origin,
@@ -612,16 +406,17 @@ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
612
406
  };
613
407
 
614
408
  const makeDepItems = (origin, fileItems, hidden, summary) => {
615
- const ordered = orderedChanges(summary.changes);
616
- if (!ordered.length) {
617
- const oldCount = summary.oldCount ?? 0;
618
- const newCount = summary.newCount ?? 0;
619
- if (oldCount === newCount) return [];
620
- return [makeDepItem(origin, fileItems, hidden, null, summary, 0)];
621
- }
622
- return ordered.map((change, index) =>
623
- makeDepItem(origin, fileItems, hidden, change, summary, index),
624
- );
409
+ const ordered = [...summary.changes].sort(compareChangeOrder);
410
+ if (ordered.length) {
411
+ return ordered.map((change, index) =>
412
+ makeDepItem(origin, fileItems, hidden, change, changeRows(change), index),
413
+ );
414
+ }
415
+ const oldCount = summary.oldCount ?? 0;
416
+ const newCount = summary.newCount ?? 0;
417
+ if (oldCount === newCount) return [];
418
+ const rows = countRows(oldCount, newCount);
419
+ return [makeDepItem(origin, fileItems, hidden, null, rows, 0)];
625
420
  };
626
421
 
627
422
  const stubFileItem = (rel, origin) => ({