a11ign 0.1.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 (59) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/dist/action/post-comment.d.ts +20 -0
  4. package/dist/action/post-comment.d.ts.map +1 -0
  5. package/dist/action/post-comment.js +93 -0
  6. package/dist/action/post-comment.js.map +1 -0
  7. package/dist/action/run.d.ts +2 -0
  8. package/dist/action/run.d.ts.map +1 -0
  9. package/dist/action/run.js +119 -0
  10. package/dist/action/run.js.map +1 -0
  11. package/dist/action/summary.d.ts +180 -0
  12. package/dist/action/summary.d.ts.map +1 -0
  13. package/dist/action/summary.js +355 -0
  14. package/dist/action/summary.js.map +1 -0
  15. package/dist/cli.d.ts +279 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +1028 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/fault-remediation.d.ts +87 -0
  20. package/dist/fault-remediation.d.ts.map +1 -0
  21. package/dist/fault-remediation.js +156 -0
  22. package/dist/fault-remediation.js.map +1 -0
  23. package/dist/forms/config.d.ts +69 -0
  24. package/dist/forms/config.d.ts.map +1 -0
  25. package/dist/forms/config.js +185 -0
  26. package/dist/forms/config.js.map +1 -0
  27. package/dist/forms/coverage.d.ts +51 -0
  28. package/dist/forms/coverage.d.ts.map +1 -0
  29. package/dist/forms/coverage.js +80 -0
  30. package/dist/forms/coverage.js.map +1 -0
  31. package/dist/forms/draft.d.ts +62 -0
  32. package/dist/forms/draft.d.ts.map +1 -0
  33. package/dist/forms/draft.js +165 -0
  34. package/dist/forms/draft.js.map +1 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +9 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/report.d.ts +48 -0
  40. package/dist/report.d.ts.map +1 -0
  41. package/dist/report.js +322 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/scan/axe-results.d.ts +25 -0
  44. package/dist/scan/axe-results.d.ts.map +1 -0
  45. package/dist/scan/axe-results.js +111 -0
  46. package/dist/scan/axe-results.js.map +1 -0
  47. package/dist/scan/axe.d.ts +158 -0
  48. package/dist/scan/axe.d.ts.map +1 -0
  49. package/dist/scan/axe.js +203 -0
  50. package/dist/scan/axe.js.map +1 -0
  51. package/dist/scan/page-title.d.ts +3 -0
  52. package/dist/scan/page-title.d.ts.map +1 -0
  53. package/dist/scan/page-title.js +41 -0
  54. package/dist/scan/page-title.js.map +1 -0
  55. package/dist/scan/run-axe.d.ts +2 -0
  56. package/dist/scan/run-axe.d.ts.map +1 -0
  57. package/dist/scan/run-axe.js +39 -0
  58. package/dist/scan/run-axe.js.map +1 -0
  59. package/package.json +60 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Rule-based layer (ADR 0002): run axe-core over a page and return its
3
+ * WCAG-tagged violations. This is the deterministic, mechanical/visual layer
4
+ * (contrast, colour, ARIA, parsing, names/roles) that a screen-reader
5
+ * read-through cannot perceive. It complements the lived-experience judge; it
6
+ * does not replace it.
7
+ *
8
+ * Scoped to WCAG A/AA to match @a11ign/evidence/wcag and the legal baseline.
9
+ *
10
+ * OPTIONAL. Playwright and @axe-core/playwright are optionalDependencies: the layer is
11
+ * ~100 lines and about a second of wall-clock, but it pulls half a gigabyte of Chromium,
12
+ * which is a poor trade for anyone who already runs axe in their own pipeline. So the
13
+ * imports are dynamic and their absence is a supported state, not a crash. The
14
+ * lived-experience layer — the part only this project does — never depends on them.
15
+ */
16
+ /**
17
+ * A/AA across WCAG 2.0/2.1/2.2 (axe tags conformance level + version).
18
+ *
19
+ * EXPORTED so a test can hold it against what axe-core actually offers. Verified 2026-08-29: axe's level
20
+ * tags are exactly `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22aa` and `wcag2aaa` — this list is
21
+ * every A/AA one, with AAA correctly out of scope, and there is no `wcag22a` (WCAG 2.2's two Level A
22
+ * additions, 3.2.6 and 3.3.7, have no axe rule at all).
23
+ *
24
+ * The reason it is pinned rather than left correct: a tag added by a future axe version would silently
25
+ * narrow the scan, and a scan that quietly checks less still reports "0 violations".
26
+ */
27
+ export const WCAG_AA_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
28
+ /**
29
+ * Fold axe's four result buckets into one verdict per criterion.
30
+ *
31
+ * A criterion usually has SEVERAL axe rules, so the buckets must be reduced with a precedence and it is
32
+ * the strict one: a violation anywhere beats review-needed, which beats clean. Two rules for a criterion
33
+ * where one passes and one needs review leaves the criterion needing review — claiming otherwise would let
34
+ * a passing fragment vouch for a fragment nobody checked.
35
+ *
36
+ * `inapplicable` counts as CLEAN rather than as ACT's `inapplicable`, and that is deliberate. axe means
37
+ * "this RULE found no elements to test"; the criterion may still have aspects no axe rule covers, so the
38
+ * page having no images tells you nothing about the rest of 1.1.1. Reporting the criterion inapplicable
39
+ * from a rule's inapplicability would be a claim about the criterion drawn from a claim about one rule.
40
+ *
41
+ * Each entry also names the axe rule ids that VIOLATED the criterion (#1606), so a reason can say which rule axe reported.
42
+ * A criterion that was not violated names none. An imported file carrying only `violations` records only violated
43
+ * criteria here, now with their ids, because `axe-results.ts` passes its buckets straight through.
44
+ */
45
+ export function coverageFrom(buckets) {
46
+ const out = {};
47
+ const record = (rules, verdict) => {
48
+ for (const rule of rules ?? []) {
49
+ for (const criterion of criteriaFromTags(Array.isArray(rule.tags) ? rule.tags.map(str) : [])) {
50
+ recordRule(out, criterion, verdict, str(rule.id));
51
+ }
52
+ }
53
+ };
54
+ // Weakest first, so the precedence above only ever upgrades.
55
+ record(buckets.inapplicable, "clean");
56
+ record(buckets.passes, "clean");
57
+ record(buckets.incomplete, "needsReview");
58
+ record(buckets.violations, "violated");
59
+ return out;
60
+ }
61
+ const RANK = { clean: 1, needsReview: 2, violated: 3 };
62
+ /**
63
+ * #1606: a stronger verdict replaces a criterion's entry and starts its rule list afresh, and a violating rule adds its id
64
+ * once. Only VIOLATING rules are named: the ids exist so a reason can say which rule axe reported, and a passing or
65
+ * review-needed rule reported no failure.
66
+ */
67
+ function recordRule(out, criterion, verdict, id) {
68
+ const current = out[criterion];
69
+ if (!current || RANK[verdict] > RANK[current.verdict])
70
+ out[criterion] = { verdict, rules: [] };
71
+ const entry = out[criterion];
72
+ if (verdict === "violated" && entry.verdict === "violated" && id && !entry.rules.includes(id))
73
+ entry.rules.push(id);
74
+ }
75
+ /** axe tags include "wcag143" for SC 1.4.3; extract criterion numbers. */
76
+ function criteriaFromTags(tags) {
77
+ const out = [];
78
+ for (const t of tags) {
79
+ const m = t.match(/^wcag(\d)(\d)(\d+)$/);
80
+ if (m)
81
+ out.push(`${m[1]}.${m[2]}.${m[3]}`);
82
+ }
83
+ return out;
84
+ }
85
+ const str = (v) => (typeof v === "string" ? v : "");
86
+ /** Map axe's violations to our findings. Shared so an imported results file and our own
87
+ * run produce identical output — a finding must not look different depending on who ran
88
+ * the scan. */
89
+ export function toFindings(violations) {
90
+ return violations.map((v) => ({
91
+ source: "axe-core",
92
+ wcag: criteriaFromTags(Array.isArray(v.tags) ? v.tags.map(str) : []),
93
+ rule: str(v.id),
94
+ impact: str(v.impact),
95
+ help: str(v.help),
96
+ helpUrl: str(v.helpUrl),
97
+ nodes: (Array.isArray(v.nodes) ? v.nodes : []).map((n) => ({
98
+ html: str(n?.html),
99
+ target: (Array.isArray(n?.target) ? n.target : []).map(String),
100
+ })),
101
+ }));
102
+ }
103
+ /** Thrown when the optional browser dependencies are not installed. */
104
+ export class AxeUnavailableError extends Error {
105
+ constructor(cause) {
106
+ super("the axe layer needs its optional dependencies: npm install playwright @axe-core/playwright && npx playwright install chromium", { cause });
107
+ this.name = "AxeUnavailableError";
108
+ }
109
+ }
110
+ // The NAMED export, not the default. The package exports the same class both ways
111
+ // (`export { AxeBuilder, AxeBuilder as default }`), but under dynamic import the default
112
+ // resolves to the module namespace, which is not constructable.
113
+ async function loadAxe() {
114
+ try {
115
+ const [playwright, axe] = await Promise.all([import("playwright"), import("@axe-core/playwright")]);
116
+ return { chromium: playwright.chromium, AxeBuilder: axe.AxeBuilder };
117
+ }
118
+ catch (e) {
119
+ throw new AxeUnavailableError(e);
120
+ }
121
+ }
122
+ /** Thrown when NEITHER the bundled browser nor the system channel could be launched. */
123
+ export class AxeLaunchError extends Error {
124
+ constructor(bundledError, channelError) {
125
+ super("the axe layer's browser could not be launched: no bundled Chromium " +
126
+ "(npx playwright install chromium) and no system Edge either", { cause: { bundledError, channelError } });
127
+ this.name = "AxeLaunchError";
128
+ }
129
+ }
130
+ /**
131
+ * Launch a browser for axe to drive — the bundled Chromium first, a system channel as the fallback.
132
+ *
133
+ * FOUND 2026-09-06: `chromium.launch()` with no options needs the bundled browser, and the Action
134
+ * deliberately skips downloading it (`PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` — half a gigabyte the Windows
135
+ * runner does not need, since the capture already drives Edge). So on the Action the bundled browser is
136
+ * ABSENT BY DESIGN, `launch()` threw, the throw was caught in `cli.ts` and reported as `ruleBased: null`
137
+ * — while the progress line printed "rule-based axe-core + real screen reader" and the smoke test never
138
+ * read `ruleBased` at all. Every Action consumer got the rule layer silently skipped.
139
+ *
140
+ * A hard-coded `channel: "msedge"` is not the fix: the CLI runs on Macs and Linux too, and a developer
141
+ * with no Edge installed would lose the layer entirely. So: try the bundled browser first (what a local
142
+ * `npx playwright install chromium` gives you), and only on failure fall back to the system channel —
143
+ * which the Windows runner has, and which happens to be the SAME engine the capture itself drives, so
144
+ * the two layers observe one rendering engine rather than two. Which one actually answered is returned
145
+ * rather than assumed, because it is evidence: a finding depends on the renderer that produced it.
146
+ */
147
+ export async function launchBrowser(chromium) {
148
+ try {
149
+ return { browser: await chromium.launch(), channel: "chromium" };
150
+ }
151
+ catch (bundledError) {
152
+ try {
153
+ return { browser: await chromium.launch({ channel: "msedge" }), channel: "msedge" };
154
+ }
155
+ catch (channelError) {
156
+ throw new AxeLaunchError(bundledError, channelError);
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * True when the rule-based layer can run here.
162
+ *
163
+ * USED TO BE cheap and wrong: it resolved the modules and stopped, which proves an IMPORT, not a LAUNCH —
164
+ * exactly the gap that let the Action announce the layer and then silently produce nothing for it. This
165
+ * now launches for real (bundled Chromium, then the system channel) and closes immediately, so the
166
+ * answer means what its name says. The cost is the same one `scanWithAxe` already pays once per run.
167
+ *
168
+ * `deps.loadAxe` is the injection seam for a test: neither of `loadAxe`'s own two failure modes (modules
169
+ * missing, no browser launchable) can be produced from inside this repo's CI without either uninstalling
170
+ * a dependency or faking the launch — so a test supplies a fake `loadAxe` whose `chromium.launch` always
171
+ * throws, and asserts this still answers `false` rather than the `true` an import-only check would give.
172
+ */
173
+ export async function axeAvailable(deps = {}) {
174
+ const resolve = deps.loadAxe ?? loadAxe;
175
+ try {
176
+ const { chromium } = await resolve();
177
+ const { browser } = await launchBrowser(chromium);
178
+ await browser.close();
179
+ return true;
180
+ }
181
+ catch (e) {
182
+ if (e instanceof AxeUnavailableError || e instanceof AxeLaunchError)
183
+ return false;
184
+ throw e;
185
+ }
186
+ }
187
+ export async function scanWithAxe(url) {
188
+ const { chromium, AxeBuilder } = await loadAxe();
189
+ const { browser, channel } = await launchBrowser(chromium);
190
+ try {
191
+ // @axe-core/playwright requires a page from an explicit context.
192
+ const context = await browser.newContext();
193
+ const page = await context.newPage();
194
+ await page.goto(url, { waitUntil: "load" });
195
+ const title = await page.title();
196
+ const results = await new AxeBuilder({ page }).withTags(WCAG_AA_TAGS).analyze();
197
+ return { findings: toFindings(results.violations), title, coverage: coverageFrom(results), browserChannel: channel };
198
+ }
199
+ finally {
200
+ await browser.close();
201
+ }
202
+ }
203
+ //# sourceMappingURL=axe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axe.js","sourceRoot":"","sources":["../../src/scan/axe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** The page's `<title>`, or "" if it cannot be determined. */
2
+ export declare function fetchPageTitle(url: string): Promise<string>;
3
+ //# sourceMappingURL=page-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-title.d.ts","sourceRoot":"","sources":["../../src/scan/page-title.ts"],"names":[],"mappings":"AA6BA,8DAA8D;AAC9D,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASjE"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The page's own title, fetched from the control plane.
3
+ *
4
+ * This exists so capture verification does not depend on the axe layer. The CLI checks
5
+ * that the screen reader actually read the target page by looking for the page's title in
6
+ * what was announced; that title used to come from axe's Playwright page, so turning axe
7
+ * off silently disabled the check.
8
+ *
9
+ * It MUST come from a source independent of the capture. Asking the worker what title it
10
+ * saw and then using that to verify the worker read the right page proves nothing — the
11
+ * check only has value because the two observations are independent.
12
+ *
13
+ * A fetched title is weaker than a rendered one: a page that sets its title in JavaScript
14
+ * will report whatever the server sent. That is acceptable here, because the check is
15
+ * deliberately lenient (one significant word) and only ever triggers a re-capture.
16
+ */
17
+ import { readFile } from "node:fs/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { titleOf } from "@a11ign/evidence/verify";
20
+ const TITLE_TIMEOUT_MS = 10_000;
21
+ async function sourceOf(url) {
22
+ if (url.startsWith("file:"))
23
+ return readFile(fileURLToPath(url), "utf8");
24
+ const response = await fetch(url, { signal: AbortSignal.timeout(TITLE_TIMEOUT_MS) });
25
+ if (!response.ok)
26
+ throw new Error(`HTTP ${response.status}`);
27
+ return response.text();
28
+ }
29
+ /** The page's `<title>`, or "" if it cannot be determined. */
30
+ export async function fetchPageTitle(url) {
31
+ try {
32
+ return titleOf(await sourceOf(url));
33
+ }
34
+ catch (e) {
35
+ // Not fatal: without a title the CLI simply skips the wrong-page check, which is the
36
+ // same position it is in for a page that has no title at all.
37
+ process.stderr.write(`could not read the page title for verification (${e.message})\n`);
38
+ return "";
39
+ }
40
+ }
41
+ //# sourceMappingURL=page-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-title.js","sourceRoot":"","sources":["../../src/scan/page-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAElD,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,KAAK,UAAU,QAAQ,CAAC,GAAW;IACjC,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;IACrF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,qFAAqF;QACrF,8DAA8D;QAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mDAAoD,CAAW,CAAC,OAAO,KAAK,CAAC,CAAC;QACnG,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=run-axe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-axe.d.ts","sourceRoot":"","sources":["../../src/scan/run-axe.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Run the axe-core rule-based scan against a URL or local file.
3
+ * Usage: npm run scan -- <url|path>
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { scanWithAxe } from "./axe.js";
9
+ async function main() {
10
+ let target = process.argv[2];
11
+ if (!target) {
12
+ console.error("Usage: npm run scan -- <url|path>");
13
+ process.exit(1);
14
+ }
15
+ if (!/^(https?|file):/.test(target) && existsSync(target)) {
16
+ target = pathToFileURL(resolve(target)).href;
17
+ }
18
+ const { findings } = await scanWithAxe(target);
19
+ console.log(`\naxe-core (rule-based layer): ${findings.length} violation(s) on ${target}\n`);
20
+ for (const f of findings) {
21
+ console.log(` [${f.impact}] ${f.wcag.join(", ") || "(no SC tag)"} ${f.rule}`);
22
+ console.log(` ${f.help}`);
23
+ for (const n of f.nodes.slice(0, 2))
24
+ console.log(` evidence: ${n.html.slice(0, 100)}`);
25
+ }
26
+ console.log("");
27
+ }
28
+ /**
29
+ * Run ONLY when this file is the program, never when it is imported — so a test (or the `import()` load
30
+ * check) can reach the functions above without executing the script. See `entry-points.test.ts`.
31
+ */
32
+ const isProgram = process.argv[1] !== undefined
33
+ && import.meta.url === pathToFileURL(process.argv[1]).href;
34
+ if (isProgram)
35
+ main().catch((err) => {
36
+ console.error(err);
37
+ process.exit(1);
38
+ });
39
+ //# sourceMappingURL=run-axe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-axe.js","sourceRoot":"","sources":["../../src/scan/run-axe.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,kCAAkC,QAAQ,CAAC,MAAM,oBAAoB,MAAM,IAAI,CAAC,CAAC;IAC7F,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,aAAa,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;OAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAE7D,IAAI,SAAS;IAAE,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAClC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "a11ign",
3
+ "version": "0.1.0",
4
+ "description": "Drives a real screen reader through a real page and reports the WCAG 2.2 AA failures a rule scanner cannot see — alongside axe, never instead of it.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "bin": {
15
+ "a11ign": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "dependencies": {
23
+ "@a11ign/evidence": "0.1.0",
24
+ "@a11ign/judge": "0.1.0",
25
+ "@a11ign/scorer": "0.1.0",
26
+ "@a11ign/worker-fleet": "0.1.0",
27
+ "yaml": "^2.9.0"
28
+ },
29
+ "optionalDependencies": {
30
+ "@axe-core/playwright": "^4.12.1",
31
+ "playwright": "^1.61.1"
32
+ },
33
+ "devDependencies": {
34
+ "axe-core": "^4.12.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "prepack": "tsc --build"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/a11ign/a11ign.git",
48
+ "directory": "packages/cli"
49
+ },
50
+ "homepage": "https://github.com/a11ign/a11ign",
51
+ "keywords": [
52
+ "accessibility",
53
+ "a11y",
54
+ "wcag",
55
+ "screen-reader",
56
+ "nvda",
57
+ "axe",
58
+ "cli"
59
+ ]
60
+ }