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.
Files changed (56) hide show
  1. package/README.md +51 -4
  2. package/dist/astro/index.d.ts +7 -34
  3. package/dist/astro/index.js +6 -26
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-CPoZMXGM.js +779 -0
  6. package/dist/audit-CpXH2Mk8.js +2 -0
  7. package/dist/{baseline-CV_3lbER.js → baseline-22Y1NWxM.js} +1 -1
  8. package/dist/{baseline-CgBmzFTr.js → baseline-DB9CZGnV.js} +17 -27
  9. package/dist/cli/index.js +336 -142
  10. package/dist/collect-CFM8gEVv.js +134 -0
  11. package/dist/command-D8l_oYbV.js +77 -0
  12. package/dist/component-C3GL1Mnu.js +146 -0
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B4IBKMO8.js +521 -0
  15. package/dist/{crawl-CtJbMNNb.js → crawl-BiI1Lau_.js} +13 -23
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/frameworks-B4ClIJgE.js +314 -0
  19. package/dist/frameworks-etFg_O8K.js +2 -0
  20. package/dist/fs-BmPtmFke.js +40 -0
  21. package/dist/html-BGTO3ypW.js +543 -0
  22. package/dist/{impact-DvgBjupx.js → impact-DZt2oBCP.js} +15 -1
  23. package/dist/index.js +2 -2
  24. package/dist/{init-DRKIdpK1.js → init-DIWDE35F.js} +13 -10
  25. package/dist/jsdom-4IMzv0eE.js +3 -0
  26. package/dist/jsdom-DCpGSLfW.js +81 -0
  27. package/dist/{json-C9xS1PNC.js → json-DjEvy1nX.js} +19 -22
  28. package/dist/json-QQuFIw1W.js +2 -0
  29. package/dist/{load-sCkKsvGQ.js → load-5wRGLvub.js} +3 -9
  30. package/dist/nuxt/index.d.ts +35 -0
  31. package/dist/nuxt/index.js +23 -0
  32. package/dist/{playwright-DSRnXmcd.js → playwright-BWniOain.js} +70 -23
  33. package/dist/{pool-DixLeu8L.js → pool-BMevaLWD.js} +4 -12
  34. package/dist/{project-CufCqIE2.js → project-CzOnkLH6.js} +14 -21
  35. package/dist/project-MFrXcw1M.js +2 -0
  36. package/dist/remediation-Dtowi2EC.js +321 -0
  37. package/dist/{render-BO0nVrrZ.js → render-DrvXRCEn.js} +8 -16
  38. package/dist/{result-2aZPfM8w.js → result-DoamKFsp.js} +115 -1
  39. package/dist/routes-CmdRUuOs.js +265 -0
  40. package/dist/run-BMASMmwO.d.ts +45 -0
  41. package/dist/run-DB34BSOZ.js +53 -0
  42. package/dist/{sarif-eSCuI0eX.js → sarif-SR3_lLYd.js} +22 -36
  43. package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
  44. package/dist/text-CKKpzkYM.js +43 -0
  45. package/dist/vite/index.d.ts +39 -0
  46. package/dist/vite/index.js +40 -0
  47. package/dist/webpack/index.d.ts +33 -0
  48. package/dist/webpack/index.js +20 -0
  49. package/package.json +36 -9
  50. package/dist/audit-B2dIKpJ5.js +0 -2
  51. package/dist/audit-DXpKkXsC.js +0 -950
  52. package/dist/escape-Dm1o_RAk.js +0 -21
  53. package/dist/html-BLEuzep6.js +0 -337
  54. package/dist/jsdom-BEu6Ra_2.js +0 -163
  55. package/dist/jsdom-C6dIyaxN.js +0 -3
  56. package/dist/routes-BxbSZKXC.js +0 -123
@@ -0,0 +1,521 @@
1
+ import { n as ENGINE_BLIND_RULES, t as DEFAULT_TAGS, u as successCriteria } from "./result-DoamKFsp.js";
2
+ import axe from "axe-core";
3
+ //#region src/audit/manual.ts
4
+ /**
5
+ * What a person has to check, and where to read about it.
6
+ *
7
+ * Automated testing finds a minority of accessibility barriers. This tool says
8
+ * so everywhere, and saying so is not much use on its own: a reader told that
9
+ * six rules "could not be evaluated" is left knowing there is a gap and not
10
+ * what to do about it.
11
+ *
12
+ * So each rule the browserless engine cannot decide carries the check somebody
13
+ * would do by hand, and each success criterion carries a link to what it
14
+ * actually requires. The point is to turn the honest disclaimer into the part
15
+ * of the report with the most work in it.
16
+ */
17
+ /**
18
+ * WCAG 2.2 Understanding pages, by success criterion.
19
+ *
20
+ * The Understanding document rather than the criterion text: the text says what
21
+ * conformance is, and the Understanding page says what it is for and how to
22
+ * meet it, which is what somebody reading a report needs.
23
+ *
24
+ * Level A and AA only, since that is what this tool audits against.
25
+ */
26
+ const UNDERSTANDING = {
27
+ "1.1.1": "non-text-content",
28
+ "1.2.1": "audio-only-and-video-only-prerecorded",
29
+ "1.2.2": "captions-prerecorded",
30
+ "1.2.3": "audio-description-or-media-alternative-prerecorded",
31
+ "1.2.4": "captions-live",
32
+ "1.2.5": "audio-description-prerecorded",
33
+ "1.3.1": "info-and-relationships",
34
+ "1.3.2": "meaningful-sequence",
35
+ "1.3.3": "sensory-characteristics",
36
+ "1.3.4": "orientation",
37
+ "1.3.5": "identify-input-purpose",
38
+ "1.4.1": "use-of-color",
39
+ "1.4.2": "audio-control",
40
+ "1.4.3": "contrast-minimum",
41
+ "1.4.4": "resize-text",
42
+ "1.4.5": "images-of-text",
43
+ "1.4.10": "reflow",
44
+ "1.4.11": "non-text-contrast",
45
+ "1.4.12": "text-spacing",
46
+ "1.4.13": "content-on-hover-or-focus",
47
+ "2.1.1": "keyboard",
48
+ "2.1.2": "no-keyboard-trap",
49
+ "2.1.4": "character-key-shortcuts",
50
+ "2.2.1": "timing-adjustable",
51
+ "2.2.2": "pause-stop-hide",
52
+ "2.3.1": "three-flashes-or-below-threshold",
53
+ "2.4.1": "bypass-blocks",
54
+ "2.4.2": "page-titled",
55
+ "2.4.3": "focus-order",
56
+ "2.4.4": "link-purpose-in-context",
57
+ "2.4.5": "multiple-ways",
58
+ "2.4.6": "headings-and-labels",
59
+ "2.4.7": "focus-visible",
60
+ "2.4.11": "focus-not-obscured-minimum",
61
+ "2.5.1": "pointer-gestures",
62
+ "2.5.2": "pointer-cancellation",
63
+ "2.5.3": "label-in-name",
64
+ "2.5.4": "motion-actuation",
65
+ "2.5.7": "dragging-movements",
66
+ "2.5.8": "target-size-minimum",
67
+ "3.1.1": "language-of-page",
68
+ "3.1.2": "language-of-parts",
69
+ "3.2.1": "on-focus",
70
+ "3.2.2": "on-input",
71
+ "3.2.3": "consistent-navigation",
72
+ "3.2.4": "consistent-identification",
73
+ "3.2.6": "consistent-help",
74
+ "3.3.1": "error-identification",
75
+ "3.3.2": "labels-or-instructions",
76
+ "3.3.3": "error-suggestion",
77
+ "3.3.4": "error-prevention-legal-financial-data",
78
+ "3.3.7": "redundant-entry",
79
+ "3.3.8": "accessible-authentication-minimum",
80
+ "4.1.2": "name-role-value",
81
+ "4.1.3": "status-messages"
82
+ };
83
+ /** Where to read what a success criterion requires, or undefined if unknown. */
84
+ function understandingUrl(criterion) {
85
+ const slug = UNDERSTANDING[criterion];
86
+ return slug === void 0 ? void 0 : `https://www.w3.org/WAI/WCAG22/Understanding/${slug}.html`;
87
+ }
88
+ /**
89
+ * The check a person does for a rule this engine could not decide.
90
+ *
91
+ * Written as an action rather than a restatement of the criterion. "Ensure
92
+ * sufficient contrast" tells somebody nothing they did not already know; the
93
+ * useful sentence names what to open and what to look at.
94
+ */
95
+ const MANUAL_CHECKS = {
96
+ "color-contrast": {
97
+ 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.",
98
+ browserAnswers: true
99
+ },
100
+ "color-contrast-enhanced": {
101
+ check: "Only needed if you are claiming AAA. Body text needs 7:1 and large text 4.5:1.",
102
+ browserAnswers: true
103
+ },
104
+ "target-size": {
105
+ 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.",
106
+ browserAnswers: true
107
+ },
108
+ "scrollable-region-focusable": {
109
+ 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.",
110
+ browserAnswers: true
111
+ },
112
+ "link-in-text-block": {
113
+ 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.",
114
+ browserAnswers: true
115
+ },
116
+ "no-autoplay-audio": {
117
+ 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.",
118
+ browserAnswers: false
119
+ },
120
+ "avoid-inline-spacing": {
121
+ 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.",
122
+ browserAnswers: true
123
+ },
124
+ "p-as-heading": {
125
+ 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.",
126
+ browserAnswers: false
127
+ },
128
+ "css-orientation-lock": {
129
+ check: "Rotate a phone or tablet. The content should work in both orientations unless one is essential, which is rare outside games and instruments.",
130
+ browserAnswers: false
131
+ }
132
+ };
133
+ /** The check for a rule, if there is one written for it. */
134
+ function manualCheckFor(ruleId) {
135
+ return MANUAL_CHECKS[ruleId];
136
+ }
137
+ //#endregion
138
+ //#region src/audit/coverage.ts
139
+ /**
140
+ * WCAG 2.2 at Levels A and AA, which is what this tool audits against.
141
+ *
142
+ * 4.1.1 Parsing is deliberately absent: it was removed in WCAG 2.2 and counting
143
+ * it would inflate the denominator with a criterion nobody has to meet.
144
+ */
145
+ const WCAG22_AA_CRITERIA = [
146
+ {
147
+ number: "1.1.1",
148
+ title: "Non-text Content",
149
+ level: "A"
150
+ },
151
+ {
152
+ number: "1.2.1",
153
+ title: "Audio-only and Video-only (Prerecorded)",
154
+ level: "A"
155
+ },
156
+ {
157
+ number: "1.2.2",
158
+ title: "Captions (Prerecorded)",
159
+ level: "A"
160
+ },
161
+ {
162
+ number: "1.2.3",
163
+ title: "Audio Description or Media Alternative (Prerecorded)",
164
+ level: "A"
165
+ },
166
+ {
167
+ number: "1.2.4",
168
+ title: "Captions (Live)",
169
+ level: "AA"
170
+ },
171
+ {
172
+ number: "1.2.5",
173
+ title: "Audio Description (Prerecorded)",
174
+ level: "AA"
175
+ },
176
+ {
177
+ number: "1.3.1",
178
+ title: "Info and Relationships",
179
+ level: "A"
180
+ },
181
+ {
182
+ number: "1.3.2",
183
+ title: "Meaningful Sequence",
184
+ level: "A"
185
+ },
186
+ {
187
+ number: "1.3.3",
188
+ title: "Sensory Characteristics",
189
+ level: "A"
190
+ },
191
+ {
192
+ number: "1.3.4",
193
+ title: "Orientation",
194
+ level: "AA"
195
+ },
196
+ {
197
+ number: "1.3.5",
198
+ title: "Identify Input Purpose",
199
+ level: "AA"
200
+ },
201
+ {
202
+ number: "1.4.1",
203
+ title: "Use of Color",
204
+ level: "A"
205
+ },
206
+ {
207
+ number: "1.4.2",
208
+ title: "Audio Control",
209
+ level: "A"
210
+ },
211
+ {
212
+ number: "1.4.3",
213
+ title: "Contrast (Minimum)",
214
+ level: "AA"
215
+ },
216
+ {
217
+ number: "1.4.4",
218
+ title: "Resize Text",
219
+ level: "AA"
220
+ },
221
+ {
222
+ number: "1.4.5",
223
+ title: "Images of Text",
224
+ level: "AA"
225
+ },
226
+ {
227
+ number: "1.4.10",
228
+ title: "Reflow",
229
+ level: "AA"
230
+ },
231
+ {
232
+ number: "1.4.11",
233
+ title: "Non-text Contrast",
234
+ level: "AA"
235
+ },
236
+ {
237
+ number: "1.4.12",
238
+ title: "Text Spacing",
239
+ level: "AA"
240
+ },
241
+ {
242
+ number: "1.4.13",
243
+ title: "Content on Hover or Focus",
244
+ level: "AA"
245
+ },
246
+ {
247
+ number: "2.1.1",
248
+ title: "Keyboard",
249
+ level: "A"
250
+ },
251
+ {
252
+ number: "2.1.2",
253
+ title: "No Keyboard Trap",
254
+ level: "A"
255
+ },
256
+ {
257
+ number: "2.1.4",
258
+ title: "Character Key Shortcuts",
259
+ level: "A"
260
+ },
261
+ {
262
+ number: "2.2.1",
263
+ title: "Timing Adjustable",
264
+ level: "A"
265
+ },
266
+ {
267
+ number: "2.2.2",
268
+ title: "Pause, Stop, Hide",
269
+ level: "A"
270
+ },
271
+ {
272
+ number: "2.3.1",
273
+ title: "Three Flashes or Below Threshold",
274
+ level: "A"
275
+ },
276
+ {
277
+ number: "2.4.1",
278
+ title: "Bypass Blocks",
279
+ level: "A"
280
+ },
281
+ {
282
+ number: "2.4.2",
283
+ title: "Page Titled",
284
+ level: "A"
285
+ },
286
+ {
287
+ number: "2.4.3",
288
+ title: "Focus Order",
289
+ level: "A"
290
+ },
291
+ {
292
+ number: "2.4.4",
293
+ title: "Link Purpose (In Context)",
294
+ level: "A"
295
+ },
296
+ {
297
+ number: "2.4.5",
298
+ title: "Multiple Ways",
299
+ level: "AA"
300
+ },
301
+ {
302
+ number: "2.4.6",
303
+ title: "Headings and Labels",
304
+ level: "AA"
305
+ },
306
+ {
307
+ number: "2.4.7",
308
+ title: "Focus Visible",
309
+ level: "AA"
310
+ },
311
+ {
312
+ number: "2.4.11",
313
+ title: "Focus Not Obscured (Minimum)",
314
+ level: "AA"
315
+ },
316
+ {
317
+ number: "2.5.1",
318
+ title: "Pointer Gestures",
319
+ level: "A"
320
+ },
321
+ {
322
+ number: "2.5.2",
323
+ title: "Pointer Cancellation",
324
+ level: "A"
325
+ },
326
+ {
327
+ number: "2.5.3",
328
+ title: "Label in Name",
329
+ level: "A"
330
+ },
331
+ {
332
+ number: "2.5.4",
333
+ title: "Motion Actuation",
334
+ level: "A"
335
+ },
336
+ {
337
+ number: "2.5.7",
338
+ title: "Dragging Movements",
339
+ level: "AA"
340
+ },
341
+ {
342
+ number: "2.5.8",
343
+ title: "Target Size (Minimum)",
344
+ level: "AA"
345
+ },
346
+ {
347
+ number: "3.1.1",
348
+ title: "Language of Page",
349
+ level: "A"
350
+ },
351
+ {
352
+ number: "3.1.2",
353
+ title: "Language of Parts",
354
+ level: "AA"
355
+ },
356
+ {
357
+ number: "3.2.1",
358
+ title: "On Focus",
359
+ level: "A"
360
+ },
361
+ {
362
+ number: "3.2.2",
363
+ title: "On Input",
364
+ level: "A"
365
+ },
366
+ {
367
+ number: "3.2.3",
368
+ title: "Consistent Navigation",
369
+ level: "AA"
370
+ },
371
+ {
372
+ number: "3.2.4",
373
+ title: "Consistent Identification",
374
+ level: "AA"
375
+ },
376
+ {
377
+ number: "3.2.6",
378
+ title: "Consistent Help",
379
+ level: "A"
380
+ },
381
+ {
382
+ number: "3.3.1",
383
+ title: "Error Identification",
384
+ level: "A"
385
+ },
386
+ {
387
+ number: "3.3.2",
388
+ title: "Labels or Instructions",
389
+ level: "A"
390
+ },
391
+ {
392
+ number: "3.3.3",
393
+ title: "Error Suggestion",
394
+ level: "AA"
395
+ },
396
+ {
397
+ number: "3.3.4",
398
+ title: "Error Prevention (Legal, Financial, Data)",
399
+ level: "AA"
400
+ },
401
+ {
402
+ number: "3.3.7",
403
+ title: "Redundant Entry",
404
+ level: "A"
405
+ },
406
+ {
407
+ number: "3.3.8",
408
+ title: "Accessible Authentication (Minimum)",
409
+ level: "AA"
410
+ },
411
+ {
412
+ number: "4.1.2",
413
+ title: "Name, Role, Value",
414
+ level: "A"
415
+ },
416
+ {
417
+ number: "4.1.3",
418
+ title: "Status Messages",
419
+ level: "AA"
420
+ }
421
+ ];
422
+ /**
423
+ * Rules axe-core would run under these tags, mapped to the criteria they cover.
424
+ *
425
+ * Experimental rules are excluded, matching `blindRulesInScope`: axe-core leaves
426
+ * them off by default, so counting them as coverage would promise checks that
427
+ * never run.
428
+ */
429
+ function rulesByCriterion(tags) {
430
+ const byCriterion = /* @__PURE__ */ new Map();
431
+ for (const rule of axe.getRules([...tags])) {
432
+ if (rule.tags.includes("experimental")) continue;
433
+ for (const criterion of successCriteria(rule.tags)) {
434
+ const rules = byCriterion.get(criterion);
435
+ if (rules) rules.push(rule.ruleId);
436
+ else byCriterion.set(criterion, [rule.ruleId]);
437
+ }
438
+ }
439
+ for (const rules of byCriterion.values()) rules.sort();
440
+ return byCriterion;
441
+ }
442
+ /** Rule ids this run reached a pass or a violation on, across every page. */
443
+ function decidedRules(audits) {
444
+ const decided = /* @__PURE__ */ new Set();
445
+ for (const audit of audits) {
446
+ for (const finding of audit.violations) decided.add(finding.ruleId);
447
+ for (const finding of audit.accepted ?? []) decided.add(finding.ruleId);
448
+ for (const outcome of audit.passes) decided.add(outcome.ruleId);
449
+ }
450
+ return decided;
451
+ }
452
+ /** Rule ids this run reached no verdict on because the engine cannot see them. */
453
+ function blindedRules(audits) {
454
+ const blind = /* @__PURE__ */ new Set();
455
+ for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind.add(finding.ruleId);
456
+ return blind;
457
+ }
458
+ /**
459
+ * What this run reached, criterion by criterion.
460
+ *
461
+ * `audits` decides `evaluated` rather than the engine's capabilities alone: a
462
+ * rule that could have run and never matched anything has not established that
463
+ * the criterion is met on this site, and saying otherwise would be a coverage
464
+ * claim resting on an empty page.
465
+ */
466
+ function buildCoverage(audits, tags = DEFAULT_TAGS) {
467
+ const byCriterion = rulesByCriterion(tags);
468
+ const decided = decidedRules(audits);
469
+ const blinded = blindedRules(audits);
470
+ const criteria = WCAG22_AA_CRITERIA.map((criterion) => {
471
+ const rules = byCriterion.get(criterion.number) ?? [];
472
+ if (rules.length === 0) return {
473
+ ...criterion,
474
+ status: "no-automated-rule",
475
+ rules,
476
+ browserWouldAnswer: false
477
+ };
478
+ if (rules.some((rule) => decided.has(rule))) return {
479
+ ...criterion,
480
+ status: "evaluated",
481
+ rules,
482
+ browserWouldAnswer: false
483
+ };
484
+ const engineBlind = rules.filter((rule) => blinded.has(rule) || ENGINE_BLIND_RULES[rule] !== void 0);
485
+ if (engineBlind.length > 0) return {
486
+ ...criterion,
487
+ status: "not-evaluated",
488
+ rules,
489
+ browserWouldAnswer: engineBlind.every((rule) => manualCheckFor(rule)?.browserAnswers === true)
490
+ };
491
+ return {
492
+ ...criterion,
493
+ status: "nothing-to-check",
494
+ rules,
495
+ browserWouldAnswer: false
496
+ };
497
+ });
498
+ const count = (status) => criteria.filter((criterion) => criterion.status === status).length;
499
+ return {
500
+ criteria,
501
+ evaluated: count("evaluated"),
502
+ notEvaluated: count("not-evaluated"),
503
+ nothingToCheck: count("nothing-to-check"),
504
+ noAutomatedRule: count("no-automated-rule"),
505
+ browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
506
+ total: WCAG22_AA_CRITERIA.length
507
+ };
508
+ }
509
+ /**
510
+ * The sentence a reader needs before drawing anything from a clean report.
511
+ *
512
+ * Deliberately leads with what was not checked. A report that opened with "23
513
+ * criteria evaluated" would read as a score, which is precisely what this
514
+ * refuses to be.
515
+ */
516
+ function coverageSummary(coverage) {
517
+ const unautomatable = coverage.noAutomatedRule;
518
+ return `Of the ${coverage.total} WCAG 2.2 A and AA success criteria, ${unautomatable} cannot be checked by any automated engine and need a person. This run reached a verdict on ${coverage.evaluated}.`;
519
+ }
520
+ //#endregion
521
+ export { understandingUrl as i, coverageSummary as n, manualCheckFor as r, buildCoverage as t };
@@ -1,3 +1,4 @@
1
+ import { i as stripBom } from "./collect-CFM8gEVv.js";
1
2
  /** Requests in flight at once. Politeness, not throughput. */
2
3
  const REQUEST_CONCURRENCY = 4;
3
4
  var CrawlError = class extends Error {
@@ -142,31 +143,18 @@ function disallowedPaths(robots) {
142
143
  return paths;
143
144
  }
144
145
  /**
145
- * Paths this crawler must not visit, from the site's own robots.txt.
146
+ * One of the two files a site publishes about itself, or undefined.
146
147
  *
147
148
  * Read directly rather than through fetchPage, which correctly refuses anything
148
- * that is not a page. A site with no robots.txt disallows nothing.
149
+ * that is not a page. A site that does not publish one is the ordinary case,
150
+ * not a failure, so it comes back empty either way.
149
151
  */
150
- async function blockedPaths(entry, impl) {
152
+ async function fetchSiteFile(entry, impl, name) {
151
153
  try {
152
- const response = await impl(new URL("/robots.txt", entry).href, { redirect: "follow" });
153
- return response.ok ? disallowedPaths(await response.text()) : [];
154
+ const response = await impl(new URL(name, entry).href, { redirect: "follow" });
155
+ return response.ok ? await response.text() : void 0;
154
156
  } catch {
155
- return [];
156
- }
157
- }
158
- /**
159
- * Page URLs the site lists for itself.
160
- *
161
- * Worth one request: a sitemap finds pages nothing links to, which link
162
- * following alone never reaches.
163
- */
164
- async function sitemapUrls(entry, impl) {
165
- try {
166
- const response = await impl(new URL("/sitemap.xml", entry).href, { redirect: "follow" });
167
- return response.ok ? urlsFromSitemap(await response.text(), entry) : [];
168
- } catch {
169
- return [];
157
+ return;
170
158
  }
171
159
  }
172
160
  /**
@@ -182,7 +170,8 @@ async function crawlSite(entry, options = {}) {
182
170
  const maxDepth = options.maxDepth ?? 3;
183
171
  const timeoutMs = options.timeoutMs ?? 15e3;
184
172
  const failures = [];
185
- const blocked = options.ignoreRobots ? [] : await blockedPaths(entry, impl);
173
+ const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt");
174
+ const blocked = robots === void 0 ? [] : disallowedPaths(robots);
186
175
  const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
187
176
  let discovery = "links";
188
177
  const queue = [];
@@ -195,7 +184,8 @@ async function crawlSite(entry, options = {}) {
195
184
  depth
196
185
  });
197
186
  };
198
- const listed = await sitemapUrls(entry, impl);
187
+ const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml");
188
+ const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
199
189
  if (listed.length > 0) {
200
190
  discovery = "sitemap";
201
191
  for (const url of listed) enqueue(url, 0);
@@ -220,7 +210,7 @@ async function crawlSite(entry, options = {}) {
220
210
  pages.push({
221
211
  absolutePath: url.href,
222
212
  relativePath: pageIdentity(url),
223
- html: html.charCodeAt(0) === 65279 ? html.slice(1) : html
213
+ html: stripBom(html)
224
214
  });
225
215
  options.onProgress?.(pages.length, queue.length);
226
216
  if (item.depth < maxDepth) for (const link of linksFrom(html, url)) enqueue(link, item.depth + 1);
@@ -0,0 +1,28 @@
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
2
+ //#region src/eleventy/index.d.ts
3
+ /** What `eleventy.after` is given. Only two of its fields are read. */
4
+ interface EleventyAfterEvent {
5
+ dir: {
6
+ input: string;
7
+ output: string;
8
+ };
9
+ /** `build`, `watch` or `serve`. Only a one-shot build is audited. */
10
+ runMode?: string;
11
+ /** `fs`, `json`, `ndjson` or `text`. Only `fs` writes files to audit. */
12
+ outputMode?: string;
13
+ }
14
+ /** The part of Eleventy's config object this plugin touches. */
15
+ interface EleventyConfigLike {
16
+ on(event: 'eleventy.after', handler: (event: EleventyAfterEvent) => Promise<void>): void;
17
+ }
18
+ type EaaKitEleventyOptions = IntegrationOptions;
19
+ /**
20
+ * Usable both as a bare plugin and as a configured one.
21
+ *
22
+ * `addPlugin(eaaKit)` and `addPlugin(eaaKit, { failOn: 'moderate' })` are both
23
+ * how Eleventy plugins are written, and a plugin that only supported the second
24
+ * would be the one people got wrong.
25
+ */
26
+ declare function eaaKit(eleventyConfig: EleventyConfigLike, options?: EaaKitEleventyOptions): void;
27
+ //#endregion
28
+ export { BuildAuditError, EaaKitEleventyOptions, EleventyAfterEvent, EleventyConfigLike, eaaKit as default };
@@ -0,0 +1,19 @@
1
+ import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
2
+ import path from "node:path";
3
+ //#region src/eleventy/index.ts
4
+ /**
5
+ * Usable both as a bare plugin and as a configured one.
6
+ *
7
+ * `addPlugin(eaaKit)` and `addPlugin(eaaKit, { failOn: 'moderate' })` are both
8
+ * how Eleventy plugins are written, and a plugin that only supported the second
9
+ * would be the one people got wrong.
10
+ */
11
+ function eaaKit(eleventyConfig, options = {}) {
12
+ eleventyConfig.on("eleventy.after", async (event) => {
13
+ if (event.runMode !== void 0 && event.runMode !== "build") return;
14
+ if (event.outputMode !== void 0 && event.outputMode !== "fs") return;
15
+ await auditBuild(path.resolve(event.dir.output), options, stderrLogger());
16
+ });
17
+ }
18
+ //#endregion
19
+ export { BuildAuditError, eaaKit as default };