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,543 @@
|
|
|
1
|
+
import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { i as escapeText, n as count, o as standardsReference, r as escapeAttribute, t as collapse } from "./text-CKKpzkYM.js";
|
|
3
|
+
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
+
import { a as isShared, i as groupIssues, n as blindRules, o as discoveryLabel, r as coverageParts, s as missedParts, t as remediationFor } from "./remediation-Dtowi2EC.js";
|
|
5
|
+
import { r as componentPath } from "./component-C3GL1Mnu.js";
|
|
6
|
+
import { i as understandingUrl, r as manualCheckFor, t as buildCoverage } from "./coverage-B4IBKMO8.js";
|
|
7
|
+
import { n as buildSummary } from "./json-DjEvy1nX.js";
|
|
8
|
+
import axe from "axe-core";
|
|
9
|
+
//#region src/audit/report/html.ts
|
|
10
|
+
/**
|
|
11
|
+
* A standalone HTML audit report.
|
|
12
|
+
*
|
|
13
|
+
* The console report is for the person who ran the command; JSON and SARIF are
|
|
14
|
+
* for other programs. This one is for somebody who was not at the terminal —
|
|
15
|
+
* the client whose site it is, or the colleague who has to fix it — so it is a
|
|
16
|
+
* single file that can be attached to an email and opened, with no server, no
|
|
17
|
+
* assets and no scripts.
|
|
18
|
+
*
|
|
19
|
+
* It says exactly what the console report says, in the same order and with the
|
|
20
|
+
* same refusals: the four result categories stay apart, rules this engine could
|
|
21
|
+
* not evaluate are named rather than quietly dropped, and nothing here adds up
|
|
22
|
+
* to a compliance claim. A report with no findings means no findings were
|
|
23
|
+
* found, which is not the same as a site being accessible, and the document
|
|
24
|
+
* says so in its own footer rather than leaving the reader to infer it.
|
|
25
|
+
*/
|
|
26
|
+
/** Severity levels, worst first, as both the scoreboard and the summary list them. */
|
|
27
|
+
const IMPACT_ORDER = [
|
|
28
|
+
"critical",
|
|
29
|
+
"serious",
|
|
30
|
+
"moderate",
|
|
31
|
+
"minor",
|
|
32
|
+
"unclassified"
|
|
33
|
+
];
|
|
34
|
+
/** Elements listed per rule before the rest are summarised. */
|
|
35
|
+
const MAX_NODES = 5;
|
|
36
|
+
/** Longest element markup shown before it is truncated. */
|
|
37
|
+
const MAX_SNIPPET = 200;
|
|
38
|
+
function buildHtmlReport(audits, options) {
|
|
39
|
+
const engine = audits[0]?.engine ?? "jsdom";
|
|
40
|
+
const failing = countAtOrAbove(audits, options.failOn);
|
|
41
|
+
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
42
|
+
const title = `Accessibility audit · ${options.directory}`;
|
|
43
|
+
return `<!doctype html>
|
|
44
|
+
<html lang="en">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="utf-8">
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
48
|
+
<meta name="generator" content="eaa-kit ${escapeAttribute(TOOL_VERSION)}">
|
|
49
|
+
<title>${escapeText(title)}</title>
|
|
50
|
+
<style>
|
|
51
|
+
${STYLES}
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<main>
|
|
56
|
+
<h1>Accessibility audit</h1>
|
|
57
|
+
${verdict(audits, failing, options)}
|
|
58
|
+
${scoreboard(audits)}
|
|
59
|
+
${issues(audits, options)}
|
|
60
|
+
${runDetails(audits, engine, generatedAt, options)}
|
|
61
|
+
${notMeasured(options)}
|
|
62
|
+
${summary(audits, failing, options)}
|
|
63
|
+
${pages(audits)}
|
|
64
|
+
${notEvaluated(audits)}
|
|
65
|
+
${coverageSection(audits)}
|
|
66
|
+
${footer()}
|
|
67
|
+
</main>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The result, in words, before any of the detail.
|
|
74
|
+
*
|
|
75
|
+
* Not colour alone: the badge carries the word as well, because a reader who
|
|
76
|
+
* cannot distinguish the two shades still has to be able to read the outcome —
|
|
77
|
+
* which would be an embarrassing thing for this document in particular to get
|
|
78
|
+
* wrong.
|
|
79
|
+
*/
|
|
80
|
+
function verdict(audits, failing, options) {
|
|
81
|
+
const unaudited = audits.filter((audit) => audit.error).length;
|
|
82
|
+
if (unaudited > 0) return banner("broken", "Could not finish", `${count(unaudited, "page")} could not be audited, so this run reached no verdict.`);
|
|
83
|
+
if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
|
|
84
|
+
const below = totalViolations(audits) - failing;
|
|
85
|
+
if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
|
|
86
|
+
const completeness = options.completeness;
|
|
87
|
+
if (completeness && !completeness.complete) return banner("partial", "No violations found, but the whole site was not measured", `${escapeText(missedParts(completeness).join(", "))}. This report describes ${count(completeness.audited, "page")}, not the site.`);
|
|
88
|
+
return banner("pass", "No violations found", "Automated testing found nothing to report.");
|
|
89
|
+
}
|
|
90
|
+
function banner(kind, heading, detail) {
|
|
91
|
+
return `<p class="verdict ${escapeAttribute(kind)}"><strong>${escapeText(heading)}</strong> ${detail}</p>`;
|
|
92
|
+
}
|
|
93
|
+
function runDetails(audits, engine, generatedAt, options) {
|
|
94
|
+
const rows = [
|
|
95
|
+
["Directory", options.directory],
|
|
96
|
+
["Pages", String(audits.length)],
|
|
97
|
+
["Engine", engine === "browser" ? "Chromium" : "jsdom (browserless)"],
|
|
98
|
+
["Threshold", `${options.failOn} and above fails the run`],
|
|
99
|
+
["Generated", generatedAt],
|
|
100
|
+
["eaa-kit", `${TOOL_VERSION} · axe-core ${axe.version}`]
|
|
101
|
+
];
|
|
102
|
+
if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
|
|
103
|
+
if (options.completeness) rows.splice(2, 0, ["Pages found via", discoveryLabel(options.completeness.discovery)]);
|
|
104
|
+
return `<h2>The run</h2>\n<dl class="run">\n${rows.map(([term, value]) => ` <div><dt>${escapeText(term)}</dt><dd>${escapeText(value)}</dd></div>`).join("\n")}\n</dl>`;
|
|
105
|
+
}
|
|
106
|
+
/** Pages named individually before the rest are counted. */
|
|
107
|
+
const MAX_UNREACHABLE = 20;
|
|
108
|
+
/**
|
|
109
|
+
* The pages this run never reached, named rather than counted away.
|
|
110
|
+
*
|
|
111
|
+
* Named, because a count is not actionable: forty URLs that failed are a broken
|
|
112
|
+
* link check, a server that fell over halfway, or a section behind a login, and
|
|
113
|
+
* which of the three it was is only visible from the addresses. The reasons come
|
|
114
|
+
* straight from the crawler, so a reader can tell a 404 from a timeout.
|
|
115
|
+
*/
|
|
116
|
+
function notMeasured(options) {
|
|
117
|
+
const completeness = options.completeness;
|
|
118
|
+
if (completeness === void 0 || completeness.complete) return "";
|
|
119
|
+
const parts = ["<h2>What this run did not measure</h2>", `<p>This report covers ${count(completeness.audited, "page")}. It is not a description of
|
|
120
|
+
the whole site, and a clean result here does not extend to anything listed below.</p>`];
|
|
121
|
+
if (completeness.truncated) parts.push(`<p class="note">The run stopped at its page limit, so pages beyond ${count(completeness.collected, "page")}
|
|
122
|
+
were never fetched. Raise <code>--max-pages</code> to go further.</p>`);
|
|
123
|
+
if (completeness.errored > 0) parts.push(`<p class="note">${count(completeness.errored, "page")} could not be audited after being
|
|
124
|
+
fetched, and ${completeness.errored === 1 ? "is" : "are"} not counted anywhere in this report.</p>`);
|
|
125
|
+
if (completeness.unreachable.length > 0) {
|
|
126
|
+
const shown = completeness.unreachable.slice(0, MAX_UNREACHABLE).map((item) => ` <li><code>${escapeText(item.location)}</code> — ${escapeText(item.reason)}</li>`).join("\n");
|
|
127
|
+
const rest = completeness.unreachable.length - MAX_UNREACHABLE;
|
|
128
|
+
parts.push(`<p>${count(completeness.unreachable.length, "page")} could not be reached:</p>`, `<ul class="unreachable">\n${shown}\n</ul>`);
|
|
129
|
+
if (rest > 0) parts.push(`<p class="note">…and ${count(rest, "more page")}.</p>`);
|
|
130
|
+
}
|
|
131
|
+
return parts.join("\n");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The counts, taken from the JSON report's own tally rather than recomputed.
|
|
135
|
+
*
|
|
136
|
+
* Two reports of one run disagreeing about how many violations there were is
|
|
137
|
+
* the kind of bug nobody notices until a client does, so there is one place
|
|
138
|
+
* that counts.
|
|
139
|
+
*/
|
|
140
|
+
function summary(audits, failing, options) {
|
|
141
|
+
const totals = buildSummary(audits, options.failOn);
|
|
142
|
+
const impacts = IMPACT_ORDER.filter((impact) => totals.byImpact[impact] > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${totals.byImpact[impact]}</li>`).join("\n");
|
|
143
|
+
return `<h2>Summary</h2>
|
|
144
|
+
<ul class="counts">
|
|
145
|
+
<li><strong>${count(totals.violations, "violation")}</strong> on ${totals.pagesWithViolations} of ${count(totals.pages, "page")}, across ${count(totals.violatingElements, "element")}</li>
|
|
146
|
+
<li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
147
|
+
<li><strong>${totals.needsReview}</strong> ${totals.needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
148
|
+
<li><strong>${totals.notEvaluated}</strong> ${totals.notEvaluated === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
|
|
149
|
+
${totals.accepted > 0 ? ` <li><strong>${totals.accepted}</strong> ${totals.accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
150
|
+
</ul>
|
|
151
|
+
${impacts ? `<ul class="impacts">\n${impacts}\n</ul>` : ""}
|
|
152
|
+
<p class="note">
|
|
153
|
+
<strong>${totals.passes}</strong> rule results were checked and met, and <strong>${totals.inapplicable}</strong>
|
|
154
|
+
found nothing on the page to check. Those two are counted separately and never added
|
|
155
|
+
together: a rule with nothing to check is not a rule that passed, and a page with no
|
|
156
|
+
images proves nothing about image alternatives.
|
|
157
|
+
</p>`;
|
|
158
|
+
}
|
|
159
|
+
function pages(audits) {
|
|
160
|
+
return `<h2>Pages</h2>\n${audits.map(pageSection).join("\n")}`;
|
|
161
|
+
}
|
|
162
|
+
function pageSection(audit) {
|
|
163
|
+
const heading = `<h3 class="page">${escapeText(audit.relativePath)}</h3>`;
|
|
164
|
+
if (audit.error) return `${heading}
|
|
165
|
+
<p class="verdict broken"><strong>Not audited</strong> ${escapeText(audit.error)}</p>`;
|
|
166
|
+
const parts = [heading];
|
|
167
|
+
if (audit.violations.length === 0) parts.push((audit.accepted ?? []).length === 0 ? "<p class=\"clean\">No violations.</p>" : "<p class=\"coverage\">No new violations.</p>");
|
|
168
|
+
else {
|
|
169
|
+
const findings = [...audit.violations].sort(byImpactThenRule).map(violation).join("\n");
|
|
170
|
+
parts.push(`<ol class="findings">\n${findings}\n</ol>`);
|
|
171
|
+
}
|
|
172
|
+
const review = audit.incomplete.filter((finding) => finding.reason === "needs-review");
|
|
173
|
+
if (review.length > 0) {
|
|
174
|
+
const items = review.map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> needs manual review${standards(finding)}</li>`).join("\n");
|
|
175
|
+
parts.push(`<p class="review-heading">A human has to decide these:</p>\n<ul>\n${items}\n</ul>`);
|
|
176
|
+
}
|
|
177
|
+
const accepted = audit.accepted ?? [];
|
|
178
|
+
if (accepted.length > 0) {
|
|
179
|
+
const items = [...accepted].sort(byImpactThenRule).map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> — ${escapeText(finding.help)} (${count(finding.nodes.length, "element")})</li>`).join("\n");
|
|
180
|
+
parts.push(`<p class="accepted-heading">Accepted by the baseline. Still violations, and not counted above:</p>\n<ul class="accepted">\n${items}\n</ul>`);
|
|
181
|
+
}
|
|
182
|
+
parts.push(coverage(audit));
|
|
183
|
+
return parts.join("\n");
|
|
184
|
+
}
|
|
185
|
+
function violation(finding) {
|
|
186
|
+
const impact = impactLabel(finding.impact);
|
|
187
|
+
const shown = finding.nodes.slice(0, MAX_NODES);
|
|
188
|
+
const remaining = finding.nodes.length - shown.length;
|
|
189
|
+
const nodes = shown.map((node) => ` <li>
|
|
190
|
+
<code class="selector">${escapeText(node.target.join(" "))}</code>
|
|
191
|
+
<pre><code>${escapeText(collapse(node.html, MAX_SNIPPET))}</code></pre>
|
|
192
|
+
</li>`).join("\n");
|
|
193
|
+
const more = remaining > 0 ? `\n <li class="more">and ${count(remaining, "more element")}</li>` : "";
|
|
194
|
+
return ` <li class="finding">
|
|
195
|
+
<p class="rule">
|
|
196
|
+
<span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span>
|
|
197
|
+
<code>${escapeText(finding.ruleId)}</code>
|
|
198
|
+
<a href="${escapeAttribute(finding.helpUrl)}">${escapeText(finding.help)}</a>
|
|
199
|
+
</p>
|
|
200
|
+
<p class="standards">${standardsText(finding) || "No mapped success criterion"}</p>
|
|
201
|
+
<ul class="nodes">
|
|
202
|
+
${nodes}${more}
|
|
203
|
+
</ul>
|
|
204
|
+
</li>`;
|
|
205
|
+
}
|
|
206
|
+
function coverage(audit) {
|
|
207
|
+
return `<p class="coverage">${escapeText(coverageParts(audit).join(" · "))}</p>`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Rules this engine reached no verdict on, listed once at the end.
|
|
211
|
+
*
|
|
212
|
+
* They are never reported as passing, and leaving them out entirely would let
|
|
213
|
+
* the reader take the rest of the document for full coverage.
|
|
214
|
+
*/
|
|
215
|
+
function notEvaluated(audits) {
|
|
216
|
+
const blind = blindRules(audits);
|
|
217
|
+
if (blind.length === 0) return "";
|
|
218
|
+
return `<h2>Not evaluated</h2>
|
|
219
|
+
<p>This engine reached no verdict on these rules. They are never reported as passing.
|
|
220
|
+
Each one below says what to check by hand, because automated testing finds a minority of
|
|
221
|
+
accessibility barriers and this is where the rest of them are.</p>
|
|
222
|
+
<ul class="not-evaluated">
|
|
223
|
+
${blind.map(({ ruleId, pages, finding }) => ` <li>
|
|
224
|
+
<code>${escapeText(ruleId)}</code> on ${count(pages, "page")}${standards(finding)}
|
|
225
|
+
<span class="reason">${escapeText(finding.reasonDetail)}</span>
|
|
226
|
+
${manualBlock(ruleId, finding)}
|
|
227
|
+
</li>`).join("\n")}
|
|
228
|
+
</ul>`;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* How much of the standard this run could reach.
|
|
232
|
+
*
|
|
233
|
+
* Always included, and placed last, where a reader who has been through the
|
|
234
|
+
* findings arrives at the limits of what produced them. The counts are shown as
|
|
235
|
+
* four separate figures and never as a ratio: `no automated rule` is the
|
|
236
|
+
* majority of WCAG, and a percentage would present a limit of automated testing
|
|
237
|
+
* as a property of this site.
|
|
238
|
+
*/
|
|
239
|
+
function coverageSection(audits) {
|
|
240
|
+
const coverage = buildCoverage(audits);
|
|
241
|
+
const rows = coverage.criteria.map((criterion) => {
|
|
242
|
+
const url = understandingUrl(criterion.number);
|
|
243
|
+
const name = escapeText(`${criterion.number} ${criterion.title}`);
|
|
244
|
+
const linked = url === void 0 ? name : `<a href="${escapeAttribute(url)}">${name}</a>`;
|
|
245
|
+
return ` <tr class="${escapeAttribute(criterion.status)}">
|
|
246
|
+
<td>${linked}</td>
|
|
247
|
+
<td>${escapeText(criterion.level)}</td>
|
|
248
|
+
<td>${statusText(criterion)}</td>
|
|
249
|
+
</tr>`;
|
|
250
|
+
}).join("\n");
|
|
251
|
+
return `<h2>Coverage of WCAG 2.2 AA</h2>
|
|
252
|
+
<p>Of the ${coverage.total} success criteria at Levels A and AA,
|
|
253
|
+
<strong>${coverage.noAutomatedRule}</strong> cannot be checked by any automated engine and
|
|
254
|
+
need a person. This run reached a verdict on <strong>${coverage.evaluated}</strong>.</p>
|
|
255
|
+
<ul class="counts">
|
|
256
|
+
<li><strong>${coverage.evaluated}</strong> evaluated here</li>
|
|
257
|
+
<li><strong>${coverage.notEvaluated}</strong> not evaluated by this engine${coverage.browserWouldAnswer > 0 ? ` — a run with a real browser would answer ${coverage.browserWouldAnswer} of them` : ""}</li>
|
|
258
|
+
<li><strong>${coverage.nothingToCheck}</strong> had rules that ran and found nothing on this site to check</li>
|
|
259
|
+
<li><strong>${coverage.noAutomatedRule}</strong> have no automated rule at all</li>
|
|
260
|
+
</ul>
|
|
261
|
+
<p class="note">
|
|
262
|
+
These four are counted separately and never added together or divided into a score. Most
|
|
263
|
+
of WCAG cannot be automated, and a percentage here would present that limit of automated
|
|
264
|
+
testing as though it were a measurement of this site.
|
|
265
|
+
</p>
|
|
266
|
+
<div class="scroll">
|
|
267
|
+
<table class="coverage">
|
|
268
|
+
<thead><tr><th>Success criterion</th><th>Level</th><th>This run</th></tr></thead>
|
|
269
|
+
<tbody>
|
|
270
|
+
${rows}
|
|
271
|
+
</tbody>
|
|
272
|
+
</table>
|
|
273
|
+
</div>`;
|
|
274
|
+
}
|
|
275
|
+
function statusText(criterion) {
|
|
276
|
+
const browser = criterion.browserWouldAnswer ? " <em>(a real browser would answer this)</em>" : "";
|
|
277
|
+
switch (criterion.status) {
|
|
278
|
+
case "evaluated": return `Evaluated${browser}`;
|
|
279
|
+
case "not-evaluated": return `This engine could not evaluate it${browser}`;
|
|
280
|
+
case "nothing-to-check": return "Rules ran and found nothing on this site to check";
|
|
281
|
+
case "no-automated-rule": return "No automated rule exists; a person must check it";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function footer() {
|
|
285
|
+
return `<hr>
|
|
286
|
+
<footer>
|
|
287
|
+
<p>
|
|
288
|
+
Generated with <a href="https://github.com/likeBloodMoon/eaa-kit">eaa-kit</a>. Automated
|
|
289
|
+
testing finds a minority of accessibility barriers: it cannot judge whether alternative
|
|
290
|
+
text is accurate, whether a page makes sense in reading order, or whether a form can
|
|
291
|
+
actually be completed with a screen reader.
|
|
292
|
+
</p>
|
|
293
|
+
<p>
|
|
294
|
+
<strong>A report with no findings is not a compliance statement.</strong> It means
|
|
295
|
+
nothing was found by this engine, which is not the same as a site being accessible.
|
|
296
|
+
</p>
|
|
297
|
+
</footer>`;
|
|
298
|
+
}
|
|
299
|
+
function standards(finding) {
|
|
300
|
+
const text = standardsText(finding);
|
|
301
|
+
return text ? ` — ${text}` : "";
|
|
302
|
+
}
|
|
303
|
+
function standardsText(finding) {
|
|
304
|
+
return escapeText(standardsReference(finding.successCriteria, finding.enClauses));
|
|
305
|
+
}
|
|
306
|
+
function totalViolations(audits) {
|
|
307
|
+
return audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
308
|
+
}
|
|
309
|
+
function byImpactThenRule(a, b) {
|
|
310
|
+
return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
|
|
311
|
+
}
|
|
312
|
+
const STYLES = `:root { color-scheme: light dark; }
|
|
313
|
+
body { margin: 0; background: #ffffff; color: #1a1a1a;
|
|
314
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.6; }
|
|
315
|
+
main { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
316
|
+
h1 { font-size: 1.9rem; line-height: 1.25; margin: 0 0 1rem; }
|
|
317
|
+
h2 { font-size: 1.35rem; margin: 2.5rem 0 0.75rem; padding-top: 1.5rem;
|
|
318
|
+
border-top: 1px solid #d4d4d4; }
|
|
319
|
+
h3.page { font-size: 1.05rem; margin: 2rem 0 0.5rem; font-family: ui-monospace, monospace; }
|
|
320
|
+
p { margin: 0 0 1rem; }
|
|
321
|
+
a { color: #0b4fa8; }
|
|
322
|
+
a:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }
|
|
323
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
324
|
+
pre { margin: 0.4rem 0 0; padding: 0.6rem 0.75rem; overflow-x: auto;
|
|
325
|
+
background: #f4f4f5; border-radius: 4px; }
|
|
326
|
+
pre code { font-size: 0.85em; }
|
|
327
|
+
ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }
|
|
328
|
+
li + li { margin-top: 0.5rem; }
|
|
329
|
+
.verdict { padding: 0.75rem 1rem; border-radius: 4px; border-left: 4px solid; }
|
|
330
|
+
.verdict.pass { background: #eef7ee; border-color: #216e39; }
|
|
331
|
+
.verdict.fail { background: #fdeeee; border-color: #a01b1b; }
|
|
332
|
+
.verdict.broken { background: #fdf4e3; border-color: #8a5a00; }
|
|
333
|
+
.verdict.partial { background: #fdf4e3; border-color: #8a5a00; }
|
|
334
|
+
.badge { display: inline-block; padding: 0 0.45rem; border-radius: 3px; font-size: 0.8rem;
|
|
335
|
+
font-weight: 600; border: 1px solid; }
|
|
336
|
+
.badge.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
|
|
337
|
+
.badge.serious { background: #fdf1e8; border-color: #a4531b; color: #7d3f14; }
|
|
338
|
+
.badge.moderate { background: #fdf9e3; border-color: #7a6100; color: #5c4900; }
|
|
339
|
+
.badge.minor { background: #eef2f8; border-color: #40556f; color: #33455a; }
|
|
340
|
+
.badge.unclassified { background: #f1f1f1; border-color: #565656; color: #444444; }
|
|
341
|
+
dl.run { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1.5rem; margin: 0; }
|
|
342
|
+
dl.run > div { display: contents; }
|
|
343
|
+
dl.run dt { font-weight: 600; }
|
|
344
|
+
dl.run dd { margin: 0; }
|
|
345
|
+
ul.counts, ul.impacts { list-style: none; padding-left: 0; }
|
|
346
|
+
ul.impacts li { display: inline-block; margin-right: 1rem; }
|
|
347
|
+
ol.findings { list-style: none; padding-left: 0; }
|
|
348
|
+
li.finding { margin: 0 0 1.5rem; padding-left: 0.9rem; border-left: 3px solid #d4d4d4; }
|
|
349
|
+
p.rule { margin-bottom: 0.25rem; }
|
|
350
|
+
p.standards, p.coverage, .reason, li.more, p.accepted-heading, ul.accepted { color: #4a4a4a; font-size: 0.9rem; }
|
|
351
|
+
p.coverage { margin-top: 0.5rem; }
|
|
352
|
+
ul.nodes { list-style: none; padding-left: 0; }
|
|
353
|
+
ul.unreachable { padding-left: 1.25rem; }
|
|
354
|
+
.remediation { border-left: 3px solid #c9c9c9; padding: 0.1rem 0 0.1rem 0.75rem; margin: 0.5rem 0; }
|
|
355
|
+
.remediation .why { margin: 0.3rem 0; opacity: 0.85; }
|
|
356
|
+
.remediation .fix { margin: 0.3rem 0; }
|
|
357
|
+
pre.suggested { background: #eef7ee; border-left: 3px solid #216e39; padding: 0.5rem; overflow-x: auto; }
|
|
358
|
+
.scroll { overflow-x: auto; }
|
|
359
|
+
table.coverage { border-collapse: collapse; width: 100%; font-size: 0.95em; }
|
|
360
|
+
table.coverage th, table.coverage td { text-align: left; padding: 0.3rem 0.6rem; border-bottom: 1px solid #e3e3e3; vertical-align: top; }
|
|
361
|
+
table.coverage tr.evaluated td:last-child { color: #216e39; }
|
|
362
|
+
table.coverage em { font-style: normal; opacity: 0.75; }
|
|
363
|
+
ul.unreachable li { margin-bottom: 0.2rem; overflow-wrap: anywhere; }
|
|
364
|
+
code.selector { color: #4a4a4a; }
|
|
365
|
+
p.clean { color: #216e39; }
|
|
366
|
+
p.note { font-size: 0.95rem; }
|
|
367
|
+
hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
|
|
368
|
+
footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
369
|
+
.scoreboard { list-style: none; display: flex; flex-wrap: wrap; gap: 0.75rem;
|
|
370
|
+
padding: 0; margin: 1.25rem 0 2rem; }
|
|
371
|
+
.tile { flex: 1 1 8rem; padding: 0.75rem 1rem; border: 1px solid; border-radius: 6px; }
|
|
372
|
+
.tile .figure { display: block; font-size: 1.75rem; font-weight: 700; line-height: 1.1; }
|
|
373
|
+
.tile .label { font-size: 0.85rem; }
|
|
374
|
+
.tile.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
|
|
375
|
+
.tile.serious { background: #fdf1e8; border-color: #a4531b; color: #7d3f14; }
|
|
376
|
+
.tile.moderate { background: #fdf9e3; border-color: #7a6100; color: #5c4900; }
|
|
377
|
+
.tile.minor { background: #eef2f8; border-color: #40556f; color: #33455a; }
|
|
378
|
+
.tile.unclassified { background: #f1f1f1; border-color: #565656; color: #444444; }
|
|
379
|
+
.tile.none { background: #eef7ee; border-color: #216e39; color: #17512a; }
|
|
380
|
+
.tile.unchecked { background: #f4f4f5; border-color: #565656; color: #444444; }
|
|
381
|
+
p.intro { color: #4a4a4a; }
|
|
382
|
+
p.manual { margin: 0.35rem 0; }
|
|
383
|
+
p.manual.browser, p.criteria { margin: 0.35rem 0; font-size: 0.9rem; color: #4a4a4a; }
|
|
384
|
+
ol.issues { list-style: none; padding: 0; }
|
|
385
|
+
li.issue { border: 1px solid #d7d7d7; border-radius: 6px; padding: 0.75rem 1rem;
|
|
386
|
+
margin: 0 0 1rem; }
|
|
387
|
+
li.issue h3 { margin: 0 0 0.25rem; font-size: 1rem; display: flex; flex-wrap: wrap;
|
|
388
|
+
gap: 0.5rem; align-items: baseline; }
|
|
389
|
+
li.issue .wcag { color: #4a4a4a; font-size: 0.85rem; font-weight: 400; }
|
|
390
|
+
p.help { margin: 0 0 0.75rem; }
|
|
391
|
+
.element { border-top: 1px solid #e6e6e6; padding-top: 0.75rem; margin-top: 0.75rem; }
|
|
392
|
+
.element:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
|
|
393
|
+
p.reach, p.sources, p.selector, p.more, p.written-in { margin: 0.35rem 0; font-size: 0.9rem; color: #4a4a4a; }
|
|
394
|
+
.element details { font-size: 0.9rem; color: #4a4a4a; }
|
|
395
|
+
.element summary { cursor: pointer; }
|
|
396
|
+
ul.page-list { margin: 0.35rem 0 0; padding-left: 1.25rem; }
|
|
397
|
+
@media (prefers-color-scheme: dark) {
|
|
398
|
+
body { background: #121212; color: #ededed; }
|
|
399
|
+
a { color: #9ec1ff; }
|
|
400
|
+
h2, hr { border-color: #3a3a3a; }
|
|
401
|
+
pre { background: #1e1e1e; }
|
|
402
|
+
li.finding { border-color: #3a3a3a; }
|
|
403
|
+
p.standards, p.coverage, .reason, li.more, code.selector, footer,
|
|
404
|
+
p.accepted-heading, ul.accepted { color: #b6b6b6; }
|
|
405
|
+
p.clean { color: #7ee2a8; }
|
|
406
|
+
.verdict.pass { background: #10240f; border-color: #7ee2a8; }
|
|
407
|
+
.verdict.fail { background: #2b1111; border-color: #ff9d9d; }
|
|
408
|
+
.verdict.broken { background: #2b2310; border-color: #ffd28a; }
|
|
409
|
+
.verdict.partial { background: #2b2310; border-color: #ffd28a; }
|
|
410
|
+
table.coverage th, table.coverage td { border-bottom-color: #333; }
|
|
411
|
+
table.coverage tr.evaluated td:last-child { color: #7ee2a8; }
|
|
412
|
+
.remediation { border-left-color: #555; }
|
|
413
|
+
pre.suggested { background: #10240f; border-left-color: #7ee2a8; }
|
|
414
|
+
.tile.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
415
|
+
.tile.serious { background: #2b1d11; border-color: #ffb98a; color: #ffd7bd; }
|
|
416
|
+
.tile.moderate { background: #2b2610; border-color: #f0d264; color: #f5e3a4; }
|
|
417
|
+
.tile.minor { background: #161c25; border-color: #9db6d6; color: #c6d6ea; }
|
|
418
|
+
.tile.unclassified { background: #1f1f1f; border-color: #9a9a9a; color: #d0d0d0; }
|
|
419
|
+
.tile.none { background: #10240f; border-color: #7ee2a8; color: #b6f0cd; }
|
|
420
|
+
.tile.unchecked { background: #1e1e1e; border-color: #9a9a9a; color: #d0d0d0; }
|
|
421
|
+
p.intro, li.issue .wcag, p.reach, p.sources, p.selector, .element details,
|
|
422
|
+
p.manual.browser, p.criteria { color: #b6b6b6; }
|
|
423
|
+
li.issue { border-color: #3a3a3a; }
|
|
424
|
+
.element { border-top-color: #2e2e2e; }
|
|
425
|
+
.badge.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
426
|
+
.badge.serious { background: #2b1c11; border-color: #ffbb8a; color: #ffd9bd; }
|
|
427
|
+
.badge.moderate { background: #262110; border-color: #ffe08a; color: #ffeec2; }
|
|
428
|
+
.badge.minor { background: #16202b; border-color: #a8c6e8; color: #cfdff0; }
|
|
429
|
+
.badge.unclassified { background: #1f1f1f; border-color: #b6b6b6; color: #d8d8d8; }
|
|
430
|
+
}
|
|
431
|
+
@media print {
|
|
432
|
+
body { background: #ffffff; color: #000000; }
|
|
433
|
+
main { max-width: none; }
|
|
434
|
+
a { color: #000000; }
|
|
435
|
+
}`;
|
|
436
|
+
/**
|
|
437
|
+
* The counts, worst first, with what was never checked beside them.
|
|
438
|
+
*
|
|
439
|
+
* A row of severity totals is the first thing anybody reads, and on its own it
|
|
440
|
+
* is the one place this document could mislead: a client seeing "0 critical, 0
|
|
441
|
+
* serious" concludes the site is fine, when the honest reading of that same run
|
|
442
|
+
* may be that six whole rule categories could not be evaluated at all. So the
|
|
443
|
+
* unevaluated count sits in the same row, in the same type size, rather than in
|
|
444
|
+
* a section further down that nobody scrolls to.
|
|
445
|
+
*/
|
|
446
|
+
function scoreboard(audits) {
|
|
447
|
+
const counted = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const issue of groupIssues(audits)) {
|
|
449
|
+
const key = impactLabel(issue.impact);
|
|
450
|
+
counted.set(key, (counted.get(key) ?? 0) + issue.occurrences);
|
|
451
|
+
}
|
|
452
|
+
const blind = blindRules(audits);
|
|
453
|
+
const tiles = IMPACT_ORDER.filter((level) => (counted.get(level) ?? 0) > 0).map((level) => `<li class="tile ${level}"><span class="figure">${counted.get(level)}</span> <span class="label">${level}</span></li>`);
|
|
454
|
+
if (tiles.length === 0) tiles.push("<li class=\"tile none\"><span class=\"figure\">0</span> <span class=\"label\">violations</span></li>");
|
|
455
|
+
if (blind.length > 0) tiles.push(`<li class="tile unchecked"><span class="figure">${blind.length}</span> <span class="label">not evaluated</span></li>`);
|
|
456
|
+
return `<ul class="scoreboard">\n${tiles.join("\n")}\n</ul>`;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* What is broken, once per element rather than once per page.
|
|
460
|
+
*
|
|
461
|
+
* The page-by-page listing further down is the same information keyed the other
|
|
462
|
+
* way round. It is the right shape for working through one page and the wrong
|
|
463
|
+
* shape for deciding what to do: on a site built from components one broken
|
|
464
|
+
* header is one line in one file, and a per-page listing reports it as many
|
|
465
|
+
* findings without ever saying they are the same defect.
|
|
466
|
+
*/
|
|
467
|
+
function issues(audits, options) {
|
|
468
|
+
const found = groupIssues(audits);
|
|
469
|
+
if (found.length === 0) return "";
|
|
470
|
+
const elements = found.reduce((total, issue) => total + issue.elements.length, 0);
|
|
471
|
+
const occurrences = found.reduce((total, issue) => total + issue.occurrences, 0);
|
|
472
|
+
const intro = occurrences === elements ? `${count(elements, "distinct element")} to fix.` : `${count(occurrences, "violation")} across the site, from ${count(elements, "distinct element")}.`;
|
|
473
|
+
return `<h2 id="issues">What to fix</h2>
|
|
474
|
+
<p class="intro">${escapeText(intro)}</p>
|
|
475
|
+
<ol class="issues">
|
|
476
|
+
${found.map((issue) => issueSection(issue, options)).join("\n")}
|
|
477
|
+
</ol>`;
|
|
478
|
+
}
|
|
479
|
+
function issueSection(issue, options) {
|
|
480
|
+
const impact = impactLabel(issue.impact);
|
|
481
|
+
const criteria = issue.successCriteria.length > 0 ? `<span class="wcag">WCAG ${escapeText(issue.successCriteria.join(" "))}</span>` : "";
|
|
482
|
+
return `<li class="issue">
|
|
483
|
+
<h3><span class="badge ${impact}">${impact}</span> <code>${escapeText(issue.ruleId)}</code> ${criteria}</h3>
|
|
484
|
+
<p class="help">${escapeText(issue.help)}</p>
|
|
485
|
+
${remediationBlock(issue.ruleId, issue.elements[0]?.html, options)}
|
|
486
|
+
${issue.elements.slice(0, MAX_NODES).map((element) => issueElement(element, options)).join("\n")}
|
|
487
|
+
${issue.elements.length > MAX_NODES ? `<p class="more">…and ${count(issue.elements.length - MAX_NODES, "more element")}</p>` : ""}
|
|
488
|
+
</li>`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* What to do about the rule, beside the finding rather than behind a link.
|
|
492
|
+
*
|
|
493
|
+
* The corrected line is built from the element that actually failed. A textbook
|
|
494
|
+
* snippet is a second thing to translate before anybody can use it, and this
|
|
495
|
+
* document is read by people who did not run the audit and may not write the
|
|
496
|
+
* code either — the closer it gets to the line as it should have been, the less
|
|
497
|
+
* of that translation is left to them.
|
|
498
|
+
*/
|
|
499
|
+
function remediationBlock(ruleId, html, options) {
|
|
500
|
+
const remediation = remediationFor(ruleId, options.framework);
|
|
501
|
+
if (remediation === void 0) return "";
|
|
502
|
+
const example = html === void 0 ? void 0 : remediation.example?.(html);
|
|
503
|
+
return `<div class="remediation">
|
|
504
|
+
<p class="why">${escapeText(remediation.why)}</p>
|
|
505
|
+
<p class="fix"><strong>Fix.</strong> ${escapeText(remediation.fix)}</p>
|
|
506
|
+
${example === void 0 ? "" : `<pre class="suggested"><code>${escapeText(collapse(example, MAX_SNIPPET))}</code></pre>`}
|
|
507
|
+
</div>`;
|
|
508
|
+
}
|
|
509
|
+
function issueElement(element, options) {
|
|
510
|
+
const sources = [...new Set(element.pages.map((page) => options.sourceFor?.(page)).filter((source) => source !== void 0))];
|
|
511
|
+
const list = element.pages.map((page) => `<li>${escapeText(page)}</li>`).join("");
|
|
512
|
+
const component = options.componentFor?.(element.html);
|
|
513
|
+
return `<div class="element">
|
|
514
|
+
<pre><code>${escapeText(collapse(element.html, MAX_SNIPPET))}</code></pre>
|
|
515
|
+
${component === void 0 ? "" : `<p class="written-in">Written in <code>${escapeText(componentPath(component))}</code></p>`}
|
|
516
|
+
${element.selector === "" ? "" : `<p class="selector"><code>${escapeText(element.selector)}</code></p>`}
|
|
517
|
+
<p class="reach">Found on ${count(element.pages.length, "page")}.${isShared(element) ? " <strong>Identical on each — likely one shared component.</strong>" : ""}</p>
|
|
518
|
+
${sources.length > 0 ? `<p class="sources">Source: ${sources.map((source) => `<code>${escapeText(source)}</code>`).join(", ")}</p>` : ""}
|
|
519
|
+
<details><summary>${count(element.pages.length, "page")}</summary><ul class="page-list">${list}</ul></details>
|
|
520
|
+
</div>`;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The manual check for a rule, and where to read what the criterion requires.
|
|
524
|
+
*
|
|
525
|
+
* Unconditional here, unlike the console report, which hides it behind a flag.
|
|
526
|
+
* This document is read once by somebody deciding what to do, not printed on
|
|
527
|
+
* every run by somebody who has read it before.
|
|
528
|
+
*/
|
|
529
|
+
function manualBlock(ruleId, finding) {
|
|
530
|
+
const manual = manualCheckFor(ruleId);
|
|
531
|
+
const links = finding.successCriteria.map((criterion) => {
|
|
532
|
+
const url = understandingUrl(criterion);
|
|
533
|
+
return url === void 0 ? void 0 : `<a href="${escapeAttribute(url)}">WCAG ${escapeText(criterion)}</a>`;
|
|
534
|
+
}).filter((link) => link !== void 0);
|
|
535
|
+
if (manual === void 0 && links.length === 0) return "";
|
|
536
|
+
return [
|
|
537
|
+
manual === void 0 ? "" : ` <p class="manual">${escapeText(manual.check)}</p>`,
|
|
538
|
+
manual?.browserAnswers === true ? " <p class=\"manual browser\">Or run the audit again with <code>--browser</code>.</p>" : "",
|
|
539
|
+
links.length === 0 ? "" : ` <p class="criteria">${links.join(" · ")}</p>`
|
|
540
|
+
].filter((line) => line !== "").join("\n");
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
export { buildHtmlReport };
|
|
@@ -22,6 +22,20 @@ function meetsThreshold(impact, threshold) {
|
|
|
22
22
|
if (impact === null || !isImpactLevel(impact)) return true;
|
|
23
23
|
return IMPACT_LEVELS.indexOf(impact) >= IMPACT_LEVELS.indexOf(threshold);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Where an impact sorts, most severe first.
|
|
27
|
+
*
|
|
28
|
+
* An unclassified impact ranks with the most severe, on the same reasoning as
|
|
29
|
+
* `--fail-on`: not knowing how bad a barrier is is not evidence that it is mild.
|
|
30
|
+
*/
|
|
31
|
+
function impactRank(impact) {
|
|
32
|
+
if (impact === null || !isImpactLevel(impact)) return 0;
|
|
33
|
+
return IMPACT_LEVELS.length - IMPACT_LEVELS.indexOf(impact);
|
|
34
|
+
}
|
|
35
|
+
/** The impact as a word, naming the gap rather than hiding it. */
|
|
36
|
+
function impactLabel(impact) {
|
|
37
|
+
return impact !== null && isImpactLevel(impact) ? impact : "unclassified";
|
|
38
|
+
}
|
|
25
39
|
/** Violations at or above `threshold`, counted per rule per page. */
|
|
26
40
|
function countAtOrAbove(audits, threshold) {
|
|
27
41
|
let total = 0;
|
|
@@ -29,4 +43,4 @@ function countAtOrAbove(audits, threshold) {
|
|
|
29
43
|
return total;
|
|
30
44
|
}
|
|
31
45
|
//#endregion
|
|
32
|
-
export {
|
|
46
|
+
export { impactRank as a, impactLabel as i, IMPACT_LEVELS as n, isImpactLevel as o, countAtOrAbove as r, meetsThreshold as s, DEFAULT_FAIL_ON as t };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-
|
|
2
|
-
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-
|
|
1
|
+
import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-5wRGLvub.js";
|
|
2
|
+
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DrvXRCEn.js";
|
|
3
3
|
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-
|
|
2
|
-
import {
|
|
1
|
+
import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-5wRGLvub.js";
|
|
2
|
+
import { t as exists } from "./fs-BmPtmFke.js";
|
|
3
|
+
import { i as note, o as warn, r as fail } from "./command-D8l_oYbV.js";
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import pc from "picocolors";
|
|
5
7
|
import { createInterface } from "node:readline/promises";
|
|
@@ -26,10 +28,7 @@ async function detectDefaults(cwd) {
|
|
|
26
28
|
}
|
|
27
29
|
/** Whether a config is already there, so init never overwrites one silently. */
|
|
28
30
|
async function existingConfig(cwd) {
|
|
29
|
-
for (const name of CONFIG_FILENAMES)
|
|
30
|
-
await stat(path.join(cwd, name));
|
|
31
|
-
return name;
|
|
32
|
-
} catch {}
|
|
31
|
+
for (const name of CONFIG_FILENAMES) if (await exists(path.join(cwd, name))) return name;
|
|
33
32
|
}
|
|
34
33
|
/** Reads answers, showing what each will be if the reader just hits enter. */
|
|
35
34
|
function terminalPrompt() {
|
|
@@ -51,7 +50,7 @@ async function runInitCommand(options = {}) {
|
|
|
51
50
|
const target = path.resolve(cwd, options.output ?? "eaa.config.json");
|
|
52
51
|
const already = await existingConfig(cwd);
|
|
53
52
|
if (already !== void 0 && !options.force) {
|
|
54
|
-
|
|
53
|
+
warn(`${already} already exists. Pass --force to overwrite it.`);
|
|
55
54
|
return { exitCode: 1 };
|
|
56
55
|
}
|
|
57
56
|
const detected = await detectDefaults(cwd);
|
|
@@ -89,13 +88,17 @@ async function runInitCommand(options = {}) {
|
|
|
89
88
|
try {
|
|
90
89
|
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
91
90
|
} catch (cause) {
|
|
92
|
-
|
|
91
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
92
|
+
fail(`Could not write ${path.basename(target)}: ${reason}`);
|
|
93
93
|
return { exitCode: 2 };
|
|
94
94
|
}
|
|
95
95
|
process.stderr.write(`Wrote ${path.relative(cwd, target) || path.basename(target)}\n`);
|
|
96
96
|
const missing = [name === "" ? "site.name" : void 0, email === "" ? "provider.email" : void 0].filter((field) => field !== void 0);
|
|
97
|
-
if (missing.length > 0)
|
|
98
|
-
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
const [verb, pronoun] = missing.length === 1 ? ["is", "it"] : ["are", "them"];
|
|
99
|
+
warn(`${missing.join(" and ")} ${verb} empty and required. Fill ${pronoun} in before generating a statement.`);
|
|
100
|
+
}
|
|
101
|
+
note("Read it before publishing anything from it: status is partially-compliant,\nwhich is the honest default before an audit has run.\n\nNext: eaa-kit audit · eaa-kit statement");
|
|
99
102
|
return {
|
|
100
103
|
file: target,
|
|
101
104
|
exitCode: 0
|