eaa-kit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +23 -8
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-Bwm1xBdJ.js +2 -0
  6. package/dist/{audit-e_uixhPG.js → audit-DRjAY2iY.js} +164 -75
  7. package/dist/{baseline-D2qWmUC6.js → baseline-BpE7DbDq.js} +1 -1
  8. package/dist/{baseline-BscHfFSW.js → baseline-rtnj3RHB.js} +1 -1
  9. package/dist/cache-D4dOIuOi.js +219 -0
  10. package/dist/{checklist-BtgkxMEa.js → checklist-DDd9jewi.js} +3 -3
  11. package/dist/cli/index.js +28 -12
  12. package/dist/{command-CZKp9Wi_.js → command-3r4fht5f.js} +6 -5
  13. package/dist/{coverage-BD2_CYns.js → coverage-cb5kPp48.js} +3 -285
  14. package/dist/coverage-n1X9CkiG.js +3 -0
  15. package/dist/criteria-CnPJKGeb.js +286 -0
  16. package/dist/eleventy/index.d.ts +1 -1
  17. package/dist/eleventy/index.js +1 -1
  18. package/dist/{html-Bmh7X_sM.js → html-BIxxF3FC.js} +6 -4
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.js +2 -2
  21. package/dist/{init-BjKYttxc.js → init-DrFfzjvm.js} +2 -2
  22. package/dist/jsdom-BUYuVJJm.js +3 -0
  23. package/dist/{jsdom-CQar6OQS.js → jsdom-CsW_xVcA.js} +11 -12
  24. package/dist/{json-PNg6BeXp.js → json-Bus6YhxB.js} +4 -3
  25. package/dist/{json-HJDgJp09.js → json-CrQmoTj6.js} +1 -1
  26. package/dist/{load-vZdV_fD1.js → load-BaWMa-G_.js} +6 -1
  27. package/dist/load-n62iXDq_.js +2 -0
  28. package/dist/nuxt/index.d.ts +1 -1
  29. package/dist/nuxt/index.js +1 -1
  30. package/dist/{playwright-C-miJP2b.js → playwright-ukCr_5o3.js} +1 -1
  31. package/dist/{pool-dkexq5xo.js → pool-B4tEUa2S.js} +6 -2
  32. package/dist/{completeness-Dwq3CPB3.js → remediation-C0klJblu.js} +55 -42
  33. package/dist/{render-CtsqR0k-.js → render-C1sUZZI_.js} +41 -3
  34. package/dist/{result-DfUru6y_.js → result-B7_zVxAX.js} +1 -1
  35. package/dist/result-CRmjjoAA.js +2 -0
  36. package/dist/{run-YiqyQA2b.d.ts → run-BK2blxMI.d.ts} +8 -0
  37. package/dist/{run-D7hdVXLA.js → run-DrS0Komf.js} +6 -2
  38. package/dist/{sarif-BKvq_AUm.js → sarif-BNCE0PNT.js} +3 -2
  39. package/dist/statement/templates/at.de.md +14 -0
  40. package/dist/statement/templates/at.en.md +14 -0
  41. package/dist/statement/templates/ch.de.md +14 -0
  42. package/dist/statement/templates/ch.en.md +14 -0
  43. package/dist/statement/templates/de.de.md +14 -0
  44. package/dist/statement/templates/de.en.md +14 -0
  45. package/dist/statement/templates/es.en.md +14 -0
  46. package/dist/statement/templates/es.es.md +15 -0
  47. package/dist/statement/templates/fr.en.md +14 -0
  48. package/dist/statement/templates/fr.fr.md +15 -0
  49. package/dist/statement/templates/it.en.md +14 -0
  50. package/dist/statement/templates/it.it.md +15 -0
  51. package/dist/statement/templates/nl.en.md +14 -0
  52. package/dist/statement/templates/nl.nl.md +15 -0
  53. package/dist/vite/index.d.ts +1 -1
  54. package/dist/vite/index.js +1 -1
  55. package/dist/webpack/index.d.ts +1 -1
  56. package/dist/webpack/index.js +1 -1
  57. package/dist/{worksheet-eUjwYLxC.js → worksheet-BIxMtbUd.js} +3 -2
  58. package/package.json +3 -2
  59. package/dist/audit-DK2StT1_.js +0 -2
  60. package/dist/coverage-7bQqWNfN.js +0 -2
  61. package/dist/jsdom-DMvU2h-f.js +0 -3
  62. package/dist/load-Bc_xDGAh.js +0 -2
package/README.md CHANGED
@@ -9,11 +9,19 @@ in the DACH region — the BFSG in Germany, the BaFG in Austria — and the stat
9
9
  the statute and supervisory body of **seven countries**: Austria, Germany, Switzerland,
10
10
  Spain, France, Italy and the Netherlands, each in its own language as well as English.
11
11
 
12
- 0.6.0 adds the half no engine can do. Of the 55 WCAG 2.2 A and AA success criteria, 34 have
13
- no automated rule at all: `checklist` writes those down as a review somebody works through,
14
- `audit --review` reads the answers back beside what the run measured, and `statement` now
15
- refuses to publish a claim of full conformance that either one contradicts. Sites behind a
16
- login or a preview protection are auditable too.
12
+ 0.7.0 makes a run cost what it should. A page that has not changed byte for byte is not
13
+ audited again, and a run with nothing to re-audit never loads an engine at all: twenty
14
+ pages go from ~2,520 ms to ~230 ms, which is about what starting the process costs. A
15
+ reused result is never passed off as a fresh one every report says how much was reused
16
+ and from when, and `diff` will not call a page fixed that this run did not look at. The
17
+ statement now also says how many of WCAG's 55 criteria a person checked by hand, and
18
+ `pnpm bench` prints the numbers in this paragraph for your own machine.
19
+
20
+ 0.6.0 added the half no engine can do. Of the 55 WCAG 2.2 A and AA success criteria, 34
21
+ have no automated rule at all: `checklist` writes those down as a review somebody works
22
+ through, `audit --review` reads the answers back beside what the run measured, and
23
+ `statement` refuses to publish a claim of full conformance that either one contradicts.
24
+ Sites behind a login or a preview protection are auditable too.
17
25
 
18
26
  ```bash
19
27
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
@@ -114,13 +122,20 @@ eaa-kit audit --review eaa-review.json --coverage
114
122
  ```
115
123
 
116
124
  **Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
117
- AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
118
- 55 with what this run reached on each — never as a percentage, because most of WCAG cannot
119
- be automated and a score would present that as a fact about your site.
125
+ AA. This tool has rules for 21 of them, and 34 need a person. Every run says so, and
126
+ `--coverage` lists all 55 with what this run reached on each — never as a percentage,
127
+ because most of WCAG cannot be automated and a score would present that as a fact about
128
+ your site.
120
129
 
121
130
  **Says what it did not measure.** A crawl that stopped at its page limit, or could not
122
131
  fetch forty URLs, no longer produces a report that looks like a complete one.
123
132
 
133
+ **Audits only what changed.** A page whose markup is byte-identical to the last run's keeps
134
+ the result it already had, and a run with nothing to re-audit never loads an engine —
135
+ twenty pages in ~230 ms instead of ~2,520 ms, which is about what starting the process
136
+ costs. Reuse is counted apart from auditing in all four reports, and `diff` will not call a
137
+ page fixed that this run did not look at. `--no-cache` audits everything regardless.
138
+
124
139
  **Reports in four shapes**: a console report for whoever ran it, JSON for other tools,
125
140
  SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
126
141
  it is.
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions } from "../run-YiqyQA2b.js";
1
+ import { n as IntegrationOptions } from "../run-BK2blxMI.js";
2
2
  //#region src/astro/index.d.ts
3
3
  /**
4
4
  * An Astro integration that audits the build Astro just produced.
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, t as BuildAuditError } from "../run-D7hdVXLA.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-DrS0Komf.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  //#region src/astro/index.ts
4
4
  var AstroAuditError = class extends Error {
@@ -1,4 +1,4 @@
1
- import { t as auditPage } from "../../jsdom-CQar6OQS.js";
1
+ import { t as auditPage } from "../../jsdom-CsW_xVcA.js";
2
2
  import { enableCompileCache } from "node:module";
3
3
  import { parentPort, workerData } from "node:worker_threads";
4
4
  //#region src/audit/runners/worker.ts
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-DRjAY2iY.js";
2
+ export { runAuditCommand };
@@ -1,12 +1,13 @@
1
1
  import { a as impactLabel, i as countAtOrAbove, r as byImpactThenRule } from "./impact-luGCnN8F.js";
2
2
  import { n as count, o as plural, t as collapse } from "./text-BF1LHMiV.js";
3
- import { c as runEngine } from "./result-DfUru6y_.js";
4
- import { a as emitDocument, c as runEngine$1, l as warn, o as fail, s as note, t as advise } from "./command-CZKp9Wi_.js";
3
+ import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
4
+ import { n as DEFAULT_TAGS, u as runEngine } from "./result-B7_zVxAX.js";
5
+ import { a as emitDocument, c as runEngine$1, l as warn, o as fail, s as note, t as advise } from "./command-3r4fht5f.js";
5
6
  import { l as reviewSentence } from "./review-CdMK2GpQ.js";
7
+ import { a as isShared, i as groupIssues, l as reusedPart, n as blindRules, o as issueTotals, r as coverageParts, t as remediationFor, u as runCompleteness } from "./remediation-C0klJblu.js";
6
8
  import { r as componentPath } from "./component-ChisoFyY.js";
7
- import { i as reviewSummary, n as buildCoverage, o as manualCheckFor, r as coverageSummary, s as understandingUrl } from "./coverage-BD2_CYns.js";
8
- import { a as blindRules, c as isShared, i as remediationFor, l as issueTotals, o as coverageParts, r as runCompleteness, s as groupIssues } from "./completeness-Dwq3CPB3.js";
9
- import { n as buildSummary } from "./json-PNg6BeXp.js";
9
+ import { a as manualCheckFor, n as coverageSummary, o as understandingUrl, r as reviewSummary, t as buildCoverage } from "./coverage-cb5kPp48.js";
10
+ import { n as buildSummary } from "./json-Bus6YhxB.js";
10
11
  import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
11
12
  import path from "node:path";
12
13
  import pc from "picocolors";
@@ -77,7 +78,7 @@ function supportsUnicode() {
77
78
  * Joins segments into one line, trimming the group to the terminal width.
78
79
  * Colour codes are applied after trimming, so they never count towards it.
79
80
  */
80
- function render(ctx, segments) {
81
+ function render$1(ctx, segments) {
81
82
  let remaining = ctx.width;
82
83
  const parts = [];
83
84
  for (const segment of segments) {
@@ -90,14 +91,14 @@ function render(ctx, segments) {
90
91
  }
91
92
  /** One line from a single segment, which is what most of this report is. */
92
93
  function line(ctx, text, paint) {
93
- return render(ctx, paint === void 0 ? [{ text }] : [{
94
+ return render$1(ctx, paint === void 0 ? [{ text }] : [{
94
95
  text,
95
96
  paint
96
97
  }]);
97
98
  }
98
99
  function headerLines(audits, ctx) {
99
100
  const engineLabel = runEngine(audits) === "browser" ? "chromium" : "jsdom (browserless)";
100
- return [render(ctx, [{
101
+ return [render$1(ctx, [{
101
102
  text: "eaa-kit audit",
102
103
  paint: ctx.c.bold
103
104
  }, {
@@ -118,14 +119,14 @@ function legendLines(ctx) {
118
119
  function pageSection(audit, ctx) {
119
120
  const lines = [line(ctx, audit.relativePath, ctx.c.underline)];
120
121
  if (audit.error) {
121
- lines.push(render(ctx, [{
122
+ lines.push(render$1(ctx, [{
122
123
  text: ` ${ctx.symbol("error")} not audited: `,
123
124
  paint: ctx.c.red
124
125
  }, { text: audit.error }]), "");
125
126
  return lines;
126
127
  }
127
128
  for (const finding of [...audit.violations].sort(byImpactThenRule)) lines.push(...violationLines(finding, ctx));
128
- for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render(ctx, [
129
+ for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render$1(ctx, [
129
130
  {
130
131
  text: ` ${ctx.symbol("review")} `,
131
132
  paint: ctx.c.yellow
@@ -141,7 +142,7 @@ function pageSection(audit, ctx) {
141
142
  ]));
142
143
  if (audit.violations.length === 0) {
143
144
  const clean = (audit.accepted ?? []).length === 0;
144
- lines.push(render(ctx, [{
145
+ lines.push(render$1(ctx, [{
145
146
  text: ` ${ctx.symbol("clean")} `,
146
147
  paint: clean ? ctx.c.green : ctx.c.dim
147
148
  }, {
@@ -162,7 +163,7 @@ function coverageLine(audit, ctx) {
162
163
  }
163
164
  function violationLines(finding, ctx) {
164
165
  const impact = impactLabel(finding.impact);
165
- const lines = [render(ctx, [
166
+ const lines = [render$1(ctx, [
166
167
  {
167
168
  text: ` ${ctx.symbol("violation")} `,
168
169
  paint: ctx.c.red
@@ -195,7 +196,7 @@ function summary(audits, ctx) {
195
196
  const clean = totals.pagesNotAudited > 0 || ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${count(audited, "page")} that were audited.` : ` No violations across ${pages}.`;
196
197
  lines.push(line(ctx, clean, ctx.c.green));
197
198
  } else {
198
- lines.push(render(ctx, [{
199
+ lines.push(render$1(ctx, [{
199
200
  text: ` ${count(totals.violations, "violation")} on ${totals.pagesWithViolations} of ${count(audited, "page")}`,
200
201
  paint: ctx.c.red
201
202
  }, {
@@ -239,7 +240,7 @@ function coverageSection(audits, ctx) {
239
240
  for (const criterion of coverage.criteria) {
240
241
  const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
241
242
  const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
242
- lines.push(render(ctx, [
243
+ lines.push(render$1(ctx, [
243
244
  {
244
245
  text: ` ${criterion.number} `,
245
246
  paint: ctx.c.bold
@@ -274,8 +275,10 @@ const STATUS_WORDS = {
274
275
  */
275
276
  function completenessLines(ctx) {
276
277
  const completeness = ctx.completeness;
277
- if (completeness === void 0 || completeness.complete) return [];
278
- const lines = [];
278
+ if (completeness === void 0) return [];
279
+ const reused = reusedPart(completeness);
280
+ const lines = reused === void 0 ? [] : [line(ctx, ` ${reused}`, ctx.c.dim)];
281
+ if (completeness.complete) return lines;
279
282
  if (completeness.unreachable.length > 0) {
280
283
  const noun = plural(completeness.unreachable.length, "page");
281
284
  const verb = completeness.unreachable.length === 1 ? "was" : "were";
@@ -309,7 +312,7 @@ function blindSection(audits, ctx) {
309
312
  line(ctx, " They are never reported as passing.", ctx.c.dim)
310
313
  ];
311
314
  for (const { ruleId, pages, finding } of blind) {
312
- lines.push(render(ctx, [
315
+ lines.push(render$1(ctx, [
313
316
  { text: ` ${ctx.symbol("blind")} ` },
314
317
  { text: ruleId },
315
318
  {
@@ -356,7 +359,7 @@ function issuesSection(audits, ctx) {
356
359
  lines.push(line(ctx, intro, ctx.c.dim));
357
360
  for (const issue of issues) {
358
361
  lines.push("");
359
- lines.push(render(ctx, [
362
+ lines.push(render$1(ctx, [
360
363
  { text: ` ${ctx.symbol("violation")} ` },
361
364
  {
362
365
  text: issue.ruleId,
@@ -397,7 +400,7 @@ function remediationLines(ruleId, html, ctx) {
397
400
  const width = ctx.width - 6 - 2;
398
401
  const lines = [...wrap(remediation.why, width).map((text) => line(ctx, `${indent}${text}`, ctx.c.dim)), ...wrap(`Fix: ${remediation.fix}`, width).map((text) => line(ctx, `${indent}${text}`))];
399
402
  const example = html === void 0 ? void 0 : remediation.example?.(html);
400
- if (example !== void 0) lines.push(render(ctx, [{
403
+ if (example !== void 0) lines.push(render$1(ctx, [{
401
404
  text: `${indent}→ `,
402
405
  paint: ctx.c.green
403
406
  }, { text: collapse(example) }]));
@@ -408,14 +411,14 @@ function whereLines(element, ctx) {
408
411
  const shown = element.pages.slice(0, ctx.maxNodes);
409
412
  const rest = element.pages.length - shown.length;
410
413
  const component = ctx.componentFor(element.html);
411
- const lines = component === void 0 ? [] : [render(ctx, [{
414
+ const lines = component === void 0 ? [] : [render$1(ctx, [{
412
415
  text: " written in ",
413
416
  paint: ctx.c.dim
414
417
  }, { text: componentPath(component) }])];
415
418
  lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
416
419
  for (const page of shown) {
417
420
  const source = ctx.sourceFor(page);
418
- lines.push(render(ctx, [{
421
+ lines.push(render$1(ctx, [{
419
422
  text: ` ${page}`,
420
423
  paint: ctx.c.dim
421
424
  }, ...source === void 0 ? [] : [{
@@ -496,12 +499,7 @@ async function resolvePages(directory, options = {}) {
496
499
  warn(await emptyDirectoryHint(shown, cwd));
497
500
  return;
498
501
  }
499
- if (unreachable.length > 0) {
500
- const verb = unreachable.length === 1 ? "file was" : "files were";
501
- warn(`${unreachable.length} ${verb} not readable, and so not audited:`);
502
- for (const file of unreachable.slice(0, 10)) note(` ${file.location} — ${file.reason}`);
503
- if (unreachable.length > 10) note(` …and ${unreachable.length - 10} more`);
504
- }
502
+ warnUnmeasured(unreachable, "file", "readable");
505
503
  return {
506
504
  pages,
507
505
  directory,
@@ -515,6 +513,22 @@ async function resolvePages(directory, options = {}) {
515
513
  };
516
514
  }
517
515
  /**
516
+ * Name what was missed, up to ten of them, and count the rest.
517
+ *
518
+ * Both collectors need this and neither may skip it: a file that could not be
519
+ * read and a URL that could not be fetched are pages with no verdict, and a run
520
+ * that mentioned neither would report the rest of the site as though it were
521
+ * the site. Ten because the point is to make the problem recognisable, and two
522
+ * hundred failures usually have the one reason the first few show — the count
523
+ * that follows is what says the list was cut.
524
+ */
525
+ function warnUnmeasured(missed, noun, reached) {
526
+ if (missed.length === 0) return;
527
+ warn(`${count(missed.length, noun)} ${missed.length === 1 ? "was" : "were"} not ${reached}, and so not audited:`);
528
+ for (const item of missed.slice(0, 10)) note(` ${item.location} — ${item.reason}`);
529
+ if (missed.length > 10) note(` …and ${missed.length - 10} more`);
530
+ }
531
+ /**
518
532
  * Fetch the pages of a running site, reporting what happened on the way.
519
533
  *
520
534
  * Returns undefined when the crawl could not start, which the caller turns into
@@ -549,12 +563,11 @@ async function crawlPages(url, options) {
549
563
  }
550
564
  const found = result.discovery === "sitemap" ? "sitemap.xml and links" : "links";
551
565
  note(`Found ${count(result.pages.length, "page")} from ${found}`);
552
- if (result.failures.length > 0) {
553
- const verb = result.failures.length === 1 ? "URL was" : "URLs were";
554
- warn(`${result.failures.length} ${verb} not fetched, and so not audited:`);
555
- for (const failure of result.failures.slice(0, 10)) note(` ${failure.url} — ${failure.reason}`);
556
- if (result.failures.length > 10) note(` …and ${result.failures.length - 10} more`);
557
- }
566
+ const failed = result.failures.map((failure) => ({
567
+ location: failure.url,
568
+ reason: failure.reason
569
+ }));
570
+ warnUnmeasured(failed, "URL", "fetched");
558
571
  if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
559
572
  const collapsed = collapsedOnto(result);
560
573
  if (collapsed.length > 0) {
@@ -569,10 +582,7 @@ async function crawlPages(url, options) {
569
582
  completeness: {
570
583
  discovery: result.discovery,
571
584
  collected: result.pages.length,
572
- unreachable: [...result.failures.map((failure) => ({
573
- location: failure.url,
574
- reason: failure.reason
575
- })), ...collapsed.map((redirect) => ({
585
+ unreachable: [...failed, ...collapsed.map((redirect) => ({
576
586
  location: redirect.requested,
577
587
  reason: `answered at ${redirect.landedOn} instead, so this page was not audited`
578
588
  }))],
@@ -639,9 +649,11 @@ async function runAuditCommand(dir, options = {}) {
639
649
  const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
640
650
  if (options.headers !== void 0 && directory !== void 0 && !options.browser) warn("--header and --basic-auth apply to pages that are fetched; this run read files.");
641
651
  try {
642
- note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
643
652
  const baseUrl = options.baseUrl ?? origin;
644
- let audits = await runEngine$1(pages, {
653
+ const { hits, misses, cache } = await splitByCache(pages, baseUrl, options);
654
+ if (misses.length === 0) note(`Nothing changed in ${label}: reusing ${count(hits.length, "page")} from the cache.`);
655
+ else note(`Auditing ${count(misses.length, "page")} in ${label}${await describeEngine(misses, options)}${hits.length === 0 ? "" : `, reusing ${count(hits.length, "unchanged page")}`}…`);
656
+ const fresh = misses.length === 0 ? [] : await runEngine$1(misses, {
645
657
  cwd: options.cwd ?? process.cwd(),
646
658
  ...options.headers === void 0 ? {} : { headers: options.headers },
647
659
  ...baseUrl === void 0 ? {} : { baseUrl },
@@ -651,10 +663,16 @@ async function runAuditCommand(dir, options = {}) {
651
663
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
652
664
  ...directory === void 0 ? {} : { directory }
653
665
  });
654
- if (!audits) return {
666
+ if (!fresh) return {
655
667
  audits: [],
656
668
  exitCode: 2
657
669
  };
670
+ for (const [index, page] of misses.entries()) {
671
+ const audit = fresh[index];
672
+ if (audit) cache?.put(page, audit);
673
+ }
674
+ await cache?.flush();
675
+ let audits = inCollectedOrder(pages, [...hits, ...fresh]);
658
676
  const failOn = options.failOn ?? "serious";
659
677
  if (options.baseline) {
660
678
  const applied = await acceptBaseline(audits, options);
@@ -688,6 +706,67 @@ async function runAuditCommand(dir, options = {}) {
688
706
  }
689
707
  }
690
708
  /**
709
+ * Which pages already have a result, and which have to be audited.
710
+ *
711
+ * Runs before the engine is imported, which is the point: a build where nothing
712
+ * changed produces no misses, and a run with no misses never loads jsdom. The
713
+ * cache itself is opened here rather than earlier so that a run given
714
+ * `--no-cache` does not read one at all.
715
+ */
716
+ async function splitByCache(pages, baseUrl, options) {
717
+ if (options.noCache) return {
718
+ hits: [],
719
+ misses: [...pages],
720
+ cache: void 0
721
+ };
722
+ const { fromEntry, openCache } = await import("./cache-D4dOIuOi.js");
723
+ const { pageUrl } = await import("./result-CRmjjoAA.js");
724
+ const axe = (await import("axe-core")).default;
725
+ const engine = options.browser ? "browser" : "jsdom";
726
+ const cache = await openCache({
727
+ toolVersion: TOOL_VERSION,
728
+ axeVersion: axe.version,
729
+ tags: DEFAULT_TAGS,
730
+ engine,
731
+ fast: options.fast === true && options.browser !== true,
732
+ ...baseUrl === void 0 ? {} : { baseUrl },
733
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
734
+ ...options.headers === void 0 ? {} : { headers: options.headers }
735
+ }, { ...options.cwd === void 0 ? {} : { cwd: options.cwd } });
736
+ const hits = [];
737
+ const misses = [];
738
+ for (const page of pages) {
739
+ const entry = cache.get(page);
740
+ if (entry === void 0) misses.push(page);
741
+ else hits.push(fromEntry(entry, page, {
742
+ url: pageUrl(page, baseUrl),
743
+ engine
744
+ }));
745
+ }
746
+ return {
747
+ hits,
748
+ misses,
749
+ cache
750
+ };
751
+ }
752
+ /**
753
+ * The audits back in the order their pages were collected in.
754
+ *
755
+ * Collection sorts by path so two runs of one build produce the same report,
756
+ * and splitting the pages into hits and misses would otherwise undo that: the
757
+ * same site would report its pages in a different order depending on which of
758
+ * them somebody had edited since yesterday.
759
+ */
760
+ function inCollectedOrder(pages, audits) {
761
+ const byPath = new Map(audits.map((audit) => [audit.relativePath, audit]));
762
+ const ordered = [];
763
+ for (const page of pages) {
764
+ const audit = byPath.get(page.relativePath);
765
+ if (audit) ordered.push(audit);
766
+ }
767
+ return ordered;
768
+ }
769
+ /**
691
770
  * Move the violations the baseline accounts for out of the failing set.
692
771
  *
693
772
  * Returns undefined when the baseline could not be read, which the caller
@@ -696,7 +775,7 @@ async function runAuditCommand(dir, options = {}) {
696
775
  * would be as wrong as silently passing.
697
776
  */
698
777
  async function acceptBaseline(audits, options) {
699
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-BscHfFSW.js");
778
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-rtnj3RHB.js");
700
779
  try {
701
780
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
702
781
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -749,25 +828,29 @@ async function loadReview(options) {
749
828
  */
750
829
  async function describeEngine(pages, options) {
751
830
  if (options.browser) return " with Chromium";
752
- const { plannedWorkers } = await import("./pool-dkexq5xo.js");
831
+ const { plannedWorkers } = await import("./pool-B4tEUa2S.js");
753
832
  const workers = options.concurrency ?? plannedWorkers(pages);
754
833
  const threads = workers > 1 ? ` across ${workers} threads` : "";
755
834
  return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
756
835
  }
757
836
  /**
758
- * Emit the chosen format, to a file when --output is given and to stdout
759
- * otherwise. Colour is dropped when writing to a file, since escape codes in a
760
- * saved report are noise.
837
+ * Render the chosen format and put it where it was asked for: a file under
838
+ * --output, stdout otherwise. Colour is dropped when writing to a file, since
839
+ * escape codes in a saved report are noise.
840
+ *
841
+ * One function rather than a renderer and a writer, because the renderer needed
842
+ * to know where the document was going anyway — which is the whole of what the
843
+ * split was carrying between them.
761
844
  */
762
845
  async function emit(audits, dir, failOn, completeness, options, review) {
763
- const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options, review);
846
+ const body = await render(audits, dir, failOn, completeness, typeof options.output === "string", options, review);
764
847
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
765
848
  if (options.output !== void 0) note(`Report written to ${options.output}`);
766
849
  }
767
- async function renderReport(audits, dir, failOn, completeness, format, toFile, options, review) {
768
- switch (format) {
850
+ async function render(audits, dir, failOn, completeness, toFile, options, review) {
851
+ switch (options.format ?? "console") {
769
852
  case "json": {
770
- const { buildJsonReport, serialiseJsonReport } = await import("./json-HJDgJp09.js");
853
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-CrQmoTj6.js");
771
854
  return serialiseJsonReport(buildJsonReport(audits, {
772
855
  directory: dir,
773
856
  ...options.url === void 0 ? {} : { sourceKind: "url" },
@@ -778,7 +861,7 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
778
861
  }));
779
862
  }
780
863
  case "sarif": {
781
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BKvq_AUm.js");
864
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BNCE0PNT.js");
782
865
  return serialiseSarifReport(buildSarifReport(audits, {
783
866
  directory: dir,
784
867
  completeness,
@@ -786,46 +869,52 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
786
869
  }));
787
870
  }
788
871
  case "html": {
789
- const { buildHtmlReport } = await import("./html-Bmh7X_sM.js");
790
- const framework = await detectedFramework(options.cwd ?? process.cwd());
872
+ const { buildHtmlReport } = await import("./html-BIxxF3FC.js");
791
873
  return buildHtmlReport(audits, {
792
- ...await sourceLookups(options.cwd ?? process.cwd(), audits),
874
+ ...await fromTheProject(audits, options),
793
875
  directory: dir,
794
876
  failOn,
795
877
  completeness,
796
878
  ...review === void 0 ? {} : { review },
797
- ...framework === void 0 ? {} : { framework },
798
879
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
799
880
  });
800
881
  }
801
- case "console": {
802
- const framework = await detectedFramework(options.cwd ?? process.cwd());
803
- return `${formatConsoleReport(audits, {
804
- ...await sourceLookups(options.cwd ?? process.cwd(), audits),
805
- dir,
806
- failOn,
807
- completeness,
808
- ...review === void 0 ? {} : { review },
809
- ...options.perPage ? { perPage: true } : {},
810
- ...options.manual ? { manual: true } : {},
811
- ...options.coverage ? { coverage: true } : {},
812
- ...framework === void 0 ? {} : { framework },
813
- ...toFile ? { color: false } : {}
814
- })}\n`;
815
- }
882
+ case "console": return `${formatConsoleReport(audits, {
883
+ ...await fromTheProject(audits, options),
884
+ dir,
885
+ failOn,
886
+ completeness,
887
+ ...review === void 0 ? {} : { review },
888
+ ...options.perPage ? { perPage: true } : {},
889
+ ...options.manual ? { manual: true } : {},
890
+ ...options.coverage ? { coverage: true } : {},
891
+ ...toFile ? { color: false } : {}
892
+ })}\n`;
816
893
  }
817
894
  }
818
895
  /**
819
- * Where a page and a failing element were written, for the two reports that say
820
- * so. Best-effort: a project using no convention this recognises gets the
821
- * report it always got, with no source named.
896
+ * What the two human-readable reports both take from the project on disk: where
897
+ * each page and each failing element was written, and what built the site.
898
+ *
899
+ * Only those two ask. JSON and SARIF are read by other programs, which have the
900
+ * project in front of them already, and building the component index for one of
901
+ * them would be several hundred milliseconds spent on nobody's behalf.
822
902
  */
823
- /** The registry id of whatever built this project, for framework-shaped advice. */
824
- async function detectedFramework(cwd) {
903
+ async function fromTheProject(audits, options) {
904
+ const cwd = options.cwd ?? process.cwd();
825
905
  const { detectFramework } = await import("./frameworks-etFg_O8K.js");
826
906
  const { readPackageJson } = await import("./project-MFrXcw1M.js");
827
- return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
907
+ const detected = await detectFramework(cwd, await readPackageJson(cwd));
908
+ return {
909
+ ...await sourceLookups(cwd, audits),
910
+ ...detected === void 0 ? {} : { framework: detected.framework.id }
911
+ };
828
912
  }
913
+ /**
914
+ * Where a page and a failing element were written, for the two reports that say
915
+ * so. Best-effort: a project using no convention this recognises gets the
916
+ * report it always got, with no source named.
917
+ */
829
918
  async function sourceLookups(cwd, audits) {
830
919
  const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
831
920
  const routes = await buildRouteMap(cwd);
@@ -1,7 +1,7 @@
1
1
  import { c as nullable, d as optional, h as string, l as number, m as safeParse, o as isoDate, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
2
  import { a as isoDate$1 } from "./text-BF1LHMiV.js";
3
3
  import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
4
- import { o as findingElements } from "./result-DfUru6y_.js";
4
+ import { s as findingElements } from "./result-B7_zVxAX.js";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  /** Default filename, used by the CLI when no path is given. */
@@ -1,2 +1,2 @@
1
- import { o as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-D2qWmUC6.js";
1
+ import { o as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-BpE7DbDq.js";
2
2
  export { BaselineError, applyBaseline, readBaseline };