@cms-lab/reporter 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Afaq Rashid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @cms-lab/reporter
2
+
3
+ HTML report rendering for cms-lab scan results.
4
+
5
+ ```ts
6
+ import { renderHtmlReport } from "@cms-lab/reporter";
7
+ ```
8
+
9
+ The CLI uses this package for `cms-lab scan --report`. The generated report is a
10
+ self-contained HTML file with grouped diagnostics and client-side filters.
11
+
12
+ ## Open Source
13
+
14
+ MIT licensed. See the repository [license](https://github.com/i-afaqrashid/cms-lab/blob/main/LICENSE), [contributing guide](https://github.com/i-afaqrashid/cms-lab/blob/main/CONTRIBUTING.md), and [support guide](https://github.com/i-afaqrashid/cms-lab/blob/main/SUPPORT.md).
@@ -0,0 +1,10 @@
1
+ import { ScanResult } from '@cms-lab/core';
2
+
3
+ type ReporterStatus = {
4
+ available: true;
5
+ format: "html";
6
+ };
7
+ declare function getReporterStatus(): ReporterStatus;
8
+ declare function renderHtmlReport(result: ScanResult): string;
9
+
10
+ export { type ReporterStatus, getReporterStatus, renderHtmlReport };
package/dist/index.js ADDED
@@ -0,0 +1,556 @@
1
+ // src/index.ts
2
+ var LOGO_SVG = `
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
4
+ <rect width="64" height="64" rx="14" fill="#111110"/>
5
+ <g transform="translate(16 16) scale(1.3333333)" stroke="#c8ea3a" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
6
+ <path d="M3 7V5a2 2 0 0 1 2-2h2"/>
7
+ <path d="M17 3h2a2 2 0 0 1 2 2v2"/>
8
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2"/>
9
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2"/>
10
+ <circle cx="12" cy="12" r="3"/>
11
+ <path d="m16 16-1.9-1.9"/>
12
+ </g>
13
+ </svg>`;
14
+ var LOGO_DATA_URI = `data:image/svg+xml,${encodeURIComponent(LOGO_SVG)}`;
15
+ function getReporterStatus() {
16
+ return {
17
+ available: true,
18
+ format: "html"
19
+ };
20
+ }
21
+ function renderHtmlReport(result) {
22
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
23
+ const diagnostics = result.diagnostics;
24
+ const status = result.summary.errors > 0 ? "failed" : "passed";
25
+ const statusClass = result.summary.errors > 0 ? "fail" : result.summary.warnings > 0 ? "warn" : "ok";
26
+ const grouped = groupDiagnostics(diagnostics);
27
+ return `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1">
32
+ <title>cms-lab report</title>
33
+ <link rel="icon" href="${LOGO_DATA_URI}" type="image/svg+xml">
34
+ <link rel="shortcut icon" href="${LOGO_DATA_URI}" type="image/svg+xml">
35
+ <link rel="apple-touch-icon" href="${LOGO_DATA_URI}">
36
+ <style>
37
+ :root {
38
+ --bg: #f6f5ef;
39
+ --surface: #ffffff;
40
+ --surface-2: #fbfaf4;
41
+ --ink: #111110;
42
+ --ink-2: #2a2a26;
43
+ --muted: #6b6b63;
44
+ --border: #e4e1d6;
45
+ --border-strong: #cfccc0;
46
+ --accent: #c8ea3a;
47
+ --error: #c0382a;
48
+ --error-bg: #fbe7e3;
49
+ --warning: #9a6a14;
50
+ --warning-bg: #f9ecc9;
51
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
52
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
53
+ --maxw: 1120px;
54
+ }
55
+ * { box-sizing: border-box; }
56
+ html, body { margin: 0; padding: 0; }
57
+ body {
58
+ margin: 0;
59
+ background: var(--bg);
60
+ color: var(--ink);
61
+ font-family: var(--sans);
62
+ font-size: 16px;
63
+ line-height: 1.55;
64
+ -webkit-font-smoothing: antialiased;
65
+ text-rendering: optimizeLegibility;
66
+ }
67
+ h1, h2, h3, p { margin: 0; }
68
+ h1, h2, h3 { font-weight: 600; letter-spacing: -0.012em; color: var(--ink); }
69
+ code { font-family: var(--mono); font-size: 0.9em; }
70
+ .page { min-height: 100vh; display: flex; flex-direction: column; }
71
+ .page main { flex: 1; }
72
+ .wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 28px; }
73
+ .topbar {
74
+ border-bottom: 1px solid var(--border);
75
+ background: var(--bg);
76
+ position: sticky;
77
+ top: 0;
78
+ z-index: 10;
79
+ backdrop-filter: saturate(140%) blur(6px);
80
+ }
81
+ .topbar-inner {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 14px;
85
+ height: 56px;
86
+ }
87
+ .brand {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ gap: 10px;
91
+ text-decoration: none;
92
+ color: var(--ink);
93
+ font-weight: 600;
94
+ flex: 0 0 auto;
95
+ }
96
+ .brand-mark {
97
+ width: 34px;
98
+ height: 34px;
99
+ border-radius: 8px;
100
+ display: block;
101
+ object-fit: cover;
102
+ flex: 0 0 auto;
103
+ box-shadow: 0 0 0 1px var(--border-strong);
104
+ }
105
+ .brand-name {
106
+ font-family: var(--mono);
107
+ font-size: 14px;
108
+ white-space: nowrap;
109
+ }
110
+ .brand-version {
111
+ font-family: var(--mono);
112
+ font-size: 11px;
113
+ color: var(--muted);
114
+ padding: 2px 6px;
115
+ border: 1px solid var(--border);
116
+ border-radius: 999px;
117
+ background: var(--surface);
118
+ }
119
+ .top-right {
120
+ margin-left: auto;
121
+ font-family: var(--mono);
122
+ font-size: 12px;
123
+ color: var(--muted);
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ white-space: nowrap;
127
+ }
128
+ .meta-line {
129
+ font-family: var(--mono);
130
+ font-size: 12px;
131
+ color: var(--muted);
132
+ margin-bottom: 12px;
133
+ }
134
+ .report-shell {
135
+ background: var(--surface);
136
+ border: 1px solid var(--border);
137
+ border-radius: 10px;
138
+ overflow: hidden;
139
+ }
140
+ .report-head {
141
+ padding: 22px 24px;
142
+ border-bottom: 1px solid var(--border);
143
+ display: grid;
144
+ grid-template-columns: 1fr;
145
+ gap: 8px;
146
+ }
147
+ .report-title-block {
148
+ display: grid;
149
+ grid-template-columns: 56px 1fr;
150
+ gap: 16px;
151
+ align-items: center;
152
+ }
153
+ .report-logo {
154
+ width: 56px;
155
+ height: 56px;
156
+ border-radius: 12px;
157
+ display: block;
158
+ object-fit: cover;
159
+ box-shadow: 0 0 0 1px var(--border-strong), 0 14px 24px -20px rgba(0,0,0,0.35);
160
+ }
161
+ .title-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
162
+ .report-head h1 {
163
+ font-size: 22px;
164
+ line-height: 1.25;
165
+ letter-spacing: -0.01em;
166
+ }
167
+ .subtitle {
168
+ font-family: var(--mono);
169
+ font-size: 12px;
170
+ color: var(--muted);
171
+ overflow-wrap: anywhere;
172
+ }
173
+ .badge {
174
+ font-family: var(--mono);
175
+ font-size: 11px;
176
+ padding: 3px 8px;
177
+ border-radius: 999px;
178
+ border: 1px solid var(--border);
179
+ background: var(--surface);
180
+ color: var(--muted);
181
+ }
182
+ .report-meta {
183
+ display: grid;
184
+ grid-template-columns: repeat(4, 1fr);
185
+ gap: 1px;
186
+ background: var(--border);
187
+ border-top: 1px solid var(--border);
188
+ border-bottom: 1px solid var(--border);
189
+ }
190
+ .stat {
191
+ background: var(--surface);
192
+ padding: 16px;
193
+ }
194
+ .stat .k {
195
+ font-family: var(--mono);
196
+ font-size: 11px;
197
+ color: var(--muted);
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.06em;
200
+ }
201
+ .stat .v {
202
+ font-family: var(--mono);
203
+ font-size: 22px;
204
+ color: var(--ink);
205
+ margin-top: 4px;
206
+ }
207
+ .stat.err .v { color: var(--error); }
208
+ .report-toolbar {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 8px;
212
+ padding: 12px 20px;
213
+ border-bottom: 1px solid var(--border);
214
+ background: var(--surface-2);
215
+ flex-wrap: wrap;
216
+ }
217
+ .chip {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ gap: 6px;
221
+ padding: 4px 9px;
222
+ border-radius: 999px;
223
+ border: 1px solid var(--border);
224
+ background: var(--surface);
225
+ font-family: var(--mono);
226
+ font-size: 12px;
227
+ color: var(--ink-2);
228
+ cursor: pointer;
229
+ }
230
+ button.chip { appearance: none; }
231
+ .chip:focus-visible {
232
+ outline: 2px solid var(--ink);
233
+ outline-offset: 2px;
234
+ }
235
+ .chip .n { color: var(--muted); }
236
+ .chip.active { background: var(--ink); color: #f6f5ef; border-color: var(--ink); }
237
+ .chip.active .n { color: #b8b7ad; }
238
+ .group-header {
239
+ padding: 10px 20px;
240
+ background: var(--surface-2);
241
+ border-bottom: 1px solid var(--border);
242
+ font-family: var(--mono);
243
+ font-size: 12px;
244
+ color: var(--muted);
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 10px;
248
+ }
249
+ .group-header .count { color: var(--ink-2); }
250
+ .diagnostic-group.is-hidden,
251
+ .diag.is-hidden {
252
+ display: none;
253
+ }
254
+ .diag {
255
+ display: grid;
256
+ grid-template-columns: 32px 110px minmax(0, 1fr);
257
+ gap: 12px;
258
+ padding: 14px 20px;
259
+ border-bottom: 1px solid var(--border);
260
+ align-items: start;
261
+ font-size: 14px;
262
+ }
263
+ .diag > * { min-width: 0; }
264
+ .diag:last-child { border-bottom: 0; }
265
+ .diag .col-num {
266
+ width: 18px;
267
+ font-family: var(--mono);
268
+ color: var(--muted);
269
+ font-size: 12px;
270
+ text-align: right;
271
+ }
272
+ .diag .sev {
273
+ font-family: var(--mono);
274
+ font-size: 11px;
275
+ padding: 2px 0;
276
+ display: inline-flex;
277
+ align-items: center;
278
+ gap: 6px;
279
+ letter-spacing: 0.04em;
280
+ text-transform: uppercase;
281
+ white-space: nowrap;
282
+ }
283
+ .diag .sev .marker {
284
+ width: 9px;
285
+ height: 9px;
286
+ display: inline-block;
287
+ }
288
+ .diag.error .sev { color: var(--error); }
289
+ .diag.error .sev .marker { background: var(--error); }
290
+ .diag.warning .sev { color: var(--warning); }
291
+ .diag.warning .sev .marker { background: var(--warning); }
292
+ .diag.info .sev { color: var(--muted); }
293
+ .diag.info .sev .marker { background: var(--muted); }
294
+ .diag .msg {
295
+ color: var(--ink);
296
+ min-width: 0;
297
+ }
298
+ .diag .msg strong { font-weight: 600; }
299
+ .diag .ctx {
300
+ display: flex;
301
+ flex-wrap: wrap;
302
+ gap: 4px 8px;
303
+ margin-top: 5px;
304
+ font-family: var(--mono);
305
+ font-size: 12px;
306
+ color: var(--muted);
307
+ min-width: 0;
308
+ }
309
+ .diag .ctx span {
310
+ display: inline;
311
+ max-width: 100%;
312
+ overflow-wrap: anywhere;
313
+ word-break: break-word;
314
+ }
315
+ .diag-code {
316
+ display: inline-flex;
317
+ white-space: nowrap;
318
+ color: var(--ink-2);
319
+ }
320
+ .path-code {
321
+ font-family: var(--mono);
322
+ font-size: 12px;
323
+ color: var(--ink-2);
324
+ overflow-wrap: anywhere;
325
+ }
326
+ .empty-state {
327
+ padding: 24px 20px;
328
+ color: var(--muted);
329
+ font-size: 14px;
330
+ }
331
+ .report-foot {
332
+ padding: 14px 20px;
333
+ border-top: 1px solid var(--border);
334
+ background: var(--surface-2);
335
+ font-family: var(--mono);
336
+ font-size: 12px;
337
+ color: var(--muted);
338
+ display: flex;
339
+ gap: 18px;
340
+ flex-wrap: wrap;
341
+ }
342
+ .footer {
343
+ padding: 36px 0 56px;
344
+ color: var(--muted);
345
+ font-size: 13px;
346
+ border-top: 1px solid var(--border);
347
+ background: var(--bg);
348
+ }
349
+ .muted { color: var(--muted); }
350
+ @media (max-width: 820px) {
351
+ .report-meta { grid-template-columns: 1fr 1fr; }
352
+ .diag { grid-template-columns: 92px 1fr; }
353
+ .diag .col-num { display: none; }
354
+ }
355
+ @media (max-width: 560px) {
356
+ .wrap { padding: 0 16px; }
357
+ .topbar-inner { gap: 10px; }
358
+ .brand { gap: 7px; }
359
+ .brand-mark { width: 30px; height: 30px; }
360
+ .brand-name { font-size: 13px; }
361
+ .brand-version { display: none; }
362
+ .report-head { padding: 20px; }
363
+ .report-title-block {
364
+ grid-template-columns: 48px 1fr;
365
+ gap: 12px;
366
+ }
367
+ .report-logo {
368
+ width: 48px;
369
+ height: 48px;
370
+ }
371
+ .report-meta .stat { padding: 14px 16px; }
372
+ .report-toolbar { padding: 12px 16px; }
373
+ .diag {
374
+ grid-template-columns: 1fr;
375
+ gap: 6px;
376
+ padding: 14px 16px;
377
+ }
378
+ .diag .sev { padding: 0; }
379
+ }
380
+ </style>
381
+ </head>
382
+ <body class="page">
383
+ <header class="topbar">
384
+ <div class="wrap topbar-inner">
385
+ <span class="brand" aria-label="cms-lab">
386
+ <img class="brand-mark" src="${LOGO_DATA_URI}" width="34" height="34" alt="" aria-hidden="true">
387
+ <span class="brand-name">cms-lab</span>
388
+ <span class="brand-version">report</span>
389
+ </span>
390
+ <div class="top-right">.cms-lab/report.html</div>
391
+ </div>
392
+ </header>
393
+ <main>
394
+ <div class="wrap" style="padding-top:32px; padding-bottom:64px;">
395
+ <div class="meta-line">cms-lab report \xB7 generated ${escapeHtml(generatedAt)}</div>
396
+
397
+ <div class="report-shell">
398
+ <div class="report-head">
399
+ <div class="report-title-block">
400
+ <img class="report-logo" src="${LOGO_DATA_URI}" width="56" height="56" alt="cms-lab logo">
401
+ <div>
402
+ <div class="title-row">
403
+ <h1>${escapeHtml(capitalize(`scan ${status}`))}</h1>
404
+ <span class="badge ${statusClass}">${escapeHtml(statusLabel(result))}</span>
405
+ ${result.summary.warnings > 0 ? `<span class="badge warn">${result.summary.warnings} ${escapeHtml(plural(result.summary.warnings, "warning"))}</span>` : ""}
406
+ ${result.summary.info > 0 ? `<span class="badge info">${result.summary.info} ${escapeHtml(plural(result.summary.info, "info item"))}</span>` : ""}
407
+ </div>
408
+ <div class="subtitle">
409
+ ${escapeHtml(projectLabel(result))} \xB7 ${result.documents.length} ${escapeHtml(plural(result.documents.length, "document"))}
410
+ </div>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ <div class="report-meta">
416
+ <div class="stat err"><div class="k">Errors</div><div class="v">${result.summary.errors}</div></div>
417
+ <div class="stat warn"><div class="k">Warnings</div><div class="v">${result.summary.warnings}</div></div>
418
+ <div class="stat info"><div class="k">Info</div><div class="v">${result.summary.info}</div></div>
419
+ <div class="stat"><div class="k">Documents</div><div class="v">${result.documents.length}</div></div>
420
+ </div>
421
+
422
+ <div class="report-toolbar" aria-label="Diagnostic filters">
423
+ <button class="chip active" type="button" data-filter-kind="all" data-filter-value="all">All <span class="n">${diagnostics.length}</span></button>
424
+ <button class="chip" type="button" data-filter-kind="severity" data-filter-value="error">Errors <span class="n">${result.summary.errors}</span></button>
425
+ <button class="chip" type="button" data-filter-kind="severity" data-filter-value="warning">Warnings <span class="n">${result.summary.warnings}</span></button>
426
+ <button class="chip" type="button" data-filter-kind="severity" data-filter-value="info">Info <span class="n">${result.summary.info}</span></button>
427
+ ${grouped.map((group) => `<button class="chip" type="button" data-filter-kind="group" data-filter-value="${escapeHtml(group.label)}">${escapeHtml(group.label)} <span class="n">${group.diagnostics.length}</span></button>`).join("")}
428
+ </div>
429
+
430
+ ${diagnostics.length === 0 ? `<div class="empty-state">No diagnostics found.</div>` : grouped.map((group) => diagnosticsGroup(group)).join("")}
431
+
432
+ <div class="report-foot">
433
+ <span>project: <span class="path-code">${escapeHtml(result.project.framework)} ${escapeHtml(result.project.router)}</span></span>
434
+ <span>documents: ${result.documents.length}</span>
435
+ <span>diagnostics: ${diagnostics.length}</span>
436
+ <span>generated: ${escapeHtml(generatedAt)}</span>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </main>
441
+ <footer class="footer">
442
+ <div class="wrap">Generated by cms-lab</div>
443
+ </footer>
444
+ <script>
445
+ (() => {
446
+ const chips = [...document.querySelectorAll("[data-filter-kind]")];
447
+ const groups = [...document.querySelectorAll("[data-diagnostic-group]")];
448
+
449
+ function applyFilter(kind, value) {
450
+ chips.forEach((chip) => {
451
+ chip.classList.toggle("active", chip.dataset.filterKind === kind && chip.dataset.filterValue === value);
452
+ });
453
+
454
+ groups.forEach((group) => {
455
+ let visibleCount = 0;
456
+ group.querySelectorAll("[data-diagnostic]").forEach((diagnostic) => {
457
+ const visible =
458
+ kind === "all" ||
459
+ (kind === "severity" && diagnostic.dataset.severity === value) ||
460
+ (kind === "group" && diagnostic.dataset.group === value);
461
+
462
+ diagnostic.classList.toggle("is-hidden", !visible);
463
+ if (visible) {
464
+ visibleCount += 1;
465
+ }
466
+ });
467
+
468
+ group.classList.toggle("is-hidden", visibleCount === 0);
469
+ const count = group.querySelector("[data-visible-count]");
470
+ if (count) {
471
+ count.textContent = visibleCount + " " + (visibleCount === 1 ? "diagnostic" : "diagnostics");
472
+ }
473
+ });
474
+ }
475
+
476
+ chips.forEach((chip) => {
477
+ chip.addEventListener("click", () => {
478
+ applyFilter(chip.dataset.filterKind || "all", chip.dataset.filterValue || "all");
479
+ });
480
+ });
481
+ })();
482
+ </script>
483
+ </body>
484
+ </html>
485
+ `;
486
+ }
487
+ function statusLabel(result) {
488
+ if (result.summary.errors > 0) {
489
+ return `${result.summary.errors} ${plural(result.summary.errors, "error")}`;
490
+ }
491
+ if (result.summary.warnings > 0) {
492
+ return `${result.summary.warnings} ${plural(result.summary.warnings, "warning")}`;
493
+ }
494
+ return "no errors";
495
+ }
496
+ function plural(value, singular) {
497
+ return value === 1 ? singular : `${singular}s`;
498
+ }
499
+ function groupDiagnostics(diagnostics) {
500
+ const groupOrder = ["routes", "fields", "seo", "a11y", "other"];
501
+ const groups = /* @__PURE__ */ new Map();
502
+ for (const diagnostic of diagnostics) {
503
+ const group = groupForDiagnostic(diagnostic);
504
+ groups.set(group, [...groups.get(group) ?? [], diagnostic]);
505
+ }
506
+ return groupOrder.map((label) => ({ label, diagnostics: groups.get(label) ?? [] })).filter((group) => group.diagnostics.length > 0);
507
+ }
508
+ function groupForDiagnostic(diagnostic) {
509
+ if (diagnostic.code.startsWith("CMS-ROUTE") || diagnostic.code === "CMS-UID-MISSING") {
510
+ return "routes";
511
+ }
512
+ if (diagnostic.code.startsWith("CMS-FIELD")) {
513
+ return "fields";
514
+ }
515
+ if (diagnostic.code.startsWith("SEO-")) {
516
+ return "seo";
517
+ }
518
+ if (diagnostic.code.startsWith("A11Y-")) {
519
+ return "a11y";
520
+ }
521
+ return "other";
522
+ }
523
+ function diagnosticsGroup(group) {
524
+ return `<section class="diagnostic-group" data-diagnostic-group="${escapeHtml(group.label)}">
525
+ <div class="group-header">
526
+ <span>${escapeHtml(group.label)}</span> \xB7 <span class="count" data-visible-count>${group.diagnostics.length} ${escapeHtml(plural(group.diagnostics.length, "diagnostic"))}</span>
527
+ </div>
528
+ ${group.diagnostics.map((diagnostic, index) => diagnosticRow(diagnostic, group.label, index + 1)).join("")}
529
+ </section>`;
530
+ }
531
+ function diagnosticRow(diagnostic, group, index) {
532
+ return `<div class="diag ${escapeHtml(diagnostic.severity)}" data-diagnostic data-group="${escapeHtml(group)}" data-severity="${escapeHtml(diagnostic.severity)}">
533
+ <div class="col-num">${index}</div>
534
+ <div class="sev"><span class="marker"></span>${escapeHtml(diagnostic.severity)}</div>
535
+ <div class="msg">
536
+ ${diagnostic.path ? `<strong>${escapeHtml(diagnostic.path)}</strong> ` : ""}${escapeHtml(diagnostic.message)}
537
+ <span class="ctx"><span class="diag-code">${escapeHtml(diagnostic.code)}</span>${diagnostic.source ? ` <span aria-hidden="true">\xB7</span> <span>${escapeHtml(diagnostic.source)}</span>` : ""}</span>
538
+ </div>
539
+ </div>`;
540
+ }
541
+ function capitalize(value) {
542
+ return value.charAt(0).toUpperCase() + value.slice(1);
543
+ }
544
+ function projectLabel(result) {
545
+ if (result.project.framework === "next" && result.project.router === "app") {
546
+ return "Next.js App Router";
547
+ }
548
+ return `${result.project.framework} ${result.project.router} router`;
549
+ }
550
+ function escapeHtml(value) {
551
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
552
+ }
553
+ export {
554
+ getReporterStatus,
555
+ renderHtmlReport
556
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@cms-lab/reporter",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "HTML report rendering for cms-lab.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/i-afaqrashid/cms-lab.git",
10
+ "directory": "packages/reporter"
11
+ },
12
+ "homepage": "https://cms-lab.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/i-afaqrashid/cms-lab/issues"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "report",
19
+ "testing"
20
+ ],
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20.10"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@cms-lab/core": "1.0.0"
39
+ },
40
+ "author": "Afaq Rashid",
41
+ "scripts": {
42
+ "build": "tsup src/index.ts --format esm --dts --clean --tsconfig ../../tsconfig.base.json"
43
+ }
44
+ }