eaa-kit 0.3.0 → 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 (50) hide show
  1. package/README.md +51 -5
  2. package/dist/astro/index.js +1 -1
  3. package/dist/audit/runners/worker.js +1 -1
  4. package/dist/{audit-DcL73mOC.js → audit-CPoZMXGM.js} +180 -36
  5. package/dist/audit-CpXH2Mk8.js +2 -0
  6. package/dist/{baseline-CuKFq4IF.js → baseline-22Y1NWxM.js} +1 -1
  7. package/dist/{baseline-s9F3fXTN.js → baseline-DB9CZGnV.js} +2 -2
  8. package/dist/cli/index.js +267 -10
  9. package/dist/{collect-BkAQ0viT.js → collect-CFM8gEVv.js} +21 -10
  10. package/dist/{command-Dxpa00Ha.js → command-D8l_oYbV.js} +2 -2
  11. package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
  12. package/dist/component-DKd3EHOg.js +2 -0
  13. package/dist/coverage-B4IBKMO8.js +521 -0
  14. package/dist/{crawl-Oxt2Gaqo.js → crawl-BiI1Lau_.js} +2 -2
  15. package/dist/eleventy/index.d.ts +28 -0
  16. package/dist/eleventy/index.js +19 -0
  17. package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
  18. package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
  19. package/dist/{html-DKiI_3gs.js → html-BGTO3ypW.js} +130 -5
  20. package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/{init-DiLiYvN1.js → init-DIWDE35F.js} +2 -2
  23. package/dist/jsdom-4IMzv0eE.js +3 -0
  24. package/dist/jsdom-DCpGSLfW.js +81 -0
  25. package/dist/{json-Cnv9nd6U.js → json-DjEvy1nX.js} +8 -3
  26. package/dist/{json-B0Y7rNjt.js → json-QQuFIw1W.js} +1 -1
  27. package/dist/{load-UYXLqGV9.js → load-5wRGLvub.js} +1 -1
  28. package/dist/nuxt/index.d.ts +35 -0
  29. package/dist/nuxt/index.js +23 -0
  30. package/dist/{playwright-DYFsGUNd.js → playwright-BWniOain.js} +1 -1
  31. package/dist/{pool-BWkWZiJW.js → pool-BMevaLWD.js} +2 -2
  32. package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
  33. package/dist/project-MFrXcw1M.js +2 -0
  34. package/dist/remediation-Dtowi2EC.js +321 -0
  35. package/dist/{render-DI_aCnAZ.js → render-DrvXRCEn.js} +3 -3
  36. package/dist/{result-DLxd2Eip.js → result-DoamKFsp.js} +78 -1
  37. package/dist/routes-CmdRUuOs.js +265 -0
  38. package/dist/{run-BW6CVuND.js → run-DB34BSOZ.js} +19 -2
  39. package/dist/{sarif-DB3WG7T9.js → sarif-SR3_lLYd.js} +14 -6
  40. package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
  41. package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
  42. package/dist/vite/index.js +1 -1
  43. package/dist/webpack/index.d.ts +33 -0
  44. package/dist/webpack/index.js +20 -0
  45. package/package.json +30 -8
  46. package/dist/audit-CuG2hYyo.js +0 -2
  47. package/dist/jsdom-BjpF-2V-.js +0 -158
  48. package/dist/jsdom-X4KYfTp8.js +0 -3
  49. package/dist/manual-Vz-oX1I_.js +0 -239
  50. package/dist/routes-C2Cgf6Ko.js +0 -119
@@ -1,8 +1,10 @@
1
- import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
2
- import { a as standardsReference, i as escapeText, n as count, r as escapeAttribute, t as collapse } from "./text-BFmNtMsV.js";
1
+ import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
+ import { i as escapeText, n as count, o as standardsReference, r as escapeAttribute, t as collapse } from "./text-CKKpzkYM.js";
3
3
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
4
- import { a as groupIssues, i as coverageParts, n as understandingUrl, o as isShared, r as blindRules, t as manualCheckFor } from "./manual-Vz-oX1I_.js";
5
- import { n as buildSummary } from "./json-Cnv9nd6U.js";
4
+ import { a as isShared, i as groupIssues, n as blindRules, o as discoveryLabel, r as coverageParts, s as missedParts, t as remediationFor } from "./remediation-Dtowi2EC.js";
5
+ import { r as componentPath } from "./component-C3GL1Mnu.js";
6
+ import { i as understandingUrl, r as manualCheckFor, t as buildCoverage } from "./coverage-B4IBKMO8.js";
7
+ import { n as buildSummary } from "./json-DjEvy1nX.js";
6
8
  import axe from "axe-core";
7
9
  //#region src/audit/report/html.ts
8
10
  /**
@@ -56,9 +58,11 @@ ${verdict(audits, failing, options)}
56
58
  ${scoreboard(audits)}
57
59
  ${issues(audits, options)}
58
60
  ${runDetails(audits, engine, generatedAt, options)}
61
+ ${notMeasured(options)}
59
62
  ${summary(audits, failing, options)}
60
63
  ${pages(audits)}
61
64
  ${notEvaluated(audits)}
65
+ ${coverageSection(audits)}
62
66
  ${footer()}
63
67
  </main>
64
68
  </body>
@@ -79,6 +83,8 @@ function verdict(audits, failing, options) {
79
83
  if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
80
84
  const below = totalViolations(audits) - failing;
81
85
  if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
86
+ const completeness = options.completeness;
87
+ if (completeness && !completeness.complete) return banner("partial", "No violations found, but the whole site was not measured", `${escapeText(missedParts(completeness).join(", "))}. This report describes ${count(completeness.audited, "page")}, not the site.`);
82
88
  return banner("pass", "No violations found", "Automated testing found nothing to report.");
83
89
  }
84
90
  function banner(kind, heading, detail) {
@@ -94,8 +100,36 @@ function runDetails(audits, engine, generatedAt, options) {
94
100
  ["eaa-kit", `${TOOL_VERSION} · axe-core ${axe.version}`]
95
101
  ];
96
102
  if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
103
+ if (options.completeness) rows.splice(2, 0, ["Pages found via", discoveryLabel(options.completeness.discovery)]);
97
104
  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>`;
98
105
  }
106
+ /** Pages named individually before the rest are counted. */
107
+ const MAX_UNREACHABLE = 20;
108
+ /**
109
+ * The pages this run never reached, named rather than counted away.
110
+ *
111
+ * Named, because a count is not actionable: forty URLs that failed are a broken
112
+ * link check, a server that fell over halfway, or a section behind a login, and
113
+ * which of the three it was is only visible from the addresses. The reasons come
114
+ * straight from the crawler, so a reader can tell a 404 from a timeout.
115
+ */
116
+ function notMeasured(options) {
117
+ const completeness = options.completeness;
118
+ if (completeness === void 0 || completeness.complete) return "";
119
+ const parts = ["<h2>What this run did not measure</h2>", `<p>This report covers ${count(completeness.audited, "page")}. It is not a description of
120
+ the whole site, and a clean result here does not extend to anything listed below.</p>`];
121
+ if (completeness.truncated) parts.push(`<p class="note">The run stopped at its page limit, so pages beyond ${count(completeness.collected, "page")}
122
+ were never fetched. Raise <code>--max-pages</code> to go further.</p>`);
123
+ if (completeness.errored > 0) parts.push(`<p class="note">${count(completeness.errored, "page")} could not be audited after being
124
+ fetched, and ${completeness.errored === 1 ? "is" : "are"} not counted anywhere in this report.</p>`);
125
+ if (completeness.unreachable.length > 0) {
126
+ const shown = completeness.unreachable.slice(0, MAX_UNREACHABLE).map((item) => ` <li><code>${escapeText(item.location)}</code> — ${escapeText(item.reason)}</li>`).join("\n");
127
+ const rest = completeness.unreachable.length - MAX_UNREACHABLE;
128
+ parts.push(`<p>${count(completeness.unreachable.length, "page")} could not be reached:</p>`, `<ul class="unreachable">\n${shown}\n</ul>`);
129
+ if (rest > 0) parts.push(`<p class="note">…and ${count(rest, "more page")}.</p>`);
130
+ }
131
+ return parts.join("\n");
132
+ }
99
133
  /**
100
134
  * The counts, taken from the JSON report's own tally rather than recomputed.
101
135
  *
@@ -193,6 +227,60 @@ ${manualBlock(ruleId, finding)}
193
227
  </li>`).join("\n")}
194
228
  </ul>`;
195
229
  }
230
+ /**
231
+ * How much of the standard this run could reach.
232
+ *
233
+ * Always included, and placed last, where a reader who has been through the
234
+ * findings arrives at the limits of what produced them. The counts are shown as
235
+ * four separate figures and never as a ratio: `no automated rule` is the
236
+ * majority of WCAG, and a percentage would present a limit of automated testing
237
+ * as a property of this site.
238
+ */
239
+ function coverageSection(audits) {
240
+ const coverage = buildCoverage(audits);
241
+ const rows = coverage.criteria.map((criterion) => {
242
+ const url = understandingUrl(criterion.number);
243
+ const name = escapeText(`${criterion.number} ${criterion.title}`);
244
+ const linked = url === void 0 ? name : `<a href="${escapeAttribute(url)}">${name}</a>`;
245
+ return ` <tr class="${escapeAttribute(criterion.status)}">
246
+ <td>${linked}</td>
247
+ <td>${escapeText(criterion.level)}</td>
248
+ <td>${statusText(criterion)}</td>
249
+ </tr>`;
250
+ }).join("\n");
251
+ return `<h2>Coverage of WCAG 2.2 AA</h2>
252
+ <p>Of the ${coverage.total} success criteria at Levels A and AA,
253
+ <strong>${coverage.noAutomatedRule}</strong> cannot be checked by any automated engine and
254
+ need a person. This run reached a verdict on <strong>${coverage.evaluated}</strong>.</p>
255
+ <ul class="counts">
256
+ <li><strong>${coverage.evaluated}</strong> evaluated here</li>
257
+ <li><strong>${coverage.notEvaluated}</strong> not evaluated by this engine${coverage.browserWouldAnswer > 0 ? ` — a run with a real browser would answer ${coverage.browserWouldAnswer} of them` : ""}</li>
258
+ <li><strong>${coverage.nothingToCheck}</strong> had rules that ran and found nothing on this site to check</li>
259
+ <li><strong>${coverage.noAutomatedRule}</strong> have no automated rule at all</li>
260
+ </ul>
261
+ <p class="note">
262
+ These four are counted separately and never added together or divided into a score. Most
263
+ of WCAG cannot be automated, and a percentage here would present that limit of automated
264
+ testing as though it were a measurement of this site.
265
+ </p>
266
+ <div class="scroll">
267
+ <table class="coverage">
268
+ <thead><tr><th>Success criterion</th><th>Level</th><th>This run</th></tr></thead>
269
+ <tbody>
270
+ ${rows}
271
+ </tbody>
272
+ </table>
273
+ </div>`;
274
+ }
275
+ function statusText(criterion) {
276
+ const browser = criterion.browserWouldAnswer ? " <em>(a real browser would answer this)</em>" : "";
277
+ switch (criterion.status) {
278
+ case "evaluated": return `Evaluated${browser}`;
279
+ case "not-evaluated": return `This engine could not evaluate it${browser}`;
280
+ case "nothing-to-check": return "Rules ran and found nothing on this site to check";
281
+ case "no-automated-rule": return "No automated rule exists; a person must check it";
282
+ }
283
+ }
196
284
  function footer() {
197
285
  return `<hr>
198
286
  <footer>
@@ -242,6 +330,7 @@ li + li { margin-top: 0.5rem; }
242
330
  .verdict.pass { background: #eef7ee; border-color: #216e39; }
243
331
  .verdict.fail { background: #fdeeee; border-color: #a01b1b; }
244
332
  .verdict.broken { background: #fdf4e3; border-color: #8a5a00; }
333
+ .verdict.partial { background: #fdf4e3; border-color: #8a5a00; }
245
334
  .badge { display: inline-block; padding: 0 0.45rem; border-radius: 3px; font-size: 0.8rem;
246
335
  font-weight: 600; border: 1px solid; }
247
336
  .badge.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
@@ -261,6 +350,17 @@ p.rule { margin-bottom: 0.25rem; }
261
350
  p.standards, p.coverage, .reason, li.more, p.accepted-heading, ul.accepted { color: #4a4a4a; font-size: 0.9rem; }
262
351
  p.coverage { margin-top: 0.5rem; }
263
352
  ul.nodes { list-style: none; padding-left: 0; }
353
+ ul.unreachable { padding-left: 1.25rem; }
354
+ .remediation { border-left: 3px solid #c9c9c9; padding: 0.1rem 0 0.1rem 0.75rem; margin: 0.5rem 0; }
355
+ .remediation .why { margin: 0.3rem 0; opacity: 0.85; }
356
+ .remediation .fix { margin: 0.3rem 0; }
357
+ pre.suggested { background: #eef7ee; border-left: 3px solid #216e39; padding: 0.5rem; overflow-x: auto; }
358
+ .scroll { overflow-x: auto; }
359
+ table.coverage { border-collapse: collapse; width: 100%; font-size: 0.95em; }
360
+ table.coverage th, table.coverage td { text-align: left; padding: 0.3rem 0.6rem; border-bottom: 1px solid #e3e3e3; vertical-align: top; }
361
+ table.coverage tr.evaluated td:last-child { color: #216e39; }
362
+ table.coverage em { font-style: normal; opacity: 0.75; }
363
+ ul.unreachable li { margin-bottom: 0.2rem; overflow-wrap: anywhere; }
264
364
  code.selector { color: #4a4a4a; }
265
365
  p.clean { color: #216e39; }
266
366
  p.note { font-size: 0.95rem; }
@@ -306,6 +406,11 @@ ul.page-list { margin: 0.35rem 0 0; padding-left: 1.25rem; }
306
406
  .verdict.pass { background: #10240f; border-color: #7ee2a8; }
307
407
  .verdict.fail { background: #2b1111; border-color: #ff9d9d; }
308
408
  .verdict.broken { background: #2b2310; border-color: #ffd28a; }
409
+ .verdict.partial { background: #2b2310; border-color: #ffd28a; }
410
+ table.coverage th, table.coverage td { border-bottom-color: #333; }
411
+ table.coverage tr.evaluated td:last-child { color: #7ee2a8; }
412
+ .remediation { border-left-color: #555; }
413
+ pre.suggested { background: #10240f; border-left-color: #7ee2a8; }
309
414
  .tile.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
310
415
  .tile.serious { background: #2b1d11; border-color: #ffb98a; color: #ffd7bd; }
311
416
  .tile.moderate { background: #2b2610; border-color: #f0d264; color: #f5e3a4; }
@@ -377,17 +482,37 @@ function issueSection(issue, options) {
377
482
  return `<li class="issue">
378
483
  <h3><span class="badge ${impact}">${impact}</span> <code>${escapeText(issue.ruleId)}</code> ${criteria}</h3>
379
484
  <p class="help">${escapeText(issue.help)}</p>
485
+ ${remediationBlock(issue.ruleId, issue.elements[0]?.html, options)}
380
486
  ${issue.elements.slice(0, MAX_NODES).map((element) => issueElement(element, options)).join("\n")}
381
487
  ${issue.elements.length > MAX_NODES ? `<p class="more">…and ${count(issue.elements.length - MAX_NODES, "more element")}</p>` : ""}
382
488
  </li>`;
383
489
  }
490
+ /**
491
+ * What to do about the rule, beside the finding rather than behind a link.
492
+ *
493
+ * The corrected line is built from the element that actually failed. A textbook
494
+ * snippet is a second thing to translate before anybody can use it, and this
495
+ * document is read by people who did not run the audit and may not write the
496
+ * code either — the closer it gets to the line as it should have been, the less
497
+ * of that translation is left to them.
498
+ */
499
+ function remediationBlock(ruleId, html, options) {
500
+ const remediation = remediationFor(ruleId, options.framework);
501
+ if (remediation === void 0) return "";
502
+ const example = html === void 0 ? void 0 : remediation.example?.(html);
503
+ return `<div class="remediation">
504
+ <p class="why">${escapeText(remediation.why)}</p>
505
+ <p class="fix"><strong>Fix.</strong> ${escapeText(remediation.fix)}</p>
506
+ ${example === void 0 ? "" : `<pre class="suggested"><code>${escapeText(collapse(example, MAX_SNIPPET))}</code></pre>`}
507
+ </div>`;
508
+ }
384
509
  function issueElement(element, options) {
385
510
  const sources = [...new Set(element.pages.map((page) => options.sourceFor?.(page)).filter((source) => source !== void 0))];
386
511
  const list = element.pages.map((page) => `<li>${escapeText(page)}</li>`).join("");
387
512
  const component = options.componentFor?.(element.html);
388
513
  return `<div class="element">
389
514
  <pre><code>${escapeText(collapse(element.html, MAX_SNIPPET))}</code></pre>
390
- ${component === void 0 ? "" : `<p class="written-in">Written in <code>${escapeText(component)}</code></p>`}
515
+ ${component === void 0 ? "" : `<p class="written-in">Written in <code>${escapeText(componentPath(component))}</code></p>`}
391
516
  ${element.selector === "" ? "" : `<p class="selector"><code>${escapeText(element.selector)}</code></p>`}
392
517
  <p class="reach">Found on ${count(element.pages.length, "page")}.${isShared(element) ? " <strong>Identical on each — likely one shared component.</strong>" : ""}</p>
393
518
  ${sources.length > 0 ? `<p class="sources">Source: ${sources.map((source) => `<code>${escapeText(source)}</code>`).join(", ")}</p>` : ""}
@@ -43,4 +43,4 @@ function countAtOrAbove(audits, threshold) {
43
43
  return total;
44
44
  }
45
45
  //#endregion
46
- export { impactRank as a, impactLabel as i, IMPACT_LEVELS as n, isImpactLevel as o, countAtOrAbove as r, DEFAULT_FAIL_ON as t };
46
+ export { impactRank as a, impactLabel as i, IMPACT_LEVELS as n, isImpactLevel as o, countAtOrAbove as r, meetsThreshold as s, DEFAULT_FAIL_ON as t };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-UYXLqGV9.js";
2
- import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DI_aCnAZ.js";
1
+ import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-5wRGLvub.js";
2
+ import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DrvXRCEn.js";
3
3
  export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
@@ -1,6 +1,6 @@
1
- import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-UYXLqGV9.js";
1
+ import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-5wRGLvub.js";
2
2
  import { t as exists } from "./fs-BmPtmFke.js";
3
- import { i as note, o as warn, r as fail } from "./command-Dxpa00Ha.js";
3
+ import { i as note, o as warn, r as fail } from "./command-D8l_oYbV.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import pc from "picocolors";
@@ -0,0 +1,3 @@
1
+ import "./result-DoamKFsp.js";
2
+ import { n as runJsdomAudit } from "./jsdom-DCpGSLfW.js";
3
+ export { runJsdomAudit };
@@ -0,0 +1,81 @@
1
+ import { c as runOptions, i as failedPage, l as shapeResults, o as pageUrl, r as blindRulesInScope, t as DEFAULT_TAGS } from "./result-DoamKFsp.js";
2
+ import axe from "axe-core";
3
+ import { Script } from "node:vm";
4
+ import { JSDOM, VirtualConsole } from "jsdom";
5
+ //#region src/audit/runners/jsdom.ts
6
+ /** Per-page ceiling; one pathological document must not stall a CI run. */
7
+ const DEFAULT_TIMEOUT_MS = 3e4;
8
+ /**
9
+ * Audit collected pages with axe-core inside jsdom.
10
+ *
11
+ * Pages are processed sequentially: jsdom parsing and axe-core are both
12
+ * CPU-bound on the main thread, so concurrency buys nothing here. A page that
13
+ * throws or times out is recorded with an `error` and the run continues.
14
+ */
15
+ async function runJsdomAudit(pages, options = {}) {
16
+ const audits = [];
17
+ for (const page of pages) audits.push(await auditPage(page, options));
18
+ return audits;
19
+ }
20
+ async function auditPage(page, options = {}) {
21
+ const tags = options.tags ?? DEFAULT_TAGS;
22
+ const url = pageUrl(page, options.baseUrl);
23
+ const startedAt = Date.now();
24
+ const identity = {
25
+ relativePath: page.relativePath,
26
+ absolutePath: page.absolutePath,
27
+ url,
28
+ engine: "jsdom"
29
+ };
30
+ let dom;
31
+ try {
32
+ dom = createDom(page.html, url);
33
+ injectAxe(dom);
34
+ const { axe: pageAxe } = dom.window;
35
+ const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags)), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
36
+ return shapeResults(results, {
37
+ ...identity,
38
+ durationMs: Date.now() - startedAt,
39
+ blind: blindRulesInScope(tags)
40
+ });
41
+ } catch (cause) {
42
+ return failedPage({
43
+ ...identity,
44
+ durationMs: Date.now() - startedAt
45
+ }, cause instanceof Error ? cause.message : String(cause));
46
+ } finally {
47
+ dom?.window.close();
48
+ }
49
+ }
50
+ function createDom(html, url) {
51
+ const virtualConsole = new VirtualConsole();
52
+ virtualConsole.on("jsdomError", () => {});
53
+ return new JSDOM(html, {
54
+ url,
55
+ virtualConsole,
56
+ runScripts: "outside-only",
57
+ pretendToBeVisual: true
58
+ });
59
+ }
60
+ /**
61
+ * axe-core is 1.3 MB of source. Compiling it once and re-running the compiled
62
+ * script in each window's context avoids re-parsing it for every page.
63
+ */
64
+ let axeScript;
65
+ function injectAxe(dom) {
66
+ axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
67
+ axeScript.runInContext(dom.getInternalVMContext());
68
+ }
69
+ async function withTimeout(promise, ms) {
70
+ promise.catch(() => {});
71
+ let timer;
72
+ try {
73
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
74
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`axe-core timed out after ${ms}ms`)), ms);
75
+ })]);
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+ //#endregion
81
+ export { runJsdomAudit as n, auditPage as t };
@@ -1,6 +1,8 @@
1
- import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
1
+ import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
2
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
3
- import { a as ruleOutcomes } from "./result-DLxd2Eip.js";
3
+ import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
4
+ import { s as ruleOutcomes } from "./result-DoamKFsp.js";
5
+ import { t as buildCoverage } from "./coverage-B4IBKMO8.js";
4
6
  import axe from "axe-core";
5
7
  /**
6
8
  * Build the machine-readable report.
@@ -30,6 +32,8 @@ function buildJsonReport(audits, options) {
30
32
  baseUrl: options.baseUrl ?? null
31
33
  },
32
34
  summary: buildSummary(audits, options.failOn),
35
+ completeness: options.completeness,
36
+ coverage: buildCoverage(audits),
33
37
  rules: buildRuleIndex(audits),
34
38
  pages: audits.map(toJsonPage)
35
39
  };
@@ -115,7 +119,8 @@ function toJsonFinding(finding) {
115
119
  nodes: finding.nodes.map((node) => ({
116
120
  html: node.html,
117
121
  target: node.target,
118
- failureSummary: node.failureSummary ?? null
122
+ failureSummary: node.failureSummary ?? null,
123
+ fingerprint: elementFingerprint(finding.ruleId, node.target.join(" "), node.html)
119
124
  }))
120
125
  };
121
126
  }
@@ -1,2 +1,2 @@
1
- import { r as serialiseJsonReport, t as buildJsonReport } from "./json-Cnv9nd6U.js";
1
+ import { r as serialiseJsonReport, t as buildJsonReport } from "./json-DjEvy1nX.js";
2
2
  export { buildJsonReport, serialiseJsonReport };
@@ -1,4 +1,4 @@
1
- import { _ as withDefault, c as object, f as safeParse, g as url, h as union, i as isoDate, l as optional, m as transform, n as email, p as string, r as enumeration, t as array, u as pipe } from "./schema-CMZ8ItGk.js";
1
+ import { _ as url, a as isoDate, d as pipe, g as union, h as transform, i as enumeration, l as object, m as string, p as safeParse, r as email, t as array, u as optional, v as withDefault } from "./schema-is6CGX2D.js";
2
2
  import { r as isFile } from "./fs-BmPtmFke.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
@@ -0,0 +1,35 @@
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
2
+ //#region src/nuxt/index.d.ts
3
+ /** The part of the Nitro instance this reads, handed over by `nitro:init`. */
4
+ interface NitroLike {
5
+ options: {
6
+ output?: {
7
+ publicDir?: string;
8
+ };
9
+ /** True for `nuxt generate`; absent for a server build. */
10
+ static?: boolean;
11
+ };
12
+ }
13
+ interface NuxtOptionsLike {
14
+ rootDir?: string;
15
+ }
16
+ interface NuxtLike {
17
+ options: NuxtOptionsLike;
18
+ hook(name: 'nitro:init', handler: (nitro: NitroLike) => void): void;
19
+ hook(name: 'close', handler: () => Promise<void>): void;
20
+ }
21
+ interface EaaKitNuxtOptions extends IntegrationOptions {
22
+ /** Directory to audit. Defaults to the one Nitro says it wrote. */
23
+ directory?: string;
24
+ /**
25
+ * Let a server build pass without auditing anything.
26
+ *
27
+ * Off by default: `nuxt build` writes no browsable HTML, and a silent pass
28
+ * over a directory with no pages in it is indistinguishable from a clean
29
+ * site.
30
+ */
31
+ allowServerBuild?: boolean;
32
+ }
33
+ declare function eaaKitModule(options?: EaaKitNuxtOptions, nuxt?: NuxtLike): void;
34
+ //#endregion
35
+ export { BuildAuditError, EaaKitNuxtOptions, NitroLike, NuxtLike, NuxtOptionsLike, eaaKitModule as default };
@@ -0,0 +1,23 @@
1
+ import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
2
+ import path from "node:path";
3
+ //#region src/nuxt/index.ts
4
+ function eaaKitModule(options = {}, nuxt) {
5
+ if (nuxt === void 0) return;
6
+ let publicDir;
7
+ let prerendered = false;
8
+ nuxt.hook("nitro:init", (nitro) => {
9
+ publicDir = nitro.options.output?.publicDir;
10
+ prerendered = nitro.options.static === true;
11
+ });
12
+ nuxt.hook("close", async () => {
13
+ const directory = options.directory ?? publicDir;
14
+ if (options.directory === void 0 && !prerendered) {
15
+ if (options.allowServerBuild === true) return;
16
+ throw new BuildAuditError("eaa-kit: this was a server build, so no pages were written to disk and there was nothing to audit. Run `nuxt generate` to prerender them, audit the running site with `eaa-kit audit --url`, or set allowServerBuild to skip this.");
17
+ }
18
+ if (directory === void 0) throw new BuildAuditError("eaa-kit: Nitro reported no public directory, so there is no path to audit. Pass `directory` to the module to name one.");
19
+ await auditBuild(path.resolve(nuxt.options.rootDir ?? process.cwd(), directory), options, stderrLogger());
20
+ });
21
+ }
22
+ //#endregion
23
+ export { BuildAuditError, eaaKitModule as default };
@@ -1,5 +1,5 @@
1
1
  import { r as isFile } from "./fs-BmPtmFke.js";
2
- import { i as pageUrl, n as failedPage, o as runOptions, s as shapeResults, t as DEFAULT_TAGS } from "./result-DLxd2Eip.js";
2
+ import { c as runOptions, i as failedPage, l as shapeResults, o as pageUrl, t as DEFAULT_TAGS } from "./result-DoamKFsp.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { stat } from "node:fs/promises";
5
5
  import path from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { r as isFile } from "./fs-BmPtmFke.js";
2
- import { i as pageUrl, n as failedPage } from "./result-DLxd2Eip.js";
2
+ import { i as failedPage, o as pageUrl } from "./result-DoamKFsp.js";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { availableParallelism } from "node:os";
5
5
  //#region src/audit/runners/pool.ts
@@ -96,7 +96,7 @@ async function runPooledAudit(pages, options = {}) {
96
96
  return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
97
97
  }
98
98
  async function auditHere(pages, options) {
99
- const { runJsdomAudit } = await import("./jsdom-X4KYfTp8.js");
99
+ const { runJsdomAudit } = await import("./jsdom-4IMzv0eE.js");
100
100
  return runJsdomAudit(pages, options);
101
101
  }
102
102
  async function runWorkers(pages, options, count, entry) {
@@ -1,5 +1,5 @@
1
1
  import { n as isDirectory, t as exists } from "./fs-BmPtmFke.js";
2
- import { r as candidateOutputs } from "./frameworks-BYa3tULg.js";
2
+ import { r as candidateOutputs } from "./frameworks-B4ClIJgE.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { glob } from "tinyglobby";
@@ -226,6 +226,12 @@ async function autoDetectSource(cwd, options = {}) {
226
226
  };
227
227
  }
228
228
  const pkg = await readPackageJson(cwd);
229
+ const { detectFramework } = await import("./frameworks-etFg_O8K.js");
230
+ const detected = await detectFramework(cwd, pkg);
231
+ if (detected !== void 0 && detected.framework.outputs.length === 0) {
232
+ step(`${detected.framework.name} renders on a server and writes no HTML to disk`);
233
+ return { steps };
234
+ }
229
235
  if (pkg === void 0) return void 0;
230
236
  const scripts = pkg.scripts ?? {};
231
237
  if (options.noBuild) return void 0;
@@ -265,4 +271,4 @@ async function autoDetectSource(cwd, options = {}) {
265
271
  };
266
272
  }
267
273
  //#endregion
268
- export { autoDetectSource, readPackageJson };
274
+ export { runScript as a, readPackageJson as i, detectPackageManager as n, startServer as o, findBuildOutput as r, autoDetectSource as t };
@@ -0,0 +1,2 @@
1
+ import { i as readPackageJson, t as autoDetectSource } from "./project-CzOnkLH6.js";
2
+ export { autoDetectSource, readPackageJson };