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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching utilities for incremental scans
|
|
3
|
+
*
|
|
4
|
+
* Caches scan results by file hash to avoid re-scanning unchanged files.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { readFile, writeFile, mkdir, stat } from 'fs/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { resolve, dirname, relative } from 'path';
|
|
10
|
+
const CACHE_VERSION = 1;
|
|
11
|
+
const CACHE_DIR = '.ally/cache';
|
|
12
|
+
const CACHE_INDEX_FILE = '.ally/cache/index.json';
|
|
13
|
+
/**
|
|
14
|
+
* Compute SHA-256 hash of file content
|
|
15
|
+
*/
|
|
16
|
+
export async function hashFile(filePath) {
|
|
17
|
+
const content = await readFile(filePath);
|
|
18
|
+
return createHash('sha256').update(content).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compute hash of content string
|
|
22
|
+
*/
|
|
23
|
+
export function hashContent(content) {
|
|
24
|
+
return createHash('sha256').update(content).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load cache index from disk
|
|
28
|
+
*/
|
|
29
|
+
export async function loadCacheIndex() {
|
|
30
|
+
const indexPath = resolve(CACHE_INDEX_FILE);
|
|
31
|
+
if (!existsSync(indexPath)) {
|
|
32
|
+
return {
|
|
33
|
+
version: CACHE_VERSION,
|
|
34
|
+
entries: {},
|
|
35
|
+
updatedAt: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = await readFile(indexPath, 'utf-8');
|
|
40
|
+
const index = JSON.parse(content);
|
|
41
|
+
// Check version compatibility
|
|
42
|
+
if (index.version !== CACHE_VERSION) {
|
|
43
|
+
return {
|
|
44
|
+
version: CACHE_VERSION,
|
|
45
|
+
entries: {},
|
|
46
|
+
updatedAt: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return index;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {
|
|
53
|
+
version: CACHE_VERSION,
|
|
54
|
+
entries: {},
|
|
55
|
+
updatedAt: Date.now(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Save cache index to disk
|
|
61
|
+
*/
|
|
62
|
+
export async function saveCacheIndex(index) {
|
|
63
|
+
const indexPath = resolve(CACHE_INDEX_FILE);
|
|
64
|
+
const cacheDir = dirname(indexPath);
|
|
65
|
+
if (!existsSync(cacheDir)) {
|
|
66
|
+
await mkdir(cacheDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
index.updatedAt = Date.now();
|
|
69
|
+
await writeFile(indexPath, JSON.stringify(index, null, 2));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get cached result for a file if valid
|
|
73
|
+
*/
|
|
74
|
+
export async function getCachedResult(filePath, standard) {
|
|
75
|
+
const index = await loadCacheIndex();
|
|
76
|
+
const relativePath = relative(process.cwd(), filePath);
|
|
77
|
+
const entry = index.entries[relativePath];
|
|
78
|
+
if (!entry) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Check if standard matches
|
|
82
|
+
if (entry.standard !== standard) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
// Check if file has been modified
|
|
87
|
+
const stats = await stat(filePath);
|
|
88
|
+
const mtime = stats.mtimeMs;
|
|
89
|
+
if (mtime !== entry.mtime) {
|
|
90
|
+
// File modified, verify with hash
|
|
91
|
+
const hash = await hashFile(filePath);
|
|
92
|
+
if (hash !== entry.hash) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Hash matches, update mtime in cache
|
|
96
|
+
entry.mtime = mtime;
|
|
97
|
+
await saveCacheIndex(index);
|
|
98
|
+
}
|
|
99
|
+
return entry.result;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cache a scan result for a file
|
|
107
|
+
*/
|
|
108
|
+
export async function cacheResult(filePath, result, standard) {
|
|
109
|
+
const index = await loadCacheIndex();
|
|
110
|
+
const relativePath = relative(process.cwd(), filePath);
|
|
111
|
+
try {
|
|
112
|
+
const stats = await stat(filePath);
|
|
113
|
+
const hash = await hashFile(filePath);
|
|
114
|
+
index.entries[relativePath] = {
|
|
115
|
+
path: relativePath,
|
|
116
|
+
hash,
|
|
117
|
+
mtime: stats.mtimeMs,
|
|
118
|
+
result,
|
|
119
|
+
standard,
|
|
120
|
+
cachedAt: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
await saveCacheIndex(index);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Silently fail if caching fails
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clear all cached results
|
|
130
|
+
*/
|
|
131
|
+
export async function clearCache() {
|
|
132
|
+
const index = {
|
|
133
|
+
version: CACHE_VERSION,
|
|
134
|
+
entries: {},
|
|
135
|
+
updatedAt: Date.now(),
|
|
136
|
+
};
|
|
137
|
+
await saveCacheIndex(index);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get cache statistics
|
|
141
|
+
*/
|
|
142
|
+
export async function getCacheStats() {
|
|
143
|
+
const index = await loadCacheIndex();
|
|
144
|
+
const entries = Object.values(index.entries);
|
|
145
|
+
if (entries.length === 0) {
|
|
146
|
+
return {
|
|
147
|
+
entries: 0,
|
|
148
|
+
size: 0,
|
|
149
|
+
oldestEntry: null,
|
|
150
|
+
newestEntry: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const timestamps = entries.map((e) => e.cachedAt);
|
|
154
|
+
return {
|
|
155
|
+
entries: entries.length,
|
|
156
|
+
size: JSON.stringify(index).length,
|
|
157
|
+
oldestEntry: Math.min(...timestamps),
|
|
158
|
+
newestEntry: Math.max(...timestamps),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Remove stale cache entries (files that no longer exist)
|
|
163
|
+
*/
|
|
164
|
+
export async function pruneCache() {
|
|
165
|
+
const index = await loadCacheIndex();
|
|
166
|
+
let removed = 0;
|
|
167
|
+
for (const path of Object.keys(index.entries)) {
|
|
168
|
+
const fullPath = resolve(path);
|
|
169
|
+
if (!existsSync(fullPath)) {
|
|
170
|
+
delete index.entries[path];
|
|
171
|
+
removed++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (removed > 0) {
|
|
175
|
+
await saveCacheIndex(index);
|
|
176
|
+
}
|
|
177
|
+
return removed;
|
|
178
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file support for Ally A11y CLI
|
|
3
|
+
*
|
|
4
|
+
* Supports loading config from:
|
|
5
|
+
* - .allyrc.json
|
|
6
|
+
* - .allyrc (JSON format)
|
|
7
|
+
* - ally.config.js (CommonJS module)
|
|
8
|
+
*
|
|
9
|
+
* Searches current directory and parent directories (like eslint)
|
|
10
|
+
*/
|
|
11
|
+
import type { Severity } from '../types/index.js';
|
|
12
|
+
import type { WcagStandard } from './scanner.js';
|
|
13
|
+
/**
|
|
14
|
+
* Configuration options for the scan command
|
|
15
|
+
*/
|
|
16
|
+
export interface ScanConfig {
|
|
17
|
+
/** Paths to scan (relative to config file or cwd) */
|
|
18
|
+
paths?: string[];
|
|
19
|
+
/** Glob patterns to ignore */
|
|
20
|
+
ignore?: string[];
|
|
21
|
+
/** Violation threshold for CI (exit with error if exceeded) */
|
|
22
|
+
threshold?: number;
|
|
23
|
+
/** WCAG standard to test against */
|
|
24
|
+
standard?: WcagStandard;
|
|
25
|
+
/** Only fail on these severity levels */
|
|
26
|
+
failOn?: Severity[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configuration options for report generation
|
|
30
|
+
*/
|
|
31
|
+
export interface ReportConfig {
|
|
32
|
+
/** Output format: json, sarif, markdown, html */
|
|
33
|
+
format?: string;
|
|
34
|
+
/** Output directory or file path */
|
|
35
|
+
output?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Configuration options for the fix command
|
|
39
|
+
*/
|
|
40
|
+
export interface FixConfig {
|
|
41
|
+
/** Show what would be fixed without making changes */
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/** Auto-approve fixes for these violation types */
|
|
44
|
+
autoApprove?: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Complete Ally configuration
|
|
48
|
+
*/
|
|
49
|
+
export interface AllyConfig {
|
|
50
|
+
scan?: ScanConfig;
|
|
51
|
+
report?: ReportConfig;
|
|
52
|
+
fix?: FixConfig;
|
|
53
|
+
}
|
|
54
|
+
/** Result from loading config */
|
|
55
|
+
export interface ConfigResult {
|
|
56
|
+
config: AllyConfig;
|
|
57
|
+
configPath: string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load configuration from .allyrc.json or similar config files
|
|
61
|
+
*
|
|
62
|
+
* Searches current directory and parent directories for config files.
|
|
63
|
+
* Returns an empty config if no file is found.
|
|
64
|
+
*
|
|
65
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
66
|
+
* @returns Loaded configuration and path to config file (if found)
|
|
67
|
+
*/
|
|
68
|
+
export declare function loadConfig(startDir?: string): Promise<ConfigResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Get the path to the config file that would be used
|
|
71
|
+
*
|
|
72
|
+
* Useful for debugging to see which config file is being loaded.
|
|
73
|
+
*
|
|
74
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
75
|
+
* @returns Path to config file or null if none found
|
|
76
|
+
*/
|
|
77
|
+
export declare function getConfigPath(startDir?: string): Promise<string | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Merge CLI options with config file options
|
|
80
|
+
*
|
|
81
|
+
* CLI options take precedence over config file options.
|
|
82
|
+
*
|
|
83
|
+
* @param cliOptions - Options from command line
|
|
84
|
+
* @param config - Options from config file
|
|
85
|
+
* @returns Merged options
|
|
86
|
+
*/
|
|
87
|
+
export declare function mergeOptions<T extends Record<string, unknown>>(cliOptions: T, configOptions: Partial<T>): T;
|
|
88
|
+
/** Result from loading ignore patterns */
|
|
89
|
+
export interface IgnoreResult {
|
|
90
|
+
patterns: string[];
|
|
91
|
+
ignorePath: string | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Load ignore patterns from .allyignore file
|
|
95
|
+
*
|
|
96
|
+
* Searches current directory and parent directories for ignore file.
|
|
97
|
+
* Returns empty patterns array if no file is found.
|
|
98
|
+
*
|
|
99
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
100
|
+
* @returns Loaded ignore patterns and path to ignore file (if found)
|
|
101
|
+
*/
|
|
102
|
+
export declare function loadIgnorePatterns(startDir?: string): Promise<IgnoreResult>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file support for Ally A11y CLI
|
|
3
|
+
*
|
|
4
|
+
* Supports loading config from:
|
|
5
|
+
* - .allyrc.json
|
|
6
|
+
* - .allyrc (JSON format)
|
|
7
|
+
* - ally.config.js (CommonJS module)
|
|
8
|
+
*
|
|
9
|
+
* Searches current directory and parent directories (like eslint)
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { readFile } from 'fs/promises';
|
|
13
|
+
import { resolve, dirname, join } from 'path';
|
|
14
|
+
/** Ignore file names to search for (in order of priority) */
|
|
15
|
+
const IGNORE_FILES = ['.allyignore', '.ally-ignore'];
|
|
16
|
+
/** Config file names to search for (in order of priority) */
|
|
17
|
+
const CONFIG_FILES = [
|
|
18
|
+
'.allyrc.json',
|
|
19
|
+
'.allyrc',
|
|
20
|
+
'ally.config.js',
|
|
21
|
+
'ally.config.cjs',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Search for config file starting from directory and moving up to root
|
|
25
|
+
*/
|
|
26
|
+
async function findConfigFile(startDir) {
|
|
27
|
+
let currentDir = resolve(startDir);
|
|
28
|
+
const root = dirname(currentDir);
|
|
29
|
+
// Keep searching until we hit the root
|
|
30
|
+
while (currentDir !== root) {
|
|
31
|
+
for (const configFile of CONFIG_FILES) {
|
|
32
|
+
const configPath = join(currentDir, configFile);
|
|
33
|
+
if (existsSync(configPath)) {
|
|
34
|
+
return configPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
currentDir = dirname(currentDir);
|
|
38
|
+
}
|
|
39
|
+
// Check root as well
|
|
40
|
+
for (const configFile of CONFIG_FILES) {
|
|
41
|
+
const configPath = join(currentDir, configFile);
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
return configPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load and parse a config file
|
|
50
|
+
*/
|
|
51
|
+
async function parseConfigFile(configPath) {
|
|
52
|
+
const ext = configPath.split('.').pop();
|
|
53
|
+
if (ext === 'js' || ext === 'cjs') {
|
|
54
|
+
// Load JavaScript config (CommonJS)
|
|
55
|
+
try {
|
|
56
|
+
const configModule = await import(configPath);
|
|
57
|
+
return configModule.default || configModule;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new Error(`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Load JSON config
|
|
64
|
+
try {
|
|
65
|
+
const content = await readFile(configPath, 'utf-8');
|
|
66
|
+
return JSON.parse(content);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof SyntaxError) {
|
|
70
|
+
throw new Error(`Invalid JSON in config file ${configPath}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to read config file ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate config structure and values
|
|
77
|
+
*/
|
|
78
|
+
function validateConfig(config, configPath) {
|
|
79
|
+
const validStandards = [
|
|
80
|
+
'wcag2a', 'wcag2aa', 'wcag2aaa',
|
|
81
|
+
'wcag21a', 'wcag21aa', 'wcag21aaa',
|
|
82
|
+
'wcag22aa', 'section508', 'best-practice'
|
|
83
|
+
];
|
|
84
|
+
const validSeverities = ['critical', 'serious', 'moderate', 'minor'];
|
|
85
|
+
if (config.scan?.standard && !validStandards.includes(config.scan.standard)) {
|
|
86
|
+
throw new Error(`Invalid standard "${config.scan.standard}" in ${configPath}. ` +
|
|
87
|
+
`Valid options: ${validStandards.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (config.scan?.failOn) {
|
|
90
|
+
for (const severity of config.scan.failOn) {
|
|
91
|
+
if (!validSeverities.includes(severity)) {
|
|
92
|
+
throw new Error(`Invalid severity "${severity}" in failOn in ${configPath}. ` +
|
|
93
|
+
`Valid options: ${validSeverities.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (config.scan?.threshold !== undefined) {
|
|
98
|
+
if (typeof config.scan.threshold !== 'number' || config.scan.threshold < 0) {
|
|
99
|
+
throw new Error(`Invalid threshold "${config.scan.threshold}" in ${configPath}. ` +
|
|
100
|
+
`Must be a non-negative number.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const validFormats = ['json', 'sarif', 'markdown', 'html'];
|
|
104
|
+
if (config.report?.format && !validFormats.includes(config.report.format)) {
|
|
105
|
+
throw new Error(`Invalid report format "${config.report.format}" in ${configPath}. ` +
|
|
106
|
+
`Valid options: ${validFormats.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Load configuration from .allyrc.json or similar config files
|
|
111
|
+
*
|
|
112
|
+
* Searches current directory and parent directories for config files.
|
|
113
|
+
* Returns an empty config if no file is found.
|
|
114
|
+
*
|
|
115
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
116
|
+
* @returns Loaded configuration and path to config file (if found)
|
|
117
|
+
*/
|
|
118
|
+
export async function loadConfig(startDir = process.cwd()) {
|
|
119
|
+
try {
|
|
120
|
+
const configPath = await findConfigFile(startDir);
|
|
121
|
+
if (!configPath) {
|
|
122
|
+
return { config: {}, configPath: null };
|
|
123
|
+
}
|
|
124
|
+
const config = await parseConfigFile(configPath);
|
|
125
|
+
validateConfig(config, configPath);
|
|
126
|
+
return { config, configPath };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// Re-throw validation/parsing errors
|
|
130
|
+
if (error instanceof Error && error.message.includes('config')) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
// Wrap other errors
|
|
134
|
+
throw new Error(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the path to the config file that would be used
|
|
139
|
+
*
|
|
140
|
+
* Useful for debugging to see which config file is being loaded.
|
|
141
|
+
*
|
|
142
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
143
|
+
* @returns Path to config file or null if none found
|
|
144
|
+
*/
|
|
145
|
+
export async function getConfigPath(startDir = process.cwd()) {
|
|
146
|
+
return findConfigFile(startDir);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Merge CLI options with config file options
|
|
150
|
+
*
|
|
151
|
+
* CLI options take precedence over config file options.
|
|
152
|
+
*
|
|
153
|
+
* @param cliOptions - Options from command line
|
|
154
|
+
* @param config - Options from config file
|
|
155
|
+
* @returns Merged options
|
|
156
|
+
*/
|
|
157
|
+
export function mergeOptions(cliOptions, configOptions) {
|
|
158
|
+
const merged = { ...configOptions };
|
|
159
|
+
// CLI options override config options (only if explicitly set)
|
|
160
|
+
for (const [key, value] of Object.entries(cliOptions)) {
|
|
161
|
+
if (value !== undefined) {
|
|
162
|
+
merged[key] = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Search for .allyignore file starting from directory and moving up to root
|
|
169
|
+
*/
|
|
170
|
+
async function findIgnoreFile(startDir) {
|
|
171
|
+
let currentDir = resolve(startDir);
|
|
172
|
+
const root = dirname(currentDir);
|
|
173
|
+
while (currentDir !== root) {
|
|
174
|
+
for (const ignoreFile of IGNORE_FILES) {
|
|
175
|
+
const ignorePath = join(currentDir, ignoreFile);
|
|
176
|
+
if (existsSync(ignorePath)) {
|
|
177
|
+
return ignorePath;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
currentDir = dirname(currentDir);
|
|
181
|
+
}
|
|
182
|
+
// Check root as well
|
|
183
|
+
for (const ignoreFile of IGNORE_FILES) {
|
|
184
|
+
const ignorePath = join(currentDir, ignoreFile);
|
|
185
|
+
if (existsSync(ignorePath)) {
|
|
186
|
+
return ignorePath;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Parse ignore file content into glob patterns
|
|
193
|
+
*
|
|
194
|
+
* Supports glob patterns, directory patterns (ending with /),
|
|
195
|
+
* comments (lines starting with #), and blank lines.
|
|
196
|
+
*/
|
|
197
|
+
function parseIgnoreFile(content) {
|
|
198
|
+
return content
|
|
199
|
+
.split('\n')
|
|
200
|
+
.map((line) => line.trim())
|
|
201
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
202
|
+
.map((pattern) => {
|
|
203
|
+
// Convert directory patterns to glob patterns
|
|
204
|
+
if (pattern.endsWith('/')) {
|
|
205
|
+
return `**/${pattern}**`;
|
|
206
|
+
}
|
|
207
|
+
// Ensure patterns without slashes match anywhere
|
|
208
|
+
if (!pattern.includes('/') && !pattern.startsWith('**/')) {
|
|
209
|
+
return `**/${pattern}`;
|
|
210
|
+
}
|
|
211
|
+
return pattern;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Load ignore patterns from .allyignore file
|
|
216
|
+
*
|
|
217
|
+
* Searches current directory and parent directories for ignore file.
|
|
218
|
+
* Returns empty patterns array if no file is found.
|
|
219
|
+
*
|
|
220
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
221
|
+
* @returns Loaded ignore patterns and path to ignore file (if found)
|
|
222
|
+
*/
|
|
223
|
+
export async function loadIgnorePatterns(startDir = process.cwd()) {
|
|
224
|
+
const ignorePath = await findIgnoreFile(startDir);
|
|
225
|
+
if (!ignorePath) {
|
|
226
|
+
return { patterns: [], ignorePath: null };
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const content = await readFile(ignorePath, 'utf-8');
|
|
230
|
+
const patterns = parseIgnoreFile(content);
|
|
231
|
+
return { patterns, ignorePath };
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// If we can't read the file, return empty patterns
|
|
235
|
+
return { patterns: [], ignorePath: null };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format converters for accessibility reports
|
|
3
|
+
* Converts AllyReport to various output formats (SARIF, JUnit, CSV)
|
|
4
|
+
*/
|
|
5
|
+
import type { AllyReport } from '../types/index.js';
|
|
6
|
+
export interface SarifReport {
|
|
7
|
+
$schema: string;
|
|
8
|
+
version: string;
|
|
9
|
+
runs: SarifRun[];
|
|
10
|
+
}
|
|
11
|
+
interface SarifRun {
|
|
12
|
+
tool: {
|
|
13
|
+
driver: {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
informationUri: string;
|
|
17
|
+
rules: SarifRule[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
results: SarifResult[];
|
|
21
|
+
}
|
|
22
|
+
interface SarifRule {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
shortDescription: {
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
fullDescription: {
|
|
29
|
+
text: string;
|
|
30
|
+
};
|
|
31
|
+
helpUri: string;
|
|
32
|
+
defaultConfiguration: {
|
|
33
|
+
level: 'error' | 'warning' | 'note';
|
|
34
|
+
};
|
|
35
|
+
properties?: {
|
|
36
|
+
tags?: string[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
interface SarifResult {
|
|
40
|
+
ruleId: string;
|
|
41
|
+
ruleIndex: number;
|
|
42
|
+
level: 'error' | 'warning' | 'note';
|
|
43
|
+
message: {
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
locations: SarifLocation[];
|
|
47
|
+
}
|
|
48
|
+
interface SarifLocation {
|
|
49
|
+
physicalLocation: {
|
|
50
|
+
artifactLocation: {
|
|
51
|
+
uri: string;
|
|
52
|
+
uriBaseId?: string;
|
|
53
|
+
};
|
|
54
|
+
region?: {
|
|
55
|
+
startLine?: number;
|
|
56
|
+
startColumn?: number;
|
|
57
|
+
snippet?: {
|
|
58
|
+
text: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert AllyReport to SARIF 2.1.0 format for GitHub Code Scanning integration
|
|
65
|
+
*/
|
|
66
|
+
export declare function convertToSarif(report: AllyReport): SarifReport;
|
|
67
|
+
/**
|
|
68
|
+
* Convert AllyReport to JUnit XML format for CI/CD tools (Jenkins, GitLab, etc.)
|
|
69
|
+
* Each violation is represented as a test failure
|
|
70
|
+
*/
|
|
71
|
+
export declare function convertToJunit(report: AllyReport): string;
|
|
72
|
+
/**
|
|
73
|
+
* Convert AllyReport to CSV format
|
|
74
|
+
* Headers: file,violation_id,impact,description,selector,wcag,help_url
|
|
75
|
+
*/
|
|
76
|
+
export declare function convertToCsv(report: AllyReport): string;
|
|
77
|
+
export {};
|