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 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>` | required | Base URL of your running dev server |
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.5');
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
- .requiredOption('-u, --url <url>', 'Base URL of your dev server (e.g. http://localhost:3000)')
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
- console.log(`\nScanning ${opts.url}...`);
9
- const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
10
- const { framework } = result;
11
- if (result.totalViolations === 0) {
12
- console.log(chalk.green('\nNo violations found.'));
13
- return;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wcag-a11y",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes",
5
5
  "type": "module",
6
6
  "bin": {