@aishware/react-a11y 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 +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +280 -0
- package/dist/pretty.d.ts +3 -0
- package/dist/pretty.d.ts.map +1 -0
- package/dist/pretty.js +68 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aishwarya Sharma
|
|
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,46 @@
|
|
|
1
|
+
# @aishware/react-a11y
|
|
2
|
+
|
|
3
|
+
A **static accessibility scanner** for React and React Native. It analyzes your
|
|
4
|
+
TSX/JSX with the TypeScript compiler and reports WCAG 2.2 issues with
|
|
5
|
+
`file:line` locations — no browser, no rendering, no configuration.
|
|
6
|
+
|
|
7
|
+
The installed command is **`react-a11y`**.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# audit the current project (platform auto-detected from package.json)
|
|
11
|
+
npx @aishware/react-a11y .
|
|
12
|
+
|
|
13
|
+
# explicit platform
|
|
14
|
+
npx @aishware/react-a11y apps/mobile --platform native
|
|
15
|
+
|
|
16
|
+
# machine-readable output
|
|
17
|
+
npx @aishware/react-a11y . --format json
|
|
18
|
+
npx @aishware/react-a11y . --format sarif --output a11y.sarif # GitHub code scanning
|
|
19
|
+
|
|
20
|
+
# gate CI: exit 1 when serious or critical issues exist (default)
|
|
21
|
+
npx @aishware/react-a11y . --fail-on serious
|
|
22
|
+
|
|
23
|
+
# apply safe mechanical fixes (e.g. miscapitalized React Native accessibility props)
|
|
24
|
+
npx @aishware/react-a11y . --fix
|
|
25
|
+
|
|
26
|
+
# scan only files changed in git — fast PR checks
|
|
27
|
+
npx @aishware/react-a11y . --changed
|
|
28
|
+
|
|
29
|
+
# every rule with severity + WCAG mapping, or the coverage report
|
|
30
|
+
npx @aishware/react-a11y --list-rules
|
|
31
|
+
npx @aishware/react-a11y --coverage
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install globally to get the bare `react-a11y` command:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install -g @aishware/react-a11y
|
|
38
|
+
react-a11y .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Full documentation, rule list and configuration:
|
|
42
|
+
<https://github.com/1aishwaryasharma/react-a11y>
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { MANUAL_CHECKS, SEVERITY_ORDER, WCAG, WCAG22_A_AA, WCAG22_TOTALS, applyFixes, detectPlatform, loadConfig, readOwnPackageMeta, scanProject, toJson, toSarif, } from '@aishware/react-a11y-core';
|
|
7
|
+
import { webProjectPasses, webRules, JSX_A11Y_COVERED_WCAG } from '@aishware/react-a11y-rules-web';
|
|
8
|
+
import { nativeRules } from '@aishware/react-a11y-rules-native';
|
|
9
|
+
import { printPretty } from './pretty.js';
|
|
10
|
+
const PKG = readOwnPackageMeta(import.meta.url);
|
|
11
|
+
const VERSION = PKG.version ?? '0.0.0';
|
|
12
|
+
const HELP = `
|
|
13
|
+
${pc.bold('react-a11y')} — accessibility scanner for React, Next.js and React Native
|
|
14
|
+
|
|
15
|
+
${pc.bold('Usage')}
|
|
16
|
+
react-a11y [path] [options]
|
|
17
|
+
|
|
18
|
+
${pc.bold('Options')}
|
|
19
|
+
--platform <web|native|auto> Rule pack to run (default: auto-detect from package.json)
|
|
20
|
+
--format <pretty|json|sarif> Output format (default: pretty)
|
|
21
|
+
--output <file> Write report to a file instead of stdout
|
|
22
|
+
--fail-on <severity|none> Exit 1 when issues at/above this severity exist (default: serious)
|
|
23
|
+
--fix Apply safe mechanical fixes, then report what remains
|
|
24
|
+
--changed Scan only files changed in git (vs HEAD, incl. untracked)
|
|
25
|
+
--list-rules Print every rule with severity and WCAG mapping (🔧 = fixable)
|
|
26
|
+
--coverage Show which WCAG 2.2 success criteria the rules cover
|
|
27
|
+
--version Print version
|
|
28
|
+
--help Show this help
|
|
29
|
+
|
|
30
|
+
${pc.bold('Web a11y')}
|
|
31
|
+
The web pack runs only what eslint-plugin-jsx-a11y does NOT cover (WCAG 2.2
|
|
32
|
+
criteria, structure, focus visibility, project-wide checks). Run jsx-a11y in
|
|
33
|
+
your ESLint config for standard web a11y; react-a11y covers the gaps and the
|
|
34
|
+
React Native + conformance story.
|
|
35
|
+
|
|
36
|
+
${pc.bold('Config')}
|
|
37
|
+
react-a11y.config.json / .react-a11yrc.json / package.json "react-a11y" key:
|
|
38
|
+
{ "platform": "web", "ignore": ["**/*.stories.tsx"], "rules": { "no-autofocus": "off" } }
|
|
39
|
+
`;
|
|
40
|
+
function fail(msg) {
|
|
41
|
+
console.error(pc.red(`error: ${msg}`));
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const args = {
|
|
46
|
+
root: process.cwd(), platform: 'auto', format: 'pretty', failOn: 'serious',
|
|
47
|
+
listRules: false, coverage: false, fix: false, changed: false,
|
|
48
|
+
};
|
|
49
|
+
const paths = [];
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
const next = () => argv[++i] ?? fail(`${arg} requires a value`);
|
|
53
|
+
switch (arg) {
|
|
54
|
+
case '--help':
|
|
55
|
+
case '-h':
|
|
56
|
+
console.log(HELP);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
// eslint-disable-next-line no-fallthrough
|
|
59
|
+
case '--version':
|
|
60
|
+
case '-v':
|
|
61
|
+
console.log(VERSION);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
case '--platform': {
|
|
64
|
+
const v = next();
|
|
65
|
+
if (v !== 'web' && v !== 'native' && v !== 'auto')
|
|
66
|
+
fail(`invalid platform "${v}"`);
|
|
67
|
+
args.platform = v;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case '--format': {
|
|
71
|
+
const v = next();
|
|
72
|
+
if (v !== 'pretty' && v !== 'json' && v !== 'sarif')
|
|
73
|
+
fail(`invalid format "${v}"`);
|
|
74
|
+
args.format = v;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case '--output':
|
|
78
|
+
case '-o':
|
|
79
|
+
args.output = next();
|
|
80
|
+
break;
|
|
81
|
+
case '--fail-on': {
|
|
82
|
+
const v = next();
|
|
83
|
+
if (v !== 'none' && !(v in SEVERITY_ORDER))
|
|
84
|
+
fail(`invalid severity "${v}"`);
|
|
85
|
+
args.failOn = v;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case '--list-rules':
|
|
89
|
+
args.listRules = true;
|
|
90
|
+
break;
|
|
91
|
+
case '--coverage':
|
|
92
|
+
args.coverage = true;
|
|
93
|
+
break;
|
|
94
|
+
case '--fix':
|
|
95
|
+
args.fix = true;
|
|
96
|
+
break;
|
|
97
|
+
case '--changed':
|
|
98
|
+
args.changed = true;
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
if (arg.startsWith('-'))
|
|
102
|
+
fail(`unknown option "${arg}" (try --help)`);
|
|
103
|
+
paths.push(arg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (paths.length > 0)
|
|
107
|
+
args.root = path.resolve(paths[0]);
|
|
108
|
+
return args;
|
|
109
|
+
}
|
|
110
|
+
function listRules() {
|
|
111
|
+
const print = (title, rules) => {
|
|
112
|
+
console.log(pc.bold(`\n${title}`));
|
|
113
|
+
for (const r of rules) {
|
|
114
|
+
const fixable = r.meta.fixable ? '🔧' : ' ';
|
|
115
|
+
console.log(` ${fixable} ${pc.cyan(r.meta.id.padEnd(34))} ${r.meta.severity.padEnd(9)} WCAG ${r.meta.wcag.join(', ').padEnd(16)} ${pc.dim(r.meta.description)}`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
print(`Web rules — complement eslint-plugin-jsx-a11y (${webRules.length})`, webRules);
|
|
119
|
+
print(`React Native rules (${nativeRules.length})`, nativeRules);
|
|
120
|
+
}
|
|
121
|
+
/** Files changed vs HEAD (staged, unstaged and untracked), for --changed. */
|
|
122
|
+
function changedFiles(root) {
|
|
123
|
+
let out;
|
|
124
|
+
try {
|
|
125
|
+
out = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
fail('--changed requires a git repository (git status failed)');
|
|
129
|
+
}
|
|
130
|
+
const files = [];
|
|
131
|
+
for (const line of out.split('\n')) {
|
|
132
|
+
if (!line.trim())
|
|
133
|
+
continue;
|
|
134
|
+
const status = line.slice(0, 2);
|
|
135
|
+
if (status.includes('D'))
|
|
136
|
+
continue; // deleted files have nothing to scan
|
|
137
|
+
let file = line.slice(3);
|
|
138
|
+
const renameArrow = file.indexOf(' -> ');
|
|
139
|
+
if (renameArrow !== -1)
|
|
140
|
+
file = file.slice(renameArrow + 4);
|
|
141
|
+
files.push(file.replace(/^"|"$/g, ''));
|
|
142
|
+
}
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
/** Apply autofixes to disk; returns counts. Fixes come back from a scan. */
|
|
146
|
+
function applyFixesToDisk(result) {
|
|
147
|
+
const byFile = new Map();
|
|
148
|
+
for (const d of result.diagnostics) {
|
|
149
|
+
if (!d.fix)
|
|
150
|
+
continue;
|
|
151
|
+
const list = byFile.get(d.file) ?? [];
|
|
152
|
+
list.push(d.fix);
|
|
153
|
+
byFile.set(d.file, list);
|
|
154
|
+
}
|
|
155
|
+
let fixed = 0;
|
|
156
|
+
for (const [file, fixes] of byFile) {
|
|
157
|
+
const full = path.join(result.root, file);
|
|
158
|
+
const source = fs.readFileSync(full, 'utf8');
|
|
159
|
+
const { output, applied } = applyFixes(source, fixes);
|
|
160
|
+
if (applied > 0) {
|
|
161
|
+
fs.writeFileSync(full, output);
|
|
162
|
+
fixed += applied;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { fixed, files: byFile.size };
|
|
166
|
+
}
|
|
167
|
+
function printCoverage() {
|
|
168
|
+
const covered = new Map();
|
|
169
|
+
const count = (rules, platform) => {
|
|
170
|
+
for (const rule of rules) {
|
|
171
|
+
for (const sc of rule.meta.wcag) {
|
|
172
|
+
const entry = covered.get(sc) ?? { web: 0, native: 0, full: false };
|
|
173
|
+
entry[platform]++;
|
|
174
|
+
if (!rule.meta.partial)
|
|
175
|
+
entry.full = true;
|
|
176
|
+
covered.set(sc, entry);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
count(webRules, 'web');
|
|
181
|
+
count(nativeRules, 'native');
|
|
182
|
+
const refs = [...covered.keys()].map((sc) => WCAG[sc]).sort((a, b) => a.sc.localeCompare(b.sc, undefined, { numeric: true }));
|
|
183
|
+
console.log(pc.bold(`\nAutomated by react-a11y: WCAG 2.2 criteria with at least one rule (${webRules.length} web + ${nativeRules.length} native rules)\n`));
|
|
184
|
+
for (const ref of refs) {
|
|
185
|
+
const entry = covered.get(ref.sc);
|
|
186
|
+
const packs = [entry.web ? `${entry.web} web` : null, entry.native ? `${entry.native} native` : null]
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.join(', ');
|
|
189
|
+
const partialBadge = entry.full ? '' : pc.yellow(' ~partial');
|
|
190
|
+
const newBadge = ref.version === '2.2' ? pc.cyan(' [new in 2.2]') : '';
|
|
191
|
+
console.log(` ${ref.sc.padEnd(7)} ${ref.name.padEnd(46)} ${ref.level.padEnd(4)} ${pc.dim(packs)}${partialBadge}${newBadge}`);
|
|
192
|
+
}
|
|
193
|
+
// Criteria deferred to eslint-plugin-jsx-a11y (run it alongside react-a11y).
|
|
194
|
+
const deferred = JSX_A11Y_COVERED_WCAG.filter((sc) => !covered.has(sc) && WCAG22_A_AA.includes(sc));
|
|
195
|
+
console.log(pc.bold('\nCovered by eslint-plugin-jsx-a11y: run it in your ESLint config\n'));
|
|
196
|
+
for (const sc of deferred.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))) {
|
|
197
|
+
const ref = WCAG[sc];
|
|
198
|
+
console.log(` ${ref.sc.padEnd(7)} ${ref.name.padEnd(46)} ${ref.level}`);
|
|
199
|
+
}
|
|
200
|
+
const handled = new Set([...covered.keys(), ...deferred]);
|
|
201
|
+
const manual = WCAG22_A_AA.filter((sc) => !handled.has(sc));
|
|
202
|
+
console.log(pc.bold('\nManual: A+AA criteria static analysis cannot decide — guided checklist\n'));
|
|
203
|
+
for (const sc of manual) {
|
|
204
|
+
const ref = WCAG[sc];
|
|
205
|
+
console.log(` ${ref.sc.padEnd(7)} ${ref.name.padEnd(46)} ${ref.level}`);
|
|
206
|
+
console.log(pc.dim(` ${MANUAL_CHECKS[sc] ?? 'Verify manually.'}`));
|
|
207
|
+
}
|
|
208
|
+
const byLevel = { A: 0, AA: 0, AAA: 0 };
|
|
209
|
+
for (const ref of refs)
|
|
210
|
+
byLevel[ref.level]++;
|
|
211
|
+
const pct = (n, total) => `${Math.round((n / total) * 100)}%`;
|
|
212
|
+
const aPlusAa = byLevel.A + byLevel.AA;
|
|
213
|
+
const aPlusAaTotal = WCAG22_TOTALS.A + WCAG22_TOTALS.AA;
|
|
214
|
+
const withJsx = WCAG22_A_AA.filter((sc) => handled.has(sc)).length;
|
|
215
|
+
console.log(pc.bold('\nCoverage (Level A + AA)'));
|
|
216
|
+
console.log(` Automated by react-a11y ${aPlusAa}/${aPlusAaTotal} (${pct(aPlusAa, aPlusAaTotal)})`);
|
|
217
|
+
console.log(` + eslint-plugin-jsx-a11y ${withJsx}/${aPlusAaTotal} (${pct(withJsx, aPlusAaTotal)}) ← when run alongside`);
|
|
218
|
+
console.log(pc.yellow(` Manual review needed ${manual.length}/${aPlusAaTotal} ← no static tool can decide these`));
|
|
219
|
+
console.log(pc.dim('\n react-a11y covers the WCAG 2.2 / structure / RN / project-wide criteria;\n' +
|
|
220
|
+
' eslint-plugin-jsx-a11y covers standard web a11y. The remaining criteria\n' +
|
|
221
|
+
' are not automatable — each has a guided manual check listed above, but a\n' +
|
|
222
|
+
` human must verify it. Run both linters and work the ${manual.length} manual items.`));
|
|
223
|
+
}
|
|
224
|
+
function main() {
|
|
225
|
+
const args = parseArgs(process.argv.slice(2));
|
|
226
|
+
if (args.listRules) {
|
|
227
|
+
listRules();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (args.coverage) {
|
|
231
|
+
printCoverage();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!fs.existsSync(args.root))
|
|
235
|
+
fail(`path does not exist: ${args.root}`);
|
|
236
|
+
const config = loadConfig(args.root);
|
|
237
|
+
const platform = args.platform !== 'auto' ? args.platform : config.platform ?? detectPlatform(args.root);
|
|
238
|
+
const rules = platform === 'native' ? nativeRules : webRules;
|
|
239
|
+
const files = args.changed ? changedFiles(args.root) : undefined;
|
|
240
|
+
// Cross-file passes need the whole project; skip them on partial scans.
|
|
241
|
+
// Passes are stateful, so each scan gets fresh instances.
|
|
242
|
+
const makePasses = () => platform === 'web' && !args.changed ? webProjectPasses(config) : [];
|
|
243
|
+
const scan = () => scanProject({ root: args.root, rules, platform, config, projectPasses: makePasses(), files });
|
|
244
|
+
let result = scan();
|
|
245
|
+
if (args.fix) {
|
|
246
|
+
const { fixed, files: fixedFiles } = applyFixesToDisk(result);
|
|
247
|
+
if (fixed > 0) {
|
|
248
|
+
console.error(pc.green(`✔ fixed ${fixed} issue${fixed === 1 ? '' : 's'} in ${fixedFiles} file${fixedFiles === 1 ? '' : 's'}`));
|
|
249
|
+
result = scan(); // report what remains after the rewrite
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(pc.dim('no autofixable issues found'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let report = null;
|
|
256
|
+
if (args.format === 'json')
|
|
257
|
+
report = toJson(result);
|
|
258
|
+
else if (args.format === 'sarif') {
|
|
259
|
+
report = toSarif(result, rules, { name: PKG.name, version: PKG.version, informationUri: PKG.homepage });
|
|
260
|
+
}
|
|
261
|
+
if (report !== null) {
|
|
262
|
+
if (args.output) {
|
|
263
|
+
fs.writeFileSync(args.output, report);
|
|
264
|
+
console.error(`report written to ${args.output}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(report);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
printPretty(result, VERSION);
|
|
272
|
+
}
|
|
273
|
+
if (args.failOn !== 'none') {
|
|
274
|
+
const threshold = SEVERITY_ORDER[args.failOn];
|
|
275
|
+
if (result.diagnostics.some((d) => SEVERITY_ORDER[d.severity] >= threshold)) {
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
main();
|
package/dist/pretty.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretty.d.ts","sourceRoot":"","sources":["../src/pretty.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,UAAU,EAAY,MAAM,2BAA2B,CAAC;AAsBlF,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAkDrE"}
|
package/dist/pretty.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { SEVERITY_ORDER } from '@aishware/react-a11y-core';
|
|
5
|
+
const SEVERITY_COLOR = {
|
|
6
|
+
critical: (s) => pc.bold(pc.red(s)),
|
|
7
|
+
serious: (s) => pc.red(s),
|
|
8
|
+
moderate: (s) => pc.yellow(s),
|
|
9
|
+
minor: (s) => pc.blue(s),
|
|
10
|
+
};
|
|
11
|
+
function snippet(root, diag) {
|
|
12
|
+
try {
|
|
13
|
+
const lines = fs.readFileSync(path.join(root, diag.file), 'utf8').split('\n');
|
|
14
|
+
const line = lines[diag.line - 1];
|
|
15
|
+
if (line === undefined)
|
|
16
|
+
return null;
|
|
17
|
+
const trimmed = line.length > 120 ? `${line.slice(0, 117)}...` : line;
|
|
18
|
+
return `${pc.dim(`${String(diag.line).padStart(5)} |`)} ${trimmed}`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function printPretty(result, version) {
|
|
25
|
+
const { diagnostics } = result;
|
|
26
|
+
const out = [];
|
|
27
|
+
out.push(`${pc.bold('react-a11y')} ${pc.dim(`v${version}`)} — platform: ${pc.cyan(result.platform)} — ` +
|
|
28
|
+
`${result.filesScanned} files scanned in ${result.durationMs}ms`);
|
|
29
|
+
out.push('');
|
|
30
|
+
if (diagnostics.length === 0) {
|
|
31
|
+
out.push(pc.green('✔ No accessibility issues found.'));
|
|
32
|
+
console.log(out.join('\n'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const byFile = new Map();
|
|
36
|
+
for (const d of diagnostics) {
|
|
37
|
+
const list = byFile.get(d.file) ?? [];
|
|
38
|
+
list.push(d);
|
|
39
|
+
byFile.set(d.file, list);
|
|
40
|
+
}
|
|
41
|
+
for (const [file, diags] of byFile) {
|
|
42
|
+
out.push(pc.bold(pc.underline(file)));
|
|
43
|
+
for (const d of diags) {
|
|
44
|
+
const pos = pc.dim(`${d.line}:${d.column}`.padEnd(8));
|
|
45
|
+
const sev = SEVERITY_COLOR[d.severity](d.severity.padEnd(8));
|
|
46
|
+
out.push(` ${pos} ${sev} ${pc.cyan(d.ruleId)}`);
|
|
47
|
+
out.push(` ${d.message}`);
|
|
48
|
+
const snip = snippet(result.root, d);
|
|
49
|
+
if (snip)
|
|
50
|
+
out.push(` ${snip}`);
|
|
51
|
+
const wcag = d.wcag
|
|
52
|
+
.map((w) => `${w.sc} ${w.name} (Level ${w.level}${w.version === '2.2' ? ', new in WCAG 2.2' : ''})`)
|
|
53
|
+
.join('; ');
|
|
54
|
+
out.push(` ${pc.dim(`WCAG ${wcag}`)}`);
|
|
55
|
+
}
|
|
56
|
+
out.push('');
|
|
57
|
+
}
|
|
58
|
+
const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
59
|
+
for (const d of diagnostics)
|
|
60
|
+
counts[d.severity]++;
|
|
61
|
+
const parts = Object.keys(SEVERITY_ORDER)
|
|
62
|
+
.sort((a, b) => SEVERITY_ORDER[b] - SEVERITY_ORDER[a])
|
|
63
|
+
.filter((s) => counts[s] > 0)
|
|
64
|
+
.map((s) => SEVERITY_COLOR[s](`${counts[s]} ${s}`));
|
|
65
|
+
out.push(pc.bold(`✖ ${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}`) +
|
|
66
|
+
` (${parts.join(', ')}) in ${byFile.size} file${byFile.size === 1 ? '' : 's'}`);
|
|
67
|
+
console.log(out.join('\n'));
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aishware/react-a11y",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Static accessibility scanner for React and React Native — reports WCAG 2.2 issues with file:line locations.",
|
|
5
|
+
"keywords": ["accessibility", "a11y", "wcag", "wcag-2.2", "react", "react-native", "expo", "linter", "cli", "sarif"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"react-a11y": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aishware/react-a11y-core": "^0.1.0",
|
|
24
|
+
"@aishware/react-a11y-rules-native": "^0.1.0",
|
|
25
|
+
"@aishware/react-a11y-rules-web": "^0.1.0",
|
|
26
|
+
"picocolors": "^1.1.0"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"homepage": "https://github.com/1aishwaryasharma/react-a11y",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/1aishwaryasharma/react-a11y.git",
|
|
33
|
+
"directory": "packages/cli"
|
|
34
|
+
}
|
|
35
|
+
}
|