@vibecodeqa/cli 0.42.0 → 0.44.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 (47) hide show
  1. package/README.md +130 -165
  2. package/dist/check-meta.js +59 -6
  3. package/dist/cli.js +299 -762
  4. package/dist/commands/explain.d.ts +2 -0
  5. package/dist/commands/explain.js +33 -0
  6. package/dist/commands/fix.d.ts +6 -0
  7. package/dist/commands/fix.js +157 -0
  8. package/dist/commands/init.d.ts +2 -0
  9. package/dist/commands/init.js +96 -0
  10. package/dist/commands/shared.d.ts +4 -0
  11. package/dist/commands/shared.js +80 -0
  12. package/dist/core.d.ts +1 -0
  13. package/dist/core.js +12 -1
  14. package/dist/delta.d.ts +45 -0
  15. package/dist/delta.js +158 -0
  16. package/dist/detect.js +2 -2
  17. package/dist/pr-comment.d.ts +1 -1
  18. package/dist/pr-comment.js +23 -4
  19. package/dist/report/html.d.ts +1 -1
  20. package/dist/report/html.js +7 -2
  21. package/dist/report/pages.d.ts +2 -0
  22. package/dist/report/pages.js +167 -0
  23. package/dist/report/styles.d.ts +1 -1
  24. package/dist/report/styles.js +37 -0
  25. package/dist/runners/accessibility.js +4 -1
  26. package/dist/runners/best-practices.js +1 -1
  27. package/dist/runners/confusion.js +28 -17
  28. package/dist/runners/design-consistency.d.ts +12 -0
  29. package/dist/runners/design-consistency.js +125 -0
  30. package/dist/runners/error-handling.js +18 -2
  31. package/dist/runners/file-cohesion.d.ts +17 -0
  32. package/dist/runners/file-cohesion.js +177 -0
  33. package/dist/runners/frontend-health.d.ts +14 -0
  34. package/dist/runners/frontend-health.js +206 -0
  35. package/dist/runners/html-quality.d.ts +8 -0
  36. package/dist/runners/html-quality.js +203 -0
  37. package/dist/runners/lint.js +6 -1
  38. package/dist/runners/react.js +1 -0
  39. package/dist/runners/secrets.js +7 -2
  40. package/dist/runners/security.js +7 -1
  41. package/dist/runners/standards.d.ts +2 -2
  42. package/dist/runners/standards.js +45 -12
  43. package/dist/runners/structure.js +1 -1
  44. package/dist/runners/styling.d.ts +15 -0
  45. package/dist/runners/styling.js +280 -0
  46. package/dist/runners/testing.js +3 -1
  47. package/package.json +2 -2
@@ -0,0 +1,125 @@
1
+ /** Design Consistency — LLM-powered audit of visual consistency across components.
2
+ *
3
+ * Pro feature. Requires VCQA_PRO_KEY env var.
4
+ *
5
+ * Detects:
6
+ * - Components that define the same visual pattern differently (accidental design system)
7
+ * - Spacing/color/typography inconsistencies across files
8
+ * - Missing component extraction opportunities
9
+ * - Tailwind class patterns that should be @apply'd or componentized
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { getProductionFiles } from "../fs-utils.js";
15
+ import { gradeFromScore } from "../types.js";
16
+ export async function runDesignConsistency(cwd) {
17
+ const start = Date.now();
18
+ const proKey = process.env.VCQA_PRO_KEY || "";
19
+ if (!proKey) {
20
+ return {
21
+ name: "design-consistency",
22
+ score: 0,
23
+ grade: "F",
24
+ details: {
25
+ premium: true,
26
+ comingSoon: true,
27
+ reason: "Set VCQA_PRO_KEY to enable design consistency analysis",
28
+ description: "LLM-powered audit of visual consistency — finds components with duplicate styling, inconsistent spacing scales, and missing component extraction opportunities.",
29
+ },
30
+ issues: [],
31
+ duration: Date.now() - start,
32
+ };
33
+ }
34
+ const files = getProductionFiles(cwd);
35
+ const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
36
+ if (componentFiles.length < 2) {
37
+ return {
38
+ name: "design-consistency",
39
+ score: 100,
40
+ grade: "A",
41
+ details: { componentsAnalyzed: componentFiles.length },
42
+ issues: [],
43
+ duration: Date.now() - start,
44
+ };
45
+ }
46
+ // Build hash of all component content for cache key
47
+ const h = createHash("sha256");
48
+ for (const f of componentFiles.sort((a, b) => a.path.localeCompare(b.path))) {
49
+ h.update(f.path);
50
+ h.update(f.content.slice(0, 2000));
51
+ }
52
+ const contentHash = h.digest("hex").slice(0, 16);
53
+ // Check cache
54
+ const cache = loadCache(cwd);
55
+ if (cache && cache.hash === contentHash) {
56
+ return {
57
+ name: "design-consistency",
58
+ score: cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12),
59
+ grade: gradeFromScore(cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12)),
60
+ details: { premium: true, componentsAnalyzed: componentFiles.length, cached: true },
61
+ issues: cache.findings,
62
+ duration: Date.now() - start,
63
+ };
64
+ }
65
+ // Extract styling snippets from top components (by size, most likely to have styling)
66
+ const candidates = componentFiles
67
+ .sort((a, b) => b.content.length - a.content.length)
68
+ .slice(0, 10);
69
+ const snippets = candidates.map((f) => ({
70
+ path: f.path,
71
+ content: f.content.slice(0, 2000),
72
+ }));
73
+ const issues = await analyzeDesign(snippets, proKey);
74
+ // Cache results
75
+ saveCache(cwd, { version: 1, hash: contentHash, findings: issues });
76
+ const score = issues.length === 0 ? 100 : Math.max(20, 100 - issues.length * 12);
77
+ return {
78
+ name: "design-consistency",
79
+ score,
80
+ grade: gradeFromScore(score),
81
+ details: { premium: true, componentsAnalyzed: componentFiles.length },
82
+ issues,
83
+ duration: Date.now() - start,
84
+ };
85
+ }
86
+ async function analyzeDesign(files, proKey) {
87
+ try {
88
+ const res = await fetch("https://api.vibecodeqa.online/api/pro/design-consistency", {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ Authorization: `Bearer ${proKey}`,
93
+ },
94
+ body: JSON.stringify({ files: files.map((f) => ({ path: f.path, content: f.content })) }),
95
+ });
96
+ if (!res.ok)
97
+ return [];
98
+ const data = (await res.json());
99
+ return data.findings || [];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ function loadCache(cwd) {
106
+ try {
107
+ const cachePath = join(cwd, ".vibe-check", "design-consistency-cache.json");
108
+ if (existsSync(cachePath)) {
109
+ const data = JSON.parse(readFileSync(cachePath, "utf-8"));
110
+ if (data.version === 1)
111
+ return data;
112
+ }
113
+ }
114
+ catch { /* corrupt cache */ }
115
+ return null;
116
+ }
117
+ function saveCache(cwd, cache) {
118
+ try {
119
+ const dir = join(cwd, ".vibe-check");
120
+ if (!existsSync(dir))
121
+ mkdirSync(dir, { recursive: true });
122
+ writeFileSync(join(dir, "design-consistency-cache.json"), JSON.stringify(cache));
123
+ }
124
+ catch { /* write failed */ }
125
+ }
@@ -25,12 +25,22 @@ export function runErrorHandling(cwd, stack) {
25
25
  const line = lines[i].trim();
26
26
  if (line.startsWith("//") || line.startsWith("*"))
27
27
  continue;
28
- // Skip string-only lines and metadata definitions
28
+ // Skip suppressed lines (// ok, // intentional, eslint-disable, biome-ignore)
29
+ if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
30
+ continue;
31
+ // Skip string-only lines, metadata definitions, and return statements with string literals
29
32
  if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
30
33
  continue;
31
34
  if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
32
35
  continue;
36
+ if (/\breturn\s+["'`]/.test(line))
37
+ continue;
38
+ if (/\brule\s*===?\s*["']/.test(line))
39
+ continue;
33
40
  if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line) || /catch\s*\{\s*\}/.test(line)) {
41
+ // Skip intentional one-liner try-catch for optional browser APIs
42
+ if (/try\s*\{.*(?:localStorage|sessionStorage|document\.).*\}\s*catch/.test(line))
43
+ continue;
34
44
  emptyCatch++;
35
45
  issues.push({ severity: "error", message: "Empty catch block", file: f.path, line: i + 1, rule: "empty-catch" });
36
46
  }
@@ -83,8 +93,14 @@ export function runErrorHandling(cwd, stack) {
83
93
  const line = lines[i].trim();
84
94
  if (line.startsWith("//") || line.startsWith("*"))
85
95
  continue;
96
+ if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
97
+ continue;
98
+ if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
99
+ continue;
86
100
  if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
87
101
  continue;
102
+ if (/\breturn\s+["'`]/.test(line))
103
+ continue;
88
104
  // JSON.parse of external input without try-catch
89
105
  if (/\bJSON\.parse\s*\(/.test(line)) {
90
106
  // Only flag if parsing user/network input (not internal known-safe files)
@@ -141,7 +157,7 @@ export function runErrorHandling(cwd, stack) {
141
157
  }
142
158
  }
143
159
  // process.exit() in non-CLI files (library code shouldn't exit)
144
- if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/")) {
160
+ if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/") && !f.path.includes("commands/") && !f.path.includes("monitor")) {
145
161
  processExit++;
146
162
  issues.push({
147
163
  severity: "warning",
@@ -0,0 +1,17 @@
1
+ /** File Cohesion — detects files with multiple responsibilities.
2
+ *
3
+ * Pro feature. Requires VCQA_PRO_KEY env var.
4
+ *
5
+ * Two-phase analysis:
6
+ * 1. Local heuristics (always run with Pro key):
7
+ * - Files with exports spanning multiple domains (auth + email + db)
8
+ * - Mixed concern signals: HTTP handlers + business logic + data access
9
+ * - High export count relative to file purpose
10
+ *
11
+ * 2. LLM-powered analysis (via api.vibecodeqa.online):
12
+ * - Labels each file's responsibility clusters
13
+ * - Suggests concrete split points
14
+ * - "This file handles auth, session, AND email — split into 3 modules"
15
+ */
16
+ import type { CheckResult } from "../types.js";
17
+ export declare function runFileCohesion(cwd: string): Promise<CheckResult>;
@@ -0,0 +1,177 @@
1
+ /** File Cohesion — detects files with multiple responsibilities.
2
+ *
3
+ * Pro feature. Requires VCQA_PRO_KEY env var.
4
+ *
5
+ * Two-phase analysis:
6
+ * 1. Local heuristics (always run with Pro key):
7
+ * - Files with exports spanning multiple domains (auth + email + db)
8
+ * - Mixed concern signals: HTTP handlers + business logic + data access
9
+ * - High export count relative to file purpose
10
+ *
11
+ * 2. LLM-powered analysis (via api.vibecodeqa.online):
12
+ * - Labels each file's responsibility clusters
13
+ * - Suggests concrete split points
14
+ * - "This file handles auth, session, AND email — split into 3 modules"
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { getProductionFiles } from "../fs-utils.js";
20
+ import { gradeFromScore } from "../types.js";
21
+ const CONCERN_PATTERNS = [
22
+ { name: "HTTP/routing", patterns: [/\b(app|router|server)\.(get|post|put|delete|patch|use)\b/, /\bRequest\b.*\bResponse\b/, /\breq\s*,\s*res\b/, /\bfastify\b|\bhono\b|\bexpress\b/] },
23
+ { name: "database", patterns: [/\b(prisma|sequelize|typeorm|knex|drizzle)\b/, /\bSELECT\b.*\bFROM\b/i, /\.query\s*\(/, /\bcreateClient\b.*\bsupabase\b/i] },
24
+ { name: "auth", patterns: [/\b(jwt|token|session|cookie|passport|oauth|login|signup|signIn|signUp)\b/i, /\bverify(Token|Session|Auth)\b/, /\bbcrypt|argon2|scrypt\b/] },
25
+ { name: "email", patterns: [/\bsendMail|sendEmail|transporter|nodemailer|resend|postmark\b/i, /\bsmtp\b/i] },
26
+ { name: "file I/O", patterns: [/\breadFileSync|writeFileSync|createReadStream|createWriteStream\b/, /\bfs\.(read|write|mkdir|unlink|stat)\b/] },
27
+ { name: "validation", patterns: [/\bz\.(string|number|object|array)\b/, /\bJoi\.(string|number|object)\b/, /\byup\.(string|number|object)\b/, /\bvalidate\w*Schema\b/] },
28
+ { name: "UI rendering", patterns: [/\bJSX\b|\breturn\s*\(?\s*</, /\buseState|useEffect|useRef|useMemo\b/, /\brender\(\)/, /\bcomponent\b/i] },
29
+ { name: "state management", patterns: [/\bcreateStore|useStore|createSlice|createReducer\b/, /\bdispatch\(|getState\(\)/, /\buseSelector|useDispatch\b/] },
30
+ { name: "testing", patterns: [/\bdescribe\s*\(|it\s*\(|test\s*\(|expect\s*\(/, /\bbeforeEach|afterEach|beforeAll\b/, /\bjest\.|vitest\./] },
31
+ { name: "CLI", patterns: [/\bprocess\.argv\b/, /\bcommander|yargs|meow|cac\b/, /\bparse(Args|Options)\b/] },
32
+ ];
33
+ export async function runFileCohesion(cwd) {
34
+ const start = Date.now();
35
+ const proKey = process.env.VCQA_PRO_KEY || "";
36
+ if (!proKey) {
37
+ return {
38
+ name: "file-cohesion",
39
+ score: 0,
40
+ grade: "F",
41
+ details: {
42
+ premium: true,
43
+ comingSoon: true,
44
+ reason: "Set VCQA_PRO_KEY to enable file cohesion analysis",
45
+ description: "Detects files with multiple responsibilities — the #1 code smell in AI-generated code. Labels each file's concern clusters and suggests concrete split points.",
46
+ },
47
+ issues: [],
48
+ duration: Date.now() - start,
49
+ };
50
+ }
51
+ const files = getProductionFiles(cwd);
52
+ const issues = [];
53
+ const cache = loadCache(cwd);
54
+ let cacheHits = 0;
55
+ // Phase 1: local heuristics — detect multi-concern files
56
+ const candidates = [];
57
+ for (const f of files) {
58
+ if (f.isTest)
59
+ continue;
60
+ const lines = f.content.split("\n").length;
61
+ if (lines < 100)
62
+ continue; // small files rarely have cohesion problems
63
+ const detectedConcerns = [];
64
+ for (const concern of CONCERN_PATTERNS) {
65
+ if (concern.patterns.some((p) => p.test(f.content))) {
66
+ detectedConcerns.push(concern.name);
67
+ }
68
+ }
69
+ if (detectedConcerns.length >= 2) {
70
+ candidates.push({ path: f.path, content: f.content, concerns: detectedConcerns, lines });
71
+ // Local issue for 2+ concerns
72
+ issues.push({
73
+ severity: detectedConcerns.length >= 3 ? "error" : "warning",
74
+ message: `${detectedConcerns.length} concerns detected: ${detectedConcerns.join(", ")} — consider splitting`,
75
+ file: f.path,
76
+ rule: "multi-concern",
77
+ });
78
+ }
79
+ }
80
+ // Phase 2: LLM analysis for top candidates (by line count, most likely to benefit from splitting)
81
+ const topCandidates = candidates
82
+ .sort((a, b) => b.lines - a.lines)
83
+ .slice(0, 5);
84
+ for (const candidate of topCandidates) {
85
+ const hash = createHash("sha256").update(candidate.content).digest("hex").slice(0, 16);
86
+ // Check cache
87
+ const cached = cache.files[candidate.path];
88
+ if (cached && cached.hash === hash) {
89
+ cacheHits++;
90
+ if (cached.suggestion) {
91
+ issues.push({
92
+ severity: "warning",
93
+ message: cached.suggestion,
94
+ file: candidate.path,
95
+ rule: "split-suggestion",
96
+ });
97
+ }
98
+ continue;
99
+ }
100
+ // Call LLM for concrete split suggestions
101
+ const analysis = await analyzeCohesion(candidate.path, candidate.content, proKey);
102
+ if (analysis) {
103
+ cache.files[candidate.path] = { hash, concerns: analysis.concerns, suggestion: analysis.suggestion };
104
+ if (analysis.suggestion) {
105
+ issues.push({
106
+ severity: "warning",
107
+ message: analysis.suggestion,
108
+ file: candidate.path,
109
+ rule: "split-suggestion",
110
+ });
111
+ }
112
+ }
113
+ }
114
+ saveCache(cwd, cache);
115
+ const totalFiles = files.filter((f) => !f.isTest).length;
116
+ const multiConcernFiles = candidates.length;
117
+ const ratio = totalFiles > 0 ? multiConcernFiles / totalFiles : 0;
118
+ const score = Math.round(Math.max(0, 100 - ratio * 300));
119
+ return {
120
+ name: "file-cohesion",
121
+ score,
122
+ grade: gradeFromScore(score),
123
+ details: {
124
+ premium: true,
125
+ totalFiles,
126
+ multiConcernFiles,
127
+ cacheHits,
128
+ candidates: candidates.map((c) => ({ path: c.path, concerns: c.concerns, lines: c.lines })),
129
+ },
130
+ issues,
131
+ duration: Date.now() - start,
132
+ };
133
+ }
134
+ async function analyzeCohesion(filePath, content, proKey) {
135
+ const truncated = content.slice(0, 4000);
136
+ try {
137
+ const res = await fetch("https://api.vibecodeqa.online/api/pro/file-cohesion", {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ Authorization: `Bearer ${proKey}`,
142
+ },
143
+ body: JSON.stringify({ file: filePath, content: truncated }),
144
+ });
145
+ if (!res.ok)
146
+ return null;
147
+ const data = (await res.json());
148
+ return {
149
+ concerns: data.concerns || [],
150
+ suggestion: data.suggestion || "",
151
+ };
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function loadCache(cwd) {
158
+ try {
159
+ const cachePath = join(cwd, ".vibe-check", "file-cohesion-cache.json");
160
+ if (existsSync(cachePath)) {
161
+ const data = JSON.parse(readFileSync(cachePath, "utf-8"));
162
+ if (data.version === 1)
163
+ return data;
164
+ }
165
+ }
166
+ catch { /* corrupt cache */ }
167
+ return { version: 1, files: {} };
168
+ }
169
+ function saveCache(cwd, cache) {
170
+ try {
171
+ const dir = join(cwd, ".vibe-check");
172
+ if (!existsSync(dir))
173
+ mkdirSync(dir, { recursive: true });
174
+ writeFileSync(join(dir, "file-cohesion-cache.json"), JSON.stringify(cache));
175
+ }
176
+ catch { /* write failed */ }
177
+ }
@@ -0,0 +1,14 @@
1
+ /** Frontend health — detects HTML/framework antipatterns in component code.
2
+ *
3
+ * Checks:
4
+ * 1. Conflicting UI frameworks (MUI + Tailwind, Chakra + Tailwind, etc.)
5
+ * 2. Large/unoptimized images (no width/height, no next/image, large src)
6
+ * 3. Inconsistent icon systems (mixing lucide + heroicons + fontawesome)
7
+ * 4. Bundle-heavy imports (entire libraries instead of specific components)
8
+ * 5. Missing loading states (async data without suspense/skeleton)
9
+ * 6. Hardcoded strings (potential i18n issues)
10
+ * 7. Missing meta tags / head management
11
+ * 8. DOM nesting violations (div in p, button in a)
12
+ */
13
+ import type { CheckResult } from "../types.js";
14
+ export declare function runFrontendHealth(cwd: string): CheckResult;
@@ -0,0 +1,206 @@
1
+ /** Frontend health — detects HTML/framework antipatterns in component code.
2
+ *
3
+ * Checks:
4
+ * 1. Conflicting UI frameworks (MUI + Tailwind, Chakra + Tailwind, etc.)
5
+ * 2. Large/unoptimized images (no width/height, no next/image, large src)
6
+ * 3. Inconsistent icon systems (mixing lucide + heroicons + fontawesome)
7
+ * 4. Bundle-heavy imports (entire libraries instead of specific components)
8
+ * 5. Missing loading states (async data without suspense/skeleton)
9
+ * 6. Hardcoded strings (potential i18n issues)
10
+ * 7. Missing meta tags / head management
11
+ * 8. DOM nesting violations (div in p, button in a)
12
+ */
13
+ import { getProductionFiles, readDeps } from "../fs-utils.js";
14
+ import { gradeFromScore } from "../types.js";
15
+ // UI framework conflicts
16
+ const UI_FRAMEWORKS = [
17
+ { name: "MUI", deps: ["@mui/material", "@mui/system", "@material-ui/core"] },
18
+ { name: "Tailwind", deps: ["tailwindcss"] },
19
+ { name: "Chakra", deps: ["@chakra-ui/react"] },
20
+ { name: "Ant Design", deps: ["antd"] },
21
+ { name: "Bootstrap", deps: ["react-bootstrap", "bootstrap"] },
22
+ { name: "Mantine", deps: ["@mantine/core"] },
23
+ { name: "Radix", deps: ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu", "@radix-ui/themes"] },
24
+ { name: "shadcn/ui", deps: ["cmdk", "vaul"] }, // shadcn uses these + Tailwind
25
+ { name: "DaisyUI", deps: ["daisyui"] },
26
+ ];
27
+ // Icon library conflicts
28
+ const ICON_LIBS = [
29
+ { name: "lucide", patterns: [/from\s+["']lucide-react["']/, /from\s+["']lucide-vue-next["']/] },
30
+ { name: "heroicons", patterns: [/from\s+["']@heroicons\/react/, /from\s+["']@heroicons\/vue/] },
31
+ { name: "fontawesome", patterns: [/from\s+["']@fortawesome/, /fa-\w+/] },
32
+ { name: "phosphor", patterns: [/from\s+["']@phosphor-icons/] },
33
+ { name: "tabler", patterns: [/from\s+["']@tabler\/icons/] },
34
+ { name: "react-icons", patterns: [/from\s+["']react-icons\//] },
35
+ { name: "material-icons", patterns: [/from\s+["']@mui\/icons-material/] },
36
+ { name: "ionicons", patterns: [/from\s+["']ionicons/, /ion-icon/] },
37
+ ];
38
+ // Heavy full-library imports
39
+ const HEAVY_IMPORTS = [
40
+ { pattern: /import\s+\*\s+as\s+\w+\s+from\s+["']lodash["']/, message: "Import entire lodash — use lodash/specific or lodash-es" },
41
+ { pattern: /from\s+["']@mui\/icons-material["'](?!\/)/, message: "Import all MUI icons — import specific: @mui/icons-material/Add" },
42
+ { pattern: /from\s+["']react-icons["'](?!\/)/, message: "Import all react-icons — import specific: react-icons/fi" },
43
+ { pattern: /from\s+["']antd["']\s*;\s*$/, message: "Import all of antd — destructure: import { Button } from 'antd'" },
44
+ { pattern: /import\s+(?:moment|dayjs)\s+from/, message: "Date library imported fully — consider tree-shakeable alternative" },
45
+ ];
46
+ // DOM nesting violations
47
+ const NESTING_VIOLATIONS = [
48
+ { parent: "<p", child: "<div", message: "div inside p — invalid HTML nesting" },
49
+ { parent: "<p", child: "<p", message: "p inside p — invalid HTML nesting" },
50
+ { parent: "<a", child: "<a", message: "a inside a — nested links are invalid" },
51
+ { parent: "<button", child: "<button", message: "button inside button — invalid nesting" },
52
+ { parent: "<a", child: "<button", message: "button inside a — use one or the other" },
53
+ ];
54
+ export function runFrontendHealth(cwd) {
55
+ const start = Date.now();
56
+ const issues = [];
57
+ const files = getProductionFiles(cwd);
58
+ const deps = readDeps(cwd);
59
+ const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
60
+ if (componentFiles.length === 0) {
61
+ return {
62
+ name: "frontend-health",
63
+ score: 0,
64
+ grade: "F",
65
+ details: { skipped: true, reason: "no component files found" },
66
+ issues: [],
67
+ duration: Date.now() - start,
68
+ };
69
+ }
70
+ // 1. Conflicting UI frameworks
71
+ const activeFrameworks = [];
72
+ for (const fw of UI_FRAMEWORKS) {
73
+ if (fw.deps.some((d) => d in deps)) {
74
+ activeFrameworks.push(fw.name);
75
+ }
76
+ }
77
+ // Tailwind + Radix/shadcn is intentional (shadcn uses Tailwind)
78
+ const conflicts = activeFrameworks.filter((f) => f !== "Radix" && f !== "shadcn/ui" && f !== "DaisyUI");
79
+ if (conflicts.length > 1 && !(conflicts.length === 2 && conflicts.includes("Tailwind") && conflicts.includes("shadcn/ui"))) {
80
+ issues.push({
81
+ severity: "error",
82
+ message: `Conflicting UI frameworks: ${conflicts.join(" + ")} — pick one`,
83
+ rule: "framework-conflict",
84
+ });
85
+ }
86
+ // 2-8. Scan component files
87
+ const iconLibsUsed = new Set();
88
+ let unoptimizedImages = 0;
89
+ let missingLoadingStates = 0;
90
+ for (const f of componentFiles) {
91
+ const lines = f.content.split("\n");
92
+ for (let i = 0; i < lines.length; i++) {
93
+ const line = lines[i];
94
+ const trimmed = line.trim();
95
+ // Icon libraries used
96
+ for (const lib of ICON_LIBS) {
97
+ if (lib.patterns.some((p) => p.test(line))) {
98
+ iconLibsUsed.add(lib.name);
99
+ }
100
+ }
101
+ // Heavy imports
102
+ for (const heavy of HEAVY_IMPORTS) {
103
+ if (heavy.pattern.test(line)) {
104
+ issues.push({
105
+ severity: "warning",
106
+ message: heavy.message,
107
+ file: f.path,
108
+ line: i + 1,
109
+ rule: "heavy-import",
110
+ });
111
+ }
112
+ }
113
+ // Unoptimized images
114
+ if (/<img\s/.test(trimmed) && !trimmed.includes("next/image") && !trimmed.includes("Image")) {
115
+ if (!trimmed.includes("width") || !trimmed.includes("height")) {
116
+ unoptimizedImages++;
117
+ if (unoptimizedImages <= 3) {
118
+ issues.push({
119
+ severity: "warning",
120
+ message: "img without width/height — causes layout shift (use next/image or set dimensions)",
121
+ file: f.path,
122
+ line: i + 1,
123
+ rule: "unoptimized-image",
124
+ });
125
+ }
126
+ }
127
+ }
128
+ // Large image sources (base64 data URLs in JSX)
129
+ if (/src\s*=\s*["']data:image\/[^"']{10000}/.test(line)) {
130
+ issues.push({
131
+ severity: "error",
132
+ message: "Large base64 image inline — move to a file and import",
133
+ file: f.path,
134
+ line: i + 1,
135
+ rule: "inline-image",
136
+ });
137
+ }
138
+ // DOM nesting violations (heuristic — not a full parser)
139
+ for (const v of NESTING_VIOLATIONS) {
140
+ if (trimmed.includes(v.child) && i > 0) {
141
+ // Check previous 5 lines for unclosed parent
142
+ const context = lines.slice(Math.max(0, i - 5), i).join(" ");
143
+ const parentOpens = (context.match(new RegExp(v.parent.replace("<", "<"), "g")) || []).length;
144
+ const parentCloses = (context.match(new RegExp(v.parent.replace("<", "</"), "g")) || []).length;
145
+ if (parentOpens > parentCloses) {
146
+ issues.push({
147
+ severity: "warning",
148
+ message: v.message,
149
+ file: f.path,
150
+ line: i + 1,
151
+ rule: "dom-nesting",
152
+ });
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ // Missing loading states (has fetch/useQuery but no loading/skeleton/suspense)
159
+ if (/\b(fetch|useQuery|useSWR|useEffect.*fetch)\b/.test(f.content)) {
160
+ if (!/\b(loading|isLoading|Skeleton|Spinner|Suspense|fallback)\b/.test(f.content)) {
161
+ missingLoadingStates++;
162
+ if (missingLoadingStates <= 3) {
163
+ issues.push({
164
+ severity: "info",
165
+ message: "Async data fetch without visible loading state",
166
+ file: f.path,
167
+ rule: "no-loading-state",
168
+ });
169
+ }
170
+ }
171
+ }
172
+ }
173
+ // Icon system conflicts
174
+ if (iconLibsUsed.size > 1) {
175
+ issues.push({
176
+ severity: "warning",
177
+ message: `Mixed icon libraries: ${[...iconLibsUsed].join(", ")} — pick one for consistency`,
178
+ rule: "mixed-icons",
179
+ });
180
+ }
181
+ // Summary issues
182
+ if (unoptimizedImages > 3) {
183
+ issues.push({
184
+ severity: "warning",
185
+ message: `${unoptimizedImages} images without dimensions — all cause layout shift`,
186
+ rule: "unoptimized-image",
187
+ });
188
+ }
189
+ // Score
190
+ const errorCount = issues.filter((i) => i.severity === "error").length;
191
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
192
+ const score = Math.max(0, 100 - errorCount * 25 - warnCount * 10);
193
+ return {
194
+ name: "frontend-health",
195
+ score,
196
+ grade: gradeFromScore(score),
197
+ details: {
198
+ componentFiles: componentFiles.length,
199
+ activeFrameworks,
200
+ iconLibsUsed: [...iconLibsUsed],
201
+ unoptimizedImages,
202
+ },
203
+ issues,
204
+ duration: Date.now() - start,
205
+ };
206
+ }
@@ -0,0 +1,8 @@
1
+ /** HTML quality — checks static HTML sites for meta tags, images, links, a11y, performance, SEO, security.
2
+ *
3
+ * Activates when the project has .html files. Works alongside framework checks —
4
+ * catches issues in static sites, landing pages, and docs that framework-specific
5
+ * checks miss entirely.
6
+ */
7
+ import type { CheckResult } from "../types.js";
8
+ export declare function runHtmlQuality(cwd: string): CheckResult;