eaa-kit 0.3.0 → 0.4.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 +51 -5
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +1 -1
- package/dist/{audit-DcL73mOC.js → audit-CPoZMXGM.js} +180 -36
- package/dist/audit-CpXH2Mk8.js +2 -0
- package/dist/{baseline-CuKFq4IF.js → baseline-22Y1NWxM.js} +1 -1
- package/dist/{baseline-s9F3fXTN.js → baseline-DB9CZGnV.js} +2 -2
- package/dist/cli/index.js +267 -10
- package/dist/{collect-BkAQ0viT.js → collect-CFM8gEVv.js} +21 -10
- package/dist/{command-Dxpa00Ha.js → command-D8l_oYbV.js} +2 -2
- package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B4IBKMO8.js +521 -0
- package/dist/{crawl-Oxt2Gaqo.js → crawl-BiI1Lau_.js} +2 -2
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
- package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
- package/dist/{html-DKiI_3gs.js → html-BGTO3ypW.js} +130 -5
- package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{init-DiLiYvN1.js → init-DIWDE35F.js} +2 -2
- package/dist/jsdom-4IMzv0eE.js +3 -0
- package/dist/jsdom-DCpGSLfW.js +81 -0
- package/dist/{json-Cnv9nd6U.js → json-DjEvy1nX.js} +8 -3
- package/dist/{json-B0Y7rNjt.js → json-QQuFIw1W.js} +1 -1
- package/dist/{load-UYXLqGV9.js → load-5wRGLvub.js} +1 -1
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DYFsGUNd.js → playwright-BWniOain.js} +1 -1
- package/dist/{pool-BWkWZiJW.js → pool-BMevaLWD.js} +2 -2
- package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-Dtowi2EC.js +321 -0
- package/dist/{render-DI_aCnAZ.js → render-DrvXRCEn.js} +3 -3
- package/dist/{result-DLxd2Eip.js → result-DoamKFsp.js} +78 -1
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/{run-BW6CVuND.js → run-DB34BSOZ.js} +19 -2
- package/dist/{sarif-DB3WG7T9.js → sarif-SR3_lLYd.js} +14 -6
- package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
- package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +30 -8
- package/dist/audit-CuG2hYyo.js +0 -2
- package/dist/jsdom-BjpF-2V-.js +0 -158
- package/dist/jsdom-X4KYfTp8.js +0 -3
- package/dist/manual-Vz-oX1I_.js +0 -239
- package/dist/routes-C2Cgf6Ko.js +0 -119
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { l as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
2
|
+
import { c as number, f as record, l as object, m as string, n as boolean, p as safeParse, s as nullable, t as array, u as optional, v as withDefault } from "../schema-is6CGX2D.js";
|
|
3
|
+
import { l as STATEMENT_LOCALES, o as COUNTRIES, r as loadConfig, s as ConfigError } from "../load-5wRGLvub.js";
|
|
4
|
+
import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-DrvXRCEn.js";
|
|
5
|
+
import { a as impactRank, n as IMPACT_LEVELS, o as isImpactLevel, s as meetsThreshold, t as DEFAULT_FAIL_ON } from "../impact-DZt2oBCP.js";
|
|
6
|
+
import { n as count, t as collapse } from "../text-CKKpzkYM.js";
|
|
6
7
|
import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
9
|
-
import {
|
|
8
|
+
import { t as elementFingerprint } from "../fingerprint-DRoneAjj.js";
|
|
9
|
+
import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-DB9CZGnV.js";
|
|
10
|
+
import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-CPoZMXGM.js";
|
|
11
|
+
import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "../command-D8l_oYbV.js";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
10
13
|
import path from "node:path";
|
|
11
14
|
import { Command, InvalidArgumentError } from "commander";
|
|
15
|
+
import pc from "picocolors";
|
|
12
16
|
//#region src/cli/baseline.ts
|
|
13
17
|
/**
|
|
14
18
|
* `eaa-kit baseline [dir]`.
|
|
@@ -80,6 +84,255 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
80
84
|
exitCode: 0
|
|
81
85
|
};
|
|
82
86
|
}
|
|
87
|
+
var DiffError = class extends Error {
|
|
88
|
+
name = "DiffError";
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Only the fields a comparison reads. `completeness` is optional: a report
|
|
92
|
+
* written before it existed says nothing about coverage, which is not the same
|
|
93
|
+
* as saying the run was complete, and is handled as the unknown it is.
|
|
94
|
+
*/
|
|
95
|
+
const reportSchema = object({
|
|
96
|
+
schemaVersion: number(),
|
|
97
|
+
generatedAt: string(),
|
|
98
|
+
target: withDefault(object({ source: withDefault(string(), () => "") }), () => ({ source: "" })),
|
|
99
|
+
completeness: optional(object({
|
|
100
|
+
complete: boolean(),
|
|
101
|
+
audited: withDefault(number(), () => 0)
|
|
102
|
+
})),
|
|
103
|
+
rules: withDefault(record(object({ help: withDefault(string(), () => "") })), () => ({})),
|
|
104
|
+
pages: withDefault(array(object({
|
|
105
|
+
path: string(),
|
|
106
|
+
error: withDefault(nullable(string()), () => null),
|
|
107
|
+
violations: withDefault(array(object({
|
|
108
|
+
ruleId: string(),
|
|
109
|
+
impact: withDefault(nullable(string()), () => null),
|
|
110
|
+
nodes: withDefault(array(object({
|
|
111
|
+
html: withDefault(string(), () => ""),
|
|
112
|
+
target: withDefault(array(string()), () => [])
|
|
113
|
+
})), () => [])
|
|
114
|
+
})), () => [])
|
|
115
|
+
})), () => [])
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* The elements a violation points at.
|
|
119
|
+
*
|
|
120
|
+
* Mirrors `findingElements` for the JSON shape, including its one subtlety: a
|
|
121
|
+
* rule can fail with no element attached, and it still has an identity. Getting
|
|
122
|
+
* this wrong here would make document-level failures invisible to a diff while
|
|
123
|
+
* remaining visible to the baseline.
|
|
124
|
+
*/
|
|
125
|
+
function elementsOf(violation) {
|
|
126
|
+
if (violation.nodes.length === 0) return [{
|
|
127
|
+
selector: "",
|
|
128
|
+
html: ""
|
|
129
|
+
}];
|
|
130
|
+
return violation.nodes.map((node) => ({
|
|
131
|
+
selector: node.target.join(" "),
|
|
132
|
+
html: node.html
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
/** Every violating element in a report, keyed by page and element identity. */
|
|
136
|
+
function entriesOf(report) {
|
|
137
|
+
const entries = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const page of report.pages) for (const violation of page.violations) {
|
|
139
|
+
const impact = violation.impact !== null && isImpactLevel(violation.impact) ? violation.impact : null;
|
|
140
|
+
for (const { selector, html } of elementsOf(violation)) {
|
|
141
|
+
const fingerprint = elementFingerprint(violation.ruleId, selector, html);
|
|
142
|
+
const key = `${page.path}\n${fingerprint}`;
|
|
143
|
+
if (entries.has(key)) continue;
|
|
144
|
+
entries.set(key, {
|
|
145
|
+
page: page.path,
|
|
146
|
+
ruleId: violation.ruleId,
|
|
147
|
+
fingerprint,
|
|
148
|
+
impact,
|
|
149
|
+
help: report.rules[violation.ruleId]?.help ?? violation.ruleId,
|
|
150
|
+
selector,
|
|
151
|
+
html
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return entries;
|
|
156
|
+
}
|
|
157
|
+
/** Pages a run reached a verdict on. A page that errored is not one of them. */
|
|
158
|
+
function auditedPages(report) {
|
|
159
|
+
return new Set(report.pages.filter((page) => page.error === null).map((page) => page.path));
|
|
160
|
+
}
|
|
161
|
+
function sideOf(report) {
|
|
162
|
+
return {
|
|
163
|
+
source: report.target.source,
|
|
164
|
+
generatedAt: report.generatedAt,
|
|
165
|
+
audited: report.completeness?.audited ?? auditedPages(report).size,
|
|
166
|
+
complete: report.completeness?.complete
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Worst first, then by page, so two runs of one comparison agree. */
|
|
170
|
+
function bySeverityThenPage(a, b) {
|
|
171
|
+
return impactRank(a.impact) - impactRank(b.impact) || a.page.localeCompare(b.page) || a.ruleId.localeCompare(b.ruleId) || a.selector.localeCompare(b.selector);
|
|
172
|
+
}
|
|
173
|
+
/** Compare two parsed reports, earlier first. */
|
|
174
|
+
function diffReports(base, head) {
|
|
175
|
+
const before = entriesOf(base);
|
|
176
|
+
const after = entriesOf(head);
|
|
177
|
+
const measuredByHead = auditedPages(head);
|
|
178
|
+
const added = [];
|
|
179
|
+
const fixed = [];
|
|
180
|
+
const unchanged = [];
|
|
181
|
+
const unmeasured = [];
|
|
182
|
+
for (const [key, entry] of after) if (before.has(key)) unchanged.push(entry);
|
|
183
|
+
else added.push(entry);
|
|
184
|
+
for (const [key, entry] of before) {
|
|
185
|
+
if (after.has(key)) continue;
|
|
186
|
+
if (measuredByHead.has(entry.page)) fixed.push(entry);
|
|
187
|
+
else unmeasured.push(entry);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
base: sideOf(base),
|
|
191
|
+
head: sideOf(head),
|
|
192
|
+
added: added.sort(bySeverityThenPage),
|
|
193
|
+
fixed: fixed.sort(bySeverityThenPage),
|
|
194
|
+
unchanged: unchanged.sort(bySeverityThenPage),
|
|
195
|
+
unmeasured: unmeasured.sort(bySeverityThenPage)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Read a JSON report from disk.
|
|
200
|
+
*
|
|
201
|
+
* Refuses a schema version it was not written against rather than reading it
|
|
202
|
+
* optimistically: a field that changed meaning is exactly what the version
|
|
203
|
+
* number exists to signal, and a diff built on a misread field would be wrong
|
|
204
|
+
* quietly.
|
|
205
|
+
*/
|
|
206
|
+
async function readReport(file, cwd = process.cwd()) {
|
|
207
|
+
const target = path.resolve(cwd, file);
|
|
208
|
+
let raw;
|
|
209
|
+
try {
|
|
210
|
+
raw = await readFile(target, "utf8");
|
|
211
|
+
} catch {
|
|
212
|
+
throw new DiffError(`Report not found: ${file}`);
|
|
213
|
+
}
|
|
214
|
+
let parsed;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(raw);
|
|
217
|
+
} catch (cause) {
|
|
218
|
+
throw new DiffError(`${file} is not valid JSON (${cause.message})`);
|
|
219
|
+
}
|
|
220
|
+
const result = safeParse(reportSchema, parsed);
|
|
221
|
+
if (!result.success) {
|
|
222
|
+
const first = result.error.issues[0];
|
|
223
|
+
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
|
+
}
|
|
225
|
+
if (result.data.schemaVersion !== 1) throw new DiffError(`${file} is schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
|
|
226
|
+
return result.data;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/cli/diff.ts
|
|
230
|
+
/**
|
|
231
|
+
* `eaa-kit diff <before.json> <after.json>`.
|
|
232
|
+
*
|
|
233
|
+
* What a change did to a site's accessibility, which is the question a review
|
|
234
|
+
* actually asks and which neither report answers on its own. Running the
|
|
235
|
+
* auditor on a branch prints everything wrong with the site; almost all of it
|
|
236
|
+
* was already there, and the two or three findings somebody introduced are
|
|
237
|
+
* somewhere in the middle of it.
|
|
238
|
+
*
|
|
239
|
+
* Deliberately stateless. A baseline is a file somebody commits and maintains,
|
|
240
|
+
* and it answers "what have we agreed to live with". This answers "what did
|
|
241
|
+
* this change do", needs no file and no decision, and can be run over any two
|
|
242
|
+
* reports after the fact.
|
|
243
|
+
*/
|
|
244
|
+
const DIFF_FORMATS = ["console", "json"];
|
|
245
|
+
/** Longest element markup shown before it is truncated. */
|
|
246
|
+
const MAX_SNIPPET = 100;
|
|
247
|
+
async function runDiffCommand(base, head, options = {}) {
|
|
248
|
+
const cwd = options.cwd ?? process.cwd();
|
|
249
|
+
let diff;
|
|
250
|
+
try {
|
|
251
|
+
diff = diffReports(await readReport(base, cwd), await readReport(head, cwd));
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
if (cause instanceof DiffError) {
|
|
254
|
+
fail(cause.message);
|
|
255
|
+
return { exitCode: 2 };
|
|
256
|
+
}
|
|
257
|
+
throw cause;
|
|
258
|
+
}
|
|
259
|
+
const failOn = options.failOn ?? "serious";
|
|
260
|
+
const failing = diff.added.filter((entry) => meetsThreshold(entry.impact, failOn));
|
|
261
|
+
const body = (options.format ?? "console") === "json" ? `${JSON.stringify(toJson(diff, failOn, failing.length), null, 2)}\n` : `${formatDiff(diff, failOn, options.output === void 0)}\n`;
|
|
262
|
+
await emitDocument(body, options.output, cwd);
|
|
263
|
+
if (options.output !== void 0) note(`Diff written to ${options.output}`);
|
|
264
|
+
if (diff.unmeasured.length > 0) warn(`${count(diff.unmeasured.length, "violation")} could not be compared: the later run did not audit ${diff.unmeasured.length === 1 ? "its page" : "their pages"}`);
|
|
265
|
+
return {
|
|
266
|
+
diff,
|
|
267
|
+
exitCode: failing.length > 0 ? 1 : 0
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The machine-readable form.
|
|
272
|
+
*
|
|
273
|
+
* `unchanged` is counted and not listed: it is the whole of the pre-existing
|
|
274
|
+
* report, it is already available in the report itself, and repeating it here
|
|
275
|
+
* would make the common case — a diff with three entries — a document nobody
|
|
276
|
+
* can read.
|
|
277
|
+
*/
|
|
278
|
+
function toJson(diff, failOn, failing) {
|
|
279
|
+
return {
|
|
280
|
+
base: diff.base,
|
|
281
|
+
head: diff.head,
|
|
282
|
+
summary: {
|
|
283
|
+
added: diff.added.length,
|
|
284
|
+
fixed: diff.fixed.length,
|
|
285
|
+
unchanged: diff.unchanged.length,
|
|
286
|
+
unmeasured: diff.unmeasured.length,
|
|
287
|
+
failOn,
|
|
288
|
+
failing
|
|
289
|
+
},
|
|
290
|
+
added: diff.added,
|
|
291
|
+
fixed: diff.fixed,
|
|
292
|
+
unmeasured: diff.unmeasured
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function formatDiff(diff, failOn, colour = true) {
|
|
296
|
+
const c = pc.createColors(colour);
|
|
297
|
+
const lines = [""];
|
|
298
|
+
lines.push(c.bold("eaa-kit diff"));
|
|
299
|
+
lines.push(c.dim(` before ${describe(diff.base)}`));
|
|
300
|
+
lines.push(c.dim(` after ${describe(diff.head)}`));
|
|
301
|
+
lines.push("");
|
|
302
|
+
if (diff.added.length === 0 && diff.fixed.length === 0) lines.push(c.green("No change in violations."));
|
|
303
|
+
if (diff.added.length > 0) {
|
|
304
|
+
lines.push(c.red(c.bold(`New (${diff.added.length})`)));
|
|
305
|
+
lines.push(...diff.added.map((entry) => entryLine(entry, c)));
|
|
306
|
+
lines.push("");
|
|
307
|
+
}
|
|
308
|
+
if (diff.fixed.length > 0) {
|
|
309
|
+
lines.push(c.green(c.bold(`Fixed (${diff.fixed.length})`)));
|
|
310
|
+
lines.push(...diff.fixed.map((entry) => entryLine(entry, c)));
|
|
311
|
+
lines.push("");
|
|
312
|
+
}
|
|
313
|
+
if (diff.unmeasured.length > 0) {
|
|
314
|
+
lines.push(c.yellow(c.bold(`Not compared (${diff.unmeasured.length})`)));
|
|
315
|
+
lines.push(c.dim(" These failed before, and the later run did not audit the page they were on."));
|
|
316
|
+
lines.push(...diff.unmeasured.map((entry) => entryLine(entry, c)));
|
|
317
|
+
lines.push("");
|
|
318
|
+
}
|
|
319
|
+
const failing = diff.added.filter((entry) => meetsThreshold(entry.impact, failOn)).length;
|
|
320
|
+
lines.push(c.bold("Summary"));
|
|
321
|
+
lines.push(` ${diff.added.length} new · ${diff.fixed.length} fixed · ${diff.unchanged.length} unchanged`);
|
|
322
|
+
lines.push(failing > 0 ? c.red(` ${failing} new at or above ${failOn} (--fail-on ${failOn})`) : c.green(` no new violations at or above ${failOn} (--fail-on ${failOn})`));
|
|
323
|
+
return lines.join("\n");
|
|
324
|
+
}
|
|
325
|
+
function describe(side) {
|
|
326
|
+
const source = side.source === "" ? "unknown source" : side.source;
|
|
327
|
+
const coverage = side.complete === void 0 ? "coverage not recorded" : side.complete ? "complete" : "incomplete";
|
|
328
|
+
return `${source} · ${count(side.audited, "page")} · ${coverage} · ${side.generatedAt}`;
|
|
329
|
+
}
|
|
330
|
+
function entryLine(entry, c) {
|
|
331
|
+
const where = entry.selector === "" ? entry.page : `${entry.page} ${c.dim(entry.selector)}`;
|
|
332
|
+
const impact = entry.impact ?? "unclassified";
|
|
333
|
+
const snippet = entry.html === "" ? "" : `\n ${c.dim(collapse(entry.html, MAX_SNIPPET))}`;
|
|
334
|
+
return ` ${c.bold(entry.ruleId)} ${c.dim(`(${impact})`)} — ${entry.help}\n ${where}${snippet}`;
|
|
335
|
+
}
|
|
83
336
|
//#endregion
|
|
84
337
|
//#region src/cli/statement.ts
|
|
85
338
|
/** Markdown for a content directory, HTML for dropping straight onto a site. */
|
|
@@ -178,7 +431,7 @@ const parseConcurrency = wholeNumber(1);
|
|
|
178
431
|
const program = new Command();
|
|
179
432
|
program.exitOverride();
|
|
180
433
|
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");
|
|
181
|
-
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("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").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, DEFAULT_FAIL_ON).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")})`, parseFormat, "console").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("--concurrency <n>", "worker threads to audit with, 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").action(async (dir, flags) => {
|
|
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, DEFAULT_FAIL_ON).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")})`, parseFormat, "console").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("--concurrency <n>", "worker threads to audit with, 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").action(async (dir, flags) => {
|
|
182
435
|
const { build, ...options } = flags;
|
|
183
436
|
const { exitCode } = await runAuditCommand(dir, {
|
|
184
437
|
...options,
|
|
@@ -186,12 +439,16 @@ program.command("audit").description("Audit built HTML against WCAG 2.2 AA").arg
|
|
|
186
439
|
});
|
|
187
440
|
process.exitCode = exitCode;
|
|
188
441
|
});
|
|
189
|
-
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", "./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("--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>", "worker threads to audit with, or 1 for none", parseConcurrency).action(async (dir, flags) => {
|
|
442
|
+
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", "./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>", "worker threads to audit with, or 1 for none", parseConcurrency).action(async (dir, flags) => {
|
|
190
443
|
const { exitCode } = await runBaselineCommand(dir, flags);
|
|
191
444
|
process.exitCode = exitCode;
|
|
192
445
|
});
|
|
446
|
+
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) => {
|
|
447
|
+
const { exitCode } = await runDiffCommand(before, after, flags);
|
|
448
|
+
process.exitCode = exitCode;
|
|
449
|
+
});
|
|
193
450
|
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) => {
|
|
194
|
-
const { runInitCommand } = await import("../init-
|
|
451
|
+
const { runInitCommand } = await import("../init-DIWDE35F.js");
|
|
195
452
|
const { exitCode } = await runInitCommand(flags);
|
|
196
453
|
process.exitCode = exitCode;
|
|
197
454
|
});
|
|
@@ -46,7 +46,8 @@ 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
|
-
|
|
49
|
+
const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable)));
|
|
50
|
+
for (const page of read) if (page !== void 0) pages.push(page);
|
|
50
51
|
}
|
|
51
52
|
return pages;
|
|
52
53
|
}
|
|
@@ -60,13 +61,19 @@ async function assertDirectory(root, original) {
|
|
|
60
61
|
}
|
|
61
62
|
if (!stats.isDirectory()) throw new BuildDirectoryError(`Build path is not a directory: ${original}`, root);
|
|
62
63
|
}
|
|
63
|
-
async function readPage(root, relativePath) {
|
|
64
|
+
async function readPage(root, relativePath, onUnreadable) {
|
|
64
65
|
const absolutePath = path.join(root, relativePath);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
absolutePath,
|
|
69
|
+
relativePath,
|
|
70
|
+
html: stripBom(await readFile(absolutePath, "utf8"))
|
|
71
|
+
};
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
if (onUnreadable === void 0) throw cause;
|
|
74
|
+
onUnreadable(relativePath, cause.message);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* What to suggest when a directory holds no HTML, or is not there at all.
|
|
@@ -79,8 +86,8 @@ async function readPage(root, relativePath) {
|
|
|
79
86
|
*/
|
|
80
87
|
async function emptyDirectoryHint(dir, cwd = process.cwd()) {
|
|
81
88
|
const head = await exists(dir, cwd) ? `${dir} holds no HTML files.` : `${dir} does not exist.`;
|
|
82
|
-
const { detectFramework } = await import("./frameworks-
|
|
83
|
-
const { readPackageJson } = await import("./project-
|
|
89
|
+
const { detectFramework } = await import("./frameworks-etFg_O8K.js");
|
|
90
|
+
const { readPackageJson } = await import("./project-MFrXcw1M.js");
|
|
84
91
|
const detected = await detectFramework(cwd, await readPackageJson(cwd));
|
|
85
92
|
if (detected !== void 0) return frameworkAdvice(head, detected, cwd, dir);
|
|
86
93
|
return await siblingAdvice(head, cwd, dir) ?? generic(head);
|
|
@@ -91,6 +98,10 @@ async function frameworkAdvice(head, detected, cwd, dir) {
|
|
|
91
98
|
const given = dir.replace(/^\.\//, "");
|
|
92
99
|
for (const output of outputs) if (output !== given && await exists(output, cwd)) return `${head} ${framework.name} writes to ${output}/, not ${dir}.\n Try: eaa-kit audit ./${output}`;
|
|
93
100
|
const lines = [`${head} This is ${article(framework.name)} ${framework.name} project.`];
|
|
101
|
+
if (outputs.length === 0) {
|
|
102
|
+
lines.push(` It renders every page on a server and writes no HTML to disk, so there is`, ` no build directory to audit. Start it, then audit what it serves:`, ...framework.serveCommand === void 0 ? [] : [` ${framework.serveCommand}`], " eaa-kit audit --url http://localhost:8000");
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
94
105
|
if (framework.staticOutput !== void 0) lines.push(` Static output takes ${framework.staticOutput.needs} — ${framework.staticOutput.how},`, ` then: eaa-kit audit ./${outputs[0]}`);
|
|
95
106
|
else if (outputs.length === 1 && outputs[0] === given) lines.push(` That is where it writes, so the build has not run yet — or it failed.`);
|
|
96
107
|
else lines.push(` It writes to ${outputs.map((output) => `${output}/`).join(" or ")}. Run your build, then:`, ` eaa-kit audit ./${outputs[0]}`);
|
|
@@ -99,7 +110,7 @@ async function frameworkAdvice(head, detected, cwd, dir) {
|
|
|
99
110
|
}
|
|
100
111
|
/** Naming a build directory that is actually there beats listing the usual ones. */
|
|
101
112
|
async function siblingAdvice(head, cwd, dir) {
|
|
102
|
-
const { FALLBACK_OUTPUTS } = await import("./frameworks-
|
|
113
|
+
const { FALLBACK_OUTPUTS } = await import("./frameworks-etFg_O8K.js");
|
|
103
114
|
const given = dir.replace(/^\.\//, "");
|
|
104
115
|
const others = (await Promise.all(FALLBACK_OUTPUTS.map(async (name) => name !== given && await exists(name, cwd) ? name : void 0))).filter((name) => name !== void 0);
|
|
105
116
|
if (others.length === 0) return void 0;
|
|
@@ -42,13 +42,13 @@ async function runEngine(pages, options) {
|
|
|
42
42
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
43
43
|
};
|
|
44
44
|
if (!options.browser) {
|
|
45
|
-
const { runPooledAudit } = await import("./pool-
|
|
45
|
+
const { runPooledAudit } = await import("./pool-BMevaLWD.js");
|
|
46
46
|
return runPooledAudit(pages, {
|
|
47
47
|
...runnerOptions,
|
|
48
48
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-
|
|
51
|
+
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-BWniOain.js");
|
|
52
52
|
try {
|
|
53
53
|
return await runBrowserAudit(options.directory, pages, runnerOptions);
|
|
54
54
|
} catch (cause) {
|
|
@@ -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-CKKpzkYM.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
@@ -96,16 +96,51 @@ function searchTermsFor(html) {
|
|
|
96
96
|
* Returns undefined when nothing matches and when more than one does. The
|
|
97
97
|
* second case is the important one: naming a file that merely happens to
|
|
98
98
|
* contain the same path sends somebody to edit the wrong component.
|
|
99
|
+
*
|
|
100
|
+
* The position comes free. The search already has to find the literal to know
|
|
101
|
+
* the file contains it, so recording where it found it costs one more scan of
|
|
102
|
+
* one string — and a report that says `Header.astro:12` opens an editor where a
|
|
103
|
+
* report that says `Header.astro` starts a search.
|
|
99
104
|
*/
|
|
100
105
|
function componentFor(index, html) {
|
|
101
106
|
for (const term of searchTermsFor(html)) {
|
|
102
107
|
const matches = [];
|
|
103
|
-
for (const [file, source] of index.files)
|
|
104
|
-
|
|
105
|
-
if (
|
|
108
|
+
for (const [file, source] of index.files) {
|
|
109
|
+
const offset = source.indexOf(term);
|
|
110
|
+
if (offset !== -1) {
|
|
111
|
+
matches.push({
|
|
112
|
+
file,
|
|
113
|
+
offset
|
|
114
|
+
});
|
|
115
|
+
if (matches.length > 1) break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const only = matches.length === 1 ? matches[0] : void 0;
|
|
119
|
+
if (only !== void 0) {
|
|
120
|
+
const source = index.files.get(only.file);
|
|
121
|
+
return {
|
|
122
|
+
file: only.file,
|
|
123
|
+
...positionOf(source, only.offset)
|
|
124
|
+
};
|
|
106
125
|
}
|
|
107
|
-
if (matches.length === 1) return matches[0];
|
|
108
126
|
}
|
|
109
127
|
}
|
|
128
|
+
/** 1-based line and column of a byte offset in a source file. */
|
|
129
|
+
function positionOf(source, offset) {
|
|
130
|
+
let line = 1;
|
|
131
|
+
let lineStart = 0;
|
|
132
|
+
for (let i = 0; i < offset; i += 1) if (source[i] === "\n") {
|
|
133
|
+
line += 1;
|
|
134
|
+
lineStart = i + 1;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
line,
|
|
138
|
+
column: offset - lineStart + 1
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** `components/Header.astro:12`, as both reports name a component. */
|
|
142
|
+
function componentPath(location) {
|
|
143
|
+
return `${location.file}:${location.line}`;
|
|
144
|
+
}
|
|
110
145
|
//#endregion
|
|
111
|
-
export {
|
|
146
|
+
export { searchTermsFor as i, componentFor as n, componentPath as r, buildComponentIndex as t };
|