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.
Files changed (56) hide show
  1. package/README.md +51 -4
  2. package/dist/astro/index.d.ts +7 -34
  3. package/dist/astro/index.js +6 -26
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-CPoZMXGM.js +779 -0
  6. package/dist/audit-CpXH2Mk8.js +2 -0
  7. package/dist/{baseline-CV_3lbER.js → baseline-22Y1NWxM.js} +1 -1
  8. package/dist/{baseline-CgBmzFTr.js → baseline-DB9CZGnV.js} +17 -27
  9. package/dist/cli/index.js +336 -142
  10. package/dist/collect-CFM8gEVv.js +134 -0
  11. package/dist/command-D8l_oYbV.js +77 -0
  12. package/dist/component-C3GL1Mnu.js +146 -0
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B4IBKMO8.js +521 -0
  15. package/dist/{crawl-CtJbMNNb.js → crawl-BiI1Lau_.js} +13 -23
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/frameworks-B4ClIJgE.js +314 -0
  19. package/dist/frameworks-etFg_O8K.js +2 -0
  20. package/dist/fs-BmPtmFke.js +40 -0
  21. package/dist/html-BGTO3ypW.js +543 -0
  22. package/dist/{impact-DvgBjupx.js → impact-DZt2oBCP.js} +15 -1
  23. package/dist/index.js +2 -2
  24. package/dist/{init-DRKIdpK1.js → init-DIWDE35F.js} +13 -10
  25. package/dist/jsdom-4IMzv0eE.js +3 -0
  26. package/dist/jsdom-DCpGSLfW.js +81 -0
  27. package/dist/{json-C9xS1PNC.js → json-DjEvy1nX.js} +19 -22
  28. package/dist/json-QQuFIw1W.js +2 -0
  29. package/dist/{load-sCkKsvGQ.js → load-5wRGLvub.js} +3 -9
  30. package/dist/nuxt/index.d.ts +35 -0
  31. package/dist/nuxt/index.js +23 -0
  32. package/dist/{playwright-DSRnXmcd.js → playwright-BWniOain.js} +70 -23
  33. package/dist/{pool-DixLeu8L.js → pool-BMevaLWD.js} +4 -12
  34. package/dist/{project-CufCqIE2.js → project-CzOnkLH6.js} +14 -21
  35. package/dist/project-MFrXcw1M.js +2 -0
  36. package/dist/remediation-Dtowi2EC.js +321 -0
  37. package/dist/{render-BO0nVrrZ.js → render-DrvXRCEn.js} +8 -16
  38. package/dist/{result-2aZPfM8w.js → result-DoamKFsp.js} +115 -1
  39. package/dist/routes-CmdRUuOs.js +265 -0
  40. package/dist/run-BMASMmwO.d.ts +45 -0
  41. package/dist/run-DB34BSOZ.js +53 -0
  42. package/dist/{sarif-eSCuI0eX.js → sarif-SR3_lLYd.js} +22 -36
  43. package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
  44. package/dist/text-CKKpzkYM.js +43 -0
  45. package/dist/vite/index.d.ts +39 -0
  46. package/dist/vite/index.js +40 -0
  47. package/dist/webpack/index.d.ts +33 -0
  48. package/dist/webpack/index.js +20 -0
  49. package/package.json +36 -9
  50. package/dist/audit-B2dIKpJ5.js +0 -2
  51. package/dist/audit-DXpKkXsC.js +0 -950
  52. package/dist/escape-Dm1o_RAk.js +0 -21
  53. package/dist/html-BLEuzep6.js +0 -337
  54. package/dist/jsdom-BEu6Ra_2.js +0 -163
  55. package/dist/jsdom-C6dIyaxN.js +0 -3
  56. package/dist/routes-BxbSZKXC.js +0 -123
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15
- }
16
- /** For attribute values, where a quote would end the attribute. */
17
- function escapeAttribute(value) {
18
- return escapeText(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19
- }
20
- //#endregion
21
- export { escapeText as n, escapeAttribute as t };
@@ -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 };
@@ -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 };
@@ -1,3 +0,0 @@
1
- import "./result-2aZPfM8w.js";
2
- import { i as runJsdomAudit } from "./jsdom-BEu6Ra_2.js";
3
- export { runJsdomAudit };
@@ -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 };