eaa-kit 0.3.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.
- package/README.md +82 -14
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +5 -1
- package/dist/{audit-DcL73mOC.js → audit-B282A3EA.js} +207 -41
- package/dist/audit-DyHPmGeD.js +2 -0
- package/dist/{baseline-CuKFq4IF.js → baseline-0u4df12v.js} +1 -1
- package/dist/{baseline-s9F3fXTN.js → baseline-CuAS2lMk.js} +6 -5
- package/dist/cli/index.js +306 -18
- package/dist/{collect-BkAQ0viT.js → collect-Cu1q9NTB.js} +53 -11
- package/dist/command-C3D7JWn6.js +160 -0
- package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B_Y6l-Ra.js +522 -0
- package/dist/{crawl-Oxt2Gaqo.js → crawl-BUWPgaGW.js} +83 -9
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/fingerprint-BjYV_0F7.js +67 -0
- package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
- package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
- package/dist/{html-DKiI_3gs.js → html-C4vktg0A.js} +130 -5
- package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
- package/dist/index.d.ts +119 -4
- package/dist/index.js +2 -2
- package/dist/{init-DiLiYvN1.js → init-CW7LfGT5.js} +10 -4
- package/dist/jsdom-22Bkt65v.js +3 -0
- package/dist/jsdom-B--cEH-G.js +89 -0
- package/dist/{json-Cnv9nd6U.js → json-DROX33kh.js} +9 -4
- package/dist/{json-B0Y7rNjt.js → json-D_Mnnft5.js} +1 -1
- package/dist/load-CFq2VQtT.js +2 -0
- package/dist/{load-UYXLqGV9.js → load-yAR4wzez.js} +133 -8
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DYFsGUNd.js → playwright-BojtYVUa.js} +30 -6
- package/dist/{pool-BWkWZiJW.js → pool-BO25OIez.js} +52 -2
- package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-CMBIrnpN.js +321 -0
- package/dist/{render-DI_aCnAZ.js → render-DbGOVmhx.js} +48 -10
- package/dist/{result-DLxd2Eip.js → result-BWcYXeRs.js} +114 -3
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/{run-BW6CVuND.js → run-C2nKFcb-.js} +19 -2
- package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
- package/dist/{sarif-DB3WG7T9.js → sarif-B-UBcVu8.js} +16 -8
- package/dist/{schema-CMZ8ItGk.js → schema-DJSF4K05.js} +18 -1
- package/dist/statement/templates/es.en.md +125 -0
- package/dist/statement/templates/es.es.md +127 -0
- package/dist/statement/templates/fr.en.md +128 -0
- package/dist/statement/templates/fr.fr.md +131 -0
- package/dist/statement/templates/it.en.md +127 -0
- package/dist/statement/templates/it.it.md +130 -0
- package/dist/statement/templates/nl.en.md +125 -0
- package/dist/statement/templates/nl.nl.md +127 -0
- package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +35 -9
- package/dist/audit-CuG2hYyo.js +0 -2
- package/dist/command-Dxpa00Ha.js +0 -77
- package/dist/fingerprint-DRoneAjj.js +0 -20
- package/dist/jsdom-BjpF-2V-.js +0 -158
- package/dist/jsdom-X4KYfTp8.js +0 -3
- package/dist/manual-Vz-oX1I_.js +0 -239
- package/dist/routes-C2Cgf6Ko.js +0 -119
package/README.md
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
[](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
|
-
|
|
7
|
-
|
|
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
|
|
14
|
+
npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
|
|
12
15
|
npx eaa-kit init # write an eaa.config.json
|
|
13
|
-
npx eaa-kit statement #
|
|
16
|
+
npx eaa-kit statement # accessibility statement, in one of seven countries
|
|
14
17
|
```
|
|
15
18
|
|
|
16
19
|
> **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
|
|
@@ -32,7 +35,9 @@ supported here either.
|
|
|
32
35
|
**Audits your build.** Globs the HTML out of `./dist`, parses it with jsdom and runs
|
|
33
36
|
axe-core against it. No Chromium download, fast enough for CI, and it never fetches
|
|
34
37
|
anything or executes your site's JavaScript. `--browser` swaps in real Chromium for the
|
|
35
|
-
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.
|
|
36
41
|
|
|
37
42
|
```bash
|
|
38
43
|
eaa-kit audit ./dist --fail-on serious
|
|
@@ -45,14 +50,21 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead:
|
|
|
45
50
|
eaa-kit audit --url http://localhost:3000
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
**Writes the statement.** A Barrierefreiheitserklärung
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
51
57
|
|
|
52
58
|
```bash
|
|
53
59
|
eaa-kit statement --output src/content/a11y.md
|
|
60
|
+
eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH
|
|
54
61
|
```
|
|
55
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
|
+
|
|
56
68
|
**Fails only on what is new.** The first run on a site that already exists finds
|
|
57
69
|
everything at once. A baseline records what is already wrong so the build fails on
|
|
58
70
|
regressions instead, without ever letting an accepted barrier look like a passing one.
|
|
@@ -62,23 +74,77 @@ eaa-kit baseline ./dist
|
|
|
62
74
|
eaa-kit audit ./dist --baseline eaa-baseline.json
|
|
63
75
|
```
|
|
64
76
|
|
|
77
|
+
**Says what a change did.** Two reports, and the difference between them: what is new, what
|
|
78
|
+
was fixed, and what the later run never looked at — which is kept apart, because a
|
|
79
|
+
violation missing from a run that stopped early was not fixed by anybody.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
eaa-kit diff before.json after.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Says what to do about it.** Not "images must have alternative text" and a link — who the
|
|
86
|
+
barrier stops, what to change, and the corrected form of *your* markup, in your framework's
|
|
87
|
+
idiom where that differs. Deterministic and offline: no model, no API key, nothing that
|
|
88
|
+
could invent a fix that looks right and is not.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
✗ image-alt critical, WCAG 1.1.1
|
|
92
|
+
A screen reader announces this image by its filename, or skips it entirely.
|
|
93
|
+
Fix: Add alt text describing what the image conveys…
|
|
94
|
+
→ <img src="/assets/logo.svg" alt="What this image shows">
|
|
95
|
+
written in src/components/Header.astro:12
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
|
|
99
|
+
AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
|
|
100
|
+
55 with what this run reached on each — never as a percentage, because most of WCAG cannot
|
|
101
|
+
be automated and a score would present that as a fact about your site.
|
|
102
|
+
|
|
103
|
+
**Says what it did not measure.** A crawl that stopped at its page limit, or could not
|
|
104
|
+
fetch forty URLs, no longer produces a report that looks like a complete one.
|
|
105
|
+
|
|
65
106
|
**Reports in four shapes**: a console report for whoever ran it, JSON for other tools,
|
|
66
107
|
SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
|
|
67
108
|
it is.
|
|
68
109
|
|
|
69
|
-
**
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
|
|
120
|
+
**Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
|
|
121
|
+
and Remix too, and integrations for
|
|
122
|
+
[Astro](docs/integrations.md#astro-integration),
|
|
123
|
+
[Nuxt](docs/integrations.md#nuxt-module),
|
|
124
|
+
[Eleventy](docs/integrations.md#eleventy-plugin) and
|
|
125
|
+
[webpack](docs/integrations.md#webpack-plugin) — or the bundled
|
|
126
|
+
[GitHub Action](docs/integrations.md#github-actions).
|
|
127
|
+
|
|
128
|
+
**Audits what a CMS serves.** WordPress, TYPO3, Craft, Laravel, Symfony, Rails and Django
|
|
129
|
+
write no HTML to disk, so eaa-kit names the command that gets the site running and audits
|
|
130
|
+
that instead — following the site's own sitemap, wherever it keeps it.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
|
|
134
|
+
```
|
|
72
135
|
|
|
73
136
|
## Documentation
|
|
74
137
|
|
|
75
138
|
| | |
|
|
76
139
|
| --- | --- |
|
|
77
140
|
| [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
|
|
78
|
-
| [
|
|
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 |
|
|
79
143
|
| [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
|
|
144
|
+
| [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
|
|
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 |
|
|
80
146
|
| [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
|
|
81
|
-
| [Integrations](docs/integrations.md) |
|
|
147
|
+
| [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
|
|
82
148
|
|
|
83
149
|
Complete generated output for every format is checked in under [examples/](examples).
|
|
84
150
|
|
|
@@ -90,7 +156,9 @@ The two things worth knowing before you rely on any of it:
|
|
|
90
156
|
text is accurate, whether a page makes sense in reading order, or whether a form can
|
|
91
157
|
actually be completed with a screen reader. A clean report means nothing was found by this
|
|
92
158
|
engine, which is not the same as a site being accessible — and none of the four output
|
|
93
|
-
formats will say otherwise on your behalf.
|
|
159
|
+
formats will say otherwise on your behalf. This is not a disclaimer the tool leaves you to
|
|
160
|
+
take on faith: it counts it. Of the 55 WCAG 2.2 A and AA criteria, 34 have no automated
|
|
161
|
+
rule at all, and every report says so.
|
|
94
162
|
|
|
95
163
|
**The browserless engine cannot decide everything.** jsdom has no layout, so rules that
|
|
96
164
|
depend on rendering — colour contrast, target size, computed overflow — cannot be
|
package/dist/astro/index.d.ts
CHANGED
package/dist/astro/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as auditBuild, t as BuildAuditError } from "../run-
|
|
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 {
|
|
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,8 +1,10 @@
|
|
|
1
|
-
import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-
|
|
2
|
-
import { n as count, t as collapse } from "./text-
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
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";
|
|
7
|
+
import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import pc from "picocolors";
|
|
8
10
|
//#region src/audit/report/console.ts
|
|
@@ -47,6 +49,9 @@ function context(options) {
|
|
|
47
49
|
sourceFor: options.sourceFor ?? (() => void 0),
|
|
48
50
|
componentFor: options.componentFor ?? (() => void 0),
|
|
49
51
|
manual: options.manual ?? false,
|
|
52
|
+
coverage: options.coverage ?? false,
|
|
53
|
+
framework: options.framework,
|
|
54
|
+
completeness: options.completeness,
|
|
50
55
|
c,
|
|
51
56
|
symbol: (kind) => {
|
|
52
57
|
switch (kind) {
|
|
@@ -182,11 +187,15 @@ function summary(audits, ctx) {
|
|
|
182
187
|
const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
|
|
183
188
|
const reviewCount = countRules(audits, "needs-review");
|
|
184
189
|
const pages = count(audits.length, "page");
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
const audited = audits.length - errored.length;
|
|
191
|
+
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}.`;
|
|
195
|
+
lines.push(line(ctx, clean, ctx.c.green));
|
|
196
|
+
} else {
|
|
188
197
|
lines.push(render(ctx, [{
|
|
189
|
-
text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${
|
|
198
|
+
text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${count(audited, "page")}`,
|
|
190
199
|
paint: ctx.c.red
|
|
191
200
|
}, {
|
|
192
201
|
text: ` (${count(elementCount, "element")})`,
|
|
@@ -202,6 +211,74 @@ function summary(audits, ctx) {
|
|
|
202
211
|
const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
|
|
203
212
|
if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
|
|
204
213
|
lines.push(...blindSection(audits, ctx));
|
|
214
|
+
lines.push(...coverageSection(audits, ctx));
|
|
215
|
+
return lines;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* How much of the standard this run could reach.
|
|
219
|
+
*
|
|
220
|
+
* One line by default. It is the sentence that stops a clean report reading as
|
|
221
|
+
* a clean site: most of WCAG cannot be checked by any automated engine, and
|
|
222
|
+
* until this was printed the report said so only in prose, in the footer, where
|
|
223
|
+
* it could be read as boilerplate.
|
|
224
|
+
*/
|
|
225
|
+
function coverageSection(audits, ctx) {
|
|
226
|
+
const coverage = buildCoverage(audits);
|
|
227
|
+
const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
|
|
228
|
+
if (coverage.browserWouldAnswer > 0) {
|
|
229
|
+
const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
|
|
230
|
+
lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
|
|
231
|
+
}
|
|
232
|
+
if (!ctx.coverage) {
|
|
233
|
+
lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
lines.push("", line(ctx, "Coverage", ctx.c.bold));
|
|
237
|
+
for (const criterion of coverage.criteria) {
|
|
238
|
+
const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
|
|
239
|
+
const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
|
|
240
|
+
lines.push(render(ctx, [
|
|
241
|
+
{
|
|
242
|
+
text: ` ${criterion.number} `,
|
|
243
|
+
paint: ctx.c.bold
|
|
244
|
+
},
|
|
245
|
+
{ text: `${criterion.title} (${criterion.level}) — ` },
|
|
246
|
+
{
|
|
247
|
+
text: `${STATUS_WORDS[criterion.status]}${note}`,
|
|
248
|
+
paint
|
|
249
|
+
}
|
|
250
|
+
]));
|
|
251
|
+
}
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
/** What each outcome is called, in words rather than a symbol. */
|
|
255
|
+
const STATUS_WORDS = {
|
|
256
|
+
evaluated: "evaluated here",
|
|
257
|
+
"not-evaluated": "this engine could not evaluate it",
|
|
258
|
+
"nothing-to-check": "rules ran and found nothing on this site to check",
|
|
259
|
+
"no-automated-rule": "no automated rule exists; a person must check it"
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* What the run never looked at.
|
|
263
|
+
*
|
|
264
|
+
* Printed before the counts rather than after them, because it changes how they
|
|
265
|
+
* read: "no violations" means one thing over a whole site and another over the
|
|
266
|
+
* twelve pages of it a crawl managed to fetch before it hit its limit.
|
|
267
|
+
*
|
|
268
|
+
* Pages that errored are left to the summary's own line, which already names
|
|
269
|
+
* them; repeating the number here would read as twice as many.
|
|
270
|
+
*/
|
|
271
|
+
function completenessLines(ctx) {
|
|
272
|
+
const completeness = ctx.completeness;
|
|
273
|
+
if (completeness === void 0 || completeness.complete) return [];
|
|
274
|
+
const lines = [];
|
|
275
|
+
if (completeness.unreachable.length > 0) {
|
|
276
|
+
const noun = plural(completeness.unreachable.length, "page");
|
|
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));
|
|
279
|
+
}
|
|
280
|
+
if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
|
|
281
|
+
if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
|
|
205
282
|
return lines;
|
|
206
283
|
}
|
|
207
284
|
/**
|
|
@@ -296,6 +373,7 @@ function issuesSection(audits, ctx) {
|
|
|
296
373
|
}] : []
|
|
297
374
|
]));
|
|
298
375
|
lines.push(line(ctx, ` ${issue.help}`, ctx.c.dim));
|
|
376
|
+
lines.push(...remediationLines(issue.ruleId, issue.elements[0]?.html, ctx));
|
|
299
377
|
for (const element of issue.elements.slice(0, ctx.maxNodes)) {
|
|
300
378
|
lines.push(line(ctx, ` ${collapse(element.html)}`));
|
|
301
379
|
lines.push(...whereLines(element, ctx));
|
|
@@ -305,6 +383,27 @@ function issuesSection(audits, ctx) {
|
|
|
305
383
|
}
|
|
306
384
|
return lines;
|
|
307
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* What to do about the rule, under the finding rather than in a link.
|
|
388
|
+
*
|
|
389
|
+
* axe-core's help text says what is wrong; this says who it stops and what to
|
|
390
|
+
* change. The corrected line is built from the element that actually failed,
|
|
391
|
+
* because a textbook snippet is a second thing to translate before anybody can
|
|
392
|
+
* use it.
|
|
393
|
+
*/
|
|
394
|
+
function remediationLines(ruleId, html, ctx) {
|
|
395
|
+
const remediation = remediationFor(ruleId, ctx.framework);
|
|
396
|
+
if (remediation === void 0) return [];
|
|
397
|
+
const indent = " ";
|
|
398
|
+
const width = ctx.width - 6 - 2;
|
|
399
|
+
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
|
+
const example = html === void 0 ? void 0 : remediation.example?.(html);
|
|
401
|
+
if (example !== void 0) lines.push(render(ctx, [{
|
|
402
|
+
text: `${indent}→ `,
|
|
403
|
+
paint: ctx.c.green
|
|
404
|
+
}, { text: collapse(example) }]));
|
|
405
|
+
return lines;
|
|
406
|
+
}
|
|
308
407
|
/** Where one element appears, and what that says about where the fix goes. */
|
|
309
408
|
function whereLines(element, ctx) {
|
|
310
409
|
const shown = element.pages.slice(0, ctx.maxNodes);
|
|
@@ -313,7 +412,7 @@ function whereLines(element, ctx) {
|
|
|
313
412
|
const lines = component === void 0 ? [] : [render(ctx, [{
|
|
314
413
|
text: " written in ",
|
|
315
414
|
paint: ctx.c.dim
|
|
316
|
-
}, { text: component }])];
|
|
415
|
+
}, { text: componentPath(component) }])];
|
|
317
416
|
lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
|
|
318
417
|
for (const page of shown) {
|
|
319
418
|
const source = ctx.sourceFor(page);
|
|
@@ -364,16 +463,22 @@ async function resolvePages(directory, options = {}) {
|
|
|
364
463
|
return {
|
|
365
464
|
pages: crawled.pages,
|
|
366
465
|
origin: crawled.origin,
|
|
367
|
-
label: options.url
|
|
466
|
+
label: options.url,
|
|
467
|
+
completeness: crawled.completeness
|
|
368
468
|
};
|
|
369
469
|
}
|
|
370
470
|
const cwd = options.cwd ?? process.cwd();
|
|
371
471
|
const shown = options.label ?? directory;
|
|
472
|
+
const unreachable = [];
|
|
372
473
|
let pages;
|
|
373
474
|
try {
|
|
374
475
|
pages = await collectPages(directory, {
|
|
375
476
|
...options.include ? { include: options.include } : {},
|
|
376
|
-
...options.exclude ? { exclude: options.exclude } : {}
|
|
477
|
+
...options.exclude ? { exclude: options.exclude } : {},
|
|
478
|
+
onUnreadable: (relativePath, reason) => unreachable.push({
|
|
479
|
+
location: relativePath,
|
|
480
|
+
reason
|
|
481
|
+
})
|
|
377
482
|
});
|
|
378
483
|
} catch (cause) {
|
|
379
484
|
if (!(cause instanceof BuildDirectoryError)) throw cause;
|
|
@@ -382,12 +487,32 @@ async function resolvePages(directory, options = {}) {
|
|
|
382
487
|
return;
|
|
383
488
|
}
|
|
384
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
|
+
}
|
|
385
497
|
warn(await emptyDirectoryHint(shown, cwd));
|
|
386
498
|
return;
|
|
387
499
|
}
|
|
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
|
+
}
|
|
388
506
|
return {
|
|
389
507
|
pages,
|
|
390
|
-
|
|
508
|
+
directory,
|
|
509
|
+
label: shown,
|
|
510
|
+
completeness: {
|
|
511
|
+
discovery: "directory",
|
|
512
|
+
collected: pages.length,
|
|
513
|
+
unreachable,
|
|
514
|
+
truncated: false
|
|
515
|
+
}
|
|
391
516
|
};
|
|
392
517
|
}
|
|
393
518
|
/**
|
|
@@ -397,7 +522,7 @@ async function resolvePages(directory, options = {}) {
|
|
|
397
522
|
* exit 2 — a run that reached no verdict, not a clean one.
|
|
398
523
|
*/
|
|
399
524
|
async function crawlPages(url, options) {
|
|
400
|
-
const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-
|
|
525
|
+
const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BUWPgaGW.js");
|
|
401
526
|
let entry;
|
|
402
527
|
try {
|
|
403
528
|
entry = parseEntryUrl(url, options.allowRemote ?? false);
|
|
@@ -412,6 +537,7 @@ async function crawlPages(url, options) {
|
|
|
412
537
|
const result = await crawlSite(entry, {
|
|
413
538
|
...options.allowRemote ? { allowRemote: true } : {},
|
|
414
539
|
...options.ignoreRobots ? { ignoreRobots: true } : {},
|
|
540
|
+
...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
|
|
415
541
|
...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
|
|
416
542
|
...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
|
|
417
543
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
@@ -432,7 +558,16 @@ async function crawlPages(url, options) {
|
|
|
432
558
|
if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
|
|
433
559
|
return {
|
|
434
560
|
pages: result.pages,
|
|
435
|
-
origin: result.origin
|
|
561
|
+
origin: result.origin,
|
|
562
|
+
completeness: {
|
|
563
|
+
discovery: result.discovery,
|
|
564
|
+
collected: result.pages.length,
|
|
565
|
+
unreachable: result.failures.map((failure) => ({
|
|
566
|
+
location: failure.url,
|
|
567
|
+
reason: failure.reason
|
|
568
|
+
})),
|
|
569
|
+
truncated: result.truncated
|
|
570
|
+
}
|
|
436
571
|
};
|
|
437
572
|
}
|
|
438
573
|
/**
|
|
@@ -444,7 +579,7 @@ async function crawlPages(url, options) {
|
|
|
444
579
|
*/
|
|
445
580
|
async function resolveAutomatically(options) {
|
|
446
581
|
const cwd = options.cwd ?? process.cwd();
|
|
447
|
-
const { autoDetectSource } = await import("./project-
|
|
582
|
+
const { autoDetectSource } = await import("./project-MFrXcw1M.js");
|
|
448
583
|
const detected = await autoDetectSource(cwd, {
|
|
449
584
|
...options.noBuild ? { noBuild: true } : {},
|
|
450
585
|
onStep: note
|
|
@@ -485,12 +620,13 @@ const OUTPUT_FORMATS = [
|
|
|
485
620
|
* piped somewhere without the chatter coming along.
|
|
486
621
|
*/
|
|
487
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.");
|
|
488
624
|
const resolved = await resolvePages(dir, options);
|
|
489
625
|
if (!resolved) return {
|
|
490
626
|
audits: [],
|
|
491
627
|
exitCode: 2
|
|
492
628
|
};
|
|
493
|
-
const { pages, origin, label, cleanup } = resolved;
|
|
629
|
+
const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
|
|
494
630
|
try {
|
|
495
631
|
note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
|
|
496
632
|
const baseUrl = options.baseUrl ?? origin;
|
|
@@ -499,8 +635,9 @@ async function runAuditCommand(dir, options = {}) {
|
|
|
499
635
|
...baseUrl === void 0 ? {} : { baseUrl },
|
|
500
636
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
501
637
|
...options.browser ? { browser: true } : {},
|
|
638
|
+
...options.fast && !options.browser ? { fast: true } : {},
|
|
502
639
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
503
|
-
...
|
|
640
|
+
...directory === void 0 ? {} : { directory }
|
|
504
641
|
});
|
|
505
642
|
if (!audits) return {
|
|
506
643
|
audits: [],
|
|
@@ -515,7 +652,8 @@ async function runAuditCommand(dir, options = {}) {
|
|
|
515
652
|
};
|
|
516
653
|
audits = applied;
|
|
517
654
|
}
|
|
518
|
-
|
|
655
|
+
const completeness = runCompleteness(audits, collection);
|
|
656
|
+
await emit(audits, label, failOn, completeness, options);
|
|
519
657
|
const unaudited = audits.filter((audit) => audit.error);
|
|
520
658
|
if (unaudited.length > 0) {
|
|
521
659
|
fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
|
|
@@ -541,7 +679,7 @@ async function runAuditCommand(dir, options = {}) {
|
|
|
541
679
|
* would be as wrong as silently passing.
|
|
542
680
|
*/
|
|
543
681
|
async function acceptBaseline(audits, options) {
|
|
544
|
-
const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-
|
|
682
|
+
const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-0u4df12v.js");
|
|
545
683
|
try {
|
|
546
684
|
const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
|
|
547
685
|
if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
|
|
@@ -568,52 +706,66 @@ async function acceptBaseline(audits, options) {
|
|
|
568
706
|
*/
|
|
569
707
|
async function describeEngine(pages, options) {
|
|
570
708
|
if (options.browser) return " with Chromium";
|
|
571
|
-
const { plannedWorkers } = await import("./pool-
|
|
709
|
+
const { plannedWorkers } = await import("./pool-BO25OIez.js");
|
|
572
710
|
const workers = options.concurrency ?? plannedWorkers(pages);
|
|
573
|
-
|
|
711
|
+
const threads = workers > 1 ? ` across ${workers} threads` : "";
|
|
712
|
+
return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
|
|
574
713
|
}
|
|
575
714
|
/**
|
|
576
715
|
* Emit the chosen format, to a file when --output is given and to stdout
|
|
577
716
|
* otherwise. Colour is dropped when writing to a file, since escape codes in a
|
|
578
717
|
* saved report are noise.
|
|
579
718
|
*/
|
|
580
|
-
async function emit(audits, dir, failOn, options) {
|
|
581
|
-
const body = await renderReport(audits, dir, failOn, options.format ?? "console", typeof options.output === "string", options);
|
|
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);
|
|
582
721
|
await emitDocument(body, options.output, options.cwd ?? process.cwd());
|
|
583
722
|
if (options.output !== void 0) note(`Report written to ${options.output}`);
|
|
584
723
|
}
|
|
585
|
-
async function renderReport(audits, dir, failOn, format, toFile, options) {
|
|
724
|
+
async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
|
|
586
725
|
switch (format) {
|
|
587
726
|
case "json": {
|
|
588
|
-
const { buildJsonReport, serialiseJsonReport } = await import("./json-
|
|
727
|
+
const { buildJsonReport, serialiseJsonReport } = await import("./json-D_Mnnft5.js");
|
|
589
728
|
return serialiseJsonReport(buildJsonReport(audits, {
|
|
590
729
|
directory: dir,
|
|
591
730
|
...options.url === void 0 ? {} : { sourceKind: "url" },
|
|
592
731
|
failOn,
|
|
732
|
+
completeness,
|
|
593
733
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
594
734
|
}));
|
|
595
735
|
}
|
|
596
736
|
case "sarif": {
|
|
597
|
-
const { buildSarifReport, serialiseSarifReport } = await import("./sarif-
|
|
598
|
-
return serialiseSarifReport(buildSarifReport(audits, {
|
|
737
|
+
const { buildSarifReport, serialiseSarifReport } = await import("./sarif-B-UBcVu8.js");
|
|
738
|
+
return serialiseSarifReport(buildSarifReport(audits, {
|
|
739
|
+
directory: dir,
|
|
740
|
+
completeness
|
|
741
|
+
}));
|
|
599
742
|
}
|
|
600
743
|
case "html": {
|
|
601
|
-
const { buildHtmlReport } = await import("./html-
|
|
744
|
+
const { buildHtmlReport } = await import("./html-C4vktg0A.js");
|
|
745
|
+
const framework = await detectedFramework(options.cwd ?? process.cwd());
|
|
602
746
|
return buildHtmlReport(audits, {
|
|
603
|
-
...await sourceLookups(options.cwd ?? process.cwd()),
|
|
747
|
+
...await sourceLookups(options.cwd ?? process.cwd(), audits),
|
|
604
748
|
directory: dir,
|
|
605
749
|
failOn,
|
|
750
|
+
completeness,
|
|
751
|
+
...framework === void 0 ? {} : { framework },
|
|
606
752
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
607
753
|
});
|
|
608
754
|
}
|
|
609
|
-
case "console":
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
}
|
|
617
769
|
}
|
|
618
770
|
}
|
|
619
771
|
/**
|
|
@@ -621,15 +773,29 @@ async function renderReport(audits, dir, failOn, format, toFile, options) {
|
|
|
621
773
|
* so. Best-effort: a project using no convention this recognises gets the
|
|
622
774
|
* report it always got, with no source named.
|
|
623
775
|
*/
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const {
|
|
776
|
+
/** The registry id of whatever built this project, for framework-shaped advice. */
|
|
777
|
+
async function detectedFramework(cwd) {
|
|
778
|
+
const { detectFramework } = await import("./frameworks-etFg_O8K.js");
|
|
779
|
+
const { readPackageJson } = await import("./project-MFrXcw1M.js");
|
|
780
|
+
return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
|
|
781
|
+
}
|
|
782
|
+
async function sourceLookups(cwd, audits) {
|
|
783
|
+
const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
|
|
627
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");
|
|
628
790
|
const components = await buildComponentIndex(cwd);
|
|
629
791
|
return {
|
|
630
792
|
sourceFor: (page) => sourceFor(routes, page),
|
|
631
793
|
componentFor: (html) => componentFor(components, html)
|
|
632
794
|
};
|
|
633
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
|
+
}
|
|
634
800
|
//#endregion
|
|
635
801
|
export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-
|
|
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 {
|
|
2
|
-
import { t as elementFingerprint } from "./fingerprint-
|
|
3
|
-
import {
|
|
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:
|
|
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 !==
|
|
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()) {
|