eaa-kit 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -4
- package/dist/astro/index.d.ts +7 -34
- package/dist/astro/index.js +6 -26
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-CPoZMXGM.js +779 -0
- package/dist/audit-CpXH2Mk8.js +2 -0
- package/dist/{baseline-CV_3lbER.js → baseline-22Y1NWxM.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-DB9CZGnV.js} +17 -27
- package/dist/cli/index.js +336 -142
- package/dist/collect-CFM8gEVv.js +134 -0
- package/dist/command-D8l_oYbV.js +77 -0
- package/dist/component-C3GL1Mnu.js +146 -0
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B4IBKMO8.js +521 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-BiI1Lau_.js} +13 -23
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/frameworks-B4ClIJgE.js +314 -0
- package/dist/frameworks-etFg_O8K.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/html-BGTO3ypW.js +543 -0
- package/dist/{impact-DvgBjupx.js → impact-DZt2oBCP.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DIWDE35F.js} +13 -10
- package/dist/jsdom-4IMzv0eE.js +3 -0
- package/dist/jsdom-DCpGSLfW.js +81 -0
- package/dist/{json-C9xS1PNC.js → json-DjEvy1nX.js} +19 -22
- package/dist/json-QQuFIw1W.js +2 -0
- package/dist/{load-sCkKsvGQ.js → load-5wRGLvub.js} +3 -9
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DSRnXmcd.js → playwright-BWniOain.js} +70 -23
- package/dist/{pool-DixLeu8L.js → pool-BMevaLWD.js} +4 -12
- package/dist/{project-CufCqIE2.js → project-CzOnkLH6.js} +14 -21
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-Dtowi2EC.js +321 -0
- package/dist/{render-BO0nVrrZ.js → render-DrvXRCEn.js} +8 -16
- package/dist/{result-2aZPfM8w.js → result-DoamKFsp.js} +115 -1
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-DB34BSOZ.js +53 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-SR3_lLYd.js} +22 -36
- package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
- package/dist/text-CKKpzkYM.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +36 -9
- package/dist/audit-B2dIKpJ5.js +0 -2
- package/dist/audit-DXpKkXsC.js +0 -950
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/html-BLEuzep6.js +0 -337
- package/dist/jsdom-BEu6Ra_2.js +0 -163
- package/dist/jsdom-C6dIyaxN.js +0 -3
- package/dist/routes-BxbSZKXC.js +0 -123
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { a as plural, n as count, t as collapse } from "./text-CKKpzkYM.js";
|
|
3
|
+
import { a as isShared, c as runCompleteness, i as groupIssues, n as blindRules, r as coverageParts, t as remediationFor } from "./remediation-Dtowi2EC.js";
|
|
4
|
+
import { r as componentPath } from "./component-C3GL1Mnu.js";
|
|
5
|
+
import { i as understandingUrl, n as coverageSummary, r as manualCheckFor, t as buildCoverage } from "./coverage-B4IBKMO8.js";
|
|
6
|
+
import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-D8l_oYbV.js";
|
|
7
|
+
import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-CFM8gEVv.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
//#region src/audit/report/console.ts
|
|
11
|
+
const DEFAULT_MAX_NODES = 3;
|
|
12
|
+
const MIN_WIDTH = 40;
|
|
13
|
+
const MAX_WIDTH = 100;
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable audit report.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not a columnar table: rule ids, help text and selectors are all
|
|
18
|
+
* variable-length, so a real table either wraps into soup or scrolls sideways
|
|
19
|
+
* on a narrow terminal. Everything is left-aligned and indented instead, and
|
|
20
|
+
* every line is assembled from segments trimmed as a group, so the width
|
|
21
|
+
* guarantee holds however long a selector or rule id turns out to be.
|
|
22
|
+
*
|
|
23
|
+
* Returns a string rather than printing, so the format is testable.
|
|
24
|
+
*/
|
|
25
|
+
function formatConsoleReport(audits, options = {}) {
|
|
26
|
+
const ctx = context(options);
|
|
27
|
+
const lines = [
|
|
28
|
+
"",
|
|
29
|
+
...headerLines(audits, ctx),
|
|
30
|
+
""
|
|
31
|
+
];
|
|
32
|
+
lines.push(...issuesSection(audits, ctx));
|
|
33
|
+
if (options.perPage) {
|
|
34
|
+
lines.push("", ...legendLines(ctx), "");
|
|
35
|
+
for (const audit of audits) lines.push(...pageSection(audit, ctx));
|
|
36
|
+
}
|
|
37
|
+
lines.push(...summary(audits, ctx));
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
function context(options) {
|
|
41
|
+
const detected = process.stdout.columns ?? 80;
|
|
42
|
+
const width = Math.min(Math.max(options.width ?? detected, MIN_WIDTH), MAX_WIDTH);
|
|
43
|
+
const c = pc.createColors(options.color ?? pc.isColorSupported);
|
|
44
|
+
const unicode = supportsUnicode();
|
|
45
|
+
return {
|
|
46
|
+
width,
|
|
47
|
+
maxNodes: options.maxNodes ?? DEFAULT_MAX_NODES,
|
|
48
|
+
failOn: options.failOn ?? "serious",
|
|
49
|
+
sourceFor: options.sourceFor ?? (() => void 0),
|
|
50
|
+
componentFor: options.componentFor ?? (() => void 0),
|
|
51
|
+
manual: options.manual ?? false,
|
|
52
|
+
coverage: options.coverage ?? false,
|
|
53
|
+
framework: options.framework,
|
|
54
|
+
completeness: options.completeness,
|
|
55
|
+
c,
|
|
56
|
+
symbol: (kind) => {
|
|
57
|
+
switch (kind) {
|
|
58
|
+
case "violation": return unicode ? "✗" : "x";
|
|
59
|
+
case "review": return "?";
|
|
60
|
+
case "blind": return unicode ? "·" : "-";
|
|
61
|
+
case "clean": return unicode ? "✓" : "+";
|
|
62
|
+
case "error": return "!";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** cmd.exe and older Windows consoles render these glyphs as mojibake. */
|
|
68
|
+
function supportsUnicode() {
|
|
69
|
+
if (process.platform !== "win32") return true;
|
|
70
|
+
return Boolean(process.env["WT_SESSION"] || process.env["TERM_PROGRAM"] || process.env["ConEmuTask"] || process.env["TERM"]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Joins segments into one line, trimming the group to the terminal width.
|
|
74
|
+
* Colour codes are applied after trimming, so they never count towards it.
|
|
75
|
+
*/
|
|
76
|
+
function render(ctx, segments) {
|
|
77
|
+
let remaining = ctx.width;
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const segment of segments) {
|
|
80
|
+
if (remaining <= 0) break;
|
|
81
|
+
const text = segment.text.length <= remaining ? segment.text : `${segment.text.slice(0, Math.max(remaining - 1, 0))}…`;
|
|
82
|
+
remaining -= text.length;
|
|
83
|
+
parts.push(segment.paint ? segment.paint(text) : text);
|
|
84
|
+
}
|
|
85
|
+
return parts.join("");
|
|
86
|
+
}
|
|
87
|
+
/** One line from a single segment, which is what most of this report is. */
|
|
88
|
+
function line(ctx, text, paint) {
|
|
89
|
+
return render(ctx, paint === void 0 ? [{ text }] : [{
|
|
90
|
+
text,
|
|
91
|
+
paint
|
|
92
|
+
}]);
|
|
93
|
+
}
|
|
94
|
+
function headerLines(audits, ctx) {
|
|
95
|
+
const engineLabel = (audits[0]?.engine ?? "jsdom") === "browser" ? "chromium" : "jsdom (browserless)";
|
|
96
|
+
return [render(ctx, [{
|
|
97
|
+
text: "eaa-kit audit",
|
|
98
|
+
paint: ctx.c.bold
|
|
99
|
+
}, {
|
|
100
|
+
text: ` ${count(audits.length, "page")} · ${engineLabel}`,
|
|
101
|
+
paint: ctx.c.dim
|
|
102
|
+
}])];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the per-page counts mean.
|
|
106
|
+
*
|
|
107
|
+
* Only with the per-page listing, which is the only place those words appear:
|
|
108
|
+
* "not applicable" reads like good news unless it is spelled out, and printing
|
|
109
|
+
* the gloss for a section that is not there is noise.
|
|
110
|
+
*/
|
|
111
|
+
function legendLines(ctx) {
|
|
112
|
+
return [line(ctx, "passed = checked and met · not applicable = nothing to check", ctx.c.dim)];
|
|
113
|
+
}
|
|
114
|
+
function pageSection(audit, ctx) {
|
|
115
|
+
const lines = [line(ctx, audit.relativePath, ctx.c.underline)];
|
|
116
|
+
if (audit.error) {
|
|
117
|
+
lines.push(render(ctx, [{
|
|
118
|
+
text: ` ${ctx.symbol("error")} not audited: `,
|
|
119
|
+
paint: ctx.c.red
|
|
120
|
+
}, { text: audit.error }]), "");
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
for (const finding of sortByImpact(audit.violations)) lines.push(...violationLines(finding, ctx));
|
|
124
|
+
for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render(ctx, [
|
|
125
|
+
{
|
|
126
|
+
text: ` ${ctx.symbol("review")} `,
|
|
127
|
+
paint: ctx.c.yellow
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
text: finding.ruleId,
|
|
131
|
+
paint: ctx.c.yellow
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
text: ` needs manual review${criteria(finding)}`,
|
|
135
|
+
paint: ctx.c.dim
|
|
136
|
+
}
|
|
137
|
+
]));
|
|
138
|
+
if (audit.violations.length === 0) {
|
|
139
|
+
const clean = (audit.accepted ?? []).length === 0;
|
|
140
|
+
lines.push(render(ctx, [{
|
|
141
|
+
text: ` ${ctx.symbol("clean")} `,
|
|
142
|
+
paint: clean ? ctx.c.green : ctx.c.dim
|
|
143
|
+
}, {
|
|
144
|
+
text: clean ? "no violations" : "no new violations",
|
|
145
|
+
paint: clean ? ctx.c.green : ctx.c.dim
|
|
146
|
+
}]));
|
|
147
|
+
}
|
|
148
|
+
for (const finding of sortByImpact(audit.accepted ?? [])) {
|
|
149
|
+
const elements = count(finding.nodes.length, "element");
|
|
150
|
+
lines.push(line(ctx, ` · ${finding.ruleId} accepted by the baseline (${elements})`, ctx.c.dim));
|
|
151
|
+
}
|
|
152
|
+
lines.push(coverageLine(audit, ctx));
|
|
153
|
+
lines.push("");
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
function coverageLine(audit, ctx) {
|
|
157
|
+
return line(ctx, ` ${coverageParts(audit).join(" · ")}`, ctx.c.dim);
|
|
158
|
+
}
|
|
159
|
+
function violationLines(finding, ctx) {
|
|
160
|
+
const impact = finding.impact ?? "unknown";
|
|
161
|
+
const lines = [render(ctx, [
|
|
162
|
+
{
|
|
163
|
+
text: ` ${ctx.symbol("violation")} `,
|
|
164
|
+
paint: ctx.c.red
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
text: finding.ruleId,
|
|
168
|
+
paint: ctx.c.bold
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
text: ` ${impact}${criteria(finding)}`,
|
|
172
|
+
paint: ctx.c.dim
|
|
173
|
+
}
|
|
174
|
+
]), line(ctx, ` ${finding.help}`)];
|
|
175
|
+
for (const node of finding.nodes.slice(0, ctx.maxNodes)) {
|
|
176
|
+
lines.push(line(ctx, ` ${node.target.join(" ")}`, ctx.c.cyan));
|
|
177
|
+
lines.push(line(ctx, ` ${collapse(node.html)}`, ctx.c.dim));
|
|
178
|
+
}
|
|
179
|
+
const hidden = finding.nodes.length - ctx.maxNodes;
|
|
180
|
+
if (hidden > 0) lines.push(line(ctx, ` + ${count(hidden, "more element")}`, ctx.c.dim));
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
183
|
+
function summary(audits, ctx) {
|
|
184
|
+
const withViolations = audits.filter((audit) => audit.violations.length > 0);
|
|
185
|
+
const errored = audits.filter((audit) => audit.error);
|
|
186
|
+
const ruleCount = audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
187
|
+
const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
|
|
188
|
+
const reviewCount = countRules(audits, "needs-review");
|
|
189
|
+
const pages = count(audits.length, "page");
|
|
190
|
+
const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
|
|
191
|
+
if (ruleCount === 0) {
|
|
192
|
+
const clean = ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${pages} that were audited.` : ` No violations across ${pages}.`;
|
|
193
|
+
lines.push(line(ctx, clean, ctx.c.green));
|
|
194
|
+
} else {
|
|
195
|
+
lines.push(render(ctx, [{
|
|
196
|
+
text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${pages}`,
|
|
197
|
+
paint: ctx.c.red
|
|
198
|
+
}, {
|
|
199
|
+
text: ` (${count(elementCount, "element")})`,
|
|
200
|
+
paint: ctx.c.dim
|
|
201
|
+
}]));
|
|
202
|
+
lines.push(thresholdLine(audits, ctx));
|
|
203
|
+
}
|
|
204
|
+
if (reviewCount > 0) {
|
|
205
|
+
const verb = reviewCount === 1 ? "needs" : "need";
|
|
206
|
+
lines.push(line(ctx, ` ${count(reviewCount, "rule")} ${verb} manual review`, ctx.c.yellow));
|
|
207
|
+
}
|
|
208
|
+
if (errored.length > 0) lines.push(line(ctx, ` ${count(errored.length, "page")} could not be audited`, ctx.c.red));
|
|
209
|
+
const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
|
|
210
|
+
if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
|
|
211
|
+
lines.push(...blindSection(audits, ctx));
|
|
212
|
+
lines.push(...coverageSection(audits, ctx));
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* How much of the standard this run could reach.
|
|
217
|
+
*
|
|
218
|
+
* One line by default. It is the sentence that stops a clean report reading as
|
|
219
|
+
* a clean site: most of WCAG cannot be checked by any automated engine, and
|
|
220
|
+
* until this was printed the report said so only in prose, in the footer, where
|
|
221
|
+
* it could be read as boilerplate.
|
|
222
|
+
*/
|
|
223
|
+
function coverageSection(audits, ctx) {
|
|
224
|
+
const coverage = buildCoverage(audits);
|
|
225
|
+
const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
|
|
226
|
+
if (coverage.browserWouldAnswer > 0) {
|
|
227
|
+
const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
|
|
228
|
+
lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
|
|
229
|
+
}
|
|
230
|
+
if (!ctx.coverage) {
|
|
231
|
+
lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
|
|
232
|
+
return lines;
|
|
233
|
+
}
|
|
234
|
+
lines.push("", line(ctx, "Coverage", ctx.c.bold));
|
|
235
|
+
for (const criterion of coverage.criteria) {
|
|
236
|
+
const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
|
|
237
|
+
const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
|
|
238
|
+
lines.push(render(ctx, [
|
|
239
|
+
{
|
|
240
|
+
text: ` ${criterion.number} `,
|
|
241
|
+
paint: ctx.c.bold
|
|
242
|
+
},
|
|
243
|
+
{ text: `${criterion.title} (${criterion.level}) — ` },
|
|
244
|
+
{
|
|
245
|
+
text: `${STATUS_WORDS[criterion.status]}${note}`,
|
|
246
|
+
paint
|
|
247
|
+
}
|
|
248
|
+
]));
|
|
249
|
+
}
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
/** What each outcome is called, in words rather than a symbol. */
|
|
253
|
+
const STATUS_WORDS = {
|
|
254
|
+
evaluated: "evaluated here",
|
|
255
|
+
"not-evaluated": "this engine could not evaluate it",
|
|
256
|
+
"nothing-to-check": "rules ran and found nothing on this site to check",
|
|
257
|
+
"no-automated-rule": "no automated rule exists; a person must check it"
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* What the run never looked at.
|
|
261
|
+
*
|
|
262
|
+
* Printed before the counts rather than after them, because it changes how they
|
|
263
|
+
* read: "no violations" means one thing over a whole site and another over the
|
|
264
|
+
* twelve pages of it a crawl managed to fetch before it hit its limit.
|
|
265
|
+
*
|
|
266
|
+
* Pages that errored are left to the summary's own line, which already names
|
|
267
|
+
* them; repeating the number here would read as twice as many.
|
|
268
|
+
*/
|
|
269
|
+
function completenessLines(ctx) {
|
|
270
|
+
const completeness = ctx.completeness;
|
|
271
|
+
if (completeness === void 0 || completeness.complete) return [];
|
|
272
|
+
const lines = [];
|
|
273
|
+
if (completeness.unreachable.length > 0) {
|
|
274
|
+
const noun = plural(completeness.unreachable.length, "page");
|
|
275
|
+
lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and were not audited`, ctx.c.yellow));
|
|
276
|
+
}
|
|
277
|
+
if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
|
|
278
|
+
if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
|
|
279
|
+
return lines;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Why the run passed or failed. Without this, a build that exits 0 while the
|
|
283
|
+
* report lists violations looks like a bug rather than a threshold choice.
|
|
284
|
+
*/
|
|
285
|
+
function thresholdLine(audits, ctx) {
|
|
286
|
+
const failOn = ctx.failOn;
|
|
287
|
+
const failing = countAtOrAbove(audits, failOn);
|
|
288
|
+
if (failing === 0) return line(ctx, ` none at or above ${failOn} (--fail-on ${failOn}), so this run passes`, ctx.c.green);
|
|
289
|
+
return line(ctx, ` ${failing} at or above ${failOn} (--fail-on ${failOn})`, ctx.c.red);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The unevaluated rules are listed once, at the end, rather than repeated under
|
|
293
|
+
* every page: on a large site the same handful recurs on each one, and a wall
|
|
294
|
+
* of "not evaluated" would bury the findings that are real.
|
|
295
|
+
*/
|
|
296
|
+
function blindSection(audits, ctx) {
|
|
297
|
+
const blind = blindRules(audits).sort((a, b) => b.pages - a.pages);
|
|
298
|
+
if (blind.length === 0) return [];
|
|
299
|
+
const lines = [
|
|
300
|
+
"",
|
|
301
|
+
line(ctx, "Not evaluated", ctx.c.bold),
|
|
302
|
+
line(ctx, " This engine reached no verdict on these.", ctx.c.dim),
|
|
303
|
+
line(ctx, " They are never reported as passing.", ctx.c.dim)
|
|
304
|
+
];
|
|
305
|
+
for (const { ruleId, pages, finding } of blind) {
|
|
306
|
+
lines.push(render(ctx, [
|
|
307
|
+
{ text: ` ${ctx.symbol("blind")} ` },
|
|
308
|
+
{ text: ruleId },
|
|
309
|
+
{
|
|
310
|
+
text: ` ${count(pages, "page")}${criteria(finding)}`,
|
|
311
|
+
paint: ctx.c.dim
|
|
312
|
+
}
|
|
313
|
+
]), line(ctx, ` ${finding.reasonDetail}`, ctx.c.dim));
|
|
314
|
+
const manual = manualCheckFor(ruleId);
|
|
315
|
+
if (manual !== void 0 && ctx.manual) {
|
|
316
|
+
for (const wrapped of wrap(manual.check, ctx.width - 8)) lines.push(line(ctx, ` ${wrapped}`, ctx.c.dim));
|
|
317
|
+
if (manual.browserAnswers) lines.push(line(ctx, " or run again with --browser", ctx.c.dim));
|
|
318
|
+
}
|
|
319
|
+
for (const criterion of finding.successCriteria) {
|
|
320
|
+
const url = understandingUrl(criterion);
|
|
321
|
+
if (url !== void 0) lines.push(line(ctx, ` ${criterion}: ${url}`, ctx.c.dim));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!ctx.manual) lines.push(line(ctx, " Run with --manual for what to check by hand.", ctx.c.dim));
|
|
325
|
+
return lines;
|
|
326
|
+
}
|
|
327
|
+
function countRules(audits, reason) {
|
|
328
|
+
const ruleIds = /* @__PURE__ */ new Set();
|
|
329
|
+
for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === reason) ruleIds.add(finding.ruleId);
|
|
330
|
+
return ruleIds.size;
|
|
331
|
+
}
|
|
332
|
+
function sortByImpact(findings) {
|
|
333
|
+
return [...findings].sort((a, b) => impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId));
|
|
334
|
+
}
|
|
335
|
+
function criteria(finding) {
|
|
336
|
+
return finding.successCriteria.length > 0 ? `, WCAG ${finding.successCriteria.join(" ")}` : "";
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* What is actually broken, once per element rather than once per page.
|
|
340
|
+
*
|
|
341
|
+
* The page-by-page listing above is the truth, and on a site built from
|
|
342
|
+
* components it is not the work: one header with a missing `alt` reappears on
|
|
343
|
+
* every page that renders it, and nothing in a per-page report says those are
|
|
344
|
+
* one line in one file. This section says it, and orders the result by what
|
|
345
|
+
* fixing it would buy.
|
|
346
|
+
*/
|
|
347
|
+
function issuesSection(audits, ctx) {
|
|
348
|
+
const issues = groupIssues(audits);
|
|
349
|
+
if (issues.length === 0) return [];
|
|
350
|
+
const elements = issues.reduce((total, issue) => total + issue.elements.length, 0);
|
|
351
|
+
const occurrences = issues.reduce((total, issue) => total + issue.occurrences, 0);
|
|
352
|
+
const lines = [line(ctx, "Issues", ctx.c.bold)];
|
|
353
|
+
const intro = occurrences === elements ? ` ${count(elements, "distinct element")} to fix.` : ` ${count(occurrences, "violation")} across the site come from ${count(elements, "distinct element")}.`;
|
|
354
|
+
lines.push(line(ctx, intro, ctx.c.dim));
|
|
355
|
+
for (const issue of issues) {
|
|
356
|
+
lines.push("");
|
|
357
|
+
lines.push(render(ctx, [
|
|
358
|
+
{ text: ` ${ctx.symbol("violation")} ` },
|
|
359
|
+
{
|
|
360
|
+
text: issue.ruleId,
|
|
361
|
+
paint: ctx.c.bold
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
text: ` ${impactLabel(issue.impact)}`,
|
|
365
|
+
paint: ctx.c.dim
|
|
366
|
+
},
|
|
367
|
+
...issue.successCriteria.length > 0 ? [{
|
|
368
|
+
text: `, WCAG ${issue.successCriteria.join(" ")}`,
|
|
369
|
+
paint: ctx.c.dim
|
|
370
|
+
}] : []
|
|
371
|
+
]));
|
|
372
|
+
lines.push(line(ctx, ` ${issue.help}`, ctx.c.dim));
|
|
373
|
+
lines.push(...remediationLines(issue.ruleId, issue.elements[0]?.html, ctx));
|
|
374
|
+
for (const element of issue.elements.slice(0, ctx.maxNodes)) {
|
|
375
|
+
lines.push(line(ctx, ` ${collapse(element.html)}`));
|
|
376
|
+
lines.push(...whereLines(element, ctx));
|
|
377
|
+
}
|
|
378
|
+
const hidden = issue.elements.length - ctx.maxNodes;
|
|
379
|
+
if (hidden > 0) lines.push(line(ctx, ` …and ${count(hidden, "more element")}`, ctx.c.dim));
|
|
380
|
+
}
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* What to do about the rule, under the finding rather than in a link.
|
|
385
|
+
*
|
|
386
|
+
* axe-core's help text says what is wrong; this says who it stops and what to
|
|
387
|
+
* change. The corrected line is built from the element that actually failed,
|
|
388
|
+
* because a textbook snippet is a second thing to translate before anybody can
|
|
389
|
+
* use it.
|
|
390
|
+
*/
|
|
391
|
+
function remediationLines(ruleId, html, ctx) {
|
|
392
|
+
const remediation = remediationFor(ruleId, ctx.framework);
|
|
393
|
+
if (remediation === void 0) return [];
|
|
394
|
+
const indent = " ";
|
|
395
|
+
const width = ctx.width - 6 - 2;
|
|
396
|
+
const lines = [...wrap(remediation.why, width).map((text) => line(ctx, `${indent}${text}`, ctx.c.dim)), ...wrap(`Fix: ${remediation.fix}`, width).map((text) => line(ctx, `${indent}${text}`))];
|
|
397
|
+
const example = html === void 0 ? void 0 : remediation.example?.(html);
|
|
398
|
+
if (example !== void 0) lines.push(render(ctx, [{
|
|
399
|
+
text: `${indent}→ `,
|
|
400
|
+
paint: ctx.c.green
|
|
401
|
+
}, { text: collapse(example) }]));
|
|
402
|
+
return lines;
|
|
403
|
+
}
|
|
404
|
+
/** Where one element appears, and what that says about where the fix goes. */
|
|
405
|
+
function whereLines(element, ctx) {
|
|
406
|
+
const shown = element.pages.slice(0, ctx.maxNodes);
|
|
407
|
+
const rest = element.pages.length - shown.length;
|
|
408
|
+
const component = ctx.componentFor(element.html);
|
|
409
|
+
const lines = component === void 0 ? [] : [render(ctx, [{
|
|
410
|
+
text: " written in ",
|
|
411
|
+
paint: ctx.c.dim
|
|
412
|
+
}, { text: componentPath(component) }])];
|
|
413
|
+
lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
|
|
414
|
+
for (const page of shown) {
|
|
415
|
+
const source = ctx.sourceFor(page);
|
|
416
|
+
lines.push(render(ctx, [{
|
|
417
|
+
text: ` ${page}`,
|
|
418
|
+
paint: ctx.c.dim
|
|
419
|
+
}, ...source === void 0 ? [] : [{
|
|
420
|
+
text: ` ${source}`,
|
|
421
|
+
paint: ctx.c.dim
|
|
422
|
+
}]]));
|
|
423
|
+
}
|
|
424
|
+
if (rest > 0) lines.push(line(ctx, ` …and ${count(rest, "more page")}`, ctx.c.dim));
|
|
425
|
+
if (isShared(element)) lines.push(line(ctx, " identical on each — likely one shared component", ctx.c.dim));
|
|
426
|
+
return lines;
|
|
427
|
+
}
|
|
428
|
+
/** Wraps a sentence to the terminal, so a paragraph of advice stays readable. */
|
|
429
|
+
function wrap(text, width) {
|
|
430
|
+
const words = text.split(" ");
|
|
431
|
+
const lines = [];
|
|
432
|
+
let line = "";
|
|
433
|
+
for (const word of words) if (line === "") line = word;
|
|
434
|
+
else if (`${line} ${word}`.length <= width) line = `${line} ${word}`;
|
|
435
|
+
else {
|
|
436
|
+
lines.push(line);
|
|
437
|
+
line = word;
|
|
438
|
+
}
|
|
439
|
+
if (line !== "") lines.push(line);
|
|
440
|
+
return lines;
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/cli/pages.ts
|
|
444
|
+
/**
|
|
445
|
+
* Collect the pages to audit, reporting to stderr on the way.
|
|
446
|
+
*
|
|
447
|
+
* Returns undefined when there is nothing to audit, having already explained
|
|
448
|
+
* why. Every caller turns that into exit 2 — a run that reached no verdict,
|
|
449
|
+
* which is not the same as a clean one.
|
|
450
|
+
*/
|
|
451
|
+
async function resolvePages(directory, options = {}) {
|
|
452
|
+
if (directory === void 0 && options.url === void 0) return resolveAutomatically(options);
|
|
453
|
+
if (options.url !== void 0) {
|
|
454
|
+
const crawled = await crawlPages(options.url, options);
|
|
455
|
+
if (!crawled) return void 0;
|
|
456
|
+
if (crawled.pages.length === 0) {
|
|
457
|
+
warn(`No pages could be fetched from ${options.url}`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
pages: crawled.pages,
|
|
462
|
+
origin: crawled.origin,
|
|
463
|
+
label: options.url,
|
|
464
|
+
completeness: crawled.completeness
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const cwd = options.cwd ?? process.cwd();
|
|
468
|
+
const shown = options.label ?? directory;
|
|
469
|
+
const unreachable = [];
|
|
470
|
+
let pages;
|
|
471
|
+
try {
|
|
472
|
+
pages = await collectPages(directory, {
|
|
473
|
+
...options.include ? { include: options.include } : {},
|
|
474
|
+
...options.exclude ? { exclude: options.exclude } : {},
|
|
475
|
+
onUnreadable: (relativePath, reason) => unreachable.push({
|
|
476
|
+
location: relativePath,
|
|
477
|
+
reason
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
} catch (cause) {
|
|
481
|
+
if (!(cause instanceof BuildDirectoryError)) throw cause;
|
|
482
|
+
fail(cause.message);
|
|
483
|
+
note(await emptyDirectoryHint(shown, cwd));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (pages.length === 0) {
|
|
487
|
+
warn(await emptyDirectoryHint(shown, cwd));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (unreachable.length > 0) {
|
|
491
|
+
const verb = unreachable.length === 1 ? "file was" : "files were";
|
|
492
|
+
warn(`${unreachable.length} ${verb} not readable, and so not audited:`);
|
|
493
|
+
for (const file of unreachable.slice(0, 10)) note(` ${file.location} — ${file.reason}`);
|
|
494
|
+
if (unreachable.length > 10) note(` …and ${unreachable.length - 10} more`);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
pages,
|
|
498
|
+
label: shown,
|
|
499
|
+
completeness: {
|
|
500
|
+
discovery: "directory",
|
|
501
|
+
collected: pages.length,
|
|
502
|
+
unreachable,
|
|
503
|
+
truncated: false
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Fetch the pages of a running site, reporting what happened on the way.
|
|
509
|
+
*
|
|
510
|
+
* Returns undefined when the crawl could not start, which the caller turns into
|
|
511
|
+
* exit 2 — a run that reached no verdict, not a clean one.
|
|
512
|
+
*/
|
|
513
|
+
async function crawlPages(url, options) {
|
|
514
|
+
const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BiI1Lau_.js");
|
|
515
|
+
let entry;
|
|
516
|
+
try {
|
|
517
|
+
entry = parseEntryUrl(url, options.allowRemote ?? false);
|
|
518
|
+
} catch (cause) {
|
|
519
|
+
if (cause instanceof CrawlError) {
|
|
520
|
+
fail(cause.message);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
throw cause;
|
|
524
|
+
}
|
|
525
|
+
note(`Crawling ${entry.origin}…`);
|
|
526
|
+
const result = await crawlSite(entry, {
|
|
527
|
+
...options.allowRemote ? { allowRemote: true } : {},
|
|
528
|
+
...options.ignoreRobots ? { ignoreRobots: true } : {},
|
|
529
|
+
...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
|
|
530
|
+
...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
|
|
531
|
+
...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
|
|
532
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
533
|
+
});
|
|
534
|
+
if (result.pages.length === 0 && result.failures.length > 0) {
|
|
535
|
+
fail(`Could not fetch ${entry.href} (${result.failures[0]?.reason})`);
|
|
536
|
+
note(" Is the site running at that address?");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const found = result.discovery === "sitemap" ? "sitemap.xml and links" : "links";
|
|
540
|
+
note(`Found ${count(result.pages.length, "page")} from ${found}`);
|
|
541
|
+
if (result.failures.length > 0) {
|
|
542
|
+
const verb = result.failures.length === 1 ? "URL was" : "URLs were";
|
|
543
|
+
warn(`${result.failures.length} ${verb} not fetched, and so not audited:`);
|
|
544
|
+
for (const failure of result.failures.slice(0, 10)) note(` ${failure.url} — ${failure.reason}`);
|
|
545
|
+
if (result.failures.length > 10) note(` …and ${result.failures.length - 10} more`);
|
|
546
|
+
}
|
|
547
|
+
if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
|
|
548
|
+
return {
|
|
549
|
+
pages: result.pages,
|
|
550
|
+
origin: result.origin,
|
|
551
|
+
completeness: {
|
|
552
|
+
discovery: result.discovery,
|
|
553
|
+
collected: result.pages.length,
|
|
554
|
+
unreachable: result.failures.map((failure) => ({
|
|
555
|
+
location: failure.url,
|
|
556
|
+
reason: failure.reason
|
|
557
|
+
})),
|
|
558
|
+
truncated: result.truncated
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* No directory and no URL: work out what this project needs.
|
|
564
|
+
*
|
|
565
|
+
* The point is that `eaa-kit audit` on its own does something useful. Anything
|
|
566
|
+
* this starts is handed back as `cleanup` so the caller can stop it once the
|
|
567
|
+
* report is written.
|
|
568
|
+
*/
|
|
569
|
+
async function resolveAutomatically(options) {
|
|
570
|
+
const cwd = options.cwd ?? process.cwd();
|
|
571
|
+
const { autoDetectSource } = await import("./project-MFrXcw1M.js");
|
|
572
|
+
const detected = await autoDetectSource(cwd, {
|
|
573
|
+
...options.noBuild ? { noBuild: true } : {},
|
|
574
|
+
onStep: note
|
|
575
|
+
});
|
|
576
|
+
if (detected?.directory !== void 0) return resolvePages(detected.directory, {
|
|
577
|
+
...options,
|
|
578
|
+
label: path.relative(cwd, detected.directory) || "."
|
|
579
|
+
});
|
|
580
|
+
if (detected?.url !== void 0) {
|
|
581
|
+
const resolved = await resolvePages(void 0, {
|
|
582
|
+
...options,
|
|
583
|
+
url: detected.url
|
|
584
|
+
});
|
|
585
|
+
if (resolved === void 0) {
|
|
586
|
+
await detected.cleanup?.();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
...resolved,
|
|
591
|
+
...detected.cleanup ? { cleanup: detected.cleanup } : {}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
await detected?.cleanup?.();
|
|
595
|
+
warn(await emptyDirectoryHint("./dist", cwd));
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/cli/audit.ts
|
|
599
|
+
const OUTPUT_FORMATS = [
|
|
600
|
+
"console",
|
|
601
|
+
"json",
|
|
602
|
+
"sarif",
|
|
603
|
+
"html"
|
|
604
|
+
];
|
|
605
|
+
/**
|
|
606
|
+
* `eaa-kit audit [dir]`.
|
|
607
|
+
*
|
|
608
|
+
* Writes progress to stderr and the report to stdout, so the report can be
|
|
609
|
+
* piped somewhere without the chatter coming along.
|
|
610
|
+
*/
|
|
611
|
+
async function runAuditCommand(dir, options = {}) {
|
|
612
|
+
const resolved = await resolvePages(dir, options);
|
|
613
|
+
if (!resolved) return {
|
|
614
|
+
audits: [],
|
|
615
|
+
exitCode: 2
|
|
616
|
+
};
|
|
617
|
+
const { pages, origin, label, cleanup, completeness: collection } = resolved;
|
|
618
|
+
try {
|
|
619
|
+
note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
|
|
620
|
+
const baseUrl = options.baseUrl ?? origin;
|
|
621
|
+
let audits = await runEngine(pages, {
|
|
622
|
+
cwd: options.cwd ?? process.cwd(),
|
|
623
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
624
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
625
|
+
...options.browser ? { browser: true } : {},
|
|
626
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
627
|
+
...options.url === void 0 && dir !== void 0 ? { directory: dir } : {}
|
|
628
|
+
});
|
|
629
|
+
if (!audits) return {
|
|
630
|
+
audits: [],
|
|
631
|
+
exitCode: 2
|
|
632
|
+
};
|
|
633
|
+
const failOn = options.failOn ?? "serious";
|
|
634
|
+
if (options.baseline) {
|
|
635
|
+
const applied = await acceptBaseline(audits, options);
|
|
636
|
+
if (!applied) return {
|
|
637
|
+
audits,
|
|
638
|
+
exitCode: 2
|
|
639
|
+
};
|
|
640
|
+
audits = applied;
|
|
641
|
+
}
|
|
642
|
+
const completeness = runCompleteness(audits, collection);
|
|
643
|
+
await emit(audits, label, failOn, completeness, options);
|
|
644
|
+
const unaudited = audits.filter((audit) => audit.error);
|
|
645
|
+
if (unaudited.length > 0) {
|
|
646
|
+
fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
|
|
647
|
+
return {
|
|
648
|
+
audits,
|
|
649
|
+
exitCode: 2
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
audits,
|
|
654
|
+
exitCode: countAtOrAbove(audits, failOn) > 0 ? 1 : 0
|
|
655
|
+
};
|
|
656
|
+
} finally {
|
|
657
|
+
await cleanup?.();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Move the violations the baseline accounts for out of the failing set.
|
|
662
|
+
*
|
|
663
|
+
* Returns undefined when the baseline could not be read, which the caller
|
|
664
|
+
* turns into exit 2: a run asked to use a baseline it cannot find has not
|
|
665
|
+
* measured what it was told to measure, and silently failing on everything
|
|
666
|
+
* would be as wrong as silently passing.
|
|
667
|
+
*/
|
|
668
|
+
async function acceptBaseline(audits, options) {
|
|
669
|
+
const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-22Y1NWxM.js");
|
|
670
|
+
try {
|
|
671
|
+
const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
|
|
672
|
+
if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
|
|
673
|
+
if (outcome.stale.length > 0) {
|
|
674
|
+
const stale = outcome.stale.length;
|
|
675
|
+
note(`${stale} baseline ${stale === 1 ? "entry no longer matches" : "entries no longer match"} and can be removed`);
|
|
676
|
+
}
|
|
677
|
+
if (outcome.expired.length > 0) advise(`${outcome.expired.length} baseline entries have expired and no longer suppress anything`);
|
|
678
|
+
return outcome.audits;
|
|
679
|
+
} catch (cause) {
|
|
680
|
+
if (cause instanceof BaselineError) {
|
|
681
|
+
fail(cause.message);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
throw cause;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* What the progress line says about the engine.
|
|
689
|
+
*
|
|
690
|
+
* The thread count is on it because it is the difference between a run that
|
|
691
|
+
* looks stalled and one that is working, and because a user comparing two
|
|
692
|
+
* timings deserves to know which one used the machine.
|
|
693
|
+
*/
|
|
694
|
+
async function describeEngine(pages, options) {
|
|
695
|
+
if (options.browser) return " with Chromium";
|
|
696
|
+
const { plannedWorkers } = await import("./pool-BMevaLWD.js");
|
|
697
|
+
const workers = options.concurrency ?? plannedWorkers(pages);
|
|
698
|
+
return workers > 1 ? ` across ${workers} threads` : "";
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Emit the chosen format, to a file when --output is given and to stdout
|
|
702
|
+
* otherwise. Colour is dropped when writing to a file, since escape codes in a
|
|
703
|
+
* saved report are noise.
|
|
704
|
+
*/
|
|
705
|
+
async function emit(audits, dir, failOn, completeness, options) {
|
|
706
|
+
const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
|
|
707
|
+
await emitDocument(body, options.output, options.cwd ?? process.cwd());
|
|
708
|
+
if (options.output !== void 0) note(`Report written to ${options.output}`);
|
|
709
|
+
}
|
|
710
|
+
async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
|
|
711
|
+
switch (format) {
|
|
712
|
+
case "json": {
|
|
713
|
+
const { buildJsonReport, serialiseJsonReport } = await import("./json-QQuFIw1W.js");
|
|
714
|
+
return serialiseJsonReport(buildJsonReport(audits, {
|
|
715
|
+
directory: dir,
|
|
716
|
+
...options.url === void 0 ? {} : { sourceKind: "url" },
|
|
717
|
+
failOn,
|
|
718
|
+
completeness,
|
|
719
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
case "sarif": {
|
|
723
|
+
const { buildSarifReport, serialiseSarifReport } = await import("./sarif-SR3_lLYd.js");
|
|
724
|
+
return serialiseSarifReport(buildSarifReport(audits, {
|
|
725
|
+
directory: dir,
|
|
726
|
+
completeness
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
case "html": {
|
|
730
|
+
const { buildHtmlReport } = await import("./html-BGTO3ypW.js");
|
|
731
|
+
const framework = await detectedFramework(options.cwd ?? process.cwd());
|
|
732
|
+
return buildHtmlReport(audits, {
|
|
733
|
+
...await sourceLookups(options.cwd ?? process.cwd()),
|
|
734
|
+
directory: dir,
|
|
735
|
+
failOn,
|
|
736
|
+
completeness,
|
|
737
|
+
...framework === void 0 ? {} : { framework },
|
|
738
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
case "console": {
|
|
742
|
+
const framework = await detectedFramework(options.cwd ?? process.cwd());
|
|
743
|
+
return `${formatConsoleReport(audits, {
|
|
744
|
+
...await sourceLookups(options.cwd ?? process.cwd()),
|
|
745
|
+
dir,
|
|
746
|
+
failOn,
|
|
747
|
+
completeness,
|
|
748
|
+
...options.perPage ? { perPage: true } : {},
|
|
749
|
+
...options.manual ? { manual: true } : {},
|
|
750
|
+
...options.coverage ? { coverage: true } : {},
|
|
751
|
+
...framework === void 0 ? {} : { framework },
|
|
752
|
+
...toFile ? { color: false } : {}
|
|
753
|
+
})}\n`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Where a page and a failing element were written, for the two reports that say
|
|
759
|
+
* so. Best-effort: a project using no convention this recognises gets the
|
|
760
|
+
* report it always got, with no source named.
|
|
761
|
+
*/
|
|
762
|
+
/** The registry id of whatever built this project, for framework-shaped advice. */
|
|
763
|
+
async function detectedFramework(cwd) {
|
|
764
|
+
const { detectFramework } = await import("./frameworks-etFg_O8K.js");
|
|
765
|
+
const { readPackageJson } = await import("./project-MFrXcw1M.js");
|
|
766
|
+
return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
|
|
767
|
+
}
|
|
768
|
+
async function sourceLookups(cwd) {
|
|
769
|
+
const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
|
|
770
|
+
const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
|
|
771
|
+
const routes = await buildRouteMap(cwd);
|
|
772
|
+
const components = await buildComponentIndex(cwd);
|
|
773
|
+
return {
|
|
774
|
+
sourceFor: (page) => sourceFor(routes, page),
|
|
775
|
+
componentFor: (html) => componentFor(components, html)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
|