eaa-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -11
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +5 -1
- package/dist/audit-DK2StT1_.js +2 -0
- package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +153 -25
- package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
- package/dist/command-CZKp9Wi_.js +251 -0
- package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/coverage-7bQqWNfN.js +2 -0
- package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
- package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/fingerprint-BjYV_0F7.js +67 -0
- package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +187 -5
- package/dist/index.js +3 -3
- package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
- package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
- package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
- package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
- package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
- package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
- package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
- package/dist/statement/templates/es.en.md +125 -0
- package/dist/statement/templates/es.es.md +127 -0
- package/dist/statement/templates/fr.en.md +128 -0
- package/dist/statement/templates/fr.fr.md +131 -0
- package/dist/statement/templates/it.en.md +127 -0
- package/dist/statement/templates/it.it.md +130 -0
- package/dist/statement/templates/nl.en.md +125 -0
- package/dist/statement/templates/nl.nl.md +127 -0
- package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +1 -1
- package/dist/webpack/index.js +1 -1
- package/dist/worksheet-eUjwYLxC.js +63 -0
- package/package.json +7 -3
- package/dist/audit-CpXH2Mk8.js +0 -2
- package/dist/baseline-22Y1NWxM.js +0 -2
- package/dist/command-D8l_oYbV.js +0 -77
- package/dist/fingerprint-DRoneAjj.js +0 -20
- package/dist/jsdom-4IMzv0eE.js +0 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { c as nullable, d as optional, h as string, l as number, m as safeParse, o as isoDate, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
|
|
2
|
+
import { a as isoDate$1 } from "./text-BF1LHMiV.js";
|
|
3
|
+
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
4
|
+
import { o as findingElements } from "./result-DfUru6y_.js";
|
|
4
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
/** Default filename, used by the CLI when no path is given. */
|
|
@@ -18,7 +19,7 @@ const entrySchema = object({
|
|
|
18
19
|
/** ISO date the entry was written. */
|
|
19
20
|
acceptedOn: withDefault(string(), () => ""),
|
|
20
21
|
/** ISO date after which this entry stops suppressing anything. */
|
|
21
|
-
expiresOn: optional(isoDate
|
|
22
|
+
expiresOn: optional(isoDate()),
|
|
22
23
|
/** Why this is being lived with. Free text, for whoever reads the file. */
|
|
23
24
|
note: optional(string())
|
|
24
25
|
});
|
|
@@ -37,7 +38,7 @@ var BaselineError = class extends Error {
|
|
|
37
38
|
* can still report on what the run actually found.
|
|
38
39
|
*/
|
|
39
40
|
function applyBaseline(audits, baseline, options = {}) {
|
|
40
|
-
const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
|
|
41
|
+
const today = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
|
|
41
42
|
const expired = [];
|
|
42
43
|
const live = /* @__PURE__ */ new Map();
|
|
43
44
|
for (const entry of baseline.entries) if (entry.expiresOn !== void 0 && entry.expiresOn < today) expired.push(entry);
|
|
@@ -93,9 +94,38 @@ function applyBaseline(audits, baseline, options = {}) {
|
|
|
93
94
|
accepted
|
|
94
95
|
};
|
|
95
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The baseline without the entries this run showed are gone.
|
|
99
|
+
*
|
|
100
|
+
* `applyBaseline` already decides which those are, and does it carefully: an
|
|
101
|
+
* entry counts as stale only when the page it names was audited and the element
|
|
102
|
+
* was not found. Entries for pages a narrowed run never looked at are not stale
|
|
103
|
+
* and are kept here, because from the outside a page nobody audited and a page
|
|
104
|
+
* that no longer exists are the same thing, and guessing wrong empties the file
|
|
105
|
+
* that is holding the rest of the site's build together.
|
|
106
|
+
*
|
|
107
|
+
* Nothing is added. That is the whole difference between this and recording a
|
|
108
|
+
* baseline again: re-recording accepts whatever the site is failing today,
|
|
109
|
+
* which on a bad day is a new barrier nobody agreed to.
|
|
110
|
+
*/
|
|
111
|
+
function pruneBaseline(baseline, stale) {
|
|
112
|
+
const gone = new Set(stale.map((entry) => key(entry.page, entry.ruleId, entry.fingerprint)));
|
|
113
|
+
if (gone.size === 0) return {
|
|
114
|
+
baseline,
|
|
115
|
+
removed: []
|
|
116
|
+
};
|
|
117
|
+
const kept = baseline.entries.filter((entry) => !gone.has(key(entry.page, entry.ruleId, entry.fingerprint)));
|
|
118
|
+
return {
|
|
119
|
+
baseline: {
|
|
120
|
+
...baseline,
|
|
121
|
+
entries: kept
|
|
122
|
+
},
|
|
123
|
+
removed: baseline.entries.filter((entry) => gone.has(key(entry.page, entry.ruleId, entry.fingerprint)))
|
|
124
|
+
};
|
|
125
|
+
}
|
|
96
126
|
/** Record every violation a run found, so a later run can fail only on new ones. */
|
|
97
127
|
function buildBaseline(audits, options = {}) {
|
|
98
|
-
const acceptedOn = isoDate(options.today ?? /* @__PURE__ */ new Date());
|
|
128
|
+
const acceptedOn = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
|
|
99
129
|
const entries = [];
|
|
100
130
|
for (const audit of audits) for (const finding of audit.violations) for (const node of findingElements(finding)) entries.push({
|
|
101
131
|
page: audit.relativePath,
|
|
@@ -109,7 +139,7 @@ function buildBaseline(audits, options = {}) {
|
|
|
109
139
|
...options.note ? { note: options.note } : {}
|
|
110
140
|
});
|
|
111
141
|
return {
|
|
112
|
-
schemaVersion:
|
|
142
|
+
schemaVersion: 2,
|
|
113
143
|
createdOn: acceptedOn,
|
|
114
144
|
entries: entries.sort(byEntry)
|
|
115
145
|
};
|
|
@@ -137,7 +167,8 @@ async function readBaseline(file, cwd = process.cwd()) {
|
|
|
137
167
|
const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
|
|
138
168
|
throw new BaselineError(`${path.basename(target)} is not an eaa-kit baseline (${issues.join("; ")})`);
|
|
139
169
|
}
|
|
140
|
-
if (result.data.schemaVersion !==
|
|
170
|
+
if (result.data.schemaVersion !== 2) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 2.\n Record it again on the current build: eaa-kit baseline\n Read the new file before committing it: it lists what this run found, which is
|
|
171
|
+
not necessarily what the old one accepted.`);
|
|
141
172
|
return result.data;
|
|
142
173
|
}
|
|
143
174
|
async function writeBaseline(file, baseline, cwd = process.cwd()) {
|
|
@@ -152,8 +183,5 @@ function key(page, ruleId, fingerprint) {
|
|
|
152
183
|
function byEntry(a, b) {
|
|
153
184
|
return a.page.localeCompare(b.page) || a.ruleId.localeCompare(b.ruleId) || a.fingerprint.localeCompare(b.fingerprint);
|
|
154
185
|
}
|
|
155
|
-
function isoDate(date) {
|
|
156
|
-
return date.toISOString().slice(0, 10);
|
|
157
|
-
}
|
|
158
186
|
//#endregion
|
|
159
|
-
export {
|
|
187
|
+
export { pruneBaseline as a, writeBaseline as c, buildBaseline as i, DEFAULT_BASELINE_FILE as n, readBaseline as o, applyBaseline as r, serialiseBaseline as s, BaselineError as t };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { n as count } from "./text-BF1LHMiV.js";
|
|
2
|
+
import { a as emitDocument, o as fail, s as note } from "./command-CZKp9Wi_.js";
|
|
3
|
+
import "./review-CdMK2GpQ.js";
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/cli/checklist.ts
|
|
7
|
+
async function runChecklistCommand(options = {}) {
|
|
8
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9
|
+
const recordPath = options.record ?? "eaa-review.json";
|
|
10
|
+
const { WCAG22_AA_CRITERIA } = await import("./coverage-7bQqWNfN.js");
|
|
11
|
+
const { answeredCount, blankReview, readReview, ReviewError, writeReview } = await import("./review-DIbOTmOM.js");
|
|
12
|
+
let existing;
|
|
13
|
+
try {
|
|
14
|
+
existing = await readReview(recordPath, cwd);
|
|
15
|
+
} catch (cause) {
|
|
16
|
+
if (!(cause instanceof ReviewError)) throw cause;
|
|
17
|
+
if (await exists(path.resolve(cwd, recordPath))) {
|
|
18
|
+
fail(cause.message);
|
|
19
|
+
return { exitCode: 2 };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const record = blankReview(WCAG22_AA_CRITERIA, existing);
|
|
23
|
+
if (options.reviewedBy !== void 0) record.reviewedBy = options.reviewedBy;
|
|
24
|
+
await writeReview(recordPath, record, cwd);
|
|
25
|
+
note(existing === void 0 ? `Review record written to ${recordPath}` : `Review record updated at ${recordPath}: ${count(answeredCount(record), "criterion")} already answered`);
|
|
26
|
+
const { buildWorksheet } = await import("./worksheet-eUjwYLxC.js");
|
|
27
|
+
await emitDocument(buildWorksheet({
|
|
28
|
+
recordPath,
|
|
29
|
+
review: record
|
|
30
|
+
}), options.output, cwd);
|
|
31
|
+
if (options.output !== void 0) note(`Worksheet written to ${options.output}`);
|
|
32
|
+
note("Answers go in the record; the worksheet is generated and never read back.");
|
|
33
|
+
return { exitCode: 0 };
|
|
34
|
+
}
|
|
35
|
+
async function exists(target) {
|
|
36
|
+
try {
|
|
37
|
+
await access(target);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { runChecklistCommand };
|
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as
|
|
3
|
-
import { l as
|
|
4
|
-
import { i as
|
|
5
|
-
import {
|
|
6
|
-
import { n as count, t as collapse } from "../text-
|
|
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
|
+
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-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
|
-
import { t as elementFingerprint } from "../fingerprint-
|
|
9
|
-
import { i as buildBaseline, n as DEFAULT_BASELINE_FILE,
|
|
10
|
-
import { n as
|
|
11
|
-
import {
|
|
8
|
+
import { t as elementFingerprint } from "../fingerprint-BjYV_0F7.js";
|
|
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";
|
|
13
|
+
import { enableCompileCache } from "node:module";
|
|
12
14
|
import { readFile } from "node:fs/promises";
|
|
13
15
|
import path from "node:path";
|
|
14
16
|
import { Command, InvalidArgumentError } from "commander";
|
|
@@ -36,16 +38,17 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
36
38
|
entries: 0,
|
|
37
39
|
exitCode: 2
|
|
38
40
|
};
|
|
39
|
-
const { pages, origin, label } = resolved;
|
|
41
|
+
const { pages, origin, label, directory } = resolved;
|
|
40
42
|
note(`Auditing ${count(pages.length, "page")} in ${label}…`);
|
|
41
43
|
const baseUrl = options.baseUrl ?? origin;
|
|
42
44
|
const audits = await runEngine(pages, {
|
|
43
45
|
cwd,
|
|
46
|
+
...options.headers === void 0 ? {} : { headers: options.headers },
|
|
44
47
|
...baseUrl === void 0 ? {} : { baseUrl },
|
|
45
48
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
46
49
|
...options.browser ? { browser: true } : {},
|
|
47
50
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
48
|
-
...
|
|
51
|
+
...directory === void 0 ? {} : { directory }
|
|
49
52
|
});
|
|
50
53
|
if (!audits) return {
|
|
51
54
|
entries: 0,
|
|
@@ -59,11 +62,12 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
59
62
|
exitCode: 2
|
|
60
63
|
};
|
|
61
64
|
}
|
|
65
|
+
const target = options.output ?? "eaa-baseline.json";
|
|
66
|
+
if (options.prune) return await prune(audits, target, cwd);
|
|
62
67
|
const baseline = buildBaseline(audits, {
|
|
63
68
|
...options.note ? { note: options.note } : {},
|
|
64
69
|
...options.expiresOn ? { expiresOn: options.expiresOn } : {}
|
|
65
70
|
});
|
|
66
|
-
const target = options.output ?? "eaa-baseline.json";
|
|
67
71
|
try {
|
|
68
72
|
await writeBaseline(target, baseline, cwd);
|
|
69
73
|
} catch (cause) {
|
|
@@ -77,13 +81,60 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
77
81
|
throw cause;
|
|
78
82
|
}
|
|
79
83
|
const entries = baseline.entries.length;
|
|
80
|
-
note(`Wrote ${entries
|
|
84
|
+
note(`Wrote ${count(entries, "entry")} to ${target}`);
|
|
81
85
|
if (entries > 0) advise("These are barriers, not exceptions. Commit the file, then work the list down.");
|
|
82
86
|
return {
|
|
83
87
|
entries,
|
|
84
88
|
exitCode: 0
|
|
85
89
|
};
|
|
86
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
|
+
}
|
|
87
138
|
var DiffError = class extends Error {
|
|
88
139
|
name = "DiffError";
|
|
89
140
|
};
|
|
@@ -222,7 +273,7 @@ async function readReport(file, cwd = process.cwd()) {
|
|
|
222
273
|
const first = result.error.issues[0];
|
|
223
274
|
throw new DiffError(`${file} is not an eaa-kit JSON report${first === void 0 || first.path.length === 0 ? "" : ` at ${first.path.join(".")}`}: ${first?.message ?? "unrecognised shape"}`);
|
|
224
275
|
}
|
|
225
|
-
if (result.data.schemaVersion !==
|
|
276
|
+
if (result.data.schemaVersion !== 2) throw new DiffError(`${file} is schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 2.\n Write both reports with the same version of eaa-kit before comparing them.`);
|
|
226
277
|
return result.data;
|
|
227
278
|
}
|
|
228
279
|
//#endregion
|
|
@@ -352,6 +403,23 @@ async function runStatementCommand(options = {}) {
|
|
|
352
403
|
});
|
|
353
404
|
let audit;
|
|
354
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
|
+
};
|
|
355
423
|
const statement = await renderStatement(config, {
|
|
356
424
|
...options.locale ? { locale: options.locale } : {},
|
|
357
425
|
...options.country ? { country: options.country } : {},
|
|
@@ -373,7 +441,8 @@ async function runStatementCommand(options = {}) {
|
|
|
373
441
|
exitCode: 0
|
|
374
442
|
};
|
|
375
443
|
} catch (cause) {
|
|
376
|
-
|
|
444
|
+
const { ReviewError } = await import("../review-DIbOTmOM.js");
|
|
445
|
+
if (cause instanceof ConfigError || cause instanceof StatementError || cause instanceof ReviewError) {
|
|
377
446
|
fail(cause.message);
|
|
378
447
|
if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
|
|
379
448
|
return {
|
|
@@ -396,6 +465,30 @@ function formatFor(output) {
|
|
|
396
465
|
//#endregion
|
|
397
466
|
//#region src/cli/index.ts
|
|
398
467
|
/**
|
|
468
|
+
* Reuse the compiled form of this tool and its dependencies between runs.
|
|
469
|
+
*
|
|
470
|
+
* Most of what a short audit costs is not the audit. jsdom alone is ~700 ms to
|
|
471
|
+
* import on a warm page cache, and V8 pays to parse and compile it from source
|
|
472
|
+
* on every single run — of a CLI somebody invokes from a build script, over and
|
|
473
|
+
* over, against code that has not changed since the last time. Node can cache
|
|
474
|
+
* the compiled bytecode instead, which is exactly the shape of this problem.
|
|
475
|
+
*
|
|
476
|
+
* Measured over ten pages: 2350 ms without, 2075 ms with. The saving is a fixed
|
|
477
|
+
* cost, so it counts for most on the small sites where fixed cost is most of
|
|
478
|
+
* the run.
|
|
479
|
+
*
|
|
480
|
+
* Best-effort by design. It is a cache: a read-only filesystem, a sandbox that
|
|
481
|
+
* forbids the directory, or a Node built without the feature all mean the run
|
|
482
|
+
* is a little slower, and none of them is a reason to fail an audit. Node's own
|
|
483
|
+
* NODE_DISABLE_COMPILE_CACHE is honoured by the call itself.
|
|
484
|
+
*/
|
|
485
|
+
function useCompileCache() {
|
|
486
|
+
try {
|
|
487
|
+
enableCompileCache();
|
|
488
|
+
} catch {}
|
|
489
|
+
}
|
|
490
|
+
useCompileCache();
|
|
491
|
+
/**
|
|
399
492
|
* A parser for a fixed set of words, listing them when the answer is not one.
|
|
400
493
|
*
|
|
401
494
|
* `normalise` is for `--country de`, where the accepted spelling differs from
|
|
@@ -428,19 +521,48 @@ const parseFormat = oneOf(OUTPUT_FORMATS);
|
|
|
428
521
|
const parsePositive = wholeNumber(1);
|
|
429
522
|
const parseDepth = wholeNumber(0);
|
|
430
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
|
+
}
|
|
431
548
|
const program = new Command();
|
|
432
549
|
program.exitOverride();
|
|
433
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");
|
|
434
|
-
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("|")})`, parseImpact
|
|
435
|
-
const {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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) => {
|
|
552
|
+
const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
|
|
553
|
+
const invocation = auditInvocation(dir, defaults, flags);
|
|
554
|
+
const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
|
|
555
|
+
process.exitCode = exitCode;
|
|
556
|
+
});
|
|
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) => {
|
|
558
|
+
const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
|
|
559
|
+
const invocation = baselineInvocation(dir, defaults, flags);
|
|
560
|
+
const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
|
|
440
561
|
process.exitCode = exitCode;
|
|
441
562
|
});
|
|
442
|
-
program.command("
|
|
443
|
-
const {
|
|
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);
|
|
444
566
|
process.exitCode = exitCode;
|
|
445
567
|
});
|
|
446
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) => {
|
|
@@ -448,11 +570,11 @@ program.command("diff").description("Compare two JSON reports: what a change mad
|
|
|
448
570
|
process.exitCode = exitCode;
|
|
449
571
|
});
|
|
450
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) => {
|
|
451
|
-
const { runInitCommand } = await import("../init-
|
|
573
|
+
const { runInitCommand } = await import("../init-BjKYttxc.js");
|
|
452
574
|
const { exitCode } = await runInitCommand(flags);
|
|
453
575
|
process.exitCode = exitCode;
|
|
454
576
|
});
|
|
455
|
-
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) => {
|
|
456
578
|
const { lang, ...options } = flags;
|
|
457
579
|
const { exitCode } = await runStatementCommand({
|
|
458
580
|
...options,
|
|
@@ -463,6 +585,12 @@ program.command("statement").description("Generate an EU accessibility statement
|
|
|
463
585
|
try {
|
|
464
586
|
await program.parseAsync(process.argv);
|
|
465
587
|
} catch (cause) {
|
|
588
|
+
if (cause instanceof ConfigError) {
|
|
589
|
+
fail(cause.message);
|
|
590
|
+
for (const issue of cause.issues) note(` ${issue}`);
|
|
591
|
+
process.exitCode = 2;
|
|
592
|
+
process.exit(2);
|
|
593
|
+
}
|
|
466
594
|
const error = cause;
|
|
467
595
|
if (typeof error.exitCode === "number") process.exitCode = error.exitCode === 0 ? 0 : 2;
|
|
468
596
|
else {
|
|
@@ -46,11 +46,32 @@ async function collectPages(dir, options = {}) {
|
|
|
46
46
|
const pages = [];
|
|
47
47
|
for (let i = 0; i < relativePaths.length; i += READ_CONCURRENCY) {
|
|
48
48
|
const batch = relativePaths.slice(i, i + READ_CONCURRENCY);
|
|
49
|
-
const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable)));
|
|
49
|
+
const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable, options.maxBytes ?? 33554432)));
|
|
50
50
|
for (const page of read) if (page !== void 0) pages.push(page);
|
|
51
51
|
}
|
|
52
52
|
return pages;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether the directory holds any HTML at all, ignoring include and exclude.
|
|
56
|
+
*
|
|
57
|
+
* Asked when a run collected nothing, to tell two different mistakes apart: a
|
|
58
|
+
* build directory with no pages in it, and a directory full of pages that the
|
|
59
|
+
* caller's own filters excluded. The advice for the first is wrong for the
|
|
60
|
+
* second — it names another directory to audit — so the question has to be
|
|
61
|
+
* answered before it is given.
|
|
62
|
+
*
|
|
63
|
+
* Globs rather than collecting: this only needs to know whether one file
|
|
64
|
+
* exists, and reading every page of a large build to answer that on a path that
|
|
65
|
+
* is about to print a warning and stop would be work for nothing.
|
|
66
|
+
*/
|
|
67
|
+
async function holdsHtml(dir) {
|
|
68
|
+
return (await glob([...DEFAULT_INCLUDE], {
|
|
69
|
+
cwd: path.resolve(dir),
|
|
70
|
+
ignore: [...DEFAULT_EXCLUDE],
|
|
71
|
+
onlyFiles: true,
|
|
72
|
+
dot: false
|
|
73
|
+
})).length > 0;
|
|
74
|
+
}
|
|
54
75
|
async function assertDirectory(root, original) {
|
|
55
76
|
let stats;
|
|
56
77
|
try {
|
|
@@ -61,9 +82,15 @@ async function assertDirectory(root, original) {
|
|
|
61
82
|
}
|
|
62
83
|
if (!stats.isDirectory()) throw new BuildDirectoryError(`Build path is not a directory: ${original}`, root);
|
|
63
84
|
}
|
|
64
|
-
async function readPage(root, relativePath, onUnreadable) {
|
|
85
|
+
async function readPage(root, relativePath, onUnreadable, maxBytes) {
|
|
65
86
|
const absolutePath = path.join(root, relativePath);
|
|
66
87
|
try {
|
|
88
|
+
const { size } = await stat(absolutePath);
|
|
89
|
+
if (size > maxBytes) {
|
|
90
|
+
if (onUnreadable === void 0) throw new Error(`${relativePath} is ${size} bytes, over the ${maxBytes} byte limit`);
|
|
91
|
+
onUnreadable(relativePath, `${megabytes(size)} MB, over the ${megabytes(maxBytes)} MB limit for one page`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
67
94
|
return {
|
|
68
95
|
absolutePath,
|
|
69
96
|
relativePath,
|
|
@@ -130,5 +157,9 @@ function generic(head) {
|
|
|
130
157
|
function article(name) {
|
|
131
158
|
return /^[aeiou]/i.test(name) ? "an" : "a";
|
|
132
159
|
}
|
|
160
|
+
/** Sizes in the megabytes a person reads, not the bytes a computer counts. */
|
|
161
|
+
function megabytes(bytes) {
|
|
162
|
+
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
163
|
+
}
|
|
133
164
|
//#endregion
|
|
134
|
-
export { stripBom as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };
|
|
165
|
+
export { stripBom as a, holdsHtml as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };
|