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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
|
|
2
2
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
3
|
-
import { t as elementFingerprint } from "./fingerprint-
|
|
4
|
-
import {
|
|
5
|
-
import { t as buildCoverage } from "./coverage-
|
|
3
|
+
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
4
|
+
import { c as ruleOutcomes } from "./result-BWcYXeRs.js";
|
|
5
|
+
import { t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
|
|
6
6
|
import axe from "axe-core";
|
|
7
7
|
/**
|
|
8
8
|
* Build the machine-readable report.
|
|
@@ -17,7 +17,7 @@ import axe from "axe-core";
|
|
|
17
17
|
function buildJsonReport(audits, options) {
|
|
18
18
|
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
19
19
|
return {
|
|
20
|
-
schemaVersion:
|
|
20
|
+
schemaVersion: 2,
|
|
21
21
|
tool: {
|
|
22
22
|
name: "eaa-kit",
|
|
23
23
|
version: TOOL_VERSION,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-
|
|
1
|
+
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-DROX33kh.js";
|
|
2
2
|
export { buildJsonReport, serialiseJsonReport };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as IMPACT_LEVELS } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { _ as union, a as integer, d as optional, f as pipe, g as transform, h as string, i as enumeration, m as safeParse, n as boolean, o as isoDate, r as email, t as array, u as object, v as url, y as withDefault } from "./schema-DJSF4K05.js";
|
|
2
3
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -8,10 +9,28 @@ import { pathToFileURL } from "node:url";
|
|
|
8
9
|
const COUNTRIES = [
|
|
9
10
|
"AT",
|
|
10
11
|
"DE",
|
|
11
|
-
"CH"
|
|
12
|
+
"CH",
|
|
13
|
+
"ES",
|
|
14
|
+
"FR",
|
|
15
|
+
"IT",
|
|
16
|
+
"NL"
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Languages a statement can be rendered in.
|
|
20
|
+
*
|
|
21
|
+
* Not every country has every one: a statement is a document under a particular
|
|
22
|
+
* legal regime, not a translation of a document under another, so each country
|
|
23
|
+
* has the language it is published in and English. `renderStatement` says which
|
|
24
|
+
* ones a country has when asked for one it does not.
|
|
25
|
+
*/
|
|
26
|
+
const STATEMENT_LOCALES = [
|
|
27
|
+
"de",
|
|
28
|
+
"en",
|
|
29
|
+
"es",
|
|
30
|
+
"fr",
|
|
31
|
+
"it",
|
|
32
|
+
"nl"
|
|
12
33
|
];
|
|
13
|
-
/** Languages a statement can be rendered in. */
|
|
14
|
-
const STATEMENT_LOCALES = ["de", "en"];
|
|
15
34
|
/**
|
|
16
35
|
* Wording follows the EU model statement: fully, partially, or not conformant
|
|
17
36
|
* with the standard. "partially-compliant" is the honest answer for most sites
|
|
@@ -50,6 +69,66 @@ const knownIssueObject = object({
|
|
|
50
69
|
* rather than a union that callers have to narrow before reading `remedyBy`.
|
|
51
70
|
*/
|
|
52
71
|
const knownIssueSchema = union([pipe(transform(string({ min: 1 }), (description) => ({ description })), knownIssueObject), knownIssueObject], "expected a description, or an object with one");
|
|
72
|
+
/**
|
|
73
|
+
* Defaults for `eaa-kit audit` and `eaa-kit baseline`, so a project says once
|
|
74
|
+
* what every invocation would otherwise repeat.
|
|
75
|
+
*
|
|
76
|
+
* Every field is optional and every one is a default: a flag actually typed on
|
|
77
|
+
* the command line wins, because the file is the project's usual answer and the
|
|
78
|
+
* flag is somebody asking for something else right now.
|
|
79
|
+
*
|
|
80
|
+
* `baseline` reads the subset that means the same thing to it. `output`,
|
|
81
|
+
* `format`, `failOn` and `baseline` are audit-only on purpose — a baseline
|
|
82
|
+
* written to the report's path would overwrite the report, and a threshold for
|
|
83
|
+
* failing a run means nothing to a command that records what it finds.
|
|
84
|
+
*/
|
|
85
|
+
const auditSchema = object({
|
|
86
|
+
/** Build directory. The positional argument wins over it. */
|
|
87
|
+
dir: optional(string({ min: 1 })),
|
|
88
|
+
include: optional(array(string({ min: 1 }))),
|
|
89
|
+
exclude: optional(array(string({ min: 1 }))),
|
|
90
|
+
/** Audit pages under their real site URL instead of file://. */
|
|
91
|
+
baseUrl: optional(url()),
|
|
92
|
+
/** Audit a running site instead of a directory. */
|
|
93
|
+
url: optional(url()),
|
|
94
|
+
/** Crawl a host that is not loopback. Off unless a project says otherwise. */
|
|
95
|
+
allowRemote: optional(boolean()),
|
|
96
|
+
ignoreRobots: optional(boolean()),
|
|
97
|
+
/** Where the site lists its pages, when that is not /sitemap.xml. */
|
|
98
|
+
sitemap: optional(string({ min: 1 })),
|
|
99
|
+
maxPages: optional(integer({ min: 1 })),
|
|
100
|
+
/** 0 audits the entry page alone. */
|
|
101
|
+
maxDepth: optional(integer({ min: 0 })),
|
|
102
|
+
/** Lowest impact that exits 1. */
|
|
103
|
+
failOn: optional(enumeration(IMPACT_LEVELS)),
|
|
104
|
+
format: optional(enumeration([
|
|
105
|
+
"console",
|
|
106
|
+
"json",
|
|
107
|
+
"sarif",
|
|
108
|
+
"html"
|
|
109
|
+
])),
|
|
110
|
+
/** Write the report here instead of stdout. */
|
|
111
|
+
output: optional(string({ min: 1 })),
|
|
112
|
+
/** Audit in real Chromium. Needs the playwright peer. */
|
|
113
|
+
browser: optional(boolean()),
|
|
114
|
+
/** Skip the rules the browserless engine cannot decide. No effect with `browser`. */
|
|
115
|
+
fast: optional(boolean()),
|
|
116
|
+
concurrency: optional(integer({ min: 1 })),
|
|
117
|
+
/** Path to a baseline; violations it accounts for do not fail the run. */
|
|
118
|
+
baseline: optional(string({ min: 1 })),
|
|
119
|
+
/** List every page and its result under the issues. */
|
|
120
|
+
perPage: optional(boolean()),
|
|
121
|
+
/** Print the manual check for each rule the engine could not evaluate. */
|
|
122
|
+
manual: optional(boolean()),
|
|
123
|
+
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
124
|
+
coverage: optional(boolean()),
|
|
125
|
+
/**
|
|
126
|
+
* False is `--no-build`: never run the project's build or start its server to
|
|
127
|
+
* find something to audit. Written in the positive because that is the state
|
|
128
|
+
* being described, and because a config file has no flags to negate.
|
|
129
|
+
*/
|
|
130
|
+
build: optional(boolean())
|
|
131
|
+
});
|
|
53
132
|
const configSchema = object({
|
|
54
133
|
site: object({
|
|
55
134
|
name: string({ min: 1 }),
|
|
@@ -91,9 +170,20 @@ const configSchema = object({
|
|
|
91
170
|
}),
|
|
92
171
|
enforcement: object({
|
|
93
172
|
/** Drives which supervisory body and statute the template names. */
|
|
94
|
-
country: enumeration(COUNTRIES) })
|
|
173
|
+
country: enumeration(COUNTRIES) }),
|
|
174
|
+
/** Defaults for the audit commands. Nothing here reaches the statement. */
|
|
175
|
+
audit: optional(auditSchema)
|
|
95
176
|
});
|
|
96
177
|
/**
|
|
178
|
+
* The `audit` block on its own.
|
|
179
|
+
*
|
|
180
|
+
* A project that only wants audit defaults should not have to write a complete
|
|
181
|
+
* statement config to get them, and `s.object` drops the keys it does not know,
|
|
182
|
+
* so the same file satisfies both readers: `statement` demands the whole
|
|
183
|
+
* document, `audit` reads this and ignores the rest.
|
|
184
|
+
*/
|
|
185
|
+
const auditConfigSchema = object({ audit: optional(auditSchema) });
|
|
186
|
+
/**
|
|
97
187
|
* Identity function that gives `eaa.config.ts` its types. Deliberately does not
|
|
98
188
|
* validate: a config file is loaded and checked in one place, so that an error
|
|
99
189
|
* points at the file rather than at wherever the module happened to be
|
|
@@ -112,7 +202,20 @@ var ConfigError = class extends Error {
|
|
|
112
202
|
};
|
|
113
203
|
/** Validate an already-loaded config object. */
|
|
114
204
|
function parseConfig(value, source = "config") {
|
|
115
|
-
|
|
205
|
+
return parse(configSchema, value, source);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Read only the `audit` block, ignoring whatever else the file holds.
|
|
209
|
+
*
|
|
210
|
+
* Returns undefined where there is no block, which is the common case: most
|
|
211
|
+
* config files exist for the statement alone, and finding one is not a reason
|
|
212
|
+
* to change how an audit runs.
|
|
213
|
+
*/
|
|
214
|
+
function parseAuditConfig(value, source = "config") {
|
|
215
|
+
return parse(auditConfigSchema, value, source).audit;
|
|
216
|
+
}
|
|
217
|
+
function parse(schema, value, source) {
|
|
218
|
+
const result = safeParse(schema, value);
|
|
116
219
|
if (result.success) return result.data;
|
|
117
220
|
const issues = result.error.issues.map((issue) => {
|
|
118
221
|
const path = issue.path.join(".");
|
|
@@ -145,10 +248,32 @@ async function loadConfig(options = {}) {
|
|
|
145
248
|
if (!file) throw new ConfigError(`No config file found in ${cwd} or its parent directories`, CONFIG_FILENAMES.map((name) => `looked for ${name}`));
|
|
146
249
|
if (!await isFile(file)) throw new ConfigError(`Config file not found: ${file}`);
|
|
147
250
|
return {
|
|
148
|
-
config: parseConfig(
|
|
251
|
+
config: parseConfig(await readConfigFile(file), path.basename(file)),
|
|
252
|
+
path: file
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Find the config file and read its `audit` block, for the commands that take
|
|
257
|
+
* defaults from it.
|
|
258
|
+
*
|
|
259
|
+
* Returns undefined when there is no config file at all. That is not an error
|
|
260
|
+
* here as it is for `statement`: `eaa-kit audit` has always run against a
|
|
261
|
+
* project that has never heard of a config file, and it must keep doing so. An
|
|
262
|
+
* explicit path that is not there is still an error, because somebody named it.
|
|
263
|
+
*/
|
|
264
|
+
async function loadAuditConfig(options = {}) {
|
|
265
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
266
|
+
const file = options.path ? path.resolve(cwd, options.path) : await findConfigFile(cwd);
|
|
267
|
+
if (!file) return void 0;
|
|
268
|
+
if (!await isFile(file)) throw new ConfigError(`Config file not found: ${file}`);
|
|
269
|
+
return {
|
|
270
|
+
audit: parseAuditConfig(await readConfigFile(file), path.basename(file)),
|
|
149
271
|
path: file
|
|
150
272
|
};
|
|
151
273
|
}
|
|
274
|
+
function readConfigFile(file) {
|
|
275
|
+
return file.endsWith(".json") ? importJson(file) : importModule(file);
|
|
276
|
+
}
|
|
152
277
|
/** Walks up from `cwd`, so the CLI works from a subdirectory of the project. */
|
|
153
278
|
async function findConfigFile(cwd) {
|
|
154
279
|
let directory = path.resolve(cwd);
|
|
@@ -183,4 +308,4 @@ async function importModule(file) {
|
|
|
183
308
|
return module.default;
|
|
184
309
|
}
|
|
185
310
|
//#endregion
|
|
186
|
-
export {
|
|
311
|
+
export { ASSESSMENT_METHODS as a, ConfigError as c, configSchema as d, defineConfig as f, loadConfig as i, ISSUE_REASONS as l, findConfigFile as n, COMPLIANCE_STATUSES as o, parseConfig as p, loadAuditConfig as r, COUNTRIES as s, CONFIG_FILENAMES as t, STATEMENT_LOCALES as u };
|
package/dist/nuxt/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/nuxt/index.d.ts
|
|
3
3
|
/** The part of the Nitro instance this reads, handed over by `nitro:init`. */
|
|
4
4
|
interface NitroLike {
|
package/dist/nuxt/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-
|
|
1
|
+
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-C2nKFcb-.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/nuxt/index.ts
|
|
4
4
|
function eaaKitModule(options = {}, nuxt) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
-
import {
|
|
2
|
+
import { a as failedPage, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, u as shapeResults } from "./result-BWcYXeRs.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
@@ -122,6 +122,21 @@ const DEFAULT_VIEWPORT = {
|
|
|
122
122
|
height: 720
|
|
123
123
|
};
|
|
124
124
|
/**
|
|
125
|
+
* Pages open at once.
|
|
126
|
+
*
|
|
127
|
+
* This runner used to audit strictly one page at a time while the browserless
|
|
128
|
+
* one had a whole measured worker pool — which had it backwards, since the
|
|
129
|
+
* browser is the slow engine: it fetches the stylesheets and images a real
|
|
130
|
+
* visitor would, and spends most of a page waiting on them rather than on the
|
|
131
|
+
* CPU. Waiting on four at once is close to free.
|
|
132
|
+
*
|
|
133
|
+
* Four rather than more because each open page holds a document tree, its
|
|
134
|
+
* decoded images and its own copy of axe-core, and Chromium's memory is the
|
|
135
|
+
* limit here rather than cores. Past this the gain flattens and the cost does
|
|
136
|
+
* not.
|
|
137
|
+
*/
|
|
138
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
139
|
+
/**
|
|
125
140
|
* Audit a built site in real Chromium.
|
|
126
141
|
*
|
|
127
142
|
* This is the engine that can answer the rules jsdom is blind to: colour
|
|
@@ -152,11 +167,20 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
152
167
|
viewport,
|
|
153
168
|
bypassCSP: true
|
|
154
169
|
});
|
|
155
|
-
const audits =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
const audits = Array.from({ length: pages.length });
|
|
171
|
+
const lanes = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, pages.length));
|
|
172
|
+
let next = 0;
|
|
173
|
+
await Promise.all(Array.from({ length: lanes }, async () => {
|
|
174
|
+
while (true) {
|
|
175
|
+
const index = next;
|
|
176
|
+
if (index >= pages.length) return;
|
|
177
|
+
next += 1;
|
|
178
|
+
audits[index] = await auditOne(context, server?.origin, pages[index], {
|
|
179
|
+
tags,
|
|
180
|
+
timeout,
|
|
181
|
+
...options
|
|
182
|
+
});
|
|
183
|
+
}
|
|
160
184
|
}));
|
|
161
185
|
await context.close();
|
|
162
186
|
return audits;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
-
import {
|
|
2
|
+
import { a as failedPage, s as pageUrl } from "./result-BWcYXeRs.js";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { availableParallelism } from "node:os";
|
|
5
5
|
//#region src/audit/runners/pool.ts
|
|
@@ -52,6 +52,16 @@ const WORK_PER_WORKER_MS = 1600;
|
|
|
52
52
|
/** Threading at all means at least two, or there is nothing to overlap with. */
|
|
53
53
|
const MIN_WORKERS = 2;
|
|
54
54
|
/**
|
|
55
|
+
* Grace above the runner's own per-page timeout before a worker is killed.
|
|
56
|
+
*
|
|
57
|
+
* The runner races axe-core against a timer, which works whenever the work
|
|
58
|
+
* yields to the event loop: the page is reported with an error and the thread
|
|
59
|
+
* lives on to take the next one. That is the better outcome, so it is given
|
|
60
|
+
* room to happen first. This is the backstop for when it cannot — see
|
|
61
|
+
* `watchdog` below.
|
|
62
|
+
*/
|
|
63
|
+
const HARD_TIMEOUT_GRACE_MS = 5e3;
|
|
64
|
+
/**
|
|
55
65
|
* Ceiling on workers regardless of core count. Past this, the run is bounded by
|
|
56
66
|
* memory bandwidth and by each worker's own start-up rather than by cores, and
|
|
57
67
|
* every extra thread holds another jsdom document tree.
|
|
@@ -96,10 +106,33 @@ async function runPooledAudit(pages, options = {}) {
|
|
|
96
106
|
return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
|
|
97
107
|
}
|
|
98
108
|
async function auditHere(pages, options) {
|
|
99
|
-
const { runJsdomAudit } = await import("./jsdom-
|
|
109
|
+
const { runJsdomAudit } = await import("./jsdom-22Bkt65v.js");
|
|
100
110
|
return runJsdomAudit(pages, options);
|
|
101
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The only place a per-page ceiling can actually be enforced.
|
|
114
|
+
*
|
|
115
|
+
* The runner's own timeout is a `Promise.race`, and a race cannot interrupt
|
|
116
|
+
* synchronous work: jsdom's parse and axe-core's walk of the tree both hold the
|
|
117
|
+
* thread, so the timer that is meant to stop them never gets to run. Measured
|
|
118
|
+
* on a 120,000-element document with a two-second ceiling, the audit was still
|
|
119
|
+
* going more than ten minutes later — and because the pool waits on its
|
|
120
|
+
* workers, the whole run went with it. A CI job hung until the platform killed
|
|
121
|
+
* it, which is the failure the ceiling exists to prevent.
|
|
122
|
+
*
|
|
123
|
+
* `worker.terminate()` is the answer, because it stops the thread whatever it
|
|
124
|
+
* is doing. So the supervisor keeps its own deadline per page and kills the
|
|
125
|
+
* thread that overruns it, records that page as unaudited, and lets the rest of
|
|
126
|
+
* the run carry on. The page is reported as a failure rather than as a clean
|
|
127
|
+
* page, which the CLI already turns into exit 2.
|
|
128
|
+
*
|
|
129
|
+
* Two runs still have no hard ceiling, because both refuse the threads that
|
|
130
|
+
* would carry it: `--concurrency 1`, and a machine with too few cores to spare
|
|
131
|
+
* one. Both are documented rather than papered over, and the size cap in
|
|
132
|
+
* `collectPages` is what keeps them bounded in practice.
|
|
133
|
+
*/
|
|
102
134
|
async function runWorkers(pages, options, count, entry) {
|
|
135
|
+
const deadlineMs = (options.timeoutMs ?? 3e4) + HARD_TIMEOUT_GRACE_MS;
|
|
103
136
|
const audits = Array.from({ length: pages.length });
|
|
104
137
|
let next = 0;
|
|
105
138
|
await Promise.all(Array.from({ length: count }, () => {
|
|
@@ -113,7 +146,13 @@ async function runWorkers(pages, options, count, entry) {
|
|
|
113
146
|
}
|
|
114
147
|
let inFlight;
|
|
115
148
|
let completed = 0;
|
|
149
|
+
let watchdog;
|
|
150
|
+
const disarm = () => {
|
|
151
|
+
if (watchdog !== void 0) clearTimeout(watchdog);
|
|
152
|
+
watchdog = void 0;
|
|
153
|
+
};
|
|
116
154
|
const finish = () => {
|
|
155
|
+
disarm();
|
|
117
156
|
worker.terminate();
|
|
118
157
|
resolve();
|
|
119
158
|
};
|
|
@@ -127,8 +166,18 @@ async function runWorkers(pages, options, count, entry) {
|
|
|
127
166
|
next += 1;
|
|
128
167
|
inFlight = index;
|
|
129
168
|
worker.postMessage(pages[index]);
|
|
169
|
+
watchdog = setTimeout(() => {
|
|
170
|
+
watchdog = void 0;
|
|
171
|
+
if (inFlight !== void 0) {
|
|
172
|
+
audits[inFlight] = failedPage(identity(pages[inFlight], options), `the audit worker was stopped after ${deadlineMs}ms on this page`);
|
|
173
|
+
inFlight = void 0;
|
|
174
|
+
}
|
|
175
|
+
finish();
|
|
176
|
+
}, deadlineMs);
|
|
177
|
+
watchdog.unref?.();
|
|
130
178
|
};
|
|
131
179
|
worker.on("message", (audit) => {
|
|
180
|
+
disarm();
|
|
132
181
|
if (inFlight !== void 0) {
|
|
133
182
|
audits[inFlight] = audit;
|
|
134
183
|
completed += 1;
|
|
@@ -136,6 +185,7 @@ async function runWorkers(pages, options, count, entry) {
|
|
|
136
185
|
feed();
|
|
137
186
|
});
|
|
138
187
|
worker.on("error", (cause) => {
|
|
188
|
+
disarm();
|
|
139
189
|
if (inFlight !== void 0 && completed > 0) audits[inFlight] = failedPage(identity(pages[inFlight], options), `audit worker failed: ${cause.message}`);
|
|
140
190
|
finish();
|
|
141
191
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as impactRank } from "./impact-DZt2oBCP.js";
|
|
2
|
-
import { t as elementFingerprint } from "./fingerprint-
|
|
3
|
-
import {
|
|
2
|
+
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
3
|
+
import { o as findingElements } from "./result-BWcYXeRs.js";
|
|
4
4
|
//#region src/audit/completeness.ts
|
|
5
5
|
/**
|
|
6
6
|
* Fold what the collector knew together with what the engine managed.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { c as number, f as record, l as object, m as string, o as isoDateTime, p as safeParse, s as nullable, t as array, v as withDefault } from "./schema-is6CGX2D.js";
|
|
2
|
-
import { n as isDirectory } from "./fs-BmPtmFke.js";
|
|
3
1
|
import { a as impactRank, n as IMPACT_LEVELS } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { c as nullable, h as string, l as number, m as safeParse, p as record, s as isoDateTime, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
|
|
3
|
+
import { u as STATEMENT_LOCALES } from "./load-yAR4wzez.js";
|
|
4
|
+
import { n as isDirectory } from "./fs-BmPtmFke.js";
|
|
4
5
|
import { i as escapeText, o as standardsReference, r as escapeAttribute } from "./text-CKKpzkYM.js";
|
|
5
6
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
6
7
|
import { readFile, readdir } from "node:fs/promises";
|
|
@@ -58,7 +59,7 @@ function summariseAuditReport(value, source = "audit report") {
|
|
|
58
59
|
const result = safeParse(reportSchema, value);
|
|
59
60
|
if (!result.success) throw new StatementError(`${source} is not an eaa-kit JSON report (${result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5).join("; ")})`);
|
|
60
61
|
const report = result.data;
|
|
61
|
-
if (report.schemaVersion !==
|
|
62
|
+
if (report.schemaVersion !== 2) throw new StatementError(`${source} has schemaVersion ${report.schemaVersion}; this version of eaa-kit reads 2`);
|
|
62
63
|
const byRule = /* @__PURE__ */ new Map();
|
|
63
64
|
const rules = new Map(Object.entries(report.rules));
|
|
64
65
|
for (const page of report.pages) for (const violation of page.violations) {
|
|
@@ -411,7 +412,7 @@ async function renderStatement(config, options = {}) {
|
|
|
411
412
|
const country = options.country ?? config.enforcement.country;
|
|
412
413
|
const locale = options.locale ?? defaultLocale(config);
|
|
413
414
|
const template = `${country.toLowerCase()}.${locale}`;
|
|
414
|
-
const markdown = tidy(renderTemplate(await loadTemplate(
|
|
415
|
+
const markdown = tidy(renderTemplate(await loadTemplate(country, locale), buildScope(config, locale, options.audit)));
|
|
415
416
|
return {
|
|
416
417
|
markdown,
|
|
417
418
|
html: toHtmlDocument(markdown, {
|
|
@@ -423,9 +424,16 @@ async function renderStatement(config, options = {}) {
|
|
|
423
424
|
template
|
|
424
425
|
};
|
|
425
426
|
}
|
|
426
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* A site gets its statement in its own language where there is one for it.
|
|
429
|
+
*
|
|
430
|
+
* From `site.locale`, which is a BCP 47 tag: `de-AT` and `de` both mean the
|
|
431
|
+
* German document. English is the fallback because every country has an English
|
|
432
|
+
* template, being the language a statement is most often also published in.
|
|
433
|
+
*/
|
|
427
434
|
function defaultLocale(config) {
|
|
428
|
-
|
|
435
|
+
const language = config.site.locale.toLowerCase().split("-")[0];
|
|
436
|
+
return STATEMENT_LOCALES.find((candidate) => candidate === language) ?? "en";
|
|
429
437
|
}
|
|
430
438
|
/**
|
|
431
439
|
* Values the templates interpolate.
|
|
@@ -522,6 +530,22 @@ function reasonScope(reason) {
|
|
|
522
530
|
};
|
|
523
531
|
}
|
|
524
532
|
/**
|
|
533
|
+
* Where each statement language formats its dates.
|
|
534
|
+
*
|
|
535
|
+
* A region is named for every one, because a bare language tag leaves the
|
|
536
|
+
* format to whatever ICU picks: `de` is de-DE, and this tool's German documents
|
|
537
|
+
* have always been dated the Austrian way. `en-GB` for the same reason —
|
|
538
|
+
* 20 August 2026, not August 20, 2026, in a European legal document.
|
|
539
|
+
*/
|
|
540
|
+
const DATE_LOCALES = {
|
|
541
|
+
de: "de-AT",
|
|
542
|
+
en: "en-GB",
|
|
543
|
+
es: "es-ES",
|
|
544
|
+
fr: "fr-FR",
|
|
545
|
+
it: "it-IT",
|
|
546
|
+
nl: "nl-NL"
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
525
549
|
* 2026-08-20 becomes 20. August 2026 or 20 August 2026.
|
|
526
550
|
*
|
|
527
551
|
* Every date reaching this has been through a schema that checks it, so the
|
|
@@ -533,7 +557,7 @@ function reasonScope(reason) {
|
|
|
533
557
|
function formatDate(iso, locale) {
|
|
534
558
|
const date = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
535
559
|
if (Number.isNaN(date.getTime())) return iso;
|
|
536
|
-
return new Intl.DateTimeFormat(locale
|
|
560
|
+
return new Intl.DateTimeFormat(DATE_LOCALES[locale], {
|
|
537
561
|
day: "numeric",
|
|
538
562
|
month: "long",
|
|
539
563
|
year: "numeric",
|
|
@@ -549,14 +573,28 @@ function tidy(markdown) {
|
|
|
549
573
|
return `${markdown.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
550
574
|
}
|
|
551
575
|
let templateDirectory;
|
|
552
|
-
|
|
576
|
+
/**
|
|
577
|
+
* The document for a country in a language, if there is one.
|
|
578
|
+
*
|
|
579
|
+
* The matrix is deliberately sparse: a country's statement is written under its
|
|
580
|
+
* own law and published in the language the law is administered in, plus
|
|
581
|
+
* English. Asking for a combination nobody wrote is an error naming the
|
|
582
|
+
* languages that country does have — not a fall back to another language, which
|
|
583
|
+
* would hand somebody a document in a language their readers may not have and
|
|
584
|
+
* do it quietly.
|
|
585
|
+
*/
|
|
586
|
+
async function loadTemplate(country, locale) {
|
|
553
587
|
templateDirectory ??= await findTemplateDirectory();
|
|
554
588
|
const directory = templateDirectory;
|
|
589
|
+
const name = `${country.toLowerCase()}.${locale}`;
|
|
555
590
|
const file = path.join(directory, `${name}.md`);
|
|
556
591
|
try {
|
|
557
592
|
return await readFile(file, "utf8");
|
|
558
593
|
} catch {
|
|
559
|
-
|
|
594
|
+
const templates = (await readdir(directory)).filter((entry) => entry.endsWith(".md")).map((entry) => entry.replace(/\.md$/, "")).sort();
|
|
595
|
+
const prefix = `${country.toLowerCase()}.`;
|
|
596
|
+
const forCountry = templates.filter((entry) => entry.startsWith(prefix)).map((entry) => entry.slice(prefix.length));
|
|
597
|
+
throw new StatementError(forCountry.length > 0 ? `No ${country} statement in ${locale}. ${country} has: ${forCountry.join(", ")}` : `No statement template for ${name}. Available: ${templates.join(", ")}`);
|
|
560
598
|
}
|
|
561
599
|
}
|
|
562
600
|
/**
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
import axe from "axe-core";
|
|
3
3
|
//#region src/audit/result.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-page ceiling for an audit, in milliseconds.
|
|
6
|
+
*
|
|
7
|
+
* Lives here rather than in the jsdom runner because the worker pool needs it
|
|
8
|
+
* to set its own deadline, and the pool deliberately does not import jsdom —
|
|
9
|
+
* loading 630 ms of dependency to supervise threads that each load their own
|
|
10
|
+
* would be pure overhead. The same reason ENGINE_BLIND_RULES sits here.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_PAGE_TIMEOUT_MS = 3e4;
|
|
4
13
|
/** WCAG 2.2 AA and everything it builds on. Best-practice rules stay off. */
|
|
5
14
|
const DEFAULT_TAGS = [
|
|
6
15
|
"wcag2a",
|
|
@@ -122,17 +131,42 @@ function ruleOutcomes(audit) {
|
|
|
122
131
|
...audit.inapplicable
|
|
123
132
|
];
|
|
124
133
|
}
|
|
125
|
-
function runOptions(tags) {
|
|
134
|
+
function runOptions(tags, options = {}) {
|
|
126
135
|
return {
|
|
127
136
|
runOnly: {
|
|
128
137
|
type: "tag",
|
|
129
138
|
values: [...tags]
|
|
130
139
|
},
|
|
131
140
|
resultTypes: ["violations", "incomplete"],
|
|
132
|
-
preload: false
|
|
141
|
+
preload: false,
|
|
142
|
+
...options.skipBlindRules ? { rules: disabledBlindRules() } : {}
|
|
133
143
|
};
|
|
134
144
|
}
|
|
135
145
|
/**
|
|
146
|
+
* The rules this engine cannot decide, switched off rather than run.
|
|
147
|
+
*
|
|
148
|
+
* Normally they are run and their verdict discarded: axe-core computes colour
|
|
149
|
+
* contrast against a stylesheet jsdom never fetched, and `shapeResults` throws
|
|
150
|
+
* the answer away as untrustworthy. The work is wasted either way, and it is
|
|
151
|
+
* not a small amount — measured at 14-19% of a page, since colour contrast is
|
|
152
|
+
* the most expensive rule axe-core has.
|
|
153
|
+
*
|
|
154
|
+
* What is lost is the element list. A rule that runs reports *which* elements
|
|
155
|
+
* it could not decide, and those are the elements a person has to check by
|
|
156
|
+
* hand; a rule that never runs cannot name them. That is the whole trade, and
|
|
157
|
+
* it is why this is behind a flag rather than the default.
|
|
158
|
+
*
|
|
159
|
+
* What does not change is the verdict. `shapeResults` already has a pass for
|
|
160
|
+
* rules axe-core skipped entirely — the preload-dependent ones arrive that way
|
|
161
|
+
* — and files them as unevaluated with the same reason. A criterion this run
|
|
162
|
+
* could not reach still reads as unreached.
|
|
163
|
+
*/
|
|
164
|
+
function disabledBlindRules() {
|
|
165
|
+
const rules = {};
|
|
166
|
+
for (const ruleId of Object.keys(ENGINE_BLIND_RULES)) rules[ruleId] = { enabled: false };
|
|
167
|
+
return rules;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
136
170
|
* Turn raw axe-core output into a PageAudit, shared by both engines so their
|
|
137
171
|
* reports are directly comparable.
|
|
138
172
|
*
|
|
@@ -279,4 +313,4 @@ function enClauses(tags) {
|
|
|
279
313
|
return [...clauses].sort();
|
|
280
314
|
}
|
|
281
315
|
//#endregion
|
|
282
|
-
export {
|
|
316
|
+
export { failedPage as a, ruleOutcomes as c, successCriteria as d, blindRulesInScope as i, runOptions as l, DEFAULT_TAGS as n, findingElements as o, ENGINE_BLIND_RULES as r, pageUrl as s, DEFAULT_PAGE_TIMEOUT_MS as t, shapeResults as u };
|
|
@@ -35,7 +35,7 @@ async function auditBuild(directory, options, logger) {
|
|
|
35
35
|
logger.info("skipped (enabled: false)");
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
const { runAuditCommand } = await import("./audit-
|
|
38
|
+
const { runAuditCommand } = await import("./audit-DyHPmGeD.js");
|
|
39
39
|
const { exitCode } = await runAuditCommand(directory, auditOptions);
|
|
40
40
|
if (exitCode === 0) {
|
|
41
41
|
logger.info("no violations at or above the threshold");
|
|
@@ -20,6 +20,13 @@ interface IntegrationOptions {
|
|
|
20
20
|
baseUrl?: string;
|
|
21
21
|
/** Audit in real Chromium. Needs the playwright peer. */
|
|
22
22
|
browser?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Skip the rules the browserless engine cannot decide rather than running
|
|
25
|
+
* them and discarding the answer. The verdict does not move — those rules are
|
|
26
|
+
* still reported as not evaluated — so what a build gives up is the list of
|
|
27
|
+
* elements a person would check by hand. No effect under `browser`.
|
|
28
|
+
*/
|
|
29
|
+
fast?: boolean;
|
|
23
30
|
concurrency?: number;
|
|
24
31
|
baseline?: string;
|
|
25
32
|
format?: OutputFormat;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { o as isImpactLevel } from "./impact-DZt2oBCP.js";
|
|
2
2
|
import { o as standardsReference } from "./text-CKKpzkYM.js";
|
|
3
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
-
import { t as elementFingerprint } from "./fingerprint-
|
|
5
|
-
import {
|
|
4
|
+
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
5
|
+
import { c as ruleOutcomes, o as findingElements } from "./result-BWcYXeRs.js";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
//#region src/audit/report/sarif.ts
|
|
8
8
|
const SARIF_VERSION = "2.1.0";
|
|
@@ -111,7 +111,7 @@ function toResults(finding, ruleIndex, uri, suppressions) {
|
|
|
111
111
|
* combines the fingerprint with the location itself.
|
|
112
112
|
*/
|
|
113
113
|
function fingerprint(ruleId, selector, html) {
|
|
114
|
-
return { "eaaKit/
|
|
114
|
+
return { "eaaKit/v2": elementFingerprint(ruleId, selector, html) };
|
|
115
115
|
}
|
|
116
116
|
/** Every rule the run knows about, so the catalogue is complete in GitHub. */
|
|
117
117
|
function buildRules(audits) {
|
|
@@ -70,6 +70,20 @@ function boolean() {
|
|
|
70
70
|
function number() {
|
|
71
71
|
return { read: (value, path, issues) => typeof value === "number" && Number.isFinite(value) ? value : fail(issues, path, "expected a number") };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A whole number, with an optional floor.
|
|
75
|
+
*
|
|
76
|
+
* `number` would accept 2.5 pages and a concurrency of 0, both of which the
|
|
77
|
+
* CLI's own parsers refuse. A config file is the same instruction typed
|
|
78
|
+
* somewhere else, so it is held to the same rule.
|
|
79
|
+
*/
|
|
80
|
+
function integer(options = {}) {
|
|
81
|
+
return { read: (value, path, issues) => {
|
|
82
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return fail(issues, path, "expected a whole number");
|
|
83
|
+
if (options.min !== void 0 && value < options.min) return fail(issues, path, `must be ${options.min} or more`);
|
|
84
|
+
return value;
|
|
85
|
+
} };
|
|
86
|
+
}
|
|
73
87
|
/** One of a fixed set. The message lists them, since that is the useful part. */
|
|
74
88
|
function enumeration(values) {
|
|
75
89
|
return { read: (value, path, issues) => typeof value === "string" && values.includes(value) ? value : fail(issues, path, `expected one of ${values.join(", ")}`) };
|
|
@@ -192,4 +206,4 @@ function email() {
|
|
|
192
206
|
} };
|
|
193
207
|
}
|
|
194
208
|
//#endregion
|
|
195
|
-
export {
|
|
209
|
+
export { union as _, integer as a, nullable as c, optional as d, pipe as f, transform as g, string as h, enumeration as i, number as l, safeParse as m, boolean as n, isoDate as o, record as p, email as r, isoDateTime as s, array as t, object as u, url as v, withDefault as y };
|