eaa-kit 0.2.1 → 0.4.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 +51 -4
- 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-CPoZMXGM.js +779 -0
- package/dist/audit-CpXH2Mk8.js +2 -0
- package/dist/{baseline-CV_3lbER.js → baseline-22Y1NWxM.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-DB9CZGnV.js} +17 -27
- package/dist/cli/index.js +336 -142
- package/dist/collect-CFM8gEVv.js +134 -0
- package/dist/command-D8l_oYbV.js +77 -0
- package/dist/component-C3GL1Mnu.js +146 -0
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B4IBKMO8.js +521 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-BiI1Lau_.js} +13 -23
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/frameworks-B4ClIJgE.js +314 -0
- package/dist/frameworks-etFg_O8K.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/html-BGTO3ypW.js +543 -0
- package/dist/{impact-DvgBjupx.js → impact-DZt2oBCP.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DIWDE35F.js} +13 -10
- package/dist/jsdom-4IMzv0eE.js +3 -0
- package/dist/jsdom-DCpGSLfW.js +81 -0
- package/dist/{json-C9xS1PNC.js → json-DjEvy1nX.js} +19 -22
- package/dist/json-QQuFIw1W.js +2 -0
- package/dist/{load-sCkKsvGQ.js → load-5wRGLvub.js} +3 -9
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DSRnXmcd.js → playwright-BWniOain.js} +70 -23
- package/dist/{pool-DixLeu8L.js → pool-BMevaLWD.js} +4 -12
- package/dist/{project-CufCqIE2.js → project-CzOnkLH6.js} +14 -21
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-Dtowi2EC.js +321 -0
- package/dist/{render-BO0nVrrZ.js → render-DrvXRCEn.js} +8 -16
- package/dist/{result-2aZPfM8w.js → result-DoamKFsp.js} +115 -1
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-DB34BSOZ.js +53 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-SR3_lLYd.js} +22 -36
- package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
- package/dist/text-CKKpzkYM.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +36 -9
- package/dist/audit-B2dIKpJ5.js +0 -2
- package/dist/audit-DXpKkXsC.js +0 -950
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/html-BLEuzep6.js +0 -337
- package/dist/jsdom-BEu6Ra_2.js +0 -163
- package/dist/jsdom-C6dIyaxN.js +0 -3
- package/dist/routes-BxbSZKXC.js +0 -123
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { i as toPosix, n as isDirectory } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { i as detectFramework } from "./frameworks-B4ClIJgE.js";
|
|
3
|
+
import { i as readPackageJson } from "./project-CzOnkLH6.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { glob } from "tinyglobby";
|
|
6
|
+
//#region src/audit/routes.ts
|
|
7
|
+
/**
|
|
8
|
+
* A path segment that serves more than one page, or none.
|
|
9
|
+
*
|
|
10
|
+
* Dynamic segments are refused rather than resolved: `[slug]` stands for every
|
|
11
|
+
* post there is, and picking one would name a file that did not produce the
|
|
12
|
+
* page in front of the reader. Private and slot folders contribute no route at
|
|
13
|
+
* all.
|
|
14
|
+
*/
|
|
15
|
+
function classifyBracketSegment(segment) {
|
|
16
|
+
if (/^\(.*\)$/.test(segment)) return "skip";
|
|
17
|
+
if (/^[[(]|^_|^@/.test(segment)) return "refuse";
|
|
18
|
+
return "route";
|
|
19
|
+
}
|
|
20
|
+
/** Drop the extension, and fold an index file into the directory it sits in. */
|
|
21
|
+
function withoutExtension(file) {
|
|
22
|
+
const route = file.replace(/\.[^./]+$/, "").replace(/\/index$/, "");
|
|
23
|
+
return route === "index" ? "" : route;
|
|
24
|
+
}
|
|
25
|
+
/** The directory a route file sits in, for routers that put the route there. */
|
|
26
|
+
function containingDirectory(file) {
|
|
27
|
+
return file.includes("/") ? file.replace(/\/[^/]+$/, "") : "";
|
|
28
|
+
}
|
|
29
|
+
/** Keep the segments that are routes, refusing the file if any is dynamic. */
|
|
30
|
+
function joinSegments(route) {
|
|
31
|
+
const kept = [];
|
|
32
|
+
for (const segment of route.split("/").filter((part) => part !== "")) {
|
|
33
|
+
const kind = classifyBracketSegment(segment);
|
|
34
|
+
if (kind === "refuse") return void 0;
|
|
35
|
+
if (kind === "skip") continue;
|
|
36
|
+
kept.push(segment);
|
|
37
|
+
}
|
|
38
|
+
return kept.join("/");
|
|
39
|
+
}
|
|
40
|
+
/** `pages/kontakt.tsx` serves `/kontakt`; the filename is the route. */
|
|
41
|
+
function fileNamedRoute(file) {
|
|
42
|
+
return joinSegments(withoutExtension(file));
|
|
43
|
+
}
|
|
44
|
+
/** `app/kontakt/page.tsx` serves `/kontakt`; the directory is the route. */
|
|
45
|
+
function directoryNamedRoute(file) {
|
|
46
|
+
return joinSegments(containingDirectory(file));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A documentation tree whose directory layout is the URL layout.
|
|
50
|
+
*
|
|
51
|
+
* No dynamic segments to worry about — these are files an author wrote, one per
|
|
52
|
+
* page — so the only work is folding the index file into its directory.
|
|
53
|
+
*/
|
|
54
|
+
function mirroredRoute(file) {
|
|
55
|
+
return withoutExtension(file);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Remix and React Router flat routes: `blog.post.tsx` serves `/blog/post`.
|
|
59
|
+
*
|
|
60
|
+
* The dot is the separator, `_index` is a directory's own page, and a trailing
|
|
61
|
+
* underscore on a segment escapes a parent layout without changing the URL. A
|
|
62
|
+
* `$` segment is dynamic and refused, as `[slug]` is elsewhere.
|
|
63
|
+
*/
|
|
64
|
+
function remixRoute(file) {
|
|
65
|
+
const withoutRouteFile = file.replace(/\/route\.[^./]+$/, "");
|
|
66
|
+
const flat = withoutRouteFile === file ? withoutExtension(file) : toPosix(withoutRouteFile);
|
|
67
|
+
const leaf = flat.includes("/") ? flat.split("/").pop() : flat;
|
|
68
|
+
if (leaf === "") return void 0;
|
|
69
|
+
const kept = [];
|
|
70
|
+
for (const raw of leaf.split(".")) {
|
|
71
|
+
const segment = raw.replace(/_$/, "");
|
|
72
|
+
if (segment === "") continue;
|
|
73
|
+
if (segment === "_index") continue;
|
|
74
|
+
if (segment.startsWith("$")) return void 0;
|
|
75
|
+
if (segment.includes("[")) return void 0;
|
|
76
|
+
kept.push(segment);
|
|
77
|
+
}
|
|
78
|
+
return kept.join("/");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Hugo content: `content/posts/hello.md` serves `/posts/hello/`.
|
|
82
|
+
*
|
|
83
|
+
* `_index.md` is a section's own page rather than a page called `_index`, which
|
|
84
|
+
* is the one place Hugo's leading underscore does not mean "private".
|
|
85
|
+
*/
|
|
86
|
+
function hugoRoute(file) {
|
|
87
|
+
const withoutIndex = file.replace(/(^|\/)_index\.[^./]+$/, "");
|
|
88
|
+
if (withoutIndex !== file) return toPosix(withoutIndex);
|
|
89
|
+
return withoutExtension(file);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Conventions, most specific first.
|
|
93
|
+
*
|
|
94
|
+
* Order still matters where the registry knows nothing about a project, which
|
|
95
|
+
* is the case for a plain directory of HTML and for anything the registry has
|
|
96
|
+
* not heard of.
|
|
97
|
+
*/
|
|
98
|
+
const CONVENTIONS = [
|
|
99
|
+
{
|
|
100
|
+
framework: "next-app",
|
|
101
|
+
ids: ["next"],
|
|
102
|
+
dir: "app",
|
|
103
|
+
pattern: "**/page.{tsx,ts,jsx,js,mdx}",
|
|
104
|
+
toRoute: directoryNamedRoute
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
framework: "next-app",
|
|
108
|
+
ids: ["next"],
|
|
109
|
+
dir: "src/app",
|
|
110
|
+
pattern: "**/page.{tsx,ts,jsx,js,mdx}",
|
|
111
|
+
toRoute: directoryNamedRoute
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
framework: "remix",
|
|
115
|
+
ids: ["remix"],
|
|
116
|
+
dir: "app/routes",
|
|
117
|
+
pattern: "**/*.{tsx,ts,jsx,js,mdx}",
|
|
118
|
+
toRoute: remixRoute
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
framework: "starlight",
|
|
122
|
+
ids: ["astro"],
|
|
123
|
+
dir: "src/content/docs",
|
|
124
|
+
pattern: "**/*.{md,mdx,mdoc}",
|
|
125
|
+
toRoute: mirroredRoute
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
framework: "astro",
|
|
129
|
+
ids: ["astro"],
|
|
130
|
+
dir: "src/pages",
|
|
131
|
+
pattern: "**/*.{astro,md,mdx,html}",
|
|
132
|
+
toRoute: fileNamedRoute
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
framework: "gatsby",
|
|
136
|
+
ids: ["gatsby"],
|
|
137
|
+
dir: "src/pages",
|
|
138
|
+
pattern: "**/*.{tsx,ts,jsx,js,md,mdx}",
|
|
139
|
+
toRoute: fileNamedRoute
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
framework: "next-pages",
|
|
143
|
+
ids: ["next"],
|
|
144
|
+
dir: "pages",
|
|
145
|
+
pattern: "**/*.{tsx,ts,jsx,js,mdx}",
|
|
146
|
+
toRoute: fileNamedRoute
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
framework: "next-pages",
|
|
150
|
+
ids: ["next"],
|
|
151
|
+
dir: "src/pages",
|
|
152
|
+
pattern: "**/*.{tsx,ts,jsx,js,mdx}",
|
|
153
|
+
toRoute: fileNamedRoute
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
framework: "nuxt",
|
|
157
|
+
ids: ["nuxt"],
|
|
158
|
+
dir: "pages",
|
|
159
|
+
pattern: "**/*.vue",
|
|
160
|
+
toRoute: fileNamedRoute
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
framework: "sveltekit",
|
|
164
|
+
ids: ["sveltekit"],
|
|
165
|
+
dir: "src/routes",
|
|
166
|
+
pattern: "**/+page.{svelte,ts,js}",
|
|
167
|
+
toRoute: directoryNamedRoute
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
framework: "docusaurus",
|
|
171
|
+
ids: ["docusaurus"],
|
|
172
|
+
dir: "docs",
|
|
173
|
+
pattern: "**/*.{md,mdx}",
|
|
174
|
+
toRoute: (file) => {
|
|
175
|
+
const route = mirroredRoute(file);
|
|
176
|
+
return route === void 0 ? void 0 : route === "" ? "docs" : `docs/${route}`;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
framework: "vitepress",
|
|
181
|
+
ids: ["vitepress"],
|
|
182
|
+
dir: "docs",
|
|
183
|
+
pattern: "**/*.md",
|
|
184
|
+
toRoute: mirroredRoute
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
framework: "hugo",
|
|
188
|
+
ids: ["hugo"],
|
|
189
|
+
dir: "content",
|
|
190
|
+
pattern: "**/*.{md,html}",
|
|
191
|
+
toRoute: hugoRoute
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
/** Every page path a build could have emitted for one route. */
|
|
195
|
+
function emittedPathsFor(route) {
|
|
196
|
+
if (route === "") return [
|
|
197
|
+
"/",
|
|
198
|
+
"index.html",
|
|
199
|
+
"index.htm"
|
|
200
|
+
];
|
|
201
|
+
return [
|
|
202
|
+
route,
|
|
203
|
+
`${route}/`,
|
|
204
|
+
`${route}.html`,
|
|
205
|
+
`${route}/index.html`
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Build a page-to-source map by reading the project's route files.
|
|
210
|
+
*
|
|
211
|
+
* Returns undefined when the project uses no convention this understands, which
|
|
212
|
+
* is not a failure — most builds are not framework projects.
|
|
213
|
+
*/
|
|
214
|
+
async function buildRouteMap(cwd, pkg) {
|
|
215
|
+
for (const convention of await orderedConventions(cwd, pkg)) {
|
|
216
|
+
const map = await mapConvention(cwd, convention);
|
|
217
|
+
if (map !== void 0) return map;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Conventions to try, the detected framework's first.
|
|
222
|
+
*
|
|
223
|
+
* Without this, `src/pages` is claimed by whichever convention is declared
|
|
224
|
+
* earliest, so a Gatsby project was reported as `next-pages`. The mapping
|
|
225
|
+
* happened to be right and the name was wrong, which is the kind of detail a
|
|
226
|
+
* reader notices and stops trusting the rest of the report over.
|
|
227
|
+
*/
|
|
228
|
+
async function orderedConventions(cwd, pkg) {
|
|
229
|
+
const detected = await detectFramework(cwd, pkg ?? await readPackageJson(cwd));
|
|
230
|
+
if (detected === void 0) return [...CONVENTIONS];
|
|
231
|
+
const id = detected.framework.id;
|
|
232
|
+
const mine = CONVENTIONS.filter((convention) => convention.ids.includes(id));
|
|
233
|
+
const rest = CONVENTIONS.filter((convention) => !convention.ids.includes(id));
|
|
234
|
+
return [...mine, ...rest];
|
|
235
|
+
}
|
|
236
|
+
/** Read one convention's directory, or undefined when it has nothing to say. */
|
|
237
|
+
async function mapConvention(cwd, convention) {
|
|
238
|
+
const directory = path.join(cwd, convention.dir);
|
|
239
|
+
if (!await isDirectory(directory)) return void 0;
|
|
240
|
+
const files = await glob([convention.pattern], {
|
|
241
|
+
cwd: directory,
|
|
242
|
+
ignore: ["**/node_modules/**"],
|
|
243
|
+
onlyFiles: true
|
|
244
|
+
});
|
|
245
|
+
if (files.length === 0) return void 0;
|
|
246
|
+
const sources = /* @__PURE__ */ new Map();
|
|
247
|
+
for (const file of files) {
|
|
248
|
+
const route = convention.toRoute(toPosix(file));
|
|
249
|
+
if (route === void 0) continue;
|
|
250
|
+
const source = `${convention.dir}/${toPosix(file)}`;
|
|
251
|
+
for (const emitted of emittedPathsFor(route)) if (!sources.has(emitted)) sources.set(emitted, source);
|
|
252
|
+
}
|
|
253
|
+
if (sources.size === 0) return void 0;
|
|
254
|
+
return {
|
|
255
|
+
framework: convention.framework,
|
|
256
|
+
sources
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** The source file for an audited page, if the map knows one. */
|
|
260
|
+
function sourceFor(map, pagePath) {
|
|
261
|
+
if (map === void 0) return void 0;
|
|
262
|
+
return map.sources.get(pagePath) ?? map.sources.get(pagePath.replace(/^\//, ""));
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { buildRouteMap, sourceFor };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { t as ImpactLevel } from "./impact-EEB9ZXmC.js";
|
|
2
|
+
//#region src/cli/audit.d.ts
|
|
3
|
+
declare const OUTPUT_FORMATS: readonly ['console', 'json', 'sarif', 'html'];
|
|
4
|
+
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/integration/run.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* What a build-time integration does once the build has written its files.
|
|
9
|
+
*
|
|
10
|
+
* Astro, Vite and Next all reach the same point by different routes: a
|
|
11
|
+
* directory exists, and the build should stop if what is in it fails the
|
|
12
|
+
* threshold. Only the hook name and the logger differ, so only those live in
|
|
13
|
+
* the integrations.
|
|
14
|
+
*/
|
|
15
|
+
interface IntegrationOptions {
|
|
16
|
+
/** Lowest impact that fails the build. Defaults to 'serious'. */
|
|
17
|
+
failOn?: ImpactLevel;
|
|
18
|
+
include?: string[];
|
|
19
|
+
exclude?: string[];
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
/** Audit in real Chromium. Needs the playwright peer. */
|
|
22
|
+
browser?: boolean;
|
|
23
|
+
concurrency?: number;
|
|
24
|
+
baseline?: string;
|
|
25
|
+
format?: OutputFormat;
|
|
26
|
+
/** Write the report here instead of the build log. */
|
|
27
|
+
output?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Report without failing the build. For the week it takes to adopt this on a
|
|
30
|
+
* site that already exists — a baseline is the honest way to go green after
|
|
31
|
+
* that.
|
|
32
|
+
*/
|
|
33
|
+
failBuild?: boolean;
|
|
34
|
+
/** Skip entirely. For turning it off per environment without unwiring it. */
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Thrown to fail the build. Its own class so a consumer can tell an audit
|
|
39
|
+
* failure from the build tool falling over.
|
|
40
|
+
*/
|
|
41
|
+
declare class BuildAuditError extends Error {
|
|
42
|
+
readonly name = "BuildAuditError";
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { IntegrationOptions as n, BuildAuditError as t };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/integration/run.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where an integration writes when its host offers no logger of its own.
|
|
4
|
+
*
|
|
5
|
+
* stderr rather than stdout, so a line about accessibility never lands in the
|
|
6
|
+
* middle of a report being piped to a file, and prefixed either way: a message
|
|
7
|
+
* in somebody's build log has to say what produced it.
|
|
8
|
+
*/
|
|
9
|
+
function stderrLogger() {
|
|
10
|
+
const write = (message) => {
|
|
11
|
+
process.stderr.write(`eaa-kit: ${message}\n`);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
info: write,
|
|
15
|
+
warn: write,
|
|
16
|
+
error: write
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Thrown to fail the build. Its own class so a consumer can tell an audit
|
|
21
|
+
* failure from the build tool falling over.
|
|
22
|
+
*/
|
|
23
|
+
var BuildAuditError = class extends Error {
|
|
24
|
+
name = "BuildAuditError";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Audit a finished build and decide whether it may proceed.
|
|
28
|
+
*
|
|
29
|
+
* Returns normally when the build should continue, and throws BuildAuditError
|
|
30
|
+
* when it should not.
|
|
31
|
+
*/
|
|
32
|
+
async function auditBuild(directory, options, logger) {
|
|
33
|
+
const { enabled, failBuild, ...auditOptions } = options;
|
|
34
|
+
if (enabled === false) {
|
|
35
|
+
logger.info("skipped (enabled: false)");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const { runAuditCommand } = await import("./audit-CpXH2Mk8.js");
|
|
39
|
+
const { exitCode } = await runAuditCommand(directory, auditOptions);
|
|
40
|
+
if (exitCode === 0) {
|
|
41
|
+
logger.info("no violations at or above the threshold");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const message = exitCode === 2 ? "the audit could not be completed, so this build was not checked" : "accessibility violations at or above the threshold";
|
|
45
|
+
if (failBuild === false) {
|
|
46
|
+
logger.warn(`${message} (failBuild: false, so the build continues)`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
logger.error(message);
|
|
50
|
+
throw new BuildAuditError(`eaa-kit: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { auditBuild as n, stderrLogger as r, BuildAuditError as t };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as isImpactLevel } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { o as standardsReference } from "./text-CKKpzkYM.js";
|
|
2
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
3
4
|
import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
|
|
5
|
+
import { a as findingElements, s as ruleOutcomes } from "./result-DoamKFsp.js";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
//#region src/audit/report/sarif.ts
|
|
6
8
|
const SARIF_VERSION = "2.1.0";
|
|
@@ -73,7 +75,7 @@ function buildSarifReport(audits, options) {
|
|
|
73
75
|
toolExecutionNotifications: notifications
|
|
74
76
|
}],
|
|
75
77
|
results,
|
|
76
|
-
properties: summaryProperties(audits)
|
|
78
|
+
properties: summaryProperties(audits, options.completeness)
|
|
77
79
|
}]
|
|
78
80
|
};
|
|
79
81
|
}
|
|
@@ -92,29 +94,16 @@ const SUPPRESSED = [{
|
|
|
92
94
|
function toResults(finding, ruleIndex, uri, suppressions) {
|
|
93
95
|
const level = toSarifLevel(finding.impact);
|
|
94
96
|
const location = { physicalLocation: { artifactLocation: { uri } } };
|
|
95
|
-
|
|
97
|
+
return findingElements(finding).map(({ selector, html }) => ({
|
|
96
98
|
ruleId: finding.ruleId,
|
|
97
99
|
ruleIndex,
|
|
98
100
|
level,
|
|
99
101
|
kind: "fail",
|
|
100
|
-
message: { text: finding.help },
|
|
102
|
+
message: { text: selector === "" ? finding.help : `${finding.help}. Element: ${selector}` },
|
|
101
103
|
locations: [location],
|
|
102
|
-
partialFingerprints: fingerprint(finding.ruleId,
|
|
104
|
+
partialFingerprints: fingerprint(finding.ruleId, selector, html),
|
|
103
105
|
...suppressions ? { suppressions } : {}
|
|
104
|
-
}
|
|
105
|
-
return finding.nodes.map((node) => {
|
|
106
|
-
const selector = node.target.join(" ");
|
|
107
|
-
return {
|
|
108
|
-
ruleId: finding.ruleId,
|
|
109
|
-
ruleIndex,
|
|
110
|
-
level,
|
|
111
|
-
kind: "fail",
|
|
112
|
-
message: { text: `${finding.help}. Element: ${selector}` },
|
|
113
|
-
locations: [location],
|
|
114
|
-
partialFingerprints: fingerprint(finding.ruleId, selector, node.html),
|
|
115
|
-
...suppressions ? { suppressions } : {}
|
|
116
|
-
};
|
|
117
|
-
});
|
|
106
|
+
}));
|
|
118
107
|
}
|
|
119
108
|
/**
|
|
120
109
|
* Identifies an alert across runs. Deliberately excludes the file path, so that
|
|
@@ -127,25 +116,14 @@ function fingerprint(ruleId, selector, html) {
|
|
|
127
116
|
/** Every rule the run knows about, so the catalogue is complete in GitHub. */
|
|
128
117
|
function buildRules(audits) {
|
|
129
118
|
const rules = /* @__PURE__ */ new Map();
|
|
130
|
-
for (const audit of audits) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
...audit.accepted ?? [],
|
|
134
|
-
...audit.incomplete,
|
|
135
|
-
...audit.passes,
|
|
136
|
-
...audit.inapplicable
|
|
137
|
-
];
|
|
138
|
-
for (const outcome of outcomes) {
|
|
139
|
-
if (rules.has(outcome.ruleId)) continue;
|
|
140
|
-
rules.set(outcome.ruleId, toSarifRule(outcome));
|
|
141
|
-
}
|
|
119
|
+
for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
|
|
120
|
+
if (rules.has(outcome.ruleId)) continue;
|
|
121
|
+
rules.set(outcome.ruleId, toSarifRule(outcome));
|
|
142
122
|
}
|
|
143
123
|
return [...rules.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
144
124
|
}
|
|
145
125
|
function toSarifRule(outcome) {
|
|
146
|
-
const
|
|
147
|
-
const clauses = outcome.enClauses.map((clause) => `EN 301 549 ${clause}`);
|
|
148
|
-
const references = [...criteria, ...clauses].join(", ");
|
|
126
|
+
const references = standardsReference(outcome.successCriteria, outcome.enClauses);
|
|
149
127
|
return {
|
|
150
128
|
id: outcome.ruleId,
|
|
151
129
|
shortDescription: { text: outcome.help },
|
|
@@ -162,7 +140,7 @@ function toSarifRule(outcome) {
|
|
|
162
140
|
* Coverage that has no place in `results` but should not vanish: a SARIF log
|
|
163
141
|
* with no results must not be mistaken for "everything was checked".
|
|
164
142
|
*/
|
|
165
|
-
function summaryProperties(audits) {
|
|
143
|
+
function summaryProperties(audits, completeness) {
|
|
166
144
|
let needsReview = 0;
|
|
167
145
|
let notEvaluated = 0;
|
|
168
146
|
const notEvaluatedRules = /* @__PURE__ */ new Set();
|
|
@@ -175,7 +153,15 @@ function summaryProperties(audits) {
|
|
|
175
153
|
pages: audits.length,
|
|
176
154
|
needsReview,
|
|
177
155
|
notEvaluated,
|
|
178
|
-
notEvaluatedRules: [...notEvaluatedRules].sort()
|
|
156
|
+
notEvaluatedRules: [...notEvaluatedRules].sort(),
|
|
157
|
+
...completeness ? {
|
|
158
|
+
complete: completeness.complete,
|
|
159
|
+
discovery: completeness.discovery,
|
|
160
|
+
pagesAudited: completeness.audited,
|
|
161
|
+
pagesErrored: completeness.errored,
|
|
162
|
+
pagesUnreachable: completeness.unreachable.length,
|
|
163
|
+
truncated: completeness.truncated
|
|
164
|
+
} : {}
|
|
179
165
|
};
|
|
180
166
|
}
|
|
181
167
|
/**
|
|
@@ -64,6 +64,9 @@ function string(options = {}) {
|
|
|
64
64
|
return value;
|
|
65
65
|
} };
|
|
66
66
|
}
|
|
67
|
+
function boolean() {
|
|
68
|
+
return { read: (value, path, issues) => typeof value === "boolean" ? value : fail(issues, path, "expected true or false") };
|
|
69
|
+
}
|
|
67
70
|
function number() {
|
|
68
71
|
return { read: (value, path, issues) => typeof value === "number" && Number.isFinite(value) ? value : fail(issues, path, "expected a number") };
|
|
69
72
|
}
|
|
@@ -189,4 +192,4 @@ function email() {
|
|
|
189
192
|
} };
|
|
190
193
|
}
|
|
191
194
|
//#endregion
|
|
192
|
-
export {
|
|
195
|
+
export { url as _, isoDate as a, number as c, pipe as d, record as f, union as g, transform as h, enumeration as i, object as l, string as m, boolean as n, isoDateTime as o, safeParse as p, email as r, nullable as s, array as t, optional as u, withDefault as v };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/text.ts
|
|
2
|
+
/**
|
|
3
|
+
* Text this package writes: escaping, counting and the standards references
|
|
4
|
+
* that appear in every report and in the statement.
|
|
5
|
+
*
|
|
6
|
+
* Both documents eaa-kit produces embed text it did not write: an issue
|
|
7
|
+
* description from a config file, axe-core's help text, and — in the audit
|
|
8
|
+
* report — the markup of the element that failed, which is by definition
|
|
9
|
+
* arbitrary HTML from somebody's build. Getting the escaping wrong in the
|
|
10
|
+
* report would mean a page that fails an accessibility audit for having a stray
|
|
11
|
+
* `<script>` hands that script to whoever opens the report.
|
|
12
|
+
*/
|
|
13
|
+
/** For text nodes. Leaves quotes alone, which are fine between tags. */
|
|
14
|
+
function escapeText(value) {
|
|
15
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16
|
+
}
|
|
17
|
+
/** For attribute values, where a quote would end the attribute. */
|
|
18
|
+
function escapeAttribute(value) {
|
|
19
|
+
return escapeText(value).replace(/"/g, """).replace(/'/g, "'");
|
|
20
|
+
}
|
|
21
|
+
/** `plural(1, 'page')` is `page`, `plural(2, 'page')` is `pages`. */
|
|
22
|
+
function plural(value, noun) {
|
|
23
|
+
return value === 1 ? noun : `${noun}s`;
|
|
24
|
+
}
|
|
25
|
+
/** `count(2, 'page')` is `2 pages`. */
|
|
26
|
+
function count(value, noun) {
|
|
27
|
+
return `${value} ${plural(value, noun)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Element markup on one line, optionally bounded: one minified page must not be
|
|
31
|
+
* able to fill a report or run past a terminal.
|
|
32
|
+
*/
|
|
33
|
+
function collapse(html, max) {
|
|
34
|
+
const flat = html.replace(/\s+/g, " ").trim();
|
|
35
|
+
if (max === void 0 || flat.length <= max) return flat;
|
|
36
|
+
return `${flat.slice(0, max - 1)}…`;
|
|
37
|
+
}
|
|
38
|
+
/** `WCAG 1.1.1, EN 301 549 9.1.1.1`, in that order, or an empty string. */
|
|
39
|
+
function standardsReference(successCriteria, enClauses) {
|
|
40
|
+
return [...successCriteria.map((criterion) => `WCAG ${criterion}`), ...enClauses.map((clause) => `EN 301 549 ${clause}`)].join(", ");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { plural as a, escapeText as i, count as n, standardsReference as o, escapeAttribute as r, collapse as t };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
|
|
2
|
+
//#region src/vite/index.d.ts
|
|
3
|
+
interface EaaKitPluginOptions extends IntegrationOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Directory to audit, relative to the Vite root. Defaults to the build's own
|
|
6
|
+
* `outDir`, which is the only place the plugin can know the files went.
|
|
7
|
+
*/
|
|
8
|
+
directory?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The parts of Vite this plugin touches, described structurally.
|
|
12
|
+
*
|
|
13
|
+
* So the published .d.ts does not reference vite, and installing eaa-kit in a
|
|
14
|
+
* project that has none costs nothing and still typechecks. The same reasoning
|
|
15
|
+
* as the Astro integration and the Playwright runner.
|
|
16
|
+
*/
|
|
17
|
+
interface ResolvedConfigLike {
|
|
18
|
+
root: string;
|
|
19
|
+
build: {
|
|
20
|
+
outDir: string;
|
|
21
|
+
};
|
|
22
|
+
logger?: {
|
|
23
|
+
info(message: string): void;
|
|
24
|
+
warn(message: string): void;
|
|
25
|
+
error(message: string): void;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface VitePluginLike {
|
|
29
|
+
name: string;
|
|
30
|
+
/** Build only: a dev server writes nothing to audit. */
|
|
31
|
+
apply: 'build';
|
|
32
|
+
/** Runs after the bundle is written, which is the earliest the files exist. */
|
|
33
|
+
enforce?: 'post';
|
|
34
|
+
configResolved(config: ResolvedConfigLike): void;
|
|
35
|
+
closeBundle(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
declare function eaaKit(options?: EaaKitPluginOptions): VitePluginLike;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { BuildAuditError, EaaKitPluginOptions, ResolvedConfigLike, VitePluginLike, eaaKit as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { n as auditBuild, t as BuildAuditError } from "../run-DB34BSOZ.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/vite/index.ts
|
|
4
|
+
function eaaKit(options = {}) {
|
|
5
|
+
let directory;
|
|
6
|
+
let logger;
|
|
7
|
+
return {
|
|
8
|
+
name: "eaa-kit",
|
|
9
|
+
apply: "build",
|
|
10
|
+
enforce: "post",
|
|
11
|
+
configResolved(config) {
|
|
12
|
+
directory = path.resolve(config.root, options.directory ?? config.build.outDir);
|
|
13
|
+
logger = prefixed(config.logger);
|
|
14
|
+
},
|
|
15
|
+
async closeBundle() {
|
|
16
|
+
if (directory === void 0 || logger === void 0) throw new BuildAuditError("eaa-kit: the plugin was not given a resolved Vite config");
|
|
17
|
+
await auditBuild(directory, options, logger);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Vite's own logger where there is one, the console otherwise.
|
|
23
|
+
*
|
|
24
|
+
* Prefixed either way: a line about accessibility in the middle of a build log
|
|
25
|
+
* needs to say what produced it.
|
|
26
|
+
*/
|
|
27
|
+
function prefixed(logger) {
|
|
28
|
+
const write = (level, message) => {
|
|
29
|
+
const line = `eaa-kit: ${message}`;
|
|
30
|
+
if (logger === void 0) process.stderr.write(`${line}\n`);
|
|
31
|
+
else logger[level](line);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
info: (message) => write("info", message),
|
|
35
|
+
warn: (message) => write("warn", message),
|
|
36
|
+
error: (message) => write("error", message)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { BuildAuditError, eaaKit as default };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
|
|
2
|
+
//#region src/webpack/index.d.ts
|
|
3
|
+
/** The part of a webpack compilation this reads. */
|
|
4
|
+
interface CompilationLike {
|
|
5
|
+
/** Non-empty when the build itself failed; there is nothing to audit then. */
|
|
6
|
+
errors: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
/** The part of the webpack compiler this plugin touches. */
|
|
9
|
+
interface CompilerLike {
|
|
10
|
+
options: {
|
|
11
|
+
output?: {
|
|
12
|
+
path?: string;
|
|
13
|
+
};
|
|
14
|
+
mode?: string;
|
|
15
|
+
};
|
|
16
|
+
watchMode?: boolean;
|
|
17
|
+
hooks: {
|
|
18
|
+
afterEmit: {
|
|
19
|
+
tapPromise(name: string, handler: (compilation: CompilationLike) => Promise<void>): void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface EaaKitWebpackOptions extends IntegrationOptions {
|
|
24
|
+
/** Directory to audit. Defaults to webpack's own `output.path`. */
|
|
25
|
+
directory?: string;
|
|
26
|
+
}
|
|
27
|
+
declare class EaaKitWebpackPlugin {
|
|
28
|
+
private readonly options;
|
|
29
|
+
constructor(options?: EaaKitWebpackOptions);
|
|
30
|
+
apply(compiler: CompilerLike): void;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { BuildAuditError, CompilationLike, CompilerLike, EaaKitWebpackOptions, EaaKitWebpackPlugin as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/webpack/index.ts
|
|
4
|
+
var EaaKitWebpackPlugin = class {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
apply(compiler) {
|
|
10
|
+
compiler.hooks.afterEmit.tapPromise("eaa-kit", async (compilation) => {
|
|
11
|
+
if (compiler.watchMode === true) return;
|
|
12
|
+
if (compilation.errors.length > 0) return;
|
|
13
|
+
const output = this.options.directory ?? compiler.options.output?.path;
|
|
14
|
+
if (output === void 0) throw new BuildAuditError("eaa-kit: webpack has no output.path, so there is no directory to audit. Set one, or pass `directory` to the plugin.");
|
|
15
|
+
await auditBuild(path.resolve(output), this.options, stderrLogger());
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { BuildAuditError, EaaKitWebpackPlugin as default };
|