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,4 +1,4 @@
1
- import { stat } from "node:fs/promises";
1
+ import { i as toPosix, n as isDirectory } from "./fs-BmPtmFke.js";
2
2
  import path from "node:path";
3
3
  import { glob } from "tinyglobby";
4
4
  //#region src/audit/routes.ts
@@ -49,7 +49,7 @@ const CONVENTIONS = [
49
49
  * without knowing the data behind it.
50
50
  */
51
51
  function routePathFor(relativeFile, framework) {
52
- let route = relativeFile.split(path.sep).join("/");
52
+ let route = toPosix(relativeFile);
53
53
  if (framework === "next-app" || framework === "sveltekit") route = route.includes("/") ? route.replace(/\/[^/]+$/, "") : "";
54
54
  else {
55
55
  route = route.replace(/\.[^./]+$/, "");
@@ -90,11 +90,7 @@ function emittedPathsFor(route) {
90
90
  async function buildRouteMap(cwd) {
91
91
  for (const convention of CONVENTIONS) {
92
92
  const directory = path.join(cwd, convention.dir);
93
- try {
94
- if (!(await stat(directory)).isDirectory()) continue;
95
- } catch {
96
- continue;
97
- }
93
+ if (!await isDirectory(directory)) continue;
98
94
  const files = await glob([convention.pattern], {
99
95
  cwd: directory,
100
96
  ignore: ["**/node_modules/**"],
@@ -105,7 +101,7 @@ async function buildRouteMap(cwd) {
105
101
  for (const file of files) {
106
102
  const route = routePathFor(file, convention.framework);
107
103
  if (route === void 0) continue;
108
- const source = `${convention.dir}/${file.split(path.sep).join("/")}`;
104
+ const source = `${convention.dir}/${toPosix(file)}`;
109
105
  for (const emitted of emittedPathsFor(route)) if (!sources.has(emitted)) sources.set(emitted, source);
110
106
  }
111
107
  if (sources.size > 0) return {
@@ -0,0 +1,45 @@
1
+ import { t as ImpactLevel } from "./impact-EEB9ZXmC.js";
2
+ //#region src/cli/audit.d.ts
3
+ declare const OUTPUT_FORMATS: readonly ['console', 'json', 'sarif', 'html'];
4
+ type OutputFormat = (typeof OUTPUT_FORMATS)[number];
5
+ //#endregion
6
+ //#region src/integration/run.d.ts
7
+ /**
8
+ * What a build-time integration does once the build has written its files.
9
+ *
10
+ * Astro, Vite and Next all reach the same point by different routes: a
11
+ * directory exists, and the build should stop if what is in it fails the
12
+ * threshold. Only the hook name and the logger differ, so only those live in
13
+ * the integrations.
14
+ */
15
+ interface IntegrationOptions {
16
+ /** Lowest impact that fails the build. Defaults to 'serious'. */
17
+ failOn?: ImpactLevel;
18
+ include?: string[];
19
+ exclude?: string[];
20
+ baseUrl?: string;
21
+ /** Audit in real Chromium. Needs the playwright peer. */
22
+ browser?: boolean;
23
+ concurrency?: number;
24
+ baseline?: string;
25
+ format?: OutputFormat;
26
+ /** Write the report here instead of the build log. */
27
+ output?: string;
28
+ /**
29
+ * Report without failing the build. For the week it takes to adopt this on a
30
+ * site that already exists — a baseline is the honest way to go green after
31
+ * that.
32
+ */
33
+ failBuild?: boolean;
34
+ /** Skip entirely. For turning it off per environment without unwiring it. */
35
+ enabled?: boolean;
36
+ }
37
+ /**
38
+ * Thrown to fail the build. Its own class so a consumer can tell an audit
39
+ * failure from the build tool falling over.
40
+ */
41
+ declare class BuildAuditError extends Error {
42
+ readonly name = "BuildAuditError";
43
+ }
44
+ //#endregion
45
+ export { IntegrationOptions as n, BuildAuditError as t };
@@ -0,0 +1,36 @@
1
+ //#region src/integration/run.ts
2
+ /**
3
+ * Thrown to fail the build. Its own class so a consumer can tell an audit
4
+ * failure from the build tool falling over.
5
+ */
6
+ var BuildAuditError = class extends Error {
7
+ name = "BuildAuditError";
8
+ };
9
+ /**
10
+ * Audit a finished build and decide whether it may proceed.
11
+ *
12
+ * Returns normally when the build should continue, and throws BuildAuditError
13
+ * when it should not.
14
+ */
15
+ async function auditBuild(directory, options, logger) {
16
+ const { enabled, failBuild, ...auditOptions } = options;
17
+ if (enabled === false) {
18
+ logger.info("skipped (enabled: false)");
19
+ return;
20
+ }
21
+ const { runAuditCommand } = await import("./audit-CuG2hYyo.js");
22
+ const { exitCode } = await runAuditCommand(directory, auditOptions);
23
+ if (exitCode === 0) {
24
+ logger.info("no violations at or above the threshold");
25
+ return;
26
+ }
27
+ const message = exitCode === 2 ? "the audit could not be completed, so this build was not checked" : "accessibility violations at or above the threshold";
28
+ if (failBuild === false) {
29
+ logger.warn(`${message} (failBuild: false, so the build continues)`);
30
+ return;
31
+ }
32
+ logger.error(message);
33
+ throw new BuildAuditError(`eaa-kit: ${message}`);
34
+ }
35
+ //#endregion
36
+ export { auditBuild as n, BuildAuditError as t };
@@ -1,6 +1,8 @@
1
- import { i as isImpactLevel } from "./impact-DvgBjupx.js";
1
+ import { o as isImpactLevel } from "./impact-YdoOtFqm.js";
2
+ import { a as standardsReference } from "./text-BFmNtMsV.js";
2
3
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
3
4
  import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
5
+ import { a as ruleOutcomes, r as findingElements } from "./result-DLxd2Eip.js";
4
6
  import path from "node:path";
5
7
  //#region src/audit/report/sarif.ts
6
8
  const SARIF_VERSION = "2.1.0";
@@ -92,29 +94,16 @@ const SUPPRESSED = [{
92
94
  function toResults(finding, ruleIndex, uri, suppressions) {
93
95
  const level = toSarifLevel(finding.impact);
94
96
  const location = { physicalLocation: { artifactLocation: { uri } } };
95
- if (finding.nodes.length === 0) return [{
97
+ return findingElements(finding).map(({ selector, html }) => ({
96
98
  ruleId: finding.ruleId,
97
99
  ruleIndex,
98
100
  level,
99
101
  kind: "fail",
100
- message: { text: finding.help },
102
+ message: { text: selector === "" ? finding.help : `${finding.help}. Element: ${selector}` },
101
103
  locations: [location],
102
- partialFingerprints: fingerprint(finding.ruleId, "", ""),
104
+ partialFingerprints: fingerprint(finding.ruleId, selector, html),
103
105
  ...suppressions ? { suppressions } : {}
104
- }];
105
- return finding.nodes.map((node) => {
106
- const selector = node.target.join(" ");
107
- return {
108
- ruleId: finding.ruleId,
109
- ruleIndex,
110
- level,
111
- kind: "fail",
112
- message: { text: `${finding.help}. Element: ${selector}` },
113
- locations: [location],
114
- partialFingerprints: fingerprint(finding.ruleId, selector, node.html),
115
- ...suppressions ? { suppressions } : {}
116
- };
117
- });
106
+ }));
118
107
  }
119
108
  /**
120
109
  * Identifies an alert across runs. Deliberately excludes the file path, so that
@@ -127,25 +116,14 @@ function fingerprint(ruleId, selector, html) {
127
116
  /** Every rule the run knows about, so the catalogue is complete in GitHub. */
128
117
  function buildRules(audits) {
129
118
  const rules = /* @__PURE__ */ new Map();
130
- for (const audit of audits) {
131
- const outcomes = [
132
- ...audit.violations,
133
- ...audit.accepted ?? [],
134
- ...audit.incomplete,
135
- ...audit.passes,
136
- ...audit.inapplicable
137
- ];
138
- for (const outcome of outcomes) {
139
- if (rules.has(outcome.ruleId)) continue;
140
- rules.set(outcome.ruleId, toSarifRule(outcome));
141
- }
119
+ for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
120
+ if (rules.has(outcome.ruleId)) continue;
121
+ rules.set(outcome.ruleId, toSarifRule(outcome));
142
122
  }
143
123
  return [...rules.values()].sort((a, b) => a.id.localeCompare(b.id));
144
124
  }
145
125
  function toSarifRule(outcome) {
146
- const criteria = outcome.successCriteria.map((criterion) => `WCAG ${criterion}`);
147
- const clauses = outcome.enClauses.map((clause) => `EN 301 549 ${clause}`);
148
- const references = [...criteria, ...clauses].join(", ");
126
+ const references = standardsReference(outcome.successCriteria, outcome.enClauses);
149
127
  return {
150
128
  id: outcome.ruleId,
151
129
  shortDescription: { text: outcome.help },
@@ -0,0 +1,43 @@
1
+ //#region src/text.ts
2
+ /**
3
+ * Text this package writes: escaping, counting and the standards references
4
+ * that appear in every report and in the statement.
5
+ *
6
+ * Both documents eaa-kit produces embed text it did not write: an issue
7
+ * description from a config file, axe-core's help text, and — in the audit
8
+ * report — the markup of the element that failed, which is by definition
9
+ * arbitrary HTML from somebody's build. Getting the escaping wrong in the
10
+ * report would mean a page that fails an accessibility audit for having a stray
11
+ * `<script>` hands that script to whoever opens the report.
12
+ */
13
+ /** For text nodes. Leaves quotes alone, which are fine between tags. */
14
+ function escapeText(value) {
15
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16
+ }
17
+ /** For attribute values, where a quote would end the attribute. */
18
+ function escapeAttribute(value) {
19
+ return escapeText(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
20
+ }
21
+ /** `plural(1, 'page')` is `page`, `plural(2, 'page')` is `pages`. */
22
+ function plural(value, noun) {
23
+ return value === 1 ? noun : `${noun}s`;
24
+ }
25
+ /** `count(2, 'page')` is `2 pages`. */
26
+ function count(value, noun) {
27
+ return `${value} ${plural(value, noun)}`;
28
+ }
29
+ /**
30
+ * Element markup on one line, optionally bounded: one minified page must not be
31
+ * able to fill a report or run past a terminal.
32
+ */
33
+ function collapse(html, max) {
34
+ const flat = html.replace(/\s+/g, " ").trim();
35
+ if (max === void 0 || flat.length <= max) return flat;
36
+ return `${flat.slice(0, max - 1)}…`;
37
+ }
38
+ /** `WCAG 1.1.1, EN 301 549 9.1.1.1`, in that order, or an empty string. */
39
+ function standardsReference(successCriteria, enClauses) {
40
+ return [...successCriteria.map((criterion) => `WCAG ${criterion}`), ...enClauses.map((clause) => `EN 301 549 ${clause}`)].join(", ");
41
+ }
42
+ //#endregion
43
+ export { standardsReference as a, escapeText as i, count as n, escapeAttribute as r, collapse as t };
@@ -0,0 +1,39 @@
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
2
+ //#region src/vite/index.d.ts
3
+ interface EaaKitPluginOptions extends IntegrationOptions {
4
+ /**
5
+ * Directory to audit, relative to the Vite root. Defaults to the build's own
6
+ * `outDir`, which is the only place the plugin can know the files went.
7
+ */
8
+ directory?: string;
9
+ }
10
+ /**
11
+ * The parts of Vite this plugin touches, described structurally.
12
+ *
13
+ * So the published .d.ts does not reference vite, and installing eaa-kit in a
14
+ * project that has none costs nothing and still typechecks. The same reasoning
15
+ * as the Astro integration and the Playwright runner.
16
+ */
17
+ interface ResolvedConfigLike {
18
+ root: string;
19
+ build: {
20
+ outDir: string;
21
+ };
22
+ logger?: {
23
+ info(message: string): void;
24
+ warn(message: string): void;
25
+ error(message: string): void;
26
+ };
27
+ }
28
+ interface VitePluginLike {
29
+ name: string;
30
+ /** Build only: a dev server writes nothing to audit. */
31
+ apply: 'build';
32
+ /** Runs after the bundle is written, which is the earliest the files exist. */
33
+ enforce?: 'post';
34
+ configResolved(config: ResolvedConfigLike): void;
35
+ closeBundle(): Promise<void>;
36
+ }
37
+ declare function eaaKit(options?: EaaKitPluginOptions): VitePluginLike;
38
+ //#endregion
39
+ export { BuildAuditError, EaaKitPluginOptions, ResolvedConfigLike, VitePluginLike, eaaKit as default };
@@ -0,0 +1,40 @@
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-BW6CVuND.js";
2
+ import path from "node:path";
3
+ //#region src/vite/index.ts
4
+ function eaaKit(options = {}) {
5
+ let directory;
6
+ let logger;
7
+ return {
8
+ name: "eaa-kit",
9
+ apply: "build",
10
+ enforce: "post",
11
+ configResolved(config) {
12
+ directory = path.resolve(config.root, options.directory ?? config.build.outDir);
13
+ logger = prefixed(config.logger);
14
+ },
15
+ async closeBundle() {
16
+ if (directory === void 0 || logger === void 0) throw new BuildAuditError("eaa-kit: the plugin was not given a resolved Vite config");
17
+ await auditBuild(directory, options, logger);
18
+ }
19
+ };
20
+ }
21
+ /**
22
+ * Vite's own logger where there is one, the console otherwise.
23
+ *
24
+ * Prefixed either way: a line about accessibility in the middle of a build log
25
+ * needs to say what produced it.
26
+ */
27
+ function prefixed(logger) {
28
+ const write = (level, message) => {
29
+ const line = `eaa-kit: ${message}`;
30
+ if (logger === void 0) process.stderr.write(`${line}\n`);
31
+ else logger[level](line);
32
+ };
33
+ return {
34
+ info: (message) => write("info", message),
35
+ warn: (message) => write("warn", message),
36
+ error: (message) => write("error", message)
37
+ };
38
+ }
39
+ //#endregion
40
+ export { BuildAuditError, eaaKit as default };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eaa-kit",
3
- "version": "0.2.0",
4
- "description": "Build-time WCAG 2.2 AA auditor and EU accessibility statement generator for static sites (EAA / BFSG / BaFG, DACH-localised).",
3
+ "version": "0.3.0",
4
+ "description": "WCAG 2.2 AA auditor and EU accessibility statement generator (EAA / BFSG / BaFG). Audits a static build or a running site from the command line.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/astro/index.d.ts",
41
41
  "import": "./dist/astro/index.js"
42
42
  },
43
+ "./vite": {
44
+ "types": "./dist/vite/index.d.ts",
45
+ "import": "./dist/vite/index.js"
46
+ },
43
47
  "./package.json": "./package.json"
44
48
  },
45
49
  "files": [
@@ -54,7 +58,8 @@
54
58
  "lint": "biome check .",
55
59
  "format": "biome check --write .",
56
60
  "smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html",
57
- "examples": "pnpm build && node scripts/generate-examples.mjs"
61
+ "examples": "pnpm build && node scripts/generate-examples.mjs",
62
+ "test:packaged": "pnpm build && node scripts/test-packaged.mjs"
58
63
  },
59
64
  "peerDependencies": {
60
65
  "playwright": ">=1.40.0",
@@ -1,2 +0,0 @@
1
- import { r as runAuditCommand } from "./audit-D4gju2BT.js";
2
- export { runAuditCommand };