react-native-accessibility-scanner 0.1.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/LICENSE +21 -0
- package/README.md +501 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +131 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/eslint/index.d.ts +59 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +181 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/reporters/console-reporter.d.ts +5 -0
- package/dist/reporters/console-reporter.d.ts.map +1 -0
- package/dist/reporters/console-reporter.js +67 -0
- package/dist/reporters/console-reporter.js.map +1 -0
- package/dist/reporters/json-reporter.d.ts +5 -0
- package/dist/reporters/json-reporter.d.ts.map +1 -0
- package/dist/reporters/json-reporter.js +13 -0
- package/dist/reporters/json-reporter.js.map +1 -0
- package/dist/reporters/markdown-reporter.d.ts +5 -0
- package/dist/reporters/markdown-reporter.d.ts.map +1 -0
- package/dist/reporters/markdown-reporter.js +66 -0
- package/dist/reporters/markdown-reporter.js.map +1 -0
- package/dist/rules/duplicate-labels.d.ts +25 -0
- package/dist/rules/duplicate-labels.d.ts.map +1 -0
- package/dist/rules/duplicate-labels.js +107 -0
- package/dist/rules/duplicate-labels.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +38 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/missing-hint.d.ts +14 -0
- package/dist/rules/missing-hint.d.ts.map +1 -0
- package/dist/rules/missing-hint.js +94 -0
- package/dist/rules/missing-hint.js.map +1 -0
- package/dist/rules/missing-label.d.ts +10 -0
- package/dist/rules/missing-label.d.ts.map +1 -0
- package/dist/rules/missing-label.js +75 -0
- package/dist/rules/missing-label.js.map +1 -0
- package/dist/rules/missing-role.d.ts +10 -0
- package/dist/rules/missing-role.d.ts.map +1 -0
- package/dist/rules/missing-role.js +82 -0
- package/dist/rules/missing-role.js.map +1 -0
- package/dist/rules/small-touch-target.d.ts +10 -0
- package/dist/rules/small-touch-target.d.ts.map +1 -0
- package/dist/rules/small-touch-target.js +90 -0
- package/dist/rules/small-touch-target.js.map +1 -0
- package/dist/rules/touchable-without-label.d.ts +17 -0
- package/dist/rules/touchable-without-label.d.ts.map +1 -0
- package/dist/rules/touchable-without-label.js +81 -0
- package/dist/rules/touchable-without-label.js.map +1 -0
- package/dist/scanner/config-loader.d.ts +3 -0
- package/dist/scanner/config-loader.d.ts.map +1 -0
- package/dist/scanner/config-loader.js +46 -0
- package/dist/scanner/config-loader.js.map +1 -0
- package/dist/scanner/file-scanner.d.ts +6 -0
- package/dist/scanner/file-scanner.d.ts.map +1 -0
- package/dist/scanner/file-scanner.js +82 -0
- package/dist/scanner/file-scanner.js.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +64 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/scorer.d.ts +26 -0
- package/dist/scanner/scorer.d.ts.map +1 -0
- package/dist/scanner/scorer.js +65 -0
- package/dist/scanner/scorer.js.map +1 -0
- package/dist/types/index.d.ts +88 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ast.d.ts +24 -0
- package/dist/utils/ast.d.ts.map +1 -0
- package/dist/utils/ast.js +170 -0
- package/dist/utils/ast.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# ♿ react-native-accessibility-scanner
|
|
2
|
+
|
|
3
|
+
> **Accessibility auditing for React Native apps.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/react-native-accessibility-scanner)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
A single tool that scans your React Native codebase for accessibility issues and generates actionable reports — before your users encounter them.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx react-native-accessibility-scan ./src
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
═══════════════════════════════════════════════════════
|
|
17
|
+
React Native Accessibility Scanner
|
|
18
|
+
═══════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
📊 Summary
|
|
21
|
+
Scanned : 12 files
|
|
22
|
+
Issues : 7 total
|
|
23
|
+
■ HIGH 2 ■ MEDIUM 3 ■ LOW 2
|
|
24
|
+
|
|
25
|
+
HIGH 2 issues
|
|
26
|
+
────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
Missing accessibilityLabel
|
|
29
|
+
src/screens/HomeScreen.tsx:43:5
|
|
30
|
+
<TouchableOpacity> is interactive but has no accessibilityLabel
|
|
31
|
+
Fix: Add accessibilityLabel="Describe the action"
|
|
32
|
+
|
|
33
|
+
♿ Accessibility Readiness Score
|
|
34
|
+
|
|
35
|
+
Overall 🟡 72/100 ████████████████░░░░
|
|
36
|
+
─────────────────────────────────────────────────
|
|
37
|
+
Labels 🔴 40/100
|
|
38
|
+
Roles 🟡 75/100
|
|
39
|
+
Touch Targets 🟢 100/100
|
|
40
|
+
Hints 🟢 94/100
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Why This Exists
|
|
46
|
+
|
|
47
|
+
React Native has no single dedicated accessibility linting tool. Existing solutions are either:
|
|
48
|
+
|
|
49
|
+
- **Web-focused** (not aware of RN-specific components like `Pressable`, `FlatList`, `Modal`)
|
|
50
|
+
- **Runtime-only** (can't catch issues at build time)
|
|
51
|
+
- **Fragmented** (separate tools for linting, reporting, CI)
|
|
52
|
+
|
|
53
|
+
<!-- This package gives you one unified tool with a CLI, ESLint plugin, GitHub Action, and programmatic API. -->
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Table of Contents
|
|
58
|
+
|
|
59
|
+
- [Installation](#installation)
|
|
60
|
+
- [Quick Start](#quick-start)
|
|
61
|
+
- [CLI Reference](#cli-reference)
|
|
62
|
+
- [Rules](#rules)
|
|
63
|
+
- [Accessibility Score](#accessibility-score)
|
|
64
|
+
<!-- - [ESLint Plugin](#eslint-plugin) -->
|
|
65
|
+
<!-- - [GitHub Action](#github-action) -->
|
|
66
|
+
<!-- - [Programmatic API](#programmatic-api) -->
|
|
67
|
+
<!-- - [Config File](#config-file) -->
|
|
68
|
+
<!-- - [Writing Custom Rules](#writing-custom-rules) -->
|
|
69
|
+
- [Roadmap](#roadmap)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# npm
|
|
77
|
+
npm install --save-dev react-native-accessibility-scanner
|
|
78
|
+
|
|
79
|
+
# yarn
|
|
80
|
+
yarn add --dev react-native-accessibility-scanner
|
|
81
|
+
|
|
82
|
+
# No install needed for one-off scans:
|
|
83
|
+
npx react-native-accessibility-scan ./src
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Scan your src directory
|
|
92
|
+
npx react-native-accessibility-scan ./src
|
|
93
|
+
|
|
94
|
+
# Fail CI on high severity issues
|
|
95
|
+
npx react-native-accessibility-scan ./src --fail-on-high
|
|
96
|
+
|
|
97
|
+
# Output JSON report
|
|
98
|
+
npx react-native-accessibility-scan ./src --json > report.json
|
|
99
|
+
|
|
100
|
+
# Save Markdown report
|
|
101
|
+
npx react-native-accessibility-scan ./src --output accessibility-report.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## CLI Reference
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Usage: react-native-accessibility-scan [path] [options]
|
|
110
|
+
|
|
111
|
+
Arguments:
|
|
112
|
+
path Path to scan (default: ./src)
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--json Output results as JSON to stdout
|
|
116
|
+
--markdown Output results as Markdown to stdout
|
|
117
|
+
--output <file> Write report to file (.json or .md)
|
|
118
|
+
--fail-on-high Exit 1 if HIGH severity issues found
|
|
119
|
+
--fail-on-medium Exit 1 if MEDIUM or higher issues found
|
|
120
|
+
--ignore <patterns...> Glob patterns to ignore
|
|
121
|
+
--disable-rules <ids> Rule IDs to disable
|
|
122
|
+
--min-width <n> Minimum touch target width (default: 44)
|
|
123
|
+
--min-height <n> Minimum touch target height (default: 44)
|
|
124
|
+
--score Show accessibility score breakdown
|
|
125
|
+
-V, --version Output version number
|
|
126
|
+
-h, --help Display help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Rules
|
|
132
|
+
|
|
133
|
+
| Rule ID | Severity | Description |
|
|
134
|
+
| ------------------------- | --------- | ----------------------------------------------------------------- |
|
|
135
|
+
| `missing-label` | 🔴 HIGH | Interactive element missing `accessibilityLabel` |
|
|
136
|
+
| `touchable-without-label` | 🔴 HIGH | Touchable with no label and no `Text` child |
|
|
137
|
+
| `missing-role` | 🟡 MEDIUM | Interactive element missing `accessibilityRole` |
|
|
138
|
+
| `small-touch-target` | 🟡 MEDIUM | Touch target below 44×44pt recommendation |
|
|
139
|
+
| `missing-hint` | 🔵 LOW | Critical action (delete/checkout/pay) missing `accessibilityHint` |
|
|
140
|
+
| `duplicate-labels` | 🔵 LOW | Multiple elements share the same `accessibilityLabel` |
|
|
141
|
+
|
|
142
|
+
### Rule Details
|
|
143
|
+
|
|
144
|
+
#### `missing-label` 🔴
|
|
145
|
+
|
|
146
|
+
Detects interactive elements (`TouchableOpacity`, `Pressable`, `Button`, etc.) that have no `accessibilityLabel` and no `Text` child. Screen readers will announce these as "button" with no context.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// ❌ Bad
|
|
150
|
+
<TouchableOpacity onPress={handleSearch} />
|
|
151
|
+
|
|
152
|
+
// ✅ Good
|
|
153
|
+
<TouchableOpacity
|
|
154
|
+
accessibilityLabel="Search"
|
|
155
|
+
onPress={handleSearch}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
// ✅ Also good — Text child acts as label
|
|
159
|
+
<TouchableOpacity onPress={handleSearch}>
|
|
160
|
+
<Text>Search</Text>
|
|
161
|
+
</TouchableOpacity>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `missing-role` 🟡
|
|
165
|
+
|
|
166
|
+
Interactive elements without `accessibilityRole` may be announced incorrectly by VoiceOver/TalkBack.
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// ❌ Bad
|
|
170
|
+
<TouchableOpacity accessibilityLabel="Search" onPress={...} />
|
|
171
|
+
|
|
172
|
+
// ✅ Good
|
|
173
|
+
<TouchableOpacity
|
|
174
|
+
accessibilityLabel="Search"
|
|
175
|
+
accessibilityRole="button"
|
|
176
|
+
onPress={...}
|
|
177
|
+
/>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### `small-touch-target` 🟡
|
|
181
|
+
|
|
182
|
+
Targets smaller than 44×44pt (iOS) or 48×48dp (Android) are hard to activate for users with motor impairments.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// ❌ Bad
|
|
186
|
+
<TouchableOpacity style={{ width: 20, height: 20 }} />
|
|
187
|
+
|
|
188
|
+
// ✅ Good
|
|
189
|
+
<TouchableOpacity style={{ width: 44, height: 44 }} />
|
|
190
|
+
// Or use padding:
|
|
191
|
+
<TouchableOpacity style={{ padding: 12 }} />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `missing-hint` 🔵
|
|
195
|
+
|
|
196
|
+
Critical actions (containing words like "delete", "checkout", "purchase", "pay") should explain what will happen.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// ❌ Bad — user doesn't know what "Delete" will do
|
|
200
|
+
<TouchableOpacity accessibilityLabel="Delete account" />
|
|
201
|
+
|
|
202
|
+
// ✅ Good
|
|
203
|
+
<TouchableOpacity
|
|
204
|
+
accessibilityLabel="Delete account"
|
|
205
|
+
accessibilityHint="Permanently removes your account and all data"
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `duplicate-labels` 🔵
|
|
210
|
+
|
|
211
|
+
When two interactive elements on the same screen share a label, screen reader users cannot distinguish them.
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// ❌ Bad — which "Edit" is which?
|
|
215
|
+
<TouchableOpacity accessibilityLabel="Edit" />
|
|
216
|
+
<TouchableOpacity accessibilityLabel="Edit" />
|
|
217
|
+
|
|
218
|
+
// ✅ Good
|
|
219
|
+
<TouchableOpacity accessibilityLabel="Edit profile" />
|
|
220
|
+
<TouchableOpacity accessibilityLabel="Edit address" />
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Accessibility Score
|
|
226
|
+
|
|
227
|
+
The scanner computes an **Accessibility Readiness Score** — a 0–100 rating for each file and overall.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npx react-native-accessibility-scan ./src --score
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
♿ Accessibility Readiness Score
|
|
235
|
+
|
|
236
|
+
Overall 🟡 72/100 ████████████████░░░░
|
|
237
|
+
─────────────────────────────────────────────────
|
|
238
|
+
Labels 🔴 40/100
|
|
239
|
+
Roles 🟡 75/100
|
|
240
|
+
Touch Targets 🟢 100/100
|
|
241
|
+
Hints 🟢 94/100
|
|
242
|
+
|
|
243
|
+
📱 Screen Scores
|
|
244
|
+
|
|
245
|
+
🔴 CheckoutScreen.tsx 42/100 (5 issues)
|
|
246
|
+
🟡 HomeScreen.tsx 68/100 (3 issues)
|
|
247
|
+
🟢 ProfileScreen.tsx 96/100 (1 issue)
|
|
248
|
+
🟢 SettingsScreen.tsx 100/100 (0 issues)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Scoring model:**
|
|
252
|
+
|
|
253
|
+
- HIGH issue: −20 points per issue
|
|
254
|
+
- MEDIUM issue: −8 points per issue
|
|
255
|
+
- LOW issue: −3 points per issue
|
|
256
|
+
- Score is clamped to [0, 100]
|
|
257
|
+
|
|
258
|
+
Use this in CI to enforce a minimum score:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const report = await AccessibilityScanner.scan({ path: "./src" });
|
|
262
|
+
const score = computeScore(report);
|
|
263
|
+
|
|
264
|
+
if (score.overall < 80) {
|
|
265
|
+
console.error(`Score ${score.overall} is below required 80`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
<!-- ## ESLint Plugin
|
|
273
|
+
|
|
274
|
+
Get inline warnings while you write code. -->
|
|
275
|
+
|
|
276
|
+
### Setup
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npm install --save-dev react-native-accessibility-scanner
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// .eslintrc.js
|
|
284
|
+
module.exports = {
|
|
285
|
+
plugins: ["react-native-accessibility-scanner"],
|
|
286
|
+
rules: {
|
|
287
|
+
"react-native-accessibility-scanner/missing-label": "error",
|
|
288
|
+
"react-native-accessibility-scanner/missing-role": "warn",
|
|
289
|
+
"react-native-accessibility-scanner/small-touch-target": "warn",
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Or use the recommended preset:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
// .eslintrc.js
|
|
298
|
+
module.exports = {
|
|
299
|
+
extends: ["plugin:react-native-accessibility-scanner/recommended"],
|
|
300
|
+
};
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
<!-- ### Import path
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
// The ESLint plugin is a separate export
|
|
307
|
+
const plugin = require("react-native-accessibility-scanner/eslint");
|
|
308
|
+
``` -->
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
<!-- ## GitHub Action
|
|
313
|
+
|
|
314
|
+
Add to `.github/workflows/accessibility.yml` (copy from `github-action/accessibility.yml` in this repo):
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
name: Accessibility Scan
|
|
318
|
+
|
|
319
|
+
on:
|
|
320
|
+
push:
|
|
321
|
+
branches: [main]
|
|
322
|
+
pull_request:
|
|
323
|
+
branches: [main]
|
|
324
|
+
|
|
325
|
+
jobs:
|
|
326
|
+
accessibility:
|
|
327
|
+
runs-on: ubuntu-latest
|
|
328
|
+
steps:
|
|
329
|
+
- uses: actions/checkout@v4
|
|
330
|
+
- uses: actions/setup-node@v4
|
|
331
|
+
with:
|
|
332
|
+
node-version: "20"
|
|
333
|
+
|
|
334
|
+
- run: npm ci
|
|
335
|
+
|
|
336
|
+
- name: Run Accessibility Scanner
|
|
337
|
+
run: npx react-native-accessibility-scan ./src --fail-on-high --output accessibility-report.json
|
|
338
|
+
|
|
339
|
+
- name: Upload Report
|
|
340
|
+
if: always()
|
|
341
|
+
uses: actions/upload-artifact@v4
|
|
342
|
+
with:
|
|
343
|
+
name: accessibility-report
|
|
344
|
+
path: accessibility-report.json
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The Action will:
|
|
348
|
+
|
|
349
|
+
- Run on every push and pull request
|
|
350
|
+
- Post a Markdown summary as a PR comment
|
|
351
|
+
- Upload the JSON report as a build artifact
|
|
352
|
+
- Fail the build if HIGH severity issues are found
|
|
353
|
+
|
|
354
|
+
--- -->
|
|
355
|
+
|
|
356
|
+
<!-- ## Programmatic API
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import {
|
|
360
|
+
AccessibilityScanner,
|
|
361
|
+
computeScore,
|
|
362
|
+
ConsoleReporter,
|
|
363
|
+
} from "react-native-accessibility-scanner";
|
|
364
|
+
|
|
365
|
+
const report = await AccessibilityScanner.scan({
|
|
366
|
+
path: "./src",
|
|
367
|
+
ignore: ["**/*.stories.tsx"],
|
|
368
|
+
failOnHigh: false,
|
|
369
|
+
touchTarget: { minWidth: 44, minHeight: 44 },
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Print to console
|
|
373
|
+
new ConsoleReporter().report(report);
|
|
374
|
+
|
|
375
|
+
// Get score
|
|
376
|
+
const score = computeScore(report);
|
|
377
|
+
console.log(score.overall); // 72
|
|
378
|
+
|
|
379
|
+
// Access issues directly
|
|
380
|
+
report.issues.forEach((issue) => {
|
|
381
|
+
console.log(
|
|
382
|
+
`${issue.severity}: ${issue.message} (${issue.file}:${issue.line})`,
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### `ScanOptions`
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
interface ScanOptions {
|
|
391
|
+
path?: string | string[]; // default: "./src"
|
|
392
|
+
ignore?: string[]; // glob patterns
|
|
393
|
+
failOnHigh?: boolean; // default: false
|
|
394
|
+
failOnMedium?: boolean; // default: false
|
|
395
|
+
touchTarget?: {
|
|
396
|
+
minWidth?: number; // default: 44
|
|
397
|
+
minHeight?: number; // default: 44
|
|
398
|
+
};
|
|
399
|
+
disabledRules?: string[]; // rule IDs to skip
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
--- -->
|
|
404
|
+
|
|
405
|
+
<!-- ## Config File
|
|
406
|
+
|
|
407
|
+
Create `accessibility-scanner.config.js` in your project root:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
module.exports = {
|
|
411
|
+
path: "./src",
|
|
412
|
+
ignore: ["**/*.stories.tsx", "**/*.test.tsx"],
|
|
413
|
+
failOnHigh: true,
|
|
414
|
+
failOnMedium: false,
|
|
415
|
+
touchTarget: {
|
|
416
|
+
minWidth: 44,
|
|
417
|
+
minHeight: 44,
|
|
418
|
+
},
|
|
419
|
+
disabledRules: [],
|
|
420
|
+
};
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The scanner automatically detects this file. You can also use:
|
|
424
|
+
|
|
425
|
+
- `.accessibility-scannerrc`
|
|
426
|
+
- `.accessibility-scannerrc.json`
|
|
427
|
+
- `"accessibility-scanner"` key in `package.json`
|
|
428
|
+
|
|
429
|
+
--- -->
|
|
430
|
+
|
|
431
|
+
<!-- ## Writing Custom Rules
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { registerRule } from "react-native-accessibility-scanner";
|
|
435
|
+
import type {
|
|
436
|
+
AccessibilityRule,
|
|
437
|
+
AccessibilityIssue,
|
|
438
|
+
RuleContext,
|
|
439
|
+
} from "react-native-accessibility-scanner";
|
|
440
|
+
import * as t from "@babel/types";
|
|
441
|
+
|
|
442
|
+
class NoHideDescendantsRule implements AccessibilityRule {
|
|
443
|
+
id = "no-hide-descendants";
|
|
444
|
+
name = "No importantForAccessibility no-hide-descendants";
|
|
445
|
+
description = "Avoid hiding subtrees from screen readers.";
|
|
446
|
+
severity = "medium" as const;
|
|
447
|
+
|
|
448
|
+
run(node: t.Node, context: RuleContext): AccessibilityIssue[] {
|
|
449
|
+
// Your AST logic here
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Register before scanning
|
|
455
|
+
registerRule(new NoHideDescendantsRule());
|
|
456
|
+
|
|
457
|
+
const report = await AccessibilityScanner.scan({ path: "./src" });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
--- -->
|
|
461
|
+
|
|
462
|
+
## Roadmap
|
|
463
|
+
|
|
464
|
+
<!-- | Version | Feature |
|
|
465
|
+
|---------|---------|
|
|
466
|
+
| **v0.1** | ✅ CLI, 6 rules, JSON/Markdown reporters |
|
|
467
|
+
| **v0.2** | Touch target detection, duplicate labels, score |
|
|
468
|
+
| **v0.3** | ESLint plugin, config file support |
|
|
469
|
+
| **v0.4** | GitHub Action, PR comments |
|
|
470
|
+
| **v1.0** | Stable API, full docs, test coverage |
|
|
471
|
+
| v1.1 | Accessibility score per screen in CI |
|
|
472
|
+
| v1.2 | Auto-fix mode (`--fix`) |
|
|
473
|
+
| v1.3 | React Navigation screen title checks |
|
|
474
|
+
| v1.4 | Modal accessibility checks |
|
|
475
|
+
| v1.5 | FlatList / SectionList rules |
|
|
476
|
+
| v2.0 | VS Code extension |
|
|
477
|
+
| v3.0 | HTML dashboard |
|
|
478
|
+
| v4.0 | AI-powered label suggestions | -->
|
|
479
|
+
|
|
480
|
+
| Version | Feature |
|
|
481
|
+
| -------- | ---------------------------------------- |
|
|
482
|
+
| **v0.1** | ✅ CLI, 6 rules, JSON/Markdown reporters |
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Contributing
|
|
487
|
+
|
|
488
|
+
1. Fork the repository
|
|
489
|
+
2. Create a branch: `git checkout -b feat/my-rule`
|
|
490
|
+
3. Make your changes
|
|
491
|
+
4. Add tests in `tests/unit/`
|
|
492
|
+
5. Run `npm test` to verify
|
|
493
|
+
6. Submit a pull request
|
|
494
|
+
|
|
495
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## License
|
|
500
|
+
|
|
501
|
+
MIT © [Arthnex](https://github.com/arthnex-admin)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const scanner_1 = require("../scanner");
|
|
12
|
+
const console_reporter_1 = require("../reporters/console-reporter");
|
|
13
|
+
const json_reporter_1 = require("../reporters/json-reporter");
|
|
14
|
+
const markdown_reporter_1 = require("../reporters/markdown-reporter");
|
|
15
|
+
const scorer_1 = require("../scanner/scorer");
|
|
16
|
+
const program = new commander_1.Command();
|
|
17
|
+
program
|
|
18
|
+
.name("react-native-accessibility-scan")
|
|
19
|
+
.description("Accessibility auditing for React Native apps.")
|
|
20
|
+
.version("0.1.0")
|
|
21
|
+
.argument("[path]", "Path to scan (default: ./src)", "./src")
|
|
22
|
+
.option("--json", "Output results as JSON to stdout")
|
|
23
|
+
.option("--markdown", "Output results as Markdown to stdout")
|
|
24
|
+
.option("--output <file>", "Write report to a file (auto-detects format from extension)")
|
|
25
|
+
.option("--fail-on-high", "Exit with code 1 if any HIGH severity issues are found")
|
|
26
|
+
.option("--fail-on-medium", "Exit with code 1 if any MEDIUM or HIGH issues are found")
|
|
27
|
+
.option("--ignore <patterns...>", "Glob patterns to ignore (space-separated)")
|
|
28
|
+
.option("--disable-rules <rules...>", "Rule IDs to disable")
|
|
29
|
+
.option("--min-width <n>", "Minimum touch target width (default: 44)", "44")
|
|
30
|
+
.option("--min-height <n>", "Minimum touch target height (default: 44)", "44")
|
|
31
|
+
.option("--score", "Show accessibility score breakdown")
|
|
32
|
+
.action(async (scanPath, opts) => {
|
|
33
|
+
const options = {
|
|
34
|
+
path: scanPath,
|
|
35
|
+
failOnHigh: opts.failOnHigh ?? false,
|
|
36
|
+
failOnMedium: opts.failOnMedium ?? false,
|
|
37
|
+
ignore: opts.ignore,
|
|
38
|
+
disabledRules: opts.disableRules,
|
|
39
|
+
touchTarget: {
|
|
40
|
+
minWidth: parseInt(opts.minWidth, 10),
|
|
41
|
+
minHeight: parseInt(opts.minHeight, 10),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
let report;
|
|
45
|
+
try {
|
|
46
|
+
report = await scanner_1.AccessibilityScanner.scan(options);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
console.error(chalk_1.default.red(`\n✖ Scanner error: ${message}\n`));
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
// ── Output format ──────────────────────────────────────────────────────────
|
|
54
|
+
if (opts.json) {
|
|
55
|
+
const output = new json_reporter_1.JsonReporter().report(report);
|
|
56
|
+
console.log(output);
|
|
57
|
+
}
|
|
58
|
+
else if (opts.markdown) {
|
|
59
|
+
const output = new markdown_reporter_1.MarkdownReporter().report(report);
|
|
60
|
+
console.log(output);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
new console_reporter_1.ConsoleReporter().report(report);
|
|
64
|
+
}
|
|
65
|
+
// ── Score display ──────────────────────────────────────────────────────────
|
|
66
|
+
if (opts.score || (!opts.json && !opts.markdown)) {
|
|
67
|
+
const score = (0, scorer_1.computeScore)(report);
|
|
68
|
+
if (!opts.json && !opts.markdown) {
|
|
69
|
+
printScore(score);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Write to file ──────────────────────────────────────────────────────────
|
|
73
|
+
if (opts.output) {
|
|
74
|
+
const outPath = path_1.default.resolve(opts.output);
|
|
75
|
+
const ext = path_1.default.extname(outPath).toLowerCase();
|
|
76
|
+
let content;
|
|
77
|
+
if (ext === ".json") {
|
|
78
|
+
content = new json_reporter_1.JsonReporter().report(report);
|
|
79
|
+
}
|
|
80
|
+
else if (ext === ".md") {
|
|
81
|
+
content = new markdown_reporter_1.MarkdownReporter().report(report);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
content = new json_reporter_1.JsonReporter().report(report);
|
|
85
|
+
}
|
|
86
|
+
(0, fs_1.writeFileSync)(outPath, content, "utf-8");
|
|
87
|
+
console.log(chalk_1.default.dim(`\n📄 Report saved to ${outPath}`));
|
|
88
|
+
}
|
|
89
|
+
// ── Exit code ──────────────────────────────────────────────────────────────
|
|
90
|
+
const { summary } = report;
|
|
91
|
+
if (options.failOnHigh && summary.highIssues > 0) {
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (options.failOnMedium && (summary.highIssues > 0 || summary.mediumIssues > 0)) {
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
function printScore(score) {
|
|
99
|
+
console.log(chalk_1.default.bold("\n♿ Accessibility Readiness Score\n"));
|
|
100
|
+
const bar = makeBar(score.overall);
|
|
101
|
+
console.log(` Overall ${(0, scorer_1.scoreEmoji)(score.overall)} ${chalk_1.default.bold(score.overall)}/100 ${bar}`);
|
|
102
|
+
console.log(chalk_1.default.dim(" ─────────────────────────────────────────────────"));
|
|
103
|
+
console.log(` Labels ${(0, scorer_1.scoreEmoji)(score.breakdown.labels)} ${score.breakdown.labels}/100`);
|
|
104
|
+
console.log(` Roles ${(0, scorer_1.scoreEmoji)(score.breakdown.roles)} ${score.breakdown.roles}/100`);
|
|
105
|
+
console.log(` Touch Targets ${(0, scorer_1.scoreEmoji)(score.breakdown.touchTargets)} ${score.breakdown.touchTargets}/100`);
|
|
106
|
+
console.log(` Hints ${(0, scorer_1.scoreEmoji)(score.breakdown.hints)} ${score.breakdown.hints}/100`);
|
|
107
|
+
if (score.screens.length > 1) {
|
|
108
|
+
console.log(chalk_1.default.bold("\n📱 Screen Scores\n"));
|
|
109
|
+
const sorted = [...score.screens].sort((a, b) => a.score - b.score);
|
|
110
|
+
for (const s of sorted) {
|
|
111
|
+
const icon = (0, scorer_1.scoreEmoji)(s.score);
|
|
112
|
+
const name = s.displayName.padEnd(32, " ");
|
|
113
|
+
console.log(` ${icon} ${chalk_1.default.white(name)} ${chalk_1.default.bold(s.score)}/100 (${s.issueCount} issues)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
function makeBar(score) {
|
|
119
|
+
const filled = Math.round(score / 5);
|
|
120
|
+
const empty = 20 - filled;
|
|
121
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
122
|
+
if (score >= 90)
|
|
123
|
+
return chalk_1.default.green(bar);
|
|
124
|
+
if (score >= 70)
|
|
125
|
+
return chalk_1.default.yellow(bar);
|
|
126
|
+
if (score >= 50)
|
|
127
|
+
return chalk_1.default.hex("#FFA500")(bar);
|
|
128
|
+
return chalk_1.default.red(bar);
|
|
129
|
+
}
|
|
130
|
+
program.parse(process.argv);
|
|
131
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|