eaa-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/astro/index.d.ts +7 -34
- package/dist/astro/index.js +6 -26
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-CuG2hYyo.js +2 -0
- package/dist/audit-DcL73mOC.js +635 -0
- package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
- package/dist/cli/index.js +81 -143
- package/dist/collect-BkAQ0viT.js +123 -0
- package/dist/command-Dxpa00Ha.js +77 -0
- package/dist/component-7kEBjv_y.js +111 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
- package/dist/frameworks-BYa3tULg.js +243 -0
- package/dist/frameworks-DaDqrJOw.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
- package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
- package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
- package/dist/jsdom-X4KYfTp8.js +3 -0
- package/dist/json-B0Y7rNjt.js +2 -0
- package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
- package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
- package/dist/manual-Vz-oX1I_.js +239 -0
- package/dist/{playwright-DWux49V3.js → playwright-DYFsGUNd.js} +79 -21
- package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
- package/dist/{project-CiyzKQud.js → project-DW08TseF.js} +37 -27
- package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
- package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
- package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-BW6CVuND.js +36 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
- package/dist/text-BFmNtMsV.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/package.json +8 -3
- package/dist/audit-CqrR9pIO.js +0 -2
- package/dist/audit-D4gju2BT.js +0 -949
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/jsdom-C6dIyaxN.js +0 -3
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { a as impactRank } from "./impact-YdoOtFqm.js";
|
|
2
|
+
import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
|
|
3
|
+
import { r as findingElements } from "./result-DLxd2Eip.js";
|
|
4
|
+
/** Whether this element looks like one component rendered on many pages. */
|
|
5
|
+
function isShared(element) {
|
|
6
|
+
return element.pages.length >= 3;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fold a run's violations into one entry per rule, and one per element within it.
|
|
10
|
+
*
|
|
11
|
+
* Accepted violations are not included: a baseline moves them out of what fails
|
|
12
|
+
* the build, and this is a view of what fails.
|
|
13
|
+
*/
|
|
14
|
+
function groupIssues(audits) {
|
|
15
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const audit of audits) for (const finding of audit.violations) {
|
|
17
|
+
let issue = byRule.get(finding.ruleId);
|
|
18
|
+
if (issue === void 0) {
|
|
19
|
+
issue = {
|
|
20
|
+
ruleId: finding.ruleId,
|
|
21
|
+
help: finding.help,
|
|
22
|
+
impact: finding.impact ?? null,
|
|
23
|
+
successCriteria: finding.successCriteria,
|
|
24
|
+
enClauses: finding.enClauses,
|
|
25
|
+
helpUrl: finding.helpUrl,
|
|
26
|
+
elements: [],
|
|
27
|
+
pages: [],
|
|
28
|
+
occurrences: 0
|
|
29
|
+
};
|
|
30
|
+
byRule.set(finding.ruleId, issue);
|
|
31
|
+
}
|
|
32
|
+
if (!issue.pages.includes(audit.relativePath)) issue.pages.push(audit.relativePath);
|
|
33
|
+
for (const node of findingElements(finding)) {
|
|
34
|
+
issue.occurrences += 1;
|
|
35
|
+
const fingerprint = elementFingerprint(finding.ruleId, node.selector, node.html);
|
|
36
|
+
const existing = issue.elements.find((element) => element.fingerprint === fingerprint);
|
|
37
|
+
if (existing) {
|
|
38
|
+
if (!existing.pages.includes(audit.relativePath)) existing.pages.push(audit.relativePath);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
issue.elements.push({
|
|
42
|
+
fingerprint,
|
|
43
|
+
selector: node.selector,
|
|
44
|
+
html: node.html,
|
|
45
|
+
pages: [audit.relativePath]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const issues = [...byRule.values()];
|
|
50
|
+
for (const issue of issues) {
|
|
51
|
+
issue.pages.sort();
|
|
52
|
+
for (const element of issue.elements) element.pages.sort();
|
|
53
|
+
issue.elements.sort(byReachThenSelector);
|
|
54
|
+
}
|
|
55
|
+
issues.sort(bySeverityThenReach);
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Unevaluated rules folded across the site, sorted by rule id.
|
|
60
|
+
*
|
|
61
|
+
* Both reports list these once at the end rather than under every page: on a
|
|
62
|
+
* large site the same handful recurs on each one, and a wall of "not evaluated"
|
|
63
|
+
* would bury the findings that are real.
|
|
64
|
+
*/
|
|
65
|
+
function blindRules(audits) {
|
|
66
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const audit of audits) for (const finding of audit.incomplete) {
|
|
68
|
+
if (finding.reason !== "engine-limitation") continue;
|
|
69
|
+
const entry = byRule.get(finding.ruleId);
|
|
70
|
+
if (entry) entry.pages += 1;
|
|
71
|
+
else byRule.set(finding.ruleId, {
|
|
72
|
+
ruleId: finding.ruleId,
|
|
73
|
+
pages: 1,
|
|
74
|
+
finding
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return [...byRule.values()].sort((a, b) => a.ruleId.localeCompare(b.ruleId));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What one page's result actually rests on, as phrases both reports print.
|
|
81
|
+
*
|
|
82
|
+
* The counts stay separate on purpose. Only `passed` is evidence that a
|
|
83
|
+
* criterion was met here; `not applicable` means the rule found nothing to
|
|
84
|
+
* check, and adding the two together would turn an empty page into a
|
|
85
|
+
* near-perfect score.
|
|
86
|
+
*/
|
|
87
|
+
function coverageParts(audit) {
|
|
88
|
+
const blind = audit.incomplete.filter((finding) => finding.reason === "engine-limitation").length;
|
|
89
|
+
const review = audit.incomplete.length - blind;
|
|
90
|
+
const parts = [`${audit.passes.length} passed`, `${audit.inapplicable.length} not applicable`];
|
|
91
|
+
if (review > 0) parts.push(`${review} to review`);
|
|
92
|
+
if (blind > 0) parts.push(`${blind} not evaluated`);
|
|
93
|
+
return parts;
|
|
94
|
+
}
|
|
95
|
+
/** Widest reach first, then by selector so two runs agree. */
|
|
96
|
+
function byReachThenSelector(a, b) {
|
|
97
|
+
return b.pages.length - a.pages.length || a.selector.localeCompare(b.selector);
|
|
98
|
+
}
|
|
99
|
+
/** Worst first, then widest reach, then by rule id. */
|
|
100
|
+
function bySeverityThenReach(a, b) {
|
|
101
|
+
return impactRank(a.impact) - impactRank(b.impact) || b.pages.length - a.pages.length || a.ruleId.localeCompare(b.ruleId);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/audit/manual.ts
|
|
105
|
+
/**
|
|
106
|
+
* What a person has to check, and where to read about it.
|
|
107
|
+
*
|
|
108
|
+
* Automated testing finds a minority of accessibility barriers. This tool says
|
|
109
|
+
* so everywhere, and saying so is not much use on its own: a reader told that
|
|
110
|
+
* six rules "could not be evaluated" is left knowing there is a gap and not
|
|
111
|
+
* what to do about it.
|
|
112
|
+
*
|
|
113
|
+
* So each rule the browserless engine cannot decide carries the check somebody
|
|
114
|
+
* would do by hand, and each success criterion carries a link to what it
|
|
115
|
+
* actually requires. The point is to turn the honest disclaimer into the part
|
|
116
|
+
* of the report with the most work in it.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* WCAG 2.2 Understanding pages, by success criterion.
|
|
120
|
+
*
|
|
121
|
+
* The Understanding document rather than the criterion text: the text says what
|
|
122
|
+
* conformance is, and the Understanding page says what it is for and how to
|
|
123
|
+
* meet it, which is what somebody reading a report needs.
|
|
124
|
+
*
|
|
125
|
+
* Level A and AA only, since that is what this tool audits against.
|
|
126
|
+
*/
|
|
127
|
+
const UNDERSTANDING = {
|
|
128
|
+
"1.1.1": "non-text-content",
|
|
129
|
+
"1.2.1": "audio-only-and-video-only-prerecorded",
|
|
130
|
+
"1.2.2": "captions-prerecorded",
|
|
131
|
+
"1.2.3": "audio-description-or-media-alternative-prerecorded",
|
|
132
|
+
"1.2.4": "captions-live",
|
|
133
|
+
"1.2.5": "audio-description-prerecorded",
|
|
134
|
+
"1.3.1": "info-and-relationships",
|
|
135
|
+
"1.3.2": "meaningful-sequence",
|
|
136
|
+
"1.3.3": "sensory-characteristics",
|
|
137
|
+
"1.3.4": "orientation",
|
|
138
|
+
"1.3.5": "identify-input-purpose",
|
|
139
|
+
"1.4.1": "use-of-color",
|
|
140
|
+
"1.4.2": "audio-control",
|
|
141
|
+
"1.4.3": "contrast-minimum",
|
|
142
|
+
"1.4.4": "resize-text",
|
|
143
|
+
"1.4.5": "images-of-text",
|
|
144
|
+
"1.4.10": "reflow",
|
|
145
|
+
"1.4.11": "non-text-contrast",
|
|
146
|
+
"1.4.12": "text-spacing",
|
|
147
|
+
"1.4.13": "content-on-hover-or-focus",
|
|
148
|
+
"2.1.1": "keyboard",
|
|
149
|
+
"2.1.2": "no-keyboard-trap",
|
|
150
|
+
"2.1.4": "character-key-shortcuts",
|
|
151
|
+
"2.2.1": "timing-adjustable",
|
|
152
|
+
"2.2.2": "pause-stop-hide",
|
|
153
|
+
"2.3.1": "three-flashes-or-below-threshold",
|
|
154
|
+
"2.4.1": "bypass-blocks",
|
|
155
|
+
"2.4.2": "page-titled",
|
|
156
|
+
"2.4.3": "focus-order",
|
|
157
|
+
"2.4.4": "link-purpose-in-context",
|
|
158
|
+
"2.4.5": "multiple-ways",
|
|
159
|
+
"2.4.6": "headings-and-labels",
|
|
160
|
+
"2.4.7": "focus-visible",
|
|
161
|
+
"2.4.11": "focus-not-obscured-minimum",
|
|
162
|
+
"2.5.1": "pointer-gestures",
|
|
163
|
+
"2.5.2": "pointer-cancellation",
|
|
164
|
+
"2.5.3": "label-in-name",
|
|
165
|
+
"2.5.4": "motion-actuation",
|
|
166
|
+
"2.5.7": "dragging-movements",
|
|
167
|
+
"2.5.8": "target-size-minimum",
|
|
168
|
+
"3.1.1": "language-of-page",
|
|
169
|
+
"3.1.2": "language-of-parts",
|
|
170
|
+
"3.2.1": "on-focus",
|
|
171
|
+
"3.2.2": "on-input",
|
|
172
|
+
"3.2.3": "consistent-navigation",
|
|
173
|
+
"3.2.4": "consistent-identification",
|
|
174
|
+
"3.2.6": "consistent-help",
|
|
175
|
+
"3.3.1": "error-identification",
|
|
176
|
+
"3.3.2": "labels-or-instructions",
|
|
177
|
+
"3.3.3": "error-suggestion",
|
|
178
|
+
"3.3.4": "error-prevention-legal-financial-data",
|
|
179
|
+
"3.3.7": "redundant-entry",
|
|
180
|
+
"3.3.8": "accessible-authentication-minimum",
|
|
181
|
+
"4.1.2": "name-role-value",
|
|
182
|
+
"4.1.3": "status-messages"
|
|
183
|
+
};
|
|
184
|
+
/** Where to read what a success criterion requires, or undefined if unknown. */
|
|
185
|
+
function understandingUrl(criterion) {
|
|
186
|
+
const slug = UNDERSTANDING[criterion];
|
|
187
|
+
return slug === void 0 ? void 0 : `https://www.w3.org/WAI/WCAG22/Understanding/${slug}.html`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The check a person does for a rule this engine could not decide.
|
|
191
|
+
*
|
|
192
|
+
* Written as an action rather than a restatement of the criterion. "Ensure
|
|
193
|
+
* sufficient contrast" tells somebody nothing they did not already know; the
|
|
194
|
+
* useful sentence names what to open and what to look at.
|
|
195
|
+
*/
|
|
196
|
+
const MANUAL_CHECKS = {
|
|
197
|
+
"color-contrast": {
|
|
198
|
+
check: "Open the page and check text against its background with a contrast checker. Body text needs 4.5:1, and large or bold text 3:1. Check the states too — hover, focus, visited, disabled and placeholder text are the ones usually missed.",
|
|
199
|
+
browserAnswers: true
|
|
200
|
+
},
|
|
201
|
+
"color-contrast-enhanced": {
|
|
202
|
+
check: "Only needed if you are claiming AAA. Body text needs 7:1 and large text 4.5:1.",
|
|
203
|
+
browserAnswers: true
|
|
204
|
+
},
|
|
205
|
+
"target-size": {
|
|
206
|
+
check: "Measure the clickable area of buttons, icon links and form controls: 24×24 CSS pixels at minimum, unless they are inline in a sentence or have that much clear space around them. Icon-only controls in a header or a media player are where this usually fails.",
|
|
207
|
+
browserAnswers: true
|
|
208
|
+
},
|
|
209
|
+
"scrollable-region-focusable": {
|
|
210
|
+
check: "Find anything that scrolls inside the page — a code block, a wide table, a long list — and try reaching it with the Tab key alone. If it cannot take focus, a keyboard user cannot scroll it.",
|
|
211
|
+
browserAnswers: true
|
|
212
|
+
},
|
|
213
|
+
"link-in-text-block": {
|
|
214
|
+
check: "Look at links inside paragraphs. If the only thing distinguishing them from the surrounding text is colour, they need an underline or a 3:1 contrast difference against that text as well.",
|
|
215
|
+
browserAnswers: true
|
|
216
|
+
},
|
|
217
|
+
"no-autoplay-audio": {
|
|
218
|
+
check: "Load the page and listen. Anything that plays for more than three seconds on its own needs a pause or stop control, or a volume control independent of the system volume.",
|
|
219
|
+
browserAnswers: false
|
|
220
|
+
},
|
|
221
|
+
"avoid-inline-spacing": {
|
|
222
|
+
check: "Override line height to 1.5×, paragraph spacing to 2×, letter spacing to 0.12× and word spacing to 0.16× the font size, then check nothing is clipped or overlapping. Inline styles that set spacing with !important are what break this.",
|
|
223
|
+
browserAnswers: true
|
|
224
|
+
},
|
|
225
|
+
"p-as-heading": {
|
|
226
|
+
check: "Look for paragraphs styled to look like headings. A screen reader reads them as body text, so they are invisible to anyone navigating by heading.",
|
|
227
|
+
browserAnswers: false
|
|
228
|
+
},
|
|
229
|
+
"css-orientation-lock": {
|
|
230
|
+
check: "Rotate a phone or tablet. The content should work in both orientations unless one is essential, which is rare outside games and instruments.",
|
|
231
|
+
browserAnswers: false
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
/** The check for a rule, if there is one written for it. */
|
|
235
|
+
function manualCheckFor(ruleId) {
|
|
236
|
+
return MANUAL_CHECKS[ruleId];
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
export { groupIssues as a, coverageParts as i, understandingUrl as n, isShared as o, blindRules as r, manualCheckFor as t };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { i as pageUrl, n as failedPage, o as runOptions, s as shapeResults, t as DEFAULT_TAGS } from "./result-DLxd2Eip.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
2
4
|
import { stat } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -73,7 +75,7 @@ async function handle(root, requestUrl, response) {
|
|
|
73
75
|
}
|
|
74
76
|
try {
|
|
75
77
|
const target = (await stat(file)).isDirectory() ? path.join(file, "index.html") : file;
|
|
76
|
-
if (!await
|
|
78
|
+
if (!await isFile(target)) {
|
|
77
79
|
response.writeHead(404).end();
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
@@ -87,13 +89,6 @@ async function handle(root, requestUrl, response) {
|
|
|
87
89
|
response.writeHead(404).end();
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
async function isReadableFile(candidate) {
|
|
91
|
-
try {
|
|
92
|
-
return (await stat(candidate)).isFile();
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
92
|
/** Resolves a request path inside the root, or nothing if it escapes it. */
|
|
98
93
|
function resolveFile(root, requestUrl) {
|
|
99
94
|
const { pathname } = new URL(requestUrl, "http://127.0.0.1");
|
|
@@ -140,12 +135,18 @@ const DEFAULT_VIEWPORT = {
|
|
|
140
135
|
*/
|
|
141
136
|
async function runBrowserAudit(directory, pages, options = {}) {
|
|
142
137
|
if (pages.length === 0) return [];
|
|
143
|
-
const chromium = await loadChromium();
|
|
138
|
+
const chromium = await loadChromium(options.cwd);
|
|
144
139
|
const tags = options.tags ?? DEFAULT_TAGS;
|
|
145
140
|
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
146
141
|
const viewport = options.viewport ?? DEFAULT_VIEWPORT;
|
|
147
142
|
const server = directory === void 0 ? void 0 : await serveDirectory(directory);
|
|
148
|
-
|
|
143
|
+
let browser;
|
|
144
|
+
try {
|
|
145
|
+
browser = await chromium.launch({ headless: true });
|
|
146
|
+
} catch (cause) {
|
|
147
|
+
await server?.close();
|
|
148
|
+
throw launchFailure(cause);
|
|
149
|
+
}
|
|
149
150
|
try {
|
|
150
151
|
const context = await browser.newContext({
|
|
151
152
|
viewport,
|
|
@@ -169,7 +170,7 @@ async function auditOne(context, origin, page, options) {
|
|
|
169
170
|
const identity = {
|
|
170
171
|
relativePath: page.relativePath,
|
|
171
172
|
absolutePath: page.absolutePath,
|
|
172
|
-
url:
|
|
173
|
+
url: pageUrl(page, options.baseUrl),
|
|
173
174
|
engine: "browser"
|
|
174
175
|
};
|
|
175
176
|
const tab = await context.newPage();
|
|
@@ -211,9 +212,27 @@ async function auditOne(context, origin, page, options) {
|
|
|
211
212
|
function servedUrl(origin, relativePath) {
|
|
212
213
|
return `${origin}/${relativePath.split("/").map(encodeURIComponent).join("/")}`;
|
|
213
214
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
/**
|
|
216
|
+
* A launch that failed because the browser was never downloaded, told as setup.
|
|
217
|
+
*
|
|
218
|
+
* The third setup failure, and the only one that used to arrive as a crash.
|
|
219
|
+
* `npm i -D playwright` does not fetch Chromium, so somebody who followed the
|
|
220
|
+
* install line exactly still lands here — and what they saw was Playwright's
|
|
221
|
+
* message boxed in ASCII, wrapped in a stack trace through this package's
|
|
222
|
+
* bundled internals. That reads as eaa-kit falling over, not as a step left to
|
|
223
|
+
* run, and the two want very different things from the reader.
|
|
224
|
+
*
|
|
225
|
+
* Anything else is passed through untouched: a launch can fail for reasons
|
|
226
|
+
* that really are bugs, and dressing those up as setup advice would send
|
|
227
|
+
* somebody off installing a browser they already have.
|
|
228
|
+
*/
|
|
229
|
+
function launchFailure(cause) {
|
|
230
|
+
if (!(cause instanceof Error)) return cause;
|
|
231
|
+
const missing = /Executable doesn't exist at (.+)/.exec(cause.message);
|
|
232
|
+
if (missing === null) return cause;
|
|
233
|
+
return new BrowserUnavailableError(`Playwright is installed, but the Chromium it drives is not.
|
|
234
|
+
Download it with: npx playwright install chromium
|
|
235
|
+
It was looked for at: ${missing[1]?.trim() ?? "an unreported path"}`);
|
|
217
236
|
}
|
|
218
237
|
/**
|
|
219
238
|
* Playwright is an optional peer dependency: the browserless path must never
|
|
@@ -221,15 +240,54 @@ function reportedUrl(page, baseUrl) {
|
|
|
221
240
|
* because "install playwright" and "install the browser binary" are different
|
|
222
241
|
* problems with different fixes.
|
|
223
242
|
*/
|
|
224
|
-
async function loadChromium() {
|
|
225
|
-
|
|
243
|
+
async function loadChromium(cwd = process.cwd()) {
|
|
244
|
+
const tried = [];
|
|
245
|
+
const specifiers = ["playwright", "@playwright/test"];
|
|
246
|
+
const roots = [cwd, void 0];
|
|
247
|
+
for (const root of roots) for (const specifier of specifiers) {
|
|
248
|
+
const loaded = await tryLoad(specifier, root);
|
|
249
|
+
if (loaded === void 0) continue;
|
|
250
|
+
tried.push(`${specifier}${root === void 0 ? " (from eaa-kit)" : ""}`);
|
|
251
|
+
const chromium = launcherIn(loaded);
|
|
252
|
+
if (chromium !== void 0) return chromium;
|
|
253
|
+
}
|
|
254
|
+
if (tried.length === 0) throw new BrowserUnavailableError("Browser mode needs Playwright, which is an optional peer dependency.\n Install it with: npm i -D playwright\n Then the browser: npx playwright install chromium");
|
|
255
|
+
throw new BrowserUnavailableError(`Found ${tried.join(" and ")}, but no chromium launcher on it.\n This usually means an incomplete or mismatched install. Try:
|
|
256
|
+
npm i -D playwright@latest
|
|
257
|
+
npx playwright install chromium --force`);
|
|
258
|
+
}
|
|
259
|
+
/** Import a specifier, resolved from `root` when given. Undefined if absent. */
|
|
260
|
+
async function tryLoad(specifier, root) {
|
|
226
261
|
try {
|
|
227
|
-
|
|
262
|
+
if (root === void 0) return await import(specifier);
|
|
263
|
+
const require = createRequire(pathToFileURL(path.join(root, "package.json")).href);
|
|
264
|
+
return await import(pathToFileURL(require.resolve(specifier)).href);
|
|
228
265
|
} catch {
|
|
229
|
-
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The chromium launcher, whichever shape the module came back in.
|
|
271
|
+
*
|
|
272
|
+
* Playwright is CommonJS. Whether `await import()` of it exposes named exports
|
|
273
|
+
* depends on Node's static analysis of the file succeeding, which is not
|
|
274
|
+
* guaranteed and differs between versions; when it does not, everything is on
|
|
275
|
+
* `default` instead. Reading only the named export produced "installed but
|
|
276
|
+
* exports no chromium launcher" against a perfectly good install.
|
|
277
|
+
*
|
|
278
|
+
* Exported for its own tests. Driving this through a real import under vitest
|
|
279
|
+
* proves nothing: vitest hands CommonJS back through an interop proxy that
|
|
280
|
+
* answers `.chromium` whether or not Node hoisted it, so both shapes look
|
|
281
|
+
* identical from in there and the test passes with this function broken. The
|
|
282
|
+
* shapes are checked here directly, and the real interop is exercised against
|
|
283
|
+
* real Node by scripts/test-packaged.mjs.
|
|
284
|
+
*/
|
|
285
|
+
function launcherIn(module) {
|
|
286
|
+
const candidates = [module, module?.default];
|
|
287
|
+
for (const candidate of candidates) {
|
|
288
|
+
const chromium = candidate?.chromium;
|
|
289
|
+
if (chromium !== void 0 && typeof chromium.launch === "function") return chromium;
|
|
230
290
|
}
|
|
231
|
-
if (!module.chromium) throw new BrowserUnavailableError("Playwright is installed but exports no chromium launcher");
|
|
232
|
-
return module.chromium;
|
|
233
291
|
}
|
|
234
292
|
//#endregion
|
|
235
293
|
export { BrowserUnavailableError, runBrowserAudit };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
1
|
+
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { i as pageUrl, n as failedPage } from "./result-DLxd2Eip.js";
|
|
4
3
|
import { Worker } from "node:worker_threads";
|
|
5
4
|
import { availableParallelism } from "node:os";
|
|
6
5
|
//#region src/audit/runners/pool.ts
|
|
@@ -97,7 +96,7 @@ async function runPooledAudit(pages, options = {}) {
|
|
|
97
96
|
return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
|
|
98
97
|
}
|
|
99
98
|
async function auditHere(pages, options) {
|
|
100
|
-
const { runJsdomAudit } = await import("./jsdom-
|
|
99
|
+
const { runJsdomAudit } = await import("./jsdom-X4KYfTp8.js");
|
|
101
100
|
return runJsdomAudit(pages, options);
|
|
102
101
|
}
|
|
103
102
|
async function runWorkers(pages, options, count, entry) {
|
|
@@ -159,11 +158,6 @@ function identity(page, options) {
|
|
|
159
158
|
durationMs: 0
|
|
160
159
|
};
|
|
161
160
|
}
|
|
162
|
-
/** Kept in step with the sequential runner's own URL derivation. */
|
|
163
|
-
function pageUrl(page, baseUrl) {
|
|
164
|
-
if (!baseUrl) return pathToFileURL(page.absolutePath).href;
|
|
165
|
-
return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
|
|
166
|
-
}
|
|
167
161
|
/**
|
|
168
162
|
* Locate the worker entry point.
|
|
169
163
|
*
|
|
@@ -180,9 +174,7 @@ async function findWorkerEntry() {
|
|
|
180
174
|
new URL("../audit/runners/worker.js", import.meta.url),
|
|
181
175
|
new URL("./worker.js", import.meta.url)
|
|
182
176
|
];
|
|
183
|
-
for (const candidate of candidates)
|
|
184
|
-
if ((await stat(candidate)).isFile()) return candidate;
|
|
185
|
-
} catch {}
|
|
177
|
+
for (const candidate of candidates) if (await isFile(candidate)) return candidate;
|
|
186
178
|
}
|
|
187
179
|
//#endregion
|
|
188
180
|
export { plannedWorkers, runPooledAudit };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as isDirectory, t as exists } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { r as candidateOutputs } from "./frameworks-BYa3tULg.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { glob } from "tinyglobby";
|
|
4
6
|
import { spawn } from "node:child_process";
|
|
@@ -17,15 +19,6 @@ import { spawn } from "node:child_process";
|
|
|
17
19
|
* project's server if the build produces nothing browsable. Naming a directory
|
|
18
20
|
* or passing --url skips all of it.
|
|
19
21
|
*/
|
|
20
|
-
/** Build output directories, in the order worth trying them. */
|
|
21
|
-
const BUILD_DIRECTORIES = [
|
|
22
|
-
"dist",
|
|
23
|
-
"out",
|
|
24
|
-
"build",
|
|
25
|
-
"_site",
|
|
26
|
-
".output/public",
|
|
27
|
-
"public"
|
|
28
|
-
];
|
|
29
22
|
/** Ports the common dev and preview servers use, tried if nothing is announced. */
|
|
30
23
|
const KNOWN_PORTS = [
|
|
31
24
|
3e3,
|
|
@@ -55,10 +48,7 @@ async function detectPackageManager(cwd) {
|
|
|
55
48
|
["pnpm-lock.yaml", "pnpm"],
|
|
56
49
|
["yarn.lock", "yarn"],
|
|
57
50
|
["bun.lockb", "bun"]
|
|
58
|
-
])
|
|
59
|
-
await stat(path.join(cwd, file));
|
|
60
|
-
return manager;
|
|
61
|
-
} catch {}
|
|
51
|
+
]) if (await exists(path.join(cwd, file))) return manager;
|
|
62
52
|
return "npm";
|
|
63
53
|
}
|
|
64
54
|
/**
|
|
@@ -69,13 +59,10 @@ async function detectPackageManager(cwd) {
|
|
|
69
59
|
* assets. What makes a directory the build output is that there is HTML in it.
|
|
70
60
|
*/
|
|
71
61
|
async function findBuildOutput(cwd) {
|
|
72
|
-
|
|
62
|
+
const candidates = await candidateOutputs(cwd, await readPackageJson(cwd));
|
|
63
|
+
for (const candidate of candidates) {
|
|
73
64
|
const directory = path.join(cwd, candidate);
|
|
74
|
-
|
|
75
|
-
if (!(await stat(directory)).isDirectory()) continue;
|
|
76
|
-
} catch {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
65
|
+
if (!await isDirectory(directory)) continue;
|
|
79
66
|
if ((await glob(["**/*.html", "**/*.htm"], {
|
|
80
67
|
cwd: directory,
|
|
81
68
|
ignore: ["**/node_modules/**"],
|
|
@@ -85,6 +72,30 @@ async function findBuildOutput(cwd) {
|
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
/**
|
|
75
|
+
* How to invoke a package-manager script on this platform.
|
|
76
|
+
*
|
|
77
|
+
* Not `shell: true` with an argument array: Node deprecated that in DEP0190
|
|
78
|
+
* because the arguments are concatenated rather than escaped, and it prints a
|
|
79
|
+
* warning into the middle of somebody's build output. Windows still needs a
|
|
80
|
+
* shell — npm and pnpm are `.cmd` shims there, which cannot be spawned
|
|
81
|
+
* directly — so cmd.exe is invoked explicitly with one command string instead.
|
|
82
|
+
*/
|
|
83
|
+
function scriptCommand(manager, script) {
|
|
84
|
+
if (process.platform !== "win32") return {
|
|
85
|
+
command: manager,
|
|
86
|
+
args: ["run", script]
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
command: process.env["ComSpec"] ?? "cmd.exe",
|
|
90
|
+
args: [
|
|
91
|
+
"/d",
|
|
92
|
+
"/s",
|
|
93
|
+
"/c",
|
|
94
|
+
`${manager} run ${script}`
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
88
99
|
* Run one of the project's own scripts and wait for it.
|
|
89
100
|
*
|
|
90
101
|
* stdio is inherited so the build's output goes where the user expects it. This
|
|
@@ -96,14 +107,14 @@ async function runScript(cwd, script) {
|
|
|
96
107
|
const manager = await detectPackageManager(cwd);
|
|
97
108
|
const command = `${manager} run ${script}`;
|
|
98
109
|
return new Promise((resolve) => {
|
|
99
|
-
const
|
|
110
|
+
const { command: bin, args } = scriptCommand(manager, script);
|
|
111
|
+
const child = spawn(bin, args, {
|
|
100
112
|
cwd,
|
|
101
113
|
stdio: [
|
|
102
114
|
"ignore",
|
|
103
115
|
"inherit",
|
|
104
116
|
"inherit"
|
|
105
|
-
]
|
|
106
|
-
shell: process.platform === "win32"
|
|
117
|
+
]
|
|
107
118
|
});
|
|
108
119
|
child.on("error", () => resolve({
|
|
109
120
|
ok: false,
|
|
@@ -138,15 +149,14 @@ async function answers(origin) {
|
|
|
138
149
|
* if nothing came up before the timeout, having already stopped the process.
|
|
139
150
|
*/
|
|
140
151
|
async function startServer(cwd, script) {
|
|
141
|
-
const
|
|
142
|
-
const child = spawn(
|
|
152
|
+
const { command: bin, args } = scriptCommand(await detectPackageManager(cwd), script);
|
|
153
|
+
const child = spawn(bin, args, {
|
|
143
154
|
cwd,
|
|
144
155
|
stdio: [
|
|
145
156
|
"ignore",
|
|
146
157
|
"pipe",
|
|
147
158
|
"pipe"
|
|
148
159
|
],
|
|
149
|
-
shell: process.platform === "win32",
|
|
150
160
|
detached: process.platform !== "win32"
|
|
151
161
|
});
|
|
152
162
|
let announced;
|
|
@@ -255,4 +265,4 @@ async function autoDetectSource(cwd, options = {}) {
|
|
|
255
265
|
};
|
|
256
266
|
}
|
|
257
267
|
//#endregion
|
|
258
|
-
export { autoDetectSource };
|
|
268
|
+
export { autoDetectSource, readPackageJson };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { _ as withDefault, a as isoDateTime, c as object, d as record, f as safeParse, o as nullable, p as string, s as number, t as array } from "./schema-CMZ8ItGk.js";
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
2
|
+
import { n as isDirectory } from "./fs-BmPtmFke.js";
|
|
3
|
+
import { a as impactRank, n as IMPACT_LEVELS } from "./impact-YdoOtFqm.js";
|
|
4
|
+
import { a as standardsReference, i as escapeText, r as escapeAttribute } from "./text-BFmNtMsV.js";
|
|
4
5
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
5
|
-
import { readFile, readdir
|
|
6
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
//#region src/statement/error.ts
|
|
@@ -110,14 +111,10 @@ function toImpact(value) {
|
|
|
110
111
|
}
|
|
111
112
|
/**
|
|
112
113
|
* Most severe first, then by rule id so two runs of the same build order the
|
|
113
|
-
* list identically.
|
|
114
|
-
* same reasoning as `--fail-on`: a missing impact is a gap in what we know, not
|
|
115
|
-
* evidence that the barrier is harmless.
|
|
114
|
+
* list identically.
|
|
116
115
|
*/
|
|
117
116
|
function bySeverityThenRule(a, b) {
|
|
118
|
-
|
|
119
|
-
const difference = rank(b) - rank(a);
|
|
120
|
-
return difference === 0 ? a.ruleId.localeCompare(b.ruleId) : difference;
|
|
117
|
+
return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
|
|
121
118
|
}
|
|
122
119
|
//#endregion
|
|
123
120
|
//#region src/statement/html.ts
|
|
@@ -524,9 +521,6 @@ function reasonScope(reason) {
|
|
|
524
521
|
isFixPlanned: reason === "fix-planned"
|
|
525
522
|
};
|
|
526
523
|
}
|
|
527
|
-
function standardsReference(successCriteria, en301549) {
|
|
528
|
-
return [...successCriteria.map((criterion) => `WCAG ${criterion}`), ...en301549.map((clause) => `EN 301 549 ${clause}`)].join(", ");
|
|
529
|
-
}
|
|
530
524
|
/**
|
|
531
525
|
* 2026-08-20 becomes 20. August 2026 or 20 August 2026.
|
|
532
526
|
*
|
|
@@ -578,9 +572,7 @@ async function findTemplateDirectory() {
|
|
|
578
572
|
path.join(here, "..", "statement", "templates"),
|
|
579
573
|
path.join(here, "statement", "templates")
|
|
580
574
|
];
|
|
581
|
-
for (const candidate of candidates)
|
|
582
|
-
if ((await stat(candidate)).isDirectory()) return candidate;
|
|
583
|
-
} catch {}
|
|
575
|
+
for (const candidate of candidates) if (await isDirectory(candidate)) return candidate;
|
|
584
576
|
throw new StatementError(`Could not locate the statement templates. Looked in: ${candidates.join(", ")}`);
|
|
585
577
|
}
|
|
586
578
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
1
2
|
import axe from "axe-core";
|
|
2
3
|
//#region src/audit/result.ts
|
|
3
4
|
/** WCAG 2.2 AA and everything it builds on. Best-practice rules stay off. */
|
|
@@ -8,6 +9,42 @@ const DEFAULT_TAGS = [
|
|
|
8
9
|
"wcag21aa",
|
|
9
10
|
"wcag22aa"
|
|
10
11
|
];
|
|
12
|
+
/**
|
|
13
|
+
* The URL a page is audited under, shared by every runner so their reports name
|
|
14
|
+
* the same page. Without a base URL that is the file it came off disk as.
|
|
15
|
+
*/
|
|
16
|
+
function pageUrl(page, baseUrl) {
|
|
17
|
+
if (!baseUrl) return pathToFileURL(page.absolutePath).href;
|
|
18
|
+
return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The elements a finding points at, as selector and markup.
|
|
22
|
+
*
|
|
23
|
+
* A rule can fail with no node attached — a document-level rule. It still has
|
|
24
|
+
* an identity, so it gets one empty element: the baseline writer, the baseline
|
|
25
|
+
* matcher, the issues view and SARIF all have to agree on what that identity
|
|
26
|
+
* is, and they only do because they all come through here.
|
|
27
|
+
*/
|
|
28
|
+
function findingElements(finding) {
|
|
29
|
+
if (finding.nodes.length === 0) return [{
|
|
30
|
+
selector: "",
|
|
31
|
+
html: ""
|
|
32
|
+
}];
|
|
33
|
+
return finding.nodes.map((node) => ({
|
|
34
|
+
selector: node.target.join(" "),
|
|
35
|
+
html: node.html
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
/** Every rule this page reached any verdict on, in one list. */
|
|
39
|
+
function ruleOutcomes(audit) {
|
|
40
|
+
return [
|
|
41
|
+
...audit.violations,
|
|
42
|
+
...audit.accepted ?? [],
|
|
43
|
+
...audit.incomplete,
|
|
44
|
+
...audit.passes,
|
|
45
|
+
...audit.inapplicable
|
|
46
|
+
];
|
|
47
|
+
}
|
|
11
48
|
function runOptions(tags) {
|
|
12
49
|
return {
|
|
13
50
|
runOnly: {
|
|
@@ -165,4 +202,4 @@ function enClauses(tags) {
|
|
|
165
202
|
return [...clauses].sort();
|
|
166
203
|
}
|
|
167
204
|
//#endregion
|
|
168
|
-
export {
|
|
205
|
+
export { ruleOutcomes as a, pageUrl as i, failedPage as n, runOptions as o, findingElements as r, shapeResults as s, DEFAULT_TAGS as t };
|