@aishware/react-a11y-core 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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/aria.d.ts +4 -0
  4. package/dist/aria.d.ts.map +1 -0
  5. package/dist/aria.js +10 -0
  6. package/dist/color.d.ts +21 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +93 -0
  9. package/dist/config.d.ts +5 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +39 -0
  12. package/dist/element.d.ts +50 -0
  13. package/dist/element.d.ts.map +1 -0
  14. package/dist/element.js +162 -0
  15. package/dist/engine.d.ts +18 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +52 -0
  18. package/dist/fixes.d.ts +15 -0
  19. package/dist/fixes.d.ts.map +1 -0
  20. package/dist/fixes.js +32 -0
  21. package/dist/helpers.d.ts +74 -0
  22. package/dist/helpers.d.ts.map +1 -0
  23. package/dist/helpers.js +209 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +14 -0
  27. package/dist/parse.d.ts +3 -0
  28. package/dist/parse.d.ts.map +1 -0
  29. package/dist/parse.js +14 -0
  30. package/dist/pkg-meta.d.ts +21 -0
  31. package/dist/pkg-meta.d.ts.map +1 -0
  32. package/dist/pkg-meta.js +31 -0
  33. package/dist/reporters/json.d.ts +3 -0
  34. package/dist/reporters/json.d.ts.map +1 -0
  35. package/dist/reporters/json.js +9 -0
  36. package/dist/reporters/sarif.d.ts +9 -0
  37. package/dist/reporters/sarif.d.ts.map +1 -0
  38. package/dist/reporters/sarif.js +61 -0
  39. package/dist/scanner.d.ts +16 -0
  40. package/dist/scanner.d.ts.map +1 -0
  41. package/dist/scanner.js +113 -0
  42. package/dist/types.d.ts +109 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/wcag.d.ts +28 -0
  46. package/dist/wcag.d.ts.map +1 -0
  47. package/dist/wcag.js +117 -0
  48. package/package.json +36 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,wBAAgB,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE5E;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,+DAA+D;AAC/D,wBAAgB,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAGvG;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG9E;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAKxE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAU5D;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAM1D;AAID,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAGzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAa3F;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAW7D;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI,CAkBxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,WAAW,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAAG,IAAI,CAYpG;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,mBAAmB,GAAG,IAAI,CAKtG;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAYzF"}
@@ -0,0 +1,209 @@
1
+ import ts from 'typescript';
2
+ import { contrastRatio, isLargeText, parseColor } from './color.js';
3
+ export function getAttr(el, name) {
4
+ return el.attrs.get(name);
5
+ }
6
+ export function hasAttr(el, name) {
7
+ return el.attrs.has(name);
8
+ }
9
+ /** Static value when statically known, otherwise undefined. */
10
+ export function staticValue(el, name) {
11
+ const attr = el.attrs.get(name);
12
+ return attr?.kind === 'static' ? attr.value : undefined;
13
+ }
14
+ /** Static string value (trimmed) when statically known to be a string. */
15
+ export function staticString(el, name) {
16
+ const v = staticValue(el, name);
17
+ return typeof v === 'string' ? v : undefined;
18
+ }
19
+ /** True when the attribute is present as a dynamic expression. */
20
+ export function isExpression(el, name) {
21
+ return el.attrs.get(name)?.kind === 'expression';
22
+ }
23
+ export function isStaticTrue(el, name) {
24
+ const v = staticValue(el, name);
25
+ return v === true || v === 'true';
26
+ }
27
+ export function isAriaHidden(el) {
28
+ return isStaticTrue(el, 'aria-hidden');
29
+ }
30
+ /**
31
+ * True when the attribute could plausibly carry a usable value: present as a
32
+ * non-empty static string, or dynamic (benefit of the doubt to avoid false
33
+ * positives in a purely static analysis).
34
+ */
35
+ export function attrProvidesValue(el, name) {
36
+ const attr = el.attrs.get(name);
37
+ if (!attr)
38
+ return false;
39
+ if (attr.kind === 'expression')
40
+ return true;
41
+ return typeof attr.value === 'string' ? attr.value.trim().length > 0 : attr.value != null && attr.value !== false;
42
+ }
43
+ /**
44
+ * Conservative check that an element's *content* can supply an accessible
45
+ * name: direct text, dynamic children, unknown components (which may render
46
+ * text), spreads, or an <img> with non-empty alt.
47
+ */
48
+ export function contentProvidesName(el) {
49
+ if (el.hasTextChild || el.hasExpressionChild)
50
+ return true;
51
+ if (hasAttr(el, 'dangerouslySetInnerHTML'))
52
+ return true;
53
+ return el.childElements.some((child) => {
54
+ if (isAriaHidden(child))
55
+ return false;
56
+ if (child.isComponent)
57
+ return true;
58
+ if (child.hasSpread)
59
+ return true;
60
+ if (child.name === 'img' && attrProvidesValue(child, 'alt'))
61
+ return true;
62
+ return contentProvidesName(child);
63
+ });
64
+ }
65
+ /** Accessible name via attributes or content, for web elements. */
66
+ export function hasAccessibleName(el) {
67
+ if (el.hasSpread)
68
+ return true;
69
+ if (attrProvidesValue(el, 'aria-label'))
70
+ return true;
71
+ if (attrProvidesValue(el, 'aria-labelledby'))
72
+ return true;
73
+ if (attrProvidesValue(el, 'title'))
74
+ return true;
75
+ return contentProvidesName(el);
76
+ }
77
+ const PRESENTATION_ROLES = new Set(['presentation', 'none']);
78
+ export function isPresentational(el) {
79
+ const role = staticString(el, 'role');
80
+ return role !== undefined && PRESENTATION_ROLES.has(role.trim().toLowerCase());
81
+ }
82
+ /**
83
+ * Statically-known value of a property in an inline style object literal,
84
+ * e.g. style={{ width: 20 }}. Returns undefined for dynamic styles,
85
+ * StyleSheet references, or non-literal values.
86
+ */
87
+ export function inlineStyleValue(el, prop) {
88
+ const style = el.attrs.get('style');
89
+ if (style?.kind !== 'expression' || !style.node || !ts.isObjectLiteralExpression(style.node))
90
+ return undefined;
91
+ for (const p of style.node.properties) {
92
+ if (!ts.isPropertyAssignment(p))
93
+ continue;
94
+ const name = ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) ? p.name.text : undefined;
95
+ if (name !== prop)
96
+ continue;
97
+ const init = p.initializer;
98
+ if (ts.isStringLiteralLike(init))
99
+ return init.text;
100
+ if (ts.isNumericLiteral(init))
101
+ return Number(init.text);
102
+ return undefined;
103
+ }
104
+ return undefined;
105
+ }
106
+ export function inlineStyleNumber(el, prop) {
107
+ const v = inlineStyleValue(el, prop);
108
+ return typeof v === 'number' ? v : undefined;
109
+ }
110
+ /**
111
+ * The full visible text of an element, when it is entirely static. Returns
112
+ * null as soon as an expression child or component child makes the text
113
+ * unknowable. aria-hidden subtrees are excluded (they are not "visible
114
+ * label" for 2.5.3 purposes).
115
+ */
116
+ export function deepStaticText(el) {
117
+ if (el.hasExpressionChild)
118
+ return null;
119
+ const parts = el.directText ? [el.directText] : [];
120
+ for (const child of el.childElements) {
121
+ if (isAriaHidden(child))
122
+ continue;
123
+ if (child.isComponent || child.hasSpread)
124
+ return null;
125
+ const childText = deepStaticText(child);
126
+ if (childText === null)
127
+ return null;
128
+ if (childText)
129
+ parts.push(childText);
130
+ }
131
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
132
+ }
133
+ /**
134
+ * Contrast of statically-known inline color/backgroundColor on the same
135
+ * element. Null when either color is dynamic, unparseable or translucent.
136
+ */
137
+ export function inlineStyleContrast(el) {
138
+ const fgRaw = inlineStyleValue(el, 'color');
139
+ const bgRaw = inlineStyleValue(el, 'backgroundColor') ?? inlineStyleValue(el, 'background');
140
+ const fg = parseColor(fgRaw);
141
+ const bg = parseColor(bgRaw);
142
+ if (!fg || !bg)
143
+ return null;
144
+ const fontSize = inlineStyleNumber(el, 'fontSize');
145
+ const weight = inlineStyleValue(el, 'fontWeight');
146
+ const bold = weight === 'bold' || weight === 700 || weight === '700' || weight === 800 || weight === '800' || weight === 900 || weight === '900';
147
+ const large = isLargeText(fontSize, bold);
148
+ return {
149
+ ratio: contrastRatio(fg, bg),
150
+ required: large ? 3 : 4.5,
151
+ large,
152
+ fontSizeKnown: fontSize !== undefined,
153
+ fg: String(fgRaw),
154
+ bg: String(bgRaw),
155
+ };
156
+ }
157
+ /**
158
+ * WCAG 1.4.3 contrast check for an element's inline literal color/background.
159
+ * Returns the finding (message + severity) to report, or null when it passes,
160
+ * the colors aren't static, or the size makes it indeterminate. Shared by the
161
+ * web and native `color-contrast` rules so the thresholds live in one place.
162
+ */
163
+ export function colorContrastFinding(el) {
164
+ const info = inlineStyleContrast(el);
165
+ if (!info)
166
+ return null;
167
+ if (info.ratio >= 4.5)
168
+ return null;
169
+ if (info.ratio >= 3 && (info.large || !info.fontSizeKnown))
170
+ return null;
171
+ if (info.ratio >= info.required && info.fontSizeKnown)
172
+ return null;
173
+ const fmt = (n) => (Math.round(n * 100) / 100).toFixed(2);
174
+ const requirement = info.large ? '3:1 (large text)' : '4.5:1';
175
+ return {
176
+ message: `Contrast between ${info.fg} and ${info.bg} is ${fmt(info.ratio)}:1 — below the ${requirement} required by WCAG 1.4.3.`,
177
+ severity: info.ratio < 3 ? 'serious' : 'moderate',
178
+ };
179
+ }
180
+ /**
181
+ * The WCAG target-size tier for a pointer target, by its smaller dimension.
182
+ * `below-min` < 24px (WCAG 2.5.8 AA), `below-recommended` < 44px (2.5.5 AAA /
183
+ * Apple HIG / Material). Keeps the 24/44 thresholds in one place; each rule
184
+ * maps the tier to its own severity and platform wording.
185
+ */
186
+ export function targetSizeTier(width, height) {
187
+ const min = Math.min(width, height);
188
+ if (min < 24)
189
+ return 'below-min';
190
+ if (min < 44)
191
+ return 'below-recommended';
192
+ return null;
193
+ }
194
+ /** Statically-known keys of an object-literal prop, e.g. accessibilityState={{...}}. */
195
+ export function objectLiteralKeys(el, attrName) {
196
+ const attr = el.attrs.get(attrName);
197
+ if (attr?.kind !== 'expression' || !attr.node || !ts.isObjectLiteralExpression(attr.node))
198
+ return undefined;
199
+ const keys = [];
200
+ for (const p of attr.node.properties) {
201
+ if (ts.isSpreadAssignment(p))
202
+ return undefined; // unknowable
203
+ if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
204
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
205
+ keys.push(p.name.text);
206
+ }
207
+ }
208
+ return keys;
209
+ }
@@ -0,0 +1,15 @@
1
+ export * from './types.js';
2
+ export * from './element.js';
3
+ export * from './helpers.js';
4
+ export * from './aria.js';
5
+ export { WCAG, WCAG22_TOTALS, WCAG22_A_AA, MANUAL_CHECKS, resolveWcag } from './wcag.js';
6
+ export { parseColor, contrastRatio, relativeLuminance, isLargeText, type Rgb } from './color.js';
7
+ export { parseSource } from './parse.js';
8
+ export { analyze, analyzeModel, type AnalyzeOptions, type AnalyzeModelOptions } from './engine.js';
9
+ export { applyFixes, fixRemoveAttr, fixRenameAttr } from './fixes.js';
10
+ export { scanProject, collectFiles, detectPlatform, type ScanOptions } from './scanner.js';
11
+ export { loadConfig, globToRegExp } from './config.js';
12
+ export { readPackageMeta, readOwnPackageMeta, type PackageMeta } from './pkg-meta.js';
13
+ export { toJson } from './reporters/json.js';
14
+ export { toSarif } from './reporters/sarif.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACnG,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export * from './types.js';
2
+ export * from './element.js';
3
+ export * from './helpers.js';
4
+ export * from './aria.js';
5
+ export { WCAG, WCAG22_TOTALS, WCAG22_A_AA, MANUAL_CHECKS, resolveWcag } from './wcag.js';
6
+ export { parseColor, contrastRatio, relativeLuminance, isLargeText } from './color.js';
7
+ export { parseSource } from './parse.js';
8
+ export { analyze, analyzeModel } from './engine.js';
9
+ export { applyFixes, fixRemoveAttr, fixRenameAttr } from './fixes.js';
10
+ export { scanProject, collectFiles, detectPlatform } from './scanner.js';
11
+ export { loadConfig, globToRegExp } from './config.js';
12
+ export { readPackageMeta, readOwnPackageMeta } from './pkg-meta.js';
13
+ export { toJson } from './reporters/json.js';
14
+ export { toSarif } from './reporters/sarif.js';
@@ -0,0 +1,3 @@
1
+ import ts from 'typescript';
2
+ export declare function parseSource(code: string, filename: string): ts.SourceFile;
3
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAU5B,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,EAAE,CAAC,UAAU,CAEzE"}
package/dist/parse.js ADDED
@@ -0,0 +1,14 @@
1
+ import ts from 'typescript';
2
+ function scriptKindFor(filename) {
3
+ if (filename.endsWith('.tsx'))
4
+ return ts.ScriptKind.TSX;
5
+ if (filename.endsWith('.mts') || filename.endsWith('.cts'))
6
+ return ts.ScriptKind.TS;
7
+ if (filename.endsWith('.ts'))
8
+ return ts.ScriptKind.TS;
9
+ // .js / .jsx / .mjs / .cjs — parse permissively so JSX in plain JS works.
10
+ return ts.ScriptKind.JSX;
11
+ }
12
+ export function parseSource(code, filename) {
13
+ return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, scriptKindFor(filename));
14
+ }
@@ -0,0 +1,21 @@
1
+ export interface PackageMeta {
2
+ name?: string;
3
+ version?: string;
4
+ homepage?: string;
5
+ }
6
+ /**
7
+ * Read a package's own metadata so names, versions and URLs live in one
8
+ * place (package.json) instead of being duplicated in source.
9
+ *
10
+ * Call with `new URL('../package.json', import.meta.url)` — resolves
11
+ * correctly from both src/ (tests) and dist/ (builds).
12
+ */
13
+ export declare function readPackageMeta(packageJsonUrl: URL | string): PackageMeta;
14
+ /**
15
+ * Like readPackageMeta, but safe when `import.meta.url` is unavailable
16
+ * (e.g. the package was bundled to CJS for a VS Code extension) — the URL
17
+ * construction itself is guarded, returning {} instead of throwing at
18
+ * module load.
19
+ */
20
+ export declare function readOwnPackageMeta(importMetaUrl: string | undefined, rel?: string): PackageMeta;
21
+ //# sourceMappingURL=pkg-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkg-meta.d.ts","sourceRoot":"","sources":["../src/pkg-meta.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,GAAG,GAAG,MAAM,GAAG,WAAW,CAOzE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAoB,GAAG,WAAW,CAM1G"}
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ /**
3
+ * Read a package's own metadata so names, versions and URLs live in one
4
+ * place (package.json) instead of being duplicated in source.
5
+ *
6
+ * Call with `new URL('../package.json', import.meta.url)` — resolves
7
+ * correctly from both src/ (tests) and dist/ (builds).
8
+ */
9
+ export function readPackageMeta(packageJsonUrl) {
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(packageJsonUrl, 'utf8'));
12
+ return { name: pkg.name, version: pkg.version, homepage: pkg.homepage };
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ /**
19
+ * Like readPackageMeta, but safe when `import.meta.url` is unavailable
20
+ * (e.g. the package was bundled to CJS for a VS Code extension) — the URL
21
+ * construction itself is guarded, returning {} instead of throwing at
22
+ * module load.
23
+ */
24
+ export function readOwnPackageMeta(importMetaUrl, rel = '../package.json') {
25
+ try {
26
+ return readPackageMeta(new URL(rel, importMetaUrl));
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScanResult } from '../types.js';
2
+ export declare function toJson(result: ScanResult): string;
3
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/reporters/json.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAYjD"}
@@ -0,0 +1,9 @@
1
+ export function toJson(result) {
2
+ return JSON.stringify({
3
+ platform: result.platform,
4
+ filesScanned: result.filesScanned,
5
+ durationMs: result.durationMs,
6
+ issueCount: result.diagnostics.length,
7
+ issues: result.diagnostics,
8
+ }, null, 2);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { Rule, ScanResult } from '../types.js';
2
+ export interface SarifToolInfo {
3
+ name?: string;
4
+ version?: string;
5
+ informationUri?: string;
6
+ }
7
+ /** SARIF 2.1.0 output, compatible with GitHub code scanning. */
8
+ export declare function toSarif(result: ScanResult, rules: Rule[], tool?: SarifToolInfo): string;
9
+ //# sourceMappingURL=sarif.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sarif.d.ts","sourceRoot":"","sources":["../../src/reporters/sarif.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAY,MAAM,aAAa,CAAC;AAS9D,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAsD3F"}
@@ -0,0 +1,61 @@
1
+ const SARIF_LEVEL = {
2
+ critical: 'error',
3
+ serious: 'error',
4
+ moderate: 'warning',
5
+ minor: 'note',
6
+ };
7
+ /** SARIF 2.1.0 output, compatible with GitHub code scanning. */
8
+ export function toSarif(result, rules, tool = {}) {
9
+ const usedRuleIds = new Set(result.diagnostics.map((d) => d.ruleId));
10
+ const ruleDescriptors = rules
11
+ .filter((r) => usedRuleIds.has(r.meta.id))
12
+ .map((r) => ({
13
+ id: r.meta.id,
14
+ shortDescription: { text: r.meta.description },
15
+ helpUri: r.meta.helpUrl,
16
+ properties: {
17
+ severity: r.meta.severity,
18
+ wcag: r.meta.wcag,
19
+ tags: ['accessibility', ...r.meta.wcag.map((sc) => `wcag-${sc.replace(/\./g, '')}`)],
20
+ },
21
+ }));
22
+ const ruleIndex = new Map(ruleDescriptors.map((r, i) => [r.id, i]));
23
+ const sarif = {
24
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
25
+ version: '2.1.0',
26
+ runs: [
27
+ {
28
+ tool: {
29
+ driver: {
30
+ name: tool.name ?? 'react-a11y',
31
+ ...(tool.version ? { version: tool.version } : {}),
32
+ ...(tool.informationUri ? { informationUri: tool.informationUri } : {}),
33
+ rules: ruleDescriptors,
34
+ },
35
+ },
36
+ results: result.diagnostics.map((d) => ({
37
+ ruleId: d.ruleId,
38
+ ruleIndex: ruleIndex.get(d.ruleId),
39
+ level: SARIF_LEVEL[d.severity],
40
+ message: {
41
+ text: `${d.message} (WCAG ${d.wcag.map((w) => `${w.sc} ${w.name}, Level ${w.level}`).join('; ')})`,
42
+ },
43
+ locations: [
44
+ {
45
+ physicalLocation: {
46
+ artifactLocation: { uri: d.file },
47
+ region: {
48
+ startLine: d.line,
49
+ startColumn: d.column,
50
+ endLine: d.endLine,
51
+ endColumn: d.endColumn,
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ })),
57
+ },
58
+ ],
59
+ };
60
+ return JSON.stringify(sarif, null, 2);
61
+ }
@@ -0,0 +1,16 @@
1
+ import type { A11yConfig, Platform, ProjectPass, Rule, ScanResult } from './types.js';
2
+ export interface ScanOptions {
3
+ root: string;
4
+ rules: Rule[];
5
+ platform: Platform;
6
+ config?: A11yConfig;
7
+ /** Cross-file analyses run alongside the per-file rules. */
8
+ projectPasses?: ProjectPass[];
9
+ /** Restrict the scan to these files (e.g. --changed); still extension-filtered. */
10
+ files?: string[];
11
+ }
12
+ /** Detect web vs native from package.json dependencies. */
13
+ export declare function detectPlatform(root: string): Platform;
14
+ export declare function collectFiles(root: string, ignore?: string[]): string[];
15
+ export declare function scanProject(options: ScanOptions): ScanResult;
16
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAc,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAWlG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,WAAW,EAAE,CAAC;IAC9B,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CASrD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA+B1E;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CA+C5D"}
@@ -0,0 +1,113 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { analyzeModel } from './engine.js';
4
+ import { buildFileModel } from './element.js';
5
+ import { parseSource } from './parse.js';
6
+ import { globToRegExp } from './config.js';
7
+ const SCAN_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
8
+ const IGNORED_DIRS = new Set([
9
+ 'node_modules', '.git', 'dist', 'build', 'out', 'coverage',
10
+ '.next', '.expo', '.turbo', '.cache', 'android', 'ios', 'vendor',
11
+ ]);
12
+ const MAX_FILE_SIZE = 1.5 * 1024 * 1024;
13
+ /** Detect web vs native from package.json dependencies. */
14
+ export function detectPlatform(root) {
15
+ try {
16
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
17
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
18
+ if (deps['react-native'] || deps['expo'])
19
+ return 'native';
20
+ }
21
+ catch {
22
+ // no package.json — default to web
23
+ }
24
+ return 'web';
25
+ }
26
+ export function collectFiles(root, ignore = []) {
27
+ const ignoreRes = ignore.map(globToRegExp);
28
+ const files = [];
29
+ function walk(dir) {
30
+ let entries;
31
+ try {
32
+ entries = fs.readdirSync(dir, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ for (const entry of entries) {
38
+ if (entry.name.startsWith('.') && entry.name !== '.')
39
+ continue;
40
+ const full = path.join(dir, entry.name);
41
+ const rel = path.relative(root, full).split(path.sep).join('/');
42
+ if (ignoreRes.some((re) => re.test(rel)))
43
+ continue;
44
+ if (entry.isDirectory()) {
45
+ if (!IGNORED_DIRS.has(entry.name))
46
+ walk(full);
47
+ }
48
+ else if (entry.isFile()) {
49
+ const ext = path.extname(entry.name);
50
+ if (!SCAN_EXTENSIONS.has(ext))
51
+ continue;
52
+ if (entry.name.endsWith('.d.ts'))
53
+ continue;
54
+ files.push(full);
55
+ }
56
+ }
57
+ }
58
+ const stat = fs.statSync(root);
59
+ if (stat.isFile())
60
+ return [root];
61
+ walk(root);
62
+ return files.sort();
63
+ }
64
+ export function scanProject(options) {
65
+ const { root, rules, platform, config = {}, projectPasses = [] } = options;
66
+ const started = performance.now();
67
+ const files = options.files
68
+ ? options.files
69
+ .map((f) => (path.isAbsolute(f) ? f : path.resolve(root, f)))
70
+ .filter((f) => SCAN_EXTENSIONS.has(path.extname(f)) && !f.endsWith('.d.ts') && fs.existsSync(f))
71
+ .sort()
72
+ : collectFiles(root, config.ignore ?? []);
73
+ const diagnostics = [];
74
+ for (const file of files) {
75
+ let code;
76
+ try {
77
+ if (fs.statSync(file).size > MAX_FILE_SIZE)
78
+ continue;
79
+ code = fs.readFileSync(file, 'utf8');
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ // Fast pre-filter: skip files with no JSX-ish content.
85
+ if (!code.includes('<'))
86
+ continue;
87
+ const filename = path.relative(root, file).split(path.sep).join('/') || path.basename(file);
88
+ const model = buildFileModel(parseSource(code, filename));
89
+ diagnostics.push(...analyzeModel(model, { filename, platform, rules, ruleSettings: config.rules }));
90
+ for (const pass of projectPasses)
91
+ pass.collect(model, filename);
92
+ }
93
+ for (const pass of projectPasses)
94
+ diagnostics.push(...pass.finalize());
95
+ for (const rule of rules) {
96
+ if (!rule.projectCheck || !rule.meta.platforms.includes(platform))
97
+ continue;
98
+ const setting = config.rules?.[rule.meta.id];
99
+ if (setting === 'off')
100
+ continue;
101
+ for (const diag of rule.projectCheck(root)) {
102
+ diagnostics.push(setting ? { ...diag, severity: setting } : diag);
103
+ }
104
+ }
105
+ diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
106
+ return {
107
+ diagnostics,
108
+ filesScanned: files.length,
109
+ durationMs: Math.round(performance.now() - started),
110
+ platform,
111
+ root,
112
+ };
113
+ }