eaa-kit 0.5.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 (69) hide show
  1. package/README.md +53 -5
  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-B282A3EA.js → audit-DRjAY2iY.js} +243 -107
  7. package/dist/{baseline-CuAS2lMk.js → baseline-BpE7DbDq.js} +36 -9
  8. package/dist/baseline-rtnj3RHB.js +2 -0
  9. package/dist/cache-D4dOIuOi.js +219 -0
  10. package/dist/checklist-DDd9jewi.js +44 -0
  11. package/dist/cli/index.js +130 -17
  12. package/dist/{command-C3D7JWn6.js → command-3r4fht5f.js} +102 -10
  13. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  14. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  15. package/dist/{coverage-B_Y6l-Ra.js → coverage-cb5kPp48.js} +45 -290
  16. package/dist/coverage-n1X9CkiG.js +3 -0
  17. package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
  18. package/dist/criteria-CnPJKGeb.js +286 -0
  19. package/dist/eleventy/index.d.ts +1 -1
  20. package/dist/eleventy/index.js +1 -1
  21. package/dist/{html-C4vktg0A.js → html-BIxxF3FC.js} +54 -30
  22. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  23. package/dist/index.d.ts +87 -1
  24. package/dist/index.js +3 -3
  25. package/dist/{init-CW7LfGT5.js → init-DrFfzjvm.js} +3 -3
  26. package/dist/jsdom-BUYuVJJm.js +3 -0
  27. package/dist/{jsdom-B--cEH-G.js → jsdom-CsW_xVcA.js} +11 -12
  28. package/dist/{json-DROX33kh.js → json-Bus6YhxB.js} +13 -17
  29. package/dist/{json-D_Mnnft5.js → json-CrQmoTj6.js} +1 -1
  30. package/dist/{load-yAR4wzez.js → load-BaWMa-G_.js} +11 -2
  31. package/dist/load-n62iXDq_.js +2 -0
  32. package/dist/nuxt/index.d.ts +1 -1
  33. package/dist/nuxt/index.js +1 -1
  34. package/dist/{playwright-BojtYVUa.js → playwright-ukCr_5o3.js} +3 -2
  35. package/dist/{pool-BO25OIez.js → pool-B4tEUa2S.js} +6 -2
  36. package/dist/{remediation-CMBIrnpN.js → remediation-C0klJblu.js} +35 -4
  37. package/dist/{render-DbGOVmhx.js → render-C1sUZZI_.js} +145 -6
  38. package/dist/{result-BWcYXeRs.js → result-B7_zVxAX.js} +24 -1
  39. package/dist/result-CRmjjoAA.js +2 -0
  40. package/dist/review-CdMK2GpQ.js +161 -0
  41. package/dist/review-DIbOTmOM.js +2 -0
  42. package/dist/{run-CtcEUhbe.d.ts → run-BK2blxMI.d.ts} +18 -0
  43. package/dist/{run-C2nKFcb-.js → run-DrS0Komf.js} +6 -2
  44. package/dist/{sarif-B-UBcVu8.js → sarif-BNCE0PNT.js} +24 -12
  45. package/dist/statement/templates/at.de.md +14 -0
  46. package/dist/statement/templates/at.en.md +14 -0
  47. package/dist/statement/templates/ch.de.md +14 -0
  48. package/dist/statement/templates/ch.en.md +14 -0
  49. package/dist/statement/templates/de.de.md +14 -0
  50. package/dist/statement/templates/de.en.md +14 -0
  51. package/dist/statement/templates/es.en.md +14 -0
  52. package/dist/statement/templates/es.es.md +15 -0
  53. package/dist/statement/templates/fr.en.md +14 -0
  54. package/dist/statement/templates/fr.fr.md +15 -0
  55. package/dist/statement/templates/it.en.md +14 -0
  56. package/dist/statement/templates/it.it.md +15 -0
  57. package/dist/statement/templates/nl.en.md +14 -0
  58. package/dist/statement/templates/nl.nl.md +15 -0
  59. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  60. package/dist/vite/index.d.ts +1 -1
  61. package/dist/vite/index.js +1 -1
  62. package/dist/webpack/index.d.ts +1 -1
  63. package/dist/webpack/index.js +1 -1
  64. package/dist/worksheet-BIxMtbUd.js +64 -0
  65. package/package.json +3 -2
  66. package/dist/audit-DyHPmGeD.js +0 -2
  67. package/dist/baseline-0u4df12v.js +0 -2
  68. package/dist/jsdom-22Bkt65v.js +0 -3
  69. package/dist/load-CFq2VQtT.js +0 -2
@@ -1,9 +1,13 @@
1
- import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
- import { a as plural, n as count, t as collapse } from "./text-CKKpzkYM.js";
3
- import { a as isShared, c as runCompleteness, i as groupIssues, n as blindRules, r as coverageParts, t as remediationFor } from "./remediation-CMBIrnpN.js";
4
- import { r as componentPath } from "./component-C3GL1Mnu.js";
5
- import { i as understandingUrl, n as coverageSummary, r as manualCheckFor, t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
6
- import { a as emitDocument, c as runEngine, l as warn, o as fail, s as note, t as advise } from "./command-C3D7JWn6.js";
1
+ import { a as impactLabel, i as countAtOrAbove, r as byImpactThenRule } from "./impact-luGCnN8F.js";
2
+ import { n as count, o as plural, t as collapse } from "./text-BF1LHMiV.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";
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";
8
+ import { r as componentPath } from "./component-ChisoFyY.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";
7
11
  import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
8
12
  import path from "node:path";
9
13
  import pc from "picocolors";
@@ -51,6 +55,7 @@ function context(options) {
51
55
  manual: options.manual ?? false,
52
56
  coverage: options.coverage ?? false,
53
57
  framework: options.framework,
58
+ review: options.review,
54
59
  completeness: options.completeness,
55
60
  c,
56
61
  symbol: (kind) => {
@@ -73,7 +78,7 @@ function supportsUnicode() {
73
78
  * Joins segments into one line, trimming the group to the terminal width.
74
79
  * Colour codes are applied after trimming, so they never count towards it.
75
80
  */
76
- function render(ctx, segments) {
81
+ function render$1(ctx, segments) {
77
82
  let remaining = ctx.width;
78
83
  const parts = [];
79
84
  for (const segment of segments) {
@@ -86,14 +91,14 @@ function render(ctx, segments) {
86
91
  }
87
92
  /** One line from a single segment, which is what most of this report is. */
88
93
  function line(ctx, text, paint) {
89
- return render(ctx, paint === void 0 ? [{ text }] : [{
94
+ return render$1(ctx, paint === void 0 ? [{ text }] : [{
90
95
  text,
91
96
  paint
92
97
  }]);
93
98
  }
94
99
  function headerLines(audits, ctx) {
95
- const engineLabel = (audits[0]?.engine ?? "jsdom") === "browser" ? "chromium" : "jsdom (browserless)";
96
- return [render(ctx, [{
100
+ const engineLabel = runEngine(audits) === "browser" ? "chromium" : "jsdom (browserless)";
101
+ return [render$1(ctx, [{
97
102
  text: "eaa-kit audit",
98
103
  paint: ctx.c.bold
99
104
  }, {
@@ -114,14 +119,14 @@ function legendLines(ctx) {
114
119
  function pageSection(audit, ctx) {
115
120
  const lines = [line(ctx, audit.relativePath, ctx.c.underline)];
116
121
  if (audit.error) {
117
- lines.push(render(ctx, [{
122
+ lines.push(render$1(ctx, [{
118
123
  text: ` ${ctx.symbol("error")} not audited: `,
119
124
  paint: ctx.c.red
120
125
  }, { text: audit.error }]), "");
121
126
  return lines;
122
127
  }
123
- for (const finding of sortByImpact(audit.violations)) lines.push(...violationLines(finding, ctx));
124
- for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render(ctx, [
128
+ for (const finding of [...audit.violations].sort(byImpactThenRule)) lines.push(...violationLines(finding, ctx));
129
+ for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render$1(ctx, [
125
130
  {
126
131
  text: ` ${ctx.symbol("review")} `,
127
132
  paint: ctx.c.yellow
@@ -137,7 +142,7 @@ function pageSection(audit, ctx) {
137
142
  ]));
138
143
  if (audit.violations.length === 0) {
139
144
  const clean = (audit.accepted ?? []).length === 0;
140
- lines.push(render(ctx, [{
145
+ lines.push(render$1(ctx, [{
141
146
  text: ` ${ctx.symbol("clean")} `,
142
147
  paint: clean ? ctx.c.green : ctx.c.dim
143
148
  }, {
@@ -145,7 +150,7 @@ function pageSection(audit, ctx) {
145
150
  paint: clean ? ctx.c.green : ctx.c.dim
146
151
  }]));
147
152
  }
148
- for (const finding of sortByImpact(audit.accepted ?? [])) {
153
+ for (const finding of [...audit.accepted ?? []].sort(byImpactThenRule)) {
149
154
  const elements = count(finding.nodes.length, "element");
150
155
  lines.push(line(ctx, ` · ${finding.ruleId} accepted by the baseline (${elements})`, ctx.c.dim));
151
156
  }
@@ -157,8 +162,8 @@ function coverageLine(audit, ctx) {
157
162
  return line(ctx, ` ${coverageParts(audit).join(" · ")}`, ctx.c.dim);
158
163
  }
159
164
  function violationLines(finding, ctx) {
160
- const impact = finding.impact ?? "unknown";
161
- const lines = [render(ctx, [
165
+ const impact = impactLabel(finding.impact);
166
+ const lines = [render$1(ctx, [
162
167
  {
163
168
  text: ` ${ctx.symbol("violation")} `,
164
169
  paint: ctx.c.red
@@ -181,35 +186,31 @@ function violationLines(finding, ctx) {
181
186
  return lines;
182
187
  }
183
188
  function summary(audits, ctx) {
184
- const withViolations = audits.filter((audit) => audit.violations.length > 0);
185
- const errored = audits.filter((audit) => audit.error);
186
- const ruleCount = audits.reduce((total, audit) => total + audit.violations.length, 0);
187
- const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
189
+ const totals = buildSummary(audits, ctx.failOn);
188
190
  const reviewCount = countRules(audits, "needs-review");
189
191
  const pages = count(audits.length, "page");
190
- const audited = audits.length - errored.length;
192
+ const audited = totals.pages - totals.pagesNotAudited;
191
193
  const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
192
- if (ruleCount === 0 && audited === 0 && errored.length > 0) lines.push(line(ctx, " Nothing was audited: no page could be read.", ctx.c.red));
193
- else if (ruleCount === 0) {
194
- const clean = errored.length > 0 || ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${count(audited, "page")} that were audited.` : ` No violations across ${pages}.`;
194
+ if (totals.violations === 0 && audited === 0 && totals.pagesNotAudited > 0) lines.push(line(ctx, " Nothing was audited: no page could be read.", ctx.c.red));
195
+ else if (totals.violations === 0) {
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}.`;
195
197
  lines.push(line(ctx, clean, ctx.c.green));
196
198
  } else {
197
- lines.push(render(ctx, [{
198
- text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${count(audited, "page")}`,
199
+ lines.push(render$1(ctx, [{
200
+ text: ` ${count(totals.violations, "violation")} on ${totals.pagesWithViolations} of ${count(audited, "page")}`,
199
201
  paint: ctx.c.red
200
202
  }, {
201
- text: ` (${count(elementCount, "element")})`,
203
+ text: ` (${count(totals.violatingElements, "element")})`,
202
204
  paint: ctx.c.dim
203
205
  }]));
204
- lines.push(thresholdLine(audits, ctx));
206
+ lines.push(thresholdLine(totals.failing, ctx));
205
207
  }
206
208
  if (reviewCount > 0) {
207
209
  const verb = reviewCount === 1 ? "needs" : "need";
208
210
  lines.push(line(ctx, ` ${count(reviewCount, "rule")} ${verb} manual review`, ctx.c.yellow));
209
211
  }
210
- if (errored.length > 0) lines.push(line(ctx, ` ${count(errored.length, "page")} could not be audited`, ctx.c.red));
211
- const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
212
- if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
212
+ if (totals.pagesNotAudited > 0) lines.push(line(ctx, ` ${count(totals.pagesNotAudited, "page")} could not be audited`, ctx.c.red));
213
+ if (totals.accepted > 0) lines.push(line(ctx, ` ${count(totals.accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
213
214
  lines.push(...blindSection(audits, ctx));
214
215
  lines.push(...coverageSection(audits, ctx));
215
216
  return lines;
@@ -223,12 +224,14 @@ function summary(audits, ctx) {
223
224
  * it could be read as boilerplate.
224
225
  */
225
226
  function coverageSection(audits, ctx) {
226
- const coverage = buildCoverage(audits);
227
+ const coverage = buildCoverage(audits, void 0, ctx.review);
227
228
  const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
228
229
  if (coverage.browserWouldAnswer > 0) {
229
230
  const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
230
231
  lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
231
232
  }
233
+ const review = reviewSummary(coverage);
234
+ if (review !== void 0) lines.push(...wrap(review, ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim)));
232
235
  if (!ctx.coverage) {
233
236
  lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
234
237
  return lines;
@@ -237,7 +240,7 @@ function coverageSection(audits, ctx) {
237
240
  for (const criterion of coverage.criteria) {
238
241
  const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
239
242
  const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
240
- lines.push(render(ctx, [
243
+ lines.push(render$1(ctx, [
241
244
  {
242
245
  text: ` ${criterion.number} `,
243
246
  paint: ctx.c.bold
@@ -248,6 +251,8 @@ function coverageSection(audits, ctx) {
248
251
  paint
249
252
  }
250
253
  ]));
254
+ const recorded = criterion.review;
255
+ if (recorded !== void 0) lines.push(line(ctx, ` ${reviewSentence(recorded)}`, ctx.c.dim));
251
256
  }
252
257
  return lines;
253
258
  }
@@ -270,8 +275,10 @@ const STATUS_WORDS = {
270
275
  */
271
276
  function completenessLines(ctx) {
272
277
  const completeness = ctx.completeness;
273
- if (completeness === void 0 || completeness.complete) return [];
274
- 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;
275
282
  if (completeness.unreachable.length > 0) {
276
283
  const noun = plural(completeness.unreachable.length, "page");
277
284
  const verb = completeness.unreachable.length === 1 ? "was" : "were";
@@ -285,9 +292,8 @@ function completenessLines(ctx) {
285
292
  * Why the run passed or failed. Without this, a build that exits 0 while the
286
293
  * report lists violations looks like a bug rather than a threshold choice.
287
294
  */
288
- function thresholdLine(audits, ctx) {
295
+ function thresholdLine(failing, ctx) {
289
296
  const failOn = ctx.failOn;
290
- const failing = countAtOrAbove(audits, failOn);
291
297
  if (failing === 0) return line(ctx, ` none at or above ${failOn} (--fail-on ${failOn}), so this run passes`, ctx.c.green);
292
298
  return line(ctx, ` ${failing} at or above ${failOn} (--fail-on ${failOn})`, ctx.c.red);
293
299
  }
@@ -306,7 +312,7 @@ function blindSection(audits, ctx) {
306
312
  line(ctx, " They are never reported as passing.", ctx.c.dim)
307
313
  ];
308
314
  for (const { ruleId, pages, finding } of blind) {
309
- lines.push(render(ctx, [
315
+ lines.push(render$1(ctx, [
310
316
  { text: ` ${ctx.symbol("blind")} ` },
311
317
  { text: ruleId },
312
318
  {
@@ -332,9 +338,6 @@ function countRules(audits, reason) {
332
338
  for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === reason) ruleIds.add(finding.ruleId);
333
339
  return ruleIds.size;
334
340
  }
335
- function sortByImpact(findings) {
336
- return [...findings].sort((a, b) => impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId));
337
- }
338
341
  function criteria(finding) {
339
342
  return finding.successCriteria.length > 0 ? `, WCAG ${finding.successCriteria.join(" ")}` : "";
340
343
  }
@@ -350,14 +353,13 @@ function criteria(finding) {
350
353
  function issuesSection(audits, ctx) {
351
354
  const issues = groupIssues(audits);
352
355
  if (issues.length === 0) return [];
353
- const elements = issues.reduce((total, issue) => total + issue.elements.length, 0);
354
- const occurrences = issues.reduce((total, issue) => total + issue.occurrences, 0);
356
+ const { elements, occurrences } = issueTotals(issues);
355
357
  const lines = [line(ctx, "Issues", ctx.c.bold)];
356
358
  const intro = occurrences === elements ? ` ${count(elements, "distinct element")} to fix.` : ` ${count(occurrences, "violation")} across the site come from ${count(elements, "distinct element")}.`;
357
359
  lines.push(line(ctx, intro, ctx.c.dim));
358
360
  for (const issue of issues) {
359
361
  lines.push("");
360
- lines.push(render(ctx, [
362
+ lines.push(render$1(ctx, [
361
363
  { text: ` ${ctx.symbol("violation")} ` },
362
364
  {
363
365
  text: issue.ruleId,
@@ -398,7 +400,7 @@ function remediationLines(ruleId, html, ctx) {
398
400
  const width = ctx.width - 6 - 2;
399
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}`))];
400
402
  const example = html === void 0 ? void 0 : remediation.example?.(html);
401
- if (example !== void 0) lines.push(render(ctx, [{
403
+ if (example !== void 0) lines.push(render$1(ctx, [{
402
404
  text: `${indent}→ `,
403
405
  paint: ctx.c.green
404
406
  }, { text: collapse(example) }]));
@@ -409,14 +411,14 @@ function whereLines(element, ctx) {
409
411
  const shown = element.pages.slice(0, ctx.maxNodes);
410
412
  const rest = element.pages.length - shown.length;
411
413
  const component = ctx.componentFor(element.html);
412
- const lines = component === void 0 ? [] : [render(ctx, [{
414
+ const lines = component === void 0 ? [] : [render$1(ctx, [{
413
415
  text: " written in ",
414
416
  paint: ctx.c.dim
415
417
  }, { text: componentPath(component) }])];
416
418
  lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
417
419
  for (const page of shown) {
418
420
  const source = ctx.sourceFor(page);
419
- lines.push(render(ctx, [{
421
+ lines.push(render$1(ctx, [{
420
422
  text: ` ${page}`,
421
423
  paint: ctx.c.dim
422
424
  }, ...source === void 0 ? [] : [{
@@ -497,12 +499,7 @@ async function resolvePages(directory, options = {}) {
497
499
  warn(await emptyDirectoryHint(shown, cwd));
498
500
  return;
499
501
  }
500
- if (unreachable.length > 0) {
501
- const verb = unreachable.length === 1 ? "file was" : "files were";
502
- warn(`${unreachable.length} ${verb} not readable, and so not audited:`);
503
- for (const file of unreachable.slice(0, 10)) note(` ${file.location} — ${file.reason}`);
504
- if (unreachable.length > 10) note(` …and ${unreachable.length - 10} more`);
505
- }
502
+ warnUnmeasured(unreachable, "file", "readable");
506
503
  return {
507
504
  pages,
508
505
  directory,
@@ -516,13 +513,29 @@ async function resolvePages(directory, options = {}) {
516
513
  };
517
514
  }
518
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
+ /**
519
532
  * Fetch the pages of a running site, reporting what happened on the way.
520
533
  *
521
534
  * Returns undefined when the crawl could not start, which the caller turns into
522
535
  * exit 2 — a run that reached no verdict, not a clean one.
523
536
  */
524
537
  async function crawlPages(url, options) {
525
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BUWPgaGW.js");
538
+ const { collapsedOnto, crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BHkJmUkA.js");
526
539
  let entry;
527
540
  try {
528
541
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -540,7 +553,8 @@ async function crawlPages(url, options) {
540
553
  ...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
541
554
  ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
542
555
  ...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
543
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
556
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
557
+ ...options.headers === void 0 ? {} : { headers: options.headers }
544
558
  });
545
559
  if (result.pages.length === 0 && result.failures.length > 0) {
546
560
  fail(`Could not fetch ${entry.href} (${result.failures[0]?.reason})`);
@@ -549,23 +563,29 @@ 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.`);
572
+ const collapsed = collapsedOnto(result);
573
+ if (collapsed.length > 0) {
574
+ const landedOn = collapsed[0]?.landedOn ?? "";
575
+ warn(`Every page requested answered at ${landedOn}, which is not where it was asked.`);
576
+ note(" A sign-in page in front of the site looks like this.");
577
+ note(" If it is behind one, pass --basic-auth user:password or --header \"Cookie: …\".");
578
+ }
559
579
  return {
560
580
  pages: result.pages,
561
581
  origin: result.origin,
562
582
  completeness: {
563
583
  discovery: result.discovery,
564
584
  collected: result.pages.length,
565
- unreachable: result.failures.map((failure) => ({
566
- location: failure.url,
567
- reason: failure.reason
568
- })),
585
+ unreachable: [...failed, ...collapsed.map((redirect) => ({
586
+ location: redirect.requested,
587
+ reason: `answered at ${redirect.landedOn} instead, so this page was not audited`
588
+ }))],
569
589
  truncated: result.truncated
570
590
  }
571
591
  };
@@ -627,11 +647,15 @@ async function runAuditCommand(dir, options = {}) {
627
647
  exitCode: 2
628
648
  };
629
649
  const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
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.");
630
651
  try {
631
- note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
632
652
  const baseUrl = options.baseUrl ?? origin;
633
- let audits = await runEngine(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, {
634
657
  cwd: options.cwd ?? process.cwd(),
658
+ ...options.headers === void 0 ? {} : { headers: options.headers },
635
659
  ...baseUrl === void 0 ? {} : { baseUrl },
636
660
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
637
661
  ...options.browser ? { browser: true } : {},
@@ -639,10 +663,16 @@ async function runAuditCommand(dir, options = {}) {
639
663
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
640
664
  ...directory === void 0 ? {} : { directory }
641
665
  });
642
- if (!audits) return {
666
+ if (!fresh) return {
643
667
  audits: [],
644
668
  exitCode: 2
645
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]);
646
676
  const failOn = options.failOn ?? "serious";
647
677
  if (options.baseline) {
648
678
  const applied = await acceptBaseline(audits, options);
@@ -653,7 +683,12 @@ async function runAuditCommand(dir, options = {}) {
653
683
  audits = applied;
654
684
  }
655
685
  const completeness = runCompleteness(audits, collection);
656
- await emit(audits, label, failOn, completeness, options);
686
+ const review = await loadReview(options);
687
+ if (review === FAILED) return {
688
+ audits,
689
+ exitCode: 2
690
+ };
691
+ await emit(audits, label, failOn, completeness, options, review);
657
692
  const unaudited = audits.filter((audit) => audit.error);
658
693
  if (unaudited.length > 0) {
659
694
  fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
@@ -671,6 +706,67 @@ async function runAuditCommand(dir, options = {}) {
671
706
  }
672
707
  }
673
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
+ /**
674
770
  * Move the violations the baseline accounts for out of the failing set.
675
771
  *
676
772
  * Returns undefined when the baseline could not be read, which the caller
@@ -679,7 +775,7 @@ async function runAuditCommand(dir, options = {}) {
679
775
  * would be as wrong as silently passing.
680
776
  */
681
777
  async function acceptBaseline(audits, options) {
682
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-0u4df12v.js");
778
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-rtnj3RHB.js");
683
779
  try {
684
780
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
685
781
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -697,6 +793,32 @@ async function acceptBaseline(audits, options) {
697
793
  throw cause;
698
794
  }
699
795
  }
796
+ /** Distinguishes "no review asked for" from "the review could not be read". */
797
+ const FAILED = Symbol("review-failed");
798
+ /**
799
+ * Read the review record, when one was asked for.
800
+ *
801
+ * Returns undefined when no `--review` was given, which is the ordinary case,
802
+ * and the sentinel when one was given and could not be read.
803
+ */
804
+ async function loadReview(options) {
805
+ if (options.review === void 0) return void 0;
806
+ const { answeredCount, readReview, ReviewError } = await import("./review-DIbOTmOM.js");
807
+ try {
808
+ const record = await readReview(options.review, options.cwd ?? process.cwd());
809
+ note(`Review record: ${count(answeredCount(record), "criterion")} answered`);
810
+ return {
811
+ record,
812
+ ...options.reviewMaxAge === void 0 ? {} : { maxAgeDays: options.reviewMaxAge }
813
+ };
814
+ } catch (cause) {
815
+ if (cause instanceof ReviewError) {
816
+ fail(cause.message);
817
+ return FAILED;
818
+ }
819
+ throw cause;
820
+ }
821
+ }
700
822
  /**
701
823
  * What the progress line says about the engine.
702
824
  *
@@ -706,79 +828,93 @@ async function acceptBaseline(audits, options) {
706
828
  */
707
829
  async function describeEngine(pages, options) {
708
830
  if (options.browser) return " with Chromium";
709
- const { plannedWorkers } = await import("./pool-BO25OIez.js");
831
+ const { plannedWorkers } = await import("./pool-B4tEUa2S.js");
710
832
  const workers = options.concurrency ?? plannedWorkers(pages);
711
833
  const threads = workers > 1 ? ` across ${workers} threads` : "";
712
834
  return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
713
835
  }
714
836
  /**
715
- * Emit the chosen format, to a file when --output is given and to stdout
716
- * otherwise. Colour is dropped when writing to a file, since escape codes in a
717
- * 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.
718
844
  */
719
- async function emit(audits, dir, failOn, completeness, options) {
720
- const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
845
+ async function emit(audits, dir, failOn, completeness, options, review) {
846
+ const body = await render(audits, dir, failOn, completeness, typeof options.output === "string", options, review);
721
847
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
722
848
  if (options.output !== void 0) note(`Report written to ${options.output}`);
723
849
  }
724
- async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
725
- switch (format) {
850
+ async function render(audits, dir, failOn, completeness, toFile, options, review) {
851
+ switch (options.format ?? "console") {
726
852
  case "json": {
727
- const { buildJsonReport, serialiseJsonReport } = await import("./json-D_Mnnft5.js");
853
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-CrQmoTj6.js");
728
854
  return serialiseJsonReport(buildJsonReport(audits, {
729
855
  directory: dir,
730
856
  ...options.url === void 0 ? {} : { sourceKind: "url" },
731
857
  failOn,
732
858
  completeness,
859
+ ...review === void 0 ? {} : { review },
733
860
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
734
861
  }));
735
862
  }
736
863
  case "sarif": {
737
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-B-UBcVu8.js");
864
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BNCE0PNT.js");
738
865
  return serialiseSarifReport(buildSarifReport(audits, {
739
866
  directory: dir,
740
- completeness
867
+ completeness,
868
+ ...review === void 0 ? {} : { review }
741
869
  }));
742
870
  }
743
871
  case "html": {
744
- const { buildHtmlReport } = await import("./html-C4vktg0A.js");
745
- const framework = await detectedFramework(options.cwd ?? process.cwd());
872
+ const { buildHtmlReport } = await import("./html-BIxxF3FC.js");
746
873
  return buildHtmlReport(audits, {
747
- ...await sourceLookups(options.cwd ?? process.cwd(), audits),
874
+ ...await fromTheProject(audits, options),
748
875
  directory: dir,
749
876
  failOn,
750
877
  completeness,
751
- ...framework === void 0 ? {} : { framework },
878
+ ...review === void 0 ? {} : { review },
752
879
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
753
880
  });
754
881
  }
755
- case "console": {
756
- const framework = await detectedFramework(options.cwd ?? process.cwd());
757
- return `${formatConsoleReport(audits, {
758
- ...await sourceLookups(options.cwd ?? process.cwd(), audits),
759
- dir,
760
- failOn,
761
- completeness,
762
- ...options.perPage ? { perPage: true } : {},
763
- ...options.manual ? { manual: true } : {},
764
- ...options.coverage ? { coverage: true } : {},
765
- ...framework === void 0 ? {} : { framework },
766
- ...toFile ? { color: false } : {}
767
- })}\n`;
768
- }
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`;
769
893
  }
770
894
  }
771
895
  /**
772
- * Where a page and a failing element were written, for the two reports that say
773
- * so. Best-effort: a project using no convention this recognises gets the
774
- * 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.
775
902
  */
776
- /** The registry id of whatever built this project, for framework-shaped advice. */
777
- async function detectedFramework(cwd) {
903
+ async function fromTheProject(audits, options) {
904
+ const cwd = options.cwd ?? process.cwd();
778
905
  const { detectFramework } = await import("./frameworks-etFg_O8K.js");
779
906
  const { readPackageJson } = await import("./project-MFrXcw1M.js");
780
- 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
+ };
781
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
+ */
782
918
  async function sourceLookups(cwd, audits) {
783
919
  const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
784
920
  const routes = await buildRouteMap(cwd);
@@ -786,7 +922,7 @@ async function sourceLookups(cwd, audits) {
786
922
  sourceFor: (page) => sourceFor(routes, page),
787
923
  componentFor: () => void 0
788
924
  };
789
- const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
925
+ const { buildComponentIndex, componentFor } = await import("./component-DcCn3BJS.js");
790
926
  const components = await buildComponentIndex(cwd);
791
927
  return {
792
928
  sourceFor: (page) => sourceFor(routes, page),