@vibecodeqa/cli 0.15.0 → 0.17.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.
@@ -0,0 +1,85 @@
1
+ /** Accessibility check — detects common a11y violations in JSX/TSX code. */
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ import { getProductionFiles } from "../fs-utils.js";
6
+ export function runAccessibility(cwd) {
7
+ const start = Date.now();
8
+ const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
9
+ if (files.length === 0) {
10
+ return { name: "accessibility", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
11
+ }
12
+ const issues = [];
13
+ let missingAlt = 0;
14
+ let clickDiv = 0;
15
+ let missingLabel = 0;
16
+ let missingLang = 0;
17
+ let autofocus = 0;
18
+ let positiveTabindex = 0;
19
+ for (const f of files) {
20
+ const lines = f.content.split("\n");
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ const trimmed = line.trim();
24
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
25
+ continue;
26
+ // 1. <img> without alt
27
+ if (/<img\b/.test(trimmed) && !/alt=/.test(trimmed)) {
28
+ const block = lines.slice(i, Math.min(i + 5, lines.length)).join(" ");
29
+ if (/<img\b/.test(block) && !/alt=/.test(block)) {
30
+ missingAlt++;
31
+ issues.push({ severity: "error", message: "<img> missing alt attribute", file: f.path, line: i + 1, rule: "img-alt" });
32
+ }
33
+ }
34
+ // 2. Click handler on non-interactive element without role/keyboard
35
+ if (/onClick=/.test(trimmed) && /<(?:div|span|p|li|section|article|header|footer)\b/.test(trimmed)) {
36
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(" ");
37
+ if (!(/role=/.test(block) && /(?:onKeyDown|onKeyUp|onKeyPress|tabIndex)/.test(block))) {
38
+ clickDiv++;
39
+ issues.push({ severity: "warning", message: "Click handler on non-interactive element without role + keyboard handler", file: f.path, line: i + 1, rule: "click-events" });
40
+ }
41
+ }
42
+ // 3. <input>/<select>/<textarea> without associated label
43
+ if (/<(?:input|select|textarea)\b/.test(trimmed) && !/type=["'](?:hidden|submit|button|reset)["']/.test(trimmed)) {
44
+ const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(" ");
45
+ if (!/aria-label=/.test(block) && !/aria-labelledby=/.test(block) && !/<label/.test(block) && !/id=/.test(trimmed)) {
46
+ missingLabel++;
47
+ issues.push({ severity: "warning", message: "Form control without label, aria-label, or aria-labelledby", file: f.path, line: i + 1, rule: "form-label" });
48
+ }
49
+ }
50
+ // 4. autoFocus
51
+ if (/\bautoFocus\b/.test(trimmed) || /\bautofocus\b/.test(trimmed)) {
52
+ autofocus++;
53
+ issues.push({ severity: "warning", message: "autoFocus can disorient screen reader users", file: f.path, line: i + 1, rule: "no-autofocus" });
54
+ }
55
+ // 5. Positive tabIndex
56
+ if (/tabIndex=\{[1-9]/.test(trimmed) || /tabindex=["'][1-9]/.test(trimmed)) {
57
+ positiveTabindex++;
58
+ issues.push({ severity: "warning", message: "Positive tabIndex disrupts natural tab order — use 0 or -1", file: f.path, line: i + 1, rule: "tabindex" });
59
+ }
60
+ }
61
+ }
62
+ // 6. Check for html lang attribute in index.html
63
+ const htmlPaths = ["index.html", "web/index.html", "public/index.html"];
64
+ for (const h of htmlPaths) {
65
+ const full = join(cwd, h);
66
+ if (!existsSync(full))
67
+ continue;
68
+ const content = readFileSync(full, "utf-8");
69
+ if (/<html\b/.test(content) && !/<html[^>]*lang=/.test(content)) {
70
+ missingLang++;
71
+ issues.push({ severity: "warning", message: "<html> missing lang attribute", file: h, rule: "html-lang" });
72
+ }
73
+ }
74
+ const errors = issues.filter((i) => i.severity === "error").length;
75
+ const warnings = issues.filter((i) => i.severity === "warning").length;
76
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warnings * 4));
77
+ return {
78
+ name: "accessibility",
79
+ score,
80
+ grade: gradeFromScore(score),
81
+ details: { jsxFiles: files.length, missingAlt, clickDiv, missingLabel, missingLang, autofocus, positiveTabindex },
82
+ issues,
83
+ duration: Date.now() - start,
84
+ };
85
+ }
@@ -0,0 +1,3 @@
1
+ /** React-specific checks — hooks rules, conditional hooks, missing keys, prop spreading. */
2
+ import type { CheckResult, StackInfo } from "../types.js";
3
+ export declare function runReact(cwd: string, stack: StackInfo): CheckResult;
@@ -0,0 +1,81 @@
1
+ /** React-specific checks — hooks rules, conditional hooks, missing keys, prop spreading. */
2
+ import { gradeFromScore } from "../types.js";
3
+ import { getProductionFiles } from "../fs-utils.js";
4
+ export function runReact(cwd, stack) {
5
+ const start = Date.now();
6
+ if (stack.framework !== "react") {
7
+ return { name: "react", score: 100, grade: "A", details: { skipped: true, reason: "not a React project" }, issues: [], duration: Date.now() - start };
8
+ }
9
+ const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
10
+ if (files.length === 0) {
11
+ return { name: "react", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
12
+ }
13
+ const issues = [];
14
+ let conditionalHooks = 0;
15
+ let missingKeys = 0;
16
+ let propSpreading = 0;
17
+ let inlineHandlers = 0;
18
+ let indexKeys = 0;
19
+ for (const f of files) {
20
+ const lines = f.content.split("\n");
21
+ // Track if we're inside a conditional block
22
+ let condDepth = 0;
23
+ for (let i = 0; i < lines.length; i++) {
24
+ const line = lines[i];
25
+ const trimmed = line.trim();
26
+ // Skip comments
27
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
28
+ continue;
29
+ // Track conditional blocks
30
+ if (/\b(if|else|switch)\s*\(/.test(trimmed))
31
+ condDepth++;
32
+ if (condDepth > 0 && trimmed.includes("{"))
33
+ condDepth++;
34
+ if (condDepth > 0 && trimmed.includes("}"))
35
+ condDepth--;
36
+ // 1. Hooks called inside conditionals
37
+ if (condDepth > 0 && /\buse[A-Z]\w*\s*\(/.test(trimmed) && !/\/\//.test(trimmed.split("use")[0])) {
38
+ conditionalHooks++;
39
+ issues.push({ severity: "error", message: "Hook called inside conditional — violates Rules of Hooks", file: f.path, line: i + 1, rule: "conditional-hook" });
40
+ }
41
+ // 2. Missing key in .map() returning JSX
42
+ if (/\.map\s*\(/.test(trimmed)) {
43
+ // Look ahead for JSX return without key
44
+ const mapBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
45
+ if (/<\w/.test(mapBlock) && !mapBlock.includes("key=") && !mapBlock.includes("key:")) {
46
+ missingKeys++;
47
+ issues.push({ severity: "error", message: "JSX in .map() without key prop", file: f.path, line: i + 1, rule: "missing-key" });
48
+ }
49
+ }
50
+ // 3. index as key
51
+ if (/key=\{(?:i|idx|index)\}/.test(trimmed) || /key=\{.*(?:, *(?:i|idx|index)\))/.test(trimmed)) {
52
+ indexKeys++;
53
+ issues.push({ severity: "warning", message: "Using index as key — can cause rendering bugs with reorderable lists", file: f.path, line: i + 1, rule: "index-key" });
54
+ }
55
+ // 4. Prop spreading ({...props} on DOM elements)
56
+ if (/\{\.\.\.(?!children)\w+\}/.test(trimmed) && /<[a-z]/.test(trimmed)) {
57
+ propSpreading++;
58
+ issues.push({ severity: "warning", message: "Spreading props onto DOM element — can pass unexpected attributes", file: f.path, line: i + 1, rule: "prop-spreading" });
59
+ }
60
+ // 5. Inline arrow functions in JSX event handlers (performance)
61
+ if (/on[A-Z]\w*=\{(?:\(\) =>|function)/.test(trimmed)) {
62
+ inlineHandlers++;
63
+ }
64
+ }
65
+ }
66
+ // Only warn about inline handlers if there are many
67
+ if (inlineHandlers > 15) {
68
+ issues.push({ severity: "warning", message: `${inlineHandlers} inline arrow functions in JSX handlers — extract to named functions for readability`, rule: "inline-handlers" });
69
+ }
70
+ const errors = issues.filter((i) => i.severity === "error").length;
71
+ const warnings = issues.filter((i) => i.severity === "warning").length;
72
+ const score = Math.max(0, Math.min(100, 100 - errors * 8 - warnings * 3));
73
+ return {
74
+ name: "react",
75
+ score,
76
+ grade: gradeFromScore(score),
77
+ details: { jsxFiles: files.length, conditionalHooks, missingKeys, indexKeys, propSpreading, inlineHandlers },
78
+ issues,
79
+ duration: Date.now() - start,
80
+ };
81
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.15.0",
4
- "description": "Code health scanner for the AI coding era. 15 checks, zero config, full report.",
3
+ "version": "0.17.0",
4
+ "description": "Code health scanner for the AI coding era. 18 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vcqa": "./dist/cli.js",