@eduardbar/drift 0.5.0 → 0.6.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/CHANGELOG.md +11 -0
- package/README.md +48 -2
- package/dist/badge.d.ts +2 -0
- package/dist/badge.js +57 -0
- package/dist/ci.d.ts +4 -0
- package/dist/ci.js +85 -0
- package/dist/cli.js +52 -1
- package/dist/report.d.ts +3 -0
- package/dist/report.js +494 -0
- package/package.json +1 -1
- package/src/badge.ts +60 -0
- package/src/ci.ts +87 -0
- package/src/cli.ts +55 -1
- package/src/report.ts +500 -0
package/dist/report.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
const VERSION = '0.6.0';
|
|
3
|
+
function severityColor(severity) {
|
|
4
|
+
switch (severity) {
|
|
5
|
+
case 'error': return '#ef4444';
|
|
6
|
+
case 'warning': return '#eab308';
|
|
7
|
+
case 'info': return '#94a3b8';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function severityIcon(severity) {
|
|
11
|
+
switch (severity) {
|
|
12
|
+
case 'error': return '✖';
|
|
13
|
+
case 'warning': return '▲';
|
|
14
|
+
case 'info': return '◦';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function scoreColor(score) {
|
|
18
|
+
if (score < 20)
|
|
19
|
+
return '#22c55e';
|
|
20
|
+
if (score < 45)
|
|
21
|
+
return '#eab308';
|
|
22
|
+
if (score < 70)
|
|
23
|
+
return '#f97316';
|
|
24
|
+
return '#ef4444';
|
|
25
|
+
}
|
|
26
|
+
function scoreLabel(score) {
|
|
27
|
+
if (score < 20)
|
|
28
|
+
return 'LOW';
|
|
29
|
+
if (score < 45)
|
|
30
|
+
return 'MODERATE';
|
|
31
|
+
if (score < 70)
|
|
32
|
+
return 'HIGH';
|
|
33
|
+
return 'CRITICAL';
|
|
34
|
+
}
|
|
35
|
+
function escapeHtml(str) {
|
|
36
|
+
return str
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"')
|
|
41
|
+
.replace(/'/g, ''');
|
|
42
|
+
}
|
|
43
|
+
export function generateHtmlReport(report) {
|
|
44
|
+
const projectName = basename(report.targetPath);
|
|
45
|
+
const scanDate = new Date(report.scannedAt).toLocaleString('en-US', {
|
|
46
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
47
|
+
hour: '2-digit', minute: '2-digit',
|
|
48
|
+
});
|
|
49
|
+
const projColor = scoreColor(report.totalScore);
|
|
50
|
+
const projLabel = scoreLabel(report.totalScore);
|
|
51
|
+
// count totals
|
|
52
|
+
let totalErrors = 0;
|
|
53
|
+
let totalWarnings = 0;
|
|
54
|
+
let totalInfos = 0;
|
|
55
|
+
for (const f of report.files) {
|
|
56
|
+
for (const issue of f.issues) {
|
|
57
|
+
if (issue.severity === 'error')
|
|
58
|
+
totalErrors++;
|
|
59
|
+
else if (issue.severity === 'warning')
|
|
60
|
+
totalWarnings++;
|
|
61
|
+
else
|
|
62
|
+
totalInfos++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const filesWithIssues = report.files.filter(f => f.issues.length > 0).length;
|
|
66
|
+
// top issues by rule
|
|
67
|
+
const byRule = {};
|
|
68
|
+
for (const f of report.files) {
|
|
69
|
+
for (const issue of f.issues) {
|
|
70
|
+
if (!byRule[issue.rule]) {
|
|
71
|
+
byRule[issue.rule] = { count: 0, severity: issue.severity };
|
|
72
|
+
}
|
|
73
|
+
byRule[issue.rule].count++;
|
|
74
|
+
// escalate severity if needed
|
|
75
|
+
const cur = byRule[issue.rule].severity;
|
|
76
|
+
if (issue.severity === 'error')
|
|
77
|
+
byRule[issue.rule].severity = 'error';
|
|
78
|
+
else if (issue.severity === 'warning' && cur !== 'error')
|
|
79
|
+
byRule[issue.rule].severity = 'warning';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const topRules = Object.entries(byRule)
|
|
83
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
84
|
+
.slice(0, 15);
|
|
85
|
+
const topRulesRows = topRules.map(([rule, { count, severity }]) => {
|
|
86
|
+
const icon = severityIcon(severity);
|
|
87
|
+
const color = severityColor(severity);
|
|
88
|
+
return `
|
|
89
|
+
<tr>
|
|
90
|
+
<td><span class="sev-icon" style="color:${color}">${icon}</span> <span class="rule-name">${escapeHtml(rule)}</span></td>
|
|
91
|
+
<td class="count-cell">${count}</td>
|
|
92
|
+
</tr>`;
|
|
93
|
+
}).join('');
|
|
94
|
+
// files sections — already sorted by score desc from buildReport
|
|
95
|
+
const fileSections = report.files
|
|
96
|
+
.filter(f => f.issues.length > 0)
|
|
97
|
+
.map(f => {
|
|
98
|
+
const hasError = f.issues.some(i => i.severity === 'error');
|
|
99
|
+
const openAttr = hasError ? ' open' : '';
|
|
100
|
+
const fColor = scoreColor(f.score);
|
|
101
|
+
const fLabel = scoreLabel(f.score);
|
|
102
|
+
const issueItems = f.issues.map(issue => {
|
|
103
|
+
const ic = severityColor(issue.severity);
|
|
104
|
+
const ii = severityIcon(issue.severity);
|
|
105
|
+
const snippet = issue.snippet
|
|
106
|
+
? `<pre class="snippet"><code>${escapeHtml(issue.snippet)}</code></pre>`
|
|
107
|
+
: '';
|
|
108
|
+
return `
|
|
109
|
+
<li class="issue-item">
|
|
110
|
+
<div class="issue-header">
|
|
111
|
+
<span class="sev-icon" style="color:${ic}">${ii}</span>
|
|
112
|
+
<span class="issue-location">Line ${issue.line}${issue.column > 0 ? `:${issue.column}` : ''}</span>
|
|
113
|
+
<span class="issue-rule">${escapeHtml(issue.rule)}</span>
|
|
114
|
+
<span class="issue-message">${escapeHtml(issue.message)}</span>
|
|
115
|
+
</div>
|
|
116
|
+
${snippet}
|
|
117
|
+
</li>`;
|
|
118
|
+
}).join('');
|
|
119
|
+
return `
|
|
120
|
+
<details${openAttr} class="file-section">
|
|
121
|
+
<summary class="file-summary">
|
|
122
|
+
<span class="file-path">${escapeHtml(f.path)}</span>
|
|
123
|
+
<span class="file-score" style="color:${fColor}">${f.score} <span class="file-label">${fLabel}</span></span>
|
|
124
|
+
<span class="file-count">${f.issues.length} issue${f.issues.length !== 1 ? 's' : ''}</span>
|
|
125
|
+
</summary>
|
|
126
|
+
<ul class="issue-list">${issueItems}
|
|
127
|
+
</ul>
|
|
128
|
+
</details>`;
|
|
129
|
+
}).join('\n');
|
|
130
|
+
return `<!DOCTYPE html>
|
|
131
|
+
<html lang="en">
|
|
132
|
+
<head>
|
|
133
|
+
<meta charset="UTF-8" />
|
|
134
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
135
|
+
<title>drift report — ${escapeHtml(projectName)}</title>
|
|
136
|
+
<style>
|
|
137
|
+
:root {
|
|
138
|
+
--bg: #0a0a0f;
|
|
139
|
+
--bg-card: #111118;
|
|
140
|
+
--bg-code: #1e1e2e;
|
|
141
|
+
--border: #2a2a3a;
|
|
142
|
+
--text: #e2e8f0;
|
|
143
|
+
--muted: #94a3b8;
|
|
144
|
+
--accent: #6366f1;
|
|
145
|
+
--error: #ef4444;
|
|
146
|
+
--warning: #eab308;
|
|
147
|
+
--info: #94a3b8;
|
|
148
|
+
--green: #22c55e;
|
|
149
|
+
--font-mono: ui-monospace, "Cascadia Code", "Fira Code", Consolas, monospace;
|
|
150
|
+
--radius: 6px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
154
|
+
|
|
155
|
+
body {
|
|
156
|
+
background: var(--bg);
|
|
157
|
+
color: var(--text);
|
|
158
|
+
font-family: var(--font-mono);
|
|
159
|
+
font-size: 14px;
|
|
160
|
+
line-height: 1.6;
|
|
161
|
+
padding: 2rem 1rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.container {
|
|
165
|
+
max-width: 900px;
|
|
166
|
+
margin: 0 auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── Header ── */
|
|
170
|
+
.header {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: space-between;
|
|
175
|
+
gap: 1.5rem;
|
|
176
|
+
margin-bottom: 2rem;
|
|
177
|
+
padding-bottom: 1.5rem;
|
|
178
|
+
border-bottom: 1px solid var(--border);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.header-left h1 {
|
|
182
|
+
font-size: 1.5rem;
|
|
183
|
+
font-weight: 700;
|
|
184
|
+
letter-spacing: -0.02em;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.header-left .scan-date {
|
|
188
|
+
color: var(--muted);
|
|
189
|
+
font-size: 0.8rem;
|
|
190
|
+
margin-top: 0.25rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.score-block {
|
|
194
|
+
text-align: right;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.score-number {
|
|
198
|
+
font-size: 4rem;
|
|
199
|
+
font-weight: 800;
|
|
200
|
+
line-height: 1;
|
|
201
|
+
letter-spacing: -0.04em;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.score-label {
|
|
205
|
+
font-size: 0.75rem;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
letter-spacing: 0.1em;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
margin-top: 0.2rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── Stats row ── */
|
|
213
|
+
.stats-row {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-wrap: wrap;
|
|
216
|
+
gap: 1rem;
|
|
217
|
+
margin-bottom: 2rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stat-card {
|
|
221
|
+
flex: 1 1 140px;
|
|
222
|
+
background: var(--bg-card);
|
|
223
|
+
border: 1px solid var(--border);
|
|
224
|
+
border-radius: var(--radius);
|
|
225
|
+
padding: 0.9rem 1.1rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.stat-card .stat-value {
|
|
229
|
+
font-size: 1.6rem;
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
line-height: 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.stat-card .stat-label {
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
font-size: 0.75rem;
|
|
237
|
+
margin-top: 0.3rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.stat-card .sev-breakdown {
|
|
241
|
+
display: flex;
|
|
242
|
+
gap: 0.8rem;
|
|
243
|
+
margin-top: 0.4rem;
|
|
244
|
+
font-size: 0.8rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ── Top rules table ── */
|
|
248
|
+
.section-title {
|
|
249
|
+
font-size: 0.7rem;
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
letter-spacing: 0.12em;
|
|
252
|
+
text-transform: uppercase;
|
|
253
|
+
color: var(--muted);
|
|
254
|
+
margin-bottom: 0.75rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.rules-table {
|
|
258
|
+
width: 100%;
|
|
259
|
+
border-collapse: collapse;
|
|
260
|
+
margin-bottom: 2rem;
|
|
261
|
+
background: var(--bg-card);
|
|
262
|
+
border: 1px solid var(--border);
|
|
263
|
+
border-radius: var(--radius);
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.rules-table th {
|
|
268
|
+
text-align: left;
|
|
269
|
+
font-size: 0.7rem;
|
|
270
|
+
letter-spacing: 0.08em;
|
|
271
|
+
text-transform: uppercase;
|
|
272
|
+
color: var(--muted);
|
|
273
|
+
padding: 0.6rem 1rem;
|
|
274
|
+
border-bottom: 1px solid var(--border);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.rules-table td {
|
|
278
|
+
padding: 0.55rem 1rem;
|
|
279
|
+
border-bottom: 1px solid var(--border);
|
|
280
|
+
vertical-align: middle;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.rules-table tr:last-child td { border-bottom: none; }
|
|
284
|
+
|
|
285
|
+
.rules-table .count-cell {
|
|
286
|
+
text-align: right;
|
|
287
|
+
color: var(--muted);
|
|
288
|
+
font-size: 0.85rem;
|
|
289
|
+
width: 60px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.rule-name { color: var(--text); }
|
|
293
|
+
.sev-icon { margin-right: 0.4rem; }
|
|
294
|
+
|
|
295
|
+
/* ── File sections ── */
|
|
296
|
+
.files-section { margin-top: 2rem; }
|
|
297
|
+
|
|
298
|
+
.file-section {
|
|
299
|
+
background: var(--bg-card);
|
|
300
|
+
border: 1px solid var(--border);
|
|
301
|
+
border-radius: var(--radius);
|
|
302
|
+
margin-bottom: 0.75rem;
|
|
303
|
+
overflow: hidden;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.file-summary {
|
|
307
|
+
display: flex;
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: 0.75rem;
|
|
311
|
+
padding: 0.75rem 1rem;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
user-select: none;
|
|
314
|
+
list-style: none;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.file-summary::-webkit-details-marker { display: none; }
|
|
318
|
+
|
|
319
|
+
.file-summary::before {
|
|
320
|
+
content: '▶';
|
|
321
|
+
font-size: 0.65rem;
|
|
322
|
+
color: var(--muted);
|
|
323
|
+
transition: transform 0.15s;
|
|
324
|
+
flex-shrink: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
details[open] > .file-summary::before { transform: rotate(90deg); }
|
|
328
|
+
|
|
329
|
+
.file-path {
|
|
330
|
+
flex: 1;
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
word-break: break-all;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.file-score {
|
|
336
|
+
font-size: 0.9rem;
|
|
337
|
+
font-weight: 700;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.file-label {
|
|
341
|
+
font-size: 0.65rem;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
letter-spacing: 0.08em;
|
|
344
|
+
text-transform: uppercase;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.file-count {
|
|
348
|
+
font-size: 0.75rem;
|
|
349
|
+
color: var(--muted);
|
|
350
|
+
background: var(--bg);
|
|
351
|
+
border: 1px solid var(--border);
|
|
352
|
+
border-radius: 99px;
|
|
353
|
+
padding: 0.1rem 0.55rem;
|
|
354
|
+
white-space: nowrap;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ── Issue list ── */
|
|
358
|
+
.issue-list {
|
|
359
|
+
list-style: none;
|
|
360
|
+
padding: 0 1rem 0.75rem;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.issue-item {
|
|
364
|
+
padding: 0.6rem 0;
|
|
365
|
+
border-top: 1px solid var(--border);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.issue-header {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-wrap: wrap;
|
|
371
|
+
align-items: baseline;
|
|
372
|
+
gap: 0.4rem 0.75rem;
|
|
373
|
+
font-size: 0.82rem;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.issue-location {
|
|
377
|
+
color: var(--muted);
|
|
378
|
+
font-size: 0.75rem;
|
|
379
|
+
white-space: nowrap;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.issue-rule {
|
|
383
|
+
background: var(--bg);
|
|
384
|
+
border: 1px solid var(--border);
|
|
385
|
+
border-radius: 3px;
|
|
386
|
+
padding: 0 0.35rem;
|
|
387
|
+
font-size: 0.72rem;
|
|
388
|
+
color: var(--muted);
|
|
389
|
+
white-space: nowrap;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.issue-message {
|
|
393
|
+
color: var(--text);
|
|
394
|
+
flex: 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* ── Snippet ── */
|
|
398
|
+
.snippet {
|
|
399
|
+
background: var(--bg-code);
|
|
400
|
+
border: 1px solid var(--border);
|
|
401
|
+
border-radius: var(--radius);
|
|
402
|
+
padding: 0.65rem 0.9rem;
|
|
403
|
+
margin-top: 0.5rem;
|
|
404
|
+
overflow-x: auto;
|
|
405
|
+
font-size: 0.8rem;
|
|
406
|
+
line-height: 1.5;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.snippet code {
|
|
410
|
+
font-family: var(--font-mono);
|
|
411
|
+
white-space: pre;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ── Footer ── */
|
|
415
|
+
.footer {
|
|
416
|
+
margin-top: 3rem;
|
|
417
|
+
padding-top: 1rem;
|
|
418
|
+
border-top: 1px solid var(--border);
|
|
419
|
+
color: var(--muted);
|
|
420
|
+
font-size: 0.75rem;
|
|
421
|
+
text-align: center;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ── Responsive ── */
|
|
425
|
+
@media (max-width: 600px) {
|
|
426
|
+
.score-number { font-size: 2.8rem; }
|
|
427
|
+
.header { flex-direction: column; align-items: flex-start; }
|
|
428
|
+
.score-block { text-align: left; }
|
|
429
|
+
}
|
|
430
|
+
</style>
|
|
431
|
+
</head>
|
|
432
|
+
<body>
|
|
433
|
+
<div class="container">
|
|
434
|
+
|
|
435
|
+
<!-- Header -->
|
|
436
|
+
<header class="header">
|
|
437
|
+
<div class="header-left">
|
|
438
|
+
<h1>${escapeHtml(projectName)}</h1>
|
|
439
|
+
<div class="scan-date">Scanned ${escapeHtml(scanDate)}</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="score-block">
|
|
442
|
+
<div class="score-number" style="color:${projColor}">${report.totalScore}</div>
|
|
443
|
+
<div class="score-label" style="color:${projColor}">${projLabel}</div>
|
|
444
|
+
</div>
|
|
445
|
+
</header>
|
|
446
|
+
|
|
447
|
+
<!-- Stats -->
|
|
448
|
+
<div class="stats-row">
|
|
449
|
+
<div class="stat-card">
|
|
450
|
+
<div class="stat-value">${report.totalFiles}</div>
|
|
451
|
+
<div class="stat-label">Files scanned</div>
|
|
452
|
+
</div>
|
|
453
|
+
<div class="stat-card">
|
|
454
|
+
<div class="stat-value">${filesWithIssues}</div>
|
|
455
|
+
<div class="stat-label">Files with issues</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="stat-card">
|
|
458
|
+
<div class="stat-value">${report.totalIssues}</div>
|
|
459
|
+
<div class="stat-label">Total issues</div>
|
|
460
|
+
<div class="sev-breakdown">
|
|
461
|
+
<span style="color:var(--error)">✖ ${totalErrors}</span>
|
|
462
|
+
<span style="color:var(--warning)">▲ ${totalWarnings}</span>
|
|
463
|
+
<span style="color:var(--info)">◦ ${totalInfos}</span>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Top rules -->
|
|
469
|
+
${topRules.length > 0 ? `<div class="section-title">Top issues by rule</div>
|
|
470
|
+
<table class="rules-table">
|
|
471
|
+
<thead>
|
|
472
|
+
<tr>
|
|
473
|
+
<th>Rule</th>
|
|
474
|
+
<th style="text-align:right">Count</th>
|
|
475
|
+
</tr>
|
|
476
|
+
</thead>
|
|
477
|
+
<tbody>${topRulesRows}
|
|
478
|
+
</tbody>
|
|
479
|
+
</table>` : ''}
|
|
480
|
+
|
|
481
|
+
<!-- Files -->
|
|
482
|
+
<div class="files-section">
|
|
483
|
+
<div class="section-title">Files with issues</div>
|
|
484
|
+
${fileSections || '<p style="color:var(--muted);font-size:0.85rem">No issues found.</p>'}
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<!-- Footer -->
|
|
488
|
+
<footer class="footer">Generated by drift v${VERSION}</footer>
|
|
489
|
+
|
|
490
|
+
</div>
|
|
491
|
+
</body>
|
|
492
|
+
</html>`;
|
|
493
|
+
}
|
|
494
|
+
//# sourceMappingURL=report.js.map
|
package/package.json
CHANGED
package/src/badge.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {} from './types.js'
|
|
2
|
+
|
|
3
|
+
const LEFT_WIDTH = 47
|
|
4
|
+
const CHAR_WIDTH = 7
|
|
5
|
+
const PADDING = 16
|
|
6
|
+
|
|
7
|
+
function scoreColor(score: number): string {
|
|
8
|
+
if (score < 20) return '#4c1'
|
|
9
|
+
if (score < 45) return '#dfb317'
|
|
10
|
+
if (score < 70) return '#fe7d37'
|
|
11
|
+
return '#e05d44'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function scoreLabel(score: number): string {
|
|
15
|
+
if (score < 20) return 'LOW'
|
|
16
|
+
if (score < 45) return 'MODERATE'
|
|
17
|
+
if (score < 70) return 'HIGH'
|
|
18
|
+
return 'CRITICAL'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rightWidth(text: string): number {
|
|
22
|
+
return text.length * CHAR_WIDTH + PADDING
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function generateBadge(score: number): string {
|
|
26
|
+
const valueText = `${score} ${scoreLabel(score)}`
|
|
27
|
+
const color = scoreColor(score)
|
|
28
|
+
|
|
29
|
+
const rWidth = rightWidth(valueText)
|
|
30
|
+
const totalWidth = LEFT_WIDTH + rWidth
|
|
31
|
+
|
|
32
|
+
const leftCenterX = LEFT_WIDTH / 2
|
|
33
|
+
const rightCenterX = LEFT_WIDTH + rWidth / 2
|
|
34
|
+
|
|
35
|
+
// shields.io pattern: font-size="110" + scale(.1) = effective 11px
|
|
36
|
+
// all X/Y coords are ×10
|
|
37
|
+
const leftTextWidth = (LEFT_WIDTH - 10) * 10
|
|
38
|
+
const rightTextWidth = (rWidth - PADDING) * 10
|
|
39
|
+
|
|
40
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="20">
|
|
41
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
42
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
43
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
44
|
+
</linearGradient>
|
|
45
|
+
<clipPath id="r">
|
|
46
|
+
<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
|
|
47
|
+
</clipPath>
|
|
48
|
+
<g clip-path="url(#r)">
|
|
49
|
+
<rect width="${LEFT_WIDTH}" height="20" fill="#555"/>
|
|
50
|
+
<rect x="${LEFT_WIDTH}" width="${rWidth}" height="20" fill="${color}"/>
|
|
51
|
+
<rect width="${totalWidth}" height="20" fill="url(#s)"/>
|
|
52
|
+
</g>
|
|
53
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
|
|
54
|
+
<text x="${leftCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
|
|
55
|
+
<text x="${leftCenterX * 10}" y="140" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
|
|
56
|
+
<text x="${rightCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
|
|
57
|
+
<text x="${rightCenterX * 10}" y="140" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
|
|
58
|
+
</g>
|
|
59
|
+
</svg>`
|
|
60
|
+
}
|
package/src/ci.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs'
|
|
2
|
+
import { relative } from 'node:path'
|
|
3
|
+
import type { DriftReport } from './types.js'
|
|
4
|
+
|
|
5
|
+
function encodeMessage(msg: string): string {
|
|
6
|
+
return msg
|
|
7
|
+
.replace(/%/g, '%25')
|
|
8
|
+
.replace(/\r/g, '%0D')
|
|
9
|
+
.replace(/\n/g, '%0A')
|
|
10
|
+
.replace(/:/g, '%3A')
|
|
11
|
+
.replace(/,/g, '%2C')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function severityToAnnotation(s: string): 'error' | 'warning' | 'notice' {
|
|
15
|
+
if (s === 'error') return 'error'
|
|
16
|
+
if (s === 'warning') return 'warning'
|
|
17
|
+
return 'notice'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scoreLabel(score: number): string {
|
|
21
|
+
if (score >= 80) return 'A'
|
|
22
|
+
if (score >= 60) return 'B'
|
|
23
|
+
if (score >= 40) return 'C'
|
|
24
|
+
if (score >= 20) return 'D'
|
|
25
|
+
return 'F'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function emitCIAnnotations(report: DriftReport): void {
|
|
29
|
+
for (const file of report.files) {
|
|
30
|
+
for (const issue of file.issues) {
|
|
31
|
+
const level = severityToAnnotation(issue.severity)
|
|
32
|
+
const relPath = relative(process.cwd(), file.path).replace(/\\/g, '/')
|
|
33
|
+
const msg = encodeMessage(`[drift/${issue.rule}] ${issue.message}`)
|
|
34
|
+
const line = issue.line ?? 1
|
|
35
|
+
const col = issue.column ?? 1
|
|
36
|
+
process.stdout.write(`::${level} file=${relPath},line=${line},col=${col}::${msg}\n`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function printCISummary(report: DriftReport): void {
|
|
42
|
+
const summaryPath = process.env['GITHUB_STEP_SUMMARY']
|
|
43
|
+
if (!summaryPath) return
|
|
44
|
+
|
|
45
|
+
const score = report.totalScore
|
|
46
|
+
const grade = scoreLabel(score)
|
|
47
|
+
|
|
48
|
+
let errors = 0
|
|
49
|
+
let warnings = 0
|
|
50
|
+
let info = 0
|
|
51
|
+
|
|
52
|
+
for (const file of report.files) {
|
|
53
|
+
for (const issue of file.issues) {
|
|
54
|
+
if (issue.severity === 'error') errors++
|
|
55
|
+
else if (issue.severity === 'warning') warnings++
|
|
56
|
+
else info++
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sorted = [...report.files]
|
|
61
|
+
.sort((a, b) => b.issues.length - a.issues.length)
|
|
62
|
+
.slice(0, 10)
|
|
63
|
+
|
|
64
|
+
const rows = sorted
|
|
65
|
+
.map((f) => {
|
|
66
|
+
const relPath = relative(process.cwd(), f.path).replace(/\\/g, '/')
|
|
67
|
+
return `| ${relPath} | ${f.score} | ${f.issues.length} |`
|
|
68
|
+
})
|
|
69
|
+
.join('\n')
|
|
70
|
+
|
|
71
|
+
const md = [
|
|
72
|
+
'## drift scan results',
|
|
73
|
+
'',
|
|
74
|
+
`**Score:** ${score}/100 — Grade **${grade}**`,
|
|
75
|
+
'',
|
|
76
|
+
'### Top files by issue count',
|
|
77
|
+
'',
|
|
78
|
+
'| File | Score | Issues |',
|
|
79
|
+
'|------|-------|--------|',
|
|
80
|
+
rows,
|
|
81
|
+
'',
|
|
82
|
+
`**Total issues:** ${errors} errors, ${warnings} warnings, ${info} info`,
|
|
83
|
+
'',
|
|
84
|
+
].join('\n')
|
|
85
|
+
|
|
86
|
+
writeFileSync(summaryPath, md, { flag: 'a' })
|
|
87
|
+
}
|