ng-upgrade-doctor 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/LICENSE +21 -0
- package/README.md +91 -0
- package/bin/ng-upgrade-doctor.js +5 -0
- package/package.json +44 -0
- package/src/analyzers/breaking-changes.js +118 -0
- package/src/analyzers/code-census.js +101 -0
- package/src/analyzers/dependency-compat.js +174 -0
- package/src/analyzers/project-snapshot.js +124 -0
- package/src/analyzers/verdict.js +118 -0
- package/src/analyzers/version-gap.js +38 -0
- package/src/ast/loader.js +38 -0
- package/src/ast/regex-scanner.js +47 -0
- package/src/ast/ts-scanner.js +154 -0
- package/src/cli.js +73 -0
- package/src/data/angular-versions.js +83 -0
- package/src/data/breaking-changes.js +223 -0
- package/src/data/known-packages.js +92 -0
- package/src/index.js +108 -0
- package/src/registry.js +95 -0
- package/src/report/html.js +539 -0
- package/src/report/json.js +60 -0
- package/src/report/terminal.js +109 -0
- package/src/utils/fs-helpers.js +67 -0
- package/src/utils/log.js +34 -0
- package/src/utils/semver.js +208 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function esc(s) {
|
|
4
|
+
return String(s == null ? '' : s)
|
|
5
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function num(n) {
|
|
10
|
+
return typeof n === 'number' ? n.toLocaleString('en-US') : esc(n);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CAT = {
|
|
14
|
+
ok: { label: 'Compatible', cls: 'ok' },
|
|
15
|
+
agnostic: { label: 'Agnostic', cls: 'ok' },
|
|
16
|
+
upgrade: { label: 'Needs upgrade', cls: 'warn' },
|
|
17
|
+
partial: { label: 'Partial', cls: 'warn' },
|
|
18
|
+
blocker: { label: 'Blocker', cls: 'bad' },
|
|
19
|
+
removed: { label: 'Removed', cls: 'info' },
|
|
20
|
+
abandoned: { label: 'Abandoned', cls: 'muted' },
|
|
21
|
+
unknown: { label: 'Unknown', cls: 'muted' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SEV = {
|
|
25
|
+
removed: { label: 'Removed', cls: 'bad' },
|
|
26
|
+
deprecated: { label: 'Deprecated', cls: 'warn' },
|
|
27
|
+
opportunity: { label: 'Opportunity', cls: 'info' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function renderHtml(report) {
|
|
31
|
+
const { snapshot, versionGap, deps, verdict, census, breakingChanges, meta } = report;
|
|
32
|
+
const s = deps.summary;
|
|
33
|
+
const scoreClass = verdict.score >= 80 ? 'good' : verdict.score >= 60 ? 'ok' : verdict.score >= 40 ? 'warn' : 'bad';
|
|
34
|
+
|
|
35
|
+
// --- Radial gauge geometry ---
|
|
36
|
+
const R = 54;
|
|
37
|
+
const CIRC = 2 * Math.PI * R;
|
|
38
|
+
const gaugeOffset = (CIRC * (1 - verdict.score / 100)).toFixed(2);
|
|
39
|
+
|
|
40
|
+
// --- Stat card: animate integers, render strings verbatim ---
|
|
41
|
+
const stat = (label, value, sub, accent) => {
|
|
42
|
+
const v = typeof value === 'number'
|
|
43
|
+
? `<div class="stat-v" data-count="${value}">0</div>`
|
|
44
|
+
: `<div class="stat-v">${esc(value)}</div>`;
|
|
45
|
+
return `<div class="stat${accent ? ' a-' + accent : ''}">${v}<div class="stat-l">${esc(label)}</div>${sub ? `<div class="stat-s">${esc(sub)}</div>` : ''}</div>`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// --- Plain-English verdict sentence ---
|
|
49
|
+
const hardBlockers = s.blocker + s.removed;
|
|
50
|
+
const verdictSentence = buildVerdictSentence(snapshot, versionGap, deps, verdict, breakingChanges, hardBlockers);
|
|
51
|
+
|
|
52
|
+
const inventoryStats = [
|
|
53
|
+
stat('Total dependencies', snapshot.totalDependencyCount, `${snapshot.prodDependencyCount} prod · ${snapshot.devDependencyCount} dev`, 'indigo'),
|
|
54
|
+
snapshot.transitiveDependencyCount != null ? stat('Transitive deps', snapshot.transitiveDependencyCount, 'from lockfile', 'indigo') : '',
|
|
55
|
+
stat('Angular packages', snapshot.angularPackageCount, snapshot.angularVersion || '', 'indigo'),
|
|
56
|
+
stat('3rd-party Angular libs', snapshot.thirdPartyAngularCount, '', 'indigo'),
|
|
57
|
+
census ? stat('Components', census.counts.components, `${census.counts.standaloneComponents} standalone`, 'teal') : '',
|
|
58
|
+
census ? stat('NgModules', census.counts.modules, `${census.counts.services} services`, 'teal') : '',
|
|
59
|
+
census ? stat('Source files', census.fileCounts.tsFiles, `${census.fileCounts.htmlTemplates} templates · ${census.fileCounts.specFiles} specs`, 'teal') : '',
|
|
60
|
+
census ? stat('Lines of code', census.counts.totalLoc, 'approx (src/**)', 'teal') : '',
|
|
61
|
+
].join('');
|
|
62
|
+
|
|
63
|
+
// --- Dependency distribution bar ---
|
|
64
|
+
const distSegments = [
|
|
65
|
+
{ key: 'ok', label: 'Compatible', count: s.ok + s.agnostic, cls: 'ok' },
|
|
66
|
+
{ key: 'warn', label: 'Upgrade / partial', count: s.upgrade + s.partial, cls: 'warn' },
|
|
67
|
+
{ key: 'blocker', label: 'Blockers', count: s.blocker, cls: 'bad' },
|
|
68
|
+
{ key: 'removed', label: 'Removed', count: s.removed, cls: 'info' },
|
|
69
|
+
{ key: 'abandoned', label: 'Abandoned', count: s.abandoned, cls: 'muted' },
|
|
70
|
+
{ key: 'unknown', label: 'Unknown', count: s.unknown, cls: 'faint' },
|
|
71
|
+
].filter((seg) => seg.count > 0);
|
|
72
|
+
const distTotal = distSegments.reduce((a, b) => a + b.count, 0) || 1;
|
|
73
|
+
const distBar = distSegments
|
|
74
|
+
.map((seg) => `<span class="seg ${seg.cls}" style="flex:${seg.count}" title="${esc(seg.label)}: ${seg.count}"></span>`)
|
|
75
|
+
.join('');
|
|
76
|
+
const distLegend = distSegments
|
|
77
|
+
.map((seg) => `<span class="lg"><i class="dot ${seg.cls}"></i>${esc(seg.label)} <b>${seg.count}</b> <span class="pct">${Math.round((seg.count / distTotal) * 100)}%</span></span>`)
|
|
78
|
+
.join('');
|
|
79
|
+
|
|
80
|
+
const compatStats = [
|
|
81
|
+
stat('Compatible', s.ok + s.agnostic, 'has a supporting release', 'green'),
|
|
82
|
+
stat('Needs work', s.upgrade + s.partial, 'behind or partial', 'amber'),
|
|
83
|
+
stat('Hard blockers', hardBlockers, 'no path to target', 'red'),
|
|
84
|
+
stat('Abandoned', s.abandoned, 'unmaintained', 'gray'),
|
|
85
|
+
].join('');
|
|
86
|
+
|
|
87
|
+
const rows = deps.packages.map((p) => {
|
|
88
|
+
const cat = CAT[p.category] || CAT.unknown;
|
|
89
|
+
return `<tr data-cat="${p.category}">
|
|
90
|
+
<td class="mono">${esc(p.name)}${p.dev ? '<span class="tag">dev</span>' : ''}</td>
|
|
91
|
+
<td><span class="pill ${cat.cls}">${esc(cat.label)}</span></td>
|
|
92
|
+
<td class="mono">${esc(p.declaredRange || '')}</td>
|
|
93
|
+
<td class="mono">${esc(p.latestVersion || '—')}</td>
|
|
94
|
+
<td class="center">${p.maxAngularSupported != null ? 'v' + p.maxAngularSupported : (p.angularCoupled ? '—' : 'n/a')}</td>
|
|
95
|
+
<td class="reason">${esc(p.reason || '')}${p.replacement ? `<div class="repl">→ ${esc(p.replacement)}</div>` : ''}</td>
|
|
96
|
+
</tr>`;
|
|
97
|
+
}).join('');
|
|
98
|
+
|
|
99
|
+
// --- Code breaking-changes section ---
|
|
100
|
+
let breakingSection = '';
|
|
101
|
+
if (breakingChanges && breakingChanges.summary.totalHits > 0) {
|
|
102
|
+
const b = breakingChanges.summary;
|
|
103
|
+
const bcStats = [
|
|
104
|
+
stat('Removed APIs in use', b.removed, 'hard breaks', 'red'),
|
|
105
|
+
stat('Deprecated APIs', b.deprecated, 'will break later', 'amber'),
|
|
106
|
+
stat('Modernization ops', b.opportunity, 'optional upgrades', 'blue'),
|
|
107
|
+
stat('Files affected', b.filesAffected, `of ${breakingChanges.filesScanned.ts + breakingChanges.filesScanned.html} scanned`, 'indigo'),
|
|
108
|
+
].join('');
|
|
109
|
+
const ruleRows = breakingChanges.rules.map((r) => {
|
|
110
|
+
const sev = SEV[r.severity] || SEV.opportunity;
|
|
111
|
+
const samples = r.samples.map((sp) => `<code>${esc(sp)}</code>`).join('');
|
|
112
|
+
const more = r.count > r.samples.length ? `<span class="muted"> +${r.count - r.samples.length} more</span>` : '';
|
|
113
|
+
return `<tr>
|
|
114
|
+
<td><span class="pill ${sev.cls}">${esc(sev.label)}</span></td>
|
|
115
|
+
<td><b>${esc(r.title)}</b><div class="reason">${esc(r.message)}</div><div class="repl">→ ${esc(r.fix)}</div></td>
|
|
116
|
+
<td class="center mono">${num(r.count)}</td>
|
|
117
|
+
<td class="center mono">${num(r.fileCount)}</td>
|
|
118
|
+
<td class="samples">${samples}${more}</td>
|
|
119
|
+
</tr>`;
|
|
120
|
+
}).join('');
|
|
121
|
+
breakingSection = `
|
|
122
|
+
<section id="code">
|
|
123
|
+
<h2><span class="h-ic">🧬</span>Code breaking changes</h2>
|
|
124
|
+
<div class="stats s4">${bcStats}</div>
|
|
125
|
+
<p class="legend">Scanned ${num(breakingChanges.filesScanned.ts)} TypeScript & ${num(breakingChanges.filesScanned.html)} template files using ${breakingChanges.astAvailable ? 'the project’s TypeScript compiler (AST)' : 'a regex fallback'}. Locations are <code>file:line:col</code>.</p>
|
|
126
|
+
<div class="tablewrap">
|
|
127
|
+
<table>
|
|
128
|
+
<thead><tr><th>Severity</th><th>Issue & recommended fix</th><th class="center">Hits</th><th class="center">Files</th><th>Sample locations</th></tr></thead>
|
|
129
|
+
<tbody>${ruleRows}</tbody>
|
|
130
|
+
</table>
|
|
131
|
+
</div>
|
|
132
|
+
</section>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const blockerCards = verdict.blockers.length
|
|
136
|
+
? verdict.blockers.map((b) => `<div class="card bad">
|
|
137
|
+
<div class="card-h"><span class="pill ${b.category === 'removed' ? 'info' : 'bad'}">${esc((CAT[b.category] || {}).label || b.category)}</span> <span class="mono">${esc(b.name)}</span></div>
|
|
138
|
+
<div class="card-b">${esc(b.reason)}</div>
|
|
139
|
+
${b.replacement ? `<div class="card-r">Recommended: <b>${esc(b.replacement)}</b></div>` : ''}
|
|
140
|
+
</div>`).join('')
|
|
141
|
+
: '<div class="empty">✓ No hard blockers detected — nice.</div>';
|
|
142
|
+
|
|
143
|
+
const pathHtml = versionGap.known && versionGap.hops.length
|
|
144
|
+
? `<div class="path">${[`<span class="hop cur">v${versionGap.currentMajor}<em>current</em></span>`]
|
|
145
|
+
.concat(versionGap.hops.map((h, i) => `<span class="arrow">→</span><span class="hop${i === versionGap.hops.length - 1 ? ' target' : ''}" title="${esc((h.notes || []).join(' · '))}">v${h.major}${i === versionGap.hops.length - 1 ? '<em>target</em>' : ''}</span>`))
|
|
146
|
+
.join('')}</div>
|
|
147
|
+
<div class="hop-notes">${versionGap.hops.map((h) => `<div class="hop-note"><div class="hop-badge">v${h.major}</div><div class="hop-req">Node ${esc(h.node)} · TS ${esc(h.ts)}</div><ul>${(h.notes || []).map((n) => `<li>${esc(n)}</li>`).join('')}</ul></div>`).join('')}</div>`
|
|
148
|
+
: '<p class="muted">Upgrade path unavailable (version not recognized).</p>';
|
|
149
|
+
|
|
150
|
+
const deductionsHtml = verdict.deductions.length
|
|
151
|
+
? `<ul class="deductions">${verdict.deductions.map((d) => `<li><span class="minus">−${d.points}</span><span class="dbar"><span style="width:${Math.min(100, d.points * 2.2)}%"></span></span><span class="dlabel">${esc(d.label)}</span></li>`).join('')}</ul>`
|
|
152
|
+
: '<p class="muted">No deductions — full marks.</p>';
|
|
153
|
+
|
|
154
|
+
const quickWins = verdict.quickWins.length
|
|
155
|
+
? `<ul class="wins">${verdict.quickWins.map((w) => `<li>${esc(w)}</li>`).join('')}</ul>`
|
|
156
|
+
: '';
|
|
157
|
+
|
|
158
|
+
return `<!doctype html>
|
|
159
|
+
<html lang="en">
|
|
160
|
+
<head>
|
|
161
|
+
<meta charset="utf-8">
|
|
162
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
163
|
+
<title>ng-upgrade-doctor · ${esc(snapshot.name)}</title>
|
|
164
|
+
<style>
|
|
165
|
+
${CSS}
|
|
166
|
+
</style>
|
|
167
|
+
</head>
|
|
168
|
+
<body>
|
|
169
|
+
<div class="wrap">
|
|
170
|
+
|
|
171
|
+
<header class="masthead">
|
|
172
|
+
<div class="brand"><span class="logo">🩺</span><div><div class="btitle">ng-upgrade-doctor</div><div class="bsub">Angular upgrade readiness & blast-radius report</div></div></div>
|
|
173
|
+
<div class="meta-right">
|
|
174
|
+
<div class="proj">${esc(snapshot.name)}</div>
|
|
175
|
+
<div class="muted small"><span title="The Angular version compatibility is assessed against. Defaults to the latest version known to the tool; override with --target <major>." style="cursor:help;border-bottom:1px dotted var(--faint)">Target Angular ${esc(deps.targetMajor)}</span>${deps.offline ? ' · offline mode' : ''}${meta.generatedAt ? ' · ' + esc(meta.generatedAt) : ''}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</header>
|
|
178
|
+
|
|
179
|
+
<section class="hero ${scoreClass}">
|
|
180
|
+
<div class="gauge" role="img" aria-label="Upgrade readiness score ${verdict.score} out of 100">
|
|
181
|
+
<svg viewBox="0 0 130 130">
|
|
182
|
+
<circle class="g-track" cx="65" cy="65" r="${R}"></circle>
|
|
183
|
+
<circle class="g-fill" cx="65" cy="65" r="${R}"
|
|
184
|
+
stroke-dasharray="${CIRC.toFixed(2)}" stroke-dashoffset="${CIRC.toFixed(2)}"
|
|
185
|
+
data-offset="${gaugeOffset}" transform="rotate(-90 65 65)"></circle>
|
|
186
|
+
</svg>
|
|
187
|
+
<div class="g-center"><div class="g-score" data-count="${verdict.score}">0</div><div class="g-out">/ 100</div></div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="hero-body">
|
|
190
|
+
<div class="band-tag ${scoreClass}">${esc(verdict.bandLabel)}</div>
|
|
191
|
+
<p class="verdict">${verdictSentence}</p>
|
|
192
|
+
<div class="kpis">
|
|
193
|
+
<div class="kpi"><div class="kpi-v">${esc(snapshot.angularVersion || '?')}</div><div class="kpi-l">Current Angular</div></div>
|
|
194
|
+
<div class="kpi"><div class="kpi-v">${versionGap.known ? versionGap.majorsBehind : '?'}</div><div class="kpi-l">Majors behind</div></div>
|
|
195
|
+
<div class="kpi"><div class="kpi-v">${hardBlockers}</div><div class="kpi-l">Hard blockers</div></div>
|
|
196
|
+
<div class="kpi" title="Estimated developer-days: roughly how many working days ONE developer would need for the whole upgrade — clearing blockers, the version hops and code fixes. Across a team, divide by the number of developers for calendar time. Heuristic range, not a commitment.">
|
|
197
|
+
<div class="kpi-v">${verdict.effort.lowDays}–${verdict.effort.highDays}</div><div class="kpi-l">Est. effort <span class="info">ⓘ</span></div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
|
|
203
|
+
<nav class="toc">
|
|
204
|
+
<a href="#inventory">Inventory</a>
|
|
205
|
+
<a href="#path">Upgrade path</a>
|
|
206
|
+
<a href="#deps">Dependencies</a>
|
|
207
|
+
${breakingSection ? '<a href="#code">Code changes</a>' : ''}
|
|
208
|
+
<a href="#plan">Plan</a>
|
|
209
|
+
</nav>
|
|
210
|
+
|
|
211
|
+
<section id="inventory">
|
|
212
|
+
<h2><span class="h-ic">📦</span>Project inventory</h2>
|
|
213
|
+
<div class="stats">${inventoryStats}</div>
|
|
214
|
+
</section>
|
|
215
|
+
|
|
216
|
+
<section id="path">
|
|
217
|
+
<h2><span class="h-ic">🧭</span>Upgrade path</h2>
|
|
218
|
+
<div class="panel">${pathHtml}</div>
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
<section id="deps">
|
|
222
|
+
<h2><span class="h-ic">🔗</span>Dependency compatibility</h2>
|
|
223
|
+
<div class="stats s4">${compatStats}</div>
|
|
224
|
+
<div class="panel distpanel">
|
|
225
|
+
<div class="distbar">${distBar}</div>
|
|
226
|
+
<div class="dist-legend">${distLegend}</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="controls" id="filters">
|
|
229
|
+
<button data-f="all" class="active">All (${s.total})</button>
|
|
230
|
+
<button data-f="blocker">Blockers (${s.blocker})</button>
|
|
231
|
+
<button data-f="removed">Removed (${s.removed})</button>
|
|
232
|
+
<button data-f="abandoned">Abandoned (${s.abandoned})</button>
|
|
233
|
+
<button data-f="partial">Partial (${s.partial})</button>
|
|
234
|
+
<button data-f="ok">Compatible (${s.ok + s.agnostic})</button>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="tablewrap">
|
|
237
|
+
<table id="deptable">
|
|
238
|
+
<thead><tr>
|
|
239
|
+
<th data-k="name">Package</th><th data-k="cat">Status</th><th data-k="range">Declared</th>
|
|
240
|
+
<th data-k="latest">Latest</th><th class="center" data-k="max">Supports</th><th>Notes</th>
|
|
241
|
+
</tr></thead>
|
|
242
|
+
<tbody>${rows}</tbody>
|
|
243
|
+
</table>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="legend">“Supports” = highest Angular major the package’s published peer dependencies accept. “n/a” = framework-agnostic library. Click a header to sort.</div>
|
|
246
|
+
</section>
|
|
247
|
+
|
|
248
|
+
${breakingSection}
|
|
249
|
+
|
|
250
|
+
<section id="plan">
|
|
251
|
+
<h2><span class="h-ic">🚑</span>Must-fix before upgrading</h2>
|
|
252
|
+
<div class="cards">${blockerCards}</div>
|
|
253
|
+
|
|
254
|
+
<h3>Readiness score breakdown</h3>
|
|
255
|
+
<div class="panel">${deductionsHtml}</div>
|
|
256
|
+
|
|
257
|
+
${quickWins ? `<h3>Quick wins</h3><div class="panel">${quickWins}</div>` : ''}
|
|
258
|
+
</section>
|
|
259
|
+
|
|
260
|
+
<footer>
|
|
261
|
+
Generated by <b>ng-upgrade-doctor</b> v${esc(meta.toolVersion)} · zero-dependency · read-only.<br>
|
|
262
|
+
Compatibility is inferred from published npm peer dependencies and a curated knowledge base; always verify against the official Angular Update Guide before upgrading.
|
|
263
|
+
</footer>
|
|
264
|
+
</div>
|
|
265
|
+
<button id="toTop" class="to-top" aria-label="Back to top" title="Back to top">↑</button>
|
|
266
|
+
${SCRIPT}
|
|
267
|
+
</body>
|
|
268
|
+
</html>`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildVerdictSentence(snapshot, versionGap, deps, verdict, breakingChanges, hardBlockers) {
|
|
272
|
+
const from = snapshot.angularMajor;
|
|
273
|
+
const to = deps.targetMajor;
|
|
274
|
+
const bits = [];
|
|
275
|
+
if (from && versionGap.known) {
|
|
276
|
+
bits.push(`Upgrading <b>Angular ${from} → ${to}</b> is rated <b>${esc(verdict.band)}</b>.`);
|
|
277
|
+
} else {
|
|
278
|
+
bits.push(`This project is rated <b>${esc(verdict.band)}</b> for upgrade readiness.`);
|
|
279
|
+
}
|
|
280
|
+
const clauses = [];
|
|
281
|
+
if (hardBlockers > 0) clauses.push(`<b>${hardBlockers}</b> hard blocker${hardBlockers === 1 ? '' : 's'} to clear`);
|
|
282
|
+
if (versionGap.known && versionGap.hopCount > 0) clauses.push(`<b>${versionGap.hopCount}</b> version hop${versionGap.hopCount === 1 ? '' : 's'}`);
|
|
283
|
+
if (breakingChanges && breakingChanges.summary.removed > 0) clauses.push(`<b>${breakingChanges.summary.removed}</b> removed-API usage${breakingChanges.summary.removed === 1 ? '' : 's'} in code`);
|
|
284
|
+
if (clauses.length) {
|
|
285
|
+
bits.push(`There ${clauses.length === 1 ? 'is' : 'are'} ${joinList(clauses)}, for an estimated <b>${verdict.effort.lowDays}–${verdict.effort.highDays} developer-days</b> of work.`);
|
|
286
|
+
} else {
|
|
287
|
+
bits.push(`Estimated effort is <b>${verdict.effort.lowDays}–${verdict.effort.highDays} developer-days</b>.`);
|
|
288
|
+
}
|
|
289
|
+
return bits.join(' ');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function joinList(arr) {
|
|
293
|
+
if (arr.length === 1) return arr[0];
|
|
294
|
+
return arr.slice(0, -1).join(', ') + ' and ' + arr[arr.length - 1];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const CSS = `
|
|
298
|
+
:root{
|
|
299
|
+
--bg:#f4f6fa; --panel:#ffffff; --ink:#151b28; --muted:#5f6b82; --faint:#9aa4ba; --line:#e6e9f0;
|
|
300
|
+
--brand:#5b3df5; --brand2:#7b5cff;
|
|
301
|
+
--good:#12855a; --ok:#1f8f6a; --warn:#b0730d; --bad:#cf3a2b; --info:#2f6fed;
|
|
302
|
+
--good-bg:#e4f6ec; --warn-bg:#fdf1de; --bad-bg:#fbe7e4; --info-bg:#e8f0fe; --muted-bg:#eef1f6; --faint-bg:#f2f4f8;
|
|
303
|
+
--shadow:0 1px 2px rgba(20,27,40,.04),0 4px 12px rgba(20,27,40,.05);
|
|
304
|
+
}
|
|
305
|
+
@media (prefers-color-scheme: dark){
|
|
306
|
+
:root{ --bg:#0d1118; --panel:#161c29; --ink:#e8edf6; --muted:#98a3ba; --faint:#5b6577; --line:#25304a;
|
|
307
|
+
--brand:#8b73ff; --brand2:#a48dff;
|
|
308
|
+
--good:#3fce8f; --ok:#3fce8f; --warn:#e5a94a; --bad:#f2705f; --info:#6ea0ff;
|
|
309
|
+
--good-bg:#10321f; --warn-bg:#33280f; --bad-bg:#3a1b16; --info-bg:#132244; --muted-bg:#1d2536; --faint-bg:#161d2b;
|
|
310
|
+
--shadow:0 1px 2px rgba(0,0,0,.28),0 5px 16px rgba(0,0,0,.26);}
|
|
311
|
+
}
|
|
312
|
+
:root[data-theme=dark]{ color-scheme:dark; }
|
|
313
|
+
*{box-sizing:border-box}
|
|
314
|
+
html{scroll-behavior:smooth}
|
|
315
|
+
body{margin:0;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}
|
|
316
|
+
.wrap{max-width:1080px;margin:0 auto;padding:28px 20px 80px}
|
|
317
|
+
.small{font-size:12px}
|
|
318
|
+
.muted{color:var(--muted)}
|
|
319
|
+
|
|
320
|
+
.masthead{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
|
321
|
+
.brand{display:flex;align-items:center;gap:12px}
|
|
322
|
+
.logo{font-size:30px;filter:drop-shadow(0 2px 6px rgba(91,61,245,.35))}
|
|
323
|
+
.btitle{font-size:19px;font-weight:800;letter-spacing:-.3px}
|
|
324
|
+
.bsub{color:var(--muted);font-size:12.5px}
|
|
325
|
+
.meta-right{text-align:right}
|
|
326
|
+
.proj{font-weight:700}
|
|
327
|
+
|
|
328
|
+
/* Hero */
|
|
329
|
+
.hero{position:relative;display:grid;grid-template-columns:170px 1fr;gap:28px;align-items:center;background:
|
|
330
|
+
radial-gradient(120% 140% at 0% 0%, color-mix(in srgb,var(--brand) 10%, var(--panel)) 0%, var(--panel) 55%);
|
|
331
|
+
border:1px solid var(--line);border-radius:20px;padding:26px 28px;box-shadow:var(--shadow);overflow:hidden}
|
|
332
|
+
.hero:before{content:"";position:absolute;inset:0 0 auto 0;height:4px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
|
|
333
|
+
.hero.good:before{background:linear-gradient(90deg,var(--good),#4fd6a0)}
|
|
334
|
+
.hero.warn:before{background:linear-gradient(90deg,var(--warn),#e6b24d)}
|
|
335
|
+
.hero.bad:before{background:linear-gradient(90deg,var(--bad),#f0866f)}
|
|
336
|
+
@media(max-width:620px){.hero{grid-template-columns:1fr;justify-items:center;text-align:center}}
|
|
337
|
+
|
|
338
|
+
.gauge{position:relative;width:150px;height:150px}
|
|
339
|
+
.gauge svg{width:150px;height:150px;display:block}
|
|
340
|
+
.g-track{fill:none;stroke:var(--muted-bg);stroke-width:12}
|
|
341
|
+
.g-fill{fill:none;stroke-width:12;stroke-linecap:round;transition:stroke-dashoffset 1.15s cubic-bezier(.34,.9,.3,1)}
|
|
342
|
+
.hero.good .g-fill{stroke:var(--good)} .hero.ok .g-fill{stroke:var(--ok)}
|
|
343
|
+
.hero.warn .g-fill{stroke:var(--warn)} .hero.bad .g-fill{stroke:var(--bad)}
|
|
344
|
+
.g-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
345
|
+
.g-score{font-size:44px;font-weight:850;line-height:1;letter-spacing:-1px}
|
|
346
|
+
.hero.good .g-score{color:var(--good)} .hero.ok .g-score{color:var(--ok)}
|
|
347
|
+
.hero.warn .g-score{color:var(--warn)} .hero.bad .g-score{color:var(--bad)}
|
|
348
|
+
.g-out{font-size:12px;color:var(--muted);font-weight:600;margin-top:2px}
|
|
349
|
+
|
|
350
|
+
.band-tag{display:inline-block;font-weight:700;font-size:13px;padding:4px 12px;border-radius:20px;margin-bottom:8px}
|
|
351
|
+
.band-tag.good{background:var(--good-bg);color:var(--good)} .band-tag.ok{background:var(--good-bg);color:var(--ok)}
|
|
352
|
+
.band-tag.warn{background:var(--warn-bg);color:var(--warn)} .band-tag.bad{background:var(--bad-bg);color:var(--bad)}
|
|
353
|
+
.verdict{margin:6px 0 16px;font-size:16px;line-height:1.5}
|
|
354
|
+
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
|
355
|
+
@media(max-width:620px){.kpis{grid-template-columns:repeat(2,1fr);width:100%}}
|
|
356
|
+
.kpi{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:10px 12px}
|
|
357
|
+
.kpi-v{font-size:19px;font-weight:800;letter-spacing:-.3px}
|
|
358
|
+
.kpi-l{color:var(--muted);font-size:11.5px;margin-top:1px}
|
|
359
|
+
|
|
360
|
+
/* TOC */
|
|
361
|
+
.toc{position:sticky;top:0;z-index:20;display:flex;gap:6px;flex-wrap:wrap;margin:22px 0 8px;padding:9px 10px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}
|
|
362
|
+
.toc a{color:var(--muted);text-decoration:none;font-size:13px;font-weight:600;padding:5px 11px;border-radius:16px}
|
|
363
|
+
.toc a:hover{color:var(--ink);background:var(--muted-bg)}
|
|
364
|
+
section{scroll-margin-top:66px}
|
|
365
|
+
.info{font-size:10px;opacity:.65;cursor:help;vertical-align:super}
|
|
366
|
+
|
|
367
|
+
h2{font-size:15px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:34px 0 14px;font-weight:800;display:flex;align-items:center;gap:8px}
|
|
368
|
+
.h-ic{font-size:16px}
|
|
369
|
+
h3{font-size:14px;margin:26px 0 10px;font-weight:700}
|
|
370
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:var(--shadow)}
|
|
371
|
+
|
|
372
|
+
.stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:12px}
|
|
373
|
+
.stats.s4{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}
|
|
374
|
+
.stat{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:15px 16px;box-shadow:var(--shadow);overflow:hidden;transition:transform .15s ease,box-shadow .15s ease}
|
|
375
|
+
.stat:hover{transform:translateY(-2px)}
|
|
376
|
+
.stat:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--faint)}
|
|
377
|
+
.stat.a-indigo:before{background:var(--brand)} .stat.a-teal:before{background:#17a2a2}
|
|
378
|
+
.stat.a-green:before{background:var(--good)} .stat.a-amber:before{background:var(--warn)}
|
|
379
|
+
.stat.a-red:before{background:var(--bad)} .stat.a-blue:before{background:var(--info)} .stat.a-gray:before{background:var(--faint)}
|
|
380
|
+
.stat-v{font-size:27px;font-weight:850;letter-spacing:-.6px;line-height:1.1}
|
|
381
|
+
.stat-l{color:var(--muted);font-size:12.5px;margin-top:3px;font-weight:600}
|
|
382
|
+
.stat-s{color:var(--faint);font-size:11.5px;margin-top:4px}
|
|
383
|
+
|
|
384
|
+
/* Distribution bar */
|
|
385
|
+
.stats + .panel{margin-top:18px}
|
|
386
|
+
.distpanel{margin-top:18px;margin-bottom:14px}
|
|
387
|
+
.distbar{display:flex;height:16px;border-radius:8px;overflow:hidden;gap:2px;background:var(--muted-bg)}
|
|
388
|
+
.distbar .seg{display:block;min-width:3px;transition:flex .6s ease}
|
|
389
|
+
.seg.ok{background:var(--good)} .seg.warn{background:var(--warn)} .seg.bad{background:var(--bad)}
|
|
390
|
+
.seg.info{background:var(--info)} .seg.muted{background:var(--faint)} .seg.faint{background:var(--line)}
|
|
391
|
+
.dist-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;font-size:12.5px}
|
|
392
|
+
.lg{display:flex;align-items:center;gap:6px;color:var(--muted)}
|
|
393
|
+
.lg b{color:var(--ink)} .pct{color:var(--faint)}
|
|
394
|
+
.dot{width:10px;height:10px;border-radius:3px;display:inline-block}
|
|
395
|
+
.dot.ok{background:var(--good)} .dot.warn{background:var(--warn)} .dot.bad{background:var(--bad)}
|
|
396
|
+
.dot.info{background:var(--info)} .dot.muted{background:var(--faint)} .dot.faint{background:var(--line)}
|
|
397
|
+
|
|
398
|
+
/* Path */
|
|
399
|
+
.path{display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-weight:800;margin-bottom:18px}
|
|
400
|
+
.hop{position:relative;background:var(--info-bg);color:var(--info);border-radius:10px;padding:8px 14px 18px;text-align:center}
|
|
401
|
+
.hop em{position:absolute;left:0;right:0;bottom:3px;font-style:normal;font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}
|
|
402
|
+
.hop.cur{background:var(--muted-bg);color:var(--muted)}
|
|
403
|
+
.hop.target{background:var(--brand);color:#fff}
|
|
404
|
+
.arrow{color:var(--faint)}
|
|
405
|
+
.hop-notes{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
|
|
406
|
+
.hop-note{border:1px solid var(--line);border-radius:12px;padding:14px;font-size:13px;background:var(--panel)}
|
|
407
|
+
.hop-badge{display:inline-block;font-weight:800;color:var(--brand);background:var(--info-bg);border-radius:8px;padding:2px 9px;font-size:13px}
|
|
408
|
+
.hop-req{color:var(--faint);font-size:11.5px;margin:8px 0 6px}
|
|
409
|
+
.hop-note ul{margin:0;padding-left:18px;color:var(--muted)}
|
|
410
|
+
.hop-note li{margin:3px 0}
|
|
411
|
+
|
|
412
|
+
/* Controls + table */
|
|
413
|
+
.controls{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 12px}
|
|
414
|
+
.controls button{background:var(--panel);border:1px solid var(--line);color:var(--muted);border-radius:20px;padding:6px 13px;font-size:12.5px;cursor:pointer;font-weight:600;transition:all .12s}
|
|
415
|
+
.controls button:hover{color:var(--ink);border-color:var(--faint)}
|
|
416
|
+
.controls button.active{background:var(--brand);color:#fff;border-color:var(--brand)}
|
|
417
|
+
.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
|
|
418
|
+
table{width:100%;border-collapse:collapse;font-size:13.5px;background:var(--panel)}
|
|
419
|
+
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
420
|
+
tbody tr{transition:background .1s}
|
|
421
|
+
tbody tr:hover{background:var(--faint-bg)}
|
|
422
|
+
tbody tr:last-child td{border-bottom:none}
|
|
423
|
+
th{background:var(--panel);cursor:pointer;user-select:none;font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
|
|
424
|
+
th:hover{color:var(--ink)}
|
|
425
|
+
td.center,th.center{text-align:center}
|
|
426
|
+
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px}
|
|
427
|
+
.tag{background:var(--muted-bg);color:var(--muted);border-radius:5px;padding:1px 5px;margin-left:6px;font-size:10.5px;font-family:inherit}
|
|
428
|
+
.reason{color:var(--muted);max-width:420px;font-size:12.5px}
|
|
429
|
+
.repl{color:var(--info);margin-top:4px;font-size:12.5px}
|
|
430
|
+
.samples{max-width:320px}
|
|
431
|
+
.samples code{display:inline-block;background:var(--muted-bg);color:var(--muted);border-radius:5px;padding:1px 6px;margin:2px 4px 2px 0;font-size:11px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
432
|
+
|
|
433
|
+
.pill{display:inline-block;border-radius:20px;padding:2px 11px;font-size:12px;font-weight:700;white-space:nowrap}
|
|
434
|
+
.pill.ok{background:var(--good-bg);color:var(--good)}
|
|
435
|
+
.pill.warn{background:var(--warn-bg);color:var(--warn)}
|
|
436
|
+
.pill.bad{background:var(--bad-bg);color:var(--bad)}
|
|
437
|
+
.pill.info{background:var(--info-bg);color:var(--info)}
|
|
438
|
+
.pill.muted{background:var(--muted-bg);color:var(--muted)}
|
|
439
|
+
|
|
440
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:12px}
|
|
441
|
+
.card{border:1px solid var(--line);border-radius:14px;padding:15px;background:var(--panel);box-shadow:var(--shadow)}
|
|
442
|
+
.card.bad{border-left:4px solid var(--bad)}
|
|
443
|
+
.card-h{display:flex;align-items:center;gap:8px;font-weight:700;margin-bottom:7px}
|
|
444
|
+
.card-b{color:var(--muted);font-size:13px}
|
|
445
|
+
.card-r{margin-top:9px;font-size:13px}
|
|
446
|
+
.empty{background:var(--good-bg);color:var(--good);border-radius:14px;padding:16px;font-weight:600}
|
|
447
|
+
|
|
448
|
+
.deductions{list-style:none;padding:0;margin:0}
|
|
449
|
+
.deductions li{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid var(--line)}
|
|
450
|
+
.deductions li:last-child{border-bottom:none}
|
|
451
|
+
.minus{min-width:40px;color:var(--bad);font-weight:800;font-variant-numeric:tabular-nums}
|
|
452
|
+
.dbar{flex:0 0 110px;height:7px;background:var(--muted-bg);border-radius:4px;overflow:hidden}
|
|
453
|
+
.dbar span{display:block;height:100%;background:linear-gradient(90deg,var(--bad),#f0866f);border-radius:4px}
|
|
454
|
+
.dlabel{color:var(--muted);font-size:13px}
|
|
455
|
+
.wins{list-style:none;padding:0;margin:0}
|
|
456
|
+
.wins li{padding:8px 0 8px 26px;position:relative;border-bottom:1px solid var(--line)}
|
|
457
|
+
.wins li:last-child{border-bottom:none}
|
|
458
|
+
.wins li:before{content:"✓";position:absolute;left:2px;top:8px;color:var(--good);font-weight:800}
|
|
459
|
+
|
|
460
|
+
.legend{color:var(--faint);font-size:12px;margin-top:10px}
|
|
461
|
+
.legend code{background:var(--muted-bg);padding:1px 5px;border-radius:4px}
|
|
462
|
+
footer{margin-top:44px;color:var(--faint);font-size:12px;text-align:center;line-height:1.7}
|
|
463
|
+
footer b{color:var(--muted)}
|
|
464
|
+
|
|
465
|
+
.to-top{position:fixed;right:22px;bottom:22px;width:46px;height:46px;border-radius:50%;border:none;background:var(--brand);color:#fff;font-size:22px;line-height:1;cursor:pointer;box-shadow:0 6px 20px rgba(20,27,40,.25);opacity:0;visibility:hidden;transform:translateY(10px);transition:opacity .2s,transform .2s,visibility .2s,background .15s;z-index:40}
|
|
466
|
+
.to-top.show{opacity:1;visibility:visible;transform:none}
|
|
467
|
+
.to-top:hover{background:var(--brand2)}
|
|
468
|
+
.to-top:focus-visible{outline:2px solid var(--brand2);outline-offset:2px}
|
|
469
|
+
|
|
470
|
+
@media (prefers-reduced-motion: reduce){
|
|
471
|
+
.g-fill{transition:none} .stat{transition:none} html{scroll-behavior:auto}
|
|
472
|
+
}
|
|
473
|
+
@media print{
|
|
474
|
+
.toc{display:none} .to-top{display:none} body{background:#fff} .stat,.panel,.card,.tablewrap,.hero{box-shadow:none}
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
|
|
478
|
+
const SCRIPT = `<script>
|
|
479
|
+
(function(){
|
|
480
|
+
function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded',fn); }
|
|
481
|
+
ready(function(){
|
|
482
|
+
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
483
|
+
|
|
484
|
+
// Gauge fill
|
|
485
|
+
var fill=document.querySelector('.g-fill');
|
|
486
|
+
if(fill){ var off=fill.getAttribute('data-offset');
|
|
487
|
+
if(reduce){ fill.style.strokeDashoffset=off; }
|
|
488
|
+
else { requestAnimationFrame(function(){ requestAnimationFrame(function(){ fill.style.strokeDashoffset=off; }); }); }
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Count-up numbers
|
|
492
|
+
document.querySelectorAll('[data-count]').forEach(function(el){
|
|
493
|
+
var target=parseInt(el.getAttribute('data-count'),10)||0;
|
|
494
|
+
if(reduce||target<=0){ el.textContent=target.toLocaleString('en-US'); return; }
|
|
495
|
+
var dur= target>1000?900:650, start=null;
|
|
496
|
+
function step(ts){ if(start===null)start=ts; var p=Math.min((ts-start)/dur,1);
|
|
497
|
+
var e=1-Math.pow(1-p,3); el.textContent=Math.floor(e*target).toLocaleString('en-US');
|
|
498
|
+
if(p<1)requestAnimationFrame(step); else el.textContent=target.toLocaleString('en-US'); }
|
|
499
|
+
requestAnimationFrame(step);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Back-to-top button
|
|
503
|
+
var toTop=document.getElementById('toTop');
|
|
504
|
+
if(toTop){
|
|
505
|
+
var onScroll=function(){ if(window.pageYOffset>400) toTop.classList.add('show'); else toTop.classList.remove('show'); };
|
|
506
|
+
window.addEventListener('scroll',onScroll,{passive:true}); onScroll();
|
|
507
|
+
toTop.addEventListener('click',function(){ window.scrollTo({top:0,behavior:reduce?'auto':'smooth'}); });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Table filtering
|
|
511
|
+
var filters=document.getElementById('filters');
|
|
512
|
+
var table=document.getElementById('deptable');
|
|
513
|
+
if(filters&&table){
|
|
514
|
+
var tbody=table.querySelector('tbody');
|
|
515
|
+
var rows=Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
|
516
|
+
filters.addEventListener('click',function(e){
|
|
517
|
+
var b=e.target.closest('button'); if(!b)return;
|
|
518
|
+
filters.querySelectorAll('button').forEach(function(x){x.classList.remove('active')});
|
|
519
|
+
b.classList.add('active');
|
|
520
|
+
var f=b.getAttribute('data-f');
|
|
521
|
+
rows.forEach(function(r){ var c=r.getAttribute('data-cat');
|
|
522
|
+
r.style.display=(f==='all'||c===f||(f==='ok'&&(c==='ok'||c==='agnostic')))?'':'none'; });
|
|
523
|
+
});
|
|
524
|
+
// Sorting
|
|
525
|
+
var dir={};
|
|
526
|
+
table.querySelectorAll('th').forEach(function(th,i){
|
|
527
|
+
th.addEventListener('click',function(){ dir[i]=!dir[i];
|
|
528
|
+
rows.slice().sort(function(a,b){
|
|
529
|
+
var x=a.children[i].innerText.trim().toLowerCase(), y=b.children[i].innerText.trim().toLowerCase();
|
|
530
|
+
if(x<y)return dir[i]?-1:1; if(x>y)return dir[i]?1:-1; return 0;
|
|
531
|
+
}).forEach(function(r){tbody.appendChild(r)});
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
})();
|
|
537
|
+
</script>`;
|
|
538
|
+
|
|
539
|
+
module.exports = { renderHtml };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Machine-readable report for CI gates and dashboards. */
|
|
4
|
+
function renderJson(report) {
|
|
5
|
+
const { snapshot, versionGap, deps, verdict, census, breakingChanges, meta } = report;
|
|
6
|
+
return JSON.stringify(
|
|
7
|
+
{
|
|
8
|
+
tool: 'ng-upgrade-doctor',
|
|
9
|
+
version: meta.toolVersion,
|
|
10
|
+
generatedAtNote: 'timestamp added by caller if available',
|
|
11
|
+
project: {
|
|
12
|
+
name: snapshot.name,
|
|
13
|
+
angularVersion: snapshot.angularVersion,
|
|
14
|
+
angularMajor: snapshot.angularMajor,
|
|
15
|
+
typescript: snapshot.tsVersion,
|
|
16
|
+
packageManager: snapshot.packageManager,
|
|
17
|
+
workspaceType: snapshot.workspaceType,
|
|
18
|
+
builder: snapshot.builder,
|
|
19
|
+
testRunner: snapshot.testRunner,
|
|
20
|
+
e2e: snapshot.e2e,
|
|
21
|
+
},
|
|
22
|
+
inventory: {
|
|
23
|
+
prodDependencies: snapshot.prodDependencyCount,
|
|
24
|
+
devDependencies: snapshot.devDependencyCount,
|
|
25
|
+
totalDependencies: snapshot.totalDependencyCount,
|
|
26
|
+
transitiveDependencies: snapshot.transitiveDependencyCount,
|
|
27
|
+
angularPackages: snapshot.angularPackageCount,
|
|
28
|
+
thirdPartyAngularLibs: snapshot.thirdPartyAngularCount,
|
|
29
|
+
census: census || null,
|
|
30
|
+
},
|
|
31
|
+
versionGap,
|
|
32
|
+
readiness: {
|
|
33
|
+
score: verdict.score,
|
|
34
|
+
band: verdict.band,
|
|
35
|
+
bandLabel: verdict.bandLabel,
|
|
36
|
+
effort: verdict.effort,
|
|
37
|
+
deductions: verdict.deductions,
|
|
38
|
+
},
|
|
39
|
+
dependencies: {
|
|
40
|
+
target: deps.targetMajor,
|
|
41
|
+
summary: deps.summary,
|
|
42
|
+
packages: deps.packages,
|
|
43
|
+
},
|
|
44
|
+
breakingChanges: breakingChanges
|
|
45
|
+
? {
|
|
46
|
+
astAvailable: breakingChanges.astAvailable,
|
|
47
|
+
filesScanned: breakingChanges.filesScanned,
|
|
48
|
+
summary: breakingChanges.summary,
|
|
49
|
+
rules: breakingChanges.rules,
|
|
50
|
+
}
|
|
51
|
+
: null,
|
|
52
|
+
blockers: verdict.blockers,
|
|
53
|
+
quickWins: verdict.quickWins,
|
|
54
|
+
},
|
|
55
|
+
null,
|
|
56
|
+
2
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { renderJson };
|