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,4 @@
1
+ /**
2
+ * Core types for Ally accessibility scanner
3
+ */
4
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Baseline management for tracking accessibility improvements and detecting regressions
3
+ */
4
+ import type { AllyReport } from '../types/index.js';
5
+ interface BaselineData {
6
+ timestamp: string;
7
+ scores: Record<string, number>;
8
+ violations: Record<string, number>;
9
+ summary: {
10
+ totalViolations: number;
11
+ criticalCount: number;
12
+ seriousCount: number;
13
+ moderateCount: number;
14
+ minorCount: number;
15
+ };
16
+ }
17
+ interface RegressionAnalysis {
18
+ improved: number;
19
+ regressed: number;
20
+ unchanged: number;
21
+ improvementPercentage: number;
22
+ regressionPercentage: number;
23
+ details: {
24
+ newViolations: Array<{
25
+ file: string;
26
+ count: number;
27
+ }>;
28
+ fixedViolations: Array<{
29
+ file: string;
30
+ count: number;
31
+ }>;
32
+ };
33
+ }
34
+ /**
35
+ * Convert report to baseline data for comparison
36
+ */
37
+ export declare function reportToBaseline(report: AllyReport): BaselineData;
38
+ /**
39
+ * Save baseline from a report
40
+ */
41
+ export declare function saveBaseline(report: AllyReport, baselineDir?: string): Promise<void>;
42
+ /**
43
+ * Load baseline data
44
+ */
45
+ export declare function loadBaseline(baselineDir?: string): Promise<BaselineData | null>;
46
+ /**
47
+ * Compare current report against baseline, detecting improvements and regressions
48
+ */
49
+ export declare function compareWithBaseline(currentReport: AllyReport, baseline: BaselineData): RegressionAnalysis;
50
+ /**
51
+ * Check if baseline exists
52
+ */
53
+ export declare function hasBaseline(baselineDir?: string): Promise<boolean>;
54
+ /**
55
+ * Delete baseline
56
+ */
57
+ export declare function deleteBaseline(baselineDir?: string): Promise<void>;
58
+ /**
59
+ * Format regression analysis for display
60
+ */
61
+ export declare function formatRegression(analysis: RegressionAnalysis): string;
62
+ export {};
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Baseline management for tracking accessibility improvements and detecting regressions
3
+ */
4
+ import { readFile, writeFile, mkdir } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ const BASELINE_DIR = '.ally';
8
+ const BASELINE_FILE = resolve(BASELINE_DIR, 'baseline.json');
9
+ /**
10
+ * Convert report to baseline data for comparison
11
+ */
12
+ export function reportToBaseline(report) {
13
+ const scores = {};
14
+ const violations = {};
15
+ for (const result of report.results) {
16
+ const fileKey = result.file || result.url || 'unknown';
17
+ violations[fileKey] = result.violations.length;
18
+ // Score based on violations (100 - violations * 10)
19
+ scores[fileKey] = Math.max(0, 100 - result.violations.length * 10);
20
+ }
21
+ return {
22
+ timestamp: new Date().toISOString(),
23
+ scores,
24
+ violations,
25
+ summary: {
26
+ totalViolations: report.summary.totalViolations,
27
+ criticalCount: report.summary.bySeverity.critical ?? 0,
28
+ seriousCount: report.summary.bySeverity.serious ?? 0,
29
+ moderateCount: report.summary.bySeverity.moderate ?? 0,
30
+ minorCount: report.summary.bySeverity.minor ?? 0,
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * Save baseline from a report
36
+ */
37
+ export async function saveBaseline(report, baselineDir = BASELINE_DIR) {
38
+ const baseline = reportToBaseline(report);
39
+ const filePath = resolve(baselineDir, 'baseline.json');
40
+ // Ensure directory exists
41
+ if (!existsSync(baselineDir)) {
42
+ await mkdir(baselineDir, { recursive: true });
43
+ }
44
+ await writeFile(filePath, JSON.stringify(baseline, null, 2));
45
+ }
46
+ /**
47
+ * Load baseline data
48
+ */
49
+ export async function loadBaseline(baselineDir = BASELINE_DIR) {
50
+ const filePath = resolve(baselineDir, 'baseline.json');
51
+ try {
52
+ if (!existsSync(filePath)) {
53
+ return null;
54
+ }
55
+ const data = await readFile(filePath, 'utf-8');
56
+ return JSON.parse(data);
57
+ }
58
+ catch (error) {
59
+ console.error('Failed to load baseline:', error);
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Compare current report against baseline, detecting improvements and regressions
65
+ */
66
+ export function compareWithBaseline(currentReport, baseline) {
67
+ const currentBaseline = reportToBaseline(currentReport);
68
+ let improved = 0;
69
+ let regressed = 0;
70
+ let unchanged = 0;
71
+ const newViolations = [];
72
+ const fixedViolations = [];
73
+ // Check each file in current baseline
74
+ for (const [file, currentViolationCount] of Object.entries(currentBaseline.violations)) {
75
+ const previousViolationCount = baseline.violations[file] ?? 0;
76
+ if (currentViolationCount < previousViolationCount) {
77
+ improved++;
78
+ const fixed = previousViolationCount - currentViolationCount;
79
+ fixedViolations.push({ file, count: fixed });
80
+ }
81
+ else if (currentViolationCount > previousViolationCount) {
82
+ regressed++;
83
+ const added = currentViolationCount - previousViolationCount;
84
+ newViolations.push({ file, count: added });
85
+ }
86
+ else {
87
+ unchanged++;
88
+ }
89
+ }
90
+ // Check for newly scanned files (files not in baseline)
91
+ for (const [file, currentViolationCount] of Object.entries(currentBaseline.violations)) {
92
+ if (!(file in baseline.violations)) {
93
+ regressed++;
94
+ newViolations.push({ file, count: currentViolationCount });
95
+ }
96
+ }
97
+ const totalFiles = Object.keys(currentBaseline.violations).length;
98
+ const improvementPercentage = totalFiles > 0 ? (improved / totalFiles) * 100 : 0;
99
+ const regressionPercentage = totalFiles > 0 ? (regressed / totalFiles) * 100 : 0;
100
+ return {
101
+ improved,
102
+ regressed,
103
+ unchanged,
104
+ improvementPercentage,
105
+ regressionPercentage,
106
+ details: {
107
+ newViolations,
108
+ fixedViolations,
109
+ },
110
+ };
111
+ }
112
+ /**
113
+ * Check if baseline exists
114
+ */
115
+ export async function hasBaseline(baselineDir = BASELINE_DIR) {
116
+ const filePath = resolve(baselineDir, 'baseline.json');
117
+ return existsSync(filePath);
118
+ }
119
+ /**
120
+ * Delete baseline
121
+ */
122
+ export async function deleteBaseline(baselineDir = BASELINE_DIR) {
123
+ const filePath = resolve(baselineDir, 'baseline.json');
124
+ if (existsSync(filePath)) {
125
+ // Can't use unlink here without importing fs, so we'll just overwrite with empty
126
+ // This is a placeholder - actual delete would be in the delete command
127
+ console.log(`Baseline can be deleted by removing ${filePath}`);
128
+ }
129
+ }
130
+ /**
131
+ * Format regression analysis for display
132
+ */
133
+ export function formatRegression(analysis) {
134
+ const lines = [];
135
+ lines.push('');
136
+ lines.push('📊 Regression Analysis');
137
+ lines.push('─'.repeat(50));
138
+ if (analysis.improved > 0) {
139
+ lines.push(`✅ Fixed: ${analysis.improved} file${analysis.improved === 1 ? '' : 's'} (${analysis.improvementPercentage.toFixed(1)}%)`);
140
+ }
141
+ if (analysis.regressed > 0) {
142
+ lines.push(`⚠️ Regressed: ${analysis.regressed} file${analysis.regressed === 1 ? '' : 's'} (${analysis.regressionPercentage.toFixed(1)}%)`);
143
+ }
144
+ if (analysis.unchanged > 0) {
145
+ lines.push(`➖ Unchanged: ${analysis.unchanged} file${analysis.unchanged === 1 ? '' : 's'}`);
146
+ }
147
+ if (analysis.details.fixedViolations.length > 0) {
148
+ lines.push('');
149
+ lines.push('✨ Issues Fixed:');
150
+ for (const { file, count } of analysis.details.fixedViolations.slice(0, 5)) {
151
+ lines.push(` ${file}: -${count} issue${count === 1 ? '' : 's'}`);
152
+ }
153
+ if (analysis.details.fixedViolations.length > 5) {
154
+ lines.push(` ... and ${analysis.details.fixedViolations.length - 5} more`);
155
+ }
156
+ }
157
+ if (analysis.details.newViolations.length > 0) {
158
+ lines.push('');
159
+ lines.push('⚠️ New Issues:');
160
+ for (const { file, count } of analysis.details.newViolations.slice(0, 5)) {
161
+ lines.push(` ${file}: +${count} issue${count === 1 ? '' : 's'}`);
162
+ }
163
+ if (analysis.details.newViolations.length > 5) {
164
+ lines.push(` ... and ${analysis.details.newViolations.length - 5} more`);
165
+ }
166
+ }
167
+ lines.push('');
168
+ return lines.join('\n');
169
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Browser abstraction layer for cross-browser accessibility testing
3
+ *
4
+ * Supports Puppeteer (default, always available) and Playwright (optional, for Firefox/WebKit)
5
+ */
6
+ export type BrowserType = 'chromium' | 'firefox' | 'webkit';
7
+ /**
8
+ * Abstract page interface for browser-agnostic operations
9
+ */
10
+ export interface PageAdapter {
11
+ goto(url: string, options?: {
12
+ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
13
+ timeout?: number;
14
+ }): Promise<void>;
15
+ setContent(html: string, options?: {
16
+ waitUntil?: 'load' | 'domcontentloaded';
17
+ }): Promise<void>;
18
+ content(): Promise<string>;
19
+ waitForSelector(selector: string, options?: {
20
+ timeout?: number;
21
+ }): Promise<void>;
22
+ evaluate<T>(fn: () => T): Promise<T>;
23
+ addStyleTag(options: {
24
+ content: string;
25
+ }): Promise<void>;
26
+ setViewport(viewport: {
27
+ width: number;
28
+ height: number;
29
+ }): Promise<void>;
30
+ screenshot(options: {
31
+ path: string;
32
+ fullPage?: boolean;
33
+ }): Promise<void>;
34
+ close(): Promise<void>;
35
+ /**
36
+ * Get the underlying page object for axe-core integration
37
+ * Returns the Puppeteer Page or Playwright Page instance
38
+ */
39
+ getUnderlyingPage(): unknown;
40
+ /**
41
+ * Get the adapter type for conditional axe-core setup
42
+ */
43
+ getAdapterType(): 'puppeteer' | 'playwright';
44
+ }
45
+ /**
46
+ * Abstract browser interface
47
+ */
48
+ export interface BrowserAdapter {
49
+ launch(): Promise<void>;
50
+ newPage(): Promise<PageAdapter>;
51
+ close(): Promise<void>;
52
+ getBrowserType(): BrowserType;
53
+ }
54
+ /**
55
+ * Check if Playwright is installed
56
+ */
57
+ export declare function isPlaywrightInstalled(): Promise<boolean>;
58
+ /**
59
+ * Error thrown when Playwright is requested but not installed
60
+ */
61
+ export declare class PlaywrightNotInstalledError extends Error {
62
+ constructor(browser: BrowserType);
63
+ }
64
+ /**
65
+ * Create a browser adapter for the specified browser type
66
+ *
67
+ * @param type Browser type: 'chromium', 'firefox', or 'webkit'
68
+ * @returns BrowserAdapter instance
69
+ *
70
+ * Note: chromium uses Puppeteer by default (always available).
71
+ * Firefox and WebKit require Playwright to be installed.
72
+ * Use --browser chromium-playwright to force Playwright for Chromium.
73
+ */
74
+ export declare function createBrowser(type?: BrowserType | 'chromium-playwright'): BrowserAdapter;
75
+ /**
76
+ * Validate browser type option
77
+ */
78
+ export declare function validateBrowserType(value: string): BrowserType;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Browser abstraction layer for cross-browser accessibility testing
3
+ *
4
+ * Supports Puppeteer (default, always available) and Playwright (optional, for Firefox/WebKit)
5
+ */
6
+ /**
7
+ * Dynamic import helper that works with optional dependencies
8
+ * Uses eval to avoid TypeScript module resolution errors
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
11
+ const dynamicImport = new Function('modulePath', 'return import(modulePath)');
12
+ /**
13
+ * Check if Playwright is installed
14
+ */
15
+ export async function isPlaywrightInstalled() {
16
+ try {
17
+ await dynamicImport('playwright');
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ /**
25
+ * Error thrown when Playwright is requested but not installed
26
+ */
27
+ export class PlaywrightNotInstalledError extends Error {
28
+ constructor(browser) {
29
+ super(`Playwright is required for ${browser} browser but is not installed.\n\n` +
30
+ `To install Playwright, run:\n` +
31
+ ` npm install playwright\n\n` +
32
+ `Then install browser binaries:\n` +
33
+ ` npx playwright install ${browser}\n\n` +
34
+ `Or use the default Chromium browser (no additional installation required):\n` +
35
+ ` ally scan --browser chromium`);
36
+ this.name = 'PlaywrightNotInstalledError';
37
+ }
38
+ }
39
+ /**
40
+ * Puppeteer adapter - wraps Puppeteer for chromium support
41
+ */
42
+ class PuppeteerPageAdapter {
43
+ page;
44
+ constructor(page) {
45
+ this.page = page;
46
+ }
47
+ async goto(url, options) {
48
+ // Map our waitUntil to Puppeteer's options
49
+ const waitUntil = options?.waitUntil === 'networkidle' ? 'networkidle2' : options?.waitUntil || 'load';
50
+ await this.page.goto(url, { waitUntil, timeout: options?.timeout });
51
+ }
52
+ async setContent(html, options) {
53
+ await this.page.setContent(html, { waitUntil: options?.waitUntil || 'domcontentloaded' });
54
+ }
55
+ async content() {
56
+ return this.page.content();
57
+ }
58
+ async waitForSelector(selector, options) {
59
+ await this.page.waitForSelector(selector, options);
60
+ }
61
+ async evaluate(fn) {
62
+ return this.page.evaluate(fn);
63
+ }
64
+ async addStyleTag(options) {
65
+ await this.page.addStyleTag(options);
66
+ }
67
+ async setViewport(viewport) {
68
+ await this.page.setViewport(viewport);
69
+ }
70
+ async screenshot(options) {
71
+ await this.page.screenshot(options);
72
+ }
73
+ async close() {
74
+ await this.page.close();
75
+ }
76
+ getUnderlyingPage() {
77
+ return this.page;
78
+ }
79
+ getAdapterType() {
80
+ return 'puppeteer';
81
+ }
82
+ }
83
+ class PuppeteerBrowserAdapter {
84
+ browser = null;
85
+ async launch() {
86
+ // Dynamic import to avoid loading at startup
87
+ const puppeteer = await import('puppeteer');
88
+ this.browser = await puppeteer.default.launch({
89
+ headless: true,
90
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
91
+ });
92
+ }
93
+ async newPage() {
94
+ if (!this.browser) {
95
+ throw new Error('Browser not launched. Call launch() first.');
96
+ }
97
+ const page = await this.browser.newPage();
98
+ return new PuppeteerPageAdapter(page);
99
+ }
100
+ async close() {
101
+ if (this.browser) {
102
+ await this.browser.close();
103
+ this.browser = null;
104
+ }
105
+ }
106
+ getBrowserType() {
107
+ return 'chromium';
108
+ }
109
+ }
110
+ /**
111
+ * Playwright adapter - supports chromium, firefox, and webkit
112
+ */
113
+ class PlaywrightPageAdapter {
114
+ page;
115
+ constructor(page) {
116
+ this.page = page;
117
+ }
118
+ async goto(url, options) {
119
+ // Playwright uses 'networkidle' directly
120
+ await this.page.goto(url, {
121
+ waitUntil: options?.waitUntil,
122
+ timeout: options?.timeout,
123
+ });
124
+ }
125
+ async setContent(html, options) {
126
+ await this.page.setContent(html, { waitUntil: options?.waitUntil || 'domcontentloaded' });
127
+ }
128
+ async content() {
129
+ return this.page.content();
130
+ }
131
+ async waitForSelector(selector, options) {
132
+ await this.page.waitForSelector(selector, options ? { timeout: options.timeout } : undefined);
133
+ }
134
+ async evaluate(fn) {
135
+ return this.page.evaluate(fn);
136
+ }
137
+ async addStyleTag(options) {
138
+ await this.page.addStyleTag(options);
139
+ }
140
+ async setViewport(viewport) {
141
+ await this.page.setViewportSize(viewport);
142
+ }
143
+ async screenshot(options) {
144
+ await this.page.screenshot(options);
145
+ }
146
+ async close() {
147
+ await this.page.close();
148
+ }
149
+ getUnderlyingPage() {
150
+ return this.page;
151
+ }
152
+ getAdapterType() {
153
+ return 'playwright';
154
+ }
155
+ }
156
+ class PlaywrightBrowserAdapter {
157
+ browser = null;
158
+ browserType;
159
+ constructor(browserType) {
160
+ this.browserType = browserType;
161
+ }
162
+ async launch() {
163
+ // Dynamic import to avoid loading at startup
164
+ let playwright;
165
+ try {
166
+ playwright = await dynamicImport('playwright');
167
+ }
168
+ catch {
169
+ throw new PlaywrightNotInstalledError(this.browserType);
170
+ }
171
+ // Select the appropriate browser engine
172
+ const browserLauncher = playwright[this.browserType];
173
+ if (!browserLauncher) {
174
+ throw new Error(`Unknown browser type: ${this.browserType}`);
175
+ }
176
+ try {
177
+ this.browser = await browserLauncher.launch({
178
+ headless: true,
179
+ });
180
+ }
181
+ catch (error) {
182
+ // Check if the error is about missing browser binaries
183
+ const errorMessage = error instanceof Error ? error.message : String(error);
184
+ if (errorMessage.includes('Executable doesn\'t exist') || errorMessage.includes('browserType.launch')) {
185
+ throw new Error(`${this.browserType} browser binaries are not installed.\n\n` +
186
+ `To install them, run:\n` +
187
+ ` npx playwright install ${this.browserType}\n\n` +
188
+ `Or install all browsers:\n` +
189
+ ` npx playwright install`);
190
+ }
191
+ throw error;
192
+ }
193
+ }
194
+ async newPage() {
195
+ if (!this.browser) {
196
+ throw new Error('Browser not launched. Call launch() first.');
197
+ }
198
+ const page = await this.browser.newPage();
199
+ return new PlaywrightPageAdapter(page);
200
+ }
201
+ async close() {
202
+ if (this.browser) {
203
+ await this.browser.close();
204
+ this.browser = null;
205
+ }
206
+ }
207
+ getBrowserType() {
208
+ return this.browserType;
209
+ }
210
+ }
211
+ /**
212
+ * Create a browser adapter for the specified browser type
213
+ *
214
+ * @param type Browser type: 'chromium', 'firefox', or 'webkit'
215
+ * @returns BrowserAdapter instance
216
+ *
217
+ * Note: chromium uses Puppeteer by default (always available).
218
+ * Firefox and WebKit require Playwright to be installed.
219
+ * Use --browser chromium-playwright to force Playwright for Chromium.
220
+ */
221
+ export function createBrowser(type = 'chromium') {
222
+ // For chromium, use Puppeteer by default (lighter weight, always available)
223
+ if (type === 'chromium') {
224
+ return new PuppeteerBrowserAdapter();
225
+ }
226
+ // For chromium-playwright, firefox, or webkit, use Playwright
227
+ const playwrightType = type === 'chromium-playwright' ? 'chromium' : type;
228
+ return new PlaywrightBrowserAdapter(playwrightType);
229
+ }
230
+ /**
231
+ * Validate browser type option
232
+ */
233
+ export function validateBrowserType(value) {
234
+ const valid = ['chromium', 'firefox', 'webkit'];
235
+ if (!valid.includes(value)) {
236
+ throw new Error(`Invalid browser: ${value}. Valid options: ${valid.join(', ')}`);
237
+ }
238
+ return value;
239
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Caching utilities for incremental scans
3
+ *
4
+ * Caches scan results by file hash to avoid re-scanning unchanged files.
5
+ */
6
+ import type { ScanResult } from '../types/index.js';
7
+ /**
8
+ * Cache index entry for a single file
9
+ */
10
+ interface CacheEntry {
11
+ /** File path (relative to project root) */
12
+ path: string;
13
+ /** Content hash */
14
+ hash: string;
15
+ /** Last modified timestamp */
16
+ mtime: number;
17
+ /** Scan result */
18
+ result: ScanResult;
19
+ /** WCAG standard used for the scan */
20
+ standard: string;
21
+ /** Cache creation timestamp */
22
+ cachedAt: number;
23
+ }
24
+ /**
25
+ * Cache index structure
26
+ */
27
+ interface CacheIndex {
28
+ /** Cache format version */
29
+ version: number;
30
+ /** Map of file path to cache entry */
31
+ entries: Record<string, CacheEntry>;
32
+ /** Last updated timestamp */
33
+ updatedAt: number;
34
+ }
35
+ /**
36
+ * Compute SHA-256 hash of file content
37
+ */
38
+ export declare function hashFile(filePath: string): Promise<string>;
39
+ /**
40
+ * Compute hash of content string
41
+ */
42
+ export declare function hashContent(content: string): string;
43
+ /**
44
+ * Load cache index from disk
45
+ */
46
+ export declare function loadCacheIndex(): Promise<CacheIndex>;
47
+ /**
48
+ * Save cache index to disk
49
+ */
50
+ export declare function saveCacheIndex(index: CacheIndex): Promise<void>;
51
+ /**
52
+ * Get cached result for a file if valid
53
+ */
54
+ export declare function getCachedResult(filePath: string, standard: string): Promise<ScanResult | null>;
55
+ /**
56
+ * Cache a scan result for a file
57
+ */
58
+ export declare function cacheResult(filePath: string, result: ScanResult, standard: string): Promise<void>;
59
+ /**
60
+ * Clear all cached results
61
+ */
62
+ export declare function clearCache(): Promise<void>;
63
+ /**
64
+ * Get cache statistics
65
+ */
66
+ export declare function getCacheStats(): Promise<{
67
+ entries: number;
68
+ size: number;
69
+ oldestEntry: number | null;
70
+ newestEntry: number | null;
71
+ }>;
72
+ /**
73
+ * Remove stale cache entries (files that no longer exist)
74
+ */
75
+ export declare function pruneCache(): Promise<number>;
76
+ export {};