eaa-kit 0.4.0 → 0.5.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 (54) hide show
  1. package/README.md +31 -9
  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-CPoZMXGM.js → audit-B282A3EA.js} +43 -21
  6. package/dist/audit-DyHPmGeD.js +2 -0
  7. package/dist/{baseline-22Y1NWxM.js → baseline-0u4df12v.js} +1 -1
  8. package/dist/{baseline-DB9CZGnV.js → baseline-CuAS2lMk.js} +6 -5
  9. package/dist/cli/index.js +50 -19
  10. package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
  11. package/dist/command-C3D7JWn6.js +160 -0
  12. package/dist/{coverage-B4IBKMO8.js → coverage-B_Y6l-Ra.js} +3 -2
  13. package/dist/{crawl-BiI1Lau_.js → crawl-BUWPgaGW.js} +83 -9
  14. package/dist/eleventy/index.d.ts +1 -1
  15. package/dist/eleventy/index.js +1 -1
  16. package/dist/fingerprint-BjYV_0F7.js +67 -0
  17. package/dist/{html-BGTO3ypW.js → html-C4vktg0A.js} +3 -3
  18. package/dist/index.d.ts +119 -4
  19. package/dist/index.js +2 -2
  20. package/dist/{init-DIWDE35F.js → init-CW7LfGT5.js} +10 -4
  21. package/dist/jsdom-22Bkt65v.js +3 -0
  22. package/dist/{jsdom-DCpGSLfW.js → jsdom-B--cEH-G.js} +12 -4
  23. package/dist/{json-DjEvy1nX.js → json-DROX33kh.js} +4 -4
  24. package/dist/{json-QQuFIw1W.js → json-D_Mnnft5.js} +1 -1
  25. package/dist/load-CFq2VQtT.js +2 -0
  26. package/dist/{load-5wRGLvub.js → load-yAR4wzez.js} +133 -8
  27. package/dist/nuxt/index.d.ts +1 -1
  28. package/dist/nuxt/index.js +1 -1
  29. package/dist/{playwright-BWniOain.js → playwright-BojtYVUa.js} +30 -6
  30. package/dist/{pool-BMevaLWD.js → pool-BO25OIez.js} +52 -2
  31. package/dist/{remediation-Dtowi2EC.js → remediation-CMBIrnpN.js} +2 -2
  32. package/dist/{render-DrvXRCEn.js → render-DbGOVmhx.js} +47 -9
  33. package/dist/{result-DoamKFsp.js → result-BWcYXeRs.js} +37 -3
  34. package/dist/{run-DB34BSOZ.js → run-C2nKFcb-.js} +1 -1
  35. package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
  36. package/dist/{sarif-SR3_lLYd.js → sarif-B-UBcVu8.js} +3 -3
  37. package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
  38. package/dist/statement/templates/es.en.md +125 -0
  39. package/dist/statement/templates/es.es.md +127 -0
  40. package/dist/statement/templates/fr.en.md +128 -0
  41. package/dist/statement/templates/fr.fr.md +131 -0
  42. package/dist/statement/templates/it.en.md +127 -0
  43. package/dist/statement/templates/it.it.md +130 -0
  44. package/dist/statement/templates/nl.en.md +125 -0
  45. package/dist/statement/templates/nl.nl.md +127 -0
  46. package/dist/vite/index.d.ts +1 -1
  47. package/dist/vite/index.js +1 -1
  48. package/dist/webpack/index.d.ts +1 -1
  49. package/dist/webpack/index.js +1 -1
  50. package/package.json +6 -2
  51. package/dist/audit-CpXH2Mk8.js +0 -2
  52. package/dist/command-D8l_oYbV.js +0 -77
  53. package/dist/fingerprint-DRoneAjj.js +0 -20
  54. package/dist/jsdom-4IMzv0eE.js +0 -3
package/README.md CHANGED
@@ -3,15 +3,17 @@
3
3
  [![CI](https://github.com/likeBloodMoon/eaa-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/likeBloodMoon/eaa-kit/actions/workflows/ci.yml)
4
4
 
5
5
  Build-time WCAG 2.2 AA auditor and EU accessibility statement generator for static sites,
6
- aimed at freelancers and small agencies in the DACH region who have to comply with the
7
- European Accessibility Act (in force since 28 June 2025), the BFSG in Germany and the BaFG
8
- in Austria.
6
+ built for the freelancers and small agencies who have to comply with the European
7
+ Accessibility Act (in force since 28 June 2025) without an accessibility budget. It started
8
+ in the DACH region — the BFSG in Germany, the BaFG in Austria — and the statement now names
9
+ the statute and supervisory body of **seven countries**: Austria, Germany, Switzerland,
10
+ Spain, France, Italy and the Netherlands, each in its own language as well as English.
9
11
 
10
12
  ```bash
11
13
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
12
14
  npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
13
15
  npx eaa-kit init # write an eaa.config.json
14
- npx eaa-kit statement # Barrierefreiheitserklärung from that config
16
+ npx eaa-kit statement # accessibility statement, in one of seven countries
15
17
  ```
16
18
 
17
19
  > **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
@@ -33,7 +35,9 @@ supported here either.
33
35
  **Audits your build.** Globs the HTML out of `./dist`, parses it with jsdom and runs
34
36
  axe-core against it. No Chromium download, fast enough for CI, and it never fetches
35
37
  anything or executes your site's JavaScript. `--browser` swaps in real Chromium for the
36
- rules that need layout and CSS.
38
+ rules that need layout and CSS; `--fast` goes the other way and skips the rules the
39
+ browserless engine cannot decide anyway, rather than running them and discarding the
40
+ answer.
37
41
 
38
42
  ```bash
39
43
  eaa-kit audit ./dist --fail-on serious
@@ -46,14 +50,21 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead:
46
50
  eaa-kit audit --url http://localhost:3000
47
51
  ```
48
52
 
49
- **Writes the statement.** A Barrierefreiheitserklärung from one config file, in German or
50
- English, as Markdown or HTML, naming the statute and supervisory body of Austria,
51
- Switzerland or Germany and optionally listing the barriers a real audit found.
53
+ **Writes the statement.** A Barrierefreiheitserklärung, a déclaration d'accessibilité, a
54
+ dichiarazione di accessibilità whatever the country calls it from one config file, as
55
+ Markdown or HTML, naming that country's statute and supervisory body, and optionally
56
+ listing the barriers a real audit found.
52
57
 
53
58
  ```bash
54
59
  eaa-kit statement --output src/content/a11y.md
60
+ eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH
55
61
  ```
56
62
 
63
+ Each country's statement is a document under its own law rather than a translation of
64
+ another's, so where a national regime asks for more than this — France's RGAA declaration,
65
+ Italy's filing with AgID — the text says so instead of letting a generated file look like
66
+ it settles the matter.
67
+
57
68
  **Fails only on what is new.** The first run on a site that already exists finds
58
69
  everything at once. A baseline records what is already wrong so the build fails on
59
70
  regressions instead, without ever letting an accepted barrier look like a passing one.
@@ -96,6 +107,16 @@ fetch forty URLs, no longer produces a report that looks like a complete one.
96
107
  SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
97
108
  it is.
98
109
 
110
+ **Says it once.** The flags a project runs on every build belong in the project, not in the
111
+ build script that repeats them. `eaa.config` — the same file the statement reads — takes an
112
+ [`audit` block](docs/audit.md#defaults-from-eaaconfig) of defaults, and `baseline` reads the
113
+ keys that mean the same thing to it. A flag you type still wins, so a one-off `--browser`
114
+ check needs no edit to a committed file.
115
+
116
+ ```jsonc
117
+ { "audit": { "dir": "build", "failOn": "critical", "browser": true } }
118
+ ```
119
+
99
120
  **Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
100
121
  and Remix too, and integrations for
101
122
  [Astro](docs/integrations.md#astro-integration),
@@ -117,7 +138,8 @@ eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
117
138
  | | |
118
139
  | --- | --- |
119
140
  | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
120
- | [The statement command](docs/statement.md) | The config file, the three countries, and filling a statement from audit results |
141
+ | [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them |
142
+ | [The statement command](docs/statement.md) | The config file, the seven countries, and filling a statement from audit results |
121
143
  | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
122
144
  | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
123
145
  | [Coverage of WCAG](docs/audit.md#how-much-of-wcag-a-run-reaches) | What an automated engine can reach at all, and what it cannot |
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions } from "../run-BMASMmwO.js";
1
+ import { n as IntegrationOptions } from "../run-CtcEUhbe.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-DB34BSOZ.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-C2nKFcb-.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,5 @@
1
- import { t as auditPage } from "../../jsdom-DCpGSLfW.js";
1
+ import { t as auditPage } from "../../jsdom-B--cEH-G.js";
2
+ import { enableCompileCache } from "node:module";
2
3
  import { parentPort, workerData } from "node:worker_threads";
3
4
  //#region src/audit/runners/worker.ts
4
5
  /**
@@ -17,6 +18,9 @@ import { parentPort, workerData } from "node:worker_threads";
17
18
  * something catastrophic — the thread running out of memory — reaches the
18
19
  * pool's error handler instead.
19
20
  */
21
+ try {
22
+ enableCompileCache();
23
+ } catch {}
20
24
  const port = parentPort;
21
25
  if (!port) throw new Error("eaa-kit audit worker was started outside a worker thread");
22
26
  const options = workerData ?? {};
@@ -1,10 +1,10 @@
1
1
  import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
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-Dtowi2EC.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
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-B4IBKMO8.js";
6
- import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-D8l_oYbV.js";
7
- import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-CFM8gEVv.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";
7
+ import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
8
8
  import path from "node:path";
9
9
  import pc from "picocolors";
10
10
  //#region src/audit/report/console.ts
@@ -187,13 +187,15 @@ function summary(audits, ctx) {
187
187
  const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
188
188
  const reviewCount = countRules(audits, "needs-review");
189
189
  const pages = count(audits.length, "page");
190
+ const audited = audits.length - errored.length;
190
191
  const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
191
- if (ruleCount === 0) {
192
- const clean = ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${pages} that were audited.` : ` No violations across ${pages}.`;
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}.`;
193
195
  lines.push(line(ctx, clean, ctx.c.green));
194
196
  } else {
195
197
  lines.push(render(ctx, [{
196
- text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${pages}`,
198
+ text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${count(audited, "page")}`,
197
199
  paint: ctx.c.red
198
200
  }, {
199
201
  text: ` (${count(elementCount, "element")})`,
@@ -272,7 +274,8 @@ function completenessLines(ctx) {
272
274
  const lines = [];
273
275
  if (completeness.unreachable.length > 0) {
274
276
  const noun = plural(completeness.unreachable.length, "page");
275
- lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and were not audited`, ctx.c.yellow));
277
+ const verb = completeness.unreachable.length === 1 ? "was" : "were";
278
+ lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and ${verb} not audited`, ctx.c.yellow));
276
279
  }
277
280
  if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
278
281
  if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
@@ -484,6 +487,13 @@ async function resolvePages(directory, options = {}) {
484
487
  return;
485
488
  }
486
489
  if (pages.length === 0) {
490
+ if ((options.include !== void 0 || options.exclude !== void 0) && await holdsHtml(directory)) {
491
+ warn(`No page in ${shown} matched the filters, so nothing was audited.`);
492
+ if (options.include !== void 0) note(` --include ${options.include.join(" ")}`);
493
+ if (options.exclude !== void 0) note(` --exclude ${options.exclude.join(" ")}`);
494
+ note(" Patterns are relative to the audited directory, with POSIX separators.");
495
+ return;
496
+ }
487
497
  warn(await emptyDirectoryHint(shown, cwd));
488
498
  return;
489
499
  }
@@ -495,6 +505,7 @@ async function resolvePages(directory, options = {}) {
495
505
  }
496
506
  return {
497
507
  pages,
508
+ directory,
498
509
  label: shown,
499
510
  completeness: {
500
511
  discovery: "directory",
@@ -511,7 +522,7 @@ async function resolvePages(directory, options = {}) {
511
522
  * exit 2 — a run that reached no verdict, not a clean one.
512
523
  */
513
524
  async function crawlPages(url, options) {
514
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BiI1Lau_.js");
525
+ const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BUWPgaGW.js");
515
526
  let entry;
516
527
  try {
517
528
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -609,12 +620,13 @@ const OUTPUT_FORMATS = [
609
620
  * piped somewhere without the chatter coming along.
610
621
  */
611
622
  async function runAuditCommand(dir, options = {}) {
623
+ if (options.fast && options.browser) warn("--fast has no effect with --browser: a real browser can decide those rules.");
612
624
  const resolved = await resolvePages(dir, options);
613
625
  if (!resolved) return {
614
626
  audits: [],
615
627
  exitCode: 2
616
628
  };
617
- const { pages, origin, label, cleanup, completeness: collection } = resolved;
629
+ const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
618
630
  try {
619
631
  note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
620
632
  const baseUrl = options.baseUrl ?? origin;
@@ -623,8 +635,9 @@ async function runAuditCommand(dir, options = {}) {
623
635
  ...baseUrl === void 0 ? {} : { baseUrl },
624
636
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
625
637
  ...options.browser ? { browser: true } : {},
638
+ ...options.fast && !options.browser ? { fast: true } : {},
626
639
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
627
- ...options.url === void 0 && dir !== void 0 ? { directory: dir } : {}
640
+ ...directory === void 0 ? {} : { directory }
628
641
  });
629
642
  if (!audits) return {
630
643
  audits: [],
@@ -666,7 +679,7 @@ async function runAuditCommand(dir, options = {}) {
666
679
  * would be as wrong as silently passing.
667
680
  */
668
681
  async function acceptBaseline(audits, options) {
669
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-22Y1NWxM.js");
682
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-0u4df12v.js");
670
683
  try {
671
684
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
672
685
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -693,9 +706,10 @@ async function acceptBaseline(audits, options) {
693
706
  */
694
707
  async function describeEngine(pages, options) {
695
708
  if (options.browser) return " with Chromium";
696
- const { plannedWorkers } = await import("./pool-BMevaLWD.js");
709
+ const { plannedWorkers } = await import("./pool-BO25OIez.js");
697
710
  const workers = options.concurrency ?? plannedWorkers(pages);
698
- return workers > 1 ? ` across ${workers} threads` : "";
711
+ const threads = workers > 1 ? ` across ${workers} threads` : "";
712
+ return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
699
713
  }
700
714
  /**
701
715
  * Emit the chosen format, to a file when --output is given and to stdout
@@ -710,7 +724,7 @@ async function emit(audits, dir, failOn, completeness, options) {
710
724
  async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
711
725
  switch (format) {
712
726
  case "json": {
713
- const { buildJsonReport, serialiseJsonReport } = await import("./json-QQuFIw1W.js");
727
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-D_Mnnft5.js");
714
728
  return serialiseJsonReport(buildJsonReport(audits, {
715
729
  directory: dir,
716
730
  ...options.url === void 0 ? {} : { sourceKind: "url" },
@@ -720,17 +734,17 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
720
734
  }));
721
735
  }
722
736
  case "sarif": {
723
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-SR3_lLYd.js");
737
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-B-UBcVu8.js");
724
738
  return serialiseSarifReport(buildSarifReport(audits, {
725
739
  directory: dir,
726
740
  completeness
727
741
  }));
728
742
  }
729
743
  case "html": {
730
- const { buildHtmlReport } = await import("./html-BGTO3ypW.js");
744
+ const { buildHtmlReport } = await import("./html-C4vktg0A.js");
731
745
  const framework = await detectedFramework(options.cwd ?? process.cwd());
732
746
  return buildHtmlReport(audits, {
733
- ...await sourceLookups(options.cwd ?? process.cwd()),
747
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
734
748
  directory: dir,
735
749
  failOn,
736
750
  completeness,
@@ -741,7 +755,7 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
741
755
  case "console": {
742
756
  const framework = await detectedFramework(options.cwd ?? process.cwd());
743
757
  return `${formatConsoleReport(audits, {
744
- ...await sourceLookups(options.cwd ?? process.cwd()),
758
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
745
759
  dir,
746
760
  failOn,
747
761
  completeness,
@@ -765,15 +779,23 @@ async function detectedFramework(cwd) {
765
779
  const { readPackageJson } = await import("./project-MFrXcw1M.js");
766
780
  return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
767
781
  }
768
- async function sourceLookups(cwd) {
782
+ async function sourceLookups(cwd, audits) {
769
783
  const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
770
- const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
771
784
  const routes = await buildRouteMap(cwd);
785
+ if (!hasElementsToAttribute(audits)) return {
786
+ sourceFor: (page) => sourceFor(routes, page),
787
+ componentFor: () => void 0
788
+ };
789
+ const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
772
790
  const components = await buildComponentIndex(cwd);
773
791
  return {
774
792
  sourceFor: (page) => sourceFor(routes, page),
775
793
  componentFor: (html) => componentFor(components, html)
776
794
  };
777
795
  }
796
+ /** Whether any element will be listed, and so looked up against the source. */
797
+ function hasElementsToAttribute(audits) {
798
+ return audits.some((audit) => audit.violations.length > 0 || (audit.accepted?.length ?? 0) > 0);
799
+ }
778
800
  //#endregion
779
801
  export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-B282A3EA.js";
2
+ export { runAuditCommand };
@@ -1,2 +1,2 @@
1
- import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-DB9CZGnV.js";
1
+ import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-CuAS2lMk.js";
2
2
  export { BaselineError, applyBaseline, readBaseline };
@@ -1,6 +1,6 @@
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$1, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
+ import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
3
+ import { o as findingElements } from "./result-BWcYXeRs.js";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  /** Default filename, used by the CLI when no path is given. */
@@ -109,7 +109,7 @@ function buildBaseline(audits, options = {}) {
109
109
  ...options.note ? { note: options.note } : {}
110
110
  });
111
111
  return {
112
- schemaVersion: 1,
112
+ schemaVersion: 2,
113
113
  createdOn: acceptedOn,
114
114
  entries: entries.sort(byEntry)
115
115
  };
@@ -137,7 +137,8 @@ async function readBaseline(file, cwd = process.cwd()) {
137
137
  const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
138
138
  throw new BaselineError(`${path.basename(target)} is not an eaa-kit baseline (${issues.join("; ")})`);
139
139
  }
140
- if (result.data.schemaVersion !== 1) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
140
+ 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
141
+ not necessarily what the old one accepted.`);
141
142
  return result.data;
142
143
  }
143
144
  async function writeBaseline(file, baseline, cwd = process.cwd()) {
package/dist/cli/index.js CHANGED
@@ -1,14 +1,15 @@
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
2
  import { a as impactRank, n as IMPACT_LEVELS, o as isImpactLevel, s as meetsThreshold, t as DEFAULT_FAIL_ON } from "../impact-DZt2oBCP.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-yAR4wzez.js";
5
+ import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-DbGOVmhx.js";
6
6
  import { n as count, t as collapse } from "../text-CKKpzkYM.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 { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-CuAS2lMk.js";
10
+ import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-B282A3EA.js";
11
+ import { a as emitDocument, c as runEngine, i as baselineInvocation, l as warn, n as auditDefaults, o as fail, r as auditInvocation, s as note, t as advise } from "../command-C3D7JWn6.js";
12
+ import { enableCompileCache } from "node:module";
12
13
  import { readFile } from "node:fs/promises";
13
14
  import path from "node:path";
14
15
  import { Command, InvalidArgumentError } from "commander";
@@ -36,7 +37,7 @@ async function runBaselineCommand(dir, options = {}) {
36
37
  entries: 0,
37
38
  exitCode: 2
38
39
  };
39
- const { pages, origin, label } = resolved;
40
+ const { pages, origin, label, directory } = resolved;
40
41
  note(`Auditing ${count(pages.length, "page")} in ${label}…`);
41
42
  const baseUrl = options.baseUrl ?? origin;
42
43
  const audits = await runEngine(pages, {
@@ -45,7 +46,7 @@ async function runBaselineCommand(dir, options = {}) {
45
46
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
46
47
  ...options.browser ? { browser: true } : {},
47
48
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
48
- ...options.url === void 0 ? { directory: path.resolve(cwd, dir) } : {}
49
+ ...directory === void 0 ? {} : { directory }
49
50
  });
50
51
  if (!audits) return {
51
52
  entries: 0,
@@ -222,7 +223,7 @@ async function readReport(file, cwd = process.cwd()) {
222
223
  const first = result.error.issues[0];
223
224
  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
225
  }
225
- if (result.data.schemaVersion !== 1) throw new DiffError(`${file} is schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
226
+ 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
227
  return result.data;
227
228
  }
228
229
  //#endregion
@@ -396,6 +397,30 @@ function formatFor(output) {
396
397
  //#endregion
397
398
  //#region src/cli/index.ts
398
399
  /**
400
+ * Reuse the compiled form of this tool and its dependencies between runs.
401
+ *
402
+ * Most of what a short audit costs is not the audit. jsdom alone is ~700 ms to
403
+ * import on a warm page cache, and V8 pays to parse and compile it from source
404
+ * on every single run — of a CLI somebody invokes from a build script, over and
405
+ * over, against code that has not changed since the last time. Node can cache
406
+ * the compiled bytecode instead, which is exactly the shape of this problem.
407
+ *
408
+ * Measured over ten pages: 2350 ms without, 2075 ms with. The saving is a fixed
409
+ * cost, so it counts for most on the small sites where fixed cost is most of
410
+ * the run.
411
+ *
412
+ * Best-effort by design. It is a cache: a read-only filesystem, a sandbox that
413
+ * forbids the directory, or a Node built without the feature all mean the run
414
+ * is a little slower, and none of them is a reason to fail an audit. Node's own
415
+ * NODE_DISABLE_COMPILE_CACHE is honoured by the call itself.
416
+ */
417
+ function useCompileCache() {
418
+ try {
419
+ enableCompileCache();
420
+ } catch {}
421
+ }
422
+ useCompileCache();
423
+ /**
399
424
  * A parser for a fixed set of words, listing them when the answer is not one.
400
425
  *
401
426
  * `normalise` is for `--country de`, where the accepted spelling differs from
@@ -431,16 +456,16 @@ const parseConcurrency = wholeNumber(1);
431
456
  const program = new Command();
432
457
  program.exitOverride();
433
458
  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
- });
459
+ 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("|")}; 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("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
460
+ const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
461
+ const invocation = auditInvocation(dir, defaults, flags);
462
+ const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
440
463
  process.exitCode = exitCode;
441
464
  });
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);
465
+ 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("--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>", "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) => {
466
+ const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
467
+ const invocation = baselineInvocation(dir, defaults, flags);
468
+ const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
444
469
  process.exitCode = exitCode;
445
470
  });
446
471
  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,7 +473,7 @@ program.command("diff").description("Compare two JSON reports: what a change mad
448
473
  process.exitCode = exitCode;
449
474
  });
450
475
  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");
476
+ const { runInitCommand } = await import("../init-CW7LfGT5.js");
452
477
  const { exitCode } = await runInitCommand(flags);
453
478
  process.exitCode = exitCode;
454
479
  });
@@ -463,6 +488,12 @@ program.command("statement").description("Generate an EU accessibility statement
463
488
  try {
464
489
  await program.parseAsync(process.argv);
465
490
  } catch (cause) {
491
+ if (cause instanceof ConfigError) {
492
+ fail(cause.message);
493
+ for (const issue of cause.issues) note(` ${issue}`);
494
+ process.exitCode = 2;
495
+ process.exit(2);
496
+ }
466
497
  const error = cause;
467
498
  if (typeof error.exitCode === "number") process.exitCode = error.exitCode === 0 ? 0 : 2;
468
499
  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 };