reslop 0.2.4 → 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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  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 +196 -404
  14. package/lib/deps.js +240 -399
  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 +138 -141
  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 +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  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 +157 -132
  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 +16 -26
  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 +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  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 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  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 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
package/lib/deps.js CHANGED
@@ -6,31 +6,43 @@ const metautil = require('metautil');
6
6
  const { jsonParse, isHashObject } = metautil;
7
7
  const files = require('./files.js');
8
8
  const { itemPath } = files;
9
+ const utilities = require('./utilities.js');
10
+ const { asText } = utilities;
9
11
  const manifest = require('./manifest.js');
10
- const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
11
- const { listFilePath, readSections, lockEntries, lockPackageCount } = manifest;
12
- const { mergeDepFile, findDeclared, applyWantedRange } = manifest;
13
- const { patchedFromRange, proposedLockVersion, proposedWanted } = manifest;
12
+ const { MANIFEST, LOCKFILE, SECTION_KEYS, isObject } = manifest;
13
+ const { depFileMeta, relOf, listFilePath, readSections } = manifest;
14
+ const { lockEntries, findDeclared, applyWantedRange } = manifest;
15
+ const { proposedLockVersion, proposedWanted } = manifest;
14
16
  const depDiff = require('./deps-diff.js');
15
- const { diffSections, mergeResolved, summarizeGroup, collectGroups } = depDiff;
16
- const { takeManifestItems, foldedDepNames, stripFoldedDepItem } = depDiff;
17
+ const { summarizeGroup, collectGroups, takeManifestItems } = depDiff;
18
+ const { foldedDepNames, stripFoldedDepItem } = depDiff;
17
19
  const { makeDepItems, stubFileItem } = depDiff;
18
20
 
19
21
  const SOURCE_EXT = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'];
20
- const REQUIRE_SPEC = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
21
- const FROM_SPEC = /\bfrom\s+['"]([^'"]+)['"]/g;
22
- const IMPORT_FILE = /import\s+['"]([^'"]+)['"]/g;
23
- const IMPORT_CALL = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
22
+ const SPEC_PATTERNS = [
23
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
24
+ /\bfrom\s+['"]([^'"]+)['"]/g,
25
+ /import\s+['"]([^'"]+)['"]/g,
26
+ /import\(\s*['"]([^'"]+)['"]\s*\)/g,
27
+ ];
28
+ const SCRIPT_RUNNERS = ['npx', 'npm', 'node'];
29
+ const SKIP_WALK = ['node_modules', '.git', '.review', 'coverage', 'dist'];
30
+ const SEVERITY_RANK = { critical: 4, high: 3, moderate: 2, low: 1, info: 0 };
31
+
32
+ const readText = (file) => {
33
+ try {
34
+ return fs.readFileSync(file, 'utf8');
35
+ } catch {
36
+ return '';
37
+ }
38
+ };
24
39
 
25
40
  const toPackageName = (spec) => {
26
41
  if (!spec || spec.startsWith('.') || spec.startsWith('/')) return '';
27
42
  if (spec.startsWith('node:')) return '';
28
- if (spec.startsWith('@')) {
29
- const parts = spec.split('/');
30
- if (parts.length < 2) return '';
31
- return `${parts[0]}/${parts[1]}`;
32
- }
33
- return spec.split('/')[0];
43
+ const parts = spec.split('/');
44
+ if (!spec.startsWith('@')) return parts[0];
45
+ return parts.length < 2 ? '' : `${parts[0]}/${parts[1]}`;
34
46
  };
35
47
 
36
48
  const addSpec = (used, spec) => {
@@ -38,54 +50,39 @@ const addSpec = (used, spec) => {
38
50
  if (name) used.add(name);
39
51
  };
40
52
 
41
- const addMatches = (used, text, pattern) => {
42
- const matches = text.matchAll(pattern);
43
- for (const match of matches) addSpec(used, match[1]);
44
- };
45
-
46
53
  const collectUsedFromText = (used, text) => {
47
- addMatches(used, text, REQUIRE_SPEC);
48
- addMatches(used, text, FROM_SPEC);
49
- addMatches(used, text, IMPORT_FILE);
50
- addMatches(used, text, IMPORT_CALL);
54
+ for (const pattern of SPEC_PATTERNS) {
55
+ for (const match of text.matchAll(pattern)) addSpec(used, match[1]);
56
+ }
51
57
  };
52
58
 
53
59
  const collectUsedFromScripts = (used, scripts) => {
54
- if (!scripts || typeof scripts !== 'object') return;
60
+ if (!isObject(scripts)) return;
55
61
  for (const script of Object.values(scripts)) {
56
- const tokens = `${script}`.split(/[\s;&|]+/);
57
- for (const token of tokens) {
62
+ for (const token of `${script}`.split(/[\s;&|]+/)) {
58
63
  if (!token || token.startsWith('-')) continue;
59
- if (token === 'npx' || token === 'npm' || token === 'node') continue;
60
- addSpec(used, token);
64
+ if (!SCRIPT_RUNNERS.includes(token)) addSpec(used, token);
61
65
  }
62
66
  }
63
67
  };
64
68
 
69
+ const hasText = (value) => typeof value === 'string' && value !== '';
70
+
65
71
  const pkgHasExportEntry = (pkg) => {
66
- if (!pkg || typeof pkg !== 'object') return false;
67
- const main = pkg.main;
68
- if (typeof main === 'string' && main) return true;
69
- const esm = pkg.module;
70
- if (typeof esm === 'string' && esm) return true;
72
+ if (!isObject(pkg)) return false;
73
+ if (hasText(pkg.main) || hasText(pkg.module)) return true;
71
74
  const exported = pkg.exports;
72
- if (typeof exported === 'string' && exported) return true;
73
- if (!exported || typeof exported !== 'object') return false;
74
- if (Array.isArray(exported)) return exported.length > 0;
75
- return Object.keys(exported).length > 0;
75
+ if (hasText(exported)) return true;
76
+ return isObject(exported) && Object.keys(exported).length > 0;
76
77
  };
77
78
 
78
- const isRuntimeFile = (value) => {
79
- if (typeof value !== 'string' || !value) return false;
80
- return !value.endsWith('.d.ts');
81
- };
79
+ const isRuntimeFile = (value) => hasText(value) && !value.endsWith('.d.ts');
82
80
 
83
81
  const hasRuntimeExport = (value) => {
84
82
  if (isRuntimeFile(value)) return true;
85
83
  if (Array.isArray(value)) return value.some(hasRuntimeExport);
86
- if (!value || typeof value !== 'object') return false;
87
- const keys = Object.keys(value);
88
- for (const key of keys) {
84
+ if (!isObject(value)) return false;
85
+ for (const key of Object.keys(value)) {
89
86
  if (key === 'types' || key === 'typings') continue;
90
87
  if (hasRuntimeExport(value[key])) return true;
91
88
  }
@@ -93,35 +90,17 @@ const hasRuntimeExport = (value) => {
93
90
  };
94
91
 
95
92
  const pkgHasBin = (pkg) => {
96
- if (!pkg || typeof pkg !== 'object') return false;
97
93
  const bin = pkg.bin;
98
94
  if (typeof bin === 'string') return !!bin;
99
- if (!bin || typeof bin !== 'object' || Array.isArray(bin)) return false;
100
- return Object.keys(bin).length > 0;
95
+ return isHashObject(bin) && Object.keys(bin).length > 0;
101
96
  };
102
97
 
103
98
  const pkgExportsLibrary = (pkg) => {
104
- if (!pkg || typeof pkg !== 'object') return false;
105
- if (pkgHasBin(pkg)) return false;
106
- if (isRuntimeFile(pkg.main)) return true;
107
- if (isRuntimeFile(pkg.module)) return true;
99
+ if (!isObject(pkg) || pkgHasBin(pkg)) return false;
100
+ if (isRuntimeFile(pkg.main) || isRuntimeFile(pkg.module)) return true;
108
101
  return hasRuntimeExport(pkg.exports);
109
102
  };
110
103
 
111
- const SKIP_WALK = ['node_modules', '.git', '.review', 'coverage', 'dist'];
112
-
113
- const collectUsedFromManifest = (used, rel) => {
114
- let text;
115
- try {
116
- text = fs.readFileSync(rel, 'utf8');
117
- } catch {
118
- return;
119
- }
120
- const pkg = jsonParse(text);
121
- if (!pkg) return;
122
- collectUsedFromScripts(used, pkg.scripts);
123
- };
124
-
125
104
  const walkUsedNames = (used, dir) => {
126
105
  let entries;
127
106
  try {
@@ -133,55 +112,36 @@ const walkUsedNames = (used, dir) => {
133
112
  if (entry.isSymbolicLink()) continue;
134
113
  const full = path.join(dir, entry.name);
135
114
  if (entry.isDirectory()) {
136
- if (SKIP_WALK.includes(entry.name)) continue;
137
- if (entry.name.startsWith('.')) continue;
138
- walkUsedNames(used, full);
139
- continue;
140
- }
141
- if (entry.name === MANIFEST) {
142
- collectUsedFromManifest(used, full);
143
- continue;
115
+ const skip = SKIP_WALK.includes(entry.name) || entry.name.startsWith('.');
116
+ if (!skip) walkUsedNames(used, full);
117
+ } else if (entry.name === MANIFEST) {
118
+ collectUsedFromScripts(used, jsonParse(readText(full))?.scripts);
119
+ } else if (SOURCE_EXT.includes(path.extname(entry.name))) {
120
+ collectUsedFromText(used, readText(full));
144
121
  }
145
- const ext = path.extname(entry.name);
146
- if (!SOURCE_EXT.includes(ext)) continue;
147
- let text;
148
- try {
149
- text = fs.readFileSync(full, 'utf8');
150
- } catch {
151
- continue;
152
- }
153
- collectUsedFromText(used, text);
154
122
  }
155
123
  };
156
124
 
157
125
  const collectUsedNames = (root) => {
158
126
  const used = new Set();
159
- if (!root) return used;
160
- walkUsedNames(used, root);
127
+ if (root) walkUsedNames(used, root);
161
128
  return used;
162
129
  };
163
130
 
164
131
  const depModuleParts = (name) => {
165
132
  if (!name.startsWith('@')) return [name];
166
133
  const parts = name.split('/');
167
- if (parts.length < 2) return [];
168
- return parts.slice(0, 2);
134
+ return parts.length < 2 ? [] : parts.slice(0, 2);
169
135
  };
170
136
 
171
137
  const readDepPkg = (root, pkgDir, name) => {
172
- if (!root || !name) return null;
138
+ if (!name) return null;
173
139
  const parts = depModuleParts(name);
174
140
  if (!parts.length) return null;
175
- const nested = pkgDir && pkgDir !== '.' ? pkgDir : '';
176
- let dir = nested ? path.join(root, nested) : root;
141
+ let dir = path.join(root, pkgDir || '.');
177
142
  while (true) {
178
143
  const file = path.join(dir, 'node_modules', ...parts, MANIFEST);
179
- let text = '';
180
- try {
181
- text = fs.readFileSync(file, 'utf8');
182
- } catch {
183
- // Missing node_modules copy; try a parent directory.
184
- }
144
+ const text = readText(file);
185
145
  if (text) return jsonParse(text);
186
146
  const parent = path.dirname(dir);
187
147
  if (parent === dir) return null;
@@ -189,61 +149,37 @@ const readDepPkg = (root, pkgDir, name) => {
189
149
  }
190
150
  };
191
151
 
192
- const depHasExportEntry = (root, pkgDir, name) => {
193
- const pkg = readDepPkg(root, pkgDir, name);
194
- if (!pkg) return false;
195
- return pkgExportsLibrary(pkg);
196
- };
197
-
198
152
  const collectExportEntries = (root, pkgDir, names) => {
199
153
  const entries = new Map();
200
154
  if (!root) return entries;
201
155
  for (const name of names) {
202
- const hasExport = depHasExportEntry(root, pkgDir, name);
203
- entries.set(name, hasExport);
156
+ const pkg = readDepPkg(root, pkgDir, name);
157
+ entries.set(name, !!pkg && pkgExportsLibrary(pkg));
204
158
  }
205
159
  return entries;
206
160
  };
207
161
 
208
- const declaredDepNames = (root, pkgDir) => {
209
- const nested = pkgDir && pkgDir !== '.' ? pkgDir : '';
210
- const file = path.join(root, nested, MANIFEST);
211
- let text;
212
- try {
213
- text = fs.readFileSync(file, 'utf8');
214
- } catch {
215
- return [];
216
- }
217
- const maps = readSections(jsonParse(text));
218
- return SECTION_KEYS.flatMap((key) => [...maps[key].keys()]);
219
- };
220
-
221
- const collectExportMeta = (root, extra = {}) => {
222
- if (extra.exportEntries !== undefined) return extra.exportEntries;
223
- const pkgDir = extra.dir ?? '.';
224
- const names = extra.names ?? declaredDepNames(root, pkgDir);
225
- return collectExportEntries(root, pkgDir, names);
162
+ const collectExportMeta = (root) => {
163
+ const pkg = jsonParse(readText(path.join(root, MANIFEST)));
164
+ const maps = readSections(pkg);
165
+ const names = SECTION_KEYS.flatMap((key) => [...maps[key].keys()]);
166
+ return collectExportEntries(root, '.', names);
226
167
  };
227
168
 
228
- const SEVERITY_RANK = { critical: 4, high: 3, moderate: 2, low: 1, info: 0 };
229
-
230
169
  const namedTitle = (name, title) => {
231
170
  if (!title) return '';
232
171
  if (!name) return title;
233
172
  const prefix = `${name}: `;
234
- if (title.startsWith(prefix)) return title;
235
- return `${prefix}${title}`;
173
+ return title.startsWith(prefix) ? title : `${prefix}${title}`;
236
174
  };
237
175
 
238
176
  const viaNotes = (via, pkgName) => {
239
177
  if (!Array.isArray(via)) return [];
240
178
  const notes = [];
241
179
  for (const item of via) {
242
- if (!item || typeof item !== 'object') continue;
243
- const title = typeof item.title === 'string' ? item.title : '';
244
- if (!title) continue;
245
- const hasName = typeof item.name === 'string' && item.name;
246
- const named = hasName ? item.name : pkgName;
180
+ const title = asText(item?.title);
181
+ if (!isObject(item) || !title) continue;
182
+ const named = hasText(item.name) ? item.name : pkgName;
247
183
  const note = namedTitle(named, title);
248
184
  if (!notes.includes(note)) notes.push(note);
249
185
  }
@@ -252,104 +188,87 @@ const viaNotes = (via, pkgName) => {
252
188
 
253
189
  const severityRank = (severity) => SEVERITY_RANK[severity] ?? 0;
254
190
 
255
- const auditFix = (entry) => {
256
- const available = entry.fixAvailable;
257
- if (!available || typeof available !== 'object') return '';
258
- if (typeof available.version !== 'string') return '';
259
- return available.version;
260
- };
191
+ const auditFix = (entry) => asText(entry.fixAvailable?.version);
261
192
 
262
193
  const auditEntryNames = (key, entry) => {
263
- const names = [];
194
+ const effects = Array.isArray(entry.effects) ? entry.effects : [];
195
+ const names = effects.filter(hasText);
264
196
  const primary = entry.name || key;
265
- if (primary) names.push(primary);
266
- const effects = entry.effects;
267
- if (!Array.isArray(effects)) return names;
268
- for (const effect of effects) {
269
- if (typeof effect === 'string' && effect) names.push(effect);
270
- }
271
- return names;
197
+ return primary ? [primary, ...names] : names;
272
198
  };
273
199
 
274
- const keepHighestAudit = (audit, name, severity, fix, range, notes) => {
200
+ const uniqueTexts = (list) => [...new Set(list.filter(Boolean))];
201
+
202
+ const keepHighestAudit = (audit, found) => {
203
+ const { name, severity } = found;
275
204
  if (!name || !severity || severity === 'info') return;
276
- const titles = [];
277
- for (const note of notes) {
278
- if (note && !titles.includes(note)) titles.push(note);
279
- }
205
+ const titles = uniqueTexts(found.titles);
280
206
  const prev = audit.get(name);
281
207
  if (prev && severityRank(prev.severity) >= severityRank(severity)) {
282
- const merged = [...prev.titles];
283
- for (const note of titles) {
284
- if (!merged.includes(note)) merged.push(note);
285
- }
286
- const next = {
208
+ audit.set(name, {
287
209
  ...prev,
288
- titles: merged,
289
- fix: prev.fix || fix,
290
- range: prev.range || range,
291
- };
292
- audit.set(name, next);
210
+ titles: uniqueTexts([...prev.titles, ...titles]),
211
+ fix: prev.fix || found.fix,
212
+ range: prev.range || found.range,
213
+ });
293
214
  return;
294
215
  }
295
216
  const title = titles[0] || '';
296
- const next = { name, severity, title, titles, fix, range };
297
- next.fix ||= '';
298
- next.range ||= '';
299
- audit.set(name, next);
217
+ const fix = found.fix || '';
218
+ const range = found.range || '';
219
+ audit.set(name, { name, severity, title, titles, fix, range });
220
+ };
221
+
222
+ const vulnFindings = (vulns) => {
223
+ const found = [];
224
+ for (const key of Object.keys(vulns)) {
225
+ const entry = vulns[key];
226
+ if (!isObject(entry)) continue;
227
+ const { severity } = entry;
228
+ const titles = viaNotes(entry.via, entry.name || key);
229
+ const fix = auditFix(entry);
230
+ const range = asText(entry.range);
231
+ for (const name of auditEntryNames(key, entry)) {
232
+ found.push({ name, severity, fix, range, titles });
233
+ }
234
+ }
235
+ return found;
236
+ };
237
+
238
+ const advisoryFinding = (entry) => {
239
+ const name = entry.module_name || entry.name;
240
+ const title = namedTitle(name, asText(entry.title));
241
+ const titles = title ? [title] : [];
242
+ const fix = auditFix(entry);
243
+ const range = asText(entry.range);
244
+ return { name, severity: entry.severity, fix, range, titles };
300
245
  };
301
246
 
302
247
  const parseAuditReport = (text) => {
303
248
  const audit = new Map();
304
- if (!text || !`${text}`.trim()) return audit;
305
- const data = jsonParse(text);
249
+ const data = text ? jsonParse(text) : null;
306
250
  if (!isHashObject(data)) return audit;
307
- const vulns = data.vulnerabilities;
308
- if (vulns && typeof vulns === 'object') {
309
- for (const key of Object.keys(vulns)) {
310
- const entry = vulns[key];
311
- if (!entry || typeof entry !== 'object') continue;
312
- const severity = entry.severity;
313
- const names = auditEntryNames(key, entry);
314
- const pkgName = entry.name || key;
315
- const titles = viaNotes(entry.via, pkgName);
316
- const fix = auditFix(entry);
317
- const range = typeof entry.range === 'string' ? entry.range : '';
318
- for (const name of names) {
319
- keepHighestAudit(audit, name, severity, fix, range, titles);
320
- }
321
- }
322
- }
323
- const advisories = data.advisories;
324
- if (advisories && typeof advisories === 'object') {
325
- for (const entry of Object.values(advisories)) {
326
- if (!entry || typeof entry !== 'object') continue;
327
- const name = entry.module_name || entry.name;
328
- const severity = entry.severity;
329
- const raw = typeof entry.title === 'string' ? entry.title : '';
330
- const title = namedTitle(name, raw);
331
- const titles = title ? [title] : [];
332
- const fix = auditFix(entry);
333
- const range = typeof entry.range === 'string' ? entry.range : '';
334
- keepHighestAudit(audit, name, severity, fix, range, titles);
335
- }
251
+ const { vulnerabilities, advisories } = data;
252
+ const found = isObject(vulnerabilities) ? vulnFindings(vulnerabilities) : [];
253
+ if (isObject(advisories)) {
254
+ const listed = Object.values(advisories).filter(isObject);
255
+ found.push(...listed.map(advisoryFinding));
336
256
  }
257
+ for (const entry of found) keepHighestAudit(audit, entry);
337
258
  return audit;
338
259
  };
339
260
 
340
261
  const parseOutdatedReport = (text) => {
341
262
  const outdated = new Map();
342
- if (!text || !`${text}`.trim()) return outdated;
343
- const data = jsonParse(text);
263
+ const data = text ? jsonParse(text) : null;
344
264
  if (!isHashObject(data)) return outdated;
345
265
  for (const name of Object.keys(data)) {
346
266
  const entry = data[name];
347
- if (!entry || typeof entry !== 'object') continue;
348
- const current = typeof entry.current === 'string' ? entry.current : '';
349
- const wanted = typeof entry.wanted === 'string' ? entry.wanted : '';
350
- const latest = typeof entry.latest === 'string' ? entry.latest : '';
351
- const type = typeof entry.type === 'string' ? entry.type : '';
352
- if (!wanted) continue;
267
+ const wanted = asText(entry?.wanted);
268
+ if (!isObject(entry) || !wanted) continue;
269
+ const current = asText(entry.current);
270
+ const latest = asText(entry.latest);
271
+ const type = asText(entry.type);
353
272
  outdated.set(name, { current, wanted, latest, type });
354
273
  }
355
274
  return outdated;
@@ -358,53 +277,40 @@ const parseOutdatedReport = (text) => {
358
277
  const markChanges = (changes, report, kind) => {
359
278
  if (!report) return changes;
360
279
  return changes.map((change) => {
361
- if (change.action === 'removed' || change.section === 'field') {
362
- return change;
363
- }
280
+ if (change.action === 'removed') return change;
364
281
  const found = report.get(change.name);
365
282
  if (!found) return change;
366
- if (kind === 'outdated' && found.wanted && found.wanted === found.current) {
367
- return change;
368
- }
283
+ const isCurrent = found.wanted && found.wanted === found.current;
284
+ if (kind === 'outdated' && isCurrent) return change;
369
285
  return { ...change, [kind]: found };
370
286
  });
371
287
  };
372
288
 
373
289
  const depExports = (exportEntries, name) => exportEntries?.get(name) === true;
374
290
 
375
- const isAddedDep = (change) => {
376
- if (change.action !== 'added') return false;
377
- if (change.section === 'resolved') return false;
378
- if (change.section === 'field') return false;
379
- return true;
380
- };
291
+ const isAddedDep = (change) =>
292
+ change.action === 'added' && change.section !== 'resolved';
381
293
 
382
294
  const unusedChangeNames = (changes, used) => {
383
- const names = [];
384
- if (!used || !used.size) return names;
385
- for (const change of changes) {
386
- if (!isAddedDep(change)) continue;
387
- if (used.has(change.name)) continue;
388
- names.push(change.name);
389
- }
390
- return names;
295
+ if (!used || !used.size) return [];
296
+ const unused = changes.filter(
297
+ (change) => isAddedDep(change) && !used.has(change.name),
298
+ );
299
+ return unused.map((change) => change.name);
391
300
  };
392
301
 
393
- const unusedDeclaredNames = (pkg, used) => {
394
- const names = [];
395
- if (!pkgHasExportEntry(pkg)) return names;
396
- if (!used || !used.size) return names;
302
+ const unusedDeclared = (pkg, used) => {
303
+ const found = [];
304
+ if (!pkgHasExportEntry(pkg) || !used?.size) return found;
397
305
  const maps = readSections(pkg);
398
306
  for (const section of SECTION_KEYS) {
399
307
  if (section === 'peerDependencies') continue;
400
- const keys = [...maps[section].keys()];
401
- keys.sort();
402
- for (const name of keys) {
403
- if (used.has(name)) continue;
404
- names.push(name);
308
+ const map = maps[section];
309
+ for (const name of [...map.keys()].sort()) {
310
+ if (!used.has(name)) found.push({ name, section, from: map.get(name) });
405
311
  }
406
312
  }
407
- return names;
313
+ return found;
408
314
  };
409
315
 
410
316
  const markUnusedChanges = (changes, used, pkg, exportEntries) => {
@@ -418,147 +324,118 @@ const markUnusedChanges = (changes, used, pkg, exportEntries) => {
418
324
 
419
325
  const unusedDeclaredChanges = (pkg, used, exportEntries) => {
420
326
  const changes = [];
421
- if (!pkgHasExportEntry(pkg)) return changes;
422
- if (!used || !used.size) return changes;
423
- const maps = readSections(pkg);
424
327
  const taken = new Set();
425
- for (const section of SECTION_KEYS) {
426
- if (section === 'peerDependencies') continue;
427
- const map = maps[section];
428
- const names = [...map.keys()];
429
- names.sort();
430
- for (const name of names) {
431
- if (used.has(name)) continue;
432
- if (taken.has(name)) continue;
433
- if (!depExports(exportEntries, name)) continue;
434
- taken.add(name);
435
- const version = map.get(name);
436
- changes.push({
437
- name,
438
- section,
439
- action: 'removed',
440
- from: version,
441
- to: '',
442
- propose: true,
443
- unused: true,
444
- });
445
- }
328
+ for (const { name, section, from } of unusedDeclared(pkg, used)) {
329
+ if (taken.has(name) || !depExports(exportEntries, name)) continue;
330
+ taken.add(name);
331
+ const action = 'removed';
332
+ const to = '';
333
+ changes.push({
334
+ name,
335
+ section,
336
+ action,
337
+ from,
338
+ to,
339
+ propose: true,
340
+ unused: true,
341
+ });
446
342
  }
447
343
  return changes;
448
344
  };
449
345
 
450
- const buildProposedChanges = (pkg, outdated, audit, lock, options = {}) => {
451
- const used = options.used;
452
- const exportEntries = options.exportEntries;
453
- const names = new Set();
454
- if (outdated) {
455
- for (const name of outdated.keys()) names.add(name);
456
- }
457
- if (audit) {
458
- for (const name of audit.keys()) names.add(name);
459
- }
460
- const listed = [...names];
346
+ const resolvedChange = (name, vul, resolved) => {
347
+ const from = resolved.get(name) || '';
348
+ if (!vul || !from) return null;
349
+ const to = proposedLockVersion(from, vul);
350
+ const section = 'resolved';
351
+ const action = 'vulnerable';
352
+ return { name, section, action, from, to, propose: true, audit: vul };
353
+ };
354
+
355
+ const declaredChange = (name, declared, out, vul) => {
356
+ const wanted = proposedWanted(out, vul);
357
+ if (!wanted && !vul) return null;
358
+ const from = declared.version;
359
+ const to = wanted ? applyWantedRange(from, wanted) : from;
360
+ if (to === from && !vul) return null;
361
+ const action = to === from ? 'vulnerable' : 'changed';
362
+ const { section } = declared;
363
+ const change = { name, section, action, from, to, propose: true };
364
+ if (out) change.outdated = out;
365
+ if (vul) change.audit = vul;
366
+ return change;
367
+ };
368
+
369
+ const mapKeys = (map) => (map ? [...map.keys()] : []);
370
+
371
+ const buildProposedChanges = (pkg, outdated, audit, lock, options) => {
372
+ const listed = [...new Set([...mapKeys(outdated), ...mapKeys(audit)])];
461
373
  listed.sort();
462
374
  const resolved = lockEntries(lock);
463
- const unused = unusedDeclaredChanges(pkg, used, exportEntries);
464
- const taken = new Set();
465
- for (const change of unused) taken.add(change.name);
466
- const changes = [...unused];
375
+ const { used, exportEntries } = options;
376
+ const changes = unusedDeclaredChanges(pkg, used, exportEntries);
377
+ const taken = new Set(changes.map((change) => change.name));
467
378
  for (const name of listed) {
468
379
  if (taken.has(name)) continue;
469
380
  const out = outdated ? outdated.get(name) : null;
470
381
  const vul = audit ? audit.get(name) : null;
471
- const type = out ? out.type : '';
472
- const declared = findDeclared(pkg, name, type);
473
- if (!declared) {
474
- if (!vul) continue;
475
- const version = resolved.get(name) || '';
476
- if (!version) continue;
477
- const to = proposedLockVersion(version, vul);
478
- changes.push({
479
- name,
480
- section: 'resolved',
481
- action: 'vulnerable',
482
- from: version,
483
- to,
484
- propose: true,
485
- audit: vul,
486
- });
487
- continue;
488
- }
489
- const wanted = proposedWanted(out, vul);
490
- if (!wanted && !vul) continue;
491
- let to = declared.version;
492
- if (wanted) to = applyWantedRange(declared.version, wanted);
493
- if (to === declared.version && !vul) continue;
494
- let action = 'changed';
495
- if (to === declared.version) action = 'vulnerable';
496
- const change = {
497
- name,
498
- section: declared.section,
499
- action,
500
- from: declared.version,
501
- to,
502
- propose: true,
503
- };
504
- if (out) change.outdated = out;
505
- if (vul) change.audit = vul;
506
- changes.push(change);
382
+ const declared = findDeclared(pkg, name, out ? out.type : '');
383
+ const change = declared
384
+ ? declaredChange(name, declared, out, vul)
385
+ : resolvedChange(name, vul, resolved);
386
+ if (change) changes.push(change);
507
387
  }
508
388
  return changes;
509
389
  };
510
390
 
511
- const loadExportEntries = (names, extra = {}, pkgDir) => {
391
+ const loadExportEntries = (names, extra, dir) => {
512
392
  const supplied = extra.exportEntries;
513
393
  if (supplied === null) return new Map();
514
- const dir = pkgDir ?? extra.dir;
515
- const missing = [];
516
- for (const name of names) {
517
- if (!supplied || !supplied.has(name)) missing.push(name);
518
- }
394
+ const missing = names.filter((name) => !supplied || !supplied.has(name));
519
395
  if (!missing.length) return supplied ?? new Map();
520
396
  const found = collectExportEntries(extra.root, dir, missing);
521
- if (!supplied) return found;
522
- return new Map([...supplied, ...found]);
397
+ return supplied ? new Map([...supplied, ...found]) : found;
523
398
  };
524
399
 
525
- const groupedSynthetics = (synthetic) => {
400
+ const spliceAtSlots = (out, synthetic, slot) => {
526
401
  const byPath = new Map();
527
- for (const [key, items] of synthetic) {
528
- const dir = key.slice(key.indexOf('\0') + 1);
529
- const rel = relOf(dir, MANIFEST);
402
+ for (const key of synthetic.keys()) {
403
+ const rel = relOf(key.slice(key.indexOf('\0') + 1), MANIFEST);
530
404
  const list = byPath.get(rel) ?? [];
531
- for (const item of items) list.push(item);
405
+ list.push(...synthetic.get(key));
532
406
  byPath.set(rel, list);
533
407
  }
534
- return byPath;
535
- };
536
-
537
- const spliceAtSlots = (out, synthetic, slot) => {
538
- const byPath = groupedSynthetics(synthetic);
539
- const inserts = [...byPath].map(([rel, items]) => ({
408
+ const inserts = [...byPath.keys()].map((rel) => ({
540
409
  at: slot.get(rel) ?? out.length,
541
- items,
410
+ items: byPath.get(rel),
542
411
  }));
543
412
  inserts.sort((left, right) => right.at - left.at);
544
- for (const insert of inserts) {
545
- out.splice(insert.at, 0, ...insert.items);
546
- }
413
+ for (const insert of inserts) out.splice(insert.at, 0, ...insert.items);
547
414
  return out;
548
415
  };
549
416
 
550
- const lastPathIndex = (items, rel) => {
551
- let at = -1;
552
- for (let i = 0; i < items.length; i++) {
553
- if (listFilePath(itemPath(items[i])) === rel) at = i;
417
+ const placeFolded = (items, { folded, synthetic, leftoverNames }) => {
418
+ const out = [];
419
+ const slot = new Map();
420
+ for (const item of items) {
421
+ const meta = depFileMeta(itemPath(item));
422
+ const pkgRel = meta ? relOf(meta.dir, MANIFEST) : '';
423
+ const key = meta ? `${item.origin}\0${meta.dir}` : '';
424
+ if (meta && synthetic.has(key) && !slot.has(pkgRel)) {
425
+ slot.set(pkgRel, out.length);
426
+ }
427
+ if (folded.has(item)) continue;
428
+ if (!meta || meta.kind !== 'manifest') {
429
+ out.push(item);
430
+ continue;
431
+ }
432
+ const names = leftoverNames.get(key);
433
+ const kept = names ? stripFoldedDepItem(item, names) : item;
434
+ if (!kept) continue;
435
+ out.push(kept);
436
+ slot.set(pkgRel, out.length);
554
437
  }
555
- return at;
556
- };
557
-
558
- const insertAfterPath = (items, added, rel) => {
559
- const at = lastPathIndex(items, rel);
560
- if (at < 0) return [...items, ...added];
561
- return [...items.slice(0, at + 1), ...added, ...items.slice(at + 1)];
438
+ return spliceAtSlots(out, synthetic, slot);
562
439
  };
563
440
 
564
441
  const foldDepItems = (
@@ -578,51 +455,22 @@ const foldDepItems = (
578
455
  const summary = summarizeGroup(group, readSides);
579
456
  const unusedNames = unusedChangeNames(summary.changes, usedNames);
580
457
  const exportEntries = loadExportEntries(unusedNames, extra, group.dir);
581
- const unused = markUnusedChanges(
582
- summary.changes,
583
- usedNames,
584
- summary.pkg,
585
- exportEntries,
586
- );
587
- const audited = markChanges(unused, audit, 'audit');
588
- const changes = markChanges(audited, outdated, 'outdated');
458
+ const { pkg } = summary;
459
+ let changes = summary.changes;
460
+ changes = markUnusedChanges(changes, usedNames, pkg, exportEntries);
461
+ changes = markChanges(changes, audit, 'audit');
462
+ changes = markChanges(changes, outdated, 'outdated');
589
463
  const next = { ...summary, changes };
590
- leftoverNames.set(key, foldedDepNames(next.changes));
464
+ leftoverNames.set(key, foldedDepNames(changes));
591
465
  const taken = takeManifestItems(next);
592
- if (next.foldLock) {
593
- for (const item of next.lockItems) taken.push(item);
594
- }
595
- if (!taken.length && !next.changes.length) continue;
466
+ if (next.foldLock) taken.push(...next.lockItems);
467
+ if (!taken.length && !changes.length) continue;
596
468
  const made = makeDepItems(group.origin, group.items, taken, next);
597
469
  if (made.length) synthetic.set(key, made);
598
470
  for (const item of taken) folded.add(item);
599
471
  }
600
472
  if (!synthetic.size && !folded.size) return items;
601
- const out = [];
602
- const slot = new Map();
603
- for (const item of items) {
604
- const rel = itemPath(item);
605
- const meta = depFileMeta(rel);
606
- if (meta) {
607
- const pkgRel = relOf(meta.dir, MANIFEST);
608
- const key = `${item.origin}\0${meta.dir}`;
609
- if (synthetic.has(key) && !slot.has(pkgRel)) {
610
- slot.set(pkgRel, out.length);
611
- }
612
- }
613
- if (folded.has(item)) continue;
614
- if (meta && meta.kind === 'manifest') {
615
- const names = leftoverNames.get(`${item.origin}\0${meta.dir}`);
616
- const kept = names ? stripFoldedDepItem(item, names) : item;
617
- if (kept) {
618
- out.push(kept);
619
- slot.set(relOf(meta.dir, MANIFEST), out.length);
620
- }
621
- continue;
622
- }
623
- out.push(item);
624
- }
625
- return spliceAtSlots(out, synthetic, slot);
473
+ return placeFolded(items, { folded, synthetic, leftoverNames });
626
474
  };
627
475
 
628
476
  const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
@@ -630,7 +478,7 @@ const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
630
478
  if (!pkg) return [];
631
479
  const lock = jsonParse(extra.lockText ?? '');
632
480
  const used = extra.usedNames;
633
- const names = unusedDeclaredNames(pkg, used);
481
+ const names = unusedDeclared(pkg, used).map((dep) => dep.name);
634
482
  const exportEntries = loadExportEntries(names, extra, extra.dir);
635
483
  const options = { used, exportEntries };
636
484
  const changes = buildProposedChanges(pkg, outdated, audit, lock, options);
@@ -645,39 +493,32 @@ const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
645
493
 
646
494
  const mergeProposedItems = (items, pkgText, outdated, audit, extra = {}) => {
647
495
  const proposed = proposeDepItems(pkgText, outdated, audit, extra);
648
- if (!proposed.length) return items;
649
496
  const taken = new Set();
650
497
  for (const item of items) {
651
- const change = item.dep && item.dep.change;
498
+ const change = item.dep?.change;
652
499
  if (change) taken.add(change.name);
653
500
  }
654
- const added = [];
655
- for (const item of proposed) {
656
- const change = item.dep && item.dep.change;
657
- if (!change || taken.has(change.name)) continue;
658
- added.push(item);
659
- }
501
+ const added = proposed.filter((item) => {
502
+ const change = item.dep?.change;
503
+ return change && !taken.has(change.name);
504
+ });
660
505
  if (!added.length) return items;
661
506
  const rel = relOf(extra.dir ?? '.', MANIFEST);
662
- return insertAfterPath(items, added, rel);
507
+ const at = items.findLastIndex(
508
+ (item) => listFilePath(itemPath(item)) === rel,
509
+ );
510
+ if (at < 0) return [...items, ...added];
511
+ return [...items.slice(0, at + 1), ...added, ...items.slice(at + 1)];
663
512
  };
664
513
 
665
514
  module.exports = {
666
515
  MANIFEST,
667
516
  LOCKFILE,
668
517
  depFileMeta,
669
- readSections,
670
- lockEntries,
671
- lockPackageCount,
672
- diffSections,
673
- mergeDepFile,
674
- mergeResolved,
675
518
  collectUsedNames,
676
519
  collectExportMeta,
677
520
  parseAuditReport,
678
521
  parseOutdatedReport,
679
- applyWantedRange,
680
- patchedFromRange,
681
522
  proposeDepItems,
682
523
  foldDepItems,
683
524
  mergeProposedItems,