@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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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();
@@ -0,0 +1,3 @@
1
+ import type { ScanResult } from '@aishware/react-a11y-core';
2
+ export declare function printPretty(result: ScanResult, version: string): void;
3
+ //# sourceMappingURL=pretty.d.ts.map
@@ -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
+ }