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.
- package/README.md +31 -9
- 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-CPoZMXGM.js → audit-B282A3EA.js} +43 -21
- package/dist/audit-DyHPmGeD.js +2 -0
- package/dist/{baseline-22Y1NWxM.js → baseline-0u4df12v.js} +1 -1
- package/dist/{baseline-DB9CZGnV.js → baseline-CuAS2lMk.js} +6 -5
- package/dist/cli/index.js +50 -19
- package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
- package/dist/command-C3D7JWn6.js +160 -0
- package/dist/{coverage-B4IBKMO8.js → coverage-B_Y6l-Ra.js} +3 -2
- package/dist/{crawl-BiI1Lau_.js → crawl-BUWPgaGW.js} +83 -9
- 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-C4vktg0A.js} +3 -3
- package/dist/index.d.ts +119 -4
- package/dist/index.js +2 -2
- package/dist/{init-DIWDE35F.js → init-CW7LfGT5.js} +10 -4
- package/dist/jsdom-22Bkt65v.js +3 -0
- package/dist/{jsdom-DCpGSLfW.js → jsdom-B--cEH-G.js} +12 -4
- package/dist/{json-DjEvy1nX.js → json-DROX33kh.js} +4 -4
- package/dist/{json-QQuFIw1W.js → json-D_Mnnft5.js} +1 -1
- package/dist/load-CFq2VQtT.js +2 -0
- package/dist/{load-5wRGLvub.js → load-yAR4wzez.js} +133 -8
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BWniOain.js → playwright-BojtYVUa.js} +30 -6
- package/dist/{pool-BMevaLWD.js → pool-BO25OIez.js} +52 -2
- package/dist/{remediation-Dtowi2EC.js → remediation-CMBIrnpN.js} +2 -2
- package/dist/{render-DrvXRCEn.js → render-DbGOVmhx.js} +47 -9
- package/dist/{result-DoamKFsp.js → result-BWcYXeRs.js} +37 -3
- package/dist/{run-DB34BSOZ.js → run-C2nKFcb-.js} +1 -1
- package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
- package/dist/{sarif-SR3_lLYd.js → sarif-B-UBcVu8.js} +3 -3
- 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/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/package.json +6 -2
- package/dist/audit-CpXH2Mk8.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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
//#region src/cli/command.ts
|
|
5
|
+
/**
|
|
6
|
+
* What every command does around the audit itself: say what is happening, run
|
|
7
|
+
* the engine, and put the document somewhere.
|
|
8
|
+
*
|
|
9
|
+
* `audit` and `baseline` are siblings — one reports what a run found and the
|
|
10
|
+
* other writes it down — so they take the same flags, choose between the same
|
|
11
|
+
* two engines and fail on the same setup problems. Keeping that in one place is
|
|
12
|
+
* what stops the two commands drifting into disagreeing about what a run is.
|
|
13
|
+
*/
|
|
14
|
+
/** Progress and diagnostics go to stderr, so the report can be piped away. */
|
|
15
|
+
function note(message) {
|
|
16
|
+
process.stderr.write(pc.dim(`${message}\n`));
|
|
17
|
+
}
|
|
18
|
+
function warn(message) {
|
|
19
|
+
process.stderr.write(`${pc.yellow("warning")} ${message}\n`);
|
|
20
|
+
}
|
|
21
|
+
function fail(message) {
|
|
22
|
+
process.stderr.write(`${pc.red("error")} ${message}\n`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Advice the reader should not miss, without the `warning` prefix: nothing has
|
|
26
|
+
* gone wrong, but what happens next is theirs to get right.
|
|
27
|
+
*/
|
|
28
|
+
function advise(message) {
|
|
29
|
+
process.stderr.write(pc.yellow(`${message}\n`));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Audit the pages with whichever engine was asked for.
|
|
33
|
+
*
|
|
34
|
+
* Returns undefined when the browser was asked for and is not usable, having
|
|
35
|
+
* already said so: Playwright missing is a setup problem with a specific fix,
|
|
36
|
+
* not a crash, and both commands turn it into exit 2.
|
|
37
|
+
*/
|
|
38
|
+
async function runEngine(pages, options) {
|
|
39
|
+
const runnerOptions = {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
|
|
42
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
43
|
+
};
|
|
44
|
+
if (!options.browser) {
|
|
45
|
+
const { runPooledAudit } = await import("./pool-BO25OIez.js");
|
|
46
|
+
return runPooledAudit(pages, {
|
|
47
|
+
...runnerOptions,
|
|
48
|
+
...options.fast ? { fast: true } : {},
|
|
49
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-BojtYVUa.js");
|
|
53
|
+
try {
|
|
54
|
+
return await runBrowserAudit(options.directory, pages, {
|
|
55
|
+
...runnerOptions,
|
|
56
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
57
|
+
});
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
if (cause instanceof BrowserUnavailableError) {
|
|
60
|
+
fail(cause.message);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
throw cause;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write a document to `output`, or to stdout when there is none. Parent
|
|
68
|
+
* directories are created, since a report path in CI usually names one that is
|
|
69
|
+
* not there yet.
|
|
70
|
+
*/
|
|
71
|
+
async function emitDocument(body, output, cwd) {
|
|
72
|
+
if (output === void 0) {
|
|
73
|
+
process.stdout.write(body);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const target = path.resolve(cwd, output);
|
|
77
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
78
|
+
await writeFile(target, body, "utf8");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Audit defaults from the project's config file.
|
|
82
|
+
*
|
|
83
|
+
* `audit` and `baseline` are run from a build script over and over with the
|
|
84
|
+
* same six flags, and the flags are the only place to say them: the config file
|
|
85
|
+
* has existed since 0.2 and served the statement alone. An `audit` block there
|
|
86
|
+
* is that list written once.
|
|
87
|
+
*
|
|
88
|
+
* Everything it returns is a default. The flags are merged over it by the
|
|
89
|
+
* caller, because the file is the project's usual answer and a flag is somebody
|
|
90
|
+
* asking for something else on this run.
|
|
91
|
+
*
|
|
92
|
+
* No config file at all is not an error, unlike for `statement`: this command
|
|
93
|
+
* has always run against projects that have never heard of one. A file that
|
|
94
|
+
* exists and cannot be read is exit 2 — it was written to be used, and running
|
|
95
|
+
* on different settings than it names would be worse than stopping.
|
|
96
|
+
*/
|
|
97
|
+
async function auditDefaults(options = {}) {
|
|
98
|
+
const { loadAuditConfig } = await import("./load-CFq2VQtT.js");
|
|
99
|
+
const loaded = await loadAuditConfig({
|
|
100
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
101
|
+
...options.config ? { path: options.config } : {}
|
|
102
|
+
});
|
|
103
|
+
if (!loaded?.audit) return {};
|
|
104
|
+
note(`Defaults from ${path.basename(loaded.path)}`);
|
|
105
|
+
return loaded.audit;
|
|
106
|
+
}
|
|
107
|
+
function auditInvocation(dir, defaults, flags) {
|
|
108
|
+
const { build, config: _config, ...typed } = flags;
|
|
109
|
+
const { dir: configDir, build: configBuild, ...fromConfig } = defaults;
|
|
110
|
+
return {
|
|
111
|
+
dir: dir ?? configDir,
|
|
112
|
+
options: {
|
|
113
|
+
...fromConfig,
|
|
114
|
+
...typed,
|
|
115
|
+
...build === false || configBuild === false ? { noBuild: true } : {}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The same, for `baseline`, which reads the defaults that mean the same thing
|
|
121
|
+
* to it.
|
|
122
|
+
*
|
|
123
|
+
* Deliberately a subset. `output` names where the report goes for one command
|
|
124
|
+
* and where the baseline goes for the other, so carrying it across would write
|
|
125
|
+
* a baseline over the path somebody set aside for a report; `format`, `failOn`
|
|
126
|
+
* and `baseline` all describe a verdict this command does not reach.
|
|
127
|
+
*/
|
|
128
|
+
function baselineInvocation(dir, defaults, flags) {
|
|
129
|
+
const { config: _config, ...typed } = flags;
|
|
130
|
+
return {
|
|
131
|
+
dir: dir ?? defaults.dir ?? "./dist",
|
|
132
|
+
options: {
|
|
133
|
+
...baselineDefaults(defaults),
|
|
134
|
+
...typed
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function baselineDefaults(config) {
|
|
139
|
+
return pick(config, [
|
|
140
|
+
"include",
|
|
141
|
+
"exclude",
|
|
142
|
+
"baseUrl",
|
|
143
|
+
"url",
|
|
144
|
+
"allowRemote",
|
|
145
|
+
"ignoreRobots",
|
|
146
|
+
"sitemap",
|
|
147
|
+
"maxPages",
|
|
148
|
+
"maxDepth",
|
|
149
|
+
"browser",
|
|
150
|
+
"concurrency"
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
/** Copies the keys that are actually set, so nothing spreads an undefined over a real value. */
|
|
154
|
+
function pick(source, keys) {
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const key of keys) if (source[key] !== void 0) out[key] = source[key];
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { emitDocument as a, runEngine as c, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-BWcYXeRs.js";
|
|
2
2
|
import axe from "axe-core";
|
|
3
3
|
//#region src/audit/manual.ts
|
|
4
4
|
/**
|
|
@@ -467,6 +467,7 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
|
467
467
|
const byCriterion = rulesByCriterion(tags);
|
|
468
468
|
const decided = decidedRules(audits);
|
|
469
469
|
const blinded = blindedRules(audits);
|
|
470
|
+
const jsdomBlindApplies = audits.length === 0 || audits.some((audit) => audit.engine !== "browser");
|
|
470
471
|
const criteria = WCAG22_AA_CRITERIA.map((criterion) => {
|
|
471
472
|
const rules = byCriterion.get(criterion.number) ?? [];
|
|
472
473
|
if (rules.length === 0) return {
|
|
@@ -481,7 +482,7 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
|
481
482
|
rules,
|
|
482
483
|
browserWouldAnswer: false
|
|
483
484
|
};
|
|
484
|
-
const engineBlind = rules.filter((rule) => blinded.has(rule) || ENGINE_BLIND_RULES[rule] !== void 0);
|
|
485
|
+
const engineBlind = rules.filter((rule) => blinded.has(rule) || jsdomBlindApplies && ENGINE_BLIND_RULES[rule] !== void 0);
|
|
485
486
|
if (engineBlind.length > 0) return {
|
|
486
487
|
...criterion,
|
|
487
488
|
status: "not-evaluated",
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as stripBom } from "./collect-Cu1q9NTB.js";
|
|
2
2
|
/** Requests in flight at once. Politeness, not throughput. */
|
|
3
3
|
const REQUEST_CONCURRENCY = 4;
|
|
4
|
+
/**
|
|
5
|
+
* Largest response body this will read, in bytes.
|
|
6
|
+
*
|
|
7
|
+
* A crawl reads whatever the server sends, and nothing obliges a server to send
|
|
8
|
+
* something reasonable — a misconfigured export endpoint, a log streamed as
|
|
9
|
+
* text/html, or a host that simply does not stop. `response.text()` buffers all
|
|
10
|
+
* of it before anyone can object, so the ceiling has to be applied while the
|
|
11
|
+
* body is still arriving.
|
|
12
|
+
*
|
|
13
|
+
* Matches the on-disk page limit: the two paths audit the same kind of
|
|
14
|
+
* document, and a page that would be declined off disk should not be accepted
|
|
15
|
+
* because it arrived over HTTP instead.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_BODY_BYTES = 33554432;
|
|
4
18
|
var CrawlError = class extends Error {
|
|
5
19
|
name = "CrawlError";
|
|
6
20
|
};
|
|
@@ -79,7 +93,7 @@ function urlsFromSitemap(xml, origin) {
|
|
|
79
93
|
return urls;
|
|
80
94
|
}
|
|
81
95
|
/** One request, with a timeout, returning HTML or a reason it is not a page. */
|
|
82
|
-
async function fetchPage(url, impl, timeoutMs, origin) {
|
|
96
|
+
async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes) {
|
|
83
97
|
const controller = new AbortController();
|
|
84
98
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
99
|
try {
|
|
@@ -106,11 +120,16 @@ async function fetchPage(url, impl, timeoutMs, origin) {
|
|
|
106
120
|
ok: false,
|
|
107
121
|
reason: `not HTML (${type.split(";")[0] || "no content-type"})`
|
|
108
122
|
};
|
|
123
|
+
const body = await readCapped(response, maxBodyBytes);
|
|
124
|
+
if (body === void 0) return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: `larger than the ${maxBodyBytes} byte limit for one page`
|
|
127
|
+
};
|
|
109
128
|
return {
|
|
110
129
|
ok: true,
|
|
111
130
|
value: {
|
|
112
131
|
url: finalUrl,
|
|
113
|
-
html:
|
|
132
|
+
html: body
|
|
114
133
|
}
|
|
115
134
|
};
|
|
116
135
|
} catch (cause) {
|
|
@@ -123,6 +142,51 @@ async function fetchPage(url, impl, timeoutMs, origin) {
|
|
|
123
142
|
clearTimeout(timer);
|
|
124
143
|
}
|
|
125
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* A response body, up to a limit, or undefined when it runs past it.
|
|
147
|
+
*
|
|
148
|
+
* Streamed rather than buffered, because `response.text()` has already read
|
|
149
|
+
* everything by the time it could be checked, and a content-length header is
|
|
150
|
+
* both optional and unverified — a chunked response carries no length at all,
|
|
151
|
+
* and one that carries a length is not obliged to tell the truth. Counting the
|
|
152
|
+
* bytes as they arrive is the only check that holds either way, and the body is
|
|
153
|
+
* cancelled the moment it goes over so nothing keeps arriving.
|
|
154
|
+
*/
|
|
155
|
+
async function readCapped(response, limit) {
|
|
156
|
+
const body = response.body;
|
|
157
|
+
if (!body) {
|
|
158
|
+
const text = await response.text();
|
|
159
|
+
return text.length > limit ? void 0 : text;
|
|
160
|
+
}
|
|
161
|
+
const reader = body.getReader();
|
|
162
|
+
const chunks = [];
|
|
163
|
+
let total = 0;
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const { done, value } = await reader.read();
|
|
167
|
+
if (done) break;
|
|
168
|
+
if (value === void 0) continue;
|
|
169
|
+
total += value.byteLength;
|
|
170
|
+
if (total > limit) {
|
|
171
|
+
await reader.cancel();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
chunks.push(value);
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
reader.releaseLock();
|
|
178
|
+
}
|
|
179
|
+
return new TextDecoder().decode(concat(chunks, total));
|
|
180
|
+
}
|
|
181
|
+
function concat(chunks, total) {
|
|
182
|
+
const joined = new Uint8Array(total);
|
|
183
|
+
let at = 0;
|
|
184
|
+
for (const chunk of chunks) {
|
|
185
|
+
joined.set(chunk, at);
|
|
186
|
+
at += chunk.byteLength;
|
|
187
|
+
}
|
|
188
|
+
return joined;
|
|
189
|
+
}
|
|
126
190
|
/** Paths robots.txt disallows for us. Only the wildcard group is read. */
|
|
127
191
|
function disallowedPaths(robots) {
|
|
128
192
|
const lines = robots.split(/\r?\n/).map((line) => line.replace(/#.*$/, "").trim());
|
|
@@ -149,12 +213,21 @@ function disallowedPaths(robots) {
|
|
|
149
213
|
* that is not a page. A site that does not publish one is the ordinary case,
|
|
150
214
|
* not a failure, so it comes back empty either way.
|
|
151
215
|
*/
|
|
152
|
-
async function fetchSiteFile(entry, impl, name) {
|
|
216
|
+
async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes) {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
219
|
try {
|
|
154
|
-
const response = await impl(new URL(name, entry).href, {
|
|
155
|
-
|
|
220
|
+
const response = await impl(new URL(name, entry).href, {
|
|
221
|
+
redirect: "follow",
|
|
222
|
+
signal: controller.signal
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) return void 0;
|
|
225
|
+
if (new URL(response.url || new URL(name, entry).href).origin !== entry.origin) return void 0;
|
|
226
|
+
return await readCapped(response, maxBodyBytes);
|
|
156
227
|
} catch {
|
|
157
228
|
return;
|
|
229
|
+
} finally {
|
|
230
|
+
clearTimeout(timer);
|
|
158
231
|
}
|
|
159
232
|
}
|
|
160
233
|
/**
|
|
@@ -169,8 +242,9 @@ async function crawlSite(entry, options = {}) {
|
|
|
169
242
|
const maxPages = options.maxPages ?? 200;
|
|
170
243
|
const maxDepth = options.maxDepth ?? 3;
|
|
171
244
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
245
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
172
246
|
const failures = [];
|
|
173
|
-
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt");
|
|
247
|
+
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes);
|
|
174
248
|
const blocked = robots === void 0 ? [] : disallowedPaths(robots);
|
|
175
249
|
const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
|
|
176
250
|
let discovery = "links";
|
|
@@ -184,7 +258,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
184
258
|
depth
|
|
185
259
|
});
|
|
186
260
|
};
|
|
187
|
-
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml");
|
|
261
|
+
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes);
|
|
188
262
|
const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
|
|
189
263
|
if (listed.length > 0) {
|
|
190
264
|
discovery = "sitemap";
|
|
@@ -196,7 +270,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
196
270
|
const batch = queue.splice(0, Math.min(REQUEST_CONCURRENCY, maxPages - pages.length));
|
|
197
271
|
const results = await Promise.all(batch.map(async (item) => ({
|
|
198
272
|
item,
|
|
199
|
-
result: await fetchPage(item.url, impl, timeoutMs, entry.origin)
|
|
273
|
+
result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes)
|
|
200
274
|
})));
|
|
201
275
|
for (const { item, result } of results) {
|
|
202
276
|
if (!result.ok) {
|
package/dist/eleventy/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as IntegrationOptions, t as BuildAuditError } from "../run-
|
|
1
|
+
import { n as IntegrationOptions, t as BuildAuditError } from "../run-CtcEUhbe.js";
|
|
2
2
|
//#region src/eleventy/index.d.ts
|
|
3
3
|
/** What `eleventy.after` is given. Only two of its fields are read. */
|
|
4
4
|
interface EleventyAfterEvent {
|
package/dist/eleventy/index.js
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region src/audit/fingerprint.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stable identity for one violating element.
|
|
5
|
+
*
|
|
6
|
+
* Derived from the rule, the selector and the element's own opening tag, and
|
|
7
|
+
* deliberately not from the file it was found in. Two consumers need this and
|
|
8
|
+
* they need it to agree: SARIF, so that moving a page does not close one code
|
|
9
|
+
* scanning alert and open an identical one, and the baseline, so that an
|
|
10
|
+
* accepted violation stays accepted when the surrounding page changes.
|
|
11
|
+
*
|
|
12
|
+
* Sixteen hex characters. This identifies a defect for humans and tooling, not
|
|
13
|
+
* a secret, and a full digest in every entry would make a baseline file for a
|
|
14
|
+
* large site unreadable.
|
|
15
|
+
*/
|
|
16
|
+
function elementFingerprint(ruleId, selector, html) {
|
|
17
|
+
return createHash("sha256").update(`${ruleId}\n${selector}\n${openingTag(html)}`).digest("hex").slice(0, 16);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The element's own tag, without anything nested inside it.
|
|
21
|
+
*
|
|
22
|
+
* axe-core hands back the failing element's outerHTML, which for a leaf like
|
|
23
|
+
* `<img>` is the element and for a container is the element and every
|
|
24
|
+
* descendant it has. Hashing all of that made the identity of a container
|
|
25
|
+
* depend on its contents — and the container that matters here is `<html>`,
|
|
26
|
+
* which every document-level rule fails against: `html-has-lang`,
|
|
27
|
+
* `document-title`, `landmark-one-main`, `page-has-heading-one`.
|
|
28
|
+
*
|
|
29
|
+
* Their outerHTML is the whole page. So adding one paragraph anywhere changed
|
|
30
|
+
* the fingerprint of every document-level violation on that page, and all three
|
|
31
|
+
* consumers believed it:
|
|
32
|
+
*
|
|
33
|
+
* - the baseline stopped suppressing barriers it had accepted, and the build
|
|
34
|
+
* went red on a page whose only change was a typo fix;
|
|
35
|
+
* - `diff` reported the same untouched barrier as both new and **fixed**, which
|
|
36
|
+
* is the one thing it exists to refuse to do;
|
|
37
|
+
* - SARIF churned its partialFingerprints, so code scanning closed an alert and
|
|
38
|
+
* opened an identical one on every edit.
|
|
39
|
+
*
|
|
40
|
+
* The identity of an element is its own tag and attributes. Where two elements
|
|
41
|
+
* share those, axe-core's selector already tells them apart — it qualifies
|
|
42
|
+
* ambiguous matches with `:nth-child(…)` — so nothing that was distinguishable
|
|
43
|
+
* before stops being distinguishable now.
|
|
44
|
+
*
|
|
45
|
+
* Anything that is not an element — the empty string a rule with no attached
|
|
46
|
+
* node carries — is returned unchanged, so those keep the identity they had.
|
|
47
|
+
*/
|
|
48
|
+
function openingTag(html) {
|
|
49
|
+
const trimmed = html.trim();
|
|
50
|
+
if (!trimmed.startsWith("<")) return trimmed;
|
|
51
|
+
let quote;
|
|
52
|
+
for (let i = 1; i < trimmed.length; i += 1) {
|
|
53
|
+
const char = trimmed[i];
|
|
54
|
+
if (quote !== void 0) {
|
|
55
|
+
if (char === quote) quote = void 0;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === "\"" || char === "'") {
|
|
59
|
+
quote = char;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === ">") return trimmed.slice(0, i + 1);
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { elementFingerprint as t };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
|
|
2
2
|
import { i as escapeText, n as count, o as standardsReference, r as escapeAttribute, t as collapse } from "./text-CKKpzkYM.js";
|
|
3
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
-
import { a as isShared, i as groupIssues, n as blindRules, o as discoveryLabel, r as coverageParts, s as missedParts, t as remediationFor } from "./remediation-
|
|
4
|
+
import { a as isShared, i as groupIssues, n as blindRules, o as discoveryLabel, r as coverageParts, s as missedParts, t as remediationFor } from "./remediation-CMBIrnpN.js";
|
|
5
5
|
import { r as componentPath } from "./component-C3GL1Mnu.js";
|
|
6
|
-
import { i as understandingUrl, r as manualCheckFor, t as buildCoverage } from "./coverage-
|
|
7
|
-
import { n as buildSummary } from "./json-
|
|
6
|
+
import { i as understandingUrl, r as manualCheckFor, t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
|
|
7
|
+
import { n as buildSummary } from "./json-DROX33kh.js";
|
|
8
8
|
import axe from "axe-core";
|
|
9
9
|
//#region src/audit/report/html.ts
|
|
10
10
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -60,10 +60,17 @@ type DefaultedSchema<T> = Schema<T> & {
|
|
|
60
60
|
//#endregion
|
|
61
61
|
//#region src/config/define.d.ts
|
|
62
62
|
/** Countries with their own supervisory body and statute text. */
|
|
63
|
-
declare const COUNTRIES: readonly ['AT', 'DE', 'CH'];
|
|
63
|
+
declare const COUNTRIES: readonly ['AT', 'DE', 'CH', 'ES', 'FR', 'IT', 'NL'];
|
|
64
64
|
type Country = (typeof COUNTRIES)[number];
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Languages a statement can be rendered in.
|
|
67
|
+
*
|
|
68
|
+
* Not every country has every one: a statement is a document under a particular
|
|
69
|
+
* legal regime, not a translation of a document under another, so each country
|
|
70
|
+
* has the language it is published in and English. `renderStatement` says which
|
|
71
|
+
* ones a country has when asked for one it does not.
|
|
72
|
+
*/
|
|
73
|
+
declare const STATEMENT_LOCALES: readonly ['de', 'en', 'es', 'fr', 'it', 'nl'];
|
|
67
74
|
type StatementLocale = (typeof STATEMENT_LOCALES)[number];
|
|
68
75
|
/**
|
|
69
76
|
* Wording follows the EU model statement: fully, partially, or not conformant
|
|
@@ -81,6 +88,61 @@ type AssessmentMethod = (typeof ASSESSMENT_METHODS)[number];
|
|
|
81
88
|
*/
|
|
82
89
|
declare const ISSUE_REASONS: readonly ['disproportionate-burden', 'out-of-scope', 'fix-planned'];
|
|
83
90
|
type IssueReason = (typeof ISSUE_REASONS)[number];
|
|
91
|
+
/**
|
|
92
|
+
* Defaults for `eaa-kit audit` and `eaa-kit baseline`, so a project says once
|
|
93
|
+
* what every invocation would otherwise repeat.
|
|
94
|
+
*
|
|
95
|
+
* Every field is optional and every one is a default: a flag actually typed on
|
|
96
|
+
* the command line wins, because the file is the project's usual answer and the
|
|
97
|
+
* flag is somebody asking for something else right now.
|
|
98
|
+
*
|
|
99
|
+
* `baseline` reads the subset that means the same thing to it. `output`,
|
|
100
|
+
* `format`, `failOn` and `baseline` are audit-only on purpose — a baseline
|
|
101
|
+
* written to the report's path would overwrite the report, and a threshold for
|
|
102
|
+
* failing a run means nothing to a command that records what it finds.
|
|
103
|
+
*/
|
|
104
|
+
declare const auditSchema: Schema<ObjectOf<{
|
|
105
|
+
/** Build directory. The positional argument wins over it. */
|
|
106
|
+
dir: OptionalSchema<string>;
|
|
107
|
+
include: OptionalSchema<string[]>;
|
|
108
|
+
exclude: OptionalSchema<string[]>;
|
|
109
|
+
/** Audit pages under their real site URL instead of file://. */
|
|
110
|
+
baseUrl: OptionalSchema<string>;
|
|
111
|
+
/** Audit a running site instead of a directory. */
|
|
112
|
+
url: OptionalSchema<string>;
|
|
113
|
+
/** Crawl a host that is not loopback. Off unless a project says otherwise. */
|
|
114
|
+
allowRemote: OptionalSchema<boolean>;
|
|
115
|
+
ignoreRobots: OptionalSchema<boolean>;
|
|
116
|
+
/** Where the site lists its pages, when that is not /sitemap.xml. */
|
|
117
|
+
sitemap: OptionalSchema<string>;
|
|
118
|
+
maxPages: OptionalSchema<number>;
|
|
119
|
+
/** 0 audits the entry page alone. */
|
|
120
|
+
maxDepth: OptionalSchema<number>;
|
|
121
|
+
/** Lowest impact that exits 1. */
|
|
122
|
+
failOn: OptionalSchema<"critical" | "minor" | "moderate" | "serious">;
|
|
123
|
+
format: OptionalSchema<"console" | "html" | "json" | "sarif">;
|
|
124
|
+
/** Write the report here instead of stdout. */
|
|
125
|
+
output: OptionalSchema<string>;
|
|
126
|
+
/** Audit in real Chromium. Needs the playwright peer. */
|
|
127
|
+
browser: OptionalSchema<boolean>;
|
|
128
|
+
/** Skip the rules the browserless engine cannot decide. No effect with `browser`. */
|
|
129
|
+
fast: OptionalSchema<boolean>;
|
|
130
|
+
concurrency: OptionalSchema<number>;
|
|
131
|
+
/** Path to a baseline; violations it accounts for do not fail the run. */
|
|
132
|
+
baseline: OptionalSchema<string>;
|
|
133
|
+
/** List every page and its result under the issues. */
|
|
134
|
+
perPage: OptionalSchema<boolean>;
|
|
135
|
+
/** Print the manual check for each rule the engine could not evaluate. */
|
|
136
|
+
manual: OptionalSchema<boolean>;
|
|
137
|
+
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
138
|
+
coverage: OptionalSchema<boolean>;
|
|
139
|
+
/**
|
|
140
|
+
* False is `--no-build`: never run the project's build or start its server to
|
|
141
|
+
* find something to audit. Written in the positive because that is the state
|
|
142
|
+
* being described, and because a config file has no flags to negate.
|
|
143
|
+
*/
|
|
144
|
+
build: OptionalSchema<boolean>;
|
|
145
|
+
}>>;
|
|
84
146
|
declare const configSchema: Schema<ObjectOf<{
|
|
85
147
|
site: Schema<ObjectOf<{
|
|
86
148
|
name: Schema<string>;
|
|
@@ -132,7 +194,50 @@ declare const configSchema: Schema<ObjectOf<{
|
|
|
132
194
|
}>>;
|
|
133
195
|
enforcement: Schema<ObjectOf<{
|
|
134
196
|
/** Drives which supervisory body and statute the template names. */
|
|
135
|
-
country: Schema<"AT" | "CH" | "DE">;
|
|
197
|
+
country: Schema<"AT" | "CH" | "DE" | "ES" | "FR" | "IT" | "NL">;
|
|
198
|
+
}>>;
|
|
199
|
+
/** Defaults for the audit commands. Nothing here reaches the statement. */
|
|
200
|
+
audit: OptionalSchema<ObjectOf<{
|
|
201
|
+
/** Build directory. The positional argument wins over it. */
|
|
202
|
+
dir: OptionalSchema<string>;
|
|
203
|
+
include: OptionalSchema<string[]>;
|
|
204
|
+
exclude: OptionalSchema<string[]>;
|
|
205
|
+
/** Audit pages under their real site URL instead of file://. */
|
|
206
|
+
baseUrl: OptionalSchema<string>;
|
|
207
|
+
/** Audit a running site instead of a directory. */
|
|
208
|
+
url: OptionalSchema<string>;
|
|
209
|
+
/** Crawl a host that is not loopback. Off unless a project says otherwise. */
|
|
210
|
+
allowRemote: OptionalSchema<boolean>;
|
|
211
|
+
ignoreRobots: OptionalSchema<boolean>;
|
|
212
|
+
/** Where the site lists its pages, when that is not /sitemap.xml. */
|
|
213
|
+
sitemap: OptionalSchema<string>;
|
|
214
|
+
maxPages: OptionalSchema<number>;
|
|
215
|
+
/** 0 audits the entry page alone. */
|
|
216
|
+
maxDepth: OptionalSchema<number>;
|
|
217
|
+
/** Lowest impact that exits 1. */
|
|
218
|
+
failOn: OptionalSchema<"critical" | "minor" | "moderate" | "serious">;
|
|
219
|
+
format: OptionalSchema<"console" | "html" | "json" | "sarif">;
|
|
220
|
+
/** Write the report here instead of stdout. */
|
|
221
|
+
output: OptionalSchema<string>;
|
|
222
|
+
/** Audit in real Chromium. Needs the playwright peer. */
|
|
223
|
+
browser: OptionalSchema<boolean>;
|
|
224
|
+
/** Skip the rules the browserless engine cannot decide. No effect with `browser`. */
|
|
225
|
+
fast: OptionalSchema<boolean>;
|
|
226
|
+
concurrency: OptionalSchema<number>;
|
|
227
|
+
/** Path to a baseline; violations it accounts for do not fail the run. */
|
|
228
|
+
baseline: OptionalSchema<string>;
|
|
229
|
+
/** List every page and its result under the issues. */
|
|
230
|
+
perPage: OptionalSchema<boolean>;
|
|
231
|
+
/** Print the manual check for each rule the engine could not evaluate. */
|
|
232
|
+
manual: OptionalSchema<boolean>;
|
|
233
|
+
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
234
|
+
coverage: OptionalSchema<boolean>;
|
|
235
|
+
/**
|
|
236
|
+
* False is `--no-build`: never run the project's build or start its server to
|
|
237
|
+
* find something to audit. Written in the positive because that is the state
|
|
238
|
+
* being described, and because a config file has no flags to negate.
|
|
239
|
+
*/
|
|
240
|
+
build: OptionalSchema<boolean>;
|
|
136
241
|
}>>;
|
|
137
242
|
}>>;
|
|
138
243
|
/**
|
|
@@ -167,6 +272,7 @@ interface EaaConfigInput {
|
|
|
167
272
|
enforcement: {
|
|
168
273
|
country: Country;
|
|
169
274
|
};
|
|
275
|
+
audit?: AuditConfig;
|
|
170
276
|
}
|
|
171
277
|
/** One barrier, as written in a config file. */
|
|
172
278
|
interface KnownIssueInput {
|
|
@@ -178,6 +284,15 @@ interface KnownIssueInput {
|
|
|
178
284
|
}
|
|
179
285
|
type EaaConfig = Infer<typeof configSchema>;
|
|
180
286
|
type KnownIssue = EaaConfig['compliance']['knownIssues'][number];
|
|
287
|
+
/**
|
|
288
|
+
* The `audit` block, as written and as parsed — every field is optional.
|
|
289
|
+
*
|
|
290
|
+
* `undefined` is mapped out of the value types rather than left in them:
|
|
291
|
+
* `s.object` never writes a key it did not parse, so an absent field is an
|
|
292
|
+
* absent key, and the commands spread this over their own options where a
|
|
293
|
+
* present-but-undefined key would overwrite a real value.
|
|
294
|
+
*/
|
|
295
|
+
type AuditConfig = { [K in keyof Infer<typeof auditSchema>]?: Exclude<Infer<typeof auditSchema>[K], undefined>; };
|
|
181
296
|
/**
|
|
182
297
|
* Identity function that gives `eaa.config.ts` its types. Deliberately does not
|
|
183
298
|
* validate: a config file is loaded and checked in one place, so that an error
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-
|
|
1
|
+
import { a as ASSESSMENT_METHODS, c as ConfigError, d as configSchema, f as defineConfig, i as loadConfig, l as ISSUE_REASONS, n as findConfigFile, o as COMPLIANCE_STATUSES, p as parseConfig, s as COUNTRIES, t as CONFIG_FILENAMES, u as STATEMENT_LOCALES } from "./load-yAR4wzez.js";
|
|
2
|
+
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DbGOVmhx.js";
|
|
3
3
|
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as DEFAULT_FAIL_ON } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { s as COUNTRIES, t as CONFIG_FILENAMES } from "./load-yAR4wzez.js";
|
|
2
3
|
import { t as exists } from "./fs-BmPtmFke.js";
|
|
3
|
-
import {
|
|
4
|
+
import { l as warn, o as fail, s as note } from "./command-C3D7JWn6.js";
|
|
4
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import pc from "picocolors";
|
|
@@ -9,7 +10,11 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
10
|
const COUNTRY_LOCALES = {
|
|
10
11
|
AT: "de-AT",
|
|
11
12
|
DE: "de-DE",
|
|
12
|
-
CH: "de-CH"
|
|
13
|
+
CH: "de-CH",
|
|
14
|
+
ES: "es-ES",
|
|
15
|
+
FR: "fr-FR",
|
|
16
|
+
IT: "it-IT",
|
|
17
|
+
NL: "nl-NL"
|
|
13
18
|
};
|
|
14
19
|
/**
|
|
15
20
|
* Everything the project already says about itself.
|
|
@@ -83,7 +88,8 @@ async function runInitCommand(options = {}) {
|
|
|
83
88
|
assessmentMethod: "self-assessment",
|
|
84
89
|
knownIssues: []
|
|
85
90
|
},
|
|
86
|
-
enforcement: { country }
|
|
91
|
+
enforcement: { country },
|
|
92
|
+
audit: { failOn: DEFAULT_FAIL_ON }
|
|
87
93
|
};
|
|
88
94
|
try {
|
|
89
95
|
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as failedPage, i as blindRulesInScope, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, t as DEFAULT_PAGE_TIMEOUT_MS, u as shapeResults } from "./result-BWcYXeRs.js";
|
|
2
2
|
import axe from "axe-core";
|
|
3
3
|
import { Script } from "node:vm";
|
|
4
4
|
import { JSDOM, VirtualConsole } from "jsdom";
|
|
5
5
|
//#region src/audit/runners/jsdom.ts
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Per-page ceiling.
|
|
8
|
+
*
|
|
9
|
+
* A soft one, and the limit is worth stating: this races axe-core against a
|
|
10
|
+
* timer, so it only fires where the work yields to the event loop. Neither
|
|
11
|
+
* jsdom's parse nor axe-core's walk of the tree does, so a document pathological
|
|
12
|
+
* enough to hold the thread runs past this unimpeded. The hard ceiling is the
|
|
13
|
+
* worker pool's, which terminates the thread; see the note on `runWorkers`.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = DEFAULT_PAGE_TIMEOUT_MS;
|
|
8
16
|
/**
|
|
9
17
|
* Audit collected pages with axe-core inside jsdom.
|
|
10
18
|
*
|
|
@@ -32,7 +40,7 @@ async function auditPage(page, options = {}) {
|
|
|
32
40
|
dom = createDom(page.html, url);
|
|
33
41
|
injectAxe(dom);
|
|
34
42
|
const { axe: pageAxe } = dom.window;
|
|
35
|
-
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags)), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
43
|
+
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags, { skipBlindRules: options.fast === true })), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
36
44
|
return shapeResults(results, {
|
|
37
45
|
...identity,
|
|
38
46
|
durationMs: Date.now() - startedAt,
|