ally-a11y 1.0.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 (84) hide show
  1. package/ACCESSIBILITY.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +940 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +528 -0
  6. package/dist/commands/audit-palette.d.ts +18 -0
  7. package/dist/commands/audit-palette.js +613 -0
  8. package/dist/commands/auto-pr.d.ts +19 -0
  9. package/dist/commands/auto-pr.js +434 -0
  10. package/dist/commands/badge.d.ts +11 -0
  11. package/dist/commands/badge.js +143 -0
  12. package/dist/commands/completion.d.ts +4 -0
  13. package/dist/commands/completion.js +185 -0
  14. package/dist/commands/crawl.d.ts +12 -0
  15. package/dist/commands/crawl.js +249 -0
  16. package/dist/commands/doctor.d.ts +5 -0
  17. package/dist/commands/doctor.js +233 -0
  18. package/dist/commands/explain.d.ts +12 -0
  19. package/dist/commands/explain.js +233 -0
  20. package/dist/commands/fix.d.ts +13 -0
  21. package/dist/commands/fix.js +668 -0
  22. package/dist/commands/health.d.ts +11 -0
  23. package/dist/commands/health.js +367 -0
  24. package/dist/commands/history.d.ts +10 -0
  25. package/dist/commands/history.js +191 -0
  26. package/dist/commands/init.d.ts +9 -0
  27. package/dist/commands/init.js +164 -0
  28. package/dist/commands/learn.d.ts +8 -0
  29. package/dist/commands/learn.js +592 -0
  30. package/dist/commands/pr-check.d.ts +12 -0
  31. package/dist/commands/pr-check.js +270 -0
  32. package/dist/commands/report.d.ts +11 -0
  33. package/dist/commands/report.js +375 -0
  34. package/dist/commands/scan-storybook.d.ts +18 -0
  35. package/dist/commands/scan-storybook.js +402 -0
  36. package/dist/commands/scan.d.ts +25 -0
  37. package/dist/commands/scan.js +673 -0
  38. package/dist/commands/stats.d.ts +5 -0
  39. package/dist/commands/stats.js +137 -0
  40. package/dist/commands/tree.d.ts +12 -0
  41. package/dist/commands/tree.js +635 -0
  42. package/dist/commands/triage.d.ts +13 -0
  43. package/dist/commands/triage.js +327 -0
  44. package/dist/commands/watch.d.ts +17 -0
  45. package/dist/commands/watch.js +302 -0
  46. package/dist/types/index.d.ts +60 -0
  47. package/dist/types/index.js +4 -0
  48. package/dist/utils/baseline.d.ts +62 -0
  49. package/dist/utils/baseline.js +169 -0
  50. package/dist/utils/browser.d.ts +78 -0
  51. package/dist/utils/browser.js +239 -0
  52. package/dist/utils/cache.d.ts +76 -0
  53. package/dist/utils/cache.js +178 -0
  54. package/dist/utils/config.d.ts +102 -0
  55. package/dist/utils/config.js +237 -0
  56. package/dist/utils/converters.d.ts +77 -0
  57. package/dist/utils/converters.js +200 -0
  58. package/dist/utils/copilot.d.ts +36 -0
  59. package/dist/utils/copilot.js +139 -0
  60. package/dist/utils/detect.d.ts +22 -0
  61. package/dist/utils/detect.js +197 -0
  62. package/dist/utils/enhanced-errors.d.ts +46 -0
  63. package/dist/utils/enhanced-errors.js +295 -0
  64. package/dist/utils/errors.d.ts +31 -0
  65. package/dist/utils/errors.js +149 -0
  66. package/dist/utils/fix-patterns.d.ts +56 -0
  67. package/dist/utils/fix-patterns.js +529 -0
  68. package/dist/utils/history-tracking.d.ts +94 -0
  69. package/dist/utils/history-tracking.js +230 -0
  70. package/dist/utils/history.d.ts +42 -0
  71. package/dist/utils/history.js +255 -0
  72. package/dist/utils/impact-scores.d.ts +44 -0
  73. package/dist/utils/impact-scores.js +257 -0
  74. package/dist/utils/retry.d.ts +24 -0
  75. package/dist/utils/retry.js +76 -0
  76. package/dist/utils/scanner.d.ts +74 -0
  77. package/dist/utils/scanner.js +606 -0
  78. package/dist/utils/scanner.test.d.ts +4 -0
  79. package/dist/utils/scanner.test.js +162 -0
  80. package/dist/utils/ui.d.ts +44 -0
  81. package/dist/utils/ui.js +276 -0
  82. package/mcp-server/dist/index.d.ts +8 -0
  83. package/mcp-server/dist/index.js +1923 -0
  84. package/package.json +88 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Impact Scoring System
3
+ *
4
+ * Scores accessibility violations by real-world user impact and business cost.
5
+ * Research shows developers are overwhelmed by violations - impact scoring
6
+ * helps prioritize what actually matters.
7
+ */
8
+ /**
9
+ * Base impact scores for violation types (0-100)
10
+ * Based on real-world user impact data
11
+ */
12
+ const BASE_IMPACT_SCORES = {
13
+ // CRITICAL - Blocks core functionality (95-100)
14
+ 'button-name': 98, // Can't use buttons at all
15
+ 'label': 97, // Can't complete forms
16
+ 'link-name': 96, // Can't navigate
17
+ 'aria-required-parent': 95, // Breaks component completely
18
+ 'aria-required-children': 95,
19
+ 'keyboard-trap': 100, // Completely stuck
20
+ // HIGH - Severely impacts usability (75-94)
21
+ 'html-has-lang': 89, // Screen reader uses wrong language
22
+ 'frame-title': 88, // Can't understand iframe content
23
+ 'form-field-multiple-labels': 87,
24
+ 'duplicate-id-active': 86, // Focus management broken
25
+ 'aria-hidden-focus': 85, // Can focus hidden elements
26
+ 'select-name': 84,
27
+ 'bypass': 83, // Must tab through entire nav
28
+ // MEDIUM - Reduces accessibility (40-74)
29
+ 'image-alt': 72, // Can't understand images
30
+ 'color-contrast': 70, // Hard to read text
31
+ 'heading-order': 65, // Confusing structure
32
+ 'landmark-one-main': 63, // Can't find main content
33
+ 'meta-viewport': 62, // Can't zoom
34
+ 'document-title': 60, // Don't know what page
35
+ 'list': 58,
36
+ 'listitem': 58,
37
+ 'definition-list': 55,
38
+ 'dlitem': 55,
39
+ // LOW - Best practice violations (0-39)
40
+ 'region': 35, // Missing landmarks
41
+ 'aria-allowed-attr': 32, // Invalid ARIA (still works)
42
+ 'aria-valid-attr-value': 30,
43
+ 'meta-refresh': 28, // Annoying but not blocking
44
+ 'tabindex': 25, // Non-standard tab order
45
+ 'accesskeys': 20, // Rarely used feature
46
+ 'empty-heading': 18, // Confusing but not blocking
47
+ 'p-as-heading': 15, // Semantic issue
48
+ };
49
+ /**
50
+ * Context multipliers adjust score based on page type
51
+ */
52
+ const CONTEXT_MULTIPLIERS = {
53
+ 'button-name': {
54
+ checkout: 1.5, // Critical in checkout flow
55
+ form: 1.3,
56
+ navigation: 1.2,
57
+ default: 1.0,
58
+ },
59
+ 'label': {
60
+ checkout: 1.5, // Can't complete purchase
61
+ form: 1.4,
62
+ default: 1.0,
63
+ },
64
+ 'link-name': {
65
+ navigation: 1.3, // Critical for navigation
66
+ landing: 1.2,
67
+ default: 1.0,
68
+ },
69
+ 'color-contrast': {
70
+ checkout: 1.3, // Must read CTA buttons
71
+ form: 1.2,
72
+ default: 1.0,
73
+ },
74
+ };
75
+ /**
76
+ * User impact data - who is affected by each violation
77
+ */
78
+ const AFFECTED_USERS = {
79
+ 'button-name': ['Screen reader users', 'Voice control users'],
80
+ 'label': ['Screen reader users', 'Voice control users', 'Cognitive disabilities'],
81
+ 'link-name': ['Screen reader users', 'Voice control users'],
82
+ 'image-alt': ['Screen reader users', 'Users on slow connections'],
83
+ 'color-contrast': ['Low vision users', 'Color blind users', 'Users in bright sunlight'],
84
+ 'html-has-lang': ['Screen reader users', 'Translation tool users'],
85
+ 'keyboard-trap': ['Keyboard-only users', 'Motor disabilities', 'Power users'],
86
+ 'bypass': ['Keyboard-only users', 'Screen reader users'],
87
+ 'heading-order': ['Screen reader users', 'Cognitive disabilities'],
88
+ 'frame-title': ['Screen reader users'],
89
+ 'meta-viewport': ['Mobile users', 'Low vision users'],
90
+ 'document-title': ['Screen reader users', 'Multiple tab users'],
91
+ 'region': ['Screen reader users'],
92
+ 'landmark-one-main': ['Screen reader users'],
93
+ 'aria-hidden-focus': ['Keyboard-only users', 'Screen reader users'],
94
+ 'select-name': ['Screen reader users', 'Voice control users'],
95
+ 'aria-required-parent': ['Screen reader users'],
96
+ 'aria-required-children': ['Screen reader users'],
97
+ 'duplicate-id-active': ['Screen reader users', 'Keyboard-only users'],
98
+ 'form-field-multiple-labels': ['Screen reader users', 'Voice control users'],
99
+ 'list': ['Screen reader users'],
100
+ 'listitem': ['Screen reader users'],
101
+ 'definition-list': ['Screen reader users'],
102
+ 'dlitem': ['Screen reader users'],
103
+ 'tabindex': ['Keyboard-only users'],
104
+ 'meta-refresh': ['Screen reader users', 'Cognitive disabilities'],
105
+ 'p-as-heading': ['Screen reader users'],
106
+ 'empty-heading': ['Screen reader users'],
107
+ 'accesskeys': ['Keyboard-only users'],
108
+ 'aria-allowed-attr': ['Screen reader users'],
109
+ 'aria-valid-attr-value': ['Screen reader users'],
110
+ };
111
+ /**
112
+ * Business impact reasoning templates
113
+ */
114
+ const BUSINESS_REASONING = {
115
+ 'button-name': 'Users cannot activate buttons, blocking core actions like "Submit", "Buy Now", or "Sign Up"',
116
+ 'label': 'Users cannot complete forms, preventing account creation, checkout, or data submission',
117
+ 'link-name': 'Users cannot navigate your site, unable to determine link destinations or purposes',
118
+ 'image-alt': 'Users miss critical visual information, including product images, diagrams, or icons',
119
+ 'color-contrast': 'Text is illegible for users with low vision or in bright environments',
120
+ 'html-has-lang': 'Screen readers pronounce content incorrectly, making text unintelligible',
121
+ 'keyboard-trap': 'Users become stuck in UI elements and cannot escape to continue browsing',
122
+ 'bypass': 'Users must tab through dozens of navigation links before reaching main content',
123
+ 'heading-order': 'Page structure is confusing, making content difficult to navigate and understand',
124
+ 'frame-title': 'Users cannot understand iframe content purpose without entering the iframe',
125
+ 'meta-viewport': 'Users cannot zoom text to readable size on mobile devices',
126
+ 'document-title': 'Users cannot identify page in browser tabs or history',
127
+ 'region': 'Users cannot quickly navigate to different page sections',
128
+ 'landmark-one-main': 'Users cannot quickly jump to main content area',
129
+ 'aria-hidden-focus': 'Hidden elements receive focus, confusing keyboard navigation',
130
+ 'select-name': 'Users cannot identify dropdown purpose or make selections confidently',
131
+ };
132
+ /**
133
+ * Estimated percentage of users affected
134
+ */
135
+ const ESTIMATED_USER_IMPACT = {
136
+ 'button-name': '15-20% of users',
137
+ 'label': '15-20% of users',
138
+ 'link-name': '15-20% of users',
139
+ 'image-alt': '10-15% of users',
140
+ 'color-contrast': '20-30% of users',
141
+ 'html-has-lang': '10-15% of users',
142
+ 'keyboard-trap': '8-12% of users',
143
+ 'bypass': '8-12% of users',
144
+ 'heading-order': '10-15% of users',
145
+ 'frame-title': '5-10% of users',
146
+ 'meta-viewport': '15-25% of users',
147
+ 'document-title': '10-15% of users',
148
+ 'default': '5-15% of users',
149
+ };
150
+ /**
151
+ * Calculate WCAG level for violation
152
+ */
153
+ function getWCAGLevel(violation) {
154
+ const tags = violation.tags || [];
155
+ if (tags.includes('wcag2aaa') || tags.includes('wcag21aaa') || tags.includes('wcag22aaa')) {
156
+ return 'AAA';
157
+ }
158
+ if (tags.includes('wcag2aa') || tags.includes('wcag21aa') || tags.includes('wcag22aa')) {
159
+ return 'AA';
160
+ }
161
+ return 'A';
162
+ }
163
+ /**
164
+ * Detect page context from HTML content
165
+ */
166
+ export function detectPageContext(html) {
167
+ const hasCheckoutKeywords = /checkout|cart|payment|order|purchase/i.test(html);
168
+ const hasFormKeywords = /<form|<input|<select|<textarea/i.test(html);
169
+ const hasNavKeywords = /<nav|navigation|menu/i.test(html);
170
+ let pageType = 'content';
171
+ if (hasCheckoutKeywords) {
172
+ pageType = 'checkout';
173
+ }
174
+ else if (hasFormKeywords) {
175
+ pageType = 'form';
176
+ }
177
+ else if (hasNavKeywords) {
178
+ pageType = 'navigation';
179
+ }
180
+ return {
181
+ pageType,
182
+ hasForm: hasFormKeywords,
183
+ hasCheckout: hasCheckoutKeywords,
184
+ isNavigation: hasNavKeywords,
185
+ };
186
+ }
187
+ /**
188
+ * Calculate impact score for a violation
189
+ */
190
+ export function calculateImpactScore(violation, context = {}) {
191
+ const violationId = violation.id;
192
+ // Get base score (default to 50 for unknown violations)
193
+ const baseScore = BASE_IMPACT_SCORES[violationId] || 50;
194
+ // Get context multiplier
195
+ const pageType = context.pageType || 'default';
196
+ const multipliers = CONTEXT_MULTIPLIERS[violationId] || {};
197
+ const multiplier = multipliers[pageType] || multipliers.default || 1.0;
198
+ // Calculate final score (capped at 100)
199
+ const score = Math.min(100, Math.round(baseScore * multiplier));
200
+ // Determine business impact category
201
+ let businessImpact;
202
+ if (score >= 95) {
203
+ businessImpact = 'critical';
204
+ }
205
+ else if (score >= 75) {
206
+ businessImpact = 'high';
207
+ }
208
+ else if (score >= 40) {
209
+ businessImpact = 'medium';
210
+ }
211
+ else {
212
+ businessImpact = 'low';
213
+ }
214
+ // Get affected user groups
215
+ const affectedUsers = AFFECTED_USERS[violationId] || ['Assistive technology users'];
216
+ // Get or generate reasoning
217
+ const reasoning = BUSINESS_REASONING[violationId] ||
218
+ `${violation.help}. This affects user experience for people using assistive technologies.`;
219
+ // Get estimated user impact
220
+ const estimatedUsers = ESTIMATED_USER_IMPACT[violationId] || ESTIMATED_USER_IMPACT.default;
221
+ // Get WCAG level
222
+ const wcagLevel = getWCAGLevel(violation);
223
+ return {
224
+ score,
225
+ reasoning,
226
+ affectedUsers,
227
+ businessImpact,
228
+ estimatedUsers,
229
+ wcagLevel,
230
+ };
231
+ }
232
+ /**
233
+ * Sort violations by impact score (highest first)
234
+ */
235
+ export function sortByImpact(violations, context = {}) {
236
+ const scored = violations.map((violation) => ({
237
+ violation,
238
+ impact: calculateImpactScore(violation, context),
239
+ }));
240
+ return scored.sort((a, b) => b.impact.score - a.impact.score);
241
+ }
242
+ /**
243
+ * Group violations by impact category
244
+ */
245
+ export function groupByImpact(violations, context = {}) {
246
+ const sorted = sortByImpact(violations, context);
247
+ const groups = {
248
+ critical: [],
249
+ high: [],
250
+ medium: [],
251
+ low: [],
252
+ };
253
+ for (const item of sorted) {
254
+ groups[item.impact.businessImpact].push(item);
255
+ }
256
+ return groups;
257
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Retry utility with exponential backoff for transient errors
3
+ */
4
+ /**
5
+ * Check if an error is transient (network/timeout related)
6
+ */
7
+ export declare function isTransientError(error: unknown): boolean;
8
+ export interface RetryOptions {
9
+ /** Maximum number of retry attempts (default: 3) */
10
+ maxRetries?: number;
11
+ /** Base delay in ms for exponential backoff (default: 1000) */
12
+ baseDelayMs?: number;
13
+ /** Callback for logging retry attempts */
14
+ onRetry?: (attempt: number, error: Error, delayMs: number) => void;
15
+ }
16
+ /**
17
+ * Execute a function with exponential backoff retry for transient errors
18
+ *
19
+ * @param fn The async function to execute
20
+ * @param options Retry configuration
21
+ * @returns The result of the function
22
+ * @throws The last error if all retries are exhausted, or immediately for non-transient errors
23
+ */
24
+ export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Retry utility with exponential backoff for transient errors
3
+ */
4
+ /** Errors that are considered transient and should be retried */
5
+ const TRANSIENT_ERROR_PATTERNS = [
6
+ 'ECONNRESET',
7
+ 'ECONNREFUSED',
8
+ 'ETIMEDOUT',
9
+ 'ESOCKETTIMEDOUT',
10
+ 'ENOTFOUND',
11
+ 'EAI_AGAIN',
12
+ 'net::ERR_',
13
+ 'Navigation timeout',
14
+ 'Timeout exceeded',
15
+ 'TimeoutError',
16
+ 'socket hang up',
17
+ 'read ECONNRESET',
18
+ ];
19
+ /**
20
+ * Check if an error is transient (network/timeout related)
21
+ */
22
+ export function isTransientError(error) {
23
+ if (!(error instanceof Error))
24
+ return false;
25
+ const message = error.message.toLowerCase();
26
+ const name = error.name.toLowerCase();
27
+ for (const pattern of TRANSIENT_ERROR_PATTERNS) {
28
+ if (message.includes(pattern.toLowerCase()) || name.includes(pattern.toLowerCase())) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ /**
35
+ * Execute a function with exponential backoff retry for transient errors
36
+ *
37
+ * @param fn The async function to execute
38
+ * @param options Retry configuration
39
+ * @returns The result of the function
40
+ * @throws The last error if all retries are exhausted, or immediately for non-transient errors
41
+ */
42
+ export async function withRetry(fn, options = {}) {
43
+ const { maxRetries = 3, baseDelayMs = 1000, onRetry, } = options;
44
+ let lastError;
45
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
46
+ try {
47
+ return await fn();
48
+ }
49
+ catch (error) {
50
+ const err = error instanceof Error ? error : new Error(String(error));
51
+ lastError = err;
52
+ // Don't retry non-transient errors
53
+ if (!isTransientError(err)) {
54
+ throw err;
55
+ }
56
+ // Don't retry if we've exhausted attempts
57
+ if (attempt >= maxRetries) {
58
+ throw err;
59
+ }
60
+ // Calculate delay with exponential backoff: 1s, 2s, 4s
61
+ const delayMs = baseDelayMs * Math.pow(2, attempt);
62
+ if (onRetry) {
63
+ onRetry(attempt + 1, err, delayMs);
64
+ }
65
+ await sleep(delayMs);
66
+ }
67
+ }
68
+ // This should never be reached, but TypeScript needs it
69
+ throw lastError ?? new Error('Retry failed');
70
+ }
71
+ /**
72
+ * Sleep for the specified duration
73
+ */
74
+ function sleep(ms) {
75
+ return new Promise(resolve => setTimeout(resolve, ms));
76
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Accessibility scanner using axe-core with Puppeteer or Playwright
3
+ */
4
+ import type { ScanResult, AllyReport, ReportSummary } from '../types/index.js';
5
+ import { type BrowserType } from './browser.js';
6
+ export type { BrowserType } from './browser.js';
7
+ export type WcagStandard = 'wcag2a' | 'wcag2aa' | 'wcag2aaa' | 'wcag21a' | 'wcag21aa' | 'wcag21aaa' | 'wcag22aa' | 'section508' | 'best-practice';
8
+ export declare const standardToTags: Record<WcagStandard, string[]>;
9
+ export declare const DEFAULT_STANDARD: WcagStandard;
10
+ export type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia';
11
+ /** Default page load timeout in milliseconds */
12
+ export declare const DEFAULT_TIMEOUT = 30000;
13
+ /** Default batch size for parallel scanning */
14
+ export declare const DEFAULT_BATCH_SIZE = 4;
15
+ /**
16
+ * Result from parallel scanning - includes file path for error tracking
17
+ */
18
+ export interface ParallelScanResult {
19
+ result?: ScanResult;
20
+ error?: {
21
+ path: string;
22
+ error: string;
23
+ };
24
+ }
25
+ /**
26
+ * Callback for progress updates during parallel scanning
27
+ */
28
+ export type ScanProgressCallback = (completed: number, total: number, currentFile?: string) => void;
29
+ export declare class AccessibilityScanner {
30
+ private browser;
31
+ private browserAdapter;
32
+ private timeout;
33
+ private browserType;
34
+ private usePlaywright;
35
+ constructor(timeout?: number, browserType?: BrowserType);
36
+ init(): Promise<void>;
37
+ close(): Promise<void>;
38
+ /**
39
+ * Get the browser type being used
40
+ */
41
+ getBrowserType(): BrowserType;
42
+ scanHtmlFile(filePath: string, standard?: WcagStandard): Promise<ScanResult>;
43
+ scanUrl(url: string, standard?: WcagStandard): Promise<ScanResult>;
44
+ scanHtmlString(html: string, identifier?: string, standard?: WcagStandard): Promise<ScanResult>;
45
+ /**
46
+ * Scan multiple HTML files in parallel batches for improved performance
47
+ * @param files Array of file paths to scan
48
+ * @param standard WCAG standard to use for scanning
49
+ * @param batchSize Number of files to scan concurrently (default: 4)
50
+ * @param onProgress Optional callback for progress updates
51
+ * @returns Array of scan results (successful) and errors (failed)
52
+ */
53
+ scanHtmlFilesParallel(files: string[], standard?: WcagStandard, batchSize?: number, onProgress?: ScanProgressCallback): Promise<{
54
+ results: ScanResult[];
55
+ errors: Array<{
56
+ path: string;
57
+ error: string;
58
+ }>;
59
+ }>;
60
+ /**
61
+ * Extract all anchor links from a URL
62
+ * This method provides type-safe access to browser pages for link extraction
63
+ */
64
+ extractLinks(url: string): Promise<string[]>;
65
+ /**
66
+ * Take a screenshot of a URL with color blindness simulation applied
67
+ */
68
+ simulateColorBlindness(url: string, type: ColorBlindnessType, outputPath: string): Promise<void>;
69
+ }
70
+ export declare function findHtmlFiles(targetPath: string, extraIgnore?: string[]): Promise<string[]>;
71
+ export declare function findComponentFiles(targetPath: string, extraIgnore?: string[]): Promise<string[]>;
72
+ export declare function calculateScore(results: ScanResult[]): number;
73
+ export declare function generateSummary(results: ScanResult[]): ReportSummary;
74
+ export declare function createReport(results: ScanResult[]): AllyReport;