reslop 0.1.1 → 0.1.2

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.
package/lib/deps.js ADDED
@@ -0,0 +1,1440 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { itemPath } = require('./files.js');
7
+ const { countSides, isCtxType } = require('./diff.js');
8
+
9
+ const MANIFEST = 'package.json';
10
+ const LOCKFILE = 'package-lock.json';
11
+ const DEP_KIND = { [MANIFEST]: 'manifest', [LOCKFILE]: 'lockfile' };
12
+ const SECTION_KEYS = [
13
+ 'dependencies',
14
+ 'devDependencies',
15
+ 'optionalDependencies',
16
+ 'peerDependencies',
17
+ ];
18
+ const SECTION_ORDER = [...SECTION_KEYS, 'resolved', 'field'];
19
+ const ACTION_ORDER = ['added', 'removed', 'changed', 'vulnerable'];
20
+ const SECTION_NAMES = SECTION_KEYS.join('|');
21
+ const SECTION_OPEN = new RegExp(`^\\s*"(${SECTION_NAMES})"\\s*:\\s*\\{\\s*$`);
22
+ const SECTION_END = /^\s*\},?\s*$/;
23
+ const DEP_ENTRY = /^\s*"([^"]+)"\s*:\s*"([^"]*)"\s*,?\s*$/;
24
+ const DEP_CAPTION = 'Dependencies in package.json & package-lock.json';
25
+ const SKIP_WALK = ['node_modules', '.git', '.review', 'coverage', 'dist'];
26
+ const SOURCE_EXT = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'];
27
+ const REQUIRE_SPEC = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
28
+ const FROM_SPEC = /\bfrom\s+['"]([^'"]+)['"]/g;
29
+ const IMPORT_FILE = /import\s+['"]([^'"]+)['"]/g;
30
+ const IMPORT_CALL = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
31
+ const SEVERITY_RANK = { critical: 4, high: 3, moderate: 2, low: 1, info: 0 };
32
+
33
+ const noSides = () => ({ oldText: '', newText: '' });
34
+
35
+ const posixRel = (rel) => {
36
+ const raw = `${rel ?? ''}`;
37
+ return raw.replaceAll('\\', '/');
38
+ };
39
+
40
+ const depFileMeta = (rel) => {
41
+ const normalized = posixRel(rel);
42
+ if (!normalized) return null;
43
+ const base = path.posix.basename(normalized);
44
+ const kind = DEP_KIND[base];
45
+ if (!kind) return null;
46
+ const dir = path.posix.dirname(normalized);
47
+ return { kind, dir, base, rel: normalized };
48
+ };
49
+
50
+ const relOf = (dir, name) => (dir === '.' ? name : `${dir}/${name}`);
51
+
52
+ const parseJson = (text) => {
53
+ if (!text || !`${text}`.trim()) return null;
54
+ try {
55
+ const data = JSON.parse(text);
56
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
57
+ return data;
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+
63
+ const emptySectionMaps = () => {
64
+ const maps = {};
65
+ for (const key of SECTION_KEYS) maps[key] = new Map();
66
+ return maps;
67
+ };
68
+
69
+ const readSectionMap = (pkg, key) => {
70
+ const map = new Map();
71
+ const raw = pkg ? pkg[key] : null;
72
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return map;
73
+ for (const name of Object.keys(raw)) {
74
+ const version = raw[name];
75
+ if (typeof version === 'string') map.set(name, version);
76
+ }
77
+ return map;
78
+ };
79
+
80
+ const readSections = (pkg) => {
81
+ const maps = emptySectionMaps();
82
+ if (!pkg) return maps;
83
+ for (const key of SECTION_KEYS) maps[key] = readSectionMap(pkg, key);
84
+ return maps;
85
+ };
86
+
87
+ const lockRootSections = (lock) => {
88
+ if (!lock) return emptySectionMaps();
89
+ const packages = lock.packages;
90
+ const root = packages && typeof packages === 'object' ? packages[''] : null;
91
+ const src = root && typeof root === 'object' ? root : lock;
92
+ return readSections(src);
93
+ };
94
+
95
+ const lockEntries = (lock) => {
96
+ const versions = new Map();
97
+ if (!lock) return versions;
98
+ const packages = lock.packages;
99
+ if (packages && typeof packages === 'object') {
100
+ for (const key of Object.keys(packages)) {
101
+ const entry = packages[key];
102
+ if (!key.startsWith('node_modules/')) continue;
103
+ if (key.includes('/node_modules/')) continue;
104
+ if (!entry || typeof entry.version !== 'string') continue;
105
+ const name = key.slice('node_modules/'.length);
106
+ versions.set(name, entry.version);
107
+ }
108
+ }
109
+ const deps = lock.dependencies;
110
+ if (!deps || typeof deps !== 'object') return versions;
111
+ for (const name of Object.keys(deps)) {
112
+ const entry = deps[name];
113
+ if (versions.has(name)) continue;
114
+ if (!entry || typeof entry.version !== 'string') continue;
115
+ versions.set(name, entry.version);
116
+ }
117
+ return versions;
118
+ };
119
+
120
+ const lockPackageCount = (lock) => {
121
+ if (!lock) return 0;
122
+ const packages = lock.packages;
123
+ if (packages && typeof packages === 'object') {
124
+ let count = 0;
125
+ for (const key of Object.keys(packages)) {
126
+ if (key !== '') count += 1;
127
+ }
128
+ return count;
129
+ }
130
+ const deps = lock.dependencies;
131
+ if (!deps || typeof deps !== 'object') return 0;
132
+ return Object.keys(deps).length;
133
+ };
134
+
135
+ const diffMaps = (oldMap, newMap, section) => {
136
+ const keys = [...oldMap.keys(), ...newMap.keys()];
137
+ const names = [...new Set(keys)];
138
+ names.sort();
139
+ const changes = [];
140
+ for (const name of names) {
141
+ const from = oldMap.has(name) ? oldMap.get(name) : '';
142
+ const to = newMap.has(name) ? newMap.get(name) : '';
143
+ if (from === to) continue;
144
+ let action = 'changed';
145
+ if (!oldMap.has(name)) action = 'added';
146
+ else if (!newMap.has(name)) action = 'removed';
147
+ changes.push({ name, section, action, from, to });
148
+ }
149
+ return changes;
150
+ };
151
+
152
+ const diffSections = (oldMaps, newMaps) => {
153
+ const changes = [];
154
+ for (const key of SECTION_KEYS) {
155
+ const found = diffMaps(oldMaps[key], newMaps[key], key);
156
+ for (const change of found) changes.push(change);
157
+ }
158
+ return changes;
159
+ };
160
+
161
+ const fieldText = (value) => {
162
+ if (value === undefined) return '';
163
+ if (typeof value === 'string') return value;
164
+ return JSON.stringify(value);
165
+ };
166
+
167
+ const sameJson = (left, right) =>
168
+ JSON.stringify(left) === JSON.stringify(right);
169
+
170
+ const diffOtherFields = (oldPkg, newPkg) => {
171
+ if (!oldPkg && !newPkg) return [];
172
+ const oldObj = oldPkg ?? {};
173
+ const newObj = newPkg ?? {};
174
+ const keys = [...Object.keys(oldObj), ...Object.keys(newObj)];
175
+ const names = [...new Set(keys)];
176
+ names.sort();
177
+ const changes = [];
178
+ for (const name of names) {
179
+ if (SECTION_KEYS.includes(name)) continue;
180
+ const prev = oldObj[name];
181
+ const next = newObj[name];
182
+ if (sameJson(prev, next)) continue;
183
+ let action = 'changed';
184
+ if (!Object.hasOwn(oldObj, name)) action = 'added';
185
+ else if (!Object.hasOwn(newObj, name)) action = 'removed';
186
+ const from = fieldText(prev);
187
+ const to = fieldText(next);
188
+ const section = 'field';
189
+ changes.push({ name, section, action, from, to });
190
+ }
191
+ return changes;
192
+ };
193
+
194
+ const onlyDepEdits = (items) => {
195
+ if (!items.length) return false;
196
+ for (const item of items) {
197
+ if (!item.hunk) return false;
198
+ const blockId = item.blockId;
199
+ const scoped = typeof blockId === 'number';
200
+ let section = '';
201
+ for (const line of item.hunk.lines) {
202
+ const open = SECTION_OPEN.exec(line.text);
203
+ let nextSection = section;
204
+ if (open) nextSection = open[1];
205
+ else if (section && SECTION_END.test(line.text)) nextSection = '';
206
+ const inBlock = !scoped || line.blockId === blockId;
207
+ if (inBlock && line.type !== 'ctx') {
208
+ const inDep = !!(open || section);
209
+ if (!inDep) return false;
210
+ const isEntry = DEP_ENTRY.test(line.text);
211
+ const isClose = SECTION_END.test(line.text);
212
+ if (!open && !isEntry && !isClose) return false;
213
+ }
214
+ section = nextSection;
215
+ }
216
+ }
217
+ return true;
218
+ };
219
+
220
+ const mapsFromHunks = (items) => {
221
+ const oldMaps = emptySectionMaps();
222
+ const newMaps = emptySectionMaps();
223
+ let section = '';
224
+ for (const item of items) {
225
+ if (!item.hunk) continue;
226
+ for (const line of item.hunk.lines) {
227
+ const open = SECTION_OPEN.exec(line.text);
228
+ if (open) {
229
+ section = open[1];
230
+ continue;
231
+ }
232
+ if (section && SECTION_END.test(line.text)) {
233
+ section = '';
234
+ continue;
235
+ }
236
+ if (!section) continue;
237
+ const entry = DEP_ENTRY.exec(line.text);
238
+ if (!entry) continue;
239
+ const name = entry[1];
240
+ const version = entry[2];
241
+ if (line.type === 'del' || line.type === 'ctx') {
242
+ oldMaps[section].set(name, version);
243
+ }
244
+ if (line.type === 'add' || line.type === 'ctx') {
245
+ newMaps[section].set(name, version);
246
+ }
247
+ }
248
+ }
249
+ return { oldMaps, newMaps };
250
+ };
251
+
252
+ const directNames = (oldMaps, newMaps) => {
253
+ const names = new Set();
254
+ for (const key of SECTION_KEYS) {
255
+ for (const name of oldMaps[key].keys()) names.add(name);
256
+ for (const name of newMaps[key].keys()) names.add(name);
257
+ }
258
+ return names;
259
+ };
260
+
261
+ const cloneJson = (value) => JSON.parse(JSON.stringify(value ?? null));
262
+
263
+ const detectIndent = (text) => {
264
+ const match = /\n([ \t]+)"/.exec(text);
265
+ if (!match) return 2;
266
+ return match[1];
267
+ };
268
+
269
+ const stringifyJson = (value, original) => {
270
+ const indent = detectIndent(original || '');
271
+ const body = JSON.stringify(value, null, indent);
272
+ return `${body}\n`;
273
+ };
274
+
275
+ const parseFieldValue = (text) => {
276
+ try {
277
+ return JSON.parse(text);
278
+ } catch {
279
+ return text;
280
+ }
281
+ };
282
+
283
+ const setSectionEntry = (pkg, section, name, version) => {
284
+ if (!version) {
285
+ const map = pkg[section];
286
+ if (!map || typeof map !== 'object') return;
287
+ delete map[name];
288
+ if (!Object.keys(map).length) delete pkg[section];
289
+ return;
290
+ }
291
+ const map = pkg[section];
292
+ if (!map || typeof map !== 'object') pkg[section] = {};
293
+ pkg[section][name] = version;
294
+ };
295
+
296
+ const dropsEntry = (change, side) => {
297
+ if (side === 'new') return change.action === 'removed';
298
+ return change.action === 'added';
299
+ };
300
+
301
+ const applyManifestChange = (pkg, change, side) => {
302
+ if (change.section === 'resolved') return pkg;
303
+ const value = side === 'new' ? change.to : change.from;
304
+ if (change.section === 'field') {
305
+ if (value === '') {
306
+ delete pkg[change.name];
307
+ return pkg;
308
+ }
309
+ pkg[change.name] = parseFieldValue(value);
310
+ return pkg;
311
+ }
312
+ const version = dropsEntry(change, side) ? '' : value;
313
+ setSectionEntry(pkg, change.section, change.name, version);
314
+ return pkg;
315
+ };
316
+
317
+ const ensureLockRoot = (lock) => {
318
+ if (!lock.packages || typeof lock.packages !== 'object') {
319
+ lock.packages = {};
320
+ }
321
+ const root = lock.packages[''];
322
+ if (!root || typeof root !== 'object') lock.packages[''] = {};
323
+ return lock.packages[''];
324
+ };
325
+
326
+ const lockNodeKey = (name) => `node_modules/${name}`;
327
+
328
+ const applyLegacyLockDep = (lock, change, side, src) => {
329
+ const deps = lock.dependencies;
330
+ if (!deps || typeof deps !== 'object') return;
331
+ if (dropsEntry(change, side)) {
332
+ delete deps[change.name];
333
+ return;
334
+ }
335
+ const srcDeps = src && src.dependencies;
336
+ if (!srcDeps || typeof srcDeps !== 'object') return;
337
+ const entry = srcDeps[change.name];
338
+ if (entry) deps[change.name] = cloneJson(entry);
339
+ };
340
+
341
+ const applyLockChange = (lock, change, side, oldLock, newLock) => {
342
+ if (change.section === 'field') return lock;
343
+ const src = side === 'new' ? newLock : oldLock;
344
+ if (change.section !== 'resolved') {
345
+ const root = ensureLockRoot(lock);
346
+ applyManifestChange(root, change, side);
347
+ applyLegacyLockDep(lock, change, side, src);
348
+ }
349
+ const packages = lock.packages;
350
+ if (!packages || typeof packages !== 'object') return lock;
351
+ const key = lockNodeKey(change.name);
352
+ if (dropsEntry(change, side)) {
353
+ delete packages[key];
354
+ return lock;
355
+ }
356
+ const srcPkgs = src && src.packages;
357
+ if (!srcPkgs || typeof srcPkgs !== 'object') return lock;
358
+ const entry = srcPkgs[key];
359
+ if (entry) packages[key] = cloneJson(entry);
360
+ return lock;
361
+ };
362
+
363
+ const seedManifest = (oldObj, newObj) => {
364
+ const base = cloneJson(oldObj) || {};
365
+ if (!newObj) return base;
366
+ for (const key of Object.keys(newObj)) {
367
+ const value = newObj[key];
368
+ if (SECTION_KEYS.includes(key)) continue;
369
+ if (Object.hasOwn(base, key)) continue;
370
+ base[key] = cloneJson(value);
371
+ }
372
+ return base;
373
+ };
374
+
375
+ const mergeDepFile = (baseText, oldText, newText, change, side, kind) => {
376
+ const oldObj = parseJson(oldText);
377
+ const newObj = parseJson(newText);
378
+ const parsedBase = parseJson(baseText);
379
+ let base;
380
+ if (parsedBase) base = cloneJson(parsedBase);
381
+ else if (kind === 'manifest') base = seedManifest(oldObj, newObj);
382
+ else base = cloneJson(oldObj) || {};
383
+ if (kind === 'lockfile') applyLockChange(base, change, side, oldObj, newObj);
384
+ else applyManifestChange(base, change, side);
385
+ const original = baseText || newText || oldText || '';
386
+ return stringifyJson(base, original);
387
+ };
388
+
389
+ const mergeResolved = (changes, oldResolved, newResolved, oldMaps, newMaps) => {
390
+ const seen = new Set();
391
+ const merged = [];
392
+ for (const change of changes) {
393
+ seen.add(change.name);
394
+ const resolvedFrom = oldResolved.get(change.name) ?? '';
395
+ const resolvedTo = newResolved.get(change.name) ?? '';
396
+ merged.push({ ...change, resolvedFrom, resolvedTo });
397
+ }
398
+ const names = [...directNames(oldMaps, newMaps)];
399
+ names.sort();
400
+ for (const name of names) {
401
+ if (seen.has(name)) continue;
402
+ const from = oldResolved.get(name) ?? '';
403
+ const to = newResolved.get(name) ?? '';
404
+ if (from === to) continue;
405
+ if (!from && !to) continue;
406
+ let action = 'changed';
407
+ if (!from) action = 'added';
408
+ else if (!to) action = 'removed';
409
+ const section = 'resolved';
410
+ const resolvedFrom = from;
411
+ const resolvedTo = to;
412
+ merged.push({ name, section, action, from, to, resolvedFrom, resolvedTo });
413
+ }
414
+ return merged;
415
+ };
416
+
417
+ const kindItems = (items, kind) => {
418
+ const matched = [];
419
+ for (const item of items) {
420
+ const rel = itemPath(item);
421
+ const meta = depFileMeta(rel);
422
+ if (meta && meta.kind === kind) matched.push(item);
423
+ }
424
+ return matched;
425
+ };
426
+
427
+ const uniquePaths = (items) => {
428
+ const files = [];
429
+ const seen = new Set();
430
+ for (const item of items) {
431
+ const rel = itemPath(item);
432
+ if (!rel || seen.has(rel)) continue;
433
+ seen.add(rel);
434
+ files.push(rel);
435
+ }
436
+ return files;
437
+ };
438
+
439
+ const filesForChange = (change, files) => {
440
+ const manifest = [];
441
+ const lockfile = [];
442
+ for (const rel of files) {
443
+ const meta = depFileMeta(rel);
444
+ if (!meta) continue;
445
+ if (meta.kind === 'manifest') manifest.push(rel);
446
+ else lockfile.push(rel);
447
+ }
448
+ if (change.section === 'field') {
449
+ if (manifest.length) return manifest;
450
+ return files;
451
+ }
452
+ if (change.section === 'resolved') {
453
+ if (lockfile.length) return lockfile;
454
+ return files;
455
+ }
456
+ if (change.propose) {
457
+ if (manifest.length) return manifest;
458
+ return files;
459
+ }
460
+ const both = [...manifest, ...lockfile];
461
+ if (both.length) return both;
462
+ return files;
463
+ };
464
+
465
+ const compareChangeOrder = (left, right) => {
466
+ const section = SECTION_ORDER.indexOf(left.section);
467
+ const other = SECTION_ORDER.indexOf(right.section);
468
+ if (section !== other) return section - other;
469
+ const leftAction = ACTION_ORDER.indexOf(left.action);
470
+ const rightAction = ACTION_ORDER.indexOf(right.action);
471
+ if (leftAction !== rightAction) return leftAction - rightAction;
472
+ return left.name.localeCompare(right.name);
473
+ };
474
+
475
+ const changeMarks = (change) => {
476
+ const marks = [];
477
+ if (change.unused) marks.push('unused');
478
+ if (change.outdated && !change.propose) marks.push('outdated');
479
+ if (change.audit) marks.push(change.audit.severity);
480
+ return marks;
481
+ };
482
+
483
+ const changeSources = (change) => {
484
+ const sources = [];
485
+ if (change.unused) {
486
+ sources.push('npm uninstall');
487
+ return sources;
488
+ }
489
+ if (change.propose && change.outdated) sources.push('npm outdated');
490
+ if (change.audit) sources.push('npm audit');
491
+ return sources;
492
+ };
493
+
494
+ const sectionTitle = (section, action, marks, sources) => {
495
+ const title = action === 'changed' ? 'version changed' : action;
496
+ const kind = section.replace(/ies$/, 'y');
497
+ let label = `${kind} ${title}`;
498
+ if (section === 'field') label = `package.json ${title}`;
499
+ if (section === 'resolved' && action === 'vulnerable') {
500
+ label = 'lockfile vulnerable';
501
+ } else if (section === 'resolved') {
502
+ label = `lockfile resolved ${title}`;
503
+ }
504
+ if (marks.includes('unused')) {
505
+ label = `${kind} unused`;
506
+ const extra = [];
507
+ for (const mark of marks) {
508
+ if (mark !== 'unused') extra.push(mark);
509
+ }
510
+ if (extra.length) label = `${label}, ${extra.join(', ')}`;
511
+ } else if (marks.length) {
512
+ label = `${label}, ${marks.join(', ')}`;
513
+ }
514
+ if (!sources.length) return label;
515
+ return `${sources.join(', ')}: ${label}`;
516
+ };
517
+
518
+ const entryText = (change, side) => {
519
+ const version = side === 'old' ? change.from : change.to;
520
+ return `"${change.name}": "${version}"`;
521
+ };
522
+
523
+ const ctxLine = (text) => {
524
+ const type = 'ctx';
525
+ return { type, text };
526
+ };
527
+
528
+ const warnLine = (text) => {
529
+ const type = 'warn';
530
+ return { type, text };
531
+ };
532
+
533
+ const noteLine = (text) => {
534
+ const type = 'note';
535
+ return { type, text };
536
+ };
537
+
538
+ const noteSepLine = () => {
539
+ const type = 'noteSep';
540
+ const text = '';
541
+ return { type, text };
542
+ };
543
+
544
+ const changeLine = (type, change, side) => {
545
+ const text = entryText(change, side);
546
+ return { type, text };
547
+ };
548
+
549
+ const pushChangeLines = (rows, change) => {
550
+ if (change.unused) {
551
+ const side = change.action === 'added' ? 'new' : 'old';
552
+ rows.push(changeLine('del', change, side));
553
+ return;
554
+ }
555
+ if (change.action === 'added') {
556
+ rows.push(changeLine('add', change, 'new'));
557
+ return;
558
+ }
559
+ if (change.action === 'removed') {
560
+ rows.push(changeLine('del', change, 'old'));
561
+ return;
562
+ }
563
+ if (change.from === change.to) {
564
+ rows.push(changeLine('ctx', change, 'old'));
565
+ return;
566
+ }
567
+ rows.push(changeLine('del', change, 'old'));
568
+ rows.push(changeLine('add', change, 'new'));
569
+ };
570
+
571
+ const lockCountText = (oldCount, newCount) => {
572
+ if (oldCount === newCount) return '';
573
+ return `lockfile packages ${oldCount} → ${newCount}`;
574
+ };
575
+
576
+ const auditNoteTexts = (audit) => {
577
+ if (!audit) return [];
578
+ const notes = [];
579
+ if (Array.isArray(audit.titles)) {
580
+ for (const note of audit.titles) {
581
+ if (note && !notes.includes(note)) notes.push(note);
582
+ }
583
+ } else if (audit.title) {
584
+ notes.push(audit.title);
585
+ }
586
+ const severity = audit.severity;
587
+ if (!notes.length) return [`npm audit ${severity}`];
588
+ const texts = [];
589
+ for (const note of notes) {
590
+ texts.push(`npm audit ${severity} ${note}`);
591
+ }
592
+ return texts;
593
+ };
594
+
595
+ const pushAuditNotes = (rows, audit) => {
596
+ const texts = auditNoteTexts(audit);
597
+ if (!texts.length) return;
598
+ rows.push(ctxLine(''));
599
+ let first = true;
600
+ for (const text of texts) {
601
+ if (!first) rows.push(noteSepLine());
602
+ first = false;
603
+ rows.push(noteLine(text));
604
+ }
605
+ };
606
+
607
+ const buildRows = (change, oldCount, newCount) => {
608
+ const rows = [ctxLine(DEP_CAPTION), ctxLine('')];
609
+ if (change) {
610
+ const marks = changeMarks(change);
611
+ const sources = changeSources(change);
612
+ const title = sectionTitle(change.section, change.action, marks, sources);
613
+ const audited = sources.includes('npm audit');
614
+ rows.push(audited ? warnLine(title) : ctxLine(title));
615
+ rows.push(ctxLine(''));
616
+ pushChangeLines(rows, change);
617
+ pushAuditNotes(rows, change.audit);
618
+ }
619
+ const countLine = lockCountText(oldCount, newCount);
620
+ if (countLine) {
621
+ if (rows.at(-1).text !== '') rows.push(ctxLine(''));
622
+ rows.push(ctxLine(countLine));
623
+ }
624
+ if (!change && !countLine) rows.push(ctxLine('lockfile updated'));
625
+ return rows;
626
+ };
627
+
628
+ const toHunkLine = (row, blockId) => {
629
+ const type = row.type;
630
+ const text = row.text;
631
+ const noNl = false;
632
+ const id = isCtxType(type) ? null : blockId;
633
+ return { type, text, noNl, blockId: id };
634
+ };
635
+
636
+ const makeHunk = (rows, blockId) => {
637
+ const lines = rows.map((row) => toHunkLine(row, blockId));
638
+ const counts = countSides(lines);
639
+ const oldCount = counts.oldCount;
640
+ const newCount = counts.newCount;
641
+ const header = `@@ -1,${oldCount} +1,${newCount} @@`;
642
+ return { oldStart: 1, oldCount, newStart: 1, newCount, header, lines };
643
+ };
644
+
645
+ const orderedChanges = (changes) => [...changes].sort(compareChangeOrder);
646
+
647
+ const pickPrimary = (items) => {
648
+ for (const item of items) {
649
+ const rel = itemPath(item);
650
+ const meta = depFileMeta(rel);
651
+ if (meta && meta.kind === 'manifest') return item;
652
+ }
653
+ return items[0];
654
+ };
655
+
656
+ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
657
+ const allFiles = uniquePaths(fileItems);
658
+ const files = change ? filesForChange(change, allFiles) : allFiles;
659
+ const primary = pickPrimary(fileItems);
660
+ const oldPath = itemPath(primary);
661
+ const newPath = oldPath;
662
+ const isNew = fileItems.some((item) => item.file && item.file.isNew);
663
+ const isDeleted = fileItems.every((item) => item.file && item.file.isDeleted);
664
+ let oldCount = 0;
665
+ let newCount = 0;
666
+ if (!change) {
667
+ oldCount = extra.oldCount ?? 0;
668
+ newCount = extra.newCount ?? 0;
669
+ }
670
+ const rows = buildRows(change, oldCount, newCount);
671
+ const hunk = makeHunk(rows, blockId);
672
+ const isBinary = false;
673
+ const preamble = [];
674
+ const hunks = [hunk];
675
+ const file = {
676
+ oldPath,
677
+ newPath,
678
+ isNew,
679
+ isDeleted,
680
+ isBinary,
681
+ preamble,
682
+ hunks,
683
+ };
684
+ const patchAdd = '';
685
+ const patchRevert = '';
686
+ const items = hidden;
687
+ const dep = { files, items, change };
688
+ const reload = !!(change && change.propose);
689
+ return { origin, file, hunk, blockId, patchAdd, patchRevert, dep, reload };
690
+ };
691
+
692
+ const makeDepItems = (origin, fileItems, hidden, summary) => {
693
+ const ordered = orderedChanges(summary.changes);
694
+ if (!ordered.length) {
695
+ return [makeDepItem(origin, fileItems, hidden, null, summary, 0)];
696
+ }
697
+ const items = [];
698
+ let blockId = 0;
699
+ for (const change of ordered) {
700
+ const made = makeDepItem(
701
+ origin,
702
+ fileItems,
703
+ hidden,
704
+ change,
705
+ summary,
706
+ blockId,
707
+ );
708
+ items.push(made);
709
+ blockId += 1;
710
+ }
711
+ return items;
712
+ };
713
+
714
+ const summarizeGroup = (group, readSides) => {
715
+ const origin = group.origin;
716
+ const dir = group.dir;
717
+ const groupItems = group.items;
718
+ const manifestItems = kindItems(groupItems, 'manifest');
719
+ const lockItems = kindItems(groupItems, 'lockfile');
720
+ const manifestRel = relOf(dir, MANIFEST);
721
+ const lockRel = relOf(dir, LOCKFILE);
722
+ const manifestSides = readSides(origin, manifestRel);
723
+ const lockSides = readSides(origin, lockRel);
724
+ const oldPkg = parseJson(manifestSides.oldText);
725
+ const newPkg = parseJson(manifestSides.newText);
726
+ const oldLock = parseJson(lockSides.oldText);
727
+ const newLock = parseJson(lockSides.newText);
728
+ const parsedManifest = !!(oldPkg || newPkg);
729
+ let oldMaps;
730
+ let newMaps;
731
+ if (parsedManifest) {
732
+ oldMaps = readSections(oldPkg);
733
+ newMaps = readSections(newPkg);
734
+ } else if (manifestItems.length) {
735
+ const parsed = mapsFromHunks(manifestItems);
736
+ oldMaps = parsed.oldMaps;
737
+ newMaps = parsed.newMaps;
738
+ } else {
739
+ oldMaps = lockRootSections(oldLock);
740
+ newMaps = lockRootSections(newLock);
741
+ }
742
+ const manifestChanges = diffSections(oldMaps, newMaps);
743
+ const otherChanges = parsedManifest ? diffOtherFields(oldPkg, newPkg) : [];
744
+ const oldResolved = lockEntries(oldLock);
745
+ const newResolved = lockEntries(newLock);
746
+ const changes = mergeResolved(
747
+ manifestChanges,
748
+ oldResolved,
749
+ newResolved,
750
+ oldMaps,
751
+ newMaps,
752
+ );
753
+ const oldCount = lockPackageCount(oldLock);
754
+ const newCount = lockPackageCount(newLock);
755
+ const foldLock = lockItems.length > 0;
756
+ const hasDepChanges = changes.length > 0;
757
+ const hunkOnlyDeps =
758
+ !parsedManifest && hasDepChanges && onlyDepEdits(manifestItems);
759
+ let foldManifest = false;
760
+ let takeAllManifest = false;
761
+ if (manifestItems.length) {
762
+ if (parsedManifest) {
763
+ foldManifest = hasDepChanges;
764
+ takeAllManifest = hasDepChanges && otherChanges.length === 0;
765
+ } else {
766
+ foldManifest = hunkOnlyDeps;
767
+ takeAllManifest = hunkOnlyDeps;
768
+ }
769
+ }
770
+ return {
771
+ changes,
772
+ oldCount,
773
+ newCount,
774
+ foldLock,
775
+ foldManifest,
776
+ takeAllManifest,
777
+ manifestItems,
778
+ lockItems,
779
+ pkg: newPkg || oldPkg,
780
+ };
781
+ };
782
+
783
+ const collectGroups = (items) => {
784
+ const groups = new Map();
785
+ for (const item of items) {
786
+ const rel = itemPath(item);
787
+ const meta = depFileMeta(rel);
788
+ if (!meta) continue;
789
+ const key = `${item.origin}\0${meta.dir}`;
790
+ let group = groups.get(key);
791
+ if (!group) {
792
+ const dir = meta.dir;
793
+ const origin = item.origin;
794
+ const items = [];
795
+ group = { dir, origin, items };
796
+ groups.set(key, group);
797
+ }
798
+ group.items.push(item);
799
+ }
800
+ return groups;
801
+ };
802
+
803
+ const takeManifestItems = (summary) => {
804
+ if (!summary.foldManifest) return [];
805
+ if (summary.takeAllManifest) return [...summary.manifestItems];
806
+ const taken = [];
807
+ for (const item of summary.manifestItems) {
808
+ if (onlyDepEdits([item])) taken.push(item);
809
+ }
810
+ return taken;
811
+ };
812
+
813
+ const toPackageName = (spec) => {
814
+ if (!spec || spec.startsWith('.') || spec.startsWith('/')) return '';
815
+ if (spec.startsWith('node:')) return '';
816
+ if (spec.startsWith('@')) {
817
+ const parts = spec.split('/');
818
+ if (parts.length < 2) return '';
819
+ return `${parts[0]}/${parts[1]}`;
820
+ }
821
+ return spec.split('/')[0];
822
+ };
823
+
824
+ const addSpec = (used, spec) => {
825
+ const name = toPackageName(spec);
826
+ if (name) used.add(name);
827
+ };
828
+
829
+ const addMatches = (used, text, pattern) => {
830
+ pattern.lastIndex = 0;
831
+ let match = pattern.exec(text);
832
+ while (match) {
833
+ addSpec(used, match[1]);
834
+ match = pattern.exec(text);
835
+ }
836
+ };
837
+
838
+ const collectUsedFromText = (used, text) => {
839
+ addMatches(used, text, REQUIRE_SPEC);
840
+ addMatches(used, text, FROM_SPEC);
841
+ addMatches(used, text, IMPORT_FILE);
842
+ addMatches(used, text, IMPORT_CALL);
843
+ };
844
+
845
+ const collectUsedFromScripts = (used, scripts) => {
846
+ if (!scripts || typeof scripts !== 'object') return;
847
+ for (const script of Object.values(scripts)) {
848
+ const tokens = `${script}`.split(/[\s;&|]+/);
849
+ for (const token of tokens) {
850
+ if (!token || token.startsWith('-')) continue;
851
+ if (token === 'npx' || token === 'npm' || token === 'node') continue;
852
+ addSpec(used, token);
853
+ }
854
+ }
855
+ };
856
+
857
+ const collectUsedFromManifest = (used, rel) => {
858
+ let text;
859
+ try {
860
+ text = fs.readFileSync(rel, 'utf8');
861
+ } catch {
862
+ return;
863
+ }
864
+ const pkg = parseJson(text);
865
+ if (!pkg) return;
866
+ collectUsedFromScripts(used, pkg.scripts);
867
+ };
868
+
869
+ const walkUsedNames = (used, dir) => {
870
+ let entries;
871
+ try {
872
+ entries = fs.readdirSync(dir, { withFileTypes: true });
873
+ } catch {
874
+ return;
875
+ }
876
+ for (const entry of entries) {
877
+ if (entry.isSymbolicLink()) continue;
878
+ const full = path.join(dir, entry.name);
879
+ if (entry.isDirectory()) {
880
+ if (SKIP_WALK.includes(entry.name)) continue;
881
+ if (entry.name.startsWith('.')) continue;
882
+ walkUsedNames(used, full);
883
+ continue;
884
+ }
885
+ if (entry.name === MANIFEST) {
886
+ collectUsedFromManifest(used, full);
887
+ continue;
888
+ }
889
+ const ext = path.extname(entry.name);
890
+ if (!SOURCE_EXT.includes(ext)) continue;
891
+ let text;
892
+ try {
893
+ text = fs.readFileSync(full, 'utf8');
894
+ } catch {
895
+ continue;
896
+ }
897
+ collectUsedFromText(used, text);
898
+ }
899
+ };
900
+
901
+ const collectUsedNames = (root) => {
902
+ const used = new Set();
903
+ if (!root) return used;
904
+ walkUsedNames(used, root);
905
+ return used;
906
+ };
907
+
908
+ const pkgHasExportEntry = (pkg) => {
909
+ if (!pkg || typeof pkg !== 'object') return false;
910
+ const main = pkg.main;
911
+ if (typeof main === 'string' && main) return true;
912
+ const esm = pkg.module;
913
+ if (typeof esm === 'string' && esm) return true;
914
+ const exported = pkg.exports;
915
+ if (typeof exported === 'string' && exported) return true;
916
+ if (!exported || typeof exported !== 'object') return false;
917
+ if (Array.isArray(exported)) return exported.length > 0;
918
+ return Object.keys(exported).length > 0;
919
+ };
920
+
921
+ const depModuleParts = (name) => {
922
+ if (!name.startsWith('@')) return [name];
923
+ const parts = name.split('/');
924
+ if (parts.length < 2) return [];
925
+ return parts.slice(0, 2);
926
+ };
927
+
928
+ const readDepPkg = (root, pkgDir, name) => {
929
+ if (!root || !name) return null;
930
+ const parts = depModuleParts(name);
931
+ if (!parts.length) return null;
932
+ const nested = pkgDir && pkgDir !== '.' ? pkgDir : '';
933
+ let dir = nested ? path.join(root, nested) : root;
934
+ while (true) {
935
+ const file = path.join(dir, 'node_modules', ...parts, MANIFEST);
936
+ let text = '';
937
+ try {
938
+ text = fs.readFileSync(file, 'utf8');
939
+ } catch {
940
+ // Missing node_modules copy; try a parent directory.
941
+ }
942
+ if (text) return parseJson(text);
943
+ const parent = path.dirname(dir);
944
+ if (parent === dir) return null;
945
+ dir = parent;
946
+ }
947
+ };
948
+
949
+ const depHasExportEntry = (root, pkgDir, name) => {
950
+ const pkg = readDepPkg(root, pkgDir, name);
951
+ if (!pkg) return true;
952
+ return pkgHasExportEntry(pkg);
953
+ };
954
+
955
+ const markUnusedChanges = (changes, used, pkg, extra = {}) => {
956
+ if (!pkgHasExportEntry(pkg)) return;
957
+ if (!used || !used.size) return;
958
+ const root = extra.root;
959
+ const pkgDir = extra.dir;
960
+ for (const change of changes) {
961
+ if (change.action !== 'added') continue;
962
+ if (change.section === 'resolved') continue;
963
+ if (change.section === 'field') continue;
964
+ const unused = !used.has(change.name);
965
+ if (!unused) continue;
966
+ if (!depHasExportEntry(root, pkgDir, change.name)) continue;
967
+ change.unused = true;
968
+ change.propose = true;
969
+ }
970
+ };
971
+
972
+ const namedTitle = (name, title) => {
973
+ if (!title) return '';
974
+ if (!name) return title;
975
+ const prefix = `${name}: `;
976
+ if (title.startsWith(prefix)) return title;
977
+ return `${prefix}${title}`;
978
+ };
979
+
980
+ const viaNotes = (via, pkgName) => {
981
+ if (!Array.isArray(via)) return [];
982
+ const notes = [];
983
+ for (const item of via) {
984
+ if (!item || typeof item !== 'object') continue;
985
+ const title = typeof item.title === 'string' ? item.title : '';
986
+ if (!title) continue;
987
+ const hasName = typeof item.name === 'string' && item.name;
988
+ const named = hasName ? item.name : pkgName;
989
+ const note = namedTitle(named, title);
990
+ if (!notes.includes(note)) notes.push(note);
991
+ }
992
+ return notes;
993
+ };
994
+
995
+ const severityRank = (severity) => SEVERITY_RANK[severity] ?? 0;
996
+
997
+ const auditFix = (entry) => {
998
+ const available = entry.fixAvailable;
999
+ if (!available || typeof available !== 'object') return '';
1000
+ if (typeof available.version !== 'string') return '';
1001
+ return available.version;
1002
+ };
1003
+
1004
+ const auditEntryNames = (key, entry) => {
1005
+ const names = [];
1006
+ const primary = entry.name || key;
1007
+ if (primary) names.push(primary);
1008
+ const effects = entry.effects;
1009
+ if (!Array.isArray(effects)) return names;
1010
+ for (const effect of effects) {
1011
+ if (typeof effect === 'string' && effect) names.push(effect);
1012
+ }
1013
+ return names;
1014
+ };
1015
+
1016
+ const keepHighestAudit = (audit, name, severity, fix, range, notes) => {
1017
+ if (!name || !severity || severity === 'info') return;
1018
+ const titles = [];
1019
+ for (const note of notes) {
1020
+ if (note && !titles.includes(note)) titles.push(note);
1021
+ }
1022
+ const prev = audit.get(name);
1023
+ if (prev && severityRank(prev.severity) >= severityRank(severity)) {
1024
+ if (!prev.fix && fix) prev.fix = fix;
1025
+ if (!prev.range && range) prev.range = range;
1026
+ for (const note of titles) {
1027
+ if (!prev.titles.includes(note)) prev.titles.push(note);
1028
+ }
1029
+ return;
1030
+ }
1031
+ const title = titles[0] || '';
1032
+ const next = { name, severity, title, titles, fix, range };
1033
+ next.fix ||= '';
1034
+ next.range ||= '';
1035
+ audit.set(name, next);
1036
+ };
1037
+
1038
+ const parseAuditReport = (text) => {
1039
+ const audit = new Map();
1040
+ if (!text || !`${text}`.trim()) return audit;
1041
+ let data;
1042
+ try {
1043
+ data = JSON.parse(text);
1044
+ } catch {
1045
+ return audit;
1046
+ }
1047
+ if (!data || typeof data !== 'object') return audit;
1048
+ const vulns = data.vulnerabilities;
1049
+ if (vulns && typeof vulns === 'object') {
1050
+ for (const key of Object.keys(vulns)) {
1051
+ const entry = vulns[key];
1052
+ if (!entry || typeof entry !== 'object') continue;
1053
+ const severity = entry.severity;
1054
+ const names = auditEntryNames(key, entry);
1055
+ const pkgName = entry.name || key;
1056
+ const titles = viaNotes(entry.via, pkgName);
1057
+ const fix = auditFix(entry);
1058
+ const range = typeof entry.range === 'string' ? entry.range : '';
1059
+ for (const name of names) {
1060
+ keepHighestAudit(audit, name, severity, fix, range, titles);
1061
+ }
1062
+ }
1063
+ }
1064
+ const advisories = data.advisories;
1065
+ if (advisories && typeof advisories === 'object') {
1066
+ for (const entry of Object.values(advisories)) {
1067
+ if (!entry || typeof entry !== 'object') continue;
1068
+ const name = entry.module_name || entry.name;
1069
+ const severity = entry.severity;
1070
+ const raw = typeof entry.title === 'string' ? entry.title : '';
1071
+ const title = namedTitle(name, raw);
1072
+ const titles = title ? [title] : [];
1073
+ const fix = auditFix(entry);
1074
+ const range = typeof entry.range === 'string' ? entry.range : '';
1075
+ keepHighestAudit(audit, name, severity, fix, range, titles);
1076
+ }
1077
+ }
1078
+ return audit;
1079
+ };
1080
+
1081
+ const parseOutdatedReport = (text) => {
1082
+ const outdated = new Map();
1083
+ if (!text || !`${text}`.trim()) return outdated;
1084
+ let data;
1085
+ try {
1086
+ data = JSON.parse(text);
1087
+ } catch {
1088
+ return outdated;
1089
+ }
1090
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return outdated;
1091
+ for (const name of Object.keys(data)) {
1092
+ const entry = data[name];
1093
+ if (!entry || typeof entry !== 'object') continue;
1094
+ const current = typeof entry.current === 'string' ? entry.current : '';
1095
+ const wanted = typeof entry.wanted === 'string' ? entry.wanted : '';
1096
+ const latest = typeof entry.latest === 'string' ? entry.latest : '';
1097
+ const type = typeof entry.type === 'string' ? entry.type : '';
1098
+ if (!wanted) continue;
1099
+ outdated.set(name, { current, wanted, latest, type });
1100
+ }
1101
+ return outdated;
1102
+ };
1103
+
1104
+ const applyWantedRange = (declared, wanted) => {
1105
+ if (!wanted) return declared ?? '';
1106
+ if (!declared) return wanted;
1107
+ if (/^(file:|git[+@:]|workspace:|https?:|npm:)/.test(declared)) {
1108
+ return declared;
1109
+ }
1110
+ if (declared.includes(' ') || declared.includes('|')) return declared;
1111
+ if (declared.includes('<') || declared.includes('>')) return declared;
1112
+ const prefix = declared.startsWith('^') || declared.startsWith('~');
1113
+ const mark = prefix ? declared[0] : '';
1114
+ return `${mark}${wanted}`;
1115
+ };
1116
+
1117
+ const findDeclared = (pkg, name, type) => {
1118
+ if (type && SECTION_KEYS.includes(type)) {
1119
+ const map = pkg[type];
1120
+ if (map && typeof map[name] === 'string') {
1121
+ return { section: type, version: map[name] };
1122
+ }
1123
+ }
1124
+ for (const section of SECTION_KEYS) {
1125
+ const map = pkg[section];
1126
+ if (map && typeof map[name] === 'string') {
1127
+ return { section, version: map[name] };
1128
+ }
1129
+ }
1130
+ return null;
1131
+ };
1132
+
1133
+ const VER_CORE = /^(\d+)\.(\d+)\.(\d+)/;
1134
+
1135
+ const parseVer = (text) => {
1136
+ const match = `${text}`.match(VER_CORE);
1137
+ if (!match) return null;
1138
+ const major = Number(match[1]);
1139
+ const minor = Number(match[2]);
1140
+ const patch = Number(match[3]);
1141
+ return [major, minor, patch];
1142
+ };
1143
+
1144
+ const fmtVer = (parts) => `${parts[0]}.${parts[1]}.${parts[2]}`;
1145
+
1146
+ const cmpVer = (left, right) => {
1147
+ if (left[0] !== right[0]) return left[0] - right[0];
1148
+ if (left[1] !== right[1]) return left[1] - right[1];
1149
+ return left[2] - right[2];
1150
+ };
1151
+
1152
+ const bumpPatch = (parts) => {
1153
+ const major = parts[0];
1154
+ const minor = parts[1];
1155
+ const patch = parts[2] + 1;
1156
+ return [major, minor, patch];
1157
+ };
1158
+
1159
+ const clauseUpper = (clause) => {
1160
+ const pattern = /(<=?)\s*(\d+\.\d+\.\d+)/g;
1161
+ const matches = clause.matchAll(pattern);
1162
+ let best = null;
1163
+ for (const match of matches) {
1164
+ const op = match[1];
1165
+ const parsed = parseVer(match[2]);
1166
+ if (!parsed) continue;
1167
+ if (best && cmpVer(parsed, best.ver) >= 0) continue;
1168
+ best = { op, ver: parsed };
1169
+ }
1170
+ return best;
1171
+ };
1172
+
1173
+ const patchedFromRange = (current, range) => {
1174
+ if (!current || !range) return '';
1175
+ const now = parseVer(current);
1176
+ if (!now) return '';
1177
+ const clauses = `${range}`.split('||');
1178
+ for (const clause of clauses) {
1179
+ const upper = clauseUpper(clause);
1180
+ if (!upper) continue;
1181
+ if (now[0] !== upper.ver[0]) continue;
1182
+ if (cmpVer(now, upper.ver) > 0) continue;
1183
+ if (upper.op === '<') return fmtVer(upper.ver);
1184
+ return fmtVer(bumpPatch(upper.ver));
1185
+ }
1186
+ return '';
1187
+ };
1188
+
1189
+ const sameMajor = (from, to) => {
1190
+ const left = parseVer(from);
1191
+ const right = parseVer(to);
1192
+ if (!left || !right) return false;
1193
+ return left[0] === right[0];
1194
+ };
1195
+
1196
+ const proposedLockVersion = (current, vul) => {
1197
+ if (!current) return '';
1198
+ if (!vul) return current;
1199
+ const fix = vul.fix;
1200
+ if (fix && sameMajor(current, fix) && fix !== current) return fix;
1201
+ const patched = patchedFromRange(current, vul.range);
1202
+ if (patched && patched !== current) return patched;
1203
+ return current;
1204
+ };
1205
+
1206
+ const proposedWanted = (out, vul) => {
1207
+ if (out && out.wanted && out.wanted !== out.current) return out.wanted;
1208
+ if (vul && vul.fix) return vul.fix;
1209
+ return '';
1210
+ };
1211
+
1212
+ const pushUnusedChanges = (changes, taken, pkg, used, extra = {}) => {
1213
+ if (!pkgHasExportEntry(pkg)) return;
1214
+ if (!used || !used.size) return;
1215
+ const root = extra.root;
1216
+ const pkgDir = extra.dir;
1217
+ const maps = readSections(pkg);
1218
+ for (const section of SECTION_KEYS) {
1219
+ if (section === 'peerDependencies') continue;
1220
+ const map = maps[section];
1221
+ const names = [...map.keys()];
1222
+ names.sort();
1223
+ for (const name of names) {
1224
+ if (used.has(name)) continue;
1225
+ if (taken.has(name)) continue;
1226
+ if (!depHasExportEntry(root, pkgDir, name)) continue;
1227
+ taken.add(name);
1228
+ const version = map.get(name);
1229
+ changes.push({
1230
+ name,
1231
+ section,
1232
+ action: 'removed',
1233
+ from: version,
1234
+ to: '',
1235
+ propose: true,
1236
+ unused: true,
1237
+ });
1238
+ }
1239
+ }
1240
+ };
1241
+
1242
+ const buildProposedChanges = (pkg, outdated, audit, lock, used, extra = {}) => {
1243
+ const names = new Set();
1244
+ if (outdated) {
1245
+ for (const name of outdated.keys()) names.add(name);
1246
+ }
1247
+ if (audit) {
1248
+ for (const name of audit.keys()) names.add(name);
1249
+ }
1250
+ const listed = [...names];
1251
+ listed.sort();
1252
+ const resolved = lockEntries(lock);
1253
+ const changes = [];
1254
+ const taken = new Set();
1255
+ pushUnusedChanges(changes, taken, pkg, used, extra);
1256
+ for (const name of listed) {
1257
+ if (taken.has(name)) continue;
1258
+ const out = outdated ? outdated.get(name) : null;
1259
+ const vul = audit ? audit.get(name) : null;
1260
+ const type = out ? out.type : '';
1261
+ const declared = findDeclared(pkg, name, type);
1262
+ if (!declared) {
1263
+ if (!vul) continue;
1264
+ const version = resolved.get(name) || '';
1265
+ if (!version) continue;
1266
+ const to = proposedLockVersion(version, vul);
1267
+ changes.push({
1268
+ name,
1269
+ section: 'resolved',
1270
+ action: 'vulnerable',
1271
+ from: version,
1272
+ to,
1273
+ propose: true,
1274
+ audit: vul,
1275
+ });
1276
+ continue;
1277
+ }
1278
+ const wanted = proposedWanted(out, vul);
1279
+ if (!wanted && !vul) continue;
1280
+ const to = wanted
1281
+ ? applyWantedRange(declared.version, wanted)
1282
+ : declared.version;
1283
+ if (to === declared.version && !vul) continue;
1284
+ let action = 'changed';
1285
+ if (to === declared.version) action = 'vulnerable';
1286
+ const change = {
1287
+ name,
1288
+ section: declared.section,
1289
+ action,
1290
+ from: declared.version,
1291
+ to,
1292
+ propose: true,
1293
+ };
1294
+ if (out) change.outdated = out;
1295
+ if (vul) change.audit = vul;
1296
+ changes.push(change);
1297
+ }
1298
+ return changes;
1299
+ };
1300
+
1301
+ const stubFileItem = (rel, origin) => {
1302
+ const file = {
1303
+ oldPath: rel,
1304
+ newPath: rel,
1305
+ isNew: false,
1306
+ isDeleted: false,
1307
+ isBinary: false,
1308
+ preamble: [],
1309
+ hunks: [],
1310
+ };
1311
+ const hunk = null;
1312
+ const blockId = 0;
1313
+ const patchAdd = '';
1314
+ const patchRevert = '';
1315
+ return { origin, file, hunk, blockId, patchAdd, patchRevert };
1316
+ };
1317
+
1318
+ const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
1319
+ const pkg = parseJson(pkgText);
1320
+ if (!pkg) return [];
1321
+ const lock = parseJson(extra.lockText ?? '');
1322
+ const used = extra.usedNames;
1323
+ const changes = buildProposedChanges(pkg, outdated, audit, lock, used, extra);
1324
+ if (!changes.length) return [];
1325
+ const stubs = [
1326
+ stubFileItem(MANIFEST, 'unstaged'),
1327
+ stubFileItem(LOCKFILE, 'unstaged'),
1328
+ ];
1329
+ const summary = { changes, oldCount: 0, newCount: 0 };
1330
+ return makeDepItems('unstaged', stubs, [], summary);
1331
+ };
1332
+
1333
+ const markAuditChanges = (changes, audit) => {
1334
+ if (!audit) return;
1335
+ for (const change of changes) {
1336
+ if (change.action === 'removed') continue;
1337
+ if (change.section === 'field') continue;
1338
+ const found = audit.get(change.name);
1339
+ if (found) change.audit = found;
1340
+ }
1341
+ };
1342
+
1343
+ const markOutdatedChanges = (changes, outdated) => {
1344
+ if (!outdated) return;
1345
+ for (const change of changes) {
1346
+ if (change.action === 'removed') continue;
1347
+ if (change.section === 'field') continue;
1348
+ const found = outdated.get(change.name);
1349
+ if (!found) continue;
1350
+ if (found.wanted && found.wanted === found.current) continue;
1351
+ change.outdated = found;
1352
+ }
1353
+ };
1354
+
1355
+ const foldDepItems = (
1356
+ items,
1357
+ readSides = noSides,
1358
+ usedNames = null,
1359
+ audit = null,
1360
+ outdated = null,
1361
+ extra = {},
1362
+ ) => {
1363
+ const groups = collectGroups(items);
1364
+ if (!groups.size) return items;
1365
+ const folded = new Set();
1366
+ const synthetic = new Map();
1367
+ for (const key of groups.keys()) {
1368
+ const group = groups.get(key);
1369
+ const summary = summarizeGroup(group, readSides);
1370
+ markUnusedChanges(summary.changes, usedNames, summary.pkg, {
1371
+ root: extra.root,
1372
+ dir: group.dir,
1373
+ });
1374
+ markAuditChanges(summary.changes, audit);
1375
+ markOutdatedChanges(summary.changes, outdated);
1376
+ const taken = takeManifestItems(summary);
1377
+ if (summary.foldLock) {
1378
+ for (const item of summary.lockItems) taken.push(item);
1379
+ }
1380
+ if (!taken.length && !summary.changes.length) continue;
1381
+ const made = makeDepItems(group.origin, group.items, taken, summary);
1382
+ synthetic.set(key, made);
1383
+ for (const item of taken) folded.add(item);
1384
+ }
1385
+ if (!synthetic.size) return items;
1386
+ const out = [];
1387
+ const placed = new Set();
1388
+ for (const item of items) {
1389
+ const rel = itemPath(item);
1390
+ const meta = depFileMeta(rel);
1391
+ if (meta) {
1392
+ const key = `${item.origin}\0${meta.dir}`;
1393
+ if (synthetic.has(key) && !placed.has(key)) {
1394
+ placed.add(key);
1395
+ const made = synthetic.get(key);
1396
+ for (const depItem of made) out.push(depItem);
1397
+ }
1398
+ }
1399
+ if (!folded.has(item)) out.push(item);
1400
+ }
1401
+ return out;
1402
+ };
1403
+
1404
+ const mergeProposedItems = (items, pkgText, outdated, audit, extra = {}) => {
1405
+ const proposed = proposeDepItems(pkgText, outdated, audit, extra);
1406
+ if (!proposed.length) return items;
1407
+ const taken = new Set();
1408
+ for (const item of items) {
1409
+ const change = item.dep && item.dep.change;
1410
+ if (change) taken.add(change.name);
1411
+ }
1412
+ const added = [];
1413
+ for (const item of proposed) {
1414
+ const change = item.dep && item.dep.change;
1415
+ if (!change || taken.has(change.name)) continue;
1416
+ added.push(item);
1417
+ }
1418
+ if (!added.length) return items;
1419
+ return [...items, ...added];
1420
+ };
1421
+
1422
+ module.exports = {
1423
+ MANIFEST,
1424
+ LOCKFILE,
1425
+ depFileMeta,
1426
+ readSections,
1427
+ lockEntries,
1428
+ lockPackageCount,
1429
+ diffSections,
1430
+ mergeDepFile,
1431
+ mergeResolved,
1432
+ collectUsedNames,
1433
+ parseAuditReport,
1434
+ parseOutdatedReport,
1435
+ applyWantedRange,
1436
+ patchedFromRange,
1437
+ proposeDepItems,
1438
+ foldDepItems,
1439
+ mergeProposedItems,
1440
+ };