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.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/dist/action/post-comment.d.ts +20 -0
- package/dist/action/post-comment.d.ts.map +1 -0
- package/dist/action/post-comment.js +93 -0
- package/dist/action/post-comment.js.map +1 -0
- package/dist/action/run.d.ts +2 -0
- package/dist/action/run.d.ts.map +1 -0
- package/dist/action/run.js +119 -0
- package/dist/action/run.js.map +1 -0
- package/dist/action/summary.d.ts +180 -0
- package/dist/action/summary.d.ts.map +1 -0
- package/dist/action/summary.js +355 -0
- package/dist/action/summary.js.map +1 -0
- package/dist/cli.d.ts +279 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1028 -0
- package/dist/cli.js.map +1 -0
- package/dist/fault-remediation.d.ts +87 -0
- package/dist/fault-remediation.d.ts.map +1 -0
- package/dist/fault-remediation.js +156 -0
- package/dist/fault-remediation.js.map +1 -0
- package/dist/forms/config.d.ts +69 -0
- package/dist/forms/config.d.ts.map +1 -0
- package/dist/forms/config.js +185 -0
- package/dist/forms/config.js.map +1 -0
- package/dist/forms/coverage.d.ts +51 -0
- package/dist/forms/coverage.d.ts.map +1 -0
- package/dist/forms/coverage.js +80 -0
- package/dist/forms/coverage.js.map +1 -0
- package/dist/forms/draft.d.ts +62 -0
- package/dist/forms/draft.d.ts.map +1 -0
- package/dist/forms/draft.js +165 -0
- package/dist/forms/draft.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +48 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +322 -0
- package/dist/report.js.map +1 -0
- package/dist/scan/axe-results.d.ts +25 -0
- package/dist/scan/axe-results.d.ts.map +1 -0
- package/dist/scan/axe-results.js +111 -0
- package/dist/scan/axe-results.js.map +1 -0
- package/dist/scan/axe.d.ts +158 -0
- package/dist/scan/axe.d.ts.map +1 -0
- package/dist/scan/axe.js +203 -0
- package/dist/scan/axe.js.map +1 -0
- package/dist/scan/page-title.d.ts +3 -0
- package/dist/scan/page-title.d.ts.map +1 -0
- package/dist/scan/page-title.js +41 -0
- package/dist/scan/page-title.js.map +1 -0
- package/dist/scan/run-axe.d.ts +2 -0
- package/dist/scan/run-axe.d.ts.map +1 -0
- package/dist/scan/run-axe.js +39 -0
- package/dist/scan/run-axe.js.map +1 -0
- package/package.json +60 -0
package/dist/scan/axe.js
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|