eaa-kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-DK2StT1_.js +2 -0
- package/dist/{audit-B282A3EA.js → audit-e_uixhPG.js} +95 -48
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-CuAS2lMk.js → baseline-D2qWmUC6.js} +36 -9
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +111 -14
- package/dist/{command-C3D7JWn6.js → command-CZKp9Wi_.js} +101 -10
- package/dist/{remediation-CMBIrnpN.js → completeness-Dwq3CPB3.js} +60 -42
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/coverage-7bQqWNfN.js +2 -0
- package/dist/{coverage-B_Y6l-Ra.js → coverage-BD2_CYns.js} +44 -7
- package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/{html-C4vktg0A.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +3 -3
- package/dist/{init-CW7LfGT5.js → init-BjKYttxc.js} +3 -3
- package/dist/{jsdom-B--cEH-G.js → jsdom-CQar6OQS.js} +1 -1
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-D_Mnnft5.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DROX33kh.js → json-PNg6BeXp.js} +11 -16
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-yAR4wzez.js → load-vZdV_fD1.js} +5 -1
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BojtYVUa.js → playwright-C-miJP2b.js} +3 -2
- package/dist/{pool-BO25OIez.js → pool-dkexq5xo.js} +2 -2
- package/dist/{render-DbGOVmhx.js → render-CtsqR0k-.js} +105 -4
- package/dist/{result-BWcYXeRs.js → result-DfUru6y_.js} +24 -1
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-C2nKFcb-.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-CtcEUhbe.d.ts → run-YiqyQA2b.d.ts} +10 -0
- package/dist/{sarif-B-UBcVu8.js → sarif-BKvq_AUm.js} +23 -12
- package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +1 -1
- package/dist/webpack/index.js +1 -1
- package/dist/worksheet-eUjwYLxC.js +63 -0
- package/package.json +2 -2
- package/dist/audit-DyHPmGeD.js +0 -2
- package/dist/baseline-0u4df12v.js +0 -2
- package/dist/jsdom-22Bkt65v.js +0 -3
- package/dist/load-CFq2VQtT.js +0 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { c as meetsThreshold, n as IMPACT_LEVELS, o as impactRank, s as isImpactLevel, t as DEFAULT_FAIL_ON } from "../impact-luGCnN8F.js";
|
|
3
3
|
import { c as nullable, d as optional, h as string, l as number, m as safeParse, n as boolean, p as record, t as array, u as object, y as withDefault } from "../schema-DJSF4K05.js";
|
|
4
|
-
import { c as ConfigError, i as loadConfig, s as COUNTRIES, u as STATEMENT_LOCALES } from "../load-
|
|
5
|
-
import { i as readAuditReport, o as
|
|
6
|
-
import { n as count, t as collapse } from "../text-
|
|
4
|
+
import { c as ConfigError, i as loadConfig, s as COUNTRIES, u as STATEMENT_LOCALES } from "../load-vZdV_fD1.js";
|
|
5
|
+
import { c as StatementError, i as readAuditReport, o as checkStatementEvidence, s as refuses, t as renderStatement } from "../render-CtsqR0k-.js";
|
|
6
|
+
import { n as count, t as collapse } from "../text-BF1LHMiV.js";
|
|
7
7
|
import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
|
|
8
8
|
import { t as elementFingerprint } from "../fingerprint-BjYV_0F7.js";
|
|
9
|
-
import { i as buildBaseline, n as DEFAULT_BASELINE_FILE,
|
|
10
|
-
import { n as
|
|
11
|
-
import {
|
|
9
|
+
import { a as pruneBaseline, c as writeBaseline, i as buildBaseline, n as DEFAULT_BASELINE_FILE, o as readBaseline, r as applyBaseline, t as BaselineError } from "../baseline-D2qWmUC6.js";
|
|
10
|
+
import { a as emitDocument, c as runEngine, d as parseHeader, i as baselineInvocation, l as warn, n as auditDefaults, o as fail, r as auditInvocation, s as note, t as advise, u as basicAuth } from "../command-CZKp9Wi_.js";
|
|
11
|
+
import { t as DEFAULT_REVIEW_FILE } from "../review-CdMK2GpQ.js";
|
|
12
|
+
import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-e_uixhPG.js";
|
|
12
13
|
import { enableCompileCache } from "node:module";
|
|
13
14
|
import { readFile } from "node:fs/promises";
|
|
14
15
|
import path from "node:path";
|
|
@@ -42,6 +43,7 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
42
43
|
const baseUrl = options.baseUrl ?? origin;
|
|
43
44
|
const audits = await runEngine(pages, {
|
|
44
45
|
cwd,
|
|
46
|
+
...options.headers === void 0 ? {} : { headers: options.headers },
|
|
45
47
|
...baseUrl === void 0 ? {} : { baseUrl },
|
|
46
48
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
47
49
|
...options.browser ? { browser: true } : {},
|
|
@@ -60,11 +62,12 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
60
62
|
exitCode: 2
|
|
61
63
|
};
|
|
62
64
|
}
|
|
65
|
+
const target = options.output ?? "eaa-baseline.json";
|
|
66
|
+
if (options.prune) return await prune(audits, target, cwd);
|
|
63
67
|
const baseline = buildBaseline(audits, {
|
|
64
68
|
...options.note ? { note: options.note } : {},
|
|
65
69
|
...options.expiresOn ? { expiresOn: options.expiresOn } : {}
|
|
66
70
|
});
|
|
67
|
-
const target = options.output ?? "eaa-baseline.json";
|
|
68
71
|
try {
|
|
69
72
|
await writeBaseline(target, baseline, cwd);
|
|
70
73
|
} catch (cause) {
|
|
@@ -78,13 +81,60 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
78
81
|
throw cause;
|
|
79
82
|
}
|
|
80
83
|
const entries = baseline.entries.length;
|
|
81
|
-
note(`Wrote ${entries
|
|
84
|
+
note(`Wrote ${count(entries, "entry")} to ${target}`);
|
|
82
85
|
if (entries > 0) advise("These are barriers, not exceptions. Commit the file, then work the list down.");
|
|
83
86
|
return {
|
|
84
87
|
entries,
|
|
85
88
|
exitCode: 0
|
|
86
89
|
};
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* `--prune`: take out what this run shows is gone, and add nothing.
|
|
93
|
+
*
|
|
94
|
+
* The audit already tells anybody running it that entries no longer match and
|
|
95
|
+
* can be removed. Acting on that advice meant editing JSON by hand, entry by
|
|
96
|
+
* entry, against a file whose whole purpose is that nobody has to remember what
|
|
97
|
+
* is in it — so most people did not, and a baseline accumulated barriers that
|
|
98
|
+
* were fixed years ago while looking exactly like one that had not been read.
|
|
99
|
+
*
|
|
100
|
+
* Recording a new baseline is not the same operation and is not a substitute:
|
|
101
|
+
* it accepts whatever the site fails today, which on a bad day quietly adopts a
|
|
102
|
+
* barrier nobody agreed to.
|
|
103
|
+
*/
|
|
104
|
+
async function prune(audits, target, cwd) {
|
|
105
|
+
let existing;
|
|
106
|
+
try {
|
|
107
|
+
existing = await readBaseline(target, cwd);
|
|
108
|
+
} catch (cause) {
|
|
109
|
+
if (cause instanceof BaselineError) {
|
|
110
|
+
fail(cause.message);
|
|
111
|
+
return {
|
|
112
|
+
entries: 0,
|
|
113
|
+
exitCode: 2
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw cause;
|
|
117
|
+
}
|
|
118
|
+
const outcome = applyBaseline(audits, existing);
|
|
119
|
+
const { baseline, removed } = pruneBaseline(existing, outcome.stale);
|
|
120
|
+
if (removed.length === 0) {
|
|
121
|
+
note(`Nothing to remove from ${target}: every entry still matches something on this run`);
|
|
122
|
+
return {
|
|
123
|
+
entries: existing.entries.length,
|
|
124
|
+
removed: 0,
|
|
125
|
+
exitCode: 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
await writeBaseline(target, baseline, cwd);
|
|
129
|
+
note(`Removed ${count(removed.length, "entry")} from ${target}; ${count(baseline.entries.length, "entry")} left`);
|
|
130
|
+
for (const entry of removed) note(` ${entry.page} ${entry.ruleId}`);
|
|
131
|
+
if (outcome.expired.length > 0) advise(`${count(outcome.expired.length, "entry")} have expired and suppress nothing; --prune leaves them for you to decide about`);
|
|
132
|
+
return {
|
|
133
|
+
entries: baseline.entries.length,
|
|
134
|
+
removed: removed.length,
|
|
135
|
+
exitCode: 0
|
|
136
|
+
};
|
|
137
|
+
}
|
|
88
138
|
var DiffError = class extends Error {
|
|
89
139
|
name = "DiffError";
|
|
90
140
|
};
|
|
@@ -353,6 +403,23 @@ async function runStatementCommand(options = {}) {
|
|
|
353
403
|
});
|
|
354
404
|
let audit;
|
|
355
405
|
if (options.audit) audit = await readAuditReport(options.audit, options.cwd ?? process.cwd());
|
|
406
|
+
let review;
|
|
407
|
+
if (options.review) {
|
|
408
|
+
const { readReview } = await import("../review-DIbOTmOM.js");
|
|
409
|
+
review = await readReview(options.review, options.cwd ?? process.cwd());
|
|
410
|
+
}
|
|
411
|
+
const problems = checkStatementEvidence({
|
|
412
|
+
config,
|
|
413
|
+
...audit ? { audit } : {},
|
|
414
|
+
...review ? { review } : {}
|
|
415
|
+
});
|
|
416
|
+
for (const problem of problems) if (problem.severity === "refuses") fail(problem.message);
|
|
417
|
+
else advise(problem.message);
|
|
418
|
+
if (refuses(problems)) return {
|
|
419
|
+
document: "",
|
|
420
|
+
format,
|
|
421
|
+
exitCode: 2
|
|
422
|
+
};
|
|
356
423
|
const statement = await renderStatement(config, {
|
|
357
424
|
...options.locale ? { locale: options.locale } : {},
|
|
358
425
|
...options.country ? { country: options.country } : {},
|
|
@@ -374,7 +441,8 @@ async function runStatementCommand(options = {}) {
|
|
|
374
441
|
exitCode: 0
|
|
375
442
|
};
|
|
376
443
|
} catch (cause) {
|
|
377
|
-
|
|
444
|
+
const { ReviewError } = await import("../review-DIbOTmOM.js");
|
|
445
|
+
if (cause instanceof ConfigError || cause instanceof StatementError || cause instanceof ReviewError) {
|
|
378
446
|
fail(cause.message);
|
|
379
447
|
if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
|
|
380
448
|
return {
|
|
@@ -453,31 +521,60 @@ const parseFormat = oneOf(OUTPUT_FORMATS);
|
|
|
453
521
|
const parsePositive = wholeNumber(1);
|
|
454
522
|
const parseDepth = wholeNumber(0);
|
|
455
523
|
const parseConcurrency = wholeNumber(1);
|
|
524
|
+
/**
|
|
525
|
+
* `--header` repeated, collected into a list the command turns into a record.
|
|
526
|
+
*
|
|
527
|
+
* Validated here so a typo stops the run before a site is crawled with a
|
|
528
|
+
* malformed credential — and the error names the header rather than repeating
|
|
529
|
+
* the value, because a bad `--header` is exactly the case where the value is a
|
|
530
|
+
* token and the terminal is a CI log.
|
|
531
|
+
*/
|
|
532
|
+
function collectHeader(value, previous = []) {
|
|
533
|
+
try {
|
|
534
|
+
parseHeader(value);
|
|
535
|
+
} catch (cause) {
|
|
536
|
+
throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
|
|
537
|
+
}
|
|
538
|
+
return [...previous, value];
|
|
539
|
+
}
|
|
540
|
+
function parseBasicAuth(value) {
|
|
541
|
+
try {
|
|
542
|
+
basicAuth(value);
|
|
543
|
+
} catch (cause) {
|
|
544
|
+
throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
|
|
545
|
+
}
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
456
548
|
const program = new Command();
|
|
457
549
|
program.exitOverride();
|
|
458
550
|
program.name("eaa-kit").description("WCAG 2.2 AA auditor and EU accessibility statement generator for static sites.\nNot legal advice.").version(TOOL_VERSION, "-v, --version");
|
|
459
|
-
program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")}; default: ${DEFAULT_FAIL_ON})`, parseImpact).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")}; default: console)`, parseFormat).option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--fast", "skip the rules the browserless engine cannot decide anyway, losing their element lists").option("--concurrency <n>", "pages to audit at once, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
|
|
551
|
+
program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")}; default: ${DEFAULT_FAIL_ON})`, parseImpact).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")}; default: console)`, parseFormat).option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--fast", "skip the rules the browserless engine cannot decide anyway, losing their element lists").option("--concurrency <n>", "pages to audit at once, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").option("--review <path>", "what a person checked, from eaa-kit checklist").option("--review-max-age <days>", "stop counting review entries older than this", parseDepth).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
|
|
460
552
|
const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
|
|
461
553
|
const invocation = auditInvocation(dir, defaults, flags);
|
|
462
554
|
const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
|
|
463
555
|
process.exitCode = exitCode;
|
|
464
556
|
});
|
|
465
|
-
program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site (default: ./dist)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "pages to audit at once, or 1 for none", parseConcurrency).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
|
|
557
|
+
program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site (default: ./dist)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--prune", "remove the entries this run shows are gone, instead of recording a new baseline").option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "pages to audit at once, or 1 for none", parseConcurrency).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
|
|
466
558
|
const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
|
|
467
559
|
const invocation = baselineInvocation(dir, defaults, flags);
|
|
468
560
|
const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
|
|
469
561
|
process.exitCode = exitCode;
|
|
470
562
|
});
|
|
563
|
+
program.command("checklist").description("Write the manual review: the 34 WCAG criteria no automated rule can reach").option("--record <path>", `where the answers live (default: ${DEFAULT_REVIEW_FILE})`).option("--output <path>", "write the worksheet here instead of stdout").option("--reviewed-by <name>", "who is carrying out the review").action(async (flags) => {
|
|
564
|
+
const { runChecklistCommand } = await import("../checklist-BtgkxMEa.js");
|
|
565
|
+
const { exitCode } = await runChecklistCommand(flags);
|
|
566
|
+
process.exitCode = exitCode;
|
|
567
|
+
});
|
|
471
568
|
program.command("diff").description("Compare two JSON reports: what a change made worse, and what it fixed").argument("<before>", "JSON report from before the change").argument("<after>", "JSON report from after it").option("--format <format>", `output format (${DIFF_FORMATS.join("|")})`, oneOf(DIFF_FORMATS)).option("--output <path>", "write the diff here instead of stdout").option("--fail-on <impact>", `exit 1 on NEW violations at or above this impact (${IMPACT_LEVELS.join("|")})`, parseImpact, DEFAULT_FAIL_ON).action(async (before, after, flags) => {
|
|
472
569
|
const { exitCode } = await runDiffCommand(before, after, flags);
|
|
473
570
|
process.exitCode = exitCode;
|
|
474
571
|
});
|
|
475
572
|
program.command("init").description("Write an eaa.config.json to generate statements from").option("--output <path>", "write here instead of eaa.config.json").option("--force", "overwrite a config that is already there").option("-y, --yes", "take every default without asking").action(async (flags) => {
|
|
476
|
-
const { runInitCommand } = await import("../init-
|
|
573
|
+
const { runInitCommand } = await import("../init-BjKYttxc.js");
|
|
477
574
|
const { exitCode } = await runInitCommand(flags);
|
|
478
575
|
process.exitCode = exitCode;
|
|
479
576
|
});
|
|
480
|
-
program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
|
|
577
|
+
program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--review <path>", "check the conformance claim against a review record; nothing from it is published").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
|
|
481
578
|
const { lang, ...options } = flags;
|
|
482
579
|
const { exitCode } = await runStatementCommand({
|
|
483
580
|
...options,
|
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
+
//#region src/audit/headers.ts
|
|
5
|
+
/**
|
|
6
|
+
* Request headers for the pages this tool fetches.
|
|
7
|
+
*
|
|
8
|
+
* The two places a small agency stages work are the two this tool could not
|
|
9
|
+
* reach: a preview deployment, which every host protects by default, and a CMS
|
|
10
|
+
* staging site behind basic auth or a session cookie. `--url` sent one fixed
|
|
11
|
+
* pair of headers and had no way to add to it, so auditing either meant putting
|
|
12
|
+
* the site on the public internet first.
|
|
13
|
+
*
|
|
14
|
+
* These values are credentials. They are handled on the principle that the tool
|
|
15
|
+
* never writes one down: nothing here reaches a report, a baseline, a SARIF log
|
|
16
|
+
* or the completeness record, and the errors below name the header rather than
|
|
17
|
+
* echoing what was typed, because a malformed `--header` is exactly the case
|
|
18
|
+
* where the value is a token and the terminal is a CI log.
|
|
19
|
+
*
|
|
20
|
+
* For the same reason there is no `headers` key in `eaa.config`. That file is
|
|
21
|
+
* committed; a token in it is a token in the repository, and a tool that
|
|
22
|
+
* offered the field would be inviting that. Pass them as flags — a shell
|
|
23
|
+
* expands `--header "Authorization: Bearer $TOKEN"` — or as the GitHub Action's
|
|
24
|
+
* input, which reads from `secrets`.
|
|
25
|
+
*/
|
|
26
|
+
/** Thrown for a header this cannot parse. Never carries the value. */
|
|
27
|
+
var HeaderError = class extends Error {
|
|
28
|
+
name = "HeaderError";
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `Name: value` into a pair.
|
|
32
|
+
*
|
|
33
|
+
* Lenient about the space after the colon and about a value containing colons,
|
|
34
|
+
* which `Authorization: Basic …` and a `Cookie` both do. Strict about the name,
|
|
35
|
+
* because a header name is a token by RFC 9110 and anything else is a typo that
|
|
36
|
+
* a server would reject in a way nobody could read.
|
|
37
|
+
*/
|
|
38
|
+
function parseHeader(input) {
|
|
39
|
+
const at = input.indexOf(":");
|
|
40
|
+
if (at <= 0) throw new HeaderError("a header must be written \"Name: value\"");
|
|
41
|
+
const name = input.slice(0, at).trim();
|
|
42
|
+
const value = input.slice(at + 1).trim();
|
|
43
|
+
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) throw new HeaderError(`"${name}" is not a valid header name`);
|
|
44
|
+
if (value === "") throw new HeaderError(`the ${name} header was given no value`);
|
|
45
|
+
if (/[\r\n]/.test(value)) throw new HeaderError(`the ${name} header contains a line break`);
|
|
46
|
+
return [name, value];
|
|
47
|
+
}
|
|
48
|
+
/** Several `Name: value` strings into the record the fetchers take. */
|
|
49
|
+
function parseHeaders(inputs) {
|
|
50
|
+
const headers = {};
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const [name, value] = parseHeader(input);
|
|
53
|
+
headers[name] = value;
|
|
54
|
+
}
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `user:password` into an `Authorization` header.
|
|
59
|
+
*
|
|
60
|
+
* Sugar for the header somebody would otherwise have to base64 by hand, which
|
|
61
|
+
* is the one credential a staging site is most likely to be behind. A password
|
|
62
|
+
* may contain colons; a username may not, which is basic auth's own rule.
|
|
63
|
+
*/
|
|
64
|
+
function basicAuth(input) {
|
|
65
|
+
if (input.indexOf(":") <= 0) throw new HeaderError("basic auth must be written \"user:password\"");
|
|
66
|
+
return ["Authorization", `Basic ${Buffer.from(input, "utf8").toString("base64")}`];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The headers a run will send, from whichever flags were given.
|
|
70
|
+
*
|
|
71
|
+
* `--basic-auth` first, so an explicit `--header "Authorization: …"` beats it:
|
|
72
|
+
* a typed header is the more specific thing to have asked for.
|
|
73
|
+
*/
|
|
74
|
+
function requestHeaders(options) {
|
|
75
|
+
const headers = {};
|
|
76
|
+
if (options.basicAuth !== void 0) {
|
|
77
|
+
const [name, value] = basicAuth(options.basicAuth);
|
|
78
|
+
headers[name] = value;
|
|
79
|
+
}
|
|
80
|
+
Object.assign(headers, parseHeaders(options.header ?? []));
|
|
81
|
+
return Object.keys(headers).length === 0 ? void 0 : headers;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
4
84
|
//#region src/cli/command.ts
|
|
5
85
|
/**
|
|
6
86
|
* What every command does around the audit itself: say what is happening, run
|
|
@@ -42,18 +122,19 @@ async function runEngine(pages, options) {
|
|
|
42
122
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
43
123
|
};
|
|
44
124
|
if (!options.browser) {
|
|
45
|
-
const { runPooledAudit } = await import("./pool-
|
|
125
|
+
const { runPooledAudit } = await import("./pool-dkexq5xo.js");
|
|
46
126
|
return runPooledAudit(pages, {
|
|
47
127
|
...runnerOptions,
|
|
48
128
|
...options.fast ? { fast: true } : {},
|
|
49
129
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
50
130
|
});
|
|
51
131
|
}
|
|
52
|
-
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-
|
|
132
|
+
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-C-miJP2b.js");
|
|
53
133
|
try {
|
|
54
134
|
return await runBrowserAudit(options.directory, pages, {
|
|
55
135
|
...runnerOptions,
|
|
56
|
-
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
136
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
137
|
+
...options.headers === void 0 ? {} : { headers: options.headers }
|
|
57
138
|
});
|
|
58
139
|
} catch (cause) {
|
|
59
140
|
if (cause instanceof BrowserUnavailableError) {
|
|
@@ -95,7 +176,7 @@ async function emitDocument(body, output, cwd) {
|
|
|
95
176
|
* on different settings than it names would be worse than stopping.
|
|
96
177
|
*/
|
|
97
178
|
async function auditDefaults(options = {}) {
|
|
98
|
-
const { loadAuditConfig } = await import("./load-
|
|
179
|
+
const { loadAuditConfig } = await import("./load-Bc_xDGAh.js");
|
|
99
180
|
const loaded = await loadAuditConfig({
|
|
100
181
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
101
182
|
...options.config ? { path: options.config } : {}
|
|
@@ -105,14 +186,19 @@ async function auditDefaults(options = {}) {
|
|
|
105
186
|
return loaded.audit;
|
|
106
187
|
}
|
|
107
188
|
function auditInvocation(dir, defaults, flags) {
|
|
108
|
-
const { build, config: _config, ...typed } = flags;
|
|
109
|
-
const { dir: configDir, build: configBuild, ...fromConfig } = defaults;
|
|
189
|
+
const { build, config: _config, header, basicAuth, ...typed } = flags;
|
|
190
|
+
const { dir: configDir, build: configBuild, headers: _fromFile, ...fromConfig } = defaults;
|
|
191
|
+
const headers = requestHeaders({
|
|
192
|
+
...header ? { header } : {},
|
|
193
|
+
...basicAuth ? { basicAuth } : {}
|
|
194
|
+
});
|
|
110
195
|
return {
|
|
111
196
|
dir: dir ?? configDir,
|
|
112
197
|
options: {
|
|
113
198
|
...fromConfig,
|
|
114
199
|
...typed,
|
|
115
|
-
...build === false || configBuild === false ? { noBuild: true } : {}
|
|
200
|
+
...build === false || configBuild === false ? { noBuild: true } : {},
|
|
201
|
+
...headers === void 0 ? {} : { headers }
|
|
116
202
|
}
|
|
117
203
|
};
|
|
118
204
|
}
|
|
@@ -126,12 +212,17 @@ function auditInvocation(dir, defaults, flags) {
|
|
|
126
212
|
* and `baseline` all describe a verdict this command does not reach.
|
|
127
213
|
*/
|
|
128
214
|
function baselineInvocation(dir, defaults, flags) {
|
|
129
|
-
const { config: _config, ...typed } = flags;
|
|
215
|
+
const { config: _config, header, basicAuth, ...typed } = flags;
|
|
216
|
+
const headers = requestHeaders({
|
|
217
|
+
...header ? { header } : {},
|
|
218
|
+
...basicAuth ? { basicAuth } : {}
|
|
219
|
+
});
|
|
130
220
|
return {
|
|
131
221
|
dir: dir ?? defaults.dir ?? "./dist",
|
|
132
222
|
options: {
|
|
133
223
|
...baselineDefaults(defaults),
|
|
134
|
-
...typed
|
|
224
|
+
...typed,
|
|
225
|
+
...headers === void 0 ? {} : { headers }
|
|
135
226
|
}
|
|
136
227
|
};
|
|
137
228
|
}
|
|
@@ -157,4 +248,4 @@ function pick(source, keys) {
|
|
|
157
248
|
return out;
|
|
158
249
|
}
|
|
159
250
|
//#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 };
|
|
251
|
+
export { emitDocument as a, runEngine as c, parseHeader as d, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t, basicAuth as u };
|
|
@@ -1,45 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as impactRank } from "./impact-luGCnN8F.js";
|
|
2
2
|
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
3
|
-
import { o as findingElements } from "./result-
|
|
4
|
-
//#region src/audit/completeness.ts
|
|
5
|
-
/**
|
|
6
|
-
* Fold what the collector knew together with what the engine managed.
|
|
7
|
-
*
|
|
8
|
-
* The audits carry the second half: a page with an `error` was collected and
|
|
9
|
-
* then not audited, and is as unmeasured as one that was never fetched.
|
|
10
|
-
*/
|
|
11
|
-
function runCompleteness(audits, collection) {
|
|
12
|
-
const errored = audits.filter((audit) => audit.error).length;
|
|
13
|
-
return {
|
|
14
|
-
...collection,
|
|
15
|
-
audited: audits.length - errored,
|
|
16
|
-
errored,
|
|
17
|
-
complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* What was missed, as phrases both reports print.
|
|
22
|
-
*
|
|
23
|
-
* Separate clauses rather than one total, for the same reason the coverage
|
|
24
|
-
* parts are: a page that could not be fetched and a page that could not be
|
|
25
|
-
* parsed are different problems with different fixes, and summing them would
|
|
26
|
-
* name neither.
|
|
27
|
-
*/
|
|
28
|
-
function missedParts(completeness) {
|
|
29
|
-
const parts = [];
|
|
30
|
-
if (completeness.unreachable.length > 0) parts.push(`${completeness.unreachable.length} could not be reached`);
|
|
31
|
-
if (completeness.errored > 0) parts.push(`${completeness.errored} could not be audited`);
|
|
32
|
-
if (completeness.truncated) parts.push("the run stopped at its page limit");
|
|
33
|
-
return parts;
|
|
34
|
-
}
|
|
35
|
-
/** How the pages were found, in words, for the run details both reports show. */
|
|
36
|
-
function discoveryLabel(discovery) {
|
|
37
|
-
switch (discovery) {
|
|
38
|
-
case "directory": return "files in the build directory";
|
|
39
|
-
case "sitemap": return "sitemap.xml and links";
|
|
40
|
-
case "links": return "links from the entry page";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
3
|
+
import { o as findingElements } from "./result-DfUru6y_.js";
|
|
43
4
|
/** Whether this element looks like one component rendered on many pages. */
|
|
44
5
|
function isShared(element) {
|
|
45
6
|
return element.pages.length >= 3;
|
|
@@ -95,6 +56,23 @@ function groupIssues(audits) {
|
|
|
95
56
|
return issues;
|
|
96
57
|
}
|
|
97
58
|
/**
|
|
59
|
+
* What the grouped issues add up to: distinct elements, and the violations they
|
|
60
|
+
* account for. Both reports open their issues section with these two numbers,
|
|
61
|
+
* and say something different when they are equal.
|
|
62
|
+
*/
|
|
63
|
+
function issueTotals(issues) {
|
|
64
|
+
let elements = 0;
|
|
65
|
+
let occurrences = 0;
|
|
66
|
+
for (const issue of issues) {
|
|
67
|
+
elements += issue.elements.length;
|
|
68
|
+
occurrences += issue.occurrences;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
elements,
|
|
72
|
+
occurrences
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
98
76
|
* Unevaluated rules folded across the site, sorted by rule id.
|
|
99
77
|
*
|
|
100
78
|
* Both reports list these once at the end rather than under every page: on a
|
|
@@ -318,4 +296,44 @@ function remediationFor(ruleId, framework) {
|
|
|
318
296
|
};
|
|
319
297
|
}
|
|
320
298
|
//#endregion
|
|
321
|
-
|
|
299
|
+
//#region src/audit/completeness.ts
|
|
300
|
+
/**
|
|
301
|
+
* Fold what the collector knew together with what the engine managed.
|
|
302
|
+
*
|
|
303
|
+
* The audits carry the second half: a page with an `error` was collected and
|
|
304
|
+
* then not audited, and is as unmeasured as one that was never fetched.
|
|
305
|
+
*/
|
|
306
|
+
function runCompleteness(audits, collection) {
|
|
307
|
+
const errored = audits.filter((audit) => audit.error).length;
|
|
308
|
+
return {
|
|
309
|
+
...collection,
|
|
310
|
+
audited: audits.length - errored,
|
|
311
|
+
errored,
|
|
312
|
+
complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* What was missed, as phrases both reports print.
|
|
317
|
+
*
|
|
318
|
+
* Separate clauses rather than one total, for the same reason the coverage
|
|
319
|
+
* parts are: a page that could not be fetched and a page that could not be
|
|
320
|
+
* parsed are different problems with different fixes, and summing them would
|
|
321
|
+
* name neither.
|
|
322
|
+
*/
|
|
323
|
+
function missedParts(completeness) {
|
|
324
|
+
const parts = [];
|
|
325
|
+
if (completeness.unreachable.length > 0) parts.push(`${completeness.unreachable.length} could not be reached`);
|
|
326
|
+
if (completeness.errored > 0) parts.push(`${completeness.errored} could not be audited`);
|
|
327
|
+
if (completeness.truncated) parts.push("the run stopped at its page limit");
|
|
328
|
+
return parts;
|
|
329
|
+
}
|
|
330
|
+
/** How the pages were found, in words, for the run details both reports show. */
|
|
331
|
+
function discoveryLabel(discovery) {
|
|
332
|
+
switch (discovery) {
|
|
333
|
+
case "directory": return "files in the build directory";
|
|
334
|
+
case "sitemap": return "sitemap.xml and links";
|
|
335
|
+
case "links": return "links from the entry page";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
export { blindRules as a, isShared as c, remediationFor as i, issueTotals as l, missedParts as n, coverageParts as o, runCompleteness as r, groupIssues as s, discoveryLabel as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as toPosix } from "./fs-BmPtmFke.js";
|
|
2
|
-
import { t as collapse } from "./text-
|
|
2
|
+
import { t as collapse } from "./text-BF1LHMiV.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as componentFor, t as buildComponentIndex } from "./component-
|
|
1
|
+
import { n as componentFor, t as buildComponentIndex } from "./component-ChisoFyY.js";
|
|
2
2
|
export { buildComponentIndex, componentFor };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-
|
|
1
|
+
import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-DfUru6y_.js";
|
|
2
|
+
import { s as criterionReview } from "./review-CdMK2GpQ.js";
|
|
2
3
|
import axe from "axe-core";
|
|
3
4
|
//#region src/audit/manual.ts
|
|
4
5
|
/**
|
|
@@ -463,7 +464,7 @@ function blindedRules(audits) {
|
|
|
463
464
|
* the criterion is met on this site, and saying otherwise would be a coverage
|
|
464
465
|
* claim resting on an empty page.
|
|
465
466
|
*/
|
|
466
|
-
function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
467
|
+
function buildCoverage(audits, tags = DEFAULT_TAGS, review) {
|
|
467
468
|
const byCriterion = rulesByCriterion(tags);
|
|
468
469
|
const decided = decidedRules(audits);
|
|
469
470
|
const blinded = blindedRules(audits);
|
|
@@ -496,15 +497,36 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
|
496
497
|
browserWouldAnswer: false
|
|
497
498
|
};
|
|
498
499
|
});
|
|
499
|
-
const
|
|
500
|
+
const reviewed = review === void 0 ? criteria : criteria.map(withReview(review));
|
|
501
|
+
const count = (status) => reviewed.filter((criterion) => criterion.status === status).length;
|
|
502
|
+
const counted = reviewed.filter((criterion) => criterion.review?.counts === true);
|
|
500
503
|
return {
|
|
501
|
-
criteria,
|
|
504
|
+
criteria: reviewed,
|
|
502
505
|
evaluated: count("evaluated"),
|
|
503
506
|
notEvaluated: count("not-evaluated"),
|
|
504
507
|
nothingToCheck: count("nothing-to-check"),
|
|
505
508
|
noAutomatedRule: count("no-automated-rule"),
|
|
506
|
-
browserWouldAnswer:
|
|
507
|
-
total: WCAG22_AA_CRITERIA.length
|
|
509
|
+
browserWouldAnswer: reviewed.filter((criterion) => criterion.browserWouldAnswer).length,
|
|
510
|
+
total: WCAG22_AA_CRITERIA.length,
|
|
511
|
+
reviewed: counted.length,
|
|
512
|
+
reviewedNotMet: counted.filter((criterion) => criterion.review?.result === "not-met").length,
|
|
513
|
+
reviewNotCounted: reviewed.filter((criterion) => criterion.review?.counts === false).length
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Attach what a person recorded, without letting it move what the engine found.
|
|
518
|
+
*
|
|
519
|
+
* `status` is untouched here, deliberately and in every branch: a review adds a
|
|
520
|
+
* second kind of claim beside the run's own, and the moment it could rewrite
|
|
521
|
+
* one the report would stop being a record of what was measured.
|
|
522
|
+
*/
|
|
523
|
+
function withReview(review) {
|
|
524
|
+
return (criterion) => {
|
|
525
|
+
const recorded = criterionReview(criterion.number, review, criterion.status === "evaluated");
|
|
526
|
+
return recorded === void 0 ? criterion : {
|
|
527
|
+
...criterion,
|
|
528
|
+
review: recorded
|
|
529
|
+
};
|
|
508
530
|
};
|
|
509
531
|
}
|
|
510
532
|
/**
|
|
@@ -518,5 +540,20 @@ function coverageSummary(coverage) {
|
|
|
518
540
|
const unautomatable = coverage.noAutomatedRule;
|
|
519
541
|
return `Of the ${coverage.total} WCAG 2.2 A and AA success criteria, ${unautomatable} cannot be checked by any automated engine and need a person. This run reached a verdict on ${coverage.evaluated}.`;
|
|
520
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* What a person added to the run, in one sentence, or nothing when nobody did.
|
|
545
|
+
*
|
|
546
|
+
* Kept out of `coverageSummary` so the sentence about the engine's reach does
|
|
547
|
+
* not change shape depending on whether a review was supplied — and because a
|
|
548
|
+
* reader has to be able to tell the two claims apart at a glance: one is what
|
|
549
|
+
* an engine measured, the other is what somebody says they checked.
|
|
550
|
+
*/
|
|
551
|
+
function reviewSummary(coverage) {
|
|
552
|
+
if (coverage.reviewed === 0 && coverage.reviewNotCounted === 0) return void 0;
|
|
553
|
+
const parts = [`A person recorded a result for ${coverage.reviewed} of the criteria this run did not reach`];
|
|
554
|
+
if (coverage.reviewedNotMet > 0) parts.push(`${coverage.reviewedNotMet} of them as not met`);
|
|
555
|
+
if (coverage.reviewNotCounted > 0) parts.push(`${coverage.reviewNotCounted} further entries were not counted`);
|
|
556
|
+
return `${parts.join("; ")}. This is a claim by a person, not a measurement.`;
|
|
557
|
+
}
|
|
521
558
|
//#endregion
|
|
522
|
-
export {
|
|
559
|
+
export { rulesByCriterion as a, reviewSummary as i, buildCoverage as n, manualCheckFor as o, coverageSummary as r, understandingUrl as s, WCAG22_AA_CRITERIA as t };
|