react-doctor-cli-dev 1.0.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 (82) hide show
  1. package/backend/.env +3 -0
  2. package/backend/dist/index.js +43 -0
  3. package/backend/dist/middleware/auth.js +16 -0
  4. package/backend/dist/routes/reports.js +93 -0
  5. package/backend/package-lock.json +2000 -0
  6. package/backend/package.json +30 -0
  7. package/backend/src/db.ts +24 -0
  8. package/backend/src/index.ts +49 -0
  9. package/backend/src/middleware/auth.ts +21 -0
  10. package/backend/src/routes/reports.ts +110 -0
  11. package/backend/tsconfig.json +12 -0
  12. package/cli/bin/react-doctor.js +29 -0
  13. package/cli/dist/commands/analyze.js +125 -0
  14. package/cli/dist/commands/full.js +366 -0
  15. package/cli/dist/commands/install.js +138 -0
  16. package/cli/dist/commands/profile.js +166 -0
  17. package/cli/dist/index.js +78 -0
  18. package/cli/dist/ui.js +113 -0
  19. package/cli/package-lock.json +936 -0
  20. package/cli/package.json +34 -0
  21. package/cli/src/commands/analyze.ts +162 -0
  22. package/cli/src/commands/full.ts +574 -0
  23. package/cli/src/commands/install.ts +163 -0
  24. package/cli/src/commands/profile.ts +246 -0
  25. package/cli/src/index.ts +84 -0
  26. package/cli/src/ui.ts +120 -0
  27. package/cli/tsconfig.json +16 -0
  28. package/core/report-compiler/index.ts +359 -0
  29. package/core/report-compiler/test-report-compiler.ts +126 -0
  30. package/core/rule-engine/context-builder.ts +146 -0
  31. package/core/rule-engine/evaluator.ts +131 -0
  32. package/core/rule-engine/index.ts +222 -0
  33. package/core/rule-engine/rules.json +304 -0
  34. package/core/rule-engine/suggestion-builder.ts +209 -0
  35. package/core/rule-engine/test-rule-engine.ts +144 -0
  36. package/core/rule-engine/types.ts +202 -0
  37. package/core/runtime/profiler/browser.ts +121 -0
  38. package/core/runtime/profiler/collectors.ts +216 -0
  39. package/core/runtime/profiler/index.ts +311 -0
  40. package/core/runtime/profiler/porfiler.ts +967 -0
  41. package/core/runtime/profiler/route-scanner.ts +76 -0
  42. package/core/runtime/profiler/score.ts +59 -0
  43. package/core/runtime/profiler/server.ts +115 -0
  44. package/core/runtime/profiler/types.ts +65 -0
  45. package/core/runtime/test-runtime-profiler.ts +226 -0
  46. package/core/static-ana/static/analyzer.ts +145 -0
  47. package/core/static-ana/static/ast-parser.ts +31 -0
  48. package/core/static-ana/static/detectors/console-log.ts +49 -0
  49. package/core/static-ana/static/detectors/dead-code.ts +51 -0
  50. package/core/static-ana/static/detectors/effect-loop.ts +45 -0
  51. package/core/static-ana/static/detectors/index.ts +16 -0
  52. package/core/static-ana/static/detectors/inline-function.ts +59 -0
  53. package/core/static-ana/static/detectors/inline-style.ts +52 -0
  54. package/core/static-ana/static/detectors/large-component.ts +79 -0
  55. package/core/static-ana/static/detectors/missing-key.ts +56 -0
  56. package/core/static-ana/static/detectors/missing-memo.ts +59 -0
  57. package/core/static-ana/static/detectors/prop-drilling.ts +66 -0
  58. package/core/static-ana/static/helpers.ts +81 -0
  59. package/core/static-ana/static/scanner.ts +93 -0
  60. package/core/static-ana/test-analyzer.ts +115 -0
  61. package/core/static-ana/types.ts +25 -0
  62. package/core/tests/mock-react-project/src/app.tsx +22 -0
  63. package/core/tests/mock-react-project/src/components/Button.tsx +9 -0
  64. package/core/tests/mock-react-project/src/components/Header.tsx +3 -0
  65. package/core/tests/mock-react-project/src/components/ListTesting.tsx +51 -0
  66. package/core/tests/mock-react-project/src/components/UserDashboard.tsx +66 -0
  67. package/core/tests/mock-react-project/src/utils.ts +4 -0
  68. package/package.json +55 -0
  69. package/react-doctor-cli-dev-1.0.0.tgz +0 -0
  70. package/shared/dist/index.d.ts +2 -0
  71. package/shared/dist/index.js +19 -0
  72. package/shared/dist/schemas.d.ts +91 -0
  73. package/shared/dist/schemas.js +82 -0
  74. package/shared/dist/types.d.ts +44 -0
  75. package/shared/dist/types.js +2 -0
  76. package/shared/package-lock.json +47 -0
  77. package/shared/package.json +21 -0
  78. package/shared/src/index.ts +4 -0
  79. package/shared/src/schemas.ts +136 -0
  80. package/shared/src/types.ts +137 -0
  81. package/shared/tsconfig.json +15 -0
  82. package/tsconfig.json +25 -0
@@ -0,0 +1,76 @@
1
+ import path from "path";
2
+ import fs from "fs-extra";
3
+ import * as parser from "@babel/parser";
4
+ import * as traverseLib from "@babel/traverse";
5
+
6
+ const traverse: Function = (traverseLib as any).default ?? (traverseLib as any);
7
+
8
+ export class RouteScanner {
9
+ static async scanForRoutes(projectPath: string): Promise<string[]> {
10
+ const routes: string[] = ["/"];
11
+
12
+ const potentialFiles = [
13
+ path.join(projectPath, "src", "App.tsx"),
14
+ path.join(projectPath, "src", "App.jsx"),
15
+ path.join(projectPath, "src", "main.tsx"),
16
+ path.join(projectPath, "src", "main.jsx"),
17
+ path.join(projectPath, "src", "routes.tsx"),
18
+ path.join(projectPath, "src", "routes.jsx"),
19
+ path.join(projectPath, "src", "router.tsx"),
20
+ path.join(projectPath, "src", "router.jsx"),
21
+ ];
22
+
23
+ for (const filePath of potentialFiles) {
24
+ if (!fs.existsSync(filePath)) continue;
25
+
26
+ try {
27
+ const code = await fs.readFile(filePath, "utf-8");
28
+ const ast = parser.parse(code, {
29
+ sourceType: "module",
30
+ plugins: ["jsx", "typescript"],
31
+ });
32
+
33
+ traverse(ast, {
34
+ // ── React Router v5: <Route path="/foo" /> ──────────
35
+ JSXOpeningElement(p: any) {
36
+ const isRoute = (p.node.name as any).name === "Route";
37
+ if (isRoute) {
38
+ const pathAttr = p.node.attributes.find(
39
+ (attr: any) => attr.name?.name === "path",
40
+ );
41
+ if (
42
+ pathAttr &&
43
+ "value" in pathAttr &&
44
+ pathAttr.value?.type === "StringLiteral"
45
+ ) {
46
+ routes.push(pathAttr.value.value);
47
+ }
48
+ }
49
+ },
50
+
51
+ // ── React Router v6: createBrowserRouter([{ path: "/foo" }]) ──
52
+ ObjectExpression(p: any) {
53
+ const pathProp = p.node.properties.find(
54
+ (prop: any) =>
55
+ prop.type === "ObjectProperty" &&
56
+ prop.key?.name === "path" &&
57
+ prop.value?.type === "StringLiteral",
58
+ );
59
+ if (pathProp) {
60
+ const routePath = pathProp.value.value;
61
+ // Skip dynamic segments like /docs/:id — only add static base routes
62
+ if (!routePath.includes(":")) {
63
+ routes.push(routePath);
64
+ }
65
+ }
66
+ },
67
+ });
68
+ } catch {
69
+ // Skip files that fail to parse silently
70
+ }
71
+ }
72
+
73
+ const result = [...new Set(routes)];
74
+ return result.length > 0 ? result : ["/"];
75
+ }
76
+ }
@@ -0,0 +1,59 @@
1
+ import { WebVitals, PageError } from "../../../shared/src/types";
2
+
3
+ /**
4
+ * Calculates a 0–100 performance score from all collected metrics.
5
+ *
6
+ * Each metric is normalized to 0–100 based on its good/poor threshold,
7
+ * then a weighted average is computed. Weights must sum to 1.0:
8
+ *
9
+ * LCP 0.30 — biggest impact on perceived load speed
10
+ * Render time 0.20 — total time to interactive
11
+ * FCP 0.15 — first sign of life
12
+ * Commit avg 0.15 — React rendering efficiency
13
+ * TTFB 0.10 — server response speed
14
+ * CLS 0.05 — visual stability
15
+ * INP 0.05 — interaction responsiveness
16
+ *
17
+ * Score bands: 90–100 Excellent, 70–89 Good, 50–69 Needs Work, <50 Poor.
18
+ *
19
+ * Penalties:
20
+ * Each JS error → -5 points (capped at -20)
21
+ * Each warning → -2 points (capped at -10)
22
+ */
23
+ export function calculatePerformanceScore(
24
+ vitals: WebVitals,
25
+ renderTime: number,
26
+ commitDurations: number[],
27
+ errors: PageError[],
28
+ ): number {
29
+ // Normalize a value to 0–100 where lower is better
30
+ function normalize(value: number, good: number, poor: number): number {
31
+ if (value <= good) return 100;
32
+ if (value >= poor) return 0;
33
+ return Math.round(100 * (1 - (value - good) / (poor - good)));
34
+ }
35
+
36
+ const avgCommit = commitDurations.length > 0
37
+ ? commitDurations.reduce((a, b) => a + b, 0) / commitDurations.length
38
+ : 0;
39
+
40
+ const scores = {
41
+ lcp: normalize(vitals.lcp, 2500, 4000) * 0.30,
42
+ renderTime: normalize(renderTime, 2000, 5000) * 0.20,
43
+ fcp: normalize(vitals.fcp, 1800, 3000) * 0.15,
44
+ commitAvg: normalize(avgCommit, 16, 100) * 0.15,
45
+ ttfb: normalize(vitals.ttfb, 800, 1800) * 0.10,
46
+ cls: normalize(vitals.cls, 0.1, 0.25) * 0.05,
47
+ inp: normalize(vitals.inp, 200, 500) * 0.05,
48
+ };
49
+
50
+ let score = Object.values(scores).reduce((a, b) => a + b, 0);
51
+
52
+ const errorCount = errors.filter(e => e.type === "error").length;
53
+ const warningCount = errors.filter(e => e.type === "warning").length;
54
+ score -= Math.min(errorCount * 5, 20);
55
+ score -= Math.min(warningCount * 2, 10);
56
+
57
+ return Math.max(0, Math.min(100, Math.round(score)));
58
+ }
59
+
@@ -0,0 +1,115 @@
1
+ import { spawn, ChildProcess, execSync } from "child_process";
2
+ import path from "path";
3
+ import fs from "fs-extra";
4
+ import os from "os";
5
+
6
+ /**
7
+ * Detects which package manager the project uses by checking
8
+ * for lock files, then spawns the dev server as a background
9
+ * child process.
10
+ *
11
+ * Windows: shell:true so npm.cmd resolves correctly.
12
+ * Linux: tries /bin/bash first, falls back to /bin/sh if bash
13
+ * is not present (Alpine, minimal containers, etc).
14
+ */
15
+ export function spawnDevServer(projectPath: string): ChildProcess {
16
+ const isWin = os.platform() === "win32";
17
+
18
+ const pkgManager = fs.existsSync(path.join(projectPath, "yarn.lock"))
19
+ ? "yarn"
20
+ : fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"))
21
+ ? "pnpm"
22
+ : "npm";
23
+
24
+ console.log(`šŸ“¦ Starting ${pkgManager} dev server...`);
25
+
26
+ // On Linux, prefer bash but fall back to sh if bash is missing
27
+ const shell = isWin
28
+ ? true
29
+ : fs.existsSync("/bin/bash")
30
+ ? "/bin/bash"
31
+ : "/bin/sh";
32
+
33
+ return spawn(pkgManager, ["run", "dev"], {
34
+ cwd: projectPath,
35
+ shell,
36
+ env: {
37
+ ...process.env,
38
+ ...(isWin ? {} : { PATH: process.env.PATH + ":/usr/local/bin:/usr/bin:/bin" }),
39
+ },
40
+ detached: !isWin,
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ windowsHide: true,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Watches the dev server's stdout and stderr for a port number.
48
+ *
49
+ * Handles both "localhost:PORT" and "127.0.0.1:PORT" since
50
+ * some Linux setups print the IP instead of the hostname.
51
+ * Also handles "port PORT" format used by some dev servers.
52
+ */
53
+ export function waitForServer(devServer: ChildProcess): Promise<number> {
54
+ return new Promise((resolve, reject) => {
55
+ let resolved = false;
56
+
57
+ const timeout = setTimeout(
58
+ () => reject(new Error("ā±ļø Dev server timed out after 60 seconds!")),
59
+ 60000,
60
+ );
61
+
62
+ const onData = (data: Buffer) => {
63
+ // Strip ANSI color codes before running the regex
64
+ const output = data.toString().replace(/\x1B\[[0-9;]*[mGKHF]/g, "");
65
+ console.log(` ${output.trim()}`);
66
+
67
+ // Match "localhost:PORT", "127.0.0.1:PORT", or "port PORT"
68
+ const match =
69
+ output.match(/(?:localhost|127\.0\.0\.1):(\d+)/) ??
70
+ output.match(/port\s+(\d+)/i);
71
+
72
+ if (match && !resolved) {
73
+ resolved = true;
74
+ clearTimeout(timeout);
75
+ devServer.stdout?.off("data", onData);
76
+ devServer.stderr?.off("data", onData);
77
+ const port = parseInt(match[1], 10);
78
+ // 2-second buffer: Vite announces port before it's fully bound
79
+ setTimeout(() => resolve(port), 2000);
80
+ }
81
+ };
82
+
83
+ devServer.stdout?.on("data", onData);
84
+ devServer.stderr?.on("data", onData);
85
+
86
+ devServer.on("error", (err) => {
87
+ clearTimeout(timeout);
88
+ reject(new Error(`āŒ Dev server failed to start: ${err.message}`));
89
+ });
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Kills the dev server process tree cleanly on both platforms.
95
+ *
96
+ * Linux: process.kill(-pid) sends SIGTERM to the entire process group.
97
+ * Windows: taskkill /F /T kills the process tree.
98
+ * ESRCH: process already stopped — ignored safely.
99
+ */
100
+ export function killDevServer(devServer: ChildProcess): void {
101
+ if (!devServer.pid) return;
102
+
103
+ try {
104
+ if (os.platform() === "win32") {
105
+ spawn("taskkill", ["/pid", devServer.pid.toString(), "/f", "/t"]);
106
+ } else {
107
+ process.kill(-devServer.pid);
108
+ }
109
+ } catch (error) {
110
+ const err = error as any;
111
+ if (err.code !== "ESRCH") {
112
+ console.warn(` āš ļø Cleanup warning: ${err.message}`);
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,65 @@
1
+ export interface ReactProfilerData {
2
+ rerenders: Record<string, number>;
3
+ commitDurations: number[];
4
+ renderTime: number;
5
+ }
6
+
7
+ export type DeviceType = "desktop" | "mobile";
8
+ export type ThrottlePreset = "none" | "slow4g" | "3g";
9
+
10
+ /**
11
+ * CPU throttle rate for Chrome's Emulation.setCPUThrottlingRate.
12
+ * 1 = real hardware speed
13
+ * 4 = 4x slowdown — matches Lighthouse mobile preset (mid-range Android)
14
+ * 6 = 6x slowdown — low-end device simulation
15
+ *
16
+ * Works on localhost because it slows JS execution itself, not network.
17
+ */
18
+ export type CpuThrottle = 1 | 4 | 6;
19
+
20
+ export interface ProfileOptions {
21
+ // Single device or array to test both in one run
22
+ device?: DeviceType | DeviceType[];
23
+ throttle?: ThrottlePreset;
24
+ // 1 = no throttle (default), 4 = Lighthouse mobile, 6 = low-end
25
+ cpuThrottle?: CpuThrottle;
26
+ }
27
+
28
+ // ─────────────────────────────────────────────────────────────
29
+ // NETWORK PRESETS (Chrome DevTools Protocol values)
30
+ // downloadThroughput and uploadThroughput are in bytes/sec
31
+ // ─────────────────────────────────────────────────────────────
32
+ export const NETWORK_PRESETS = {
33
+ none: null,
34
+ slow4g: {
35
+ downloadThroughput: (9 * 1024 * 1024) / 8,
36
+ uploadThroughput: (750 * 1024) / 8,
37
+ latency: 170,
38
+ },
39
+ "3g": {
40
+ downloadThroughput: (1.5 * 1024 * 1024) / 8,
41
+ uploadThroughput: (750 * 1024) / 8,
42
+ latency: 300,
43
+ },
44
+ } as const;
45
+
46
+ // ─────────────────────────────────────────────────────────────
47
+ // DEVICE PRESETS
48
+ // ─────────────────────────────────────────────────────────────
49
+ export const DEVICE_PRESETS = {
50
+ desktop: {
51
+ viewport: { width: 1280, height: 720 },
52
+ userAgent: null,
53
+ hasTouch: false,
54
+ isMobile: false,
55
+ },
56
+ mobile: {
57
+ viewport: { width: 390, height: 844 }, // iPhone 12 Pro
58
+ userAgent:
59
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) " +
60
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 " +
61
+ "Mobile/15E148 Safari/604.1",
62
+ hasTouch: true,
63
+ isMobile: true,
64
+ },
65
+ } as const;
@@ -0,0 +1,226 @@
1
+ import { RuntimeProfiler } from "./profiler";
2
+
3
+ import path from "path";
4
+
5
+ // ─────────────────────────────────────────────────────────────
6
+ // STATUS HELPERS
7
+ // ─────────────────────────────────────────────────────────────
8
+
9
+ type VitalKey = "lcp" | "fcp" | "ttfb" | "inp" | "cls";
10
+
11
+ const vitalThresholds: Record<VitalKey, { good: number; poor: number }> = {
12
+ lcp: { good: 2500, poor: 4000 },
13
+ fcp: { good: 1800, poor: 3000 },
14
+ ttfb: { good: 800, poor: 1800 },
15
+ inp: { good: 200, poor: 500 },
16
+ cls: { good: 0.1, poor: 0.25 },
17
+ };
18
+
19
+ function getStatus(metric: string, value: number): string {
20
+ const limit = vitalThresholds[metric.toLowerCase() as VitalKey];
21
+ if (!limit) return "⚪ Unknown";
22
+ return value <= limit.good ? "🟢 Good" : value <= limit.poor ? "🟔 Needs Improvement" : "šŸ”“ Poor";
23
+ }
24
+
25
+ type HealthKey = "heap" | "weight" | "nodes";
26
+
27
+ const healthThresholds: Record<HealthKey, { good: number; poor: number }> = {
28
+ heap: { good: 150, poor: 300 },
29
+ weight: { good: 3, poor: 8 },
30
+ nodes: { good: 1500, poor: 3000 },
31
+ };
32
+
33
+ function getSystemStatus(metric: string, value: number): string {
34
+ const limit = healthThresholds[metric.toLowerCase() as HealthKey];
35
+ if (!limit) return "⚪ Unknown";
36
+ return value <= limit.good ? "🟢 Good" : value <= limit.poor ? "🟔 Needs Improvement" : "šŸ”“ Poor";
37
+ }
38
+
39
+ function getRenderTimeStatus(ms: number): string {
40
+ return ms <= 2000 ? "🟢 Good" : ms <= 4000 ? "🟔 Needs Improvement" : "šŸ”“ Poor";
41
+ }
42
+
43
+ // Score badge — clear visual band so you know at a glance how the app did
44
+ function getScoreBadge(score: number): string {
45
+ if (score >= 90) return `${score}/100 🟢 Excellent`;
46
+ if (score >= 70) return `${score}/100 🟔 Good`;
47
+ if (score >= 50) return `${score}/100 🟠 Needs Work`;
48
+ return `${score}/100 šŸ”“ Poor`;
49
+ }
50
+
51
+ function describeDevice(device: string): string {
52
+ return device === "mobile"
53
+ ? "šŸ“± Mobile (iPhone 12 Pro, 390Ɨ844)"
54
+ : "šŸ–„ļø Desktop (1280Ɨ720)";
55
+ }
56
+
57
+ function describeThrottle(throttle: string): string {
58
+ if (throttle === "3g") return "🐢 3G (1.5 Mbps / 300ms RTT)";
59
+ if (throttle === "slow4g") return "🐌 Slow 4G (9 Mbps / 170ms RTT)";
60
+ return "⚔ No throttle (localhost speed)";
61
+ }
62
+
63
+ function describeCpu(rate: number): string {
64
+ if (rate === 4) return "āš™ļø CPU 4x slowdown (Lighthouse mobile preset)";
65
+ if (rate === 6) return "āš™ļø CPU 6x slowdown (low-end device)";
66
+ return "āš™ļø CPU no throttle (real hardware speed)";
67
+ }
68
+
69
+ // ─────────────────────────────────────────────────────────────
70
+ // AUDIT OPTIONS
71
+ // ─────────────────────────────────────────────────────────────
72
+
73
+ // device: "desktop" | "mobile" | ["desktop", "mobile"]
74
+ // throttle: "none" | "slow4g" | "3g"
75
+ // āš ļø Only meaningful against deployed URLs, not localhost.
76
+ //
77
+ // cpuThrottle: 1 (default) | 4 (Lighthouse mobile) | 6 (low-end)
78
+ // āœ… Works on localhost — slows JS execution, not data transfer.
79
+ // Use 4 to simulate what a mid-range Android phone feels like.
80
+ const AUDIT_OPTIONS = {
81
+ device: ["desktop", "mobile"] as ("desktop" | "mobile")[],
82
+ throttle: "none" as const,
83
+ cpuThrottle: 1 as 1 | 4 | 6,
84
+ };
85
+
86
+ // ─────────────────────────────────────────────────────────────
87
+ // MAIN TEST
88
+ // ─────────────────────────────────────────────────────────────
89
+
90
+ export async function startTest() {
91
+ console.log("=========================================================");
92
+ console.log("🩺 REACT DOCTOR: FULL SMART DIAGNOSTIC 🩺");
93
+ console.log("=========================================================");
94
+
95
+ const targetProject = path.resolve(
96
+ process.argv[2] || process.env.TARGET_PROJECT || process.cwd(),
97
+ );
98
+
99
+ console.log(`šŸ“ Target: ${targetProject}`);
100
+
101
+ const deviceList = Array.isArray(AUDIT_OPTIONS.device)
102
+ ? AUDIT_OPTIONS.device
103
+ : [AUDIT_OPTIONS.device];
104
+ deviceList.forEach(d => console.log(describeDevice(d)));
105
+ console.log(describeThrottle(AUDIT_OPTIONS.throttle));
106
+ console.log(describeCpu(AUDIT_OPTIONS.cpuThrottle));
107
+
108
+ const profiler = new RuntimeProfiler(targetProject);
109
+
110
+ try {
111
+ const masterReport = await profiler.profile([], AUDIT_OPTIONS);
112
+ const routesFound = Object.keys(masterReport);
113
+
114
+ if (routesFound.length === 0) {
115
+ console.log("\nāš ļø No routes were audited.");
116
+ return;
117
+ }
118
+
119
+ routesFound.forEach((key) => {
120
+ const report = masterReport[key];
121
+ const [route] = key.includes("::") ? key.split("::") : [key];
122
+
123
+ console.log(`\nšŸ“ ROUTE: ${route}`);
124
+ console.log(` Device: ${report.deviceType} | CPU: ${report.cpuThrottling}x | Network: ${AUDIT_OPTIONS.throttle}`);
125
+
126
+ // ── PERFORMANCE SCORE ───────────────────────────────────
127
+ console.log(`\nšŸ† PERFORMANCE SCORE: ${getScoreBadge(report.performanceScore)}`);
128
+ console.log("---------------------------------------------------------");
129
+
130
+ // ── TABLE 1: WEB VITALS ─────────────────────────────────
131
+ console.log("⚔ SPEED METRICS");
132
+ console.table({
133
+ "LCP (Paint)": { Value: `${report.metrics.lcp.toFixed(0)}ms`, Status: getStatus("lcp", report.metrics.lcp) },
134
+ "FCP (Content)": { Value: `${report.metrics.fcp.toFixed(0)}ms`, Status: getStatus("fcp", report.metrics.fcp) },
135
+ "TTFB (Server)": { Value: `${report.metrics.ttfb.toFixed(0)}ms`, Status: getStatus("ttfb", report.metrics.ttfb) },
136
+ "CLS (Stability)":{ Value: report.metrics.cls.toFixed(3), Status: getStatus("cls", report.metrics.cls) },
137
+ "INP (Response)": { Value: `${report.metrics.inp.toFixed(0)}ms`, Status: getStatus("inp", report.metrics.inp) },
138
+ });
139
+
140
+ // ── TABLE 2: SYSTEM HEALTH ──────────────────────────────
141
+ console.log("🩺 SYSTEM HEALTH");
142
+ console.table({
143
+ "Memory (RAM)": { Value: `${report.stats.jsHeapMB} MB`, Status: getSystemStatus("heap", parseFloat(report.stats.jsHeapMB)) },
144
+ "Page Weight": { Value: `${report.stats.payloadMB} MB`, Status: getSystemStatus("weight", parseFloat(report.stats.payloadMB)) },
145
+ "DOM Nodes": { Value: report.stats.domNodes, Status: getSystemStatus("nodes", report.stats.domNodes) },
146
+ "Render Time": { Value: `${report.renderTime}ms`, Status: getRenderTimeStatus(report.renderTime) },
147
+ });
148
+
149
+ if (report.stats.topOffender) {
150
+ console.log(`šŸ” TOP OFFENDER: ${report.stats.topOffender.name} (${report.stats.topOffender.size.toFixed(2)} MB)`);
151
+ }
152
+
153
+ // ── TABLE 3: REACT PROFILER ─────────────────────────────
154
+ console.log("\nāš›ļø REACT PROFILER");
155
+
156
+ if (report.commitDurations.length > 0) {
157
+ const total = report.commitDurations.reduce((a, b) => a + b, 0);
158
+ const avg = (total / report.commitDurations.length).toFixed(2);
159
+ const max = Math.max(...report.commitDurations).toFixed(2);
160
+ const slow = report.commitDurations.filter(d => d > 16).length;
161
+
162
+ console.log(` Commits: ${report.commitDurations.length} total`);
163
+ console.log(` Avg commit: ${avg}ms`);
164
+ console.log(` Slowest: ${max}ms`);
165
+ if (slow > 0) {
166
+ console.log(` āš ļø ${slow} commit(s) exceeded 16ms (60fps budget)`);
167
+ }
168
+ } else {
169
+ console.log(" Commits: none recorded (app may not be in dev mode)");
170
+ }
171
+
172
+ const rerenderEntries = Object.entries(report.rerenders);
173
+ if (rerenderEntries.length > 0) {
174
+ console.log("\n Re-renders per component:");
175
+ const rerenderTable: Record<string, { Renders: number; Status: string }> = {};
176
+ for (const [name, count] of rerenderEntries.sort(([, a], [, b]) => b - a)) {
177
+ rerenderTable[name] = {
178
+ Renders: count,
179
+ Status: count >= 10 ? "šŸ”“ Excessive" : count >= 5 ? "🟔 High" : "🟢 Normal",
180
+ };
181
+ }
182
+ console.table(rerenderTable);
183
+ }
184
+
185
+ // ── ERRORS & WARNINGS ───────────────────────────────────
186
+ console.log("\nšŸ› ERRORS & WARNINGS");
187
+ if (report.errors.length === 0) {
188
+ console.log(" āœ… No errors or warnings detected");
189
+ } else {
190
+ const jsErrors = report.errors.filter(e => e.type === "error");
191
+ const warnings = report.errors.filter(e => e.type === "warning");
192
+
193
+ if (jsErrors.length > 0) {
194
+ console.log(` āŒ ${jsErrors.length} error(s):`);
195
+ jsErrors.forEach(e => console.log(` [${e.source}] ${e.message.slice(0, 120)}`));
196
+ }
197
+ if (warnings.length > 0) {
198
+ console.log(` āš ļø ${warnings.length} warning(s):`);
199
+ warnings.forEach(e => console.log(` [${e.source}] ${e.message.slice(0, 120)}`));
200
+ }
201
+ }
202
+
203
+ // ── SCREENSHOTS ─────────────────────────────────────────
204
+ console.log("\nšŸ“ø SCREENSHOTS");
205
+ if (report.screenshots.length === 0) {
206
+ console.log(" No screenshots captured");
207
+ } else {
208
+ report.screenshots.forEach(s => {
209
+ console.log(` ${s.label.padEnd(10)} → captured at ${s.takenAt}ms`);
210
+ });
211
+ console.log(` šŸ“ PNG files saved to: core/reports/screenshots/`);
212
+ }
213
+
214
+ console.log("---------------------------------------------------------");
215
+ });
216
+
217
+ console.log("\nāœ… Full Diagnostic Finished Successfully.");
218
+ } catch (error) {
219
+ console.error("\nāŒ PROFILER ERROR!");
220
+ console.error(error);
221
+ }
222
+ }
223
+
224
+ if (require.main === module) {
225
+ startTest();
226
+ }
@@ -0,0 +1,145 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ASTParser } from './ast-parser';
4
+ import { StaticReport, ComponentIssue } from '../../../shared/src/types';
5
+ import { ScannedFile } from './scanner';
6
+
7
+ // Import all detectors
8
+ import {
9
+ detectConsoleLogs,
10
+ detectLargeComponents,
11
+ detectInlineFunctions,
12
+ detectDeadCode,
13
+ detectInfiniteLoops,
14
+ detectInlineStyles,
15
+ detectMissingKeys,
16
+ detectPropDrilling,
17
+ detectMissingMemo
18
+ } from './detectors';
19
+
20
+ export class StaticAnalyzer {
21
+ private parser: ASTParser;
22
+
23
+ constructor() {
24
+ this.parser = new ASTParser();
25
+ }
26
+
27
+ /**
28
+ * Orchestrates the analysis of multiple files
29
+ */
30
+ async analyze(files: ScannedFile[]): Promise<StaticReport> {
31
+ console.log(`\n${"=".repeat(70)}`);
32
+ console.log(`šŸ” Static Analysis - Analyzing ${files.length} file(s)`);
33
+ console.log("=".repeat(70));
34
+
35
+ const allIssues: ComponentIssue[] = [];
36
+ let filesAnalyzed = 0;
37
+ let filesFailed = 0;
38
+
39
+ for (const file of files) {
40
+ try {
41
+ const code = fs.readFileSync(file.path, 'utf-8');
42
+ const ast = this.parser.parse(code, file.path);
43
+
44
+ const fileIssues = [
45
+ ...detectConsoleLogs(ast, file.relativePath),
46
+ ...detectLargeComponents(ast, file.relativePath),
47
+ ...detectInlineFunctions(ast, file.relativePath),
48
+ ...detectDeadCode(ast, file.relativePath),
49
+ ...detectInfiniteLoops(ast, file.relativePath),
50
+ ...detectInlineStyles(ast, file.relativePath),
51
+ ...detectMissingKeys(ast, file.relativePath),
52
+ ...detectPropDrilling(ast, file.relativePath),
53
+ ...detectMissingMemo(ast, file.relativePath),
54
+ ];
55
+
56
+ allIssues.push(...fileIssues);
57
+ filesAnalyzed++;
58
+
59
+ if (fileIssues.length > 0) {
60
+ console.log(` āš ļø ${file.relativePath.padEnd(50)} ${fileIssues.length} issue(s)`);
61
+ } else {
62
+ console.log(` āœ… ${file.relativePath}`);
63
+ }
64
+
65
+ } catch (error) {
66
+ filesFailed++;
67
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
68
+ console.error(` āŒ ${file.relativePath.padEnd(50)} ${errorMsg}`);
69
+ }
70
+ }
71
+
72
+ const grade = this.calculateGrade(allIssues, filesAnalyzed);
73
+
74
+ return {
75
+ timestamp: new Date().toISOString(),
76
+ componentCount: filesAnalyzed,
77
+ issues: allIssues,
78
+ filesAnalyzed,
79
+ filesFailed,
80
+ grade
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Persistence: Saves the results to static-report.json
86
+ */
87
+ saveReport(report: StaticReport): string {
88
+ // šŸ“ __dirname gives the directory of this current file.
89
+ // We go up two levels to reach the 'react_doctor' root, then into 'reports'.
90
+ const reportDir = path.resolve(__dirname, '../../reports');
91
+
92
+ // šŸ“ Ensure the directory exists (create it if missing)
93
+ if (!fs.existsSync(reportDir)) {
94
+ fs.mkdirSync(reportDir, { recursive: true });
95
+ }
96
+
97
+ const filePath = path.join(reportDir, 'staticreport.json');
98
+
99
+ try {
100
+ fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8');
101
+ console.log(`\nšŸ“„ Static Report saved to: ${filePath}`);
102
+ return filePath;
103
+ } catch (error) {
104
+ throw new Error(`Failed to save Static JSON report: ${error}`);
105
+ }
106
+ }
107
+
108
+ private calculateGrade(issues: ComponentIssue[], fileCount: number): string {
109
+ if (fileCount === 0) return 'A+';
110
+ const hasCritical = issues.some(i => i.severity === 'critical');
111
+
112
+ const totalPenalty = issues.reduce((acc, issue) => {
113
+ switch (issue.severity) {
114
+ case 'critical': return acc + 30;
115
+ case 'warning': return acc + 10;
116
+ case 'info': return acc + 2;
117
+ default: return acc;
118
+ }
119
+ }, 0);
120
+
121
+ const scorePerFile = totalPenalty / fileCount;
122
+
123
+ if (hasCritical) return 'F';
124
+ if (scorePerFile === 0) return 'A+';
125
+ if (scorePerFile < 5) return 'A';
126
+ if (scorePerFile < 15) return 'B';
127
+ if (scorePerFile < 30) return 'C';
128
+ if (scorePerFile < 50) return 'D';
129
+ return 'F';
130
+ }
131
+
132
+ getSummary(report: StaticReport) {
133
+ const critical = report.issues.filter(i => i.severity === 'critical').length;
134
+ const warnings = report.issues.filter(i => i.severity === 'warning').length;
135
+ const info = report.issues.filter(i => i.severity === 'info').length;
136
+
137
+ const byDetector: Record<string, number> = {};
138
+ report.issues.forEach(issue => {
139
+ const detectorType = issue.id.split('-')[0];
140
+ byDetector[detectorType] = (byDetector[detectorType] || 0) + 1;
141
+ });
142
+
143
+ return { critical, warnings, info, byDetector };
144
+ }
145
+ }