plexus-peers 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/bin/plexus.js +134 -0
- package/lib/ansi.js +19 -0
- package/lib/engine.js +31 -0
- package/lib/fix.js +109 -0
- package/lib/format.js +17 -0
- package/lib/graph.js +100 -0
- package/lib/html-escape.js +22 -0
- package/lib/npm.js +85 -0
- package/lib/render-html.js +541 -0
- package/lib/run-analysis.js +181 -0
- package/lib/server.js +77 -0
- package/lib/terminal.js +91 -0
- package/package.json +31 -0
- package/public/index.html +217 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatBytes, sizeClass } = require('./format');
|
|
4
|
+
const { escapeHtml: esc, npmPackageUrl, pkgSlugForDom: slug } = require('./html-escape');
|
|
5
|
+
|
|
6
|
+
function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes = {}, meta = {}) {
|
|
7
|
+
const sourceNote =
|
|
8
|
+
meta.source === 'registry'
|
|
9
|
+
? ' — versions resolved from npm registry (no local node_modules)'
|
|
10
|
+
: '';
|
|
11
|
+
const directSet = new Set(Object.keys(directDeps));
|
|
12
|
+
|
|
13
|
+
const entries = Object.entries(graph).sort(([a], [b]) => a.localeCompare(b));
|
|
14
|
+
|
|
15
|
+
const conflictCount = entries.reduce((acc, [, info]) => {
|
|
16
|
+
return acc + info.peerDeps.filter(p => !p.ok && p.installed).length;
|
|
17
|
+
}, 0);
|
|
18
|
+
const missingOptionalCount = entries.reduce((acc, [, info]) => {
|
|
19
|
+
return acc + info.peerDeps.filter(p => !p.ok && !p.installed).length;
|
|
20
|
+
}, 0);
|
|
21
|
+
const okCount = entries.reduce((acc, [, info]) => {
|
|
22
|
+
return acc + info.peerDeps.filter(p => p.ok).length;
|
|
23
|
+
}, 0);
|
|
24
|
+
|
|
25
|
+
// Size stats
|
|
26
|
+
const totalDiskSize = entries.reduce((acc, [, info]) => acc + (info.diskSize ?? 0), 0);
|
|
27
|
+
|
|
28
|
+
// Abbreviate long semver ranges for compact display
|
|
29
|
+
function shortRange(range) {
|
|
30
|
+
// "^10.0.8 || ^11.0 || ^12.0 || ^13.0" → "^10 | ^11 | ^12 | ^13"
|
|
31
|
+
return range.replace(/(\d+)\.\d+(\.\d+)?/g, '$1').replace(/\s*\|\|\s*/g, ' | ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cardUid = (() => {
|
|
35
|
+
let n = 0;
|
|
36
|
+
return () => ++n;
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
const cards = entries
|
|
40
|
+
.map(([pkgName, info]) => {
|
|
41
|
+
const isDirect = directSet.has(pkgName);
|
|
42
|
+
const conflictPeers = info.peerDeps.filter(p => !p.ok && p.installed);
|
|
43
|
+
const optionalPeers = info.peerDeps.filter(p => !p.ok && !p.installed);
|
|
44
|
+
const okPeers = info.peerDeps.filter(p => p.ok);
|
|
45
|
+
|
|
46
|
+
const statusClass = info.missing
|
|
47
|
+
? 'card-missing'
|
|
48
|
+
: conflictPeers.length > 0
|
|
49
|
+
? 'card-conflict'
|
|
50
|
+
: optionalPeers.length > 0 && okPeers.length === 0
|
|
51
|
+
? 'card-warning'
|
|
52
|
+
: 'card-ok';
|
|
53
|
+
|
|
54
|
+
const uid = cardUid();
|
|
55
|
+
|
|
56
|
+
// Conflict rows — most important, always visible
|
|
57
|
+
const conflictRows = conflictPeers
|
|
58
|
+
.map(
|
|
59
|
+
p => `
|
|
60
|
+
<div class="peer-row peer-conflict">
|
|
61
|
+
<span class="peer-icon">✗</span>
|
|
62
|
+
<a href="${esc(npmPackageUrl(p.name))}" target="_blank" class="peer-name">${esc(p.name)}</a>
|
|
63
|
+
<span class="peer-detail">have <strong>v${esc(p.installed)}</strong> · needs <span class="needs-range" title="${esc(p.range)}">${esc(shortRange(p.range))}</span></span>
|
|
64
|
+
</div>`,
|
|
65
|
+
)
|
|
66
|
+
.join('');
|
|
67
|
+
|
|
68
|
+
// OK rows — compact, secondary
|
|
69
|
+
const okRows = okPeers
|
|
70
|
+
.map(
|
|
71
|
+
p => `
|
|
72
|
+
<div class="peer-row peer-ok">
|
|
73
|
+
<span class="peer-icon">✓</span>
|
|
74
|
+
<a href="${esc(npmPackageUrl(p.name))}" target="_blank" class="peer-name">${esc(p.name)}</a>
|
|
75
|
+
<span class="peer-version-ok">v${esc(p.installed)}</span>
|
|
76
|
+
</div>`,
|
|
77
|
+
)
|
|
78
|
+
.join('');
|
|
79
|
+
|
|
80
|
+
// Optional peers — collapsed by default
|
|
81
|
+
const optionalRows = optionalPeers
|
|
82
|
+
.map(
|
|
83
|
+
p => `
|
|
84
|
+
<div class="peer-row peer-missing">
|
|
85
|
+
<span class="peer-icon">·</span>
|
|
86
|
+
<a href="${esc(npmPackageUrl(p.name))}" target="_blank" class="peer-name">${esc(p.name)}</a>
|
|
87
|
+
<span class="peer-optional-label">optional · not installed</span>
|
|
88
|
+
</div>`,
|
|
89
|
+
)
|
|
90
|
+
.join('');
|
|
91
|
+
|
|
92
|
+
const optionalToggle =
|
|
93
|
+
optionalPeers.length > 0
|
|
94
|
+
? `
|
|
95
|
+
<div class="optional-toggle" onclick="toggleOptional(${uid})">
|
|
96
|
+
<span id="opt-arrow-${uid}">▸</span> ${optionalPeers.length} optional peer${optionalPeers.length > 1 ? 's' : ''}
|
|
97
|
+
</div>
|
|
98
|
+
<div id="opt-rows-${uid}" class="optional-rows hidden">${optionalRows}</div>`
|
|
99
|
+
: '';
|
|
100
|
+
|
|
101
|
+
const noPeers =
|
|
102
|
+
info.peerDeps.length === 0 ? '<div class="no-peers">no peer dependencies</div>' : '';
|
|
103
|
+
|
|
104
|
+
// Header meta
|
|
105
|
+
const sizeStr = formatBytes(info.diskSize);
|
|
106
|
+
const sizeCls = sizeClass(info.diskSize);
|
|
107
|
+
const bundleInfo = bundleSizes[pkgName];
|
|
108
|
+
|
|
109
|
+
const requiredByHtml =
|
|
110
|
+
info.requiredBy.length > 0
|
|
111
|
+
? `<div class="required-by">peer of: ${info.requiredBy.map(r => `<a href="#pkg-${slug(r)}">${esc(r)}</a>`).join(', ')}</div>`
|
|
112
|
+
: '';
|
|
113
|
+
|
|
114
|
+
return `<div class="card ${statusClass}" id="pkg-${slug(pkgName)}" data-conflicts="${conflictPeers.length}" data-pkg="${esc(pkgName)}" data-size="${info.diskSize ?? 0}">
|
|
115
|
+
<div class="card-header">
|
|
116
|
+
<a href="${esc(npmPackageUrl(pkgName))}" target="_blank" class="pkg-name">${esc(pkgName)}</a>
|
|
117
|
+
<span class="pkg-meta">
|
|
118
|
+
<span class="pkg-version">v${info.missing ? '?' : esc(info.version)}</span>
|
|
119
|
+
${sizeStr ? `<span class="size-badge ${sizeCls}">${esc(sizeStr)}</span>` : ''}
|
|
120
|
+
${bundleInfo ? `<span class="size-badge size-bundle" title="Bundle size (gzip)">gzip ${esc(formatBytes(bundleInfo.gzip))}</span>` : ''}
|
|
121
|
+
</span>
|
|
122
|
+
<div class="badges">
|
|
123
|
+
${isDirect ? '<span class="badge badge-direct">direct</span>' : ''}
|
|
124
|
+
${conflictPeers.length > 0 ? `<span class="badge badge-conflict">⚠ ${conflictPeers.length}</span>` : ''}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
${requiredByHtml}
|
|
128
|
+
${conflictRows}${okRows}${optionalToggle}${noPeers}
|
|
129
|
+
</div>`;
|
|
130
|
+
})
|
|
131
|
+
.join('\n');
|
|
132
|
+
|
|
133
|
+
const conflictSummaryRows = entries
|
|
134
|
+
.flatMap(([pkgName, info]) =>
|
|
135
|
+
info.peerDeps
|
|
136
|
+
.filter(p => !p.ok && p.installed)
|
|
137
|
+
.map(
|
|
138
|
+
p => `<tr>
|
|
139
|
+
<td><a href="#pkg-${slug(pkgName)}" class="pkg-link">${esc(pkgName)}</a></td>
|
|
140
|
+
<td><a href="${esc(npmPackageUrl(p.name))}" target="_blank" class="pkg-link">${esc(p.name)}</a></td>
|
|
141
|
+
<td><code>${esc(p.range)}</code></td>
|
|
142
|
+
<td><span class="version-mismatch">v${esc(p.installed)}</span></td>
|
|
143
|
+
</tr>`,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
.join('\n');
|
|
147
|
+
|
|
148
|
+
return `<!DOCTYPE html>
|
|
149
|
+
<html lang="en">
|
|
150
|
+
<head>
|
|
151
|
+
<meta charset="UTF-8">
|
|
152
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
153
|
+
<title>Plexus — ${esc(rootPkg.name ?? 'package')}</title>
|
|
154
|
+
<style>
|
|
155
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
156
|
+
:root {
|
|
157
|
+
--bg: #0a0a0a;
|
|
158
|
+
--surface: #111111;
|
|
159
|
+
--surface2: #171717;
|
|
160
|
+
--code: #1c1c1c;
|
|
161
|
+
--border: #27272a;
|
|
162
|
+
--border-hover: #3f3f46;
|
|
163
|
+
--text: #d4d4d8;
|
|
164
|
+
--text-bright: #fafafa;
|
|
165
|
+
--muted: #71717a;
|
|
166
|
+
--muted2: #52525b;
|
|
167
|
+
--green: #86efac;
|
|
168
|
+
--green-bg: #14532d;
|
|
169
|
+
--red: #f87171;
|
|
170
|
+
--red-bg: #1c1010;
|
|
171
|
+
--red-border: #7f1d1d;
|
|
172
|
+
--yellow: #fbbf24;
|
|
173
|
+
--yellow-bg: #1c1810;
|
|
174
|
+
--yellow-border: #78350f;
|
|
175
|
+
--accent: #e4e4e7;
|
|
176
|
+
--link: #d4d4d8;
|
|
177
|
+
--link-hover: #ffffff;
|
|
178
|
+
}
|
|
179
|
+
body { background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; line-height: 1.6; -webkit-font-smoothing: antialiased; }
|
|
180
|
+
.shell { max-width: 1120px; margin: 0 auto; padding: 28px 20px 56px; }
|
|
181
|
+
a { color: var(--link); text-decoration: none; }
|
|
182
|
+
a:hover { color: var(--link-hover); text-decoration: underline; }
|
|
183
|
+
code { font-family: ui-monospace, 'Cascadia Code', monospace; font-size: 12px; background: var(--code); padding: 2px 6px; border-radius: 4px; color: var(--muted); }
|
|
184
|
+
|
|
185
|
+
.hero { text-align: center; padding: 20px 0 32px; border-bottom: 1px solid var(--border); }
|
|
186
|
+
.hero h1 { font-size: clamp(2rem, 5vw, 3rem); font-weight: 600; letter-spacing: -0.03em; color: var(--text-bright); margin-bottom: 12px; }
|
|
187
|
+
.hero-meta { font-size: 1rem; color: var(--muted); }
|
|
188
|
+
.hero-meta strong { color: var(--text); font-weight: 600; }
|
|
189
|
+
.hero-ver { font-family: ui-monospace, monospace; color: var(--muted2); }
|
|
190
|
+
.hero-sub { font-size: 0.875rem; color: var(--muted2); margin-top: 10px; max-width: 40rem; margin-left: auto; margin-right: auto; line-height: 1.55; }
|
|
191
|
+
.generated { font-size: 12px; color: var(--muted2); margin-top: 18px; }
|
|
192
|
+
|
|
193
|
+
.stats { display: flex; gap: 12px; padding: 24px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; justify-content: center; }
|
|
194
|
+
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 20px; text-align: center; min-width: 104px; box-shadow: 0 0 0 1px rgba(255,255,255,0.02); }
|
|
195
|
+
.stat-num { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
|
196
|
+
.stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 6px; }
|
|
197
|
+
.num-conflict { color: var(--red); }
|
|
198
|
+
.num-warning { color: var(--yellow); }
|
|
199
|
+
.num-ok { color: var(--green); }
|
|
200
|
+
.num-neutral { color: var(--text-bright); }
|
|
201
|
+
|
|
202
|
+
.toolbar { padding: 20px 0; border-bottom: 1px solid var(--border); display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
203
|
+
.toolbar input { background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 8px 14px; border-radius: 8px; font-size: 13px; width: min(260px, 100%); }
|
|
204
|
+
.toolbar input:focus { outline: none; border-color: var(--border-hover); }
|
|
205
|
+
.toolbar input::placeholder { color: var(--muted2); }
|
|
206
|
+
.filter-btn { background: var(--surface); border: 1px solid var(--border); color: var(--muted); padding: 8px 14px; border-radius: 8px; font-size: 13px; cursor: pointer; transition: border-color .15s, color .15s, background .15s; }
|
|
207
|
+
.filter-btn:hover, .filter-btn.active { border-color: var(--border-hover); color: var(--text-bright); }
|
|
208
|
+
.filter-btn.active { background: var(--surface2); }
|
|
209
|
+
|
|
210
|
+
.grid-wrap { padding: 20px 0 36px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
|
|
211
|
+
.grid-heading { font-size: 0.9375rem; font-weight: 600; color: var(--text-bright); margin-bottom: 14px; letter-spacing: -0.02em; }
|
|
212
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
|
|
213
|
+
|
|
214
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; transition: border-color .15s; box-shadow: 0 0 0 1px rgba(255,255,255,0.02); }
|
|
215
|
+
.card:hover { border-color: var(--border-hover); }
|
|
216
|
+
.card-conflict { border-left: 3px solid var(--red); background: var(--red-bg); border-color: var(--red-border); }
|
|
217
|
+
.card-warning { border-left: 3px solid var(--yellow); background: var(--yellow-bg); border-color: var(--yellow-border); }
|
|
218
|
+
.card-ok { border-left: 3px solid var(--border-hover); }
|
|
219
|
+
.card-missing { border-left: 3px solid var(--red); opacity: 0.55; }
|
|
220
|
+
|
|
221
|
+
.card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
|
|
222
|
+
.pkg-name { font-weight: 600; font-size: 13px; color: var(--text-bright); flex-shrink: 0; }
|
|
223
|
+
.pkg-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
224
|
+
.pkg-version { color: var(--muted); font-size: 11px; font-family: ui-monospace, monospace; }
|
|
225
|
+
.badges { display: flex; gap: 6px; margin-left: auto; flex-shrink: 0; }
|
|
226
|
+
.badge { font-size: 10px; padding: 2px 7px; border-radius: 6px; font-weight: 600; letter-spacing: 0.02em; }
|
|
227
|
+
.badge-direct { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
|
|
228
|
+
.badge-conflict { background: #450a0a; color: #fca5a5; border: 1px solid var(--red-border); }
|
|
229
|
+
|
|
230
|
+
.required-by { font-size: 11px; color: var(--muted); margin: 4px 0 6px; }
|
|
231
|
+
.required-by a { color: var(--accent); }
|
|
232
|
+
.no-peers { font-size: 11px; color: var(--muted); font-style: italic; margin-top: 6px; }
|
|
233
|
+
|
|
234
|
+
.peer-row { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
|
|
235
|
+
.peer-row:last-of-type { border-bottom: none; }
|
|
236
|
+
.peer-icon { width: 14px; text-align: center; flex-shrink: 0; font-size: 11px; }
|
|
237
|
+
.peer-name { min-width: 0; flex: 0 0 auto; color: var(--text); }
|
|
238
|
+
.peer-name:hover { color: var(--text-bright); }
|
|
239
|
+
|
|
240
|
+
.peer-ok .peer-icon { color: var(--green); }
|
|
241
|
+
.peer-version-ok { margin-left: auto; color: var(--muted); font-family: ui-monospace, monospace; font-size: 11px; }
|
|
242
|
+
|
|
243
|
+
.peer-conflict .peer-icon { color: var(--red); }
|
|
244
|
+
.peer-detail { margin-left: auto; font-size: 11px; color: var(--red); white-space: nowrap; }
|
|
245
|
+
.peer-detail strong { color: #fca5a5; }
|
|
246
|
+
.needs-range { color: var(--muted); font-family: ui-monospace, monospace; cursor: help; border-bottom: 1px dashed var(--muted2); }
|
|
247
|
+
|
|
248
|
+
.peer-missing .peer-icon { color: var(--border-hover); }
|
|
249
|
+
.peer-optional-label { margin-left: auto; font-size: 10px; color: var(--muted); font-style: italic; }
|
|
250
|
+
|
|
251
|
+
.optional-toggle { font-size: 11px; color: var(--muted); cursor: pointer; margin-top: 6px; user-select: none; padding: 2px 0; }
|
|
252
|
+
.optional-toggle:hover { color: var(--text-bright); }
|
|
253
|
+
.optional-rows { margin-top: 4px; }
|
|
254
|
+
|
|
255
|
+
.conflict-table-section { margin: 0 0 36px; }
|
|
256
|
+
.conflict-table-section h2 { font-size: 1rem; font-weight: 600; margin-bottom: 14px; color: var(--text-bright); }
|
|
257
|
+
.conflict-table-section h2.h2-danger { color: var(--red); }
|
|
258
|
+
.conflict-table-section h2.h2-fix { color: var(--text-bright); }
|
|
259
|
+
table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: 12px; overflow: hidden; border: 1px solid var(--border); }
|
|
260
|
+
th { background: var(--surface2); padding: 12px 14px; text-align: left; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid var(--border); }
|
|
261
|
+
td { padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 13px; vertical-align: top; }
|
|
262
|
+
tr:last-child td { border-bottom: none; }
|
|
263
|
+
tr:hover td { background: var(--surface2); }
|
|
264
|
+
.pkg-link { font-weight: 500; }
|
|
265
|
+
.badge-fix { background: var(--green-bg); color: var(--green); border: 1px solid #166534; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; margin-right: 4px; display: inline-block; margin-top: 2px; }
|
|
266
|
+
.no-cascade { color: var(--green); font-size: 12px; }
|
|
267
|
+
.cascade-list { margin: 0; padding: 0 0 0 18px; font-size: 12px; color: var(--yellow); }
|
|
268
|
+
.cascade-list li { margin-bottom: 4px; }
|
|
269
|
+
.json-block-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
270
|
+
.json-block-header .h2-fix { margin-bottom: 0; }
|
|
271
|
+
.copy-json-btn {
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
background: var(--surface2);
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
color: var(--text-bright);
|
|
276
|
+
padding: 8px 14px;
|
|
277
|
+
border-radius: 8px;
|
|
278
|
+
font-size: 12px;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
font-family: system-ui, sans-serif;
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
transition: border-color .15s, background .15s, color .15s;
|
|
283
|
+
}
|
|
284
|
+
.copy-json-btn:hover { border-color: var(--border-hover); background: var(--border); }
|
|
285
|
+
.copy-json-btn.copied { color: var(--green); border-color: rgba(134,239,172,0.35); }
|
|
286
|
+
.json-block-wrap { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--code); }
|
|
287
|
+
.json-block { margin: 0; background: transparent; border: none; border-radius: 0; padding: 20px 22px; font-size: 12px; font-family: ui-monospace, monospace; overflow-x: auto; white-space: pre; color: var(--text); line-height: 1.65; }
|
|
288
|
+
.json-block mark { background: rgba(255,255,255,0.12); color: var(--text-bright); border-radius: 3px; padding: 0 2px; }
|
|
289
|
+
.version-ok { color: var(--green); }
|
|
290
|
+
.size-badge { font-size: 10px; padding: 2px 7px; border-radius: 6px; font-family: ui-monospace, monospace; }
|
|
291
|
+
.size-bundle { background: rgba(63,63,70,0.45); color: var(--muted); border: 1px solid var(--border); }
|
|
292
|
+
.size-large { background: rgba(127,29,29,0.35); color: #fca5a5; }
|
|
293
|
+
.size-medium { background: rgba(120,53,15,0.35); color: var(--yellow); }
|
|
294
|
+
.size-small { background: rgba(20,83,45,0.35); color: var(--green); }
|
|
295
|
+
.size-rank-table td:nth-child(3) { font-family: ui-monospace, monospace; }
|
|
296
|
+
.gzip-col { color: var(--muted); font-size: 12px; }
|
|
297
|
+
|
|
298
|
+
.hidden { display: none !important; }
|
|
299
|
+
</style>
|
|
300
|
+
</head>
|
|
301
|
+
<body>
|
|
302
|
+
|
|
303
|
+
<div class="shell">
|
|
304
|
+
<header class="hero">
|
|
305
|
+
<h1>Plexus</h1>
|
|
306
|
+
<p class="hero-meta"><strong>${esc(rootPkg.name ?? '(unnamed)')}</strong> <span class="hero-ver">v${esc(rootPkg.version ?? '?')}</span></p>
|
|
307
|
+
<p class="hero-sub">Peer dependency analysis${sourceNote}</p>
|
|
308
|
+
<p class="generated">Generated ${esc(new Date().toLocaleString())}</p>
|
|
309
|
+
</header>
|
|
310
|
+
|
|
311
|
+
<div class="stats">
|
|
312
|
+
<div class="stat"><div class="stat-num num-neutral">${Object.keys(directDeps).length}</div><div class="stat-label">Direct deps</div></div>
|
|
313
|
+
<div class="stat"><div class="stat-num num-neutral">${entries.length}</div><div class="stat-label">Total in graph</div></div>
|
|
314
|
+
<div class="stat"><div class="stat-num num-ok">${okCount}</div><div class="stat-label">Satisfied peers</div></div>
|
|
315
|
+
<div class="stat"><div class="stat-num num-conflict">${conflictCount}</div><div class="stat-label">Conflicts</div></div>
|
|
316
|
+
<div class="stat"><div class="stat-num num-warning">${missingOptionalCount}</div><div class="stat-label">Missing optional</div></div>
|
|
317
|
+
<div class="stat"><div class="stat-num num-neutral" style="font-size:17px">${esc(formatBytes(totalDiskSize))}</div><div class="stat-label">Total disk (node_modules)</div></div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div class="toolbar">
|
|
321
|
+
<input type="text" id="search" placeholder="Search packages…">
|
|
322
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
323
|
+
<button class="filter-btn" data-filter="conflicts">Conflicts only</button>
|
|
324
|
+
<button class="filter-btn" data-filter="direct">Direct only</button>
|
|
325
|
+
<button class="filter-btn" data-filter="ok">No issues</button>
|
|
326
|
+
<button class="filter-btn" id="sort-size-btn" style="margin-left:auto">Sort by size ↕</button>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div class="grid-wrap">
|
|
330
|
+
<h2 class="grid-heading">Packages</h2>
|
|
331
|
+
<div class="grid" id="grid">
|
|
332
|
+
${cards}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
${
|
|
337
|
+
conflictCount > 0
|
|
338
|
+
? `
|
|
339
|
+
<div class="conflict-table-section">
|
|
340
|
+
<h2 class="h2-danger">✗ ${conflictCount} Version Conflict${conflictCount > 1 ? 's' : ''}</h2>
|
|
341
|
+
<table>
|
|
342
|
+
<thead><tr><th>Package</th><th>Requires peer</th><th>Required range</th><th>Installed</th></tr></thead>
|
|
343
|
+
<tbody>${conflictSummaryRows}</tbody>
|
|
344
|
+
</table>
|
|
345
|
+
</div>`
|
|
346
|
+
: ''
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
${
|
|
350
|
+
Object.keys(resolutions).length > 0
|
|
351
|
+
? (() => {
|
|
352
|
+
const rows = Object.entries(resolutions)
|
|
353
|
+
.map(([pkg, r]) => {
|
|
354
|
+
const cascadeHtml =
|
|
355
|
+
r.cascades.length > 0
|
|
356
|
+
? `<ul class="cascade-list">${r.cascades.map(c => `<li>⚠ <b>${esc(c.peerName)}</b>: needs <code>${esc(c.peerRange)}</code>, have v${esc(c.installedVer)}</li>`).join('')}</ul>`
|
|
357
|
+
: '<span class="no-cascade">No new cascades</span>';
|
|
358
|
+
const fixesHtml =
|
|
359
|
+
r.fixes.length > 0
|
|
360
|
+
? r.fixes.map(f => `<span class="badge badge-fix">${esc(f)}</span>`).join(' ')
|
|
361
|
+
: '<span style="color:var(--muted)">none</span>';
|
|
362
|
+
const stillHtml =
|
|
363
|
+
r.stillConflicts.length > 0
|
|
364
|
+
? r.stillConflicts
|
|
365
|
+
.map(f => `<span class="badge badge-conflict">${esc(f)}</span>`)
|
|
366
|
+
.join(' ')
|
|
367
|
+
: '';
|
|
368
|
+
return `<tr>
|
|
369
|
+
<td><a href="${esc(npmPackageUrl(pkg))}" target="_blank" class="pkg-link">${esc(pkg)}</a></td>
|
|
370
|
+
<td><code>${esc(r.current)}</code> → <code class="version-ok">^${esc(r.latest)}</code></td>
|
|
371
|
+
<td>${fixesHtml}${stillHtml}</td>
|
|
372
|
+
<td>${cascadeHtml}</td>
|
|
373
|
+
</tr>`;
|
|
374
|
+
})
|
|
375
|
+
.join('\n');
|
|
376
|
+
|
|
377
|
+
return `<div class="conflict-table-section">
|
|
378
|
+
<h2 class="h2-fix">Resolution plan (--fix)</h2>
|
|
379
|
+
<table>
|
|
380
|
+
<thead><tr><th>Package</th><th>Upgrade</th><th>Fixes / Still broken</th><th>Cascade risks</th></tr></thead>
|
|
381
|
+
<tbody>${rows}</tbody>
|
|
382
|
+
</table>
|
|
383
|
+
</div>`;
|
|
384
|
+
})()
|
|
385
|
+
: ''
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
${
|
|
389
|
+
Object.keys(resolutions).length > 0
|
|
390
|
+
? (() => {
|
|
391
|
+
const suggested = JSON.parse(JSON.stringify(rootPkg));
|
|
392
|
+
for (const [pkg, r] of Object.entries(resolutions)) {
|
|
393
|
+
if (suggested.dependencies?.[pkg]) suggested.dependencies[pkg] = `^${r.latest}`;
|
|
394
|
+
if (suggested.devDependencies?.[pkg]) suggested.devDependencies[pkg] = `^${r.latest}`;
|
|
395
|
+
}
|
|
396
|
+
const changed = Object.keys(resolutions);
|
|
397
|
+
const markRe = new RegExp(
|
|
398
|
+
`"(${changed.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})"`,
|
|
399
|
+
'g',
|
|
400
|
+
);
|
|
401
|
+
const markHolders = [];
|
|
402
|
+
let depsJson = JSON.stringify(
|
|
403
|
+
{ dependencies: suggested.dependencies, devDependencies: suggested.devDependencies },
|
|
404
|
+
null,
|
|
405
|
+
2,
|
|
406
|
+
);
|
|
407
|
+
depsJson = depsJson.replace(markRe, (_, pkgName) => {
|
|
408
|
+
const h = `<mark>"${esc(pkgName)}"</mark>`;
|
|
409
|
+
markHolders.push(h);
|
|
410
|
+
return `__PLEXUS_M${markHolders.length - 1}__`;
|
|
411
|
+
});
|
|
412
|
+
depsJson = esc(depsJson);
|
|
413
|
+
markHolders.forEach((h, i) => {
|
|
414
|
+
depsJson = depsJson.replace(`__PLEXUS_M${i}__`, h);
|
|
415
|
+
});
|
|
416
|
+
return `<div class="conflict-table-section">
|
|
417
|
+
<div class="json-block-header">
|
|
418
|
+
<h2 class="h2-fix">Suggested package.json (highlighted = changed)</h2>
|
|
419
|
+
<button type="button" class="copy-json-btn" id="copy-suggested-json" aria-label="Copy JSON to clipboard">Copy JSON</button>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="json-block-wrap">
|
|
422
|
+
<pre class="json-block" id="suggested-json-pre">${depsJson}</pre>
|
|
423
|
+
</div>
|
|
424
|
+
</div>`;
|
|
425
|
+
})()
|
|
426
|
+
: ''
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<script>
|
|
432
|
+
function toggleOptional(uid) {
|
|
433
|
+
var rows = document.getElementById('opt-rows-' + uid);
|
|
434
|
+
var arrow = document.getElementById('opt-arrow-' + uid);
|
|
435
|
+
if (!rows) return;
|
|
436
|
+
rows.classList.toggle('hidden');
|
|
437
|
+
arrow.textContent = rows.classList.contains('hidden') ? '▸' : '▾';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
441
|
+
var currentFilter = 'all';
|
|
442
|
+
var sortedBySize = false;
|
|
443
|
+
var searchInput = document.getElementById('search');
|
|
444
|
+
var grid = document.getElementById('grid');
|
|
445
|
+
|
|
446
|
+
searchInput.addEventListener('input', filterCards);
|
|
447
|
+
|
|
448
|
+
document.querySelectorAll('.filter-btn[data-filter]').forEach(function(btn) {
|
|
449
|
+
btn.addEventListener('click', function() {
|
|
450
|
+
currentFilter = btn.getAttribute('data-filter');
|
|
451
|
+
document.querySelectorAll('.filter-btn[data-filter]').forEach(function(b) { b.classList.remove('active'); });
|
|
452
|
+
btn.classList.add('active');
|
|
453
|
+
filterCards();
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
var sortBtn = document.getElementById('sort-size-btn');
|
|
458
|
+
if (sortBtn) {
|
|
459
|
+
sortBtn.addEventListener('click', function() {
|
|
460
|
+
sortedBySize = !sortedBySize;
|
|
461
|
+
sortBtn.classList.toggle('active', sortedBySize);
|
|
462
|
+
sortBtn.textContent = sortedBySize ? 'Sort by name ↕' : 'Sort by size ↕';
|
|
463
|
+
var cards = Array.from(grid.querySelectorAll('.card'));
|
|
464
|
+
cards.sort(function(a, b) {
|
|
465
|
+
if (sortedBySize) {
|
|
466
|
+
return parseInt(b.getAttribute('data-size') || 0) - parseInt(a.getAttribute('data-size') || 0);
|
|
467
|
+
}
|
|
468
|
+
return (a.getAttribute('data-pkg') || '').localeCompare(b.getAttribute('data-pkg') || '');
|
|
469
|
+
});
|
|
470
|
+
cards.forEach(function(card) { grid.appendChild(card); });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function filterCards() {
|
|
475
|
+
var search = searchInput.value.toLowerCase();
|
|
476
|
+
document.querySelectorAll('.card').forEach(function(card) {
|
|
477
|
+
var pkg = (card.getAttribute('data-pkg') || '').toLowerCase();
|
|
478
|
+
var hasConflict = card.classList.contains('card-conflict');
|
|
479
|
+
var hasWarning = card.classList.contains('card-warning');
|
|
480
|
+
var hasDirect = card.querySelector('.badge-direct');
|
|
481
|
+
|
|
482
|
+
var matchesSearch = !search || pkg.indexOf(search) !== -1;
|
|
483
|
+
var matchesFilter =
|
|
484
|
+
currentFilter === 'all' ||
|
|
485
|
+
(currentFilter === 'conflicts' && hasConflict) ||
|
|
486
|
+
(currentFilter === 'direct' && !!hasDirect) ||
|
|
487
|
+
(currentFilter === 'ok' && !hasConflict && !hasWarning);
|
|
488
|
+
|
|
489
|
+
card.classList.toggle('hidden', !matchesSearch || !matchesFilter);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
var copyJsonBtn = document.getElementById('copy-suggested-json');
|
|
494
|
+
var suggestedJsonPre = document.getElementById('suggested-json-pre');
|
|
495
|
+
if (copyJsonBtn && suggestedJsonPre) {
|
|
496
|
+
copyJsonBtn.addEventListener('click', function () {
|
|
497
|
+
var text = suggestedJsonPre.textContent || '';
|
|
498
|
+
var label = copyJsonBtn.textContent;
|
|
499
|
+
function done(ok) {
|
|
500
|
+
copyJsonBtn.textContent = ok ? 'Copied!' : 'Copy failed';
|
|
501
|
+
copyJsonBtn.classList.toggle('copied', ok);
|
|
502
|
+
setTimeout(function () {
|
|
503
|
+
copyJsonBtn.textContent = label;
|
|
504
|
+
copyJsonBtn.classList.remove('copied');
|
|
505
|
+
}, 2000);
|
|
506
|
+
}
|
|
507
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
508
|
+
navigator.clipboard.writeText(text).then(function () { done(true); }).catch(function () {
|
|
509
|
+
if (fallbackCopyJson(text)) done(true);
|
|
510
|
+
else done(false);
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
if (fallbackCopyJson(text)) done(true);
|
|
514
|
+
else done(false);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
function fallbackCopyJson(text) {
|
|
521
|
+
try {
|
|
522
|
+
var ta = document.createElement('textarea');
|
|
523
|
+
ta.value = text;
|
|
524
|
+
ta.setAttribute('readonly', '');
|
|
525
|
+
ta.style.position = 'fixed';
|
|
526
|
+
ta.style.left = '-9999px';
|
|
527
|
+
document.body.appendChild(ta);
|
|
528
|
+
ta.select();
|
|
529
|
+
var ok = document.execCommand('copy');
|
|
530
|
+
document.body.removeChild(ta);
|
|
531
|
+
return ok;
|
|
532
|
+
} catch (e) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
</script>
|
|
537
|
+
</body>
|
|
538
|
+
</html>`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = { renderHtml };
|