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.
Files changed (43) hide show
  1. package/README.md +2 -1
  2. package/dist/astro/index.d.ts +7 -34
  3. package/dist/astro/index.js +6 -26
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-CuG2hYyo.js +2 -0
  6. package/dist/audit-DcL73mOC.js +635 -0
  7. package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
  8. package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
  9. package/dist/cli/index.js +81 -143
  10. package/dist/collect-BkAQ0viT.js +123 -0
  11. package/dist/command-Dxpa00Ha.js +77 -0
  12. package/dist/component-7kEBjv_y.js +111 -0
  13. package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
  14. package/dist/frameworks-BYa3tULg.js +243 -0
  15. package/dist/frameworks-DaDqrJOw.js +2 -0
  16. package/dist/fs-BmPtmFke.js +40 -0
  17. package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
  18. package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
  19. package/dist/index.js +2 -2
  20. package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
  21. package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
  22. package/dist/jsdom-X4KYfTp8.js +3 -0
  23. package/dist/json-B0Y7rNjt.js +2 -0
  24. package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
  25. package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
  26. package/dist/manual-Vz-oX1I_.js +239 -0
  27. package/dist/{playwright-DWux49V3.js → playwright-DYFsGUNd.js} +79 -21
  28. package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
  29. package/dist/{project-CiyzKQud.js → project-DW08TseF.js} +37 -27
  30. package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
  31. package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
  32. package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
  33. package/dist/run-BMASMmwO.d.ts +45 -0
  34. package/dist/run-BW6CVuND.js +36 -0
  35. package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
  36. package/dist/text-BFmNtMsV.js +43 -0
  37. package/dist/vite/index.d.ts +39 -0
  38. package/dist/vite/index.js +40 -0
  39. package/package.json +8 -3
  40. package/dist/audit-CqrR9pIO.js +0 -2
  41. package/dist/audit-D4gju2BT.js +0 -949
  42. package/dist/escape-Dm1o_RAk.js +0 -21
  43. 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 = baseline.entries.filter((entry) => entry.expiresOn !== void 0 && entry.expiresOn < today);
41
+ const expired = [];
41
42
  const live = /* @__PURE__ */ new Map();
42
- for (const entry of baseline.entries) {
43
- if (entry.expiresOn !== void 0 && entry.expiresOn < today) continue;
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: [...expired].sort(byEntry),
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
- const nodes = finding.nodes.length > 0 ? finding.nodes.map((node) => ({
103
- selector: node.target.join(" "),
104
- html: node.html
105
- })) : [{
106
- selector: "",
107
- html: ""
108
- }];
109
- for (const node of nodes) entries.push({
110
- page: audit.relativePath,
111
- ruleId: finding.ruleId,
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-sCkKsvGQ.js";
3
- import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-BO0nVrrZ.js";
4
- import { i as isImpactLevel, n as IMPACT_LEVELS, t as DEFAULT_FAIL_ON } from "../impact-DvgBjupx.js";
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-CgBmzFTr.js";
7
- import { i as resolvePages, n as isOutputFormat, r as runAuditCommand, t as OUTPUT_FORMATS } from "../audit-D4gju2BT.js";
8
- import { mkdir, writeFile } from "node:fs/promises";
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
- process.stderr.write(pc.dim(`Auditing ${pages.length} ${pages.length === 1 ? "page" : "pages"} in ${label}…\n`));
37
- const effectiveBaseUrl = options.baseUrl ?? origin;
38
- const runnerOptions = {
39
- ...effectiveBaseUrl === void 0 ? {} : { baseUrl: effectiveBaseUrl },
40
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
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
- process.stderr.write(`${pc.red("error")} ${unaudited.length} of ${audits.length} pages could not be audited, so no baseline was written\n`);
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
- process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
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 count = baseline.entries.length;
90
- process.stderr.write(pc.dim(`Wrote ${count} ${count === 1 ? "entry" : "entries"} to ${target}\n`));
91
- if (count > 0) process.stderr.write(pc.yellow("These are barriers, not exceptions. Commit the file, then work the list down.\n"));
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: count,
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
- process.stderr.write(pc.dim(`Statement for ${config.site.url} from ${path.basename(configPath)} (${statement.template}, ${format})\n`));
107
+ note(`Statement for ${config.site.url} from ${path.basename(configPath)} (${statement.template}, ${format})`);
125
108
  if (audit) {
126
- process.stderr.write(pc.dim(`${audit.findings.length} ${audit.findings.length === 1 ? "barrier" : "barriers"} taken from ${path.basename(options.audit ?? "")}\n`));
127
- if (audit.findings.length > 0) process.stderr.write(pc.yellow("Rewrite those descriptions in your own words before publishing.\n"));
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
- if (options.output) {
131
- const target = path.resolve(options.cwd ?? process.cwd(), options.output);
132
- await mkdir(path.dirname(target), { recursive: true });
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
- process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
145
- if (cause instanceof ConfigError) for (const issue of cause.issues) process.stderr.write(pc.dim(` ${issue}\n`));
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, options) => {
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["build"] === false ? { noBuild: true } : {},
171
- ...options["perPage"] === true ? { perPage: true } : {},
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, options) => {
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 (options) => {
208
- const { runInitCommand } = await import("../init-DRKIdpK1.js");
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 (options) => {
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
- ...typeof options["config"] === "string" ? { config: options["config"] } : {},
219
- ...options["lang"] ? { locale: options["lang"] } : {},
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 };