eaa-kit 0.4.0 → 0.6.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 (64) hide show
  1. package/README.md +66 -11
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/audit-DK2StT1_.js +2 -0
  6. package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
  7. package/dist/baseline-BscHfFSW.js +2 -0
  8. package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
  9. package/dist/checklist-BtgkxMEa.js +44 -0
  10. package/dist/cli/index.js +153 -25
  11. package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
  12. package/dist/command-CZKp9Wi_.js +251 -0
  13. package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
  14. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  15. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  16. package/dist/coverage-7bQqWNfN.js +2 -0
  17. package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
  18. package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
  19. package/dist/eleventy/index.d.ts +1 -1
  20. package/dist/eleventy/index.js +1 -1
  21. package/dist/fingerprint-BjYV_0F7.js +67 -0
  22. package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
  23. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  24. package/dist/index.d.ts +187 -5
  25. package/dist/index.js +3 -3
  26. package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
  27. package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
  28. package/dist/jsdom-DMvU2h-f.js +3 -0
  29. package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
  30. package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
  31. package/dist/load-Bc_xDGAh.js +2 -0
  32. package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
  33. package/dist/nuxt/index.d.ts +1 -1
  34. package/dist/nuxt/index.js +1 -1
  35. package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
  36. package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
  37. package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
  38. package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
  39. package/dist/review-CdMK2GpQ.js +161 -0
  40. package/dist/review-DIbOTmOM.js +2 -0
  41. package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
  42. package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
  43. package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
  44. package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
  45. package/dist/statement/templates/es.en.md +125 -0
  46. package/dist/statement/templates/es.es.md +127 -0
  47. package/dist/statement/templates/fr.en.md +128 -0
  48. package/dist/statement/templates/fr.fr.md +131 -0
  49. package/dist/statement/templates/it.en.md +127 -0
  50. package/dist/statement/templates/it.it.md +130 -0
  51. package/dist/statement/templates/nl.en.md +125 -0
  52. package/dist/statement/templates/nl.nl.md +127 -0
  53. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  54. package/dist/vite/index.d.ts +1 -1
  55. package/dist/vite/index.js +1 -1
  56. package/dist/webpack/index.d.ts +1 -1
  57. package/dist/webpack/index.js +1 -1
  58. package/dist/worksheet-eUjwYLxC.js +63 -0
  59. package/package.json +7 -3
  60. package/dist/audit-CpXH2Mk8.js +0 -2
  61. package/dist/baseline-22Y1NWxM.js +0 -2
  62. package/dist/command-D8l_oYbV.js +0 -77
  63. package/dist/fingerprint-DRoneAjj.js +0 -20
  64. package/dist/jsdom-4IMzv0eE.js +0 -3
@@ -1,6 +1,7 @@
1
- import { a as isoDate$1, c as number, l as object, m as string, p as safeParse, s as nullable, t as array, u as optional, v as withDefault } from "./schema-is6CGX2D.js";
2
- import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
3
- import { a as findingElements } from "./result-DoamKFsp.js";
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
+ import { a as isoDate$1 } from "./text-BF1LHMiV.js";
3
+ import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
4
+ import { o as findingElements } from "./result-DfUru6y_.js";
4
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  /** Default filename, used by the CLI when no path is given. */
@@ -18,7 +19,7 @@ const entrySchema = object({
18
19
  /** ISO date the entry was written. */
19
20
  acceptedOn: withDefault(string(), () => ""),
20
21
  /** ISO date after which this entry stops suppressing anything. */
21
- expiresOn: optional(isoDate$1()),
22
+ expiresOn: optional(isoDate()),
22
23
  /** Why this is being lived with. Free text, for whoever reads the file. */
23
24
  note: optional(string())
24
25
  });
@@ -37,7 +38,7 @@ var BaselineError = class extends Error {
37
38
  * can still report on what the run actually found.
38
39
  */
39
40
  function applyBaseline(audits, baseline, options = {}) {
40
- const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
41
+ const today = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
41
42
  const expired = [];
42
43
  const live = /* @__PURE__ */ new Map();
43
44
  for (const entry of baseline.entries) if (entry.expiresOn !== void 0 && entry.expiresOn < today) expired.push(entry);
@@ -93,9 +94,38 @@ function applyBaseline(audits, baseline, options = {}) {
93
94
  accepted
94
95
  };
95
96
  }
97
+ /**
98
+ * The baseline without the entries this run showed are gone.
99
+ *
100
+ * `applyBaseline` already decides which those are, and does it carefully: an
101
+ * entry counts as stale only when the page it names was audited and the element
102
+ * was not found. Entries for pages a narrowed run never looked at are not stale
103
+ * and are kept here, because from the outside a page nobody audited and a page
104
+ * that no longer exists are the same thing, and guessing wrong empties the file
105
+ * that is holding the rest of the site's build together.
106
+ *
107
+ * Nothing is added. That is the whole difference between this and recording a
108
+ * baseline again: re-recording accepts whatever the site is failing today,
109
+ * which on a bad day is a new barrier nobody agreed to.
110
+ */
111
+ function pruneBaseline(baseline, stale) {
112
+ const gone = new Set(stale.map((entry) => key(entry.page, entry.ruleId, entry.fingerprint)));
113
+ if (gone.size === 0) return {
114
+ baseline,
115
+ removed: []
116
+ };
117
+ const kept = baseline.entries.filter((entry) => !gone.has(key(entry.page, entry.ruleId, entry.fingerprint)));
118
+ return {
119
+ baseline: {
120
+ ...baseline,
121
+ entries: kept
122
+ },
123
+ removed: baseline.entries.filter((entry) => gone.has(key(entry.page, entry.ruleId, entry.fingerprint)))
124
+ };
125
+ }
96
126
  /** Record every violation a run found, so a later run can fail only on new ones. */
97
127
  function buildBaseline(audits, options = {}) {
98
- const acceptedOn = isoDate(options.today ?? /* @__PURE__ */ new Date());
128
+ const acceptedOn = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
99
129
  const entries = [];
100
130
  for (const audit of audits) for (const finding of audit.violations) for (const node of findingElements(finding)) entries.push({
101
131
  page: audit.relativePath,
@@ -109,7 +139,7 @@ function buildBaseline(audits, options = {}) {
109
139
  ...options.note ? { note: options.note } : {}
110
140
  });
111
141
  return {
112
- schemaVersion: 1,
142
+ schemaVersion: 2,
113
143
  createdOn: acceptedOn,
114
144
  entries: entries.sort(byEntry)
115
145
  };
@@ -137,7 +167,8 @@ async function readBaseline(file, cwd = process.cwd()) {
137
167
  const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
138
168
  throw new BaselineError(`${path.basename(target)} is not an eaa-kit baseline (${issues.join("; ")})`);
139
169
  }
140
- if (result.data.schemaVersion !== 1) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
170
+ if (result.data.schemaVersion !== 2) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 2.\n Record it again on the current build: eaa-kit baseline\n Read the new file before committing it: it lists what this run found, which is
171
+ not necessarily what the old one accepted.`);
141
172
  return result.data;
142
173
  }
143
174
  async function writeBaseline(file, baseline, cwd = process.cwd()) {
@@ -152,8 +183,5 @@ function key(page, ruleId, fingerprint) {
152
183
  function byEntry(a, b) {
153
184
  return a.page.localeCompare(b.page) || a.ruleId.localeCompare(b.ruleId) || a.fingerprint.localeCompare(b.fingerprint);
154
185
  }
155
- function isoDate(date) {
156
- return date.toISOString().slice(0, 10);
157
- }
158
186
  //#endregion
159
- export { readBaseline as a, buildBaseline as i, DEFAULT_BASELINE_FILE as n, serialiseBaseline as o, applyBaseline as r, writeBaseline as s, BaselineError as t };
187
+ export { pruneBaseline as a, writeBaseline as c, buildBaseline as i, DEFAULT_BASELINE_FILE as n, readBaseline as o, applyBaseline as r, serialiseBaseline as s, BaselineError as t };
@@ -0,0 +1,44 @@
1
+ import { n as count } from "./text-BF1LHMiV.js";
2
+ import { a as emitDocument, o as fail, s as note } from "./command-CZKp9Wi_.js";
3
+ import "./review-CdMK2GpQ.js";
4
+ import { access } from "node:fs/promises";
5
+ import path from "node:path";
6
+ //#region src/cli/checklist.ts
7
+ async function runChecklistCommand(options = {}) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const recordPath = options.record ?? "eaa-review.json";
10
+ const { WCAG22_AA_CRITERIA } = await import("./coverage-7bQqWNfN.js");
11
+ const { answeredCount, blankReview, readReview, ReviewError, writeReview } = await import("./review-DIbOTmOM.js");
12
+ let existing;
13
+ try {
14
+ existing = await readReview(recordPath, cwd);
15
+ } catch (cause) {
16
+ if (!(cause instanceof ReviewError)) throw cause;
17
+ if (await exists(path.resolve(cwd, recordPath))) {
18
+ fail(cause.message);
19
+ return { exitCode: 2 };
20
+ }
21
+ }
22
+ const record = blankReview(WCAG22_AA_CRITERIA, existing);
23
+ if (options.reviewedBy !== void 0) record.reviewedBy = options.reviewedBy;
24
+ await writeReview(recordPath, record, cwd);
25
+ note(existing === void 0 ? `Review record written to ${recordPath}` : `Review record updated at ${recordPath}: ${count(answeredCount(record), "criterion")} already answered`);
26
+ const { buildWorksheet } = await import("./worksheet-eUjwYLxC.js");
27
+ await emitDocument(buildWorksheet({
28
+ recordPath,
29
+ review: record
30
+ }), options.output, cwd);
31
+ if (options.output !== void 0) note(`Worksheet written to ${options.output}`);
32
+ note("Answers go in the record; the worksheet is generated and never read back.");
33
+ return { exitCode: 0 };
34
+ }
35
+ async function exists(target) {
36
+ try {
37
+ await access(target);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ //#endregion
44
+ export { runChecklistCommand };
package/dist/cli/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { c as number, f as record, l as object, m as string, n as boolean, p as safeParse, s as nullable, t as array, u as optional, v as withDefault } from "../schema-is6CGX2D.js";
3
- import { l as STATEMENT_LOCALES, o as COUNTRIES, r as loadConfig, s as ConfigError } from "../load-5wRGLvub.js";
4
- import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-DrvXRCEn.js";
5
- import { a as impactRank, n as IMPACT_LEVELS, o as isImpactLevel, s as meetsThreshold, t as DEFAULT_FAIL_ON } from "../impact-DZt2oBCP.js";
6
- import { n as count, t as collapse } from "../text-CKKpzkYM.js";
2
+ import { c as meetsThreshold, n as IMPACT_LEVELS, o as impactRank, s as isImpactLevel, t as DEFAULT_FAIL_ON } from "../impact-luGCnN8F.js";
3
+ import { c as nullable, d as optional, h as string, l as number, m as safeParse, n as boolean, p as record, t as array, u as object, y as withDefault } from "../schema-DJSF4K05.js";
4
+ import { c as ConfigError, i as loadConfig, s as COUNTRIES, u as STATEMENT_LOCALES } from "../load-vZdV_fD1.js";
5
+ import { c as StatementError, i as readAuditReport, o as checkStatementEvidence, s as refuses, t as renderStatement } from "../render-CtsqR0k-.js";
6
+ import { n as count, t as collapse } from "../text-BF1LHMiV.js";
7
7
  import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
8
- import { t as elementFingerprint } from "../fingerprint-DRoneAjj.js";
9
- import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-DB9CZGnV.js";
10
- import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-CPoZMXGM.js";
11
- import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "../command-D8l_oYbV.js";
8
+ import { t as elementFingerprint } from "../fingerprint-BjYV_0F7.js";
9
+ import { a as pruneBaseline, c as writeBaseline, i as buildBaseline, n as DEFAULT_BASELINE_FILE, o as readBaseline, r as applyBaseline, t as BaselineError } from "../baseline-D2qWmUC6.js";
10
+ import { a as emitDocument, c as runEngine, d as parseHeader, i as baselineInvocation, l as warn, n as auditDefaults, o as fail, r as auditInvocation, s as note, t as advise, u as basicAuth } from "../command-CZKp9Wi_.js";
11
+ import { t as DEFAULT_REVIEW_FILE } from "../review-CdMK2GpQ.js";
12
+ import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-e_uixhPG.js";
13
+ import { enableCompileCache } from "node:module";
12
14
  import { readFile } from "node:fs/promises";
13
15
  import path from "node:path";
14
16
  import { Command, InvalidArgumentError } from "commander";
@@ -36,16 +38,17 @@ async function runBaselineCommand(dir, options = {}) {
36
38
  entries: 0,
37
39
  exitCode: 2
38
40
  };
39
- const { pages, origin, label } = resolved;
41
+ const { pages, origin, label, directory } = resolved;
40
42
  note(`Auditing ${count(pages.length, "page")} in ${label}…`);
41
43
  const baseUrl = options.baseUrl ?? origin;
42
44
  const audits = await runEngine(pages, {
43
45
  cwd,
46
+ ...options.headers === void 0 ? {} : { headers: options.headers },
44
47
  ...baseUrl === void 0 ? {} : { baseUrl },
45
48
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
46
49
  ...options.browser ? { browser: true } : {},
47
50
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
48
- ...options.url === void 0 ? { directory: path.resolve(cwd, dir) } : {}
51
+ ...directory === void 0 ? {} : { directory }
49
52
  });
50
53
  if (!audits) return {
51
54
  entries: 0,
@@ -59,11 +62,12 @@ async function runBaselineCommand(dir, options = {}) {
59
62
  exitCode: 2
60
63
  };
61
64
  }
65
+ const target = options.output ?? "eaa-baseline.json";
66
+ if (options.prune) return await prune(audits, target, cwd);
62
67
  const baseline = buildBaseline(audits, {
63
68
  ...options.note ? { note: options.note } : {},
64
69
  ...options.expiresOn ? { expiresOn: options.expiresOn } : {}
65
70
  });
66
- const target = options.output ?? "eaa-baseline.json";
67
71
  try {
68
72
  await writeBaseline(target, baseline, cwd);
69
73
  } catch (cause) {
@@ -77,13 +81,60 @@ async function runBaselineCommand(dir, options = {}) {
77
81
  throw cause;
78
82
  }
79
83
  const entries = baseline.entries.length;
80
- note(`Wrote ${entries} ${entries === 1 ? "entry" : "entries"} to ${target}`);
84
+ note(`Wrote ${count(entries, "entry")} to ${target}`);
81
85
  if (entries > 0) advise("These are barriers, not exceptions. Commit the file, then work the list down.");
82
86
  return {
83
87
  entries,
84
88
  exitCode: 0
85
89
  };
86
90
  }
91
+ /**
92
+ * `--prune`: take out what this run shows is gone, and add nothing.
93
+ *
94
+ * The audit already tells anybody running it that entries no longer match and
95
+ * can be removed. Acting on that advice meant editing JSON by hand, entry by
96
+ * entry, against a file whose whole purpose is that nobody has to remember what
97
+ * is in it — so most people did not, and a baseline accumulated barriers that
98
+ * were fixed years ago while looking exactly like one that had not been read.
99
+ *
100
+ * Recording a new baseline is not the same operation and is not a substitute:
101
+ * it accepts whatever the site fails today, which on a bad day quietly adopts a
102
+ * barrier nobody agreed to.
103
+ */
104
+ async function prune(audits, target, cwd) {
105
+ let existing;
106
+ try {
107
+ existing = await readBaseline(target, cwd);
108
+ } catch (cause) {
109
+ if (cause instanceof BaselineError) {
110
+ fail(cause.message);
111
+ return {
112
+ entries: 0,
113
+ exitCode: 2
114
+ };
115
+ }
116
+ throw cause;
117
+ }
118
+ const outcome = applyBaseline(audits, existing);
119
+ const { baseline, removed } = pruneBaseline(existing, outcome.stale);
120
+ if (removed.length === 0) {
121
+ note(`Nothing to remove from ${target}: every entry still matches something on this run`);
122
+ return {
123
+ entries: existing.entries.length,
124
+ removed: 0,
125
+ exitCode: 0
126
+ };
127
+ }
128
+ await writeBaseline(target, baseline, cwd);
129
+ note(`Removed ${count(removed.length, "entry")} from ${target}; ${count(baseline.entries.length, "entry")} left`);
130
+ for (const entry of removed) note(` ${entry.page} ${entry.ruleId}`);
131
+ if (outcome.expired.length > 0) advise(`${count(outcome.expired.length, "entry")} have expired and suppress nothing; --prune leaves them for you to decide about`);
132
+ return {
133
+ entries: baseline.entries.length,
134
+ removed: removed.length,
135
+ exitCode: 0
136
+ };
137
+ }
87
138
  var DiffError = class extends Error {
88
139
  name = "DiffError";
89
140
  };
@@ -222,7 +273,7 @@ async function readReport(file, cwd = process.cwd()) {
222
273
  const first = result.error.issues[0];
223
274
  throw new DiffError(`${file} is not an eaa-kit JSON report${first === void 0 || first.path.length === 0 ? "" : ` at ${first.path.join(".")}`}: ${first?.message ?? "unrecognised shape"}`);
224
275
  }
225
- if (result.data.schemaVersion !== 1) throw new DiffError(`${file} is schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
276
+ if (result.data.schemaVersion !== 2) throw new DiffError(`${file} is schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 2.\n Write both reports with the same version of eaa-kit before comparing them.`);
226
277
  return result.data;
227
278
  }
228
279
  //#endregion
@@ -352,6 +403,23 @@ async function runStatementCommand(options = {}) {
352
403
  });
353
404
  let audit;
354
405
  if (options.audit) audit = await readAuditReport(options.audit, options.cwd ?? process.cwd());
406
+ let review;
407
+ if (options.review) {
408
+ const { readReview } = await import("../review-DIbOTmOM.js");
409
+ review = await readReview(options.review, options.cwd ?? process.cwd());
410
+ }
411
+ const problems = checkStatementEvidence({
412
+ config,
413
+ ...audit ? { audit } : {},
414
+ ...review ? { review } : {}
415
+ });
416
+ for (const problem of problems) if (problem.severity === "refuses") fail(problem.message);
417
+ else advise(problem.message);
418
+ if (refuses(problems)) return {
419
+ document: "",
420
+ format,
421
+ exitCode: 2
422
+ };
355
423
  const statement = await renderStatement(config, {
356
424
  ...options.locale ? { locale: options.locale } : {},
357
425
  ...options.country ? { country: options.country } : {},
@@ -373,7 +441,8 @@ async function runStatementCommand(options = {}) {
373
441
  exitCode: 0
374
442
  };
375
443
  } catch (cause) {
376
- if (cause instanceof ConfigError || cause instanceof StatementError) {
444
+ const { ReviewError } = await import("../review-DIbOTmOM.js");
445
+ if (cause instanceof ConfigError || cause instanceof StatementError || cause instanceof ReviewError) {
377
446
  fail(cause.message);
378
447
  if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
379
448
  return {
@@ -396,6 +465,30 @@ function formatFor(output) {
396
465
  //#endregion
397
466
  //#region src/cli/index.ts
398
467
  /**
468
+ * Reuse the compiled form of this tool and its dependencies between runs.
469
+ *
470
+ * Most of what a short audit costs is not the audit. jsdom alone is ~700 ms to
471
+ * import on a warm page cache, and V8 pays to parse and compile it from source
472
+ * on every single run — of a CLI somebody invokes from a build script, over and
473
+ * over, against code that has not changed since the last time. Node can cache
474
+ * the compiled bytecode instead, which is exactly the shape of this problem.
475
+ *
476
+ * Measured over ten pages: 2350 ms without, 2075 ms with. The saving is a fixed
477
+ * cost, so it counts for most on the small sites where fixed cost is most of
478
+ * the run.
479
+ *
480
+ * Best-effort by design. It is a cache: a read-only filesystem, a sandbox that
481
+ * forbids the directory, or a Node built without the feature all mean the run
482
+ * is a little slower, and none of them is a reason to fail an audit. Node's own
483
+ * NODE_DISABLE_COMPILE_CACHE is honoured by the call itself.
484
+ */
485
+ function useCompileCache() {
486
+ try {
487
+ enableCompileCache();
488
+ } catch {}
489
+ }
490
+ useCompileCache();
491
+ /**
399
492
  * A parser for a fixed set of words, listing them when the answer is not one.
400
493
  *
401
494
  * `normalise` is for `--country de`, where the accepted spelling differs from
@@ -428,19 +521,48 @@ const parseFormat = oneOf(OUTPUT_FORMATS);
428
521
  const parsePositive = wholeNumber(1);
429
522
  const parseDepth = wholeNumber(0);
430
523
  const parseConcurrency = wholeNumber(1);
524
+ /**
525
+ * `--header` repeated, collected into a list the command turns into a record.
526
+ *
527
+ * Validated here so a typo stops the run before a site is crawled with a
528
+ * malformed credential — and the error names the header rather than repeating
529
+ * the value, because a bad `--header` is exactly the case where the value is a
530
+ * token and the terminal is a CI log.
531
+ */
532
+ function collectHeader(value, previous = []) {
533
+ try {
534
+ parseHeader(value);
535
+ } catch (cause) {
536
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
537
+ }
538
+ return [...previous, value];
539
+ }
540
+ function parseBasicAuth(value) {
541
+ try {
542
+ basicAuth(value);
543
+ } catch (cause) {
544
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
545
+ }
546
+ return value;
547
+ }
431
548
  const program = new Command();
432
549
  program.exitOverride();
433
550
  program.name("eaa-kit").description("WCAG 2.2 AA auditor and EU accessibility statement generator for static sites.\nNot legal advice.").version(TOOL_VERSION, "-v, --version");
434
- program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")})`, parseImpact, DEFAULT_FAIL_ON).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")})`, parseFormat, "console").option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--concurrency <n>", "worker threads to audit with, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").action(async (dir, flags) => {
435
- const { build, ...options } = flags;
436
- const { exitCode } = await runAuditCommand(dir, {
437
- ...options,
438
- ...build === false ? { noBuild: true } : {}
439
- });
551
+ program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")}; default: ${DEFAULT_FAIL_ON})`, parseImpact).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")}; default: console)`, parseFormat).option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--fast", "skip the rules the browserless engine cannot decide anyway, losing their element lists").option("--concurrency <n>", "pages to audit at once, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").option("--review <path>", "what a person checked, from eaa-kit checklist").option("--review-max-age <days>", "stop counting review entries older than this", parseDepth).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
552
+ const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
553
+ const invocation = auditInvocation(dir, defaults, flags);
554
+ const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
555
+ process.exitCode = exitCode;
556
+ });
557
+ program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site (default: ./dist)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--prune", "remove the entries this run shows are gone, instead of recording a new baseline").option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "pages to audit at once, or 1 for none", parseConcurrency).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
558
+ const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
559
+ const invocation = baselineInvocation(dir, defaults, flags);
560
+ const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
440
561
  process.exitCode = exitCode;
441
562
  });
442
- program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site", "./dist").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "worker threads to audit with, or 1 for none", parseConcurrency).action(async (dir, flags) => {
443
- const { exitCode } = await runBaselineCommand(dir, flags);
563
+ program.command("checklist").description("Write the manual review: the 34 WCAG criteria no automated rule can reach").option("--record <path>", `where the answers live (default: ${DEFAULT_REVIEW_FILE})`).option("--output <path>", "write the worksheet here instead of stdout").option("--reviewed-by <name>", "who is carrying out the review").action(async (flags) => {
564
+ const { runChecklistCommand } = await import("../checklist-BtgkxMEa.js");
565
+ const { exitCode } = await runChecklistCommand(flags);
444
566
  process.exitCode = exitCode;
445
567
  });
446
568
  program.command("diff").description("Compare two JSON reports: what a change made worse, and what it fixed").argument("<before>", "JSON report from before the change").argument("<after>", "JSON report from after it").option("--format <format>", `output format (${DIFF_FORMATS.join("|")})`, oneOf(DIFF_FORMATS)).option("--output <path>", "write the diff here instead of stdout").option("--fail-on <impact>", `exit 1 on NEW violations at or above this impact (${IMPACT_LEVELS.join("|")})`, parseImpact, DEFAULT_FAIL_ON).action(async (before, after, flags) => {
@@ -448,11 +570,11 @@ program.command("diff").description("Compare two JSON reports: what a change mad
448
570
  process.exitCode = exitCode;
449
571
  });
450
572
  program.command("init").description("Write an eaa.config.json to generate statements from").option("--output <path>", "write here instead of eaa.config.json").option("--force", "overwrite a config that is already there").option("-y, --yes", "take every default without asking").action(async (flags) => {
451
- const { runInitCommand } = await import("../init-DIWDE35F.js");
573
+ const { runInitCommand } = await import("../init-BjKYttxc.js");
452
574
  const { exitCode } = await runInitCommand(flags);
453
575
  process.exitCode = exitCode;
454
576
  });
455
- program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
577
+ program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--review <path>", "check the conformance claim against a review record; nothing from it is published").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
456
578
  const { lang, ...options } = flags;
457
579
  const { exitCode } = await runStatementCommand({
458
580
  ...options,
@@ -463,6 +585,12 @@ program.command("statement").description("Generate an EU accessibility statement
463
585
  try {
464
586
  await program.parseAsync(process.argv);
465
587
  } catch (cause) {
588
+ if (cause instanceof ConfigError) {
589
+ fail(cause.message);
590
+ for (const issue of cause.issues) note(` ${issue}`);
591
+ process.exitCode = 2;
592
+ process.exit(2);
593
+ }
466
594
  const error = cause;
467
595
  if (typeof error.exitCode === "number") process.exitCode = error.exitCode === 0 ? 0 : 2;
468
596
  else {
@@ -46,11 +46,32 @@ async function collectPages(dir, options = {}) {
46
46
  const pages = [];
47
47
  for (let i = 0; i < relativePaths.length; i += READ_CONCURRENCY) {
48
48
  const batch = relativePaths.slice(i, i + READ_CONCURRENCY);
49
- const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable)));
49
+ const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable, options.maxBytes ?? 33554432)));
50
50
  for (const page of read) if (page !== void 0) pages.push(page);
51
51
  }
52
52
  return pages;
53
53
  }
54
+ /**
55
+ * Whether the directory holds any HTML at all, ignoring include and exclude.
56
+ *
57
+ * Asked when a run collected nothing, to tell two different mistakes apart: a
58
+ * build directory with no pages in it, and a directory full of pages that the
59
+ * caller's own filters excluded. The advice for the first is wrong for the
60
+ * second — it names another directory to audit — so the question has to be
61
+ * answered before it is given.
62
+ *
63
+ * Globs rather than collecting: this only needs to know whether one file
64
+ * exists, and reading every page of a large build to answer that on a path that
65
+ * is about to print a warning and stop would be work for nothing.
66
+ */
67
+ async function holdsHtml(dir) {
68
+ return (await glob([...DEFAULT_INCLUDE], {
69
+ cwd: path.resolve(dir),
70
+ ignore: [...DEFAULT_EXCLUDE],
71
+ onlyFiles: true,
72
+ dot: false
73
+ })).length > 0;
74
+ }
54
75
  async function assertDirectory(root, original) {
55
76
  let stats;
56
77
  try {
@@ -61,9 +82,15 @@ async function assertDirectory(root, original) {
61
82
  }
62
83
  if (!stats.isDirectory()) throw new BuildDirectoryError(`Build path is not a directory: ${original}`, root);
63
84
  }
64
- async function readPage(root, relativePath, onUnreadable) {
85
+ async function readPage(root, relativePath, onUnreadable, maxBytes) {
65
86
  const absolutePath = path.join(root, relativePath);
66
87
  try {
88
+ const { size } = await stat(absolutePath);
89
+ if (size > maxBytes) {
90
+ if (onUnreadable === void 0) throw new Error(`${relativePath} is ${size} bytes, over the ${maxBytes} byte limit`);
91
+ onUnreadable(relativePath, `${megabytes(size)} MB, over the ${megabytes(maxBytes)} MB limit for one page`);
92
+ return;
93
+ }
67
94
  return {
68
95
  absolutePath,
69
96
  relativePath,
@@ -130,5 +157,9 @@ function generic(head) {
130
157
  function article(name) {
131
158
  return /^[aeiou]/i.test(name) ? "an" : "a";
132
159
  }
160
+ /** Sizes in the megabytes a person reads, not the bytes a computer counts. */
161
+ function megabytes(bytes) {
162
+ return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
163
+ }
133
164
  //#endregion
134
- export { stripBom as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };
165
+ export { stripBom as a, holdsHtml as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };