equall-cli 0.1.1 → 0.1.2

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.
@@ -1,690 +0,0 @@
1
- // src/scoring/score.ts
2
- var SEVERITY_WEIGHT = {
3
- critical: 10,
4
- serious: 5,
5
- moderate: 2,
6
- minor: 1
7
- };
8
- var MAX_PENALTY_PER_CRITERION = 15;
9
- function computeScanResult(issues, filesScanned, scannersUsed, durationMs, targetLevel = "AA", criteriaCovered = [], criteriaTotal = 0) {
10
- const summary = computeSummary(issues, filesScanned);
11
- const score = computeScore(issues, filesScanned);
12
- const pourScores = computePourScores(issues, filesScanned);
13
- const conformanceLevel = computeConformanceLevel(issues, summary, targetLevel);
14
- return {
15
- score,
16
- conformance_level: conformanceLevel,
17
- pour_scores: pourScores,
18
- issues,
19
- summary,
20
- scanners_used: scannersUsed,
21
- criteria_covered: criteriaCovered,
22
- criteria_total: criteriaTotal,
23
- scanned_at: (/* @__PURE__ */ new Date()).toISOString(),
24
- duration_ms: durationMs
25
- };
26
- }
27
- function computeSummary(issues, filesScanned) {
28
- const bySeverity = {
29
- critical: 0,
30
- serious: 0,
31
- moderate: 0,
32
- minor: 0
33
- };
34
- const byScanner = {};
35
- const criteriaSet = /* @__PURE__ */ new Set();
36
- const failedCriteriaSet = /* @__PURE__ */ new Set();
37
- for (const issue of issues) {
38
- bySeverity[issue.severity]++;
39
- byScanner[issue.scanner] = (byScanner[issue.scanner] ?? 0) + 1;
40
- for (const c of issue.wcag_criteria) {
41
- criteriaSet.add(c);
42
- failedCriteriaSet.add(c);
43
- }
44
- }
45
- return {
46
- files_scanned: filesScanned,
47
- total_issues: issues.length,
48
- by_severity: bySeverity,
49
- by_scanner: byScanner,
50
- criteria_tested: [...criteriaSet].sort(),
51
- criteria_failed: [...failedCriteriaSet].sort()
52
- };
53
- }
54
- function computeScore(issues, filesScanned) {
55
- if (issues.length === 0) return 100;
56
- const penaltyByCriterion = /* @__PURE__ */ new Map();
57
- for (const issue of issues) {
58
- const weight = SEVERITY_WEIGHT[issue.severity];
59
- for (const criterion of issue.wcag_criteria) {
60
- const current = penaltyByCriterion.get(criterion) ?? 0;
61
- penaltyByCriterion.set(criterion, Math.min(current + weight, MAX_PENALTY_PER_CRITERION));
62
- }
63
- if (issue.wcag_criteria.length === 0) {
64
- const key = `_${issue.scanner}:${issue.scanner_rule_id}`;
65
- const current = penaltyByCriterion.get(key) ?? 0;
66
- penaltyByCriterion.set(key, Math.min(current + weight, MAX_PENALTY_PER_CRITERION));
67
- }
68
- }
69
- const totalRawPenalty = [...penaltyByCriterion.values()].reduce((a, b) => a + b, 0);
70
- const scaleFactor = 1 / (1 + Math.log10(Math.max(1, filesScanned)));
71
- const scaledPenalty = totalRawPenalty * scaleFactor;
72
- const k = 0.02;
73
- const score = 100 * Math.exp(-k * scaledPenalty);
74
- return Math.max(0, Math.round(score));
75
- }
76
- function computePourScores(issues, filesScanned) {
77
- const pourIssues = {
78
- perceivable: [],
79
- operable: [],
80
- understandable: [],
81
- robust: []
82
- };
83
- const pourCriteria = {
84
- perceivable: /* @__PURE__ */ new Set(),
85
- operable: /* @__PURE__ */ new Set(),
86
- understandable: /* @__PURE__ */ new Set(),
87
- robust: /* @__PURE__ */ new Set()
88
- };
89
- for (const issue of issues) {
90
- if (!issue.pour) continue;
91
- pourIssues[issue.pour].push(issue);
92
- for (const c of issue.wcag_criteria) {
93
- pourCriteria[issue.pour].add(c);
94
- }
95
- }
96
- const scaleFactor = 1 / (1 + Math.log10(Math.max(1, filesScanned)));
97
- const k = 0.02;
98
- function pourScore(principle) {
99
- const principleIssues = pourIssues[principle];
100
- const criteriaCount = pourCriteria[principle].size;
101
- if (criteriaCount === 0 && principleIssues.length === 0) return null;
102
- if (principleIssues.length === 0) return 100;
103
- const penaltyByCriterion = /* @__PURE__ */ new Map();
104
- for (const issue of principleIssues) {
105
- const weight = SEVERITY_WEIGHT[issue.severity];
106
- for (const criterion of issue.wcag_criteria) {
107
- const current = penaltyByCriterion.get(criterion) ?? 0;
108
- penaltyByCriterion.set(criterion, Math.min(current + weight, MAX_PENALTY_PER_CRITERION));
109
- }
110
- if (issue.wcag_criteria.length === 0) {
111
- const key = `_${issue.scanner}:${issue.scanner_rule_id}`;
112
- const current = penaltyByCriterion.get(key) ?? 0;
113
- penaltyByCriterion.set(key, Math.min(current + weight, MAX_PENALTY_PER_CRITERION));
114
- }
115
- }
116
- const totalRawPenalty = [...penaltyByCriterion.values()].reduce((a, b) => a + b, 0);
117
- const scaledPenalty = totalRawPenalty * scaleFactor;
118
- return Math.max(0, Math.round(100 * Math.exp(-k * scaledPenalty)));
119
- }
120
- return {
121
- perceivable: pourScore("perceivable"),
122
- operable: pourScore("operable"),
123
- understandable: pourScore("understandable"),
124
- robust: pourScore("robust")
125
- };
126
- }
127
- function computeConformanceLevel(issues, summary, targetLevel) {
128
- const failedByLevel = {
129
- A: /* @__PURE__ */ new Set(),
130
- AA: /* @__PURE__ */ new Set(),
131
- AAA: /* @__PURE__ */ new Set()
132
- };
133
- for (const issue of issues) {
134
- if (!issue.wcag_level) continue;
135
- for (const c of issue.wcag_criteria) {
136
- failedByLevel[issue.wcag_level].add(c);
137
- }
138
- }
139
- const hasA = failedByLevel.A.size > 0;
140
- const hasAA = failedByLevel.AA.size > 0;
141
- const hasAAA = failedByLevel.AAA.size > 0;
142
- if (targetLevel === "A") {
143
- if (!hasA) return summary.criteria_tested.length > 0 ? "A" : "None";
144
- return "Partial A";
145
- }
146
- if (targetLevel === "AA") {
147
- if (!hasA && !hasAA) return summary.criteria_tested.length > 0 ? "AA" : "None";
148
- if (!hasA) return "A";
149
- return "Partial A";
150
- }
151
- if (targetLevel === "AAA") {
152
- if (!hasA && !hasAA && !hasAAA) return summary.criteria_tested.length > 0 ? "AAA" : "None";
153
- if (!hasA && !hasAA) return "AA";
154
- if (!hasA) return "A";
155
- return "Partial A";
156
- }
157
- return "None";
158
- }
159
-
160
- // src/scan.ts
161
- import { resolve as resolve2 } from "path";
162
-
163
- // src/discover.ts
164
- import { readFile } from "fs/promises";
165
- import { resolve, extname } from "path";
166
- var EXT_MAP = {
167
- ".html": "html",
168
- ".htm": "html",
169
- ".jsx": "jsx",
170
- ".tsx": "tsx",
171
- ".vue": "vue",
172
- ".svelte": "svelte",
173
- ".astro": "astro"
174
- };
175
- var DEFAULT_INCLUDE = [
176
- "**/*.html",
177
- "**/*.htm",
178
- "**/*.jsx",
179
- "**/*.tsx",
180
- "**/*.vue",
181
- "**/*.svelte",
182
- "**/*.astro"
183
- ];
184
- var DEFAULT_EXCLUDE = [
185
- "**/node_modules/**",
186
- "**/dist/**",
187
- "**/build/**",
188
- "**/.next/**",
189
- "**/.nuxt/**",
190
- "**/coverage/**",
191
- "**/*.min.*",
192
- "**/*.test.*",
193
- "**/*.spec.*",
194
- "**/__tests__/**",
195
- "**/*.stories.*",
196
- "**/storybook-static/**"
197
- ];
198
- async function discoverFiles(rootPath, options) {
199
- const { globby } = await import("globby");
200
- const includePatterns = options.include_patterns.length > 0 ? options.include_patterns : DEFAULT_INCLUDE;
201
- const excludePatterns = [
202
- ...DEFAULT_EXCLUDE,
203
- ...options.exclude_patterns
204
- ];
205
- const paths = await globby(includePatterns, {
206
- cwd: rootPath,
207
- ignore: excludePatterns,
208
- absolute: false,
209
- gitignore: true
210
- });
211
- const files = [];
212
- for (const relativePath of paths) {
213
- const absolutePath = resolve(rootPath, relativePath);
214
- try {
215
- const content = await readFile(absolutePath, "utf-8");
216
- const ext = extname(relativePath).toLowerCase();
217
- const type = EXT_MAP[ext] ?? "other";
218
- files.push({
219
- path: relativePath,
220
- absolute_path: absolutePath,
221
- content,
222
- type
223
- });
224
- } catch {
225
- }
226
- }
227
- return files;
228
- }
229
-
230
- // src/scanners/axe-scanner.ts
231
- function parseWcagTags(tags) {
232
- const criteria = [];
233
- let level = null;
234
- let pour = null;
235
- for (const tag of tags) {
236
- const criterionMatch = tag.match(/^wcag([1-4])(\d)(\d+)$/);
237
- if (criterionMatch) {
238
- criteria.push(`${criterionMatch[1]}.${criterionMatch[2]}.${criterionMatch[3]}`);
239
- continue;
240
- }
241
- if (tag === "wcag2a" || tag === "wcag21a" || tag === "wcag22a") level = "A";
242
- else if (tag === "wcag2aa" || tag === "wcag21aa" || tag === "wcag22aa") level = "AA";
243
- else if (tag === "wcag2aaa" || tag === "wcag21aaa" || tag === "wcag22aaa") level = "AAA";
244
- if (tag === "cat.text-alternatives" || tag === "cat.color" || tag === "cat.sensory-and-visual-cues" || tag === "cat.time-and-media" || tag === "cat.tables" || tag === "cat.forms") {
245
- pour = pour ?? "perceivable";
246
- }
247
- if (tag === "cat.keyboard" || tag === "cat.navigation" || tag === "cat.time-and-media") {
248
- pour = pour ?? "operable";
249
- }
250
- if (tag === "cat.language" || tag === "cat.parsing" || tag === "cat.forms") {
251
- pour = pour ?? "understandable";
252
- }
253
- if (tag === "cat.name-role-value" || tag === "cat.structure" || tag === "cat.aria") {
254
- pour = pour ?? "robust";
255
- }
256
- }
257
- return { criteria, level, pour };
258
- }
259
- function mapSeverity(impact) {
260
- switch (impact) {
261
- case "critical":
262
- return "critical";
263
- case "serious":
264
- return "serious";
265
- case "moderate":
266
- return "moderate";
267
- case "minor":
268
- return "minor";
269
- default:
270
- return "moderate";
271
- }
272
- }
273
- function pourFromCriterion(criterion) {
274
- const principle = criterion.charAt(0);
275
- switch (principle) {
276
- case "1":
277
- return "perceivable";
278
- case "2":
279
- return "operable";
280
- case "3":
281
- return "understandable";
282
- case "4":
283
- return "robust";
284
- default:
285
- return null;
286
- }
287
- }
288
- var AxeScanner = class {
289
- name = "axe-core";
290
- version = "";
291
- coveredCriteria = [
292
- "1.1.1",
293
- "1.2.1",
294
- "1.2.2",
295
- "1.3.1",
296
- "1.3.4",
297
- "1.3.5",
298
- "1.4.1",
299
- "1.4.2",
300
- "1.4.3",
301
- "1.4.4",
302
- "1.4.12",
303
- "2.1.1",
304
- "2.1.3",
305
- "2.2.1",
306
- "2.2.2",
307
- "2.4.1",
308
- "2.4.2",
309
- "2.4.4",
310
- "2.5.3",
311
- "2.5.8",
312
- "3.1.1",
313
- "3.1.2",
314
- "3.3.2",
315
- "4.1.2"
316
- ];
317
- async isAvailable() {
318
- try {
319
- await import("axe-core");
320
- await import("jsdom");
321
- return true;
322
- } catch {
323
- return false;
324
- }
325
- }
326
- async scan(context) {
327
- const axeModule = await import("axe-core");
328
- const axe = axeModule.default ?? axeModule;
329
- const { JSDOM } = await import("jsdom");
330
- this.version = axe.version ?? "unknown";
331
- const htmlFiles = context.files.filter(
332
- (f) => f.type === "html" || f.type === "jsx" || f.type === "tsx" || f.type === "vue"
333
- );
334
- const scannableFiles = htmlFiles.filter((f) => {
335
- if (f.type === "html") return true;
336
- return f.content.includes("<") && (f.content.includes("return") || f.content.includes("<template"));
337
- });
338
- const allIssues = [];
339
- const runTags = buildRunTags(context.options.wcag_level);
340
- for (const file of scannableFiles) {
341
- try {
342
- const html = extractHtml(file.content, file.type);
343
- if (!html.trim()) continue;
344
- const issues = await this.scanHtml(axe, JSDOM, html, file.path, runTags);
345
- allIssues.push(...issues);
346
- } catch (error) {
347
- const msg = error instanceof Error ? error.message : String(error);
348
- console.warn(` [axe-core] Skipped ${file.path}: ${msg.slice(0, 80)}`);
349
- }
350
- }
351
- return allIssues;
352
- }
353
- async scanHtml(axe, JSDOMClass, html, filePath, runTags) {
354
- const fullHtml = html.includes("<html") ? html : `
355
- <!DOCTYPE html>
356
- <html lang="en">
357
- <head><title>Scan</title></head>
358
- <body>${html}</body>
359
- </html>
360
- `;
361
- const originalConsoleError = console.error;
362
- console.error = (...args) => {
363
- const msg = String(args[0] ?? "");
364
- if (msg.includes("Not implemented") || msg.includes("HTMLCanvasElement")) return;
365
- originalConsoleError(...args);
366
- };
367
- const dom = new JSDOMClass(fullHtml, {
368
- runScripts: "outside-only",
369
- pretendToBeVisual: true,
370
- virtualConsole: new (await import("jsdom")).VirtualConsole()
371
- });
372
- try {
373
- const document = dom.window.document;
374
- axe.configure({
375
- rules: [
376
- { id: "color-contrast", enabled: false },
377
- { id: "color-contrast-enhanced", enabled: false }
378
- ]
379
- });
380
- const results = await axe.run(document.documentElement, {
381
- runOnly: {
382
- type: "tag",
383
- values: runTags
384
- },
385
- resultTypes: ["violations"]
386
- });
387
- const issues = [];
388
- for (const violation of results.violations) {
389
- const { criteria, level, pour } = parseWcagTags(violation.tags);
390
- const derivedPour = pour ?? (criteria[0] ? pourFromCriterion(criteria[0]) : null);
391
- for (const node of violation.nodes) {
392
- issues.push({
393
- scanner: "axe-core",
394
- scanner_rule_id: violation.id,
395
- wcag_criteria: criteria,
396
- wcag_level: level,
397
- pour: derivedPour,
398
- file_path: filePath,
399
- line: null,
400
- // axe-core doesn't provide line numbers on static HTML
401
- column: null,
402
- html_snippet: node.html?.slice(0, 200) ?? null,
403
- severity: mapSeverity(violation.impact),
404
- message: `${violation.help} (${violation.id})`,
405
- help_url: violation.helpUrl ?? null,
406
- suggestion: node.failureSummary ?? null
407
- });
408
- }
409
- }
410
- return issues;
411
- } finally {
412
- dom.window.close();
413
- console.error = originalConsoleError;
414
- }
415
- }
416
- };
417
- function buildRunTags(level) {
418
- const tags = ["wcag2a", "wcag21a", "wcag22a"];
419
- if (level === "AA" || level === "AAA") {
420
- tags.push("wcag2aa", "wcag21aa", "wcag22aa");
421
- }
422
- if (level === "AAA") {
423
- tags.push("wcag2aaa", "wcag21aaa", "wcag22aaa");
424
- }
425
- tags.push("best-practice");
426
- return tags;
427
- }
428
- function extractHtml(content, type) {
429
- if (type === "html") return content;
430
- if (type === "vue") {
431
- const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
432
- return templateMatch?.[1] ?? "";
433
- }
434
- if (type === "jsx" || type === "tsx") {
435
- const returnMatch = content.match(/return\s*\(\s*([\s\S]*?)\s*\)\s*[;\n}]/);
436
- if (returnMatch) return returnMatch[1];
437
- const singleReturn = content.match(/return\s+(<[\s\S]*?>[\s\S]*?<\/[\s\S]*?>)/);
438
- return singleReturn?.[1] ?? "";
439
- }
440
- return content;
441
- }
442
-
443
- // src/scanners/eslint-jsx-a11y-scanner.ts
444
- var RULE_WCAG_MAP = {
445
- "jsx-a11y/alt-text": { criteria: ["1.1.1"], pour: "perceivable" },
446
- "jsx-a11y/anchor-has-content": { criteria: ["2.4.4", "4.1.2"], pour: "operable" },
447
- "jsx-a11y/anchor-is-valid": { criteria: ["2.4.4"], pour: "operable" },
448
- "jsx-a11y/aria-activedescendant-has-tabindex": { criteria: ["4.1.2"], pour: "robust" },
449
- "jsx-a11y/aria-props": { criteria: ["4.1.2"], pour: "robust" },
450
- "jsx-a11y/aria-proptypes": { criteria: ["4.1.2"], pour: "robust" },
451
- "jsx-a11y/aria-role": { criteria: ["4.1.2"], pour: "robust" },
452
- "jsx-a11y/aria-unsupported-elements": { criteria: ["4.1.2"], pour: "robust" },
453
- "jsx-a11y/autocomplete-valid": { criteria: ["1.3.5"], pour: "perceivable" },
454
- "jsx-a11y/click-events-have-key-events": { criteria: ["2.1.1"], pour: "operable" },
455
- "jsx-a11y/heading-has-content": { criteria: ["2.4.6"], pour: "operable" },
456
- "jsx-a11y/html-has-lang": { criteria: ["3.1.1"], pour: "understandable" },
457
- "jsx-a11y/iframe-has-title": { criteria: ["2.4.1", "4.1.2"], pour: "operable" },
458
- "jsx-a11y/img-redundant-alt": { criteria: ["1.1.1"], pour: "perceivable" },
459
- "jsx-a11y/interactive-supports-focus": { criteria: ["2.1.1", "2.4.7"], pour: "operable" },
460
- "jsx-a11y/label-has-associated-control": { criteria: ["1.3.1", "3.3.2"], pour: "perceivable" },
461
- "jsx-a11y/lang": { criteria: ["3.1.2"], pour: "understandable" },
462
- "jsx-a11y/media-has-caption": { criteria: ["1.2.2", "1.2.3"], pour: "perceivable" },
463
- "jsx-a11y/mouse-events-have-key-events": { criteria: ["2.1.1"], pour: "operable" },
464
- "jsx-a11y/no-access-key": { criteria: ["2.1.1"], pour: "operable" },
465
- "jsx-a11y/no-autofocus": { criteria: ["2.4.3"], pour: "operable" },
466
- "jsx-a11y/no-distracting-elements": { criteria: ["2.3.1"], pour: "operable" },
467
- "jsx-a11y/no-interactive-element-to-noninteractive-role": { criteria: ["4.1.2"], pour: "robust" },
468
- "jsx-a11y/no-noninteractive-element-interactions": { criteria: ["2.1.1"], pour: "operable" },
469
- "jsx-a11y/no-noninteractive-element-to-interactive-role": { criteria: ["4.1.2"], pour: "robust" },
470
- "jsx-a11y/no-noninteractive-tabindex": { criteria: ["2.4.3"], pour: "operable" },
471
- "jsx-a11y/no-redundant-roles": { criteria: ["4.1.2"], pour: "robust" },
472
- "jsx-a11y/no-static-element-interactions": { criteria: ["2.1.1"], pour: "operable" },
473
- "jsx-a11y/prefer-tag-over-role": { criteria: ["4.1.2"], pour: "robust" },
474
- "jsx-a11y/role-has-required-aria-props": { criteria: ["4.1.2"], pour: "robust" },
475
- "jsx-a11y/role-supports-aria-props": { criteria: ["4.1.2"], pour: "robust" },
476
- "jsx-a11y/scope": { criteria: ["1.3.1"], pour: "perceivable" },
477
- "jsx-a11y/tabindex-no-positive": { criteria: ["2.4.3"], pour: "operable" }
478
- };
479
- function mapSeverity2(eslintSeverity) {
480
- switch (eslintSeverity) {
481
- case 2:
482
- return "serious";
483
- // error
484
- case 1:
485
- return "moderate";
486
- // warning
487
- default:
488
- return "minor";
489
- }
490
- }
491
- var AA_CRITERIA = /* @__PURE__ */ new Set(["1.3.5", "2.4.6", "2.4.7", "3.1.2", "3.3.2"]);
492
- var EslintJsxA11yScanner = class {
493
- name = "eslint-jsx-a11y";
494
- version = "";
495
- coveredCriteria = [
496
- "1.1.1",
497
- "1.2.2",
498
- "1.2.3",
499
- "1.3.1",
500
- "1.3.5",
501
- "2.1.1",
502
- "2.3.1",
503
- "2.4.1",
504
- "2.4.3",
505
- "2.4.4",
506
- "2.4.6",
507
- "2.4.7",
508
- "3.1.1",
509
- "3.1.2",
510
- "3.3.2",
511
- "4.1.2"
512
- ];
513
- async isAvailable() {
514
- try {
515
- await import("eslint");
516
- await import("eslint-plugin-jsx-a11y");
517
- return true;
518
- } catch {
519
- return false;
520
- }
521
- }
522
- async scan(context) {
523
- const { ESLint } = await import("eslint");
524
- const jsxA11yModule = await import("eslint-plugin-jsx-a11y");
525
- const jsxA11y = jsxA11yModule.default ?? jsxA11yModule;
526
- try {
527
- const { createRequire } = await import("module");
528
- const require2 = createRequire(import.meta.url);
529
- const pluginPkg = require2("eslint-plugin-jsx-a11y/package.json");
530
- this.version = pluginPkg.version ?? "unknown";
531
- } catch {
532
- this.version = jsxA11y?.meta?.version ?? "unknown";
533
- }
534
- const jsxFiles = context.files.filter((f) => f.type === "jsx" || f.type === "tsx");
535
- if (jsxFiles.length === 0) return [];
536
- const tsParser = await import("@typescript-eslint/parser");
537
- const eslint = new ESLint({
538
- overrideConfigFile: true,
539
- overrideConfig: [
540
- {
541
- files: ["**/*.{jsx,tsx,js,ts}"],
542
- plugins: {
543
- "jsx-a11y": jsxA11y
544
- },
545
- rules: Object.fromEntries(
546
- Object.keys(RULE_WCAG_MAP).map((rule) => [rule, "error"])
547
- ),
548
- languageOptions: {
549
- parser: tsParser.default ?? tsParser,
550
- parserOptions: {
551
- ecmaFeatures: { jsx: true }
552
- }
553
- }
554
- }
555
- ],
556
- cwd: context.root_path
557
- });
558
- const allIssues = [];
559
- const filePaths = jsxFiles.map((f) => f.absolute_path);
560
- try {
561
- const results = await eslint.lintFiles(filePaths);
562
- for (const result of results) {
563
- const relativePath = jsxFiles.find(
564
- (f) => f.absolute_path === result.filePath
565
- )?.path ?? result.filePath;
566
- for (const msg of result.messages) {
567
- if (!msg.ruleId || !msg.ruleId.startsWith("jsx-a11y/")) continue;
568
- const wcagMapping = RULE_WCAG_MAP[msg.ruleId];
569
- const criteria = wcagMapping?.criteria ?? [];
570
- const pour = wcagMapping?.pour ?? null;
571
- const aCriteria = criteria.filter((c) => !AA_CRITERIA.has(c));
572
- const aaCriteria = criteria.filter((c) => AA_CRITERIA.has(c));
573
- const groups = [];
574
- if (aCriteria.length > 0) groups.push({ criteria: aCriteria, level: "A" });
575
- if (aaCriteria.length > 0) groups.push({ criteria: aaCriteria, level: "AA" });
576
- if (groups.length === 0) groups.push({ criteria, level: "A" });
577
- for (const group of groups) {
578
- allIssues.push({
579
- scanner: "eslint-jsx-a11y",
580
- scanner_rule_id: msg.ruleId,
581
- wcag_criteria: group.criteria,
582
- wcag_level: group.level,
583
- pour,
584
- file_path: relativePath,
585
- line: msg.line ?? null,
586
- column: msg.column ?? null,
587
- html_snippet: null,
588
- severity: mapSeverity2(msg.severity),
589
- message: `${msg.message} (${msg.ruleId})`,
590
- help_url: `https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/${msg.ruleId.replace("jsx-a11y/", "")}.md`,
591
- suggestion: msg.fix ? "Auto-fixable" : null
592
- });
593
- }
594
- }
595
- }
596
- } catch (error) {
597
- const errMsg = error instanceof Error ? error.message : String(error);
598
- console.warn(` [eslint-jsx-a11y] Scan failed: ${errMsg.slice(0, 120)}`);
599
- }
600
- return allIssues;
601
- }
602
- };
603
-
604
- // src/scanners/index.ts
605
- var ALL_SCANNERS = [
606
- new AxeScanner(),
607
- new EslintJsxA11yScanner()
608
- ];
609
- async function getAvailableScanners() {
610
- const checks = await Promise.all(
611
- ALL_SCANNERS.map(async (scanner) => ({
612
- scanner,
613
- available: await scanner.isAvailable()
614
- }))
615
- );
616
- return checks.filter((c) => c.available).map((c) => c.scanner);
617
- }
618
-
619
- // src/scan.ts
620
- async function runScan(options = {}) {
621
- const rootPath = resolve2(options.path ?? process.cwd());
622
- const startTime = Date.now();
623
- const scanOptions = {
624
- wcag_level: options.level ?? "AA",
625
- include_patterns: options.include ?? [],
626
- exclude_patterns: options.exclude ?? []
627
- };
628
- const files = await discoverFiles(rootPath, scanOptions);
629
- if (files.length === 0) {
630
- return computeScanResult([], 0, [], Date.now() - startTime, scanOptions.wcag_level);
631
- }
632
- const scanners = await getAvailableScanners();
633
- if (scanners.length === 0) {
634
- console.warn("No scanners available. Install axe-core and jsdom for HTML scanning.");
635
- return computeScanResult([], files.length, [], Date.now() - startTime, scanOptions.wcag_level);
636
- }
637
- const scanContext = { root_path: rootPath, files, options: scanOptions };
638
- const scannerResults = await Promise.allSettled(
639
- scanners.map(async (scanner) => {
640
- const issues = await scanner.scan(scanContext);
641
- return {
642
- scanner,
643
- issues
644
- };
645
- })
646
- );
647
- const allIssues = [];
648
- const scannersUsed = [];
649
- for (const result of scannerResults) {
650
- if (result.status === "fulfilled") {
651
- const { scanner, issues } = result.value;
652
- allIssues.push(...issues);
653
- scannersUsed.push({
654
- name: scanner.name,
655
- version: scanner.version,
656
- rules_count: 0,
657
- // Could be enhanced per scanner
658
- issues_found: issues.length
659
- });
660
- } else {
661
- const err = result.reason instanceof Error ? result.reason.message : String(result.reason);
662
- console.warn(` [scanner] Failed: ${err.slice(0, 120)}`);
663
- }
664
- }
665
- const deduped = deduplicateIssues(allIssues);
666
- const criteriaCovered = [...new Set(scanners.flatMap((s) => s.coveredCriteria))].sort();
667
- const WCAG_TOTAL = { A: 30, AA: 57, AAA: 78 };
668
- const criteriaTotal = WCAG_TOTAL[scanOptions.wcag_level] ?? 57;
669
- const durationMs = Date.now() - startTime;
670
- return computeScanResult(deduped, files.length, scannersUsed, durationMs, scanOptions.wcag_level, criteriaCovered, criteriaTotal);
671
- }
672
- function deduplicateIssues(issues) {
673
- const seen = /* @__PURE__ */ new Set();
674
- const result = [];
675
- for (const issue of issues) {
676
- const sortedCriteria = [...issue.wcag_criteria].sort().join(",");
677
- const location = issue.line != null ? `L${issue.line}:${issue.column ?? 0}` : issue.html_snippet?.slice(0, 80) ?? "no-loc";
678
- const key = `${issue.file_path}|${sortedCriteria}|${location}`;
679
- if (!seen.has(key)) {
680
- seen.add(key);
681
- result.push(issue);
682
- }
683
- }
684
- return result;
685
- }
686
-
687
- export {
688
- computeScanResult,
689
- runScan
690
- };