wcag-a11y 0.3.5 → 0.3.6
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/README.md +14 -2
- package/dist/cli.js +11 -2
- package/dist/fixer.js +76 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,11 +158,15 @@ wcag-a11y fix -u http://localhost:3000 --apply
|
|
|
158
158
|
|
|
159
159
|
# Auto-discover pages + write fixes
|
|
160
160
|
wcag-a11y fix -u http://localhost:3000 --crawl --apply
|
|
161
|
+
|
|
162
|
+
# Skip rescanning — load violations from an existing report
|
|
163
|
+
wcag-a11y fix --from-report
|
|
164
|
+
wcag-a11y fix --from-report ./reports/a11y-report.md --apply
|
|
161
165
|
```
|
|
162
166
|
|
|
163
167
|
**How it works:**
|
|
164
168
|
|
|
165
|
-
1. Runs the same scan as `wcag-a11y scan`
|
|
169
|
+
1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
|
|
166
170
|
2. For each violation, locates the source file — checks `violation.source` (React dev mode) first, then falls back to grepping `./src` for unique identifiers in the HTML snippet (`id=`, `name=`, `for=`, local `src=`, text content)
|
|
167
171
|
3. Groups violations by file (multiple violations in the same file → one AI call)
|
|
168
172
|
4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
|
|
@@ -186,14 +190,22 @@ src/components/Navbar.jsx — 2 violation(s)
|
|
|
186
190
|
|
|
187
191
|
| Flag | Default | Description |
|
|
188
192
|
|---|---|---|
|
|
189
|
-
| `-u, --url <url>` |
|
|
193
|
+
| `-u, --url <url>` | — | Base URL of your running dev server. Required unless `--from-report` is used |
|
|
190
194
|
| `-p, --pages <pages...>` | `/` | Specific pages to scan |
|
|
191
195
|
| `-c, --crawl` | off | Auto-discover pages by following same-origin links |
|
|
196
|
+
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of scanning. Useful when you already ran `scan --report` and just want to apply fixes |
|
|
192
197
|
| `--apply` | off | Write patched files to disk (dry-run without this flag) |
|
|
193
198
|
| `--provider <name>` | from config | Override AI provider for this run: `gemini`, `openai`, or `ollama` |
|
|
194
199
|
|
|
195
200
|
> **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
|
|
196
201
|
|
|
202
|
+
**Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
wcag-a11y scan -u http://localhost:3000 --report # review a11y-report.md
|
|
206
|
+
wcag-a11y fix --from-report --apply # patch files from that report
|
|
207
|
+
```
|
|
208
|
+
|
|
197
209
|
---
|
|
198
210
|
|
|
199
211
|
## AI Providers
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const program = new Command();
|
|
|
13
13
|
program
|
|
14
14
|
.name('wcag-a11y')
|
|
15
15
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
16
|
-
.version('0.3.
|
|
16
|
+
.version('0.3.6');
|
|
17
17
|
program
|
|
18
18
|
.command('init')
|
|
19
19
|
.description('Create a11y.config.json in the current directory')
|
|
@@ -77,21 +77,30 @@ program
|
|
|
77
77
|
program
|
|
78
78
|
.command('fix')
|
|
79
79
|
.description('Scan for violations and apply AI fixes directly to source files')
|
|
80
|
-
.
|
|
80
|
+
.option('-u, --url <url>', 'Base URL of your dev server (e.g. http://localhost:3000)')
|
|
81
81
|
.option('-p, --pages <pages...>', 'Specific pages to scan', ['/'])
|
|
82
82
|
.option('-c, --crawl', 'Auto-discover pages by following same-origin links', false)
|
|
83
|
+
.option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
|
|
83
84
|
.option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
|
|
84
85
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
|
|
85
86
|
.action(async (opts) => {
|
|
87
|
+
if (!opts.url && !opts.fromReport) {
|
|
88
|
+
console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
86
91
|
try {
|
|
87
92
|
const config = loadConfig();
|
|
88
93
|
if (opts.provider)
|
|
89
94
|
config.provider = opts.provider;
|
|
90
95
|
const provider = createAIProvider(config);
|
|
96
|
+
const reportPath = opts.fromReport
|
|
97
|
+
? (opts.fromReport === true ? 'a11y-report.md' : opts.fromReport)
|
|
98
|
+
: undefined;
|
|
91
99
|
await runFix({
|
|
92
100
|
url: opts.url,
|
|
93
101
|
pages: opts.pages,
|
|
94
102
|
crawl: opts.crawl,
|
|
103
|
+
reportPath,
|
|
95
104
|
apply: opts.apply,
|
|
96
105
|
provider,
|
|
97
106
|
srcDir: resolve(process.cwd(), 'src'),
|
package/dist/fixer.js
CHANGED
|
@@ -5,15 +5,32 @@ import { crawl } from './crawler.js';
|
|
|
5
5
|
const SOURCE_EXTS = new Set(['.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.html']);
|
|
6
6
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', '.nuxt', 'coverage', 'public']);
|
|
7
7
|
export async function runFix(opts) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
let allViolations;
|
|
9
|
+
let framework;
|
|
10
|
+
if (opts.reportPath) {
|
|
11
|
+
const absReport = resolve(process.cwd(), opts.reportPath);
|
|
12
|
+
if (!existsSync(absReport)) {
|
|
13
|
+
throw new Error(`Report file not found: ${opts.reportPath}`);
|
|
14
|
+
}
|
|
15
|
+
console.log(`\nLoading violations from ${opts.reportPath}...`);
|
|
16
|
+
allViolations = parseReportViolations(absReport);
|
|
17
|
+
if (allViolations.length === 0) {
|
|
18
|
+
console.log(chalk.green('\nNo violations found in report.'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(`Found ${allViolations.length} violation(s) in report. Locating source files...\n`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(`\nScanning ${opts.url}...`);
|
|
25
|
+
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
|
|
26
|
+
framework = result.framework;
|
|
27
|
+
if (result.totalViolations === 0) {
|
|
28
|
+
console.log(chalk.green('\nNo violations found.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
|
|
32
|
+
allViolations = result.pages.flatMap((p) => p.violations);
|
|
14
33
|
}
|
|
15
|
-
console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
|
|
16
|
-
const allViolations = result.pages.flatMap((p) => p.violations);
|
|
17
34
|
// Group violations by resolved source file path
|
|
18
35
|
const fileGroups = new Map();
|
|
19
36
|
let unlocated = 0;
|
|
@@ -97,6 +114,57 @@ export async function runFix(opts) {
|
|
|
97
114
|
console.log(chalk.yellow(`${unlocated} violation(s) could not be located in source files.`));
|
|
98
115
|
}
|
|
99
116
|
}
|
|
117
|
+
function parseReportViolations(reportPath) {
|
|
118
|
+
const content = readFileSync(reportPath, 'utf8');
|
|
119
|
+
const violations = [];
|
|
120
|
+
const pageParts = content.split(/^## Page:/m).slice(1);
|
|
121
|
+
for (const part of pageParts) {
|
|
122
|
+
const pageUrl = part.split('\n')[0].trim();
|
|
123
|
+
if (part.includes('✅ No violations found'))
|
|
124
|
+
continue;
|
|
125
|
+
for (const group of part.split(/^---$/m)) {
|
|
126
|
+
const headingMatch = group.match(/^###\s+\S+\s+\[(\w+)\]\s+(.+)$/m);
|
|
127
|
+
if (!headingMatch)
|
|
128
|
+
continue;
|
|
129
|
+
const impact = headingMatch[1].toLowerCase();
|
|
130
|
+
const description = headingMatch[2].trim();
|
|
131
|
+
const ruleMatch = group.match(/\*\*Rule:\*\*\s+`([^`]+)`/);
|
|
132
|
+
if (!ruleMatch)
|
|
133
|
+
continue;
|
|
134
|
+
const ruleId = ruleMatch[1];
|
|
135
|
+
let wcag = '';
|
|
136
|
+
let level = 'A';
|
|
137
|
+
const wcagStd = group.match(/\*\*WCAG:\*\*\s+SC\s+([\d.]+)\s+\(Level\s+([A-Z]+)\)/);
|
|
138
|
+
if (wcagStd) {
|
|
139
|
+
wcag = wcagStd[1];
|
|
140
|
+
level = wcagStd[2];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const wcagAI = group.match(/\*\*WCAG:\*\*\s+WCAG\s+[\d.]+\s+SC\s+([\d.]+)/);
|
|
144
|
+
if (wcagAI)
|
|
145
|
+
wcag = wcagAI[1];
|
|
146
|
+
}
|
|
147
|
+
const repMatch = group.match(/\*\*Representative element:\*\*\s*\n`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
|
|
148
|
+
if (!repMatch)
|
|
149
|
+
continue;
|
|
150
|
+
const selector = repMatch[1];
|
|
151
|
+
const source = repMatch[2];
|
|
152
|
+
const htmlMatch = group.match(/\*\*Representative element:\*\*[\s\S]*?```html\n([\s\S]*?)\n```/);
|
|
153
|
+
const html = htmlMatch?.[1] ?? '';
|
|
154
|
+
violations.push({ ruleId, wcag, level, impact, description, selector, html, page: pageUrl, ...(source ? { source } : {}) });
|
|
155
|
+
const alsoSection = group.match(/\*\*Also affects[^*]*\*\*([\s\S]*?)(?:\n\n\*\*|\n---|\n```|$)/);
|
|
156
|
+
if (alsoSection) {
|
|
157
|
+
for (const line of alsoSection[1].split('\n')) {
|
|
158
|
+
const m = line.match(/^-\s+`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
|
|
159
|
+
if (!m)
|
|
160
|
+
continue;
|
|
161
|
+
violations.push({ ruleId, wcag, level, impact, description, selector: m[1], html: '', page: pageUrl, ...(m[2] ? { source: m[2] } : {}) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return violations;
|
|
167
|
+
}
|
|
100
168
|
export function findSourceFile(violation, srcDir) {
|
|
101
169
|
// Priority 1: React dev-mode source annotation
|
|
102
170
|
if (violation.source) {
|