eaa-kit 0.2.1 → 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 +80 -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-DSRnXmcd.js → playwright-DYFsGUNd.js} +70 -23
- package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
- package/dist/{project-CufCqIE2.js → project-DW08TseF.js} +8 -21
- 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-B2dIKpJ5.js +0 -2
- package/dist/audit-DXpKkXsC.js +0 -950
- 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,5 @@
|
|
|
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";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { stat } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -74,7 +75,7 @@ async function handle(root, requestUrl, response) {
|
|
|
74
75
|
}
|
|
75
76
|
try {
|
|
76
77
|
const target = (await stat(file)).isDirectory() ? path.join(file, "index.html") : file;
|
|
77
|
-
if (!await
|
|
78
|
+
if (!await isFile(target)) {
|
|
78
79
|
response.writeHead(404).end();
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
@@ -88,13 +89,6 @@ async function handle(root, requestUrl, response) {
|
|
|
88
89
|
response.writeHead(404).end();
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
async function isReadableFile(candidate) {
|
|
92
|
-
try {
|
|
93
|
-
return (await stat(candidate)).isFile();
|
|
94
|
-
} catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
92
|
/** Resolves a request path inside the root, or nothing if it escapes it. */
|
|
99
93
|
function resolveFile(root, requestUrl) {
|
|
100
94
|
const { pathname } = new URL(requestUrl, "http://127.0.0.1");
|
|
@@ -151,7 +145,7 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
151
145
|
browser = await chromium.launch({ headless: true });
|
|
152
146
|
} catch (cause) {
|
|
153
147
|
await server?.close();
|
|
154
|
-
throw cause;
|
|
148
|
+
throw launchFailure(cause);
|
|
155
149
|
}
|
|
156
150
|
try {
|
|
157
151
|
const context = await browser.newContext({
|
|
@@ -176,7 +170,7 @@ async function auditOne(context, origin, page, options) {
|
|
|
176
170
|
const identity = {
|
|
177
171
|
relativePath: page.relativePath,
|
|
178
172
|
absolutePath: page.absolutePath,
|
|
179
|
-
url:
|
|
173
|
+
url: pageUrl(page, options.baseUrl),
|
|
180
174
|
engine: "browser"
|
|
181
175
|
};
|
|
182
176
|
const tab = await context.newPage();
|
|
@@ -218,9 +212,27 @@ async function auditOne(context, origin, page, options) {
|
|
|
218
212
|
function servedUrl(origin, relativePath) {
|
|
219
213
|
return `${origin}/${relativePath.split("/").map(encodeURIComponent).join("/")}`;
|
|
220
214
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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"}`);
|
|
224
236
|
}
|
|
225
237
|
/**
|
|
226
238
|
* Playwright is an optional peer dependency: the browserless path must never
|
|
@@ -229,18 +241,53 @@ function reportedUrl(page, baseUrl) {
|
|
|
229
241
|
* problems with different fixes.
|
|
230
242
|
*/
|
|
231
243
|
async function loadChromium(cwd = process.cwd()) {
|
|
232
|
-
|
|
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) {
|
|
233
261
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (module === void 0) try {
|
|
238
|
-
module = await import("playwright");
|
|
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);
|
|
239
265
|
} catch {
|
|
240
|
-
|
|
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;
|
|
241
290
|
}
|
|
242
|
-
if (!module.chromium) throw new BrowserUnavailableError("Playwright is installed but exports no chromium launcher");
|
|
243
|
-
return module.chromium;
|
|
244
291
|
}
|
|
245
292
|
//#endregion
|
|
246
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/**"],
|
|
@@ -278,4 +265,4 @@ async function autoDetectSource(cwd, options = {}) {
|
|
|
278
265
|
};
|
|
279
266
|
}
|
|
280
267
|
//#endregion
|
|
281
|
-
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 };
|