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.
- package/ACCESSIBILITY.md +205 -0
- package/LICENSE +21 -0
- package/README.md +940 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +528 -0
- package/dist/commands/audit-palette.d.ts +18 -0
- package/dist/commands/audit-palette.js +613 -0
- package/dist/commands/auto-pr.d.ts +19 -0
- package/dist/commands/auto-pr.js +434 -0
- package/dist/commands/badge.d.ts +11 -0
- package/dist/commands/badge.js +143 -0
- package/dist/commands/completion.d.ts +4 -0
- package/dist/commands/completion.js +185 -0
- package/dist/commands/crawl.d.ts +12 -0
- package/dist/commands/crawl.js +249 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +233 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.js +233 -0
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.js +668 -0
- package/dist/commands/health.d.ts +11 -0
- package/dist/commands/health.js +367 -0
- package/dist/commands/history.d.ts +10 -0
- package/dist/commands/history.js +191 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/learn.d.ts +8 -0
- package/dist/commands/learn.js +592 -0
- package/dist/commands/pr-check.d.ts +12 -0
- package/dist/commands/pr-check.js +270 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +375 -0
- package/dist/commands/scan-storybook.d.ts +18 -0
- package/dist/commands/scan-storybook.js +402 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +673 -0
- package/dist/commands/stats.d.ts +5 -0
- package/dist/commands/stats.js +137 -0
- package/dist/commands/tree.d.ts +12 -0
- package/dist/commands/tree.js +635 -0
- package/dist/commands/triage.d.ts +13 -0
- package/dist/commands/triage.js +327 -0
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.js +302 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/baseline.d.ts +62 -0
- package/dist/utils/baseline.js +169 -0
- package/dist/utils/browser.d.ts +78 -0
- package/dist/utils/browser.js +239 -0
- package/dist/utils/cache.d.ts +76 -0
- package/dist/utils/cache.js +178 -0
- package/dist/utils/config.d.ts +102 -0
- package/dist/utils/config.js +237 -0
- package/dist/utils/converters.d.ts +77 -0
- package/dist/utils/converters.js +200 -0
- package/dist/utils/copilot.d.ts +36 -0
- package/dist/utils/copilot.js +139 -0
- package/dist/utils/detect.d.ts +22 -0
- package/dist/utils/detect.js +197 -0
- package/dist/utils/enhanced-errors.d.ts +46 -0
- package/dist/utils/enhanced-errors.js +295 -0
- package/dist/utils/errors.d.ts +31 -0
- package/dist/utils/errors.js +149 -0
- package/dist/utils/fix-patterns.d.ts +56 -0
- package/dist/utils/fix-patterns.js +529 -0
- package/dist/utils/history-tracking.d.ts +94 -0
- package/dist/utils/history-tracking.js +230 -0
- package/dist/utils/history.d.ts +42 -0
- package/dist/utils/history.js +255 -0
- package/dist/utils/impact-scores.d.ts +44 -0
- package/dist/utils/impact-scores.js +257 -0
- package/dist/utils/retry.d.ts +24 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/scanner.d.ts +74 -0
- package/dist/utils/scanner.js +606 -0
- package/dist/utils/scanner.test.d.ts +4 -0
- package/dist/utils/scanner.test.js +162 -0
- package/dist/utils/ui.d.ts +44 -0
- package/dist/utils/ui.js +276 -0
- package/mcp-server/dist/index.d.ts +8 -0
- package/mcp-server/dist/index.js +1923 -0
- package/package.json +88 -0
|
@@ -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 {};
|