wcag-a11y 0.4.0 → 0.4.2
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 +147 -145
- package/dist/ai/base.js +12 -6
- package/dist/ai/fallback-fix.js +126 -0
- package/dist/ai/prompt.js +13 -6
- package/dist/cli.js +9 -4
- package/dist/config.js +3 -2
- package/dist/crawler.js +1 -1
- package/dist/fixer.js +2 -1
- package/dist/reporter/markdown.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Dannyplusplus12/WCAG-A11y/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y`
|
|
5
|
+
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y` crawls your running dev server with Playwright, runs 40+ WCAG 2.1/2.2 checks, and uses AI to generate ready-to-paste fix prompts **or write the fixes directly into your source files**.
|
|
6
6
|
|
|
7
7
|
Two modes:
|
|
8
|
-
- **`scan`** — find violations +
|
|
8
|
+
- **`scan`** — find violations + get AI prompts you paste into Cursor, Copilot, or Claude
|
|
9
9
|
- **`fix`** — find violations + patch source files automatically (dry-run by default, `--apply` to write)
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Try it instantly
|
|
14
14
|
|
|
15
|
-
No dev server, no config
|
|
15
|
+
No dev server, no config:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npx wcag-a11y demo
|
|
@@ -22,41 +22,50 @@ npx wcag-a11y demo
|
|
|
22
22
|
|
|
23
23
|
## What the output looks like
|
|
24
24
|
|
|
25
|
-
Running a scan prints a violation summary per page, then AI-generated fixes for each rule:
|
|
26
|
-
|
|
27
25
|
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
✖ critical
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
26
|
+
WCAG A11y — scan complete
|
|
27
|
+
────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
✖ http://localhost:3000 3 critical 4 serious 3 moderate
|
|
30
|
+
|
|
31
|
+
[CRITICAL] Images must have an alt attribute WCAG 1.1.1
|
|
32
|
+
→ img
|
|
33
|
+
[CRITICAL] Form inputs must have an associated label WCAG 1.3.1
|
|
34
|
+
→ input[type="email"]
|
|
35
|
+
[CRITICAL] Buttons must have an accessible name WCAG 4.1.2
|
|
36
|
+
→ button[type="submit"]
|
|
37
|
+
[SERIOUS] Normal text must meet 4.5:1 contrast ratio WCAG 1.4.3
|
|
38
|
+
→ p
|
|
39
|
+
[SERIOUS] Links must have descriptive text WCAG 2.4.4
|
|
40
|
+
→ a[href="/sale"]
|
|
41
|
+
… and 5 more
|
|
42
|
+
|
|
43
|
+
────────────────────────────────────────────────────────────
|
|
44
|
+
Total: 3 critical · 4 serious · 3 moderate
|
|
45
|
+
|
|
46
|
+
AI Fix Prompts — paste any of these into Cursor, Copilot, or Claude
|
|
47
|
+
────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
[img-alt]
|
|
50
|
+
→ img
|
|
51
|
+
Screen reader users hear nothing for this image — branding, instructions,
|
|
52
|
+
or data it conveys is completely invisible to them.
|
|
53
|
+
┌─ Copy this prompt ──────────────────────────────────────
|
|
54
|
+
│ Fix WCAG 1.1.1 (Level A) — img is missing an alt attribute
|
|
55
|
+
│
|
|
56
|
+
│ Current HTML:
|
|
57
|
+
│ <img src="banner.jpg">
|
|
58
|
+
│
|
|
59
|
+
│ How to fix:
|
|
60
|
+
│ Add alt text describing the image content.
|
|
61
|
+
│ Use alt="" if the image is purely decorative.
|
|
62
|
+
│ Example: <img src="banner.jpg" alt="Summer sale — 50% off">
|
|
63
|
+
└─────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
… 9 more prompts — full report saved to a11y-report.md
|
|
55
66
|
```
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
|
|
68
|
+
Each prompt tells you the WCAG criterion, shows the broken element, and gives the exact fix — ready to paste into your AI editor.
|
|
60
69
|
|
|
61
70
|
---
|
|
62
71
|
|
|
@@ -66,62 +75,27 @@ The full output is saved to `a11y-report.md` by default. Pass `--no-report` to s
|
|
|
66
75
|
npm install -g wcag-a11y
|
|
67
76
|
```
|
|
68
77
|
|
|
69
|
-
##
|
|
78
|
+
## Quick start
|
|
70
79
|
|
|
71
80
|
```bash
|
|
72
|
-
|
|
73
|
-
wcag-a11y init --
|
|
74
|
-
wcag-a11y init --
|
|
75
|
-
wcag-a11y init --
|
|
76
|
-
wcag-a11y init --
|
|
77
|
-
wcag-a11y init --
|
|
78
|
-
wcag-a11y init
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
wcag-a11y
|
|
82
|
-
wcag-a11y init --provider azure-openai # Azure OpenAI
|
|
83
|
-
wcag-a11y init --provider ollama # Local — no API key needed
|
|
81
|
+
# 1. Configure your AI provider and framework (Gemini is free, no credit card)
|
|
82
|
+
wcag-a11y init --framework next # Next.js
|
|
83
|
+
wcag-a11y init --framework react # React / Vite
|
|
84
|
+
wcag-a11y init --framework vue # Vue / Nuxt
|
|
85
|
+
wcag-a11y init --framework angular # Angular
|
|
86
|
+
wcag-a11y init --framework svelte # Svelte / SvelteKit
|
|
87
|
+
wcag-a11y init # plain HTML or auto-detect
|
|
88
|
+
|
|
89
|
+
# 2. Start your dev server, then scan
|
|
90
|
+
wcag-a11y scan -u http://localhost:3000
|
|
84
91
|
```
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
Add `--pages / /about /contact` to scan specific routes, or `--crawl` to follow links automatically.
|
|
87
94
|
|
|
88
95
|
---
|
|
89
96
|
|
|
90
97
|
## Commands
|
|
91
98
|
|
|
92
|
-
### `wcag-a11y demo`
|
|
93
|
-
|
|
94
|
-
Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
wcag-a11y demo # violations + AI fixes (default, requires config)
|
|
98
|
-
wcag-a11y demo --no-ai # violations only, no AI — faster
|
|
99
|
-
wcag-a11y demo --no-report # skip saving a11y-report.md
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
| Flag | Description |
|
|
103
|
-
|---|---|
|
|
104
|
-
| `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
|
|
105
|
-
| `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
### `wcag-a11y init`
|
|
110
|
-
|
|
111
|
-
Create `a11y.config.json` in the current directory, pre-configured for your chosen provider.
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
wcag-a11y init # Gemini (default)
|
|
115
|
-
wcag-a11y init --provider openai # OpenAI
|
|
116
|
-
wcag-a11y init --provider ollama # Ollama (local)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
| Flag | Description |
|
|
120
|
-
|---|---|
|
|
121
|
-
| `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
99
|
### `wcag-a11y scan`
|
|
126
100
|
|
|
127
101
|
Scan a running dev server for accessibility violations.
|
|
@@ -137,16 +111,17 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
|
137
111
|
| Flag | Default | Description |
|
|
138
112
|
|---|---|---|
|
|
139
113
|
| `-u, --url <url>` | required | Base URL of your running dev server |
|
|
140
|
-
| `-p, --pages <
|
|
141
|
-
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages
|
|
142
|
-
| `--no-
|
|
143
|
-
| `--no-
|
|
144
|
-
| `--no-explain` |
|
|
145
|
-
| `--terminal` | off | Print violations
|
|
146
|
-
| `--fast-mode` | off | Output only
|
|
147
|
-
| `--group <strategy>` | `rule` | `rule
|
|
148
|
-
| `--ci` | off | Exit with code `1` if any violations are found
|
|
149
|
-
| `--provider <name>` | from config | Override
|
|
114
|
+
| `-p, --pages <paths...>` | `/` | Paths to scan. Space-separated: `--pages / /about /contact` |
|
|
115
|
+
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages |
|
|
116
|
+
| `--no-ai` | — | Skip AI fix generation — scan runs faster, violations only |
|
|
117
|
+
| `--no-report` | — | Skip saving `a11y-report.md` |
|
|
118
|
+
| `--no-explain` | — | Omit explanations, show prompts only |
|
|
119
|
+
| `--terminal` | off | Print violations and AI prompts to terminal |
|
|
120
|
+
| `--fast-mode` | off | Output only the raw prompts — no summaries or decoration |
|
|
121
|
+
| `--group <strategy>` | `rule` | `rule`: one prompt per rule type. `none`: one prompt per element |
|
|
122
|
+
| `--ci` | off | Exit with code `1` if any violations are found |
|
|
123
|
+
| `--provider <name>` | from config | Override AI provider for this run |
|
|
124
|
+
| `--framework <name>` | from config | Override framework for this run (e.g. `next`, `react`, `vue`, `angular`, `svelte`, `astro`) |
|
|
150
125
|
|
|
151
126
|
---
|
|
152
127
|
|
|
@@ -155,34 +130,13 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
|
155
130
|
Scan for violations and apply AI-generated patches directly to your source files. Works with any framework — React, Vue, Angular, Svelte, or plain HTML.
|
|
156
131
|
|
|
157
132
|
```bash
|
|
158
|
-
#
|
|
159
|
-
wcag-a11y fix -u http://localhost:3000
|
|
160
|
-
|
|
161
|
-
# Preview specific pages
|
|
162
|
-
wcag-a11y fix -u http://localhost:3000 --pages / /about /contact
|
|
163
|
-
|
|
164
|
-
# Write fixes to disk
|
|
165
|
-
wcag-a11y fix -u http://localhost:3000 --apply
|
|
166
|
-
|
|
167
|
-
# Auto-discover pages + write fixes
|
|
168
|
-
wcag-a11y fix -u http://localhost:3000 --crawl --apply
|
|
169
|
-
|
|
170
|
-
# Skip rescanning — load violations from an existing report
|
|
171
|
-
wcag-a11y fix --from-report
|
|
172
|
-
wcag-a11y fix --from-report ./reports/a11y-report.md --apply
|
|
133
|
+
wcag-a11y fix -u http://localhost:3000 # dry-run: show diff, nothing written
|
|
134
|
+
wcag-a11y fix -u http://localhost:3000 --apply # write fixes to disk
|
|
135
|
+
wcag-a11y fix --from-report --apply # patch from an existing report
|
|
173
136
|
```
|
|
174
137
|
|
|
175
|
-
**How it works:**
|
|
176
|
-
|
|
177
|
-
1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
|
|
178
|
-
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)
|
|
179
|
-
3. Groups violations by file (multiple violations in the same file → one AI call)
|
|
180
|
-
4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
|
|
181
|
-
5. Shows a colored diff before writing anything
|
|
182
|
-
6. With `--apply`, overwrites the file; without it, only prints the diff
|
|
183
|
-
|
|
184
138
|
```
|
|
185
|
-
src/components/Navbar.jsx — 2
|
|
139
|
+
src/components/Navbar.jsx — 2 violations
|
|
186
140
|
· [button-name] Buttons must have an accessible name
|
|
187
141
|
· [aria-valid-role] Elements must use valid ARIA roles
|
|
188
142
|
|
|
@@ -196,52 +150,93 @@ src/components/Navbar.jsx — 2 violation(s)
|
|
|
196
150
|
+ <li>Home</li>
|
|
197
151
|
```
|
|
198
152
|
|
|
153
|
+
**Common workflow:** scan first to review, then patch:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md
|
|
157
|
+
wcag-a11y fix --from-report --apply # patches files from that report, no second crawl
|
|
158
|
+
```
|
|
159
|
+
|
|
199
160
|
| Flag | Default | Description |
|
|
200
161
|
|---|---|---|
|
|
201
|
-
| `-u, --url <url>` | — | Base URL
|
|
202
|
-
| `-p, --pages <
|
|
162
|
+
| `-u, --url <url>` | — | Base URL. Required unless `--from-report` is used |
|
|
163
|
+
| `-p, --pages <paths...>` | `/` | Paths to scan |
|
|
203
164
|
| `-c, --crawl` | off | Auto-discover pages by following same-origin links |
|
|
204
|
-
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of
|
|
205
|
-
| `--apply` | off | Write
|
|
206
|
-
| `--provider <name>` | from config | Override AI provider for this run
|
|
165
|
+
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of rescanning |
|
|
166
|
+
| `--apply` | off | Write fixes to disk (dry-run without this flag) |
|
|
167
|
+
| `--provider <name>` | from config | Override AI provider for this run |
|
|
168
|
+
| `--framework <name>` | from config | Override framework for this run (e.g. `next`, `react`, `vue`, `angular`, `svelte`, `astro`) |
|
|
169
|
+
|
|
170
|
+
---
|
|
207
171
|
|
|
208
|
-
|
|
172
|
+
### `wcag-a11y init`
|
|
209
173
|
|
|
210
|
-
|
|
174
|
+
Create `a11y.config.json` pre-configured for your chosen provider and framework.
|
|
211
175
|
|
|
212
176
|
```bash
|
|
213
|
-
wcag-a11y
|
|
214
|
-
wcag-a11y
|
|
177
|
+
wcag-a11y init # Gemini (free, default)
|
|
178
|
+
wcag-a11y init --provider openai --framework next # OpenAI + Next.js
|
|
179
|
+
wcag-a11y init --provider ollama --framework react # local Ollama + React
|
|
180
|
+
# … 12 providers total, any framework string accepted
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Flag | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `--provider <name>` | AI provider. Default: `gemini`. See [AI Providers](#ai-providers) for all options |
|
|
186
|
+
| `--framework <name>` | Your project framework — saved to config so every scan uses it automatically |
|
|
187
|
+
|
|
188
|
+
Framework is saved as `"framework"` in `a11y.config.json`. You can also edit the file directly at any time. Supported values for best results: `next`, `react`, `vue`, `nuxt`, `angular`, `svelte`, `gatsby`, `remix`, `astro` — or any free-form string.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### `wcag-a11y demo`
|
|
193
|
+
|
|
194
|
+
Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
wcag-a11y demo # violations + AI fix prompts (requires config)
|
|
198
|
+
wcag-a11y demo --no-ai # violations only, no AI
|
|
215
199
|
```
|
|
216
200
|
|
|
217
201
|
---
|
|
218
202
|
|
|
219
203
|
## AI Providers
|
|
220
204
|
|
|
221
|
-
12 providers
|
|
205
|
+
12 providers supported. Configure once in `a11y.config.json`, or override per-run with `--provider`.
|
|
222
206
|
|
|
223
|
-
| Provider | `--provider`
|
|
207
|
+
| Provider | `--provider` | Default model | Notes |
|
|
224
208
|
|---|---|---|---|
|
|
225
|
-
| Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` |
|
|
226
|
-
| OpenAI | `openai` | `gpt-4o-mini` |
|
|
227
|
-
| Anthropic | `anthropic` | `claude-sonnet-4-6` |
|
|
228
|
-
| Mistral | `mistral` | `mistral-large-latest` |
|
|
229
|
-
| Groq | `groq` | `llama-3.3-70b-versatile` |
|
|
230
|
-
| Cohere | `cohere` | `command-r-plus` |
|
|
231
|
-
| xAI | `xai` | `grok-2` |
|
|
232
|
-
| DeepSeek | `deepseek` | `deepseek-chat` |
|
|
233
|
-
| Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` |
|
|
234
|
-
| Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` |
|
|
235
|
-
| Azure OpenAI | `azure-openai` | *(your deployment)* |
|
|
236
|
-
| Ollama | `ollama` | `llama3` |
|
|
237
|
-
|
|
238
|
-
All models are configurable.
|
|
209
|
+
| Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | Free tier available |
|
|
210
|
+
| OpenAI | `openai` | `gpt-4o-mini` | |
|
|
211
|
+
| Anthropic | `anthropic` | `claude-sonnet-4-6` | |
|
|
212
|
+
| Mistral | `mistral` | `mistral-large-latest` | |
|
|
213
|
+
| Groq | `groq` | `llama-3.3-70b-versatile` | Fast inference |
|
|
214
|
+
| Cohere | `cohere` | `command-r-plus` | |
|
|
215
|
+
| xAI | `xai` | `grok-2` | |
|
|
216
|
+
| DeepSeek | `deepseek` | `deepseek-chat` | |
|
|
217
|
+
| Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | Open-source models |
|
|
218
|
+
| Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | |
|
|
219
|
+
| Azure OpenAI | `azure-openai` | *(your deployment)* | |
|
|
220
|
+
| Ollama | `ollama` | `llama3` | Local — no API key |
|
|
221
|
+
|
|
222
|
+
All models are configurable. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data — you always get something actionable.
|
|
239
223
|
|
|
240
224
|
---
|
|
241
225
|
|
|
242
|
-
## Config
|
|
226
|
+
## Config
|
|
243
227
|
|
|
244
|
-
Only the fields for your
|
|
228
|
+
Run `wcag-a11y init` to generate `a11y.config.json`. Only fill in the fields for your chosen provider. This file is gitignored by default.
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"provider": "gemini",
|
|
233
|
+
"apiKey": "YOUR_GEMINI_API_KEY",
|
|
234
|
+
"framework": "next"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
<details>
|
|
239
|
+
<summary>Full config reference (all 12 providers)</summary>
|
|
245
240
|
|
|
246
241
|
```json
|
|
247
242
|
{
|
|
@@ -287,11 +282,16 @@ Only the fields for your active `provider` are required. This file is gitignored
|
|
|
287
282
|
}
|
|
288
283
|
```
|
|
289
284
|
|
|
285
|
+
</details>
|
|
286
|
+
|
|
290
287
|
---
|
|
291
288
|
|
|
292
289
|
## What it checks
|
|
293
290
|
|
|
294
|
-
40+ rules across 10
|
|
291
|
+
40+ rules across 10 WCAG 2.1/2.2 categories: Text Alternatives, Color Contrast, Forms, Keyboard, ARIA, Structure, Links, Media, Tables, and Language.
|
|
292
|
+
|
|
293
|
+
<details>
|
|
294
|
+
<summary>Full rule list</summary>
|
|
295
295
|
|
|
296
296
|
### Text Alternatives — WCAG 1.1.1
|
|
297
297
|
|
|
@@ -394,6 +394,8 @@ Only the fields for your active `provider` are required. This file is gitignored
|
|
|
394
394
|
| `html-lang` | serious | `<html>` must have a `lang` attribute |
|
|
395
395
|
| `html-lang-valid` | serious | `lang` attribute must be a valid BCP 47 language tag |
|
|
396
396
|
|
|
397
|
+
</details>
|
|
398
|
+
|
|
397
399
|
---
|
|
398
400
|
|
|
399
401
|
## Use in CI/CD
|
package/dist/ai/base.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fallbackExplanation } from './fallback-explanation.js';
|
|
2
|
+
import { getFix, getFixCategory } from './fallback-fix.js';
|
|
2
3
|
export class BaseAIProvider {
|
|
3
4
|
parse(text, groups, framework) {
|
|
4
5
|
try {
|
|
@@ -7,7 +8,7 @@ export class BaseAIProvider {
|
|
|
7
8
|
return groups.map((g) => {
|
|
8
9
|
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
9
10
|
return found
|
|
10
|
-
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
11
|
+
? { ...found, selectors: g.selectors, instanceCount: g.count, fixCategory: getFixCategory(g.ruleId) }
|
|
11
12
|
: this.fallbackFix(g, framework);
|
|
12
13
|
});
|
|
13
14
|
}
|
|
@@ -20,18 +21,23 @@ export class BaseAIProvider {
|
|
|
20
21
|
}
|
|
21
22
|
fallbackFix(g, framework) {
|
|
22
23
|
const v = g.representative;
|
|
23
|
-
const selectorList = g.selectors.map((s) => `-
|
|
24
|
+
const selectorList = g.selectors.map((s) => `- \`${s}\``).join('\n');
|
|
24
25
|
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
25
|
-
const fwNote = framework ? `This project uses ${framework}
|
|
26
|
+
const fwNote = framework ? `This project uses ${framework}.\n\n` : '';
|
|
27
|
+
const fixInstructions = getFix(g.ruleId);
|
|
28
|
+
const fixSection = fixInstructions ? `\n\nHow to fix:\n${fixInstructions}` : '';
|
|
26
29
|
const prompt = g.count > 1
|
|
27
|
-
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n
|
|
28
|
-
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n
|
|
30
|
+
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}\n\nApply this fix to all ${g.count} instances across the codebase.`
|
|
31
|
+
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n\nCurrent HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}`;
|
|
32
|
+
const category = getFixCategory(g.ruleId);
|
|
33
|
+
const fixedCode = category === 'edit-element' || !category ? v.html : undefined;
|
|
29
34
|
return {
|
|
30
35
|
ruleId: g.ruleId,
|
|
31
36
|
selectors: g.selectors,
|
|
32
37
|
instanceCount: g.count,
|
|
33
38
|
explanation,
|
|
34
|
-
fixedCode
|
|
39
|
+
fixedCode,
|
|
40
|
+
fixCategory: getFixCategory(g.ruleId),
|
|
35
41
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
36
42
|
optimalPrompt: prompt,
|
|
37
43
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const RULE_FIX_INSTRUCTIONS = {
|
|
2
|
+
// Text alternatives
|
|
3
|
+
'img-alt': 'Add an `alt` attribute describing the image content. Use `alt=""` for decorative images.\nExample: `<img src="photo.jpg" alt="Team photo at the 2024 company retreat">`',
|
|
4
|
+
'input-image-alt': 'Add an `alt` attribute to the `<input type="image">` describing the button\'s action.\nExample: `<input type="image" src="submit.png" alt="Submit form">`',
|
|
5
|
+
'svg-title': 'Add a `<title>` element as the first child of `<svg>` and add `aria-labelledby` pointing to its `id`.\nExample: `<svg aria-labelledby="chartTitle" role="img"><title id="chartTitle">Monthly sales bar chart</title>...</svg>`',
|
|
6
|
+
'object-alt': 'Add descriptive fallback content inside the `<object>` tag, or use `aria-label`.\nExample: `<object data="report.pdf"><p>Download the Q4 2024 report (PDF)</p></object>`',
|
|
7
|
+
'role-img-alt': 'Add `aria-label` or `aria-labelledby` to the element with `role="img"`.\nExample: `<div role="img" aria-label="Star rating: 4 out of 5"></div>`',
|
|
8
|
+
'image-redundant-alt': 'Change the `alt` to `alt=""` (empty string) since the image\'s meaning is already conveyed by adjacent text — screen readers skip empty-alt images automatically.',
|
|
9
|
+
// Tables
|
|
10
|
+
'table-headers': 'Add `<th scope="col">` for column headers and `<th scope="row">` for row headers.\nExample: `<thead><tr><th scope="col">Name</th><th scope="col">Price</th></tr></thead>`',
|
|
11
|
+
'table-scope-valid': 'Change the `scope` attribute to a valid value: `"col"`, `"row"`, `"colgroup"`, or `"rowgroup"`. Remove it if the element is not a header.',
|
|
12
|
+
'td-headers-attr': 'Either add matching `id` attributes to the referenced `<th>` elements, or remove the invalid `headers` attributes from the `<td>` elements.',
|
|
13
|
+
'table-duplicate-name': 'Make the `<caption>` and `summary` convey different information: use `<caption>` for a short visible title and `summary` for an extended description, or remove the redundant attribute.',
|
|
14
|
+
// Structure
|
|
15
|
+
'heading-order': 'Restructure headings so levels are never skipped. Change the offending tag to the correct level (e.g., change `<h4>` directly under `<h2>` to `<h3>`). Use CSS for visual sizing — never pick heading levels for appearance.',
|
|
16
|
+
'page-title': 'Add or update `<title>` in `<head>` with a unique, descriptive title.\nUse the format: `<title>Page Name — Site Name</title>`',
|
|
17
|
+
'landmark-one-main': 'Wrap primary page content in exactly one `<main id="main-content">` element. Remove any duplicate `<main>` elements.',
|
|
18
|
+
'list-structure': 'Replace non-semantic markup with proper list structure.\nExample: change `<div class="item">` repeated items into `<ul><li>...</li><li>...</li></ul>` for unordered, or `<ol>` for ordered lists.',
|
|
19
|
+
'region-landmark': 'Wrap the content in the appropriate landmark element: `<header>`, `<nav aria-label="...">`, `<main>`, `<aside aria-label="...">`, or `<footer>`. For generic sections: `<section aria-label="Section name">`.',
|
|
20
|
+
'duplicate-id': 'Make each `id` unique across the page. If the ID is referenced by `aria-labelledby` or `aria-describedby`, update those references to match the new unique IDs. Append a suffix like `-nav`, `-footer`, or a number to disambiguate.',
|
|
21
|
+
'frame-title': 'Add a `title` attribute to the `<iframe>` describing its content.\nExample: `<iframe title="Product demo video" src="..."></iframe>`',
|
|
22
|
+
'meta-viewport': 'Remove `user-scalable=no` and ensure `maximum-scale` is not below 5.\nChange to: `<meta name="viewport" content="width=device-width, initial-scale=1">`',
|
|
23
|
+
'marquee': 'Replace `<marquee>` with a CSS-animated element. Respect `prefers-reduced-motion`:\n`@media (prefers-reduced-motion: reduce) { .marquee { animation: none; } }`\nProvide static fallback content for motion-sensitive users.',
|
|
24
|
+
'p-as-heading': 'Replace the visually bold `<p>` with the appropriate heading tag (`<h2>`, `<h3>`, etc.) matching its position in the document outline. Apply visual styles via CSS, not `<b>` or `<strong>` tags inside `<p>`.',
|
|
25
|
+
// Media
|
|
26
|
+
'video-captions': 'Add a `<track kind="captions">` element inside the `<video>` pointing to a WebVTT (.vtt) file.\nExample: `<track kind="captions" src="/captions-en.vtt" srclang="en" label="English" default>`',
|
|
27
|
+
'audio-description': 'Add a `<track kind="descriptions">` element inside `<video>` for audio descriptions.\nExample: `<track kind="descriptions" src="/descriptions-en.vtt" srclang="en" label="Audio description">`',
|
|
28
|
+
'audio-transcript': 'Provide a text transcript immediately after the `<audio>` element, either as a link or inline.\nExample: `<a href="/transcript.html">Read transcript</a>`',
|
|
29
|
+
// Links
|
|
30
|
+
'link-name': 'Add descriptive text inside the `<a>`, or use `aria-label` to describe the destination.\nExample: `<a href="/products" aria-label="View all products">View all</a>`. Avoid "click here" or "read more".',
|
|
31
|
+
'link-empty': 'Add descriptive text or `aria-label` to the empty anchor. If it is a decorative icon, add `aria-label`. If non-functional, remove the `<a>` element entirely.',
|
|
32
|
+
'identical-links-different-purpose': 'Add unique `aria-label` to each link to distinguish its destination.\nExample: `<a href="/product-a" aria-label="Read more about Product A">Read more</a>` and `<a href="/product-b" aria-label="Read more about Product B">Read more</a>`',
|
|
33
|
+
'link-new-window-warn': 'Add `rel="noopener noreferrer"` and warn users the link opens a new tab.\nExample: `<a href="..." target="_blank" rel="noopener noreferrer">Download PDF <span class="sr-only">(opens in new tab)</span></a>`',
|
|
34
|
+
// Forms
|
|
35
|
+
'label-missing': 'Add a `<label>` linked via `for`/`id`, or add `aria-label` directly to the input.\nExample: `<label for="user-email">Email address</label><input id="user-email" type="email">`',
|
|
36
|
+
'label-empty': 'Add descriptive text inside the existing `<label>`.\nExample: change `<label for="q"></label>` to `<label for="q">Search</label>`',
|
|
37
|
+
'error-identification': 'Link error messages to their fields using `aria-describedby` and add `role="alert"` to the error container.\nExample: `<input aria-describedby="email-error"><span id="email-error" role="alert">Please enter a valid email address</span>`',
|
|
38
|
+
'autocomplete': 'Add the correct `autocomplete` attribute to the input. Common values: `name`, `email`, `tel`, `current-password`, `new-password`, `given-name`, `family-name`, `street-address`, `postal-code`.\nExample: `<input type="email" autocomplete="email">`',
|
|
39
|
+
'input-button-name': 'Add a descriptive `value` to `<input type="button/submit/reset">` or `aria-label` to icon buttons.\nExample: `<input type="submit" value="Submit registration form">` or `<button aria-label="Search"><svg aria-hidden="true">...</svg></button>`',
|
|
40
|
+
'fieldset-legend': 'Wrap related controls in `<fieldset>` with `<legend>` as its first child.\nExample: `<fieldset><legend>Shipping address</legend><label>...</label><input>...</fieldset>`',
|
|
41
|
+
'form-field-required-label': 'Add `required` and `aria-required="true"` to the input. Indicate required status in the label text.\nExample: `<label for="name">Full name <span aria-hidden="true">*</span><span class="sr-only">(required)</span></label><input id="name" required aria-required="true">`',
|
|
42
|
+
// Language
|
|
43
|
+
'html-lang': 'Add a `lang` attribute to the `<html>` element with the correct BCP 47 language tag.\nExample: `<html lang="en">` for English, `<html lang="vi">` for Vietnamese, `<html lang="fr">` for French.',
|
|
44
|
+
'html-lang-valid': 'Replace the invalid `lang` value with a valid BCP 47 language tag.\nValid examples: `"en"`, `"en-US"`, `"fr"`, `"de"`, `"es"`, `"zh"`, `"ja"`, `"ko"`, `"vi"`, `"ar"`.',
|
|
45
|
+
// Color contrast
|
|
46
|
+
'color-contrast-text': 'Increase the contrast ratio between the text and background to at least 4.5:1. Darken the text color or lighten the background. Verify with the WebAIM Contrast Checker.\nExample: change `color: #999` on a white background to `color: #767676` (minimum passing value).',
|
|
47
|
+
'color-contrast-large-text': 'Increase the contrast ratio to at least 3:1 for large text (18pt/24px+ regular, or 14pt/18.67px+ bold). Adjust the text or background color and verify with a contrast checker.',
|
|
48
|
+
// Keyboard
|
|
49
|
+
'no-positive-tabindex': 'Remove the `tabindex` attribute or set it to `tabindex="0"`. Reorder DOM elements to create the correct focus sequence — never use positive tabindex values to manage tab order.',
|
|
50
|
+
'interactive-not-focusable': 'Add `role="button"` (or the correct role) and `tabindex="0"`. Add keyboard event handlers for Enter and Space to match the click handler.\nExample: `<div role="button" tabindex="0" onclick="handleClick()" onkeydown="if(event.key===\'Enter\'||event.key===\' \')handleClick()">`',
|
|
51
|
+
'skip-link': 'Add a skip link as the very first child of `<body>`:\n`<a href="#main-content" class="skip-link">Skip to main content</a>`\nEnsure the target exists: `<main id="main-content">` (add `id="main-content"` to your existing `<main>` element).\nAdd CSS: `.skip-link { position: absolute; left: -9999px; } .skip-link:focus { position: static; left: 0; top: 0; z-index: 9999; padding: 8px; background: #fff; color: #000; }`',
|
|
52
|
+
'focus-visible': 'Remove `outline: none` or `outline: 0` from `:focus` styles. Add a clearly visible focus indicator.\nExample: `a:focus-visible, button:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }`. Never suppress focus outlines without an equally visible replacement.',
|
|
53
|
+
'scrollable-region-focusable': 'Add `tabindex="0"` to the scrollable container so keyboard users can focus and scroll it.\nExample: `<div class="overflow-auto" tabindex="0" aria-label="Scrollable content">...</div>`',
|
|
54
|
+
'accesskey-unique': 'Change duplicate `accesskey` values so each one is unique across the page. If shortcuts are not needed, remove the `accesskey` attribute entirely.',
|
|
55
|
+
// ARIA
|
|
56
|
+
'aria-valid-role': 'Remove the invalid `role` value or replace it with a valid WAI-ARIA role such as `button`, `checkbox`, `dialog`, `listbox`, `menu`, `menuitem`, `option`, `radio`, `tab`, `tabpanel`, or `tooltip`.',
|
|
57
|
+
'aria-required-attr': 'Add the missing required ARIA state or property for this role.\nExamples: `role="checkbox"` needs `aria-checked`; `role="combobox"` needs `aria-expanded`; `role="slider"` needs `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.',
|
|
58
|
+
'aria-hidden-focus': 'Remove `aria-hidden="true"` from any ancestor of a focusable element, or move `aria-hidden` to a sibling that contains no focusable descendants. Never place interactive elements inside `aria-hidden="true"` containers.',
|
|
59
|
+
'button-name': 'Add visible text content or `aria-label` to the `<button>`.\nExample: `<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>` or `<button>Submit order</button>`',
|
|
60
|
+
'aria-required-children': 'Add the required child elements with the correct ARIA roles inside this container.\nExamples: `role="listbox"` must contain `role="option"` children; `role="menu"` must contain `role="menuitem"` children; `role="grid"` must contain `role="row"` children.',
|
|
61
|
+
'aria-required-parent': 'Move this element inside its required ARIA parent container.\nExamples: `role="option"` must be inside `role="listbox"`; `role="tab"` must be inside `role="tablist"`; `role="menuitem"` must be inside `role="menu"`.',
|
|
62
|
+
'aria-prohibited-attr': 'Remove the prohibited ARIA attribute from this element. Elements with `role="presentation"` or `role="none"` must not have naming attributes like `aria-label` or `aria-labelledby`. Check the WAI-ARIA spec for the element\'s prohibited attributes.',
|
|
63
|
+
};
|
|
64
|
+
export function getFix(ruleId) {
|
|
65
|
+
return RULE_FIX_INSTRUCTIONS[ruleId];
|
|
66
|
+
}
|
|
67
|
+
const RULE_FIX_CATEGORY = {
|
|
68
|
+
// edit-element — fix targets the flagged element's own attributes or content
|
|
69
|
+
'img-alt': 'edit-element',
|
|
70
|
+
'input-image-alt': 'edit-element',
|
|
71
|
+
'svg-title': 'edit-element',
|
|
72
|
+
'object-alt': 'edit-element',
|
|
73
|
+
'role-img-alt': 'edit-element',
|
|
74
|
+
'image-redundant-alt': 'edit-element',
|
|
75
|
+
'table-scope-valid': 'edit-element',
|
|
76
|
+
'table-duplicate-name': 'edit-element',
|
|
77
|
+
'page-title': 'edit-element',
|
|
78
|
+
'frame-title': 'edit-element',
|
|
79
|
+
'meta-viewport': 'edit-element',
|
|
80
|
+
'p-as-heading': 'edit-element',
|
|
81
|
+
'heading-order': 'edit-element',
|
|
82
|
+
'duplicate-id': 'edit-element',
|
|
83
|
+
'link-name': 'edit-element',
|
|
84
|
+
'link-empty': 'edit-element',
|
|
85
|
+
'identical-links-different-purpose': 'edit-element',
|
|
86
|
+
'link-new-window-warn': 'edit-element',
|
|
87
|
+
'label-empty': 'edit-element',
|
|
88
|
+
'autocomplete': 'edit-element',
|
|
89
|
+
'input-button-name': 'edit-element',
|
|
90
|
+
'html-lang': 'edit-element',
|
|
91
|
+
'html-lang-valid': 'edit-element',
|
|
92
|
+
'no-positive-tabindex': 'edit-element',
|
|
93
|
+
'interactive-not-focusable': 'edit-element',
|
|
94
|
+
'scrollable-region-focusable': 'edit-element',
|
|
95
|
+
'accesskey-unique': 'edit-element',
|
|
96
|
+
'aria-valid-role': 'edit-element',
|
|
97
|
+
'aria-required-attr': 'edit-element',
|
|
98
|
+
'aria-prohibited-attr': 'edit-element',
|
|
99
|
+
'button-name': 'edit-element',
|
|
100
|
+
'aria-required-children': 'edit-element',
|
|
101
|
+
'video-captions': 'edit-element',
|
|
102
|
+
'audio-description': 'edit-element',
|
|
103
|
+
// add-elsewhere — fix inserts a new element at a different location
|
|
104
|
+
'skip-link': 'add-elsewhere',
|
|
105
|
+
'audio-transcript': 'add-elsewhere',
|
|
106
|
+
'label-missing': 'add-elsewhere',
|
|
107
|
+
'error-identification': 'add-elsewhere',
|
|
108
|
+
// change-css — fix lives in a stylesheet, not in the flagged element's markup
|
|
109
|
+
'color-contrast-text': 'change-css',
|
|
110
|
+
'color-contrast-large-text': 'change-css',
|
|
111
|
+
'focus-visible': 'change-css',
|
|
112
|
+
'marquee': 'change-css',
|
|
113
|
+
// restructure — fix touches multiple elements or wraps / moves content
|
|
114
|
+
'table-headers': 'restructure',
|
|
115
|
+
'td-headers-attr': 'restructure',
|
|
116
|
+
'landmark-one-main': 'restructure',
|
|
117
|
+
'list-structure': 'restructure',
|
|
118
|
+
'region-landmark': 'restructure',
|
|
119
|
+
'fieldset-legend': 'restructure',
|
|
120
|
+
'form-field-required-label': 'restructure',
|
|
121
|
+
'aria-hidden-focus': 'restructure',
|
|
122
|
+
'aria-required-parent': 'restructure',
|
|
123
|
+
};
|
|
124
|
+
export function getFixCategory(ruleId) {
|
|
125
|
+
return RULE_FIX_CATEGORY[ruleId];
|
|
126
|
+
}
|
package/dist/ai/prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getFix, getFixCategory } from './fallback-fix.js';
|
|
1
2
|
const FRAMEWORK_SYNTAX = {
|
|
2
3
|
'Next.js': 'React/TSX (JSX)',
|
|
3
4
|
'Gatsby': 'React/JSX',
|
|
@@ -12,22 +13,28 @@ const FRAMEWORK_SYNTAX = {
|
|
|
12
13
|
export function buildPrompt(groups, framework) {
|
|
13
14
|
const syntax = framework ? (FRAMEWORK_SYNTAX[framework] ?? 'JSX/TSX') : null;
|
|
14
15
|
const items = groups
|
|
15
|
-
.map((g, i) =>
|
|
16
|
+
.map((g, i) => {
|
|
17
|
+
const fixHint = getFix(g.ruleId);
|
|
18
|
+
const category = getFixCategory(g.ruleId) ?? 'edit-element';
|
|
19
|
+
const fixLine = fixHint ? `\n Fix guidance: ${fixHint}` : '';
|
|
20
|
+
const categoryLine = `\n Fix type: ${category}`;
|
|
21
|
+
return `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
|
|
16
22
|
Page: ${g.page}
|
|
17
23
|
Instances: ${g.count} element(s)
|
|
18
24
|
Selectors: ${g.selectors.join(', ')}
|
|
19
25
|
Representative Element: ${g.representative.html}
|
|
20
|
-
Problem: ${g.description}
|
|
26
|
+
Problem: ${g.description}${categoryLine}${fixLine}`;
|
|
27
|
+
})
|
|
21
28
|
.join('\n\n');
|
|
22
29
|
const frameworkLine = framework
|
|
23
30
|
? `\nFramework: ${framework}. All "fixedCode" values must use ${syntax} syntax — not raw HTML.\n`
|
|
24
31
|
: '';
|
|
25
32
|
const fixedCodeInstruction = syntax
|
|
26
|
-
? `"fixedCode": the
|
|
27
|
-
: `"fixedCode": the corrected HTML snippet
|
|
33
|
+
? `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected ${syntax} snippet — NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`
|
|
34
|
+
: `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected HTML snippet — NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`;
|
|
28
35
|
const optimalPromptInstruction = framework
|
|
29
|
-
? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2)
|
|
30
|
-
: `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2)
|
|
36
|
+
? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required in ${syntax} syntax — include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be actionable without requiring additional research.`
|
|
37
|
+
: `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required — include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be fully actionable: tell the developer exactly what to write, not just that something needs fixing.`;
|
|
31
38
|
return `You are a WCAG accessibility expert. Analyze these violations and return a JSON array.${frameworkLine}
|
|
32
39
|
Each item must have:
|
|
33
40
|
- "ruleId": the rule id from the input
|
package/dist/cli.js
CHANGED
|
@@ -13,13 +13,14 @@ 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.4.
|
|
16
|
+
.version('0.4.2');
|
|
17
17
|
program
|
|
18
18
|
.command('init')
|
|
19
19
|
.description('Create a11y.config.json in the current directory')
|
|
20
20
|
.option('--provider <name>', 'AI provider to configure (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)', 'gemini')
|
|
21
|
+
.option('--framework <name>', 'Your project framework — saves to config so every run uses it automatically (e.g. next, react, vue, angular, svelte, astro)')
|
|
21
22
|
.action((opts) => {
|
|
22
|
-
initConfig(opts.provider);
|
|
23
|
+
initConfig(opts.provider, opts.framework);
|
|
23
24
|
});
|
|
24
25
|
program
|
|
25
26
|
.command('scan')
|
|
@@ -35,10 +36,11 @@ program
|
|
|
35
36
|
.option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
|
|
36
37
|
.option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
|
|
37
38
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
39
|
+
.option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
|
|
38
40
|
.action(async (opts) => {
|
|
39
41
|
try {
|
|
40
42
|
console.log(`\nScanning ${opts.url}...`);
|
|
41
|
-
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
|
|
43
|
+
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework });
|
|
42
44
|
if (opts.terminal && !opts.fastMode) {
|
|
43
45
|
printTerminalReport(result);
|
|
44
46
|
}
|
|
@@ -51,10 +53,11 @@ program
|
|
|
51
53
|
const provider = createAIProvider(config);
|
|
52
54
|
const allViolations = result.pages.flatMap((p) => p.violations);
|
|
53
55
|
const ruleGroups = groupViolations(allViolations, strategy);
|
|
56
|
+
const framework = result.framework ?? config.framework;
|
|
54
57
|
if (!opts.fastMode) {
|
|
55
58
|
console.log(`\nGenerating AI fixes for ${ruleGroups.length} rule groups (${allViolations.length} violations)...`);
|
|
56
59
|
}
|
|
57
|
-
const fixes = await provider.generateFixes(allViolations, strategy,
|
|
60
|
+
const fixes = await provider.generateFixes(allViolations, strategy, framework);
|
|
58
61
|
if (opts.terminal) {
|
|
59
62
|
printAIPrompts(fixes, { explain: opts.explain, fastMode: opts.fastMode });
|
|
60
63
|
}
|
|
@@ -83,6 +86,7 @@ program
|
|
|
83
86
|
.option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
|
|
84
87
|
.option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
|
|
85
88
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
89
|
+
.option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
|
|
86
90
|
.action(async (opts) => {
|
|
87
91
|
if (!opts.url && !opts.fromReport) {
|
|
88
92
|
console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
|
|
@@ -104,6 +108,7 @@ program
|
|
|
104
108
|
apply: opts.apply,
|
|
105
109
|
provider,
|
|
106
110
|
srcDir: resolve(process.cwd(), 'src'),
|
|
111
|
+
framework: opts.framework ?? config.framework,
|
|
107
112
|
});
|
|
108
113
|
}
|
|
109
114
|
catch (err) {
|
package/dist/config.js
CHANGED
|
@@ -82,13 +82,14 @@ const STARTER_CONFIGS = {
|
|
|
82
82
|
`Created ${CONFIG_FILE} — fill in your Azure OpenAI endpoint, deployment, and API key from https://portal.azure.com`,
|
|
83
83
|
],
|
|
84
84
|
};
|
|
85
|
-
export function initConfig(provider = 'gemini') {
|
|
85
|
+
export function initConfig(provider = 'gemini', framework) {
|
|
86
86
|
const configPath = join(process.cwd(), CONFIG_FILE);
|
|
87
87
|
if (existsSync(configPath)) {
|
|
88
88
|
console.log(`${CONFIG_FILE} already exists.`);
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
const [starter, message] = STARTER_CONFIGS[provider];
|
|
92
|
-
|
|
92
|
+
const config = framework ? { ...starter, framework } : starter;
|
|
93
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
93
94
|
console.log(message);
|
|
94
95
|
}
|
package/dist/crawler.js
CHANGED
package/dist/fixer.js
CHANGED
|
@@ -18,11 +18,12 @@ export async function runFix(opts) {
|
|
|
18
18
|
console.log(chalk.green('\nNo violations found in report.'));
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
+
framework = opts.framework;
|
|
21
22
|
console.log(`Found ${allViolations.length} violation(s) in report. Locating source files...\n`);
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
25
|
console.log(`\nScanning ${opts.url}...`);
|
|
25
|
-
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
|
|
26
|
+
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework });
|
|
26
27
|
framework = result.framework;
|
|
27
28
|
if (result.totalViolations === 0) {
|
|
28
29
|
console.log(chalk.green('\nNo violations found.'));
|
|
@@ -74,7 +74,11 @@ function buildFullReport(result, fixes) {
|
|
|
74
74
|
lines.push('');
|
|
75
75
|
}
|
|
76
76
|
if (fix) {
|
|
77
|
-
lines.push('**Why it matters:**', fix.explanation, ''
|
|
77
|
+
lines.push('**Why it matters:**', fix.explanation, '');
|
|
78
|
+
if ((!fix.fixCategory || fix.fixCategory === 'edit-element') && fix.fixedCode) {
|
|
79
|
+
lines.push('**Fixed code:**', '```html', fix.fixedCode, '```', '');
|
|
80
|
+
}
|
|
81
|
+
lines.push('**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
|
|
78
82
|
}
|
|
79
83
|
lines.push('---', '');
|
|
80
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wcag-a11y",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wcag",
|