eaa-kit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +31 -9
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/{audit-CPoZMXGM.js → audit-B282A3EA.js} +43 -21
  6. package/dist/audit-DyHPmGeD.js +2 -0
  7. package/dist/{baseline-22Y1NWxM.js → baseline-0u4df12v.js} +1 -1
  8. package/dist/{baseline-DB9CZGnV.js → baseline-CuAS2lMk.js} +6 -5
  9. package/dist/cli/index.js +50 -19
  10. package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
  11. package/dist/command-C3D7JWn6.js +160 -0
  12. package/dist/{coverage-B4IBKMO8.js → coverage-B_Y6l-Ra.js} +3 -2
  13. package/dist/{crawl-BiI1Lau_.js → crawl-BUWPgaGW.js} +83 -9
  14. package/dist/eleventy/index.d.ts +1 -1
  15. package/dist/eleventy/index.js +1 -1
  16. package/dist/fingerprint-BjYV_0F7.js +67 -0
  17. package/dist/{html-BGTO3ypW.js → html-C4vktg0A.js} +3 -3
  18. package/dist/index.d.ts +119 -4
  19. package/dist/index.js +2 -2
  20. package/dist/{init-DIWDE35F.js → init-CW7LfGT5.js} +10 -4
  21. package/dist/jsdom-22Bkt65v.js +3 -0
  22. package/dist/{jsdom-DCpGSLfW.js → jsdom-B--cEH-G.js} +12 -4
  23. package/dist/{json-DjEvy1nX.js → json-DROX33kh.js} +4 -4
  24. package/dist/{json-QQuFIw1W.js → json-D_Mnnft5.js} +1 -1
  25. package/dist/load-CFq2VQtT.js +2 -0
  26. package/dist/{load-5wRGLvub.js → load-yAR4wzez.js} +133 -8
  27. package/dist/nuxt/index.d.ts +1 -1
  28. package/dist/nuxt/index.js +1 -1
  29. package/dist/{playwright-BWniOain.js → playwright-BojtYVUa.js} +30 -6
  30. package/dist/{pool-BMevaLWD.js → pool-BO25OIez.js} +52 -2
  31. package/dist/{remediation-Dtowi2EC.js → remediation-CMBIrnpN.js} +2 -2
  32. package/dist/{render-DrvXRCEn.js → render-DbGOVmhx.js} +47 -9
  33. package/dist/{result-DoamKFsp.js → result-BWcYXeRs.js} +37 -3
  34. package/dist/{run-DB34BSOZ.js → run-C2nKFcb-.js} +1 -1
  35. package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
  36. package/dist/{sarif-SR3_lLYd.js → sarif-B-UBcVu8.js} +3 -3
  37. package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
  38. package/dist/statement/templates/es.en.md +125 -0
  39. package/dist/statement/templates/es.es.md +127 -0
  40. package/dist/statement/templates/fr.en.md +128 -0
  41. package/dist/statement/templates/fr.fr.md +131 -0
  42. package/dist/statement/templates/it.en.md +127 -0
  43. package/dist/statement/templates/it.it.md +130 -0
  44. package/dist/statement/templates/nl.en.md +125 -0
  45. package/dist/statement/templates/nl.nl.md +127 -0
  46. package/dist/vite/index.d.ts +1 -1
  47. package/dist/vite/index.js +1 -1
  48. package/dist/webpack/index.d.ts +1 -1
  49. package/dist/webpack/index.js +1 -1
  50. package/package.json +6 -2
  51. package/dist/audit-CpXH2Mk8.js +0 -2
  52. package/dist/command-D8l_oYbV.js +0 -77
  53. package/dist/fingerprint-DRoneAjj.js +0 -20
  54. package/dist/jsdom-4IMzv0eE.js +0 -3
@@ -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 { n as ENGINE_BLIND_RULES, t as DEFAULT_TAGS, u as successCriteria } from "./result-DoamKFsp.js";
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 { i as stripBom } from "./collect-CFM8gEVv.js";
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: await response.text()
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, { redirect: "follow" });
155
- return response.ok ? await response.text() : void 0;
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) {
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
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 {
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
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/eleventy/index.ts
4
4
  /**
@@ -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-Dtowi2EC.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-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-B4IBKMO8.js";
7
- import { n as buildSummary } from "./json-DjEvy1nX.js";
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
- /** Languages a statement can be rendered in. */
66
- declare const STATEMENT_LOCALES: readonly ['de', 'en'];
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 COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-5wRGLvub.js";
2
- import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DrvXRCEn.js";
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 { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-5wRGLvub.js";
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 { i as note, o as warn, r as fail } from "./command-D8l_oYbV.js";
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");
@@ -0,0 +1,3 @@
1
+ import "./result-BWcYXeRs.js";
2
+ import { n as runJsdomAudit } from "./jsdom-B--cEH-G.js";
3
+ export { runJsdomAudit };
@@ -1,10 +1,18 @@
1
- import { c as runOptions, i as failedPage, l as shapeResults, o as pageUrl, r as blindRulesInScope, t as DEFAULT_TAGS } from "./result-DoamKFsp.js";
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
- /** Per-page ceiling; one pathological document must not stall a CI run. */
7
- const DEFAULT_TIMEOUT_MS = 3e4;
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,