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.
- package/README.md +66 -11
- 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-DK2StT1_.js +2 -0
- package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +153 -25
- package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
- package/dist/command-CZKp9Wi_.js +251 -0
- package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/coverage-7bQqWNfN.js +2 -0
- package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
- package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/fingerprint-BjYV_0F7.js +67 -0
- package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +187 -5
- package/dist/index.js +3 -3
- package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
- package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
- package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
- package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
- package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
- package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
- package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -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-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +1 -1
- package/dist/webpack/index.js +1 -1
- package/dist/worksheet-eUjwYLxC.js +63 -0
- package/package.json +7 -3
- package/dist/audit-CpXH2Mk8.js +0 -2
- package/dist/baseline-22Y1NWxM.js +0 -2
- package/dist/command-D8l_oYbV.js +0 -77
- package/dist/fingerprint-DRoneAjj.js +0 -20
- package/dist/jsdom-4IMzv0eE.js +0 -3
package/README.md
CHANGED
|
@@ -3,15 +3,24 @@
|
|
|
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.
|
|
11
|
+
|
|
12
|
+
0.6.0 adds the half no engine can do. Of the 55 WCAG 2.2 A and AA success criteria, 34 have
|
|
13
|
+
no automated rule at all: `checklist` writes those down as a review somebody works through,
|
|
14
|
+
`audit --review` reads the answers back beside what the run measured, and `statement` now
|
|
15
|
+
refuses to publish a claim of full conformance that either one contradicts. Sites behind a
|
|
16
|
+
login or a preview protection are auditable too.
|
|
9
17
|
|
|
10
18
|
```bash
|
|
11
19
|
npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
|
|
12
20
|
npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
|
|
13
21
|
npx eaa-kit init # write an eaa.config.json
|
|
14
|
-
npx eaa-kit statement #
|
|
22
|
+
npx eaa-kit statement # accessibility statement, in one of seven countries
|
|
23
|
+
npx eaa-kit checklist # the manual review no engine can do for you
|
|
15
24
|
```
|
|
16
25
|
|
|
17
26
|
> **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
|
|
@@ -33,7 +42,9 @@ supported here either.
|
|
|
33
42
|
**Audits your build.** Globs the HTML out of `./dist`, parses it with jsdom and runs
|
|
34
43
|
axe-core against it. No Chromium download, fast enough for CI, and it never fetches
|
|
35
44
|
anything or executes your site's JavaScript. `--browser` swaps in real Chromium for the
|
|
36
|
-
rules that need layout and CSS
|
|
45
|
+
rules that need layout and CSS; `--fast` goes the other way and skips the rules the
|
|
46
|
+
browserless engine cannot decide anyway, rather than running them and discarding the
|
|
47
|
+
answer.
|
|
37
48
|
|
|
38
49
|
```bash
|
|
39
50
|
eaa-kit audit ./dist --fail-on serious
|
|
@@ -46,14 +57,21 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead:
|
|
|
46
57
|
eaa-kit audit --url http://localhost:3000
|
|
47
58
|
```
|
|
48
59
|
|
|
49
|
-
**Writes the statement.** A Barrierefreiheitserklärung
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
**Writes the statement.** A Barrierefreiheitserklärung, a déclaration d'accessibilité, a
|
|
61
|
+
dichiarazione di accessibilità — whatever the country calls it — from one config file, as
|
|
62
|
+
Markdown or HTML, naming that country's statute and supervisory body, and optionally
|
|
63
|
+
listing the barriers a real audit found.
|
|
52
64
|
|
|
53
65
|
```bash
|
|
54
66
|
eaa-kit statement --output src/content/a11y.md
|
|
67
|
+
eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH
|
|
55
68
|
```
|
|
56
69
|
|
|
70
|
+
Each country's statement is a document under its own law rather than a translation of
|
|
71
|
+
another's, so where a national regime asks for more than this — France's RGAA declaration,
|
|
72
|
+
Italy's filing with AgID — the text says so instead of letting a generated file look like
|
|
73
|
+
it settles the matter.
|
|
74
|
+
|
|
57
75
|
**Fails only on what is new.** The first run on a site that already exists finds
|
|
58
76
|
everything at once. A baseline records what is already wrong so the build fails on
|
|
59
77
|
regressions instead, without ever letting an accepted barrier look like a passing one.
|
|
@@ -84,6 +102,17 @@ could invent a fix that looks right and is not.
|
|
|
84
102
|
written in src/components/Header.astro:12
|
|
85
103
|
```
|
|
86
104
|
|
|
105
|
+
**Records the part no engine can do.** 34 of the 55 criteria have no automated rule at
|
|
106
|
+
all. `eaa-kit checklist` writes them out as a worksheet and a record; `audit --review`
|
|
107
|
+
reads the record back and reports what a person checked *beside* what the run measured,
|
|
108
|
+
never folded into it. A review never overrules the engine, never counts an entry nobody
|
|
109
|
+
answered, and never counts one that has aged out.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
eaa-kit checklist --output a11y-review.md
|
|
113
|
+
eaa-kit audit --review eaa-review.json --coverage
|
|
114
|
+
```
|
|
115
|
+
|
|
87
116
|
**Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
|
|
88
117
|
AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
|
|
89
118
|
55 with what this run reached on each — never as a percentage, because most of WCAG cannot
|
|
@@ -96,6 +125,16 @@ fetch forty URLs, no longer produces a report that looks like a complete one.
|
|
|
96
125
|
SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
|
|
97
126
|
it is.
|
|
98
127
|
|
|
128
|
+
**Says it once.** The flags a project runs on every build belong in the project, not in the
|
|
129
|
+
build script that repeats them. `eaa.config` — the same file the statement reads — takes an
|
|
130
|
+
[`audit` block](docs/audit.md#defaults-from-eaaconfig) of defaults, and `baseline` reads the
|
|
131
|
+
keys that mean the same thing to it. A flag you type still wins, so a one-off `--browser`
|
|
132
|
+
check needs no edit to a committed file.
|
|
133
|
+
|
|
134
|
+
```jsonc
|
|
135
|
+
{ "audit": { "dir": "build", "failOn": "critical", "browser": true } }
|
|
136
|
+
```
|
|
137
|
+
|
|
99
138
|
**Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
|
|
100
139
|
and Remix too, and integrations for
|
|
101
140
|
[Astro](docs/integrations.md#astro-integration),
|
|
@@ -112,15 +151,27 @@ that instead — following the site's own sitemap, wherever it keeps it.
|
|
|
112
151
|
eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
|
|
113
152
|
```
|
|
114
153
|
|
|
154
|
+
**Reaches a site behind a login.** A preview deployment or a staging CMS refuses anonymous
|
|
155
|
+
requests, which used to put both out of reach. `--basic-auth` and a repeatable `--header`
|
|
156
|
+
cover them, on the crawl and in the browser, and the credentials are never written into a
|
|
157
|
+
report, a baseline or a SARIF log — nor accepted from `eaa.config`, which is a committed
|
|
158
|
+
file.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
eaa-kit audit --url https://preview.example.com --basic-auth user:password
|
|
162
|
+
```
|
|
163
|
+
|
|
115
164
|
## Documentation
|
|
116
165
|
|
|
117
166
|
| | |
|
|
118
167
|
| --- | --- |
|
|
119
168
|
| [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
|
|
120
|
-
| [
|
|
169
|
+
| [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them |
|
|
170
|
+
| [The statement command](docs/statement.md) | The config file, the seven countries, and filling a statement from audit results |
|
|
121
171
|
| [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
|
|
122
172
|
| [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
|
|
123
173
|
| [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 |
|
|
174
|
+
| [Recording a manual review](docs/review.md) | The `checklist` command, the record, and what a review is not allowed to do |
|
|
124
175
|
| [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
|
|
125
176
|
| [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
|
|
126
177
|
|
|
@@ -147,11 +198,15 @@ you](docs/audit.md#what-the-browserless-engine-can-and-cannot-tell-you).
|
|
|
147
198
|
|
|
148
199
|
The same applies to the statement: it says what you told it. eaa-kit cannot check whether
|
|
149
200
|
those claims are true, and a statement claiming full conformance for a site that is not
|
|
150
|
-
conformant is worse than no statement at all
|
|
201
|
+
conformant is worse than no statement at all — which is why, given evidence, it now refuses
|
|
202
|
+
to write that particular one. `statement --audit` will not claim full conformance over a
|
|
203
|
+
report listing barriers, and `--review` will not claim it over criteria somebody recorded as
|
|
204
|
+
not met.
|
|
151
205
|
|
|
152
206
|
## Changelog
|
|
153
207
|
|
|
154
|
-
See [CHANGELOG.md](CHANGELOG.md).
|
|
208
|
+
See [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for what 0.6.0 is meant to
|
|
209
|
+
add and what it deliberately will not.
|
|
155
210
|
|
|
156
211
|
## License
|
|
157
212
|
|
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-D7hdVXLA.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-
|
|
1
|
+
import { t as auditPage } from "../../jsdom-CQar6OQS.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,13 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
1
|
+
import { a as impactLabel, i as countAtOrAbove, r as byImpactThenRule } from "./impact-luGCnN8F.js";
|
|
2
|
+
import { n as count, o as plural, t as collapse } from "./text-BF1LHMiV.js";
|
|
3
|
+
import { c as runEngine } from "./result-DfUru6y_.js";
|
|
4
|
+
import { a as emitDocument, c as runEngine$1, l as warn, o as fail, s as note, t as advise } from "./command-CZKp9Wi_.js";
|
|
5
|
+
import { l as reviewSentence } from "./review-CdMK2GpQ.js";
|
|
6
|
+
import { r as componentPath } from "./component-ChisoFyY.js";
|
|
7
|
+
import { i as reviewSummary, n as buildCoverage, o as manualCheckFor, r as coverageSummary, s as understandingUrl } from "./coverage-BD2_CYns.js";
|
|
8
|
+
import { a as blindRules, c as isShared, i as remediationFor, l as issueTotals, o as coverageParts, r as runCompleteness, s as groupIssues } from "./completeness-Dwq3CPB3.js";
|
|
9
|
+
import { n as buildSummary } from "./json-PNg6BeXp.js";
|
|
10
|
+
import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
|
|
8
11
|
import path from "node:path";
|
|
9
12
|
import pc from "picocolors";
|
|
10
13
|
//#region src/audit/report/console.ts
|
|
@@ -51,6 +54,7 @@ function context(options) {
|
|
|
51
54
|
manual: options.manual ?? false,
|
|
52
55
|
coverage: options.coverage ?? false,
|
|
53
56
|
framework: options.framework,
|
|
57
|
+
review: options.review,
|
|
54
58
|
completeness: options.completeness,
|
|
55
59
|
c,
|
|
56
60
|
symbol: (kind) => {
|
|
@@ -92,7 +96,7 @@ function line(ctx, text, paint) {
|
|
|
92
96
|
}]);
|
|
93
97
|
}
|
|
94
98
|
function headerLines(audits, ctx) {
|
|
95
|
-
const engineLabel = (audits
|
|
99
|
+
const engineLabel = runEngine(audits) === "browser" ? "chromium" : "jsdom (browserless)";
|
|
96
100
|
return [render(ctx, [{
|
|
97
101
|
text: "eaa-kit audit",
|
|
98
102
|
paint: ctx.c.bold
|
|
@@ -120,7 +124,7 @@ function pageSection(audit, ctx) {
|
|
|
120
124
|
}, { text: audit.error }]), "");
|
|
121
125
|
return lines;
|
|
122
126
|
}
|
|
123
|
-
for (const finding of
|
|
127
|
+
for (const finding of [...audit.violations].sort(byImpactThenRule)) lines.push(...violationLines(finding, ctx));
|
|
124
128
|
for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render(ctx, [
|
|
125
129
|
{
|
|
126
130
|
text: ` ${ctx.symbol("review")} `,
|
|
@@ -145,7 +149,7 @@ function pageSection(audit, ctx) {
|
|
|
145
149
|
paint: clean ? ctx.c.green : ctx.c.dim
|
|
146
150
|
}]));
|
|
147
151
|
}
|
|
148
|
-
for (const finding of
|
|
152
|
+
for (const finding of [...audit.accepted ?? []].sort(byImpactThenRule)) {
|
|
149
153
|
const elements = count(finding.nodes.length, "element");
|
|
150
154
|
lines.push(line(ctx, ` · ${finding.ruleId} accepted by the baseline (${elements})`, ctx.c.dim));
|
|
151
155
|
}
|
|
@@ -157,7 +161,7 @@ function coverageLine(audit, ctx) {
|
|
|
157
161
|
return line(ctx, ` ${coverageParts(audit).join(" · ")}`, ctx.c.dim);
|
|
158
162
|
}
|
|
159
163
|
function violationLines(finding, ctx) {
|
|
160
|
-
const impact = finding.impact
|
|
164
|
+
const impact = impactLabel(finding.impact);
|
|
161
165
|
const lines = [render(ctx, [
|
|
162
166
|
{
|
|
163
167
|
text: ` ${ctx.symbol("violation")} `,
|
|
@@ -181,33 +185,31 @@ function violationLines(finding, ctx) {
|
|
|
181
185
|
return lines;
|
|
182
186
|
}
|
|
183
187
|
function summary(audits, ctx) {
|
|
184
|
-
const
|
|
185
|
-
const errored = audits.filter((audit) => audit.error);
|
|
186
|
-
const ruleCount = audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
187
|
-
const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
|
|
188
|
+
const totals = buildSummary(audits, ctx.failOn);
|
|
188
189
|
const reviewCount = countRules(audits, "needs-review");
|
|
189
190
|
const pages = count(audits.length, "page");
|
|
191
|
+
const audited = totals.pages - totals.pagesNotAudited;
|
|
190
192
|
const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
+
if (totals.violations === 0 && audited === 0 && totals.pagesNotAudited > 0) lines.push(line(ctx, " Nothing was audited: no page could be read.", ctx.c.red));
|
|
194
|
+
else if (totals.violations === 0) {
|
|
195
|
+
const clean = totals.pagesNotAudited > 0 || ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${count(audited, "page")} that were audited.` : ` No violations across ${pages}.`;
|
|
193
196
|
lines.push(line(ctx, clean, ctx.c.green));
|
|
194
197
|
} else {
|
|
195
198
|
lines.push(render(ctx, [{
|
|
196
|
-
text: ` ${count(
|
|
199
|
+
text: ` ${count(totals.violations, "violation")} on ${totals.pagesWithViolations} of ${count(audited, "page")}`,
|
|
197
200
|
paint: ctx.c.red
|
|
198
201
|
}, {
|
|
199
|
-
text: ` (${count(
|
|
202
|
+
text: ` (${count(totals.violatingElements, "element")})`,
|
|
200
203
|
paint: ctx.c.dim
|
|
201
204
|
}]));
|
|
202
|
-
lines.push(thresholdLine(
|
|
205
|
+
lines.push(thresholdLine(totals.failing, ctx));
|
|
203
206
|
}
|
|
204
207
|
if (reviewCount > 0) {
|
|
205
208
|
const verb = reviewCount === 1 ? "needs" : "need";
|
|
206
209
|
lines.push(line(ctx, ` ${count(reviewCount, "rule")} ${verb} manual review`, ctx.c.yellow));
|
|
207
210
|
}
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
|
|
211
|
+
if (totals.pagesNotAudited > 0) lines.push(line(ctx, ` ${count(totals.pagesNotAudited, "page")} could not be audited`, ctx.c.red));
|
|
212
|
+
if (totals.accepted > 0) lines.push(line(ctx, ` ${count(totals.accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
|
|
211
213
|
lines.push(...blindSection(audits, ctx));
|
|
212
214
|
lines.push(...coverageSection(audits, ctx));
|
|
213
215
|
return lines;
|
|
@@ -221,12 +223,14 @@ function summary(audits, ctx) {
|
|
|
221
223
|
* it could be read as boilerplate.
|
|
222
224
|
*/
|
|
223
225
|
function coverageSection(audits, ctx) {
|
|
224
|
-
const coverage = buildCoverage(audits);
|
|
226
|
+
const coverage = buildCoverage(audits, void 0, ctx.review);
|
|
225
227
|
const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
|
|
226
228
|
if (coverage.browserWouldAnswer > 0) {
|
|
227
229
|
const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
|
|
228
230
|
lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
|
|
229
231
|
}
|
|
232
|
+
const review = reviewSummary(coverage);
|
|
233
|
+
if (review !== void 0) lines.push(...wrap(review, ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim)));
|
|
230
234
|
if (!ctx.coverage) {
|
|
231
235
|
lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
|
|
232
236
|
return lines;
|
|
@@ -246,6 +250,8 @@ function coverageSection(audits, ctx) {
|
|
|
246
250
|
paint
|
|
247
251
|
}
|
|
248
252
|
]));
|
|
253
|
+
const recorded = criterion.review;
|
|
254
|
+
if (recorded !== void 0) lines.push(line(ctx, ` ${reviewSentence(recorded)}`, ctx.c.dim));
|
|
249
255
|
}
|
|
250
256
|
return lines;
|
|
251
257
|
}
|
|
@@ -272,7 +278,8 @@ function completenessLines(ctx) {
|
|
|
272
278
|
const lines = [];
|
|
273
279
|
if (completeness.unreachable.length > 0) {
|
|
274
280
|
const noun = plural(completeness.unreachable.length, "page");
|
|
275
|
-
|
|
281
|
+
const verb = completeness.unreachable.length === 1 ? "was" : "were";
|
|
282
|
+
lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and ${verb} not audited`, ctx.c.yellow));
|
|
276
283
|
}
|
|
277
284
|
if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
|
|
278
285
|
if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
|
|
@@ -282,9 +289,8 @@ function completenessLines(ctx) {
|
|
|
282
289
|
* Why the run passed or failed. Without this, a build that exits 0 while the
|
|
283
290
|
* report lists violations looks like a bug rather than a threshold choice.
|
|
284
291
|
*/
|
|
285
|
-
function thresholdLine(
|
|
292
|
+
function thresholdLine(failing, ctx) {
|
|
286
293
|
const failOn = ctx.failOn;
|
|
287
|
-
const failing = countAtOrAbove(audits, failOn);
|
|
288
294
|
if (failing === 0) return line(ctx, ` none at or above ${failOn} (--fail-on ${failOn}), so this run passes`, ctx.c.green);
|
|
289
295
|
return line(ctx, ` ${failing} at or above ${failOn} (--fail-on ${failOn})`, ctx.c.red);
|
|
290
296
|
}
|
|
@@ -329,9 +335,6 @@ function countRules(audits, reason) {
|
|
|
329
335
|
for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === reason) ruleIds.add(finding.ruleId);
|
|
330
336
|
return ruleIds.size;
|
|
331
337
|
}
|
|
332
|
-
function sortByImpact(findings) {
|
|
333
|
-
return [...findings].sort((a, b) => impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId));
|
|
334
|
-
}
|
|
335
338
|
function criteria(finding) {
|
|
336
339
|
return finding.successCriteria.length > 0 ? `, WCAG ${finding.successCriteria.join(" ")}` : "";
|
|
337
340
|
}
|
|
@@ -347,8 +350,7 @@ function criteria(finding) {
|
|
|
347
350
|
function issuesSection(audits, ctx) {
|
|
348
351
|
const issues = groupIssues(audits);
|
|
349
352
|
if (issues.length === 0) return [];
|
|
350
|
-
const elements
|
|
351
|
-
const occurrences = issues.reduce((total, issue) => total + issue.occurrences, 0);
|
|
353
|
+
const { elements, occurrences } = issueTotals(issues);
|
|
352
354
|
const lines = [line(ctx, "Issues", ctx.c.bold)];
|
|
353
355
|
const intro = occurrences === elements ? ` ${count(elements, "distinct element")} to fix.` : ` ${count(occurrences, "violation")} across the site come from ${count(elements, "distinct element")}.`;
|
|
354
356
|
lines.push(line(ctx, intro, ctx.c.dim));
|
|
@@ -484,6 +486,13 @@ async function resolvePages(directory, options = {}) {
|
|
|
484
486
|
return;
|
|
485
487
|
}
|
|
486
488
|
if (pages.length === 0) {
|
|
489
|
+
if ((options.include !== void 0 || options.exclude !== void 0) && await holdsHtml(directory)) {
|
|
490
|
+
warn(`No page in ${shown} matched the filters, so nothing was audited.`);
|
|
491
|
+
if (options.include !== void 0) note(` --include ${options.include.join(" ")}`);
|
|
492
|
+
if (options.exclude !== void 0) note(` --exclude ${options.exclude.join(" ")}`);
|
|
493
|
+
note(" Patterns are relative to the audited directory, with POSIX separators.");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
487
496
|
warn(await emptyDirectoryHint(shown, cwd));
|
|
488
497
|
return;
|
|
489
498
|
}
|
|
@@ -495,6 +504,7 @@ async function resolvePages(directory, options = {}) {
|
|
|
495
504
|
}
|
|
496
505
|
return {
|
|
497
506
|
pages,
|
|
507
|
+
directory,
|
|
498
508
|
label: shown,
|
|
499
509
|
completeness: {
|
|
500
510
|
discovery: "directory",
|
|
@@ -511,7 +521,7 @@ async function resolvePages(directory, options = {}) {
|
|
|
511
521
|
* exit 2 — a run that reached no verdict, not a clean one.
|
|
512
522
|
*/
|
|
513
523
|
async function crawlPages(url, options) {
|
|
514
|
-
const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-
|
|
524
|
+
const { collapsedOnto, crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BHkJmUkA.js");
|
|
515
525
|
let entry;
|
|
516
526
|
try {
|
|
517
527
|
entry = parseEntryUrl(url, options.allowRemote ?? false);
|
|
@@ -529,7 +539,8 @@ async function crawlPages(url, options) {
|
|
|
529
539
|
...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
|
|
530
540
|
...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
|
|
531
541
|
...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
|
|
532
|
-
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
542
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
543
|
+
...options.headers === void 0 ? {} : { headers: options.headers }
|
|
533
544
|
});
|
|
534
545
|
if (result.pages.length === 0 && result.failures.length > 0) {
|
|
535
546
|
fail(`Could not fetch ${entry.href} (${result.failures[0]?.reason})`);
|
|
@@ -545,16 +556,26 @@ async function crawlPages(url, options) {
|
|
|
545
556
|
if (result.failures.length > 10) note(` …and ${result.failures.length - 10} more`);
|
|
546
557
|
}
|
|
547
558
|
if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
|
|
559
|
+
const collapsed = collapsedOnto(result);
|
|
560
|
+
if (collapsed.length > 0) {
|
|
561
|
+
const landedOn = collapsed[0]?.landedOn ?? "";
|
|
562
|
+
warn(`Every page requested answered at ${landedOn}, which is not where it was asked.`);
|
|
563
|
+
note(" A sign-in page in front of the site looks like this.");
|
|
564
|
+
note(" If it is behind one, pass --basic-auth user:password or --header \"Cookie: …\".");
|
|
565
|
+
}
|
|
548
566
|
return {
|
|
549
567
|
pages: result.pages,
|
|
550
568
|
origin: result.origin,
|
|
551
569
|
completeness: {
|
|
552
570
|
discovery: result.discovery,
|
|
553
571
|
collected: result.pages.length,
|
|
554
|
-
unreachable: result.failures.map((failure) => ({
|
|
572
|
+
unreachable: [...result.failures.map((failure) => ({
|
|
555
573
|
location: failure.url,
|
|
556
574
|
reason: failure.reason
|
|
557
|
-
})),
|
|
575
|
+
})), ...collapsed.map((redirect) => ({
|
|
576
|
+
location: redirect.requested,
|
|
577
|
+
reason: `answered at ${redirect.landedOn} instead, so this page was not audited`
|
|
578
|
+
}))],
|
|
558
579
|
truncated: result.truncated
|
|
559
580
|
}
|
|
560
581
|
};
|
|
@@ -609,22 +630,26 @@ const OUTPUT_FORMATS = [
|
|
|
609
630
|
* piped somewhere without the chatter coming along.
|
|
610
631
|
*/
|
|
611
632
|
async function runAuditCommand(dir, options = {}) {
|
|
633
|
+
if (options.fast && options.browser) warn("--fast has no effect with --browser: a real browser can decide those rules.");
|
|
612
634
|
const resolved = await resolvePages(dir, options);
|
|
613
635
|
if (!resolved) return {
|
|
614
636
|
audits: [],
|
|
615
637
|
exitCode: 2
|
|
616
638
|
};
|
|
617
|
-
const { pages, origin, label, cleanup, completeness: collection } = resolved;
|
|
639
|
+
const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
|
|
640
|
+
if (options.headers !== void 0 && directory !== void 0 && !options.browser) warn("--header and --basic-auth apply to pages that are fetched; this run read files.");
|
|
618
641
|
try {
|
|
619
642
|
note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
|
|
620
643
|
const baseUrl = options.baseUrl ?? origin;
|
|
621
|
-
let audits = await runEngine(pages, {
|
|
644
|
+
let audits = await runEngine$1(pages, {
|
|
622
645
|
cwd: options.cwd ?? process.cwd(),
|
|
646
|
+
...options.headers === void 0 ? {} : { headers: options.headers },
|
|
623
647
|
...baseUrl === void 0 ? {} : { baseUrl },
|
|
624
648
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
625
649
|
...options.browser ? { browser: true } : {},
|
|
650
|
+
...options.fast && !options.browser ? { fast: true } : {},
|
|
626
651
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
627
|
-
...
|
|
652
|
+
...directory === void 0 ? {} : { directory }
|
|
628
653
|
});
|
|
629
654
|
if (!audits) return {
|
|
630
655
|
audits: [],
|
|
@@ -640,7 +665,12 @@ async function runAuditCommand(dir, options = {}) {
|
|
|
640
665
|
audits = applied;
|
|
641
666
|
}
|
|
642
667
|
const completeness = runCompleteness(audits, collection);
|
|
643
|
-
|
|
668
|
+
const review = await loadReview(options);
|
|
669
|
+
if (review === FAILED) return {
|
|
670
|
+
audits,
|
|
671
|
+
exitCode: 2
|
|
672
|
+
};
|
|
673
|
+
await emit(audits, label, failOn, completeness, options, review);
|
|
644
674
|
const unaudited = audits.filter((audit) => audit.error);
|
|
645
675
|
if (unaudited.length > 0) {
|
|
646
676
|
fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
|
|
@@ -666,7 +696,7 @@ async function runAuditCommand(dir, options = {}) {
|
|
|
666
696
|
* would be as wrong as silently passing.
|
|
667
697
|
*/
|
|
668
698
|
async function acceptBaseline(audits, options) {
|
|
669
|
-
const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-
|
|
699
|
+
const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-BscHfFSW.js");
|
|
670
700
|
try {
|
|
671
701
|
const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
|
|
672
702
|
if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
|
|
@@ -684,6 +714,32 @@ async function acceptBaseline(audits, options) {
|
|
|
684
714
|
throw cause;
|
|
685
715
|
}
|
|
686
716
|
}
|
|
717
|
+
/** Distinguishes "no review asked for" from "the review could not be read". */
|
|
718
|
+
const FAILED = Symbol("review-failed");
|
|
719
|
+
/**
|
|
720
|
+
* Read the review record, when one was asked for.
|
|
721
|
+
*
|
|
722
|
+
* Returns undefined when no `--review` was given, which is the ordinary case,
|
|
723
|
+
* and the sentinel when one was given and could not be read.
|
|
724
|
+
*/
|
|
725
|
+
async function loadReview(options) {
|
|
726
|
+
if (options.review === void 0) return void 0;
|
|
727
|
+
const { answeredCount, readReview, ReviewError } = await import("./review-DIbOTmOM.js");
|
|
728
|
+
try {
|
|
729
|
+
const record = await readReview(options.review, options.cwd ?? process.cwd());
|
|
730
|
+
note(`Review record: ${count(answeredCount(record), "criterion")} answered`);
|
|
731
|
+
return {
|
|
732
|
+
record,
|
|
733
|
+
...options.reviewMaxAge === void 0 ? {} : { maxAgeDays: options.reviewMaxAge }
|
|
734
|
+
};
|
|
735
|
+
} catch (cause) {
|
|
736
|
+
if (cause instanceof ReviewError) {
|
|
737
|
+
fail(cause.message);
|
|
738
|
+
return FAILED;
|
|
739
|
+
}
|
|
740
|
+
throw cause;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
687
743
|
/**
|
|
688
744
|
* What the progress line says about the engine.
|
|
689
745
|
*
|
|
@@ -693,47 +749,51 @@ async function acceptBaseline(audits, options) {
|
|
|
693
749
|
*/
|
|
694
750
|
async function describeEngine(pages, options) {
|
|
695
751
|
if (options.browser) return " with Chromium";
|
|
696
|
-
const { plannedWorkers } = await import("./pool-
|
|
752
|
+
const { plannedWorkers } = await import("./pool-dkexq5xo.js");
|
|
697
753
|
const workers = options.concurrency ?? plannedWorkers(pages);
|
|
698
|
-
|
|
754
|
+
const threads = workers > 1 ? ` across ${workers} threads` : "";
|
|
755
|
+
return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
|
|
699
756
|
}
|
|
700
757
|
/**
|
|
701
758
|
* Emit the chosen format, to a file when --output is given and to stdout
|
|
702
759
|
* otherwise. Colour is dropped when writing to a file, since escape codes in a
|
|
703
760
|
* saved report are noise.
|
|
704
761
|
*/
|
|
705
|
-
async function emit(audits, dir, failOn, completeness, options) {
|
|
706
|
-
const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
|
|
762
|
+
async function emit(audits, dir, failOn, completeness, options, review) {
|
|
763
|
+
const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options, review);
|
|
707
764
|
await emitDocument(body, options.output, options.cwd ?? process.cwd());
|
|
708
765
|
if (options.output !== void 0) note(`Report written to ${options.output}`);
|
|
709
766
|
}
|
|
710
|
-
async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
|
|
767
|
+
async function renderReport(audits, dir, failOn, completeness, format, toFile, options, review) {
|
|
711
768
|
switch (format) {
|
|
712
769
|
case "json": {
|
|
713
|
-
const { buildJsonReport, serialiseJsonReport } = await import("./json-
|
|
770
|
+
const { buildJsonReport, serialiseJsonReport } = await import("./json-HJDgJp09.js");
|
|
714
771
|
return serialiseJsonReport(buildJsonReport(audits, {
|
|
715
772
|
directory: dir,
|
|
716
773
|
...options.url === void 0 ? {} : { sourceKind: "url" },
|
|
717
774
|
failOn,
|
|
718
775
|
completeness,
|
|
776
|
+
...review === void 0 ? {} : { review },
|
|
719
777
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
720
778
|
}));
|
|
721
779
|
}
|
|
722
780
|
case "sarif": {
|
|
723
|
-
const { buildSarifReport, serialiseSarifReport } = await import("./sarif-
|
|
781
|
+
const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BKvq_AUm.js");
|
|
724
782
|
return serialiseSarifReport(buildSarifReport(audits, {
|
|
725
783
|
directory: dir,
|
|
726
|
-
completeness
|
|
784
|
+
completeness,
|
|
785
|
+
...review === void 0 ? {} : { review }
|
|
727
786
|
}));
|
|
728
787
|
}
|
|
729
788
|
case "html": {
|
|
730
|
-
const { buildHtmlReport } = await import("./html-
|
|
789
|
+
const { buildHtmlReport } = await import("./html-Bmh7X_sM.js");
|
|
731
790
|
const framework = await detectedFramework(options.cwd ?? process.cwd());
|
|
732
791
|
return buildHtmlReport(audits, {
|
|
733
|
-
...await sourceLookups(options.cwd ?? process.cwd()),
|
|
792
|
+
...await sourceLookups(options.cwd ?? process.cwd(), audits),
|
|
734
793
|
directory: dir,
|
|
735
794
|
failOn,
|
|
736
795
|
completeness,
|
|
796
|
+
...review === void 0 ? {} : { review },
|
|
737
797
|
...framework === void 0 ? {} : { framework },
|
|
738
798
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
739
799
|
});
|
|
@@ -741,10 +801,11 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
|
|
|
741
801
|
case "console": {
|
|
742
802
|
const framework = await detectedFramework(options.cwd ?? process.cwd());
|
|
743
803
|
return `${formatConsoleReport(audits, {
|
|
744
|
-
...await sourceLookups(options.cwd ?? process.cwd()),
|
|
804
|
+
...await sourceLookups(options.cwd ?? process.cwd(), audits),
|
|
745
805
|
dir,
|
|
746
806
|
failOn,
|
|
747
807
|
completeness,
|
|
808
|
+
...review === void 0 ? {} : { review },
|
|
748
809
|
...options.perPage ? { perPage: true } : {},
|
|
749
810
|
...options.manual ? { manual: true } : {},
|
|
750
811
|
...options.coverage ? { coverage: true } : {},
|
|
@@ -765,15 +826,23 @@ async function detectedFramework(cwd) {
|
|
|
765
826
|
const { readPackageJson } = await import("./project-MFrXcw1M.js");
|
|
766
827
|
return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
|
|
767
828
|
}
|
|
768
|
-
async function sourceLookups(cwd) {
|
|
829
|
+
async function sourceLookups(cwd, audits) {
|
|
769
830
|
const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
|
|
770
|
-
const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
|
|
771
831
|
const routes = await buildRouteMap(cwd);
|
|
832
|
+
if (!hasElementsToAttribute(audits)) return {
|
|
833
|
+
sourceFor: (page) => sourceFor(routes, page),
|
|
834
|
+
componentFor: () => void 0
|
|
835
|
+
};
|
|
836
|
+
const { buildComponentIndex, componentFor } = await import("./component-DcCn3BJS.js");
|
|
772
837
|
const components = await buildComponentIndex(cwd);
|
|
773
838
|
return {
|
|
774
839
|
sourceFor: (page) => sourceFor(routes, page),
|
|
775
840
|
componentFor: (html) => componentFor(components, html)
|
|
776
841
|
};
|
|
777
842
|
}
|
|
843
|
+
/** Whether any element will be listed, and so looked up against the source. */
|
|
844
|
+
function hasElementsToAttribute(audits) {
|
|
845
|
+
return audits.some((audit) => audit.violations.length > 0 || (audit.accepted?.length ?? 0) > 0);
|
|
846
|
+
}
|
|
778
847
|
//#endregion
|
|
779
848
|
export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
|