eaa-kit 0.2.0 → 0.3.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 +2 -1
- package/dist/astro/index.d.ts +7 -34
- package/dist/astro/index.js +6 -26
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-CuG2hYyo.js +2 -0
- package/dist/audit-DcL73mOC.js +635 -0
- package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
- package/dist/cli/index.js +81 -143
- package/dist/collect-BkAQ0viT.js +123 -0
- package/dist/command-Dxpa00Ha.js +77 -0
- package/dist/component-7kEBjv_y.js +111 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
- package/dist/frameworks-BYa3tULg.js +243 -0
- package/dist/frameworks-DaDqrJOw.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
- package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
- package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
- package/dist/jsdom-X4KYfTp8.js +3 -0
- package/dist/json-B0Y7rNjt.js +2 -0
- package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
- package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
- package/dist/manual-Vz-oX1I_.js +239 -0
- package/dist/{playwright-DWux49V3.js → playwright-DYFsGUNd.js} +79 -21
- package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
- package/dist/{project-CiyzKQud.js → project-DW08TseF.js} +37 -27
- package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
- package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
- package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-BW6CVuND.js +36 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
- package/dist/text-BFmNtMsV.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/package.json +8 -3
- package/dist/audit-CqrR9pIO.js +0 -2
- package/dist/audit-D4gju2BT.js +0 -949
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/jsdom-C6dIyaxN.js +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _ as withDefault, c as object, f as safeParse, i as isoDate$1, l as optional, o as nullable, p as string, s as number, t as array } from "./schema-CMZ8ItGk.js";
|
|
2
2
|
import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
|
|
3
|
+
import { r as findingElements } from "./result-DLxd2Eip.js";
|
|
3
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
/** Default filename, used by the CLI when no path is given. */
|
|
@@ -37,12 +38,10 @@ var BaselineError = class extends Error {
|
|
|
37
38
|
*/
|
|
38
39
|
function applyBaseline(audits, baseline, options = {}) {
|
|
39
40
|
const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
|
|
40
|
-
const expired =
|
|
41
|
+
const expired = [];
|
|
41
42
|
const live = /* @__PURE__ */ new Map();
|
|
42
|
-
for (const entry of baseline.entries)
|
|
43
|
-
|
|
44
|
-
live.set(key(entry.page, entry.ruleId, entry.fingerprint), entry);
|
|
45
|
-
}
|
|
43
|
+
for (const entry of baseline.entries) if (entry.expiresOn !== void 0 && entry.expiresOn < today) expired.push(entry);
|
|
44
|
+
else live.set(key(entry.page, entry.ruleId, entry.fingerprint), entry);
|
|
46
45
|
const matched = /* @__PURE__ */ new Set();
|
|
47
46
|
let accepted = 0;
|
|
48
47
|
const next = audits.map((audit) => {
|
|
@@ -90,7 +89,7 @@ function applyBaseline(audits, baseline, options = {}) {
|
|
|
90
89
|
return {
|
|
91
90
|
audits: next,
|
|
92
91
|
stale: [...live.entries()].filter(([id, entry]) => !matched.has(id) && audited.has(entry.page)).map(([, entry]) => entry).sort(byEntry),
|
|
93
|
-
expired:
|
|
92
|
+
expired: expired.sort(byEntry),
|
|
94
93
|
accepted
|
|
95
94
|
};
|
|
96
95
|
}
|
|
@@ -98,26 +97,17 @@ function applyBaseline(audits, baseline, options = {}) {
|
|
|
98
97
|
function buildBaseline(audits, options = {}) {
|
|
99
98
|
const acceptedOn = isoDate(options.today ?? /* @__PURE__ */ new Date());
|
|
100
99
|
const entries = [];
|
|
101
|
-
for (const audit of audits) for (const finding of audit.violations) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fingerprint: elementFingerprint(finding.ruleId, node.selector, node.html),
|
|
113
|
-
selector: node.selector,
|
|
114
|
-
help: finding.help,
|
|
115
|
-
impact: finding.impact ?? null,
|
|
116
|
-
acceptedOn,
|
|
117
|
-
...options.expiresOn ? { expiresOn: options.expiresOn } : {},
|
|
118
|
-
...options.note ? { note: options.note } : {}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
100
|
+
for (const audit of audits) for (const finding of audit.violations) for (const node of findingElements(finding)) entries.push({
|
|
101
|
+
page: audit.relativePath,
|
|
102
|
+
ruleId: finding.ruleId,
|
|
103
|
+
fingerprint: elementFingerprint(finding.ruleId, node.selector, node.html),
|
|
104
|
+
selector: node.selector,
|
|
105
|
+
help: finding.help,
|
|
106
|
+
impact: finding.impact ?? null,
|
|
107
|
+
acceptedOn,
|
|
108
|
+
...options.expiresOn ? { expiresOn: options.expiresOn } : {},
|
|
109
|
+
...options.note ? { note: options.note } : {}
|
|
110
|
+
});
|
|
121
111
|
return {
|
|
122
112
|
schemaVersion: 1,
|
|
123
113
|
createdOn: acceptedOn,
|
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { l as STATEMENT_LOCALES, o as COUNTRIES, r as loadConfig, s as ConfigError } from "../load-
|
|
3
|
-
import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-
|
|
4
|
-
import {
|
|
2
|
+
import { l as STATEMENT_LOCALES, o as COUNTRIES, r as loadConfig, s as ConfigError } from "../load-UYXLqGV9.js";
|
|
3
|
+
import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-DI_aCnAZ.js";
|
|
4
|
+
import { n as IMPACT_LEVELS, t as DEFAULT_FAIL_ON } from "../impact-YdoOtFqm.js";
|
|
5
|
+
import { n as count } from "../text-BFmNtMsV.js";
|
|
5
6
|
import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
|
|
6
|
-
import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-s9F3fXTN.js";
|
|
8
|
+
import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-DcL73mOC.js";
|
|
9
|
+
import { a as runEngine, i as note, n as emitDocument, r as fail, t as advise } from "../command-Dxpa00Ha.js";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { Command, InvalidArgumentError } from "commander";
|
|
11
|
-
import pc from "picocolors";
|
|
12
12
|
//#region src/cli/baseline.ts
|
|
13
13
|
/**
|
|
14
14
|
* `eaa-kit baseline [dir]`.
|
|
@@ -33,37 +33,23 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
33
33
|
exitCode: 2
|
|
34
34
|
};
|
|
35
35
|
const { pages, origin, label } = resolved;
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
...
|
|
36
|
+
note(`Auditing ${count(pages.length, "page")} in ${label}…`);
|
|
37
|
+
const baseUrl = options.baseUrl ?? origin;
|
|
38
|
+
const audits = await runEngine(pages, {
|
|
39
|
+
cwd,
|
|
40
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
41
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
42
|
+
...options.browser ? { browser: true } : {},
|
|
43
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
44
|
+
...options.url === void 0 ? { directory: path.resolve(cwd, dir) } : {}
|
|
45
|
+
});
|
|
46
|
+
if (!audits) return {
|
|
47
|
+
entries: 0,
|
|
48
|
+
exitCode: 2
|
|
41
49
|
};
|
|
42
|
-
let audits;
|
|
43
|
-
if (options.browser) {
|
|
44
|
-
const { BrowserUnavailableError, runBrowserAudit } = await import("../playwright-DWux49V3.js");
|
|
45
|
-
try {
|
|
46
|
-
audits = await runBrowserAudit(options.url === void 0 ? path.resolve(cwd, dir) : void 0, pages, runnerOptions);
|
|
47
|
-
} catch (cause) {
|
|
48
|
-
if (cause instanceof BrowserUnavailableError) {
|
|
49
|
-
process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
|
|
50
|
-
return {
|
|
51
|
-
entries: 0,
|
|
52
|
-
exitCode: 2
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
throw cause;
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
const { runPooledAudit } = await import("../pool-DixLeu8L.js");
|
|
59
|
-
audits = await runPooledAudit(pages, {
|
|
60
|
-
...runnerOptions,
|
|
61
|
-
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
50
|
const unaudited = audits.filter((audit) => audit.error);
|
|
65
51
|
if (unaudited.length > 0) {
|
|
66
|
-
|
|
52
|
+
fail(`${unaudited.length} of ${audits.length} pages could not be audited, so no baseline was written`);
|
|
67
53
|
return {
|
|
68
54
|
entries: 0,
|
|
69
55
|
exitCode: 2
|
|
@@ -78,7 +64,7 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
78
64
|
await writeBaseline(target, baseline, cwd);
|
|
79
65
|
} catch (cause) {
|
|
80
66
|
if (cause instanceof BaselineError) {
|
|
81
|
-
|
|
67
|
+
fail(cause.message);
|
|
82
68
|
return {
|
|
83
69
|
entries: 0,
|
|
84
70
|
exitCode: 2
|
|
@@ -86,11 +72,11 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
86
72
|
}
|
|
87
73
|
throw cause;
|
|
88
74
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (
|
|
75
|
+
const entries = baseline.entries.length;
|
|
76
|
+
note(`Wrote ${entries} ${entries === 1 ? "entry" : "entries"} to ${target}`);
|
|
77
|
+
if (entries > 0) advise("These are barriers, not exceptions. Commit the file, then work the list down.");
|
|
92
78
|
return {
|
|
93
|
-
entries
|
|
79
|
+
entries,
|
|
94
80
|
exitCode: 0
|
|
95
81
|
};
|
|
96
82
|
}
|
|
@@ -98,9 +84,6 @@ async function runBaselineCommand(dir, options = {}) {
|
|
|
98
84
|
//#region src/cli/statement.ts
|
|
99
85
|
/** Markdown for a content directory, HTML for dropping straight onto a site. */
|
|
100
86
|
const STATEMENT_FORMATS = ["markdown", "html"];
|
|
101
|
-
function isStatementFormat(value) {
|
|
102
|
-
return STATEMENT_FORMATS.includes(value);
|
|
103
|
-
}
|
|
104
87
|
/**
|
|
105
88
|
* `eaa-kit statement`.
|
|
106
89
|
*
|
|
@@ -121,19 +104,16 @@ async function runStatementCommand(options = {}) {
|
|
|
121
104
|
...options.country ? { country: options.country } : {},
|
|
122
105
|
...audit ? { audit } : {}
|
|
123
106
|
});
|
|
124
|
-
|
|
107
|
+
note(`Statement for ${config.site.url} from ${path.basename(configPath)} (${statement.template}, ${format})`);
|
|
125
108
|
if (audit) {
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
const from = path.basename(options.audit ?? "");
|
|
110
|
+
note(`${count(audit.findings.length, "barrier")} taken from ${from}`);
|
|
111
|
+
if (audit.findings.length > 0) advise("Rewrite those descriptions in your own words before publishing.");
|
|
128
112
|
}
|
|
129
113
|
const document = format === "html" ? statement.html : statement.markdown;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await writeFile(target, document, "utf8");
|
|
134
|
-
process.stderr.write(pc.dim(`Written to ${options.output}\n`));
|
|
135
|
-
} else process.stdout.write(document);
|
|
136
|
-
process.stderr.write(pc.yellow("Review before publishing. This is a draft, not legal advice.\n"));
|
|
114
|
+
await emitDocument(document, options.output, options.cwd ?? process.cwd());
|
|
115
|
+
if (options.output !== void 0) note(`Written to ${options.output}`);
|
|
116
|
+
advise("Review before publishing. This is a draft, not legal advice.");
|
|
137
117
|
return {
|
|
138
118
|
document,
|
|
139
119
|
format,
|
|
@@ -141,8 +121,8 @@ async function runStatementCommand(options = {}) {
|
|
|
141
121
|
};
|
|
142
122
|
} catch (cause) {
|
|
143
123
|
if (cause instanceof ConfigError || cause instanceof StatementError) {
|
|
144
|
-
|
|
145
|
-
if (cause instanceof ConfigError) for (const issue of cause.issues)
|
|
124
|
+
fail(cause.message);
|
|
125
|
+
if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
|
|
146
126
|
return {
|
|
147
127
|
document: "",
|
|
148
128
|
format,
|
|
@@ -162,109 +142,67 @@ function formatFor(output) {
|
|
|
162
142
|
}
|
|
163
143
|
//#endregion
|
|
164
144
|
//#region src/cli/index.ts
|
|
145
|
+
/**
|
|
146
|
+
* A parser for a fixed set of words, listing them when the answer is not one.
|
|
147
|
+
*
|
|
148
|
+
* `normalise` is for `--country de`, where the accepted spelling differs from
|
|
149
|
+
* what somebody types.
|
|
150
|
+
*/
|
|
151
|
+
function oneOf(values, normalise = (value) => value) {
|
|
152
|
+
return (value) => {
|
|
153
|
+
const candidate = normalise(value);
|
|
154
|
+
if (!values.includes(candidate)) throw new InvalidArgumentError(`expected one of ${values.join(", ")}`);
|
|
155
|
+
return candidate;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** A whole number no smaller than `min`. Depth allows 0: audit the entry page alone. */
|
|
159
|
+
function wholeNumber(min) {
|
|
160
|
+
return (value) => {
|
|
161
|
+
const parsed = Number(value);
|
|
162
|
+
if (!Number.isInteger(parsed) || parsed < min) throw new InvalidArgumentError(`expected a whole number of ${min} or more`);
|
|
163
|
+
return parsed;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function parseDate(value) {
|
|
167
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`))) throw new InvalidArgumentError("expected an ISO date, e.g. 2026-12-31");
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
const parseLocale = oneOf(STATEMENT_LOCALES);
|
|
171
|
+
const parseCountry = oneOf(COUNTRIES, (value) => value.toUpperCase());
|
|
172
|
+
const parseStatementFormat = oneOf(STATEMENT_FORMATS);
|
|
173
|
+
const parseImpact = oneOf(IMPACT_LEVELS);
|
|
174
|
+
const parseFormat = oneOf(OUTPUT_FORMATS);
|
|
175
|
+
const parsePositive = wholeNumber(1);
|
|
176
|
+
const parseDepth = wholeNumber(0);
|
|
177
|
+
const parseConcurrency = wholeNumber(1);
|
|
165
178
|
const program = new Command();
|
|
166
179
|
program.exitOverride();
|
|
167
180
|
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");
|
|
168
|
-
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("--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,
|
|
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) => {
|
|
182
|
+
const { build, ...options } = flags;
|
|
169
183
|
const { exitCode } = await runAuditCommand(dir, {
|
|
170
|
-
...options
|
|
171
|
-
...
|
|
172
|
-
...Array.isArray(options["include"]) ? { include: options["include"] } : {},
|
|
173
|
-
...Array.isArray(options["exclude"]) ? { exclude: options["exclude"] } : {},
|
|
174
|
-
...typeof options["baseUrl"] === "string" ? { baseUrl: options["baseUrl"] } : {},
|
|
175
|
-
failOn: options["failOn"],
|
|
176
|
-
format: options["format"],
|
|
177
|
-
...typeof options["output"] === "string" ? { output: options["output"] } : {},
|
|
178
|
-
...options["browser"] === true ? { browser: true } : {},
|
|
179
|
-
...typeof options["concurrency"] === "number" ? { concurrency: options["concurrency"] } : {},
|
|
180
|
-
...typeof options["baseline"] === "string" ? { baseline: options["baseline"] } : {},
|
|
181
|
-
...typeof options["url"] === "string" ? { url: options["url"] } : {},
|
|
182
|
-
...options["allowRemote"] === true ? { allowRemote: true } : {},
|
|
183
|
-
...options["ignoreRobots"] === true ? { ignoreRobots: true } : {},
|
|
184
|
-
...typeof options["maxPages"] === "number" ? { maxPages: options["maxPages"] } : {},
|
|
185
|
-
...typeof options["maxDepth"] === "number" ? { maxDepth: options["maxDepth"] } : {}
|
|
184
|
+
...options,
|
|
185
|
+
...build === false ? { noBuild: true } : {}
|
|
186
186
|
});
|
|
187
187
|
process.exitCode = exitCode;
|
|
188
188
|
});
|
|
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,
|
|
190
|
-
const { exitCode } = await runBaselineCommand(dir,
|
|
191
|
-
...Array.isArray(options["include"]) ? { include: options["include"] } : {},
|
|
192
|
-
...Array.isArray(options["exclude"]) ? { exclude: options["exclude"] } : {},
|
|
193
|
-
...typeof options["baseUrl"] === "string" ? { baseUrl: options["baseUrl"] } : {},
|
|
194
|
-
...typeof options["output"] === "string" ? { output: options["output"] } : {},
|
|
195
|
-
...typeof options["note"] === "string" ? { note: options["note"] } : {},
|
|
196
|
-
...typeof options["expiresOn"] === "string" ? { expiresOn: options["expiresOn"] } : {},
|
|
197
|
-
...options["browser"] === true ? { browser: true } : {},
|
|
198
|
-
...typeof options["concurrency"] === "number" ? { concurrency: options["concurrency"] } : {},
|
|
199
|
-
...typeof options["url"] === "string" ? { url: options["url"] } : {},
|
|
200
|
-
...options["allowRemote"] === true ? { allowRemote: true } : {},
|
|
201
|
-
...options["ignoreRobots"] === true ? { ignoreRobots: true } : {},
|
|
202
|
-
...typeof options["maxPages"] === "number" ? { maxPages: options["maxPages"] } : {},
|
|
203
|
-
...typeof options["maxDepth"] === "number" ? { maxDepth: options["maxDepth"] } : {}
|
|
204
|
-
});
|
|
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) => {
|
|
190
|
+
const { exitCode } = await runBaselineCommand(dir, flags);
|
|
205
191
|
process.exitCode = exitCode;
|
|
206
192
|
});
|
|
207
|
-
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 (
|
|
208
|
-
const { runInitCommand } = await import("../init-
|
|
209
|
-
const { exitCode } = await runInitCommand(
|
|
210
|
-
...typeof options["output"] === "string" ? { output: options["output"] } : {},
|
|
211
|
-
...options["force"] === true ? { force: true } : {},
|
|
212
|
-
...options["yes"] === true ? { yes: true } : {}
|
|
213
|
-
});
|
|
193
|
+
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-DiLiYvN1.js");
|
|
195
|
+
const { exitCode } = await runInitCommand(flags);
|
|
214
196
|
process.exitCode = exitCode;
|
|
215
197
|
});
|
|
216
|
-
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 (
|
|
198
|
+
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) => {
|
|
199
|
+
const { lang, ...options } = flags;
|
|
217
200
|
const { exitCode } = await runStatementCommand({
|
|
218
|
-
...
|
|
219
|
-
...
|
|
220
|
-
...options["country"] ? { country: options["country"] } : {},
|
|
221
|
-
...typeof options["audit"] === "string" ? { audit: options["audit"] } : {},
|
|
222
|
-
...options["format"] ? { format: options["format"] } : {},
|
|
223
|
-
...typeof options["output"] === "string" ? { output: options["output"] } : {}
|
|
201
|
+
...options,
|
|
202
|
+
...lang === void 0 ? {} : { locale: lang }
|
|
224
203
|
});
|
|
225
204
|
process.exitCode = exitCode;
|
|
226
205
|
});
|
|
227
|
-
function parseLocale(value) {
|
|
228
|
-
if (!STATEMENT_LOCALES.includes(value)) throw new InvalidArgumentError(`expected one of ${STATEMENT_LOCALES.join(", ")}`);
|
|
229
|
-
return value;
|
|
230
|
-
}
|
|
231
|
-
function parseCountry(value) {
|
|
232
|
-
const upper = value.toUpperCase();
|
|
233
|
-
if (!COUNTRIES.includes(upper)) throw new InvalidArgumentError(`expected one of ${COUNTRIES.join(", ")}`);
|
|
234
|
-
return upper;
|
|
235
|
-
}
|
|
236
|
-
function parseStatementFormat(value) {
|
|
237
|
-
if (!isStatementFormat(value)) throw new InvalidArgumentError(`expected one of ${STATEMENT_FORMATS.join(", ")}`);
|
|
238
|
-
return value;
|
|
239
|
-
}
|
|
240
|
-
function parseDate(value) {
|
|
241
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`))) throw new InvalidArgumentError("expected an ISO date, e.g. 2026-12-31");
|
|
242
|
-
return value;
|
|
243
|
-
}
|
|
244
|
-
function parsePositive(value) {
|
|
245
|
-
const parsed = Number(value);
|
|
246
|
-
if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("expected a whole number of 1 or more");
|
|
247
|
-
return parsed;
|
|
248
|
-
}
|
|
249
|
-
/** Depth 0 is meaningful here: audit only the entry page. */
|
|
250
|
-
function parseDepth(value) {
|
|
251
|
-
const parsed = Number(value);
|
|
252
|
-
if (!Number.isInteger(parsed) || parsed < 0) throw new InvalidArgumentError("expected a whole number of 0 or more");
|
|
253
|
-
return parsed;
|
|
254
|
-
}
|
|
255
|
-
function parseConcurrency(value) {
|
|
256
|
-
const parsed = Number(value);
|
|
257
|
-
if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("expected a whole number of 1 or more");
|
|
258
|
-
return parsed;
|
|
259
|
-
}
|
|
260
|
-
function parseImpact(value) {
|
|
261
|
-
if (!isImpactLevel(value)) throw new InvalidArgumentError(`expected one of ${IMPACT_LEVELS.join(", ")}`);
|
|
262
|
-
return value;
|
|
263
|
-
}
|
|
264
|
-
function parseFormat(value) {
|
|
265
|
-
if (!isOutputFormat(value)) throw new InvalidArgumentError(`expected one of ${OUTPUT_FORMATS.join(", ")}`);
|
|
266
|
-
return value;
|
|
267
|
-
}
|
|
268
206
|
try {
|
|
269
207
|
await program.parseAsync(process.argv);
|
|
270
208
|
} catch (cause) {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { i as toPosix, t as exists } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
5
|
+
//#region src/audit/collect.ts
|
|
6
|
+
/** Every HTML document a static build is expected to emit. */
|
|
7
|
+
const DEFAULT_INCLUDE = ["**/*.html", "**/*.htm"];
|
|
8
|
+
/** Vendored and tooling directories are never part of the shipped site. */
|
|
9
|
+
const DEFAULT_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
|
|
10
|
+
/** Number of files read in parallel; keeps large builds under the fd limit. */
|
|
11
|
+
const READ_CONCURRENCY = 24;
|
|
12
|
+
/** A UTF-8 byte-order mark is not markup, and jsdom treats it as text. */
|
|
13
|
+
function stripBom(html) {
|
|
14
|
+
return html.charCodeAt(0) === 65279 ? html.slice(1) : html;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when the build directory itself is unusable. A missing or wrong
|
|
18
|
+
* `dist/` is a user mistake worth reporting loudly, unlike a directory that
|
|
19
|
+
* simply holds no HTML.
|
|
20
|
+
*/
|
|
21
|
+
var BuildDirectoryError = class extends Error {
|
|
22
|
+
dir;
|
|
23
|
+
name = "BuildDirectoryError";
|
|
24
|
+
constructor(message, dir) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.dir = dir;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Glob HTML files out of a build directory and read them.
|
|
31
|
+
*
|
|
32
|
+
* Returns pages sorted by relative path so reports and snapshots are stable
|
|
33
|
+
* across platforms. An empty array means "no HTML found" — the caller decides
|
|
34
|
+
* whether that is an error.
|
|
35
|
+
*/
|
|
36
|
+
async function collectPages(dir, options = {}) {
|
|
37
|
+
const root = path.resolve(dir);
|
|
38
|
+
await assertDirectory(root, dir);
|
|
39
|
+
const relativePaths = (await glob(options.include ?? DEFAULT_INCLUDE, {
|
|
40
|
+
cwd: root,
|
|
41
|
+
ignore: options.exclude ?? DEFAULT_EXCLUDE,
|
|
42
|
+
onlyFiles: true,
|
|
43
|
+
dot: false,
|
|
44
|
+
absolute: false
|
|
45
|
+
})).map(toPosix).sort();
|
|
46
|
+
const pages = [];
|
|
47
|
+
for (let i = 0; i < relativePaths.length; i += READ_CONCURRENCY) {
|
|
48
|
+
const batch = relativePaths.slice(i, i + READ_CONCURRENCY);
|
|
49
|
+
pages.push(...await Promise.all(batch.map((relativePath) => readPage(root, relativePath))));
|
|
50
|
+
}
|
|
51
|
+
return pages;
|
|
52
|
+
}
|
|
53
|
+
async function assertDirectory(root, original) {
|
|
54
|
+
let stats;
|
|
55
|
+
try {
|
|
56
|
+
stats = await stat(root);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
if (cause.code === "ENOENT") throw new BuildDirectoryError(`Build directory not found: ${original}`, root);
|
|
59
|
+
throw new BuildDirectoryError(`Build directory is not readable: ${original} (${cause.message})`, root);
|
|
60
|
+
}
|
|
61
|
+
if (!stats.isDirectory()) throw new BuildDirectoryError(`Build path is not a directory: ${original}`, root);
|
|
62
|
+
}
|
|
63
|
+
async function readPage(root, relativePath) {
|
|
64
|
+
const absolutePath = path.join(root, relativePath);
|
|
65
|
+
return {
|
|
66
|
+
absolutePath,
|
|
67
|
+
relativePath,
|
|
68
|
+
html: stripBom(await readFile(absolutePath, "utf8"))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* What to suggest when a directory holds no HTML, or is not there at all.
|
|
73
|
+
*
|
|
74
|
+
* Nearly always the wrong directory rather than a site with no pages, and the
|
|
75
|
+
* commonest way to arrive is a framework whose build emits no browsable HTML —
|
|
76
|
+
* so rather than repeating "check the path", this works out what the project is
|
|
77
|
+
* and names the next step for it. Where static output is not possible at all,
|
|
78
|
+
* that step is `--url` rather than advice that cannot apply.
|
|
79
|
+
*/
|
|
80
|
+
async function emptyDirectoryHint(dir, cwd = process.cwd()) {
|
|
81
|
+
const head = await exists(dir, cwd) ? `${dir} holds no HTML files.` : `${dir} does not exist.`;
|
|
82
|
+
const { detectFramework } = await import("./frameworks-DaDqrJOw.js");
|
|
83
|
+
const { readPackageJson } = await import("./project-DW08TseF.js");
|
|
84
|
+
const detected = await detectFramework(cwd, await readPackageJson(cwd));
|
|
85
|
+
if (detected !== void 0) return frameworkAdvice(head, detected, cwd, dir);
|
|
86
|
+
return await siblingAdvice(head, cwd, dir) ?? generic(head);
|
|
87
|
+
}
|
|
88
|
+
/** Advice built from what the registry knows about this framework. */
|
|
89
|
+
async function frameworkAdvice(head, detected, cwd, dir) {
|
|
90
|
+
const { framework, outputs } = detected;
|
|
91
|
+
const given = dir.replace(/^\.\//, "");
|
|
92
|
+
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
|
+
const lines = [`${head} This is ${article(framework.name)} ${framework.name} project.`];
|
|
94
|
+
if (framework.staticOutput !== void 0) lines.push(` Static output takes ${framework.staticOutput.needs} — ${framework.staticOutput.how},`, ` then: eaa-kit audit ./${outputs[0]}`);
|
|
95
|
+
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
|
+
else lines.push(` It writes to ${outputs.map((output) => `${output}/`).join(" or ")}. Run your build, then:`, ` eaa-kit audit ./${outputs[0]}`);
|
|
97
|
+
if (framework.serves) lines.push(" A site that renders on a server cannot be written to disk at all. Audit it", " running instead:", " eaa-kit audit --url http://localhost:3000");
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
/** Naming a build directory that is actually there beats listing the usual ones. */
|
|
101
|
+
async function siblingAdvice(head, cwd, dir) {
|
|
102
|
+
const { FALLBACK_OUTPUTS } = await import("./frameworks-DaDqrJOw.js");
|
|
103
|
+
const given = dir.replace(/^\.\//, "");
|
|
104
|
+
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
|
+
if (others.length === 0) return void 0;
|
|
106
|
+
return `${head} This project also has ${others.map((name) => `${name}/`).join(", ")} — try one of those.`;
|
|
107
|
+
}
|
|
108
|
+
function generic(head) {
|
|
109
|
+
return `${head} Point eaa-kit at the directory your build fills with .html files —\n commonly dist/, build/, out/ or _site/, depending on the builder.
|
|
110
|
+
If your site renders on a server and never writes HTML, audit it running:
|
|
111
|
+
eaa-kit audit --url http://localhost:3000`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* "a Next.js project", "an Eleventy project".
|
|
115
|
+
*
|
|
116
|
+
* By letter rather than by sound, which is wrong for words like "hour" and
|
|
117
|
+
* right for every framework name in the registry.
|
|
118
|
+
*/
|
|
119
|
+
function article(name) {
|
|
120
|
+
return /^[aeiou]/i.test(name) ? "an" : "a";
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { stripBom as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
//#region src/cli/command.ts
|
|
5
|
+
/**
|
|
6
|
+
* What every command does around the audit itself: say what is happening, run
|
|
7
|
+
* the engine, and put the document somewhere.
|
|
8
|
+
*
|
|
9
|
+
* `audit` and `baseline` are siblings — one reports what a run found and the
|
|
10
|
+
* other writes it down — so they take the same flags, choose between the same
|
|
11
|
+
* two engines and fail on the same setup problems. Keeping that in one place is
|
|
12
|
+
* what stops the two commands drifting into disagreeing about what a run is.
|
|
13
|
+
*/
|
|
14
|
+
/** Progress and diagnostics go to stderr, so the report can be piped away. */
|
|
15
|
+
function note(message) {
|
|
16
|
+
process.stderr.write(pc.dim(`${message}\n`));
|
|
17
|
+
}
|
|
18
|
+
function warn(message) {
|
|
19
|
+
process.stderr.write(`${pc.yellow("warning")} ${message}\n`);
|
|
20
|
+
}
|
|
21
|
+
function fail(message) {
|
|
22
|
+
process.stderr.write(`${pc.red("error")} ${message}\n`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Advice the reader should not miss, without the `warning` prefix: nothing has
|
|
26
|
+
* gone wrong, but what happens next is theirs to get right.
|
|
27
|
+
*/
|
|
28
|
+
function advise(message) {
|
|
29
|
+
process.stderr.write(pc.yellow(`${message}\n`));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Audit the pages with whichever engine was asked for.
|
|
33
|
+
*
|
|
34
|
+
* Returns undefined when the browser was asked for and is not usable, having
|
|
35
|
+
* already said so: Playwright missing is a setup problem with a specific fix,
|
|
36
|
+
* not a crash, and both commands turn it into exit 2.
|
|
37
|
+
*/
|
|
38
|
+
async function runEngine(pages, options) {
|
|
39
|
+
const runnerOptions = {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
|
|
42
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
43
|
+
};
|
|
44
|
+
if (!options.browser) {
|
|
45
|
+
const { runPooledAudit } = await import("./pool-BWkWZiJW.js");
|
|
46
|
+
return runPooledAudit(pages, {
|
|
47
|
+
...runnerOptions,
|
|
48
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-DYFsGUNd.js");
|
|
52
|
+
try {
|
|
53
|
+
return await runBrowserAudit(options.directory, pages, runnerOptions);
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
if (cause instanceof BrowserUnavailableError) {
|
|
56
|
+
fail(cause.message);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw cause;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Write a document to `output`, or to stdout when there is none. Parent
|
|
64
|
+
* directories are created, since a report path in CI usually names one that is
|
|
65
|
+
* not there yet.
|
|
66
|
+
*/
|
|
67
|
+
async function emitDocument(body, output, cwd) {
|
|
68
|
+
if (output === void 0) {
|
|
69
|
+
process.stdout.write(body);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const target = path.resolve(cwd, output);
|
|
73
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
74
|
+
await writeFile(target, body, "utf8");
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { runEngine as a, note as i, emitDocument as n, warn as o, fail as r, advise as t };
|