pagetrace 0.5.0 → 0.7.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 +46 -1
- package/README.md +16 -5
- package/dist/cli.cjs +117 -49
- package/dist/cli.js +117 -49
- package/dist/index.cjs +115 -47
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +115 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
While the version is below 1.0.0, breaking changes ship in a minor release.
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The terminal audit report is laid out rather than printed. Findings sit under a
|
|
14
|
+
rule per severity, prose wraps to the terminal instead of running off the right
|
|
15
|
+
edge, the affected routes get their own line, and the page count is right
|
|
16
|
+
aligned against the headline. The old output put explanation, fix and routes at
|
|
17
|
+
the same indent with no wrapping, so on a real site the useful part was the
|
|
18
|
+
hardest part to find.
|
|
19
|
+
- `formatAuditPretty(groups, meta, columns?)` takes an optional terminal width,
|
|
20
|
+
defaulting to 80. The CLI passes `process.stdout.columns`, which keeps
|
|
21
|
+
`report.ts` free of environment reads. Piped output falls back to 80 and drops
|
|
22
|
+
colour, so it stays readable in a CI log.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- A word longer than the terminal is hard-broken instead of overflowing. A
|
|
27
|
+
canonical URL is a single word and is routinely longer than 80 characters, so
|
|
28
|
+
`duplicate.canonical` could push a line well past the edge.
|
|
29
|
+
|
|
30
|
+
## [0.6.0] - 2026-09-06
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Breaking.** `duplicate.title`, `duplicate.description` and
|
|
35
|
+
`duplicate.canonical` now emit one finding per affected route instead of a
|
|
36
|
+
single finding with `route: null`. Integrations reading `route` on these three
|
|
37
|
+
codes will see a path where they saw null; the codes themselves are unchanged.
|
|
38
|
+
|
|
39
|
+
Found by auditing a real site. The report said "2 pages share the same meta
|
|
40
|
+
description" and could not say which two, because the routes lived in `after`,
|
|
41
|
+
which the rollup drops — leaving the only actionable part of the finding
|
|
42
|
+
invisible. Per-route findings also make the counts truthful and point
|
|
43
|
+
`--format github` annotations at the pages rather than at "site".
|
|
44
|
+
|
|
9
45
|
## [0.5.0] - 2026-09-06
|
|
10
46
|
|
|
11
47
|
### Added
|
|
@@ -20,8 +56,15 @@ While the version is below 1.0.0, breaking changes ship in a minor release.
|
|
|
20
56
|
- A composite GitHub Action (`action.yml`). Three lines in a workflow run the
|
|
21
57
|
check and post the findings as a pull request comment, editing the previous
|
|
22
58
|
comment on each push rather than stacking new ones. It fetches the baseline ref
|
|
23
|
-
first, since a shallow CI checkout usually has only the PR head.
|
|
59
|
+
first, since a shallow CI checkout usually has only the PR head. The comment is
|
|
60
|
+
skipped for pull requests from a fork, which run with a read-only token and
|
|
61
|
+
would otherwise fail with a 403 through no fault of the contributor; the
|
|
62
|
+
findings still reach the step summary and still set the exit code.
|
|
24
63
|
- `snapshotFromGitRef(ref, path)` is exported.
|
|
64
|
+
- `examples/site`, a small deliberately correct site with a committed baseline.
|
|
65
|
+
The action runs against it on every pull request to this repo, so a change
|
|
66
|
+
that breaks the action's own wiring fails here rather than in someone else's
|
|
67
|
+
CI. It doubles as a worked example of what a clean surface looks like.
|
|
25
68
|
|
|
26
69
|
### Fixed
|
|
27
70
|
|
|
@@ -194,6 +237,8 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
|
|
|
194
237
|
crawling; diff classified by transition; absolute, cross-page and hreflang audit
|
|
195
238
|
rules; pretty, JSON, markdown, GitHub and HTML reporters.
|
|
196
239
|
|
|
240
|
+
[0.7.0]: https://github.com/shyamexe/pagetrace/compare/v0.6.0...v0.7.0
|
|
241
|
+
[0.6.0]: https://github.com/shyamexe/pagetrace/compare/v0.5.0...v0.6.0
|
|
197
242
|
[0.5.0]: https://github.com/shyamexe/pagetrace/compare/v0.4.0...v0.5.0
|
|
198
243
|
[0.4.0]: https://github.com/shyamexe/pagetrace/compare/v0.3.0...v0.4.0
|
|
199
244
|
[0.3.0]: https://github.com/shyamexe/pagetrace/compare/v0.2.0...v0.3.0
|
package/README.md
CHANGED
|
@@ -72,16 +72,27 @@ npx pagetrace audit --url https://example.com --format html --out audit.html
|
|
|
72
72
|
Findings are rolled up by issue rather than by page, so one template defect reads as a single row affecting 43 pages instead of 43 separate lines. Each row carries why it matters and how to fix it, and the fix is platform-aware — `pagetrace` reads the generator tag and asset paths, so a WordPress site gets Yoast and Rank Math instructions rather than generic advice.
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
|
|
75
|
+
pagetrace · https://acme.test
|
|
76
|
+
43 pages · WordPress · 2026-09-06
|
|
77
|
+
|
|
78
|
+
ERRORS ─────────────────────────────────────────────────────────────────────── 2
|
|
79
|
+
|
|
80
|
+
✗ 2 pages canonicalise to https://acme.test/shop. 2 pages
|
|
76
81
|
Several pages pointing at one canonical means those pages are declaring
|
|
77
82
|
themselves duplicates and will not rank independently.
|
|
78
|
-
|
|
83
|
+
→ A common symptom of a plugin canonicalising every archive page to the
|
|
84
|
+
parent.
|
|
85
|
+
/shop/page/2, /shop/page/3
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
✗ Page has no <h1>. 1 page
|
|
81
88
|
The h1 anchors the document outline used for passage extraction.
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
→ Many themes render the post title as h2 inside archive templates. Check
|
|
90
|
+
single.php or the block template for this post type.
|
|
84
91
|
/tag/widgets
|
|
92
|
+
|
|
93
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
2 issues 2 errors
|
|
95
|
+
3 findings across 43 pages
|
|
85
96
|
```
|
|
86
97
|
|
|
87
98
|
Auditing runs cross-page rules the per-page checks cannot see: duplicate titles and descriptions, several pages canonicalising to one URL, canonicals pointing away from their own path or at another host entirely, and a full hreflang check. Paginated archives and AMP variants are left alone, since canonicalising those to their parent is correct.
|
package/dist/cli.cjs
CHANGED
|
@@ -266,39 +266,38 @@ function auditCrossPage(snapshot) {
|
|
|
266
266
|
}
|
|
267
267
|
return map;
|
|
268
268
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (routes.length > 1) {
|
|
282
|
-
findings.push({
|
|
283
|
-
code: "duplicate.description",
|
|
284
|
-
severity: "warn",
|
|
285
|
-
route: null,
|
|
286
|
-
message: `${routes.length} pages share the same meta description.`,
|
|
287
|
-
after: routes
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
for (const [canonical, routes] of group((p) => p.canonical)) {
|
|
292
|
-
if (routes.length > 1) {
|
|
293
|
-
findings.push({
|
|
294
|
-
code: "duplicate.canonical",
|
|
295
|
-
severity: "error",
|
|
296
|
-
route: null,
|
|
297
|
-
message: `${routes.length} pages canonicalise to ${canonical}.`,
|
|
298
|
-
after: routes
|
|
299
|
-
});
|
|
269
|
+
const duplicates = (code, severity, keyed, message) => {
|
|
270
|
+
for (const [value, routes] of keyed) {
|
|
271
|
+
if (routes.length < 2) continue;
|
|
272
|
+
for (const route of routes) {
|
|
273
|
+
findings.push({
|
|
274
|
+
code,
|
|
275
|
+
severity,
|
|
276
|
+
route,
|
|
277
|
+
message: message(value, routes.length),
|
|
278
|
+
after: routes
|
|
279
|
+
});
|
|
280
|
+
}
|
|
300
281
|
}
|
|
301
|
-
}
|
|
282
|
+
};
|
|
283
|
+
duplicates(
|
|
284
|
+
"duplicate.title",
|
|
285
|
+
"warn",
|
|
286
|
+
group((p) => p.title),
|
|
287
|
+
(title, n) => `${n} pages share the title "${title}".`
|
|
288
|
+
);
|
|
289
|
+
duplicates(
|
|
290
|
+
"duplicate.description",
|
|
291
|
+
"warn",
|
|
292
|
+
group((p) => p.description),
|
|
293
|
+
(_, n) => `${n} pages share the same meta description.`
|
|
294
|
+
);
|
|
295
|
+
duplicates(
|
|
296
|
+
"duplicate.canonical",
|
|
297
|
+
"error",
|
|
298
|
+
group((p) => p.canonical),
|
|
299
|
+
(canonical, n) => `${n} pages canonicalise to ${canonical}.`
|
|
300
|
+
);
|
|
302
301
|
const expectedOrigin = snapshot.site.origin ?? null;
|
|
303
302
|
for (const page of pages) {
|
|
304
303
|
if (!page.canonical) continue;
|
|
@@ -941,33 +940,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
941
940
|
const shown = routes.slice(0, limit).join(", ");
|
|
942
941
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
943
942
|
}
|
|
944
|
-
|
|
943
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
944
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
945
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
946
|
+
function wrapText(text2, width) {
|
|
947
|
+
const lines = [];
|
|
948
|
+
let line = "";
|
|
949
|
+
const flush = () => {
|
|
950
|
+
if (line) lines.push(line);
|
|
951
|
+
line = "";
|
|
952
|
+
};
|
|
953
|
+
for (const word of text2.split(/\s+/)) {
|
|
954
|
+
if (word.length > width) {
|
|
955
|
+
flush();
|
|
956
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (line && line.length + 1 + word.length > width) {
|
|
960
|
+
flush();
|
|
961
|
+
line = word;
|
|
962
|
+
} else {
|
|
963
|
+
line = line ? `${line} ${word}` : word;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
flush();
|
|
967
|
+
return lines;
|
|
968
|
+
}
|
|
969
|
+
function sectionRule(severity, count, width) {
|
|
970
|
+
const label = PLURAL[severity].toUpperCase();
|
|
971
|
+
const tail = String(count);
|
|
972
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
973
|
+
return `${BADGE[severity](import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\u2500".repeat(dashes))} ${import_picocolors.default.dim(tail)}`;
|
|
974
|
+
}
|
|
975
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
976
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
977
|
+
const indent = " ";
|
|
978
|
+
const body = width - indent.length;
|
|
945
979
|
const lines = [
|
|
946
|
-
import_picocolors.default.bold(meta.target
|
|
947
|
-
import_picocolors.default.dim(
|
|
980
|
+
`${import_picocolors.default.bold("pagetrace")} ${import_picocolors.default.dim("\xB7")} ${meta.target}`,
|
|
981
|
+
import_picocolors.default.dim(
|
|
982
|
+
[
|
|
983
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
984
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
985
|
+
meta.generatedAt
|
|
986
|
+
].filter(Boolean).join(" \xB7 ")
|
|
987
|
+
),
|
|
948
988
|
""
|
|
949
989
|
];
|
|
950
990
|
if (groups.length === 0) {
|
|
951
|
-
lines.push(import_picocolors.default.green(
|
|
991
|
+
lines.push(import_picocolors.default.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
952
992
|
return lines.join("\n");
|
|
953
993
|
}
|
|
954
|
-
for (const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
994
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
995
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
996
|
+
if (inSeverity.length === 0) continue;
|
|
997
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
998
|
+
for (const group of inSeverity) {
|
|
999
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1000
|
+
const headline = group.message;
|
|
1001
|
+
const room = width - 2 - scope.length - 1;
|
|
1002
|
+
if (headline.length <= room) {
|
|
1003
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1004
|
+
lines.push(
|
|
1005
|
+
`${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(headline)}${pad}${import_picocolors.default.dim(scope)}`
|
|
1006
|
+
);
|
|
1007
|
+
} else {
|
|
1008
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1009
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(line)}` : `${indent}${import_picocolors.default.bold(line)}`);
|
|
1010
|
+
}
|
|
1011
|
+
lines.push(`${indent}${import_picocolors.default.dim(scope)}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (group.detail) {
|
|
1014
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (group.fix) {
|
|
1017
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1018
|
+
lines.push(`${indent}${import_picocolors.default.cyan("\u2192")} ${first}`);
|
|
1019
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (group.routes.length > 0) {
|
|
1022
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1023
|
+
lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
lines.push("");
|
|
1027
|
+
}
|
|
961
1028
|
}
|
|
962
1029
|
const { issues, instances, total } = countIssues(groups);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
);
|
|
1030
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
|
|
1031
|
+
lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
|
|
1032
|
+
lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
966
1033
|
lines.push(
|
|
967
1034
|
import_picocolors.default.dim(
|
|
968
|
-
|
|
1035
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
969
1036
|
)
|
|
970
1037
|
);
|
|
1038
|
+
lines.push("");
|
|
971
1039
|
return lines.join("\n");
|
|
972
1040
|
}
|
|
973
1041
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1560,7 +1628,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1560
1628
|
pageCount: pages.length,
|
|
1561
1629
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1562
1630
|
};
|
|
1563
|
-
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta);
|
|
1631
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1564
1632
|
if (flags.out) {
|
|
1565
1633
|
await (0, import_promises2.writeFile)(flags.out, `${output}
|
|
1566
1634
|
`, "utf8");
|
|
@@ -1573,7 +1641,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1573
1641
|
}
|
|
1574
1642
|
});
|
|
1575
1643
|
cli.help();
|
|
1576
|
-
cli.version("0.
|
|
1644
|
+
cli.version("0.7.0");
|
|
1577
1645
|
async function main() {
|
|
1578
1646
|
try {
|
|
1579
1647
|
cli.parse(process.argv, { run: false });
|
package/dist/cli.js
CHANGED
|
@@ -243,39 +243,38 @@ function auditCrossPage(snapshot) {
|
|
|
243
243
|
}
|
|
244
244
|
return map;
|
|
245
245
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (routes.length > 1) {
|
|
259
|
-
findings.push({
|
|
260
|
-
code: "duplicate.description",
|
|
261
|
-
severity: "warn",
|
|
262
|
-
route: null,
|
|
263
|
-
message: `${routes.length} pages share the same meta description.`,
|
|
264
|
-
after: routes
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
for (const [canonical, routes] of group((p) => p.canonical)) {
|
|
269
|
-
if (routes.length > 1) {
|
|
270
|
-
findings.push({
|
|
271
|
-
code: "duplicate.canonical",
|
|
272
|
-
severity: "error",
|
|
273
|
-
route: null,
|
|
274
|
-
message: `${routes.length} pages canonicalise to ${canonical}.`,
|
|
275
|
-
after: routes
|
|
276
|
-
});
|
|
246
|
+
const duplicates = (code, severity, keyed, message) => {
|
|
247
|
+
for (const [value, routes] of keyed) {
|
|
248
|
+
if (routes.length < 2) continue;
|
|
249
|
+
for (const route of routes) {
|
|
250
|
+
findings.push({
|
|
251
|
+
code,
|
|
252
|
+
severity,
|
|
253
|
+
route,
|
|
254
|
+
message: message(value, routes.length),
|
|
255
|
+
after: routes
|
|
256
|
+
});
|
|
257
|
+
}
|
|
277
258
|
}
|
|
278
|
-
}
|
|
259
|
+
};
|
|
260
|
+
duplicates(
|
|
261
|
+
"duplicate.title",
|
|
262
|
+
"warn",
|
|
263
|
+
group((p) => p.title),
|
|
264
|
+
(title, n) => `${n} pages share the title "${title}".`
|
|
265
|
+
);
|
|
266
|
+
duplicates(
|
|
267
|
+
"duplicate.description",
|
|
268
|
+
"warn",
|
|
269
|
+
group((p) => p.description),
|
|
270
|
+
(_, n) => `${n} pages share the same meta description.`
|
|
271
|
+
);
|
|
272
|
+
duplicates(
|
|
273
|
+
"duplicate.canonical",
|
|
274
|
+
"error",
|
|
275
|
+
group((p) => p.canonical),
|
|
276
|
+
(canonical, n) => `${n} pages canonicalise to ${canonical}.`
|
|
277
|
+
);
|
|
279
278
|
const expectedOrigin = snapshot.site.origin ?? null;
|
|
280
279
|
for (const page of pages) {
|
|
281
280
|
if (!page.canonical) continue;
|
|
@@ -918,33 +917,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
918
917
|
const shown = routes.slice(0, limit).join(", ");
|
|
919
918
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
920
919
|
}
|
|
921
|
-
|
|
920
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
921
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
922
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
923
|
+
function wrapText(text2, width) {
|
|
924
|
+
const lines = [];
|
|
925
|
+
let line = "";
|
|
926
|
+
const flush = () => {
|
|
927
|
+
if (line) lines.push(line);
|
|
928
|
+
line = "";
|
|
929
|
+
};
|
|
930
|
+
for (const word of text2.split(/\s+/)) {
|
|
931
|
+
if (word.length > width) {
|
|
932
|
+
flush();
|
|
933
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (line && line.length + 1 + word.length > width) {
|
|
937
|
+
flush();
|
|
938
|
+
line = word;
|
|
939
|
+
} else {
|
|
940
|
+
line = line ? `${line} ${word}` : word;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
flush();
|
|
944
|
+
return lines;
|
|
945
|
+
}
|
|
946
|
+
function sectionRule(severity, count, width) {
|
|
947
|
+
const label = PLURAL[severity].toUpperCase();
|
|
948
|
+
const tail = String(count);
|
|
949
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
950
|
+
return `${BADGE[severity](pc.bold(label))} ${pc.dim("\u2500".repeat(dashes))} ${pc.dim(tail)}`;
|
|
951
|
+
}
|
|
952
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
953
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
954
|
+
const indent = " ";
|
|
955
|
+
const body = width - indent.length;
|
|
922
956
|
const lines = [
|
|
923
|
-
pc.bold(meta.target
|
|
924
|
-
pc.dim(
|
|
957
|
+
`${pc.bold("pagetrace")} ${pc.dim("\xB7")} ${meta.target}`,
|
|
958
|
+
pc.dim(
|
|
959
|
+
[
|
|
960
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
961
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
962
|
+
meta.generatedAt
|
|
963
|
+
].filter(Boolean).join(" \xB7 ")
|
|
964
|
+
),
|
|
925
965
|
""
|
|
926
966
|
];
|
|
927
967
|
if (groups.length === 0) {
|
|
928
|
-
lines.push(pc.green(
|
|
968
|
+
lines.push(pc.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
929
969
|
return lines.join("\n");
|
|
930
970
|
}
|
|
931
|
-
for (const
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
971
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
972
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
973
|
+
if (inSeverity.length === 0) continue;
|
|
974
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
975
|
+
for (const group of inSeverity) {
|
|
976
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
977
|
+
const headline = group.message;
|
|
978
|
+
const room = width - 2 - scope.length - 1;
|
|
979
|
+
if (headline.length <= room) {
|
|
980
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
981
|
+
lines.push(
|
|
982
|
+
`${BADGE[severity](GLYPH[severity])} ${pc.bold(headline)}${pad}${pc.dim(scope)}`
|
|
983
|
+
);
|
|
984
|
+
} else {
|
|
985
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
986
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${pc.bold(line)}` : `${indent}${pc.bold(line)}`);
|
|
987
|
+
}
|
|
988
|
+
lines.push(`${indent}${pc.dim(scope)}`);
|
|
989
|
+
}
|
|
990
|
+
if (group.detail) {
|
|
991
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${pc.dim(line)}`);
|
|
992
|
+
}
|
|
993
|
+
if (group.fix) {
|
|
994
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
995
|
+
lines.push(`${indent}${pc.cyan("\u2192")} ${first}`);
|
|
996
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
997
|
+
}
|
|
998
|
+
if (group.routes.length > 0) {
|
|
999
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1000
|
+
lines.push(`${indent}${pc.dim(line)}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
}
|
|
938
1005
|
}
|
|
939
1006
|
const { issues, instances, total } = countIssues(groups);
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
);
|
|
1007
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
|
|
1008
|
+
lines.push(pc.dim("\u2500".repeat(width)));
|
|
1009
|
+
lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
943
1010
|
lines.push(
|
|
944
1011
|
pc.dim(
|
|
945
|
-
|
|
1012
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
946
1013
|
)
|
|
947
1014
|
);
|
|
1015
|
+
lines.push("");
|
|
948
1016
|
return lines.join("\n");
|
|
949
1017
|
}
|
|
950
1018
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1537,7 +1605,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1537
1605
|
pageCount: pages.length,
|
|
1538
1606
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1539
1607
|
};
|
|
1540
|
-
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta);
|
|
1608
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1541
1609
|
if (flags.out) {
|
|
1542
1610
|
await writeFile(flags.out, `${output}
|
|
1543
1611
|
`, "utf8");
|
|
@@ -1550,7 +1618,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1550
1618
|
}
|
|
1551
1619
|
});
|
|
1552
1620
|
cli.help();
|
|
1553
|
-
cli.version("0.
|
|
1621
|
+
cli.version("0.7.0");
|
|
1554
1622
|
async function main() {
|
|
1555
1623
|
try {
|
|
1556
1624
|
cli.parse(process.argv, { run: false });
|
package/dist/index.cjs
CHANGED
|
@@ -307,39 +307,38 @@ function auditCrossPage(snapshot) {
|
|
|
307
307
|
}
|
|
308
308
|
return map;
|
|
309
309
|
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (routes.length > 1) {
|
|
323
|
-
findings.push({
|
|
324
|
-
code: "duplicate.description",
|
|
325
|
-
severity: "warn",
|
|
326
|
-
route: null,
|
|
327
|
-
message: `${routes.length} pages share the same meta description.`,
|
|
328
|
-
after: routes
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
for (const [canonical, routes] of group((p) => p.canonical)) {
|
|
333
|
-
if (routes.length > 1) {
|
|
334
|
-
findings.push({
|
|
335
|
-
code: "duplicate.canonical",
|
|
336
|
-
severity: "error",
|
|
337
|
-
route: null,
|
|
338
|
-
message: `${routes.length} pages canonicalise to ${canonical}.`,
|
|
339
|
-
after: routes
|
|
340
|
-
});
|
|
310
|
+
const duplicates = (code, severity, keyed, message) => {
|
|
311
|
+
for (const [value, routes] of keyed) {
|
|
312
|
+
if (routes.length < 2) continue;
|
|
313
|
+
for (const route of routes) {
|
|
314
|
+
findings.push({
|
|
315
|
+
code,
|
|
316
|
+
severity,
|
|
317
|
+
route,
|
|
318
|
+
message: message(value, routes.length),
|
|
319
|
+
after: routes
|
|
320
|
+
});
|
|
321
|
+
}
|
|
341
322
|
}
|
|
342
|
-
}
|
|
323
|
+
};
|
|
324
|
+
duplicates(
|
|
325
|
+
"duplicate.title",
|
|
326
|
+
"warn",
|
|
327
|
+
group((p) => p.title),
|
|
328
|
+
(title, n) => `${n} pages share the title "${title}".`
|
|
329
|
+
);
|
|
330
|
+
duplicates(
|
|
331
|
+
"duplicate.description",
|
|
332
|
+
"warn",
|
|
333
|
+
group((p) => p.description),
|
|
334
|
+
(_, n) => `${n} pages share the same meta description.`
|
|
335
|
+
);
|
|
336
|
+
duplicates(
|
|
337
|
+
"duplicate.canonical",
|
|
338
|
+
"error",
|
|
339
|
+
group((p) => p.canonical),
|
|
340
|
+
(canonical, n) => `${n} pages canonicalise to ${canonical}.`
|
|
341
|
+
);
|
|
343
342
|
const expectedOrigin = snapshot.site.origin ?? null;
|
|
344
343
|
for (const page of pages) {
|
|
345
344
|
if (!page.canonical) continue;
|
|
@@ -1186,33 +1185,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
1186
1185
|
const shown = routes.slice(0, limit).join(", ");
|
|
1187
1186
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
1188
1187
|
}
|
|
1189
|
-
|
|
1188
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
1189
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
1190
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
1191
|
+
function wrapText(text2, width) {
|
|
1192
|
+
const lines = [];
|
|
1193
|
+
let line = "";
|
|
1194
|
+
const flush = () => {
|
|
1195
|
+
if (line) lines.push(line);
|
|
1196
|
+
line = "";
|
|
1197
|
+
};
|
|
1198
|
+
for (const word of text2.split(/\s+/)) {
|
|
1199
|
+
if (word.length > width) {
|
|
1200
|
+
flush();
|
|
1201
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1205
|
+
flush();
|
|
1206
|
+
line = word;
|
|
1207
|
+
} else {
|
|
1208
|
+
line = line ? `${line} ${word}` : word;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
flush();
|
|
1212
|
+
return lines;
|
|
1213
|
+
}
|
|
1214
|
+
function sectionRule(severity, count, width) {
|
|
1215
|
+
const label = PLURAL[severity].toUpperCase();
|
|
1216
|
+
const tail = String(count);
|
|
1217
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
1218
|
+
return `${BADGE[severity](import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\u2500".repeat(dashes))} ${import_picocolors.default.dim(tail)}`;
|
|
1219
|
+
}
|
|
1220
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
1221
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
1222
|
+
const indent = " ";
|
|
1223
|
+
const body = width - indent.length;
|
|
1190
1224
|
const lines = [
|
|
1191
|
-
import_picocolors.default.bold(meta.target
|
|
1192
|
-
import_picocolors.default.dim(
|
|
1225
|
+
`${import_picocolors.default.bold("pagetrace")} ${import_picocolors.default.dim("\xB7")} ${meta.target}`,
|
|
1226
|
+
import_picocolors.default.dim(
|
|
1227
|
+
[
|
|
1228
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
1229
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
1230
|
+
meta.generatedAt
|
|
1231
|
+
].filter(Boolean).join(" \xB7 ")
|
|
1232
|
+
),
|
|
1193
1233
|
""
|
|
1194
1234
|
];
|
|
1195
1235
|
if (groups.length === 0) {
|
|
1196
|
-
lines.push(import_picocolors.default.green(
|
|
1236
|
+
lines.push(import_picocolors.default.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
1197
1237
|
return lines.join("\n");
|
|
1198
1238
|
}
|
|
1199
|
-
for (const
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1239
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
1240
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
1241
|
+
if (inSeverity.length === 0) continue;
|
|
1242
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
1243
|
+
for (const group of inSeverity) {
|
|
1244
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1245
|
+
const headline = group.message;
|
|
1246
|
+
const room = width - 2 - scope.length - 1;
|
|
1247
|
+
if (headline.length <= room) {
|
|
1248
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1249
|
+
lines.push(
|
|
1250
|
+
`${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(headline)}${pad}${import_picocolors.default.dim(scope)}`
|
|
1251
|
+
);
|
|
1252
|
+
} else {
|
|
1253
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1254
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(line)}` : `${indent}${import_picocolors.default.bold(line)}`);
|
|
1255
|
+
}
|
|
1256
|
+
lines.push(`${indent}${import_picocolors.default.dim(scope)}`);
|
|
1257
|
+
}
|
|
1258
|
+
if (group.detail) {
|
|
1259
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (group.fix) {
|
|
1262
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1263
|
+
lines.push(`${indent}${import_picocolors.default.cyan("\u2192")} ${first}`);
|
|
1264
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (group.routes.length > 0) {
|
|
1267
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1268
|
+
lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
lines.push("");
|
|
1272
|
+
}
|
|
1206
1273
|
}
|
|
1207
1274
|
const { issues, instances, total } = countIssues(groups);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
);
|
|
1275
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
|
|
1276
|
+
lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
|
|
1277
|
+
lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
1211
1278
|
lines.push(
|
|
1212
1279
|
import_picocolors.default.dim(
|
|
1213
|
-
|
|
1280
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
1214
1281
|
)
|
|
1215
1282
|
);
|
|
1283
|
+
lines.push("");
|
|
1216
1284
|
return lines.join("\n");
|
|
1217
1285
|
}
|
|
1218
1286
|
function formatAuditMarkdown(groups, meta) {
|
package/dist/index.d.cts
CHANGED
|
@@ -192,7 +192,13 @@ interface AuditMeta {
|
|
|
192
192
|
pageCount: number;
|
|
193
193
|
generatedAt: string;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* The terminal report. Findings are grouped under a rule per severity, prose is
|
|
197
|
+
* wrapped to the terminal rather than running off it, and the affected routes
|
|
198
|
+
* sit on their own line — they are the part you act on, and they used to blend
|
|
199
|
+
* into the surrounding text.
|
|
200
|
+
*/
|
|
201
|
+
declare function formatAuditPretty(groups: Aggregate[], meta: AuditMeta, columns?: number): string;
|
|
196
202
|
declare function formatAuditMarkdown(groups: Aggregate[], meta: AuditMeta): string;
|
|
197
203
|
/** Self-contained HTML report, suitable for handing to a client. */
|
|
198
204
|
declare function formatAuditHtml(groups: Aggregate[], meta: AuditMeta): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -192,7 +192,13 @@ interface AuditMeta {
|
|
|
192
192
|
pageCount: number;
|
|
193
193
|
generatedAt: string;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* The terminal report. Findings are grouped under a rule per severity, prose is
|
|
197
|
+
* wrapped to the terminal rather than running off it, and the affected routes
|
|
198
|
+
* sit on their own line — they are the part you act on, and they used to blend
|
|
199
|
+
* into the surrounding text.
|
|
200
|
+
*/
|
|
201
|
+
declare function formatAuditPretty(groups: Aggregate[], meta: AuditMeta, columns?: number): string;
|
|
196
202
|
declare function formatAuditMarkdown(groups: Aggregate[], meta: AuditMeta): string;
|
|
197
203
|
/** Self-contained HTML report, suitable for handing to a client. */
|
|
198
204
|
declare function formatAuditHtml(groups: Aggregate[], meta: AuditMeta): string;
|
package/dist/index.js
CHANGED
|
@@ -236,39 +236,38 @@ function auditCrossPage(snapshot) {
|
|
|
236
236
|
}
|
|
237
237
|
return map;
|
|
238
238
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (routes.length > 1) {
|
|
252
|
-
findings.push({
|
|
253
|
-
code: "duplicate.description",
|
|
254
|
-
severity: "warn",
|
|
255
|
-
route: null,
|
|
256
|
-
message: `${routes.length} pages share the same meta description.`,
|
|
257
|
-
after: routes
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
for (const [canonical, routes] of group((p) => p.canonical)) {
|
|
262
|
-
if (routes.length > 1) {
|
|
263
|
-
findings.push({
|
|
264
|
-
code: "duplicate.canonical",
|
|
265
|
-
severity: "error",
|
|
266
|
-
route: null,
|
|
267
|
-
message: `${routes.length} pages canonicalise to ${canonical}.`,
|
|
268
|
-
after: routes
|
|
269
|
-
});
|
|
239
|
+
const duplicates = (code, severity, keyed, message) => {
|
|
240
|
+
for (const [value, routes] of keyed) {
|
|
241
|
+
if (routes.length < 2) continue;
|
|
242
|
+
for (const route of routes) {
|
|
243
|
+
findings.push({
|
|
244
|
+
code,
|
|
245
|
+
severity,
|
|
246
|
+
route,
|
|
247
|
+
message: message(value, routes.length),
|
|
248
|
+
after: routes
|
|
249
|
+
});
|
|
250
|
+
}
|
|
270
251
|
}
|
|
271
|
-
}
|
|
252
|
+
};
|
|
253
|
+
duplicates(
|
|
254
|
+
"duplicate.title",
|
|
255
|
+
"warn",
|
|
256
|
+
group((p) => p.title),
|
|
257
|
+
(title, n) => `${n} pages share the title "${title}".`
|
|
258
|
+
);
|
|
259
|
+
duplicates(
|
|
260
|
+
"duplicate.description",
|
|
261
|
+
"warn",
|
|
262
|
+
group((p) => p.description),
|
|
263
|
+
(_, n) => `${n} pages share the same meta description.`
|
|
264
|
+
);
|
|
265
|
+
duplicates(
|
|
266
|
+
"duplicate.canonical",
|
|
267
|
+
"error",
|
|
268
|
+
group((p) => p.canonical),
|
|
269
|
+
(canonical, n) => `${n} pages canonicalise to ${canonical}.`
|
|
270
|
+
);
|
|
272
271
|
const expectedOrigin = snapshot.site.origin ?? null;
|
|
273
272
|
for (const page of pages) {
|
|
274
273
|
if (!page.canonical) continue;
|
|
@@ -1115,33 +1114,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
1115
1114
|
const shown = routes.slice(0, limit).join(", ");
|
|
1116
1115
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
1117
1116
|
}
|
|
1118
|
-
|
|
1117
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
1118
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
1119
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
1120
|
+
function wrapText(text2, width) {
|
|
1121
|
+
const lines = [];
|
|
1122
|
+
let line = "";
|
|
1123
|
+
const flush = () => {
|
|
1124
|
+
if (line) lines.push(line);
|
|
1125
|
+
line = "";
|
|
1126
|
+
};
|
|
1127
|
+
for (const word of text2.split(/\s+/)) {
|
|
1128
|
+
if (word.length > width) {
|
|
1129
|
+
flush();
|
|
1130
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1134
|
+
flush();
|
|
1135
|
+
line = word;
|
|
1136
|
+
} else {
|
|
1137
|
+
line = line ? `${line} ${word}` : word;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
flush();
|
|
1141
|
+
return lines;
|
|
1142
|
+
}
|
|
1143
|
+
function sectionRule(severity, count, width) {
|
|
1144
|
+
const label = PLURAL[severity].toUpperCase();
|
|
1145
|
+
const tail = String(count);
|
|
1146
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
1147
|
+
return `${BADGE[severity](pc.bold(label))} ${pc.dim("\u2500".repeat(dashes))} ${pc.dim(tail)}`;
|
|
1148
|
+
}
|
|
1149
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
1150
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
1151
|
+
const indent = " ";
|
|
1152
|
+
const body = width - indent.length;
|
|
1119
1153
|
const lines = [
|
|
1120
|
-
pc.bold(meta.target
|
|
1121
|
-
pc.dim(
|
|
1154
|
+
`${pc.bold("pagetrace")} ${pc.dim("\xB7")} ${meta.target}`,
|
|
1155
|
+
pc.dim(
|
|
1156
|
+
[
|
|
1157
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
1158
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
1159
|
+
meta.generatedAt
|
|
1160
|
+
].filter(Boolean).join(" \xB7 ")
|
|
1161
|
+
),
|
|
1122
1162
|
""
|
|
1123
1163
|
];
|
|
1124
1164
|
if (groups.length === 0) {
|
|
1125
|
-
lines.push(pc.green(
|
|
1165
|
+
lines.push(pc.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
1126
1166
|
return lines.join("\n");
|
|
1127
1167
|
}
|
|
1128
|
-
for (const
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1168
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
1169
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
1170
|
+
if (inSeverity.length === 0) continue;
|
|
1171
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
1172
|
+
for (const group of inSeverity) {
|
|
1173
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1174
|
+
const headline = group.message;
|
|
1175
|
+
const room = width - 2 - scope.length - 1;
|
|
1176
|
+
if (headline.length <= room) {
|
|
1177
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1178
|
+
lines.push(
|
|
1179
|
+
`${BADGE[severity](GLYPH[severity])} ${pc.bold(headline)}${pad}${pc.dim(scope)}`
|
|
1180
|
+
);
|
|
1181
|
+
} else {
|
|
1182
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1183
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${pc.bold(line)}` : `${indent}${pc.bold(line)}`);
|
|
1184
|
+
}
|
|
1185
|
+
lines.push(`${indent}${pc.dim(scope)}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (group.detail) {
|
|
1188
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${pc.dim(line)}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (group.fix) {
|
|
1191
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1192
|
+
lines.push(`${indent}${pc.cyan("\u2192")} ${first}`);
|
|
1193
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1194
|
+
}
|
|
1195
|
+
if (group.routes.length > 0) {
|
|
1196
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1197
|
+
lines.push(`${indent}${pc.dim(line)}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
lines.push("");
|
|
1201
|
+
}
|
|
1135
1202
|
}
|
|
1136
1203
|
const { issues, instances, total } = countIssues(groups);
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
);
|
|
1204
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
|
|
1205
|
+
lines.push(pc.dim("\u2500".repeat(width)));
|
|
1206
|
+
lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
1140
1207
|
lines.push(
|
|
1141
1208
|
pc.dim(
|
|
1142
|
-
|
|
1209
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
1143
1210
|
)
|
|
1144
1211
|
);
|
|
1212
|
+
lines.push("");
|
|
1145
1213
|
return lines.join("\n");
|
|
1146
1214
|
}
|
|
1147
1215
|
function formatAuditMarkdown(groups, meta) {
|