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
package/dist/escape-Dm1o_RAk.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
//#region src/escape.ts
|
|
2
|
-
/**
|
|
3
|
-
* HTML escaping, shared by everything in this package that writes HTML.
|
|
4
|
-
*
|
|
5
|
-
* Both documents eaa-kit produces embed text it did not write: an issue
|
|
6
|
-
* description from a config file, axe-core's help text, and — in the audit
|
|
7
|
-
* report — the markup of the element that failed, which is by definition
|
|
8
|
-
* arbitrary HTML from somebody's build. Getting this wrong in the report would
|
|
9
|
-
* mean a page that fails an accessibility audit for having a stray `<script>`
|
|
10
|
-
* hands that script to whoever opens the report.
|
|
11
|
-
*/
|
|
12
|
-
/** For text nodes. Leaves quotes alone, which are fine between tags. */
|
|
13
|
-
function escapeText(value) {
|
|
14
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
15
|
-
}
|
|
16
|
-
/** For attribute values, where a quote would end the attribute. */
|
|
17
|
-
function escapeAttribute(value) {
|
|
18
|
-
return escapeText(value).replace(/"/g, """).replace(/'/g, "'");
|
|
19
|
-
}
|
|
20
|
-
//#endregion
|
|
21
|
-
export { escapeText as n, escapeAttribute as t };
|
package/dist/html-BLEuzep6.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { i as isImpactLevel, r as countAtOrAbove } from "./impact-DvgBjupx.js";
|
|
2
|
-
import { n as escapeText, t as escapeAttribute } from "./escape-Dm1o_RAk.js";
|
|
3
|
-
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
-
import axe from "axe-core";
|
|
5
|
-
//#region src/audit/report/html.ts
|
|
6
|
-
/**
|
|
7
|
-
* A standalone HTML audit report.
|
|
8
|
-
*
|
|
9
|
-
* The console report is for the person who ran the command; JSON and SARIF are
|
|
10
|
-
* for other programs. This one is for somebody who was not at the terminal —
|
|
11
|
-
* the client whose site it is, or the colleague who has to fix it — so it is a
|
|
12
|
-
* single file that can be attached to an email and opened, with no server, no
|
|
13
|
-
* assets and no scripts.
|
|
14
|
-
*
|
|
15
|
-
* It says exactly what the console report says, in the same order and with the
|
|
16
|
-
* same refusals: the four result categories stay apart, rules this engine could
|
|
17
|
-
* not evaluate are named rather than quietly dropped, and nothing here adds up
|
|
18
|
-
* to a compliance claim. A report with no findings means no findings were
|
|
19
|
-
* found, which is not the same as a site being accessible, and the document
|
|
20
|
-
* says so in its own footer rather than leaving the reader to infer it.
|
|
21
|
-
*/
|
|
22
|
-
/** Elements listed per rule before the rest are summarised. */
|
|
23
|
-
const MAX_NODES = 5;
|
|
24
|
-
/** Longest element markup shown before it is truncated. */
|
|
25
|
-
const MAX_SNIPPET = 200;
|
|
26
|
-
function buildHtmlReport(audits, options) {
|
|
27
|
-
const engine = audits[0]?.engine ?? "jsdom";
|
|
28
|
-
const failing = countAtOrAbove(audits, options.failOn);
|
|
29
|
-
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
30
|
-
const title = `Accessibility audit · ${options.directory}`;
|
|
31
|
-
return `<!doctype html>
|
|
32
|
-
<html lang="en">
|
|
33
|
-
<head>
|
|
34
|
-
<meta charset="utf-8">
|
|
35
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
-
<meta name="generator" content="eaa-kit ${escapeAttribute(TOOL_VERSION)}">
|
|
37
|
-
<title>${escapeText(title)}</title>
|
|
38
|
-
<style>
|
|
39
|
-
${STYLES}
|
|
40
|
-
</style>
|
|
41
|
-
</head>
|
|
42
|
-
<body>
|
|
43
|
-
<main>
|
|
44
|
-
<h1>Accessibility audit</h1>
|
|
45
|
-
${verdict(audits, failing, options)}
|
|
46
|
-
${runDetails(audits, engine, generatedAt, options)}
|
|
47
|
-
${summary(audits, failing, options)}
|
|
48
|
-
${pages(audits)}
|
|
49
|
-
${notEvaluated(audits)}
|
|
50
|
-
${footer()}
|
|
51
|
-
</main>
|
|
52
|
-
</body>
|
|
53
|
-
</html>
|
|
54
|
-
`;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* The result, in words, before any of the detail.
|
|
58
|
-
*
|
|
59
|
-
* Not colour alone: the badge carries the word as well, because a reader who
|
|
60
|
-
* cannot distinguish the two shades still has to be able to read the outcome —
|
|
61
|
-
* which would be an embarrassing thing for this document in particular to get
|
|
62
|
-
* wrong.
|
|
63
|
-
*/
|
|
64
|
-
function verdict(audits, failing, options) {
|
|
65
|
-
const unaudited = audits.filter((audit) => audit.error).length;
|
|
66
|
-
if (unaudited > 0) return banner("broken", "Could not finish", `${count(unaudited, "page")} could not be audited, so this run reached no verdict.`);
|
|
67
|
-
if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
|
|
68
|
-
const below = totalViolations(audits) - failing;
|
|
69
|
-
if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
|
|
70
|
-
return banner("pass", "No violations found", "Automated testing found nothing to report.");
|
|
71
|
-
}
|
|
72
|
-
function banner(kind, heading, detail) {
|
|
73
|
-
return `<p class="verdict ${escapeAttribute(kind)}"><strong>${escapeText(heading)}</strong> ${detail}</p>`;
|
|
74
|
-
}
|
|
75
|
-
function runDetails(audits, engine, generatedAt, options) {
|
|
76
|
-
const rows = [
|
|
77
|
-
["Directory", options.directory],
|
|
78
|
-
["Pages", String(audits.length)],
|
|
79
|
-
["Engine", engine === "browser" ? "Chromium" : "jsdom (browserless)"],
|
|
80
|
-
["Threshold", `${options.failOn} and above fails the run`],
|
|
81
|
-
["Generated", generatedAt],
|
|
82
|
-
["eaa-kit", `${TOOL_VERSION} · axe-core ${axe.version}`]
|
|
83
|
-
];
|
|
84
|
-
if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
|
|
85
|
-
return `<h2>The run</h2>\n<dl class="run">\n${rows.map(([term, value]) => ` <div><dt>${escapeText(term)}</dt><dd>${escapeText(value)}</dd></div>`).join("\n")}\n</dl>`;
|
|
86
|
-
}
|
|
87
|
-
function summary(audits, failing, options) {
|
|
88
|
-
const byImpact = /* @__PURE__ */ new Map();
|
|
89
|
-
let needsReview = 0;
|
|
90
|
-
let blind = 0;
|
|
91
|
-
let passes = 0;
|
|
92
|
-
let inapplicable = 0;
|
|
93
|
-
let elements = 0;
|
|
94
|
-
let accepted = 0;
|
|
95
|
-
for (const audit of audits) {
|
|
96
|
-
for (const finding of audit.accepted ?? []) accepted += finding.nodes.length;
|
|
97
|
-
for (const finding of audit.violations) {
|
|
98
|
-
const impact = finding.impact && isImpactLevel(finding.impact) ? finding.impact : "unclassified";
|
|
99
|
-
byImpact.set(impact, (byImpact.get(impact) ?? 0) + 1);
|
|
100
|
-
elements += finding.nodes.length;
|
|
101
|
-
}
|
|
102
|
-
for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind += 1;
|
|
103
|
-
else needsReview += 1;
|
|
104
|
-
passes += audit.passes.length;
|
|
105
|
-
inapplicable += audit.inapplicable.length;
|
|
106
|
-
}
|
|
107
|
-
const impacts = [
|
|
108
|
-
"critical",
|
|
109
|
-
"serious",
|
|
110
|
-
"moderate",
|
|
111
|
-
"minor",
|
|
112
|
-
"unclassified"
|
|
113
|
-
].filter((impact) => (byImpact.get(impact) ?? 0) > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${byImpact.get(impact)}</li>`).join("\n");
|
|
114
|
-
const withViolations = audits.filter((audit) => audit.violations.length > 0).length;
|
|
115
|
-
return `<h2>Summary</h2>
|
|
116
|
-
<ul class="counts">
|
|
117
|
-
<li><strong>${count(totalViolations(audits), "violation")}</strong> on ${withViolations} of ${count(audits.length, "page")}, across ${count(elements, "element")}</li>
|
|
118
|
-
<li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
119
|
-
<li><strong>${needsReview}</strong> ${needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
120
|
-
<li><strong>${blind}</strong> ${blind === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
|
|
121
|
-
${accepted > 0 ? ` <li><strong>${accepted}</strong> ${accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
122
|
-
</ul>
|
|
123
|
-
${impacts ? `<ul class="impacts">\n${impacts}\n</ul>` : ""}
|
|
124
|
-
<p class="note">
|
|
125
|
-
<strong>${passes}</strong> rule results were checked and met, and <strong>${inapplicable}</strong>
|
|
126
|
-
found nothing on the page to check. Those two are counted separately and never added
|
|
127
|
-
together: a rule with nothing to check is not a rule that passed, and a page with no
|
|
128
|
-
images proves nothing about image alternatives.
|
|
129
|
-
</p>`;
|
|
130
|
-
}
|
|
131
|
-
function pages(audits) {
|
|
132
|
-
return `<h2>Pages</h2>\n${audits.map(pageSection).join("\n")}`;
|
|
133
|
-
}
|
|
134
|
-
function pageSection(audit) {
|
|
135
|
-
const heading = `<h3 class="page">${escapeText(audit.relativePath)}</h3>`;
|
|
136
|
-
if (audit.error) return `${heading}
|
|
137
|
-
<p class="verdict broken"><strong>Not audited</strong> ${escapeText(audit.error)}</p>`;
|
|
138
|
-
const parts = [heading];
|
|
139
|
-
if (audit.violations.length === 0) parts.push((audit.accepted ?? []).length === 0 ? "<p class=\"clean\">No violations.</p>" : "<p class=\"coverage\">No new violations.</p>");
|
|
140
|
-
else {
|
|
141
|
-
const findings = [...audit.violations].sort(byImpactThenRule).map(violation).join("\n");
|
|
142
|
-
parts.push(`<ol class="findings">\n${findings}\n</ol>`);
|
|
143
|
-
}
|
|
144
|
-
const review = audit.incomplete.filter((finding) => finding.reason === "needs-review");
|
|
145
|
-
if (review.length > 0) {
|
|
146
|
-
const items = review.map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> needs manual review${standards(finding)}</li>`).join("\n");
|
|
147
|
-
parts.push(`<p class="review-heading">A human has to decide these:</p>\n<ul>\n${items}\n</ul>`);
|
|
148
|
-
}
|
|
149
|
-
const accepted = audit.accepted ?? [];
|
|
150
|
-
if (accepted.length > 0) {
|
|
151
|
-
const items = [...accepted].sort(byImpactThenRule).map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> — ${escapeText(finding.help)} (${count(finding.nodes.length, "element")})</li>`).join("\n");
|
|
152
|
-
parts.push(`<p class="accepted-heading">Accepted by the baseline. Still violations, and not counted above:</p>\n<ul class="accepted">\n${items}\n</ul>`);
|
|
153
|
-
}
|
|
154
|
-
parts.push(coverage(audit));
|
|
155
|
-
return parts.join("\n");
|
|
156
|
-
}
|
|
157
|
-
function violation(finding) {
|
|
158
|
-
const impact = finding.impact && isImpactLevel(finding.impact) ? finding.impact : "unclassified";
|
|
159
|
-
const shown = finding.nodes.slice(0, MAX_NODES);
|
|
160
|
-
const remaining = finding.nodes.length - shown.length;
|
|
161
|
-
const nodes = shown.map((node) => ` <li>
|
|
162
|
-
<code class="selector">${escapeText(node.target.join(" "))}</code>
|
|
163
|
-
<pre><code>${escapeText(snippet(node.html))}</code></pre>
|
|
164
|
-
</li>`).join("\n");
|
|
165
|
-
const more = remaining > 0 ? `\n <li class="more">and ${count(remaining, "more element")}</li>` : "";
|
|
166
|
-
return ` <li class="finding">
|
|
167
|
-
<p class="rule">
|
|
168
|
-
<span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span>
|
|
169
|
-
<code>${escapeText(finding.ruleId)}</code>
|
|
170
|
-
<a href="${escapeAttribute(finding.helpUrl)}">${escapeText(finding.help)}</a>
|
|
171
|
-
</p>
|
|
172
|
-
<p class="standards">${standardsText(finding) || "No mapped success criterion"}</p>
|
|
173
|
-
<ul class="nodes">
|
|
174
|
-
${nodes}${more}
|
|
175
|
-
</ul>
|
|
176
|
-
</li>`;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* What this page's result rests on.
|
|
180
|
-
*
|
|
181
|
-
* The four counts stay apart for the same reason they do everywhere else: only
|
|
182
|
-
* `passed` is evidence that anything was met here.
|
|
183
|
-
*/
|
|
184
|
-
function coverage(audit) {
|
|
185
|
-
const blind = audit.incomplete.filter((finding) => finding.reason === "engine-limitation").length;
|
|
186
|
-
const review = audit.incomplete.length - blind;
|
|
187
|
-
const parts = [`${audit.passes.length} passed`, `${audit.inapplicable.length} not applicable`];
|
|
188
|
-
if (review > 0) parts.push(`${review} to review`);
|
|
189
|
-
if (blind > 0) parts.push(`${blind} not evaluated`);
|
|
190
|
-
return `<p class="coverage">${escapeText(parts.join(" · "))}</p>`;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Rules this engine reached no verdict on, listed once at the end.
|
|
194
|
-
*
|
|
195
|
-
* They are never reported as passing, and leaving them out entirely would let
|
|
196
|
-
* the reader take the rest of the document for full coverage.
|
|
197
|
-
*/
|
|
198
|
-
function notEvaluated(audits) {
|
|
199
|
-
const byRule = /* @__PURE__ */ new Map();
|
|
200
|
-
for (const audit of audits) for (const finding of audit.incomplete) {
|
|
201
|
-
if (finding.reason !== "engine-limitation") continue;
|
|
202
|
-
const existing = byRule.get(finding.ruleId);
|
|
203
|
-
if (existing) existing.pages += 1;
|
|
204
|
-
else byRule.set(finding.ruleId, {
|
|
205
|
-
pages: 1,
|
|
206
|
-
detail: finding.reasonDetail,
|
|
207
|
-
finding
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if (byRule.size === 0) return "";
|
|
211
|
-
return `<h2>Not evaluated</h2>
|
|
212
|
-
<p>This engine reached no verdict on these rules. They are never reported as passing.</p>
|
|
213
|
-
<ul class="not-evaluated">
|
|
214
|
-
${[...byRule.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ruleId, entry]) => ` <li>
|
|
215
|
-
<code>${escapeText(ruleId)}</code> on ${count(entry.pages, "page")}${standards(entry.finding)}
|
|
216
|
-
<span class="reason">${escapeText(entry.detail)}</span>
|
|
217
|
-
</li>`).join("\n")}
|
|
218
|
-
</ul>`;
|
|
219
|
-
}
|
|
220
|
-
function footer() {
|
|
221
|
-
return `<hr>
|
|
222
|
-
<footer>
|
|
223
|
-
<p>
|
|
224
|
-
Generated with <a href="https://github.com/likeBloodMoon/eaa-kit">eaa-kit</a>. Automated
|
|
225
|
-
testing finds a minority of accessibility barriers: it cannot judge whether alternative
|
|
226
|
-
text is accurate, whether a page makes sense in reading order, or whether a form can
|
|
227
|
-
actually be completed with a screen reader.
|
|
228
|
-
</p>
|
|
229
|
-
<p>
|
|
230
|
-
<strong>A report with no findings is not a compliance statement.</strong> It means
|
|
231
|
-
nothing was found by this engine, which is not the same as a site being accessible.
|
|
232
|
-
</p>
|
|
233
|
-
</footer>`;
|
|
234
|
-
}
|
|
235
|
-
function standards(finding) {
|
|
236
|
-
const text = standardsText(finding);
|
|
237
|
-
return text ? ` — ${text}` : "";
|
|
238
|
-
}
|
|
239
|
-
function standardsText(finding) {
|
|
240
|
-
const parts = [...finding.successCriteria.map((criterion) => `WCAG ${criterion}`), ...finding.enClauses.map((clause) => `EN 301 549 ${clause}`)];
|
|
241
|
-
return escapeText(parts.join(", "));
|
|
242
|
-
}
|
|
243
|
-
/** Element markup, on one line and bounded, so one minified page cannot fill the report. */
|
|
244
|
-
function snippet(html) {
|
|
245
|
-
const collapsed = html.replace(/\s+/g, " ").trim();
|
|
246
|
-
return collapsed.length > MAX_SNIPPET ? `${collapsed.slice(0, 199)}…` : collapsed;
|
|
247
|
-
}
|
|
248
|
-
function totalViolations(audits) {
|
|
249
|
-
return audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
250
|
-
}
|
|
251
|
-
function byImpactThenRule(a, b) {
|
|
252
|
-
const order = [
|
|
253
|
-
"critical",
|
|
254
|
-
"serious",
|
|
255
|
-
"moderate",
|
|
256
|
-
"minor"
|
|
257
|
-
];
|
|
258
|
-
const rank = (finding) => {
|
|
259
|
-
const index = finding.impact ? order.indexOf(finding.impact) : -1;
|
|
260
|
-
return index === -1 ? -1 : index;
|
|
261
|
-
};
|
|
262
|
-
const difference = rank(a) - rank(b);
|
|
263
|
-
return difference === 0 ? a.ruleId.localeCompare(b.ruleId) : difference;
|
|
264
|
-
}
|
|
265
|
-
function count(value, noun) {
|
|
266
|
-
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
267
|
-
}
|
|
268
|
-
const STYLES = `:root { color-scheme: light dark; }
|
|
269
|
-
body { margin: 0; background: #ffffff; color: #1a1a1a;
|
|
270
|
-
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.6; }
|
|
271
|
-
main { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
272
|
-
h1 { font-size: 1.9rem; line-height: 1.25; margin: 0 0 1rem; }
|
|
273
|
-
h2 { font-size: 1.35rem; margin: 2.5rem 0 0.75rem; padding-top: 1.5rem;
|
|
274
|
-
border-top: 1px solid #d4d4d4; }
|
|
275
|
-
h3.page { font-size: 1.05rem; margin: 2rem 0 0.5rem; font-family: ui-monospace, monospace; }
|
|
276
|
-
p { margin: 0 0 1rem; }
|
|
277
|
-
a { color: #0b4fa8; }
|
|
278
|
-
a:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }
|
|
279
|
-
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
280
|
-
pre { margin: 0.4rem 0 0; padding: 0.6rem 0.75rem; overflow-x: auto;
|
|
281
|
-
background: #f4f4f5; border-radius: 4px; }
|
|
282
|
-
pre code { font-size: 0.85em; }
|
|
283
|
-
ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }
|
|
284
|
-
li + li { margin-top: 0.5rem; }
|
|
285
|
-
.verdict { padding: 0.75rem 1rem; border-radius: 4px; border-left: 4px solid; }
|
|
286
|
-
.verdict.pass { background: #eef7ee; border-color: #216e39; }
|
|
287
|
-
.verdict.fail { background: #fdeeee; border-color: #a01b1b; }
|
|
288
|
-
.verdict.broken { background: #fdf4e3; border-color: #8a5a00; }
|
|
289
|
-
.badge { display: inline-block; padding: 0 0.45rem; border-radius: 3px; font-size: 0.8rem;
|
|
290
|
-
font-weight: 600; border: 1px solid; }
|
|
291
|
-
.badge.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
|
|
292
|
-
.badge.serious { background: #fdf1e8; border-color: #a4531b; color: #7d3f14; }
|
|
293
|
-
.badge.moderate { background: #fdf9e3; border-color: #7a6100; color: #5c4900; }
|
|
294
|
-
.badge.minor { background: #eef2f8; border-color: #40556f; color: #33455a; }
|
|
295
|
-
.badge.unclassified { background: #f1f1f1; border-color: #565656; color: #444444; }
|
|
296
|
-
dl.run { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1.5rem; margin: 0; }
|
|
297
|
-
dl.run > div { display: contents; }
|
|
298
|
-
dl.run dt { font-weight: 600; }
|
|
299
|
-
dl.run dd { margin: 0; }
|
|
300
|
-
ul.counts, ul.impacts { list-style: none; padding-left: 0; }
|
|
301
|
-
ul.impacts li { display: inline-block; margin-right: 1rem; }
|
|
302
|
-
ol.findings { list-style: none; padding-left: 0; }
|
|
303
|
-
li.finding { margin: 0 0 1.5rem; padding-left: 0.9rem; border-left: 3px solid #d4d4d4; }
|
|
304
|
-
p.rule { margin-bottom: 0.25rem; }
|
|
305
|
-
p.standards, p.coverage, .reason, li.more, p.accepted-heading, ul.accepted { color: #4a4a4a; font-size: 0.9rem; }
|
|
306
|
-
p.coverage { margin-top: 0.5rem; }
|
|
307
|
-
ul.nodes { list-style: none; padding-left: 0; }
|
|
308
|
-
code.selector { color: #4a4a4a; }
|
|
309
|
-
p.clean { color: #216e39; }
|
|
310
|
-
p.note { font-size: 0.95rem; }
|
|
311
|
-
hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
|
|
312
|
-
footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
313
|
-
@media (prefers-color-scheme: dark) {
|
|
314
|
-
body { background: #121212; color: #ededed; }
|
|
315
|
-
a { color: #9ec1ff; }
|
|
316
|
-
h2, hr { border-color: #3a3a3a; }
|
|
317
|
-
pre { background: #1e1e1e; }
|
|
318
|
-
li.finding { border-color: #3a3a3a; }
|
|
319
|
-
p.standards, p.coverage, .reason, li.more, code.selector, footer,
|
|
320
|
-
p.accepted-heading, ul.accepted { color: #b6b6b6; }
|
|
321
|
-
p.clean { color: #7ee2a8; }
|
|
322
|
-
.verdict.pass { background: #10240f; border-color: #7ee2a8; }
|
|
323
|
-
.verdict.fail { background: #2b1111; border-color: #ff9d9d; }
|
|
324
|
-
.verdict.broken { background: #2b2310; border-color: #ffd28a; }
|
|
325
|
-
.badge.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
326
|
-
.badge.serious { background: #2b1c11; border-color: #ffbb8a; color: #ffd9bd; }
|
|
327
|
-
.badge.moderate { background: #262110; border-color: #ffe08a; color: #ffeec2; }
|
|
328
|
-
.badge.minor { background: #16202b; border-color: #a8c6e8; color: #cfdff0; }
|
|
329
|
-
.badge.unclassified { background: #1f1f1f; border-color: #b6b6b6; color: #d8d8d8; }
|
|
330
|
-
}
|
|
331
|
-
@media print {
|
|
332
|
-
body { background: #ffffff; color: #000000; }
|
|
333
|
-
main { max-width: none; }
|
|
334
|
-
a { color: #000000; }
|
|
335
|
-
}`;
|
|
336
|
-
//#endregion
|
|
337
|
-
export { buildHtmlReport };
|
package/dist/jsdom-BEu6Ra_2.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { i as shapeResults, n as failedPage, r as runOptions, t as DEFAULT_TAGS } from "./result-2aZPfM8w.js";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { Script } from "node:vm";
|
|
4
|
-
import axe from "axe-core";
|
|
5
|
-
import { JSDOM, VirtualConsole } from "jsdom";
|
|
6
|
-
//#region src/audit/runners/jsdom.ts
|
|
7
|
-
/**
|
|
8
|
-
* Rules this engine structurally cannot decide.
|
|
9
|
-
*
|
|
10
|
-
* jsdom has no layout: every element reports a 0x0 box, computed style is
|
|
11
|
-
* limited to the inline cascade, and nothing is ever fetched. axe-core does not
|
|
12
|
-
* know that, so it happily returns `pass` for some of these — `target-size`
|
|
13
|
-
* passes on any page because a 0x0 target gets measured against nothing, and
|
|
14
|
-
* `color-contrast` passes on pages whose colours were never computed. Both were
|
|
15
|
-
* observed on real sites. Reporting either as a pass would be a false clean
|
|
16
|
-
* bill of health, so every rule listed here is force-reported as incomplete no
|
|
17
|
-
* matter which bucket axe-core put it in.
|
|
18
|
-
*
|
|
19
|
-
* The browser runner passes an empty map instead: with real layout these rules
|
|
20
|
-
* are exactly the ones it exists to answer.
|
|
21
|
-
*
|
|
22
|
-
* Rules tagged `experimental` are listed for completeness but filtered out at
|
|
23
|
-
* scope time: axe-core does not run them by default, so a browser run would not
|
|
24
|
-
* evaluate them either and telling the user to re-run with --browser would be
|
|
25
|
-
* misleading.
|
|
26
|
-
*/
|
|
27
|
-
const ENGINE_BLIND_RULES = {
|
|
28
|
-
"color-contrast": {
|
|
29
|
-
detail: "needs rendered foreground and background colours",
|
|
30
|
-
applicabilityUnreliable: false
|
|
31
|
-
},
|
|
32
|
-
"color-contrast-enhanced": {
|
|
33
|
-
detail: "needs rendered foreground and background colours",
|
|
34
|
-
applicabilityUnreliable: false
|
|
35
|
-
},
|
|
36
|
-
"target-size": {
|
|
37
|
-
detail: "needs element geometry; every box is 0x0 without layout",
|
|
38
|
-
applicabilityUnreliable: false
|
|
39
|
-
},
|
|
40
|
-
"scrollable-region-focusable": {
|
|
41
|
-
detail: "needs computed overflow",
|
|
42
|
-
applicabilityUnreliable: true
|
|
43
|
-
},
|
|
44
|
-
"link-in-text-block": {
|
|
45
|
-
detail: "needs rendered colours and text decoration",
|
|
46
|
-
applicabilityUnreliable: true
|
|
47
|
-
},
|
|
48
|
-
"no-autoplay-audio": {
|
|
49
|
-
detail: "needs media duration, and media is never loaded",
|
|
50
|
-
applicabilityUnreliable: true
|
|
51
|
-
},
|
|
52
|
-
"avoid-inline-spacing": {
|
|
53
|
-
detail: "needs computed spacing after the full cascade",
|
|
54
|
-
applicabilityUnreliable: false
|
|
55
|
-
},
|
|
56
|
-
"p-as-heading": {
|
|
57
|
-
detail: "needs computed font size and weight",
|
|
58
|
-
applicabilityUnreliable: false
|
|
59
|
-
},
|
|
60
|
-
"css-orientation-lock": {
|
|
61
|
-
detail: "needs CSS media query evaluation",
|
|
62
|
-
applicabilityUnreliable: true
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
/** Per-page ceiling; one pathological document must not stall a CI run. */
|
|
66
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
67
|
-
/**
|
|
68
|
-
* Audit collected pages with axe-core inside jsdom.
|
|
69
|
-
*
|
|
70
|
-
* Pages are processed sequentially: jsdom parsing and axe-core are both
|
|
71
|
-
* CPU-bound on the main thread, so concurrency buys nothing here. A page that
|
|
72
|
-
* throws or times out is recorded with an `error` and the run continues.
|
|
73
|
-
*/
|
|
74
|
-
async function runJsdomAudit(pages, options = {}) {
|
|
75
|
-
const audits = [];
|
|
76
|
-
for (const page of pages) audits.push(await auditPage(page, options));
|
|
77
|
-
return audits;
|
|
78
|
-
}
|
|
79
|
-
async function auditPage(page, options = {}) {
|
|
80
|
-
const tags = options.tags ?? DEFAULT_TAGS;
|
|
81
|
-
const url = pageUrl(page, options.baseUrl);
|
|
82
|
-
const startedAt = Date.now();
|
|
83
|
-
const identity = {
|
|
84
|
-
relativePath: page.relativePath,
|
|
85
|
-
absolutePath: page.absolutePath,
|
|
86
|
-
url,
|
|
87
|
-
engine: "jsdom"
|
|
88
|
-
};
|
|
89
|
-
let dom;
|
|
90
|
-
try {
|
|
91
|
-
dom = createDom(page.html, url);
|
|
92
|
-
injectAxe(dom);
|
|
93
|
-
const { axe: pageAxe } = dom.window;
|
|
94
|
-
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags)), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
95
|
-
return shapeResults(results, {
|
|
96
|
-
...identity,
|
|
97
|
-
durationMs: Date.now() - startedAt,
|
|
98
|
-
blind: blindRulesInScope(tags)
|
|
99
|
-
});
|
|
100
|
-
} catch (cause) {
|
|
101
|
-
return failedPage({
|
|
102
|
-
...identity,
|
|
103
|
-
durationMs: Date.now() - startedAt
|
|
104
|
-
}, cause instanceof Error ? cause.message : String(cause));
|
|
105
|
-
} finally {
|
|
106
|
-
dom?.window.close();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function createDom(html, url) {
|
|
110
|
-
const virtualConsole = new VirtualConsole();
|
|
111
|
-
virtualConsole.on("jsdomError", () => {});
|
|
112
|
-
return new JSDOM(html, {
|
|
113
|
-
url,
|
|
114
|
-
virtualConsole,
|
|
115
|
-
runScripts: "outside-only",
|
|
116
|
-
pretendToBeVisual: true
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* axe-core is 1.3 MB of source. Compiling it once and re-running the compiled
|
|
121
|
-
* script in each window's context avoids re-parsing it for every page.
|
|
122
|
-
*/
|
|
123
|
-
let axeScript;
|
|
124
|
-
function injectAxe(dom) {
|
|
125
|
-
axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
|
|
126
|
-
axeScript.runInContext(dom.getInternalVMContext());
|
|
127
|
-
}
|
|
128
|
-
const blindScopeCache = /* @__PURE__ */ new Map();
|
|
129
|
-
/**
|
|
130
|
-
* Blind rules the requested tag filter would actually have run. Experimental
|
|
131
|
-
* rules are excluded: axe-core leaves them off by default, so a browser run
|
|
132
|
-
* would not have evaluated them either.
|
|
133
|
-
*/
|
|
134
|
-
function blindRulesInScope(tags) {
|
|
135
|
-
const key = [...tags].sort().join(",");
|
|
136
|
-
const cached = blindScopeCache.get(key);
|
|
137
|
-
if (cached) return cached;
|
|
138
|
-
const inScope = /* @__PURE__ */ new Map();
|
|
139
|
-
for (const rule of axe.getRules([...tags])) {
|
|
140
|
-
if (rule.tags.includes("experimental")) continue;
|
|
141
|
-
const blind = ENGINE_BLIND_RULES[rule.ruleId];
|
|
142
|
-
if (blind) inScope.set(rule.ruleId, blind);
|
|
143
|
-
}
|
|
144
|
-
blindScopeCache.set(key, inScope);
|
|
145
|
-
return inScope;
|
|
146
|
-
}
|
|
147
|
-
function pageUrl(page, baseUrl) {
|
|
148
|
-
if (!baseUrl) return pathToFileURL(page.absolutePath).href;
|
|
149
|
-
return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
|
|
150
|
-
}
|
|
151
|
-
async function withTimeout(promise, ms) {
|
|
152
|
-
promise.catch(() => {});
|
|
153
|
-
let timer;
|
|
154
|
-
try {
|
|
155
|
-
return await Promise.race([promise, new Promise((_resolve, reject) => {
|
|
156
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`axe-core timed out after ${ms}ms`)), ms);
|
|
157
|
-
})]);
|
|
158
|
-
} finally {
|
|
159
|
-
clearTimeout(timer);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
//#endregion
|
|
163
|
-
export { runJsdomAudit as i, auditPage as n, blindRulesInScope as r, ENGINE_BLIND_RULES as t };
|
package/dist/jsdom-C6dIyaxN.js
DELETED
package/dist/routes-BxbSZKXC.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { glob } from "tinyglobby";
|
|
4
|
-
//#region src/audit/routes.ts
|
|
5
|
-
/** Route file conventions, most specific first. */
|
|
6
|
-
const CONVENTIONS = [
|
|
7
|
-
{
|
|
8
|
-
framework: "next-app",
|
|
9
|
-
dir: "app",
|
|
10
|
-
pattern: "**/page.{tsx,ts,jsx,js,mdx}"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
framework: "next-app",
|
|
14
|
-
dir: "src/app",
|
|
15
|
-
pattern: "**/page.{tsx,ts,jsx,js,mdx}"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
framework: "next-pages",
|
|
19
|
-
dir: "pages",
|
|
20
|
-
pattern: "**/*.{tsx,ts,jsx,js,mdx}"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
framework: "next-pages",
|
|
24
|
-
dir: "src/pages",
|
|
25
|
-
pattern: "**/*.{tsx,ts,jsx,js,mdx}"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
framework: "astro",
|
|
29
|
-
dir: "src/pages",
|
|
30
|
-
pattern: "**/*.{astro,md,mdx}"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
framework: "nuxt",
|
|
34
|
-
dir: "pages",
|
|
35
|
-
pattern: "**/*.vue"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
framework: "sveltekit",
|
|
39
|
-
dir: "src/routes",
|
|
40
|
-
pattern: "**/+page.{svelte,ts,js}"
|
|
41
|
-
}
|
|
42
|
-
];
|
|
43
|
-
/**
|
|
44
|
-
* The URL path a route file serves.
|
|
45
|
-
*
|
|
46
|
-
* Returns undefined for anything that is not a page: a Next.js route group
|
|
47
|
-
* `(marketing)` contributes no segment, a private folder `_lib` contributes no
|
|
48
|
-
* route at all, and a dynamic segment `[slug]` cannot be resolved to one path
|
|
49
|
-
* without knowing the data behind it.
|
|
50
|
-
*/
|
|
51
|
-
function routePathFor(relativeFile, framework) {
|
|
52
|
-
let route = relativeFile.split(path.sep).join("/");
|
|
53
|
-
if (framework === "next-app" || framework === "sveltekit") route = route.includes("/") ? route.replace(/\/[^/]+$/, "") : "";
|
|
54
|
-
else {
|
|
55
|
-
route = route.replace(/\.[^./]+$/, "");
|
|
56
|
-
route = route.replace(/\/index$/, "");
|
|
57
|
-
if (route === "index") route = "";
|
|
58
|
-
}
|
|
59
|
-
const segments = route.split("/").filter((segment) => segment !== "");
|
|
60
|
-
const kept = [];
|
|
61
|
-
for (const segment of segments) {
|
|
62
|
-
if (/^[[(]|^_|^@/.test(segment)) {
|
|
63
|
-
if (/^\(.*\)$/.test(segment)) continue;
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
kept.push(segment);
|
|
67
|
-
}
|
|
68
|
-
return kept.join("/");
|
|
69
|
-
}
|
|
70
|
-
/** Every page path a build could have emitted for one route. */
|
|
71
|
-
function emittedPathsFor(route) {
|
|
72
|
-
if (route === "") return [
|
|
73
|
-
"/",
|
|
74
|
-
"index.html",
|
|
75
|
-
"index.htm"
|
|
76
|
-
];
|
|
77
|
-
return [
|
|
78
|
-
route,
|
|
79
|
-
`${route}/`,
|
|
80
|
-
`${route}.html`,
|
|
81
|
-
`${route}/index.html`
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Build a page-to-source map by reading the project's route files.
|
|
86
|
-
*
|
|
87
|
-
* Returns undefined when the project uses no convention this understands, which
|
|
88
|
-
* is not a failure — most builds are not framework projects.
|
|
89
|
-
*/
|
|
90
|
-
async function buildRouteMap(cwd) {
|
|
91
|
-
for (const convention of CONVENTIONS) {
|
|
92
|
-
const directory = path.join(cwd, convention.dir);
|
|
93
|
-
try {
|
|
94
|
-
if (!(await stat(directory)).isDirectory()) continue;
|
|
95
|
-
} catch {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const files = await glob([convention.pattern], {
|
|
99
|
-
cwd: directory,
|
|
100
|
-
ignore: ["**/node_modules/**"],
|
|
101
|
-
onlyFiles: true
|
|
102
|
-
});
|
|
103
|
-
if (files.length === 0) continue;
|
|
104
|
-
const sources = /* @__PURE__ */ new Map();
|
|
105
|
-
for (const file of files) {
|
|
106
|
-
const route = routePathFor(file, convention.framework);
|
|
107
|
-
if (route === void 0) continue;
|
|
108
|
-
const source = `${convention.dir}/${file.split(path.sep).join("/")}`;
|
|
109
|
-
for (const emitted of emittedPathsFor(route)) if (!sources.has(emitted)) sources.set(emitted, source);
|
|
110
|
-
}
|
|
111
|
-
if (sources.size > 0) return {
|
|
112
|
-
framework: convention.framework,
|
|
113
|
-
sources
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/** The source file for an audited page, if the map knows one. */
|
|
118
|
-
function sourceFor(map, pagePath) {
|
|
119
|
-
if (map === void 0) return void 0;
|
|
120
|
-
return map.sources.get(pagePath) ?? map.sources.get(pagePath.replace(/^\//, ""));
|
|
121
|
-
}
|
|
122
|
-
//#endregion
|
|
123
|
-
export { buildRouteMap, sourceFor };
|