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,209 @@
1
+ // Takes a fired rule and the evaluation context, and produces
2
+ // a clean Suggestion object ready for the report.
3
+ //
4
+ // The key job of this file is PLACEHOLDER RESOLUTION.
5
+ // Rule messages in rules.json contain tokens like {metrics.lcp}
6
+ // that get replaced with real values from the context.
7
+ //
8
+ // Example:
9
+ // Template: "Your LCP is {metrics.lcp}ms."
10
+ // Context: metrics.lcp = 2396
11
+ // Output: "Your LCP is 2396ms."
12
+ //
13
+ // This makes every suggestion specific to the actual project
14
+ // being analyzed — not just generic advice.
15
+ // ─────────────────────────────────────────────────────────────
16
+
17
+ import { Suggestion } from "../../shared/src/types";
18
+ import { RuleDefinition, EvaluationContext } from "./types";
19
+
20
+ /**
21
+ * Builds a Suggestion object from a fired rule and the context.
22
+ *
23
+ * The returned Suggestion has:
24
+ * id — same as the rule id
25
+ * title — the rule title (no placeholders)
26
+ * description — the rule message with placeholders resolved
27
+ * severity — copied from the rule
28
+ * affectedComponent — the most relevant component name (if any)
29
+ * fix — the concrete fix instructions
30
+ *
31
+ * @param rule — the rule that fired
32
+ * @param context — the context used to resolve placeholders
33
+ */
34
+ export function buildSuggestion(
35
+ rule: RuleDefinition,
36
+ context: EvaluationContext,
37
+ ): Suggestion {
38
+ // Resolve all {placeholder} tokens in the message string.
39
+ // The fix string can also contain placeholders like {component}.
40
+ const description = resolvePlaceholders(rule.message, context);
41
+ const fix = resolvePlaceholders(rule.fix, context);
42
+
43
+ // Find the most relevant component name for this suggestion.
44
+ // We use this to populate the affectedComponent field, which
45
+ // the dashboard uses to highlight the specific component.
46
+ const affectedComponent = resolveAffectedComponent(rule, context);
47
+
48
+ return {
49
+ id: rule.id,
50
+ title: rule.title,
51
+ description,
52
+ severity: rule.severity,
53
+ fix,
54
+ ...(affectedComponent ? { affectedComponent } : {}),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Resolves {placeholder} tokens in a template string.
60
+ *
61
+ * Supported placeholders and what they resolve to:
62
+ *
63
+ * {metrics.lcp} → actual LCP value rounded to nearest ms
64
+ * {metrics.fcp} → actual FCP value rounded to nearest ms
65
+ * {metrics.cls} → CLS score rounded to 3 decimal places
66
+ * {metrics.inp} → INP value rounded to nearest ms
67
+ * {metrics.ttfb} → TTFB value rounded to nearest ms
68
+ * {renderTime} → total render time in ms
69
+ * {commitDuration} → slowest React commit duration in ms
70
+ * {avgCommit} → average commit duration in ms (1 decimal)
71
+ * {rerenders} → re-render count of the top component
72
+ * {component} → name of the most re-rendered component
73
+ * {domNodes} → number of DOM nodes
74
+ * {payloadMB} → page weight in MB (2 decimal places)
75
+ * {jsHeapMB} → JS heap usage in MB (2 decimal places)
76
+ * {errorCount} → number of JS errors
77
+ * {performanceScore}→ 0-100 performance score
78
+ *
79
+ * Unknown placeholders are left as-is so they don't silently
80
+ * disappear if there's a typo in rules.json.
81
+ */
82
+ function resolvePlaceholders(
83
+ template: string,
84
+ context: EvaluationContext,
85
+ ): string {
86
+ // Build a map of all known placeholder names → resolved values.
87
+ // We format numbers here so the messages read naturally.
88
+ const values: Record<string, string> = {
89
+ "metrics.lcp": `${Math.round(context.metrics.lcp)}`,
90
+ "metrics.fcp": `${Math.round(context.metrics.fcp)}`,
91
+ "metrics.cls": `${context.metrics.cls.toFixed(3)}`,
92
+ "metrics.inp": `${Math.round(context.metrics.inp)}`,
93
+ "metrics.ttfb": `${Math.round(context.metrics.ttfb)}`,
94
+ "renderTime": `${context.renderTime}`,
95
+ "commitDuration": `${context.slowestCommit.toFixed(1)}`,
96
+ "avgCommit": `${context.avgCommit.toFixed(1)}`,
97
+ "rerenders": `${context.topRerenderCount}`,
98
+ "component": context.topRerenderComponent || "unknown",
99
+ "domNodes": `${context.domNodes}`,
100
+ "payloadMB": `${context.payloadMB.toFixed(2)}`,
101
+ "jsHeapMB": `${context.jsHeapMB.toFixed(2)}`,
102
+ "errorCount": `${context.errorCount}`,
103
+ "slowCommitCount": `${context.slowCommitCount}`,
104
+ "performanceScore": `${context.performanceScore}`,
105
+ };
106
+
107
+ // Replace every {placeholder} in the template string.
108
+ // The regex matches anything between { and } and looks it up
109
+ // in the values map above.
110
+ return template.replace(/\{([^}]+)\}/g, (match, key) => {
111
+ // If the key is known, return its value. Otherwise keep the
112
+ // original {key} so the developer knows something is missing.
113
+ return values[key] ?? match;
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Determines which component is most relevant to this suggestion.
119
+ *
120
+ * Priority order:
121
+ * 1. If the rule is about re-renders, use the top re-render component
122
+ * 2. If there's a static issue matching the rule category, use that component
123
+ * 3. Otherwise return undefined (no specific component to blame)
124
+ *
125
+ * The affectedComponent name is shown in the dashboard to help
126
+ * the developer navigate directly to the problematic file.
127
+ */
128
+ function resolveAffectedComponent(
129
+ rule: RuleDefinition,
130
+ context: EvaluationContext,
131
+ ): string | undefined {
132
+ // For re-render related rules, the top re-render component is the target.
133
+ // These rules fire because a specific component is re-rendering too much,
134
+ // so we point the developer directly at that component.
135
+ //
136
+ // NOTE: "inline-function" is intentionally excluded here now — the new
137
+ // static-only "inline-functions-detected" rule should resolve its component
138
+ // from the static issues map below, not from the re-render data. The cross
139
+ // rule "inline-functions-with-high-rerenders" still uses the re-render data
140
+ // because it fires when both are true.
141
+ if (
142
+ rule.id.includes("excessive-rerender") ||
143
+ rule.id.includes("missing-memo") ||
144
+ rule.id.includes("slow-commit") ||
145
+ rule.id === "inline-functions-with-high-rerenders"
146
+ ) {
147
+ if (context.topRerenderComponent) {
148
+ return context.topRerenderComponent;
149
+ }
150
+ }
151
+
152
+ // For static rules (and cross rules with a static component), find the
153
+ // first matching issue's component name from the static report.
154
+ //
155
+ // The map below links each rule ID to the issue ID prefix it targets.
156
+ // Example: "missing-list-keys" targets issues starting with "missing-key"
157
+ // because generateIssueId('missing-key', file, line) is how the detector
158
+ // names them. startsWith() catches all of them regardless of file/line.
159
+ //
160
+ // FIX: Added "inline-functions-detected" for the new static-only rule.
161
+ // FIX: console-log and dead-code issues DO carry a component name from
162
+ // the detector — they were already in the map and working correctly.
163
+ const staticPrefixMap: Record<string, string> = {
164
+ "console-logs-in-production": "console-log",
165
+ "missing-list-keys": "missing-key",
166
+ "inline-styles-detected": "inline-style",
167
+ "prop-drilling-detected": "prop-drilling",
168
+ "dead-code-detected": "dead-code",
169
+ "effect-loop-risk": "effect-loop",
170
+ "missing-memo-with-rerenders": "missing-memo",
171
+ "large-component-with-slow-commit": "large-component",
172
+ "inline-functions-with-high-rerenders": "inline-function",
173
+ "inline-functions-detected": "inline-function", // FIX: new rule
174
+ "missing-memo-with-slow-commit": "missing-memo",
175
+ "prop-drilling-with-rerenders": "prop-drilling",
176
+ };
177
+
178
+ const prefix = staticPrefixMap[rule.id];
179
+ if (prefix) {
180
+ const matchingIssue = context.issues.find(i => i.id.startsWith(prefix));
181
+ if (matchingIssue?.component) {
182
+ return matchingIssue.component;
183
+ }
184
+ }
185
+
186
+ // Rules like slow-lcp, slow-ttfb, heavy-payload, high-dom-nodes,
187
+ // low-performance-score are page-level — no single component to blame.
188
+ return undefined;
189
+ }
190
+
191
+ /**
192
+ * Builds all suggestions from a list of fired rules.
193
+ * Sorts them by severity: critical → warning → info.
194
+ *
195
+ * @param firedRules — rules whose conditions evaluated to true
196
+ * @param context — the evaluation context for placeholder resolution
197
+ */
198
+ export function buildAllSuggestions(
199
+ firedRules: RuleDefinition[],
200
+ context: EvaluationContext,
201
+ ): Suggestion[] {
202
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
203
+
204
+ return firedRules
205
+ .map(rule => buildSuggestion(rule, context))
206
+ .sort((a, b) =>
207
+ (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3),
208
+ );
209
+ }
@@ -0,0 +1,144 @@
1
+ // Test runner for the Rule Engine.
2
+ // Reads the existing staticreport.json and runtimereport.json
3
+ // from core/reports/ and runs the Rule Engine against them.
4
+ //
5
+ // Usage:
6
+ // npx ts-node --compiler-options '{"module":"commonjs"}'
7
+ // rule-engine/test-rule-engine.ts
8
+ // ─────────────────────────────────────────────────────────────
9
+
10
+ import path from "path";
11
+ import fs from "fs-extra";
12
+ import { RuleEngine } from "./index";
13
+ import { StaticReport, RuntimeReport } from "../../shared/src/types";
14
+ import { RuleEngineResult } from "./types";
15
+
16
+ // ── Severity badge helpers ────────────────────────────────────
17
+
18
+ function severityIcon(s: string): string {
19
+ if (s === "critical") return "❌";
20
+ if (s === "warning") return "⚠️ ";
21
+ return "ℹ️ ";
22
+ }
23
+
24
+ function severityLabel(s: string): string {
25
+ if (s === "critical") return "CRITICAL";
26
+ if (s === "warning") return "WARNING";
27
+ return "INFO";
28
+ }
29
+
30
+ // ── Load reports ─────────────────────────────────────────────
31
+
32
+ async function loadReports(): Promise<{
33
+ staticReport: StaticReport | null;
34
+ runtimeReports: Record<string, RuntimeReport>;
35
+ }> {
36
+ // Reports live in core/reports/ — one level up from rule-engine/
37
+ const reportsDir = path.resolve(__dirname, "..", "reports");
38
+
39
+ const staticPath = path.join(reportsDir, "staticreport.json");
40
+ const runtimePath = path.join(reportsDir, "runtimereport.json");
41
+
42
+ let staticReport: StaticReport | null = null;
43
+ let runtimeReports: Record<string, RuntimeReport> = {};
44
+
45
+ // Load static report if it exists
46
+ if (fs.existsSync(staticPath)) {
47
+ staticReport = await fs.readJson(staticPath);
48
+ console.log(`✅ Loaded staticreport.json`);
49
+ } else {
50
+ console.log(`⚠️ staticreport.json not found — running without static data`);
51
+ }
52
+
53
+ // Load runtime report if it exists
54
+ if (fs.existsSync(runtimePath)) {
55
+ runtimeReports = await fs.readJson(runtimePath);
56
+ const routeCount = Object.keys(runtimeReports).length;
57
+ console.log(`✅ Loaded runtimereport.json (${routeCount} route(s))`);
58
+ } else {
59
+ console.log(`⚠️ runtimereport.json not found — running without runtime data`);
60
+ }
61
+
62
+ return { staticReport, runtimeReports };
63
+ }
64
+
65
+ // ── Print results ─────────────────────────────────────────────
66
+
67
+ function printResults(results: RuleEngineResult[]): void {
68
+ for (const result of results) {
69
+ console.log(`\n${"=".repeat(60)}`);
70
+ console.log(`📍 Route: ${result.route} [${result.device}]`);
71
+ console.log(`${"=".repeat(60)}`);
72
+ console.log(` Total suggestions: ${result.summary.total}`);
73
+ console.log(` ❌ Critical: ${result.summary.critical}`);
74
+ console.log(` ⚠️ Warnings: ${result.summary.warning}`);
75
+ console.log(` ℹ️ Info: ${result.summary.info}`);
76
+
77
+ if (result.suggestions.length === 0) {
78
+ console.log("\n ✅ No issues found — great work!\n");
79
+ continue;
80
+ }
81
+
82
+ console.log();
83
+
84
+ for (const suggestion of result.suggestions) {
85
+ const icon = severityIcon(suggestion.severity);
86
+ const label = severityLabel(suggestion.severity);
87
+
88
+ console.log(`${icon} [${label}] ${suggestion.title}`);
89
+
90
+ if (suggestion.affectedComponent) {
91
+ console.log(` Component: ${suggestion.affectedComponent}`);
92
+ }
93
+
94
+ console.log(` ${suggestion.description}`);
95
+ console.log();
96
+ console.log(` Fix:`);
97
+
98
+ // Print each line of the fix indented for readability
99
+ suggestion.fix.split("\n").forEach(line => {
100
+ console.log(` ${line}`);
101
+ });
102
+
103
+ console.log(`${"─".repeat(60)}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // ── Main ──────────────────────────────────────────────────────
109
+
110
+ async function main() {
111
+ console.log("=========================================================");
112
+ console.log("🧠 REACT DOCTOR — RULE ENGINE TEST");
113
+ console.log("=========================================================\n");
114
+
115
+ // Load reports from disk
116
+ const { staticReport, runtimeReports } = await loadReports();
117
+
118
+ if (!staticReport && Object.keys(runtimeReports).length === 0) {
119
+ console.error(
120
+ "\n❌ No reports found.\n" +
121
+ " Run the static analyzer or profiler first:\n" +
122
+ " npx ts-node ... static-ana/test-analyzer.ts\n" +
123
+ " npx ts-node ... runtime/test-runtime-profiler.ts <path>\n",
124
+ );
125
+ process.exit(1);
126
+ }
127
+
128
+ // Run the Rule Engine
129
+ const engine = new RuleEngine();
130
+ const results = await engine.run(staticReport, runtimeReports);
131
+
132
+ // Print results to terminal
133
+ printResults(results);
134
+
135
+ console.log("\n=========================================================");
136
+ console.log("✅ Rule Engine test complete.");
137
+ console.log(`📄 Suggestions saved to: core/reports/suggestions.json`);
138
+ console.log("=========================================================\n");
139
+ }
140
+
141
+ main().catch(err => {
142
+ console.error("\n❌ Rule Engine error:", err.message);
143
+ process.exit(1);
144
+ });
@@ -0,0 +1,202 @@
1
+ import { StaticReport, RuntimeReport, Suggestion } from "../../shared/src/types";
2
+ // ─────────────────────────────────────────────────────────────
3
+ // RULE DEFINITION
4
+ //
5
+ // This is the shape of each entry inside rules.json.
6
+ // When you add a new rule, you write it as this interface.
7
+ //
8
+ // Example rule:
9
+ // {
10
+ // "id": "slow-lcp",
11
+ // "category": "runtime",
12
+ // "severity": "critical",
13
+ // "title": "LCP is too slow",
14
+ // "condition": { "runtime": "metrics.lcp > 2500" },
15
+ // "message": "Your LCP is {metrics.lcp}ms. Consider lazy-loading your hero image.",
16
+ // "fix": "Add loading='lazy' to your largest above-the-fold image."
17
+ // }
18
+ // ─────────────────────────────────────────────────────────────
19
+
20
+ export interface RuleCondition {
21
+ // A JavaScript expression string evaluated against the runtime report.
22
+ // Example: "metrics.lcp > 2500"
23
+ // Example: "rerenders['Divider'] >= 5"
24
+ // Can be undefined if this rule doesn't check runtime data.
25
+ runtime?: string;
26
+
27
+ // A JavaScript expression string evaluated against the static report.
28
+ // Example: "issues.some(i => i.id.startsWith('missing-memo'))"
29
+ // Example: "issues.filter(i => i.id.startsWith('console-log')).length > 0"
30
+ // Can be undefined if this rule doesn't check static data.
31
+ static?: string;
32
+ }
33
+
34
+ export interface RuleDefinition {
35
+ // Unique identifier for this rule — used to deduplicate suggestions
36
+ // and reference specific rules in tests.
37
+ // Example: "slow-lcp", "missing-memo-with-rerenders"
38
+ id: string;
39
+
40
+ // Which category this rule belongs to:
41
+ // "runtime" — only uses data from the runtime profiler
42
+ // "static" — only uses data from the static analyzer
43
+ // "cross" — combines both reports for a deeper insight
44
+ // Cross rules are the most powerful — they catch
45
+ // problems that neither analysis alone could find.
46
+ category: "runtime" | "static" | "cross";
47
+
48
+ // How serious this problem is:
49
+ // "critical" — must fix, directly hurts users
50
+ // "warning" — should fix, noticeable impact
51
+ // "info" — nice to fix, minor improvement
52
+ severity: "critical" | "warning" | "info";
53
+
54
+ // Short headline shown in the dashboard and terminal output.
55
+ // Example: "LCP is too slow"
56
+ title: string;
57
+
58
+ // The condition(s) that must be true for this rule to fire.
59
+ // For "runtime" category: only condition.runtime is required.
60
+ // For "static" category: only condition.static is required.
61
+ // For "cross" category: BOTH must be true simultaneously.
62
+ condition: RuleCondition;
63
+
64
+ // The explanation message shown to the developer.
65
+ // Supports {placeholder} tokens that get replaced with real values.
66
+ // Available tokens:
67
+ // {metrics.lcp} — actual LCP value from the runtime report
68
+ // {metrics.fcp} — actual FCP value
69
+ // {renderTime} — total render time
70
+ // {component} — name of the affected component (if found)
71
+ // {rerenders} — re-render count of the affected component
72
+ // {commitDuration} — slowest commit duration
73
+ // Example: "Your LCP is {metrics.lcp}ms — users wait too long."
74
+ message: string;
75
+
76
+ // A concrete, actionable fix the developer can apply right now.
77
+ // Be specific — not "improve performance" but "add React.lazy() to..."
78
+ fix: string;
79
+ }
80
+
81
+ // ─────────────────────────────────────────────────────────────
82
+ // EVALUATION CONTEXT
83
+ //
84
+ // This object is assembled from both reports and passed to the
85
+ // evaluator. It contains everything a rule condition can access.
86
+ //
87
+ // When a condition string like "metrics.lcp > 2500" is evaluated,
88
+ // it runs as a JS expression with this object as its scope.
89
+ // ─────────────────────────────────────────────────────────────
90
+
91
+ export interface EvaluationContext {
92
+ // ── Runtime fields ──────────────────────────────────────────
93
+ // The 5 web vitals (all in ms except cls which is a score)
94
+ metrics: {
95
+ lcp: number;
96
+ fcp: number;
97
+ cls: number;
98
+ inp: number;
99
+ ttfb: number;
100
+ };
101
+
102
+ // Total time from navigation start to networkidle0 (ms)
103
+ renderTime: number;
104
+
105
+ // Re-render counts per component: { "Divider": 5, "App": 1, ... }
106
+ rerenders: Record<string, number>;
107
+
108
+ // How long each React commit took in ms: [40.2, 1.1, 8.7]
109
+ commitDurations: number[];
110
+
111
+ // Slowest single commit duration in ms (0 if no commits recorded)
112
+ slowestCommit: number;
113
+
114
+ // Average commit duration in ms (0 if no commits recorded)
115
+ avgCommit: number;
116
+
117
+ // Number of commits that exceeded the 16ms 60fps budget
118
+ slowCommitCount: number;
119
+
120
+ // Total number of DOM elements on the page
121
+ domNodes: number;
122
+
123
+ // Total page payload in MB (as a number, not a string)
124
+ payloadMB: number;
125
+
126
+ // JS heap usage in MB (as a number, not a string)
127
+ jsHeapMB: number;
128
+
129
+ // JS errors caught during page load
130
+ errors: Array<{ type: "error" | "warning"; message: string }>;
131
+
132
+ // Number of JS errors (shorthand for errors.filter(...).length)
133
+ errorCount: number;
134
+
135
+ // Number of console warnings
136
+ warningCount: number;
137
+
138
+ // The 0–100 performance score calculated by the profiler
139
+ performanceScore: number;
140
+
141
+ // ── Static fields ────────────────────────────────────────────
142
+ // All issues found by the static analyzer
143
+ issues: Array<{
144
+ id: string;
145
+ component: string;
146
+ file: string;
147
+ line: number;
148
+ severity: string;
149
+ message: string;
150
+ }>;
151
+
152
+ // Shorthand counts by severity
153
+ criticalIssueCount: number;
154
+ warningIssueCount: number;
155
+
156
+ // True if any issue ID starts with the given prefix.
157
+ // Used in conditions like: hasIssue('missing-memo')
158
+ // This is a function placed on the context object so conditions
159
+ // can call it: "hasIssue('missing-memo')"
160
+ hasIssue: (prefix: string) => boolean;
161
+
162
+ // Returns all issues whose ID starts with the given prefix.
163
+ // Used when you need the actual issue objects in a condition.
164
+ // Example: "getIssues('large-component').length > 0"
165
+ getIssues: (prefix: string) => EvaluationContext["issues"];
166
+
167
+ // The name of the component most affected (highest re-render count).
168
+ // Used in message placeholders: "Wrap {component} in React.memo()"
169
+ topRerenderComponent: string;
170
+
171
+ // The re-render count of the top component above
172
+ topRerenderCount: number;
173
+ }
174
+
175
+ // ─────────────────────────────────────────────────────────────
176
+ // RULE ENGINE RESULT
177
+ //
178
+ // The final output of the Rule Engine — what gets written to
179
+ // suggestions.json and passed to the Report Compiler.
180
+ // ─────────────────────────────────────────────────────────────
181
+
182
+ export interface RuleEngineResult {
183
+ // ISO timestamp of when the rule engine ran
184
+ timestamp: string;
185
+
186
+ // Which route was analyzed (e.g. "/" or "/about")
187
+ route: string;
188
+
189
+ // Which device the runtime report came from
190
+ device: string;
191
+
192
+ // All generated suggestions, sorted by severity (critical first)
193
+ suggestions: Suggestion[];
194
+
195
+ // Summary counts for quick display
196
+ summary: {
197
+ critical: number;
198
+ warning: number;
199
+ info: number;
200
+ total: number;
201
+ };
202
+ }
@@ -0,0 +1,121 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import fs from "fs-extra";
4
+ import { execSync } from "child_process";
5
+
6
+ /**
7
+ * Finds and returns the path to the Chrome/Chromium executable.
8
+ *
9
+ * Windows: checks common installation paths including per-user AppData.
10
+ * Linux: checks all known system paths + tries `which` as a fallback.
11
+ * macOS: checks the standard Applications folder.
12
+ *
13
+ * Throws a clear error if no browser is found.
14
+ */
15
+ export function getBrowserPath(): string {
16
+ const platform = os.platform();
17
+
18
+ if (platform === "win32") {
19
+ const winPaths = [
20
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
21
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
22
+ `C:\\Users\\${os.userInfo().username}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe`,
23
+ "C:\\Program Files\\Chromium\\Application\\chrome.exe",
24
+ ];
25
+ for (const p of winPaths) {
26
+ if (fs.existsSync(p)) return p;
27
+ }
28
+ throw new Error(
29
+ "❌ Chrome not found! Please install Google Chrome on Windows.\n" +
30
+ " Download: https://www.google.com/chrome/",
31
+ );
32
+ }
33
+
34
+ if (platform === "darwin") {
35
+ const macPaths = [
36
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
37
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
38
+ ];
39
+ for (const p of macPaths) {
40
+ if (fs.existsSync(p)) return p;
41
+ }
42
+ throw new Error(
43
+ "❌ Chrome not found! Please install Google Chrome on macOS.\n" +
44
+ " Download: https://www.google.com/chrome/",
45
+ );
46
+ }
47
+
48
+ // Linux — check all known install locations
49
+ const linuxPaths = [
50
+ "/usr/bin/google-chrome",
51
+ "/usr/bin/google-chrome-stable",
52
+ "/usr/bin/chromium",
53
+ "/usr/bin/chromium-browser",
54
+ "/usr/local/bin/chrome",
55
+ "/usr/local/bin/chromium",
56
+ "/opt/google/chrome/chrome",
57
+ "/opt/google/chrome/google-chrome",
58
+ "/snap/bin/chromium",
59
+ "/snap/bin/google-chrome",
60
+ ];
61
+
62
+ for (const p of linuxPaths) {
63
+ if (fs.existsSync(p)) return p;
64
+ }
65
+
66
+ // Last resort: try `which` to find it anywhere on PATH
67
+ for (const cmd of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]) {
68
+ try {
69
+ const result = execSync(`which ${cmd}`, { stdio: ["ignore", "pipe", "ignore"] })
70
+ .toString()
71
+ .trim();
72
+ if (result && fs.existsSync(result)) return result;
73
+ } catch {
74
+ // not found via which, try next
75
+ }
76
+ }
77
+
78
+ throw new Error(
79
+ "❌ No compatible browser found on Linux!\n\n" +
80
+ " Install one of the following:\n" +
81
+ " Ubuntu/Debian: sudo apt install chromium-browser\n" +
82
+ " Fedora: sudo dnf install chromium\n" +
83
+ " Snap: sudo snap install chromium\n" +
84
+ " Or install Google Chrome from: https://www.google.com/chrome/",
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Reads the web-vitals IIFE bundle from local node_modules.
90
+ *
91
+ * Uses { content } injection — NO network request, works offline.
92
+ */
93
+ export function getWebVitalsScript(projectPath: string, profilerDir: string): string {
94
+ const filename = "web-vitals.iife.js";
95
+
96
+ const candidates: string[] = [
97
+ path.resolve(profilerDir, "..", "..", "..", "node_modules", "web-vitals", "dist", filename),
98
+ path.resolve(profilerDir, "..", "..", "..", "..", "node_modules", "web-vitals", "dist", filename),
99
+ path.join(projectPath, "node_modules", "web-vitals", "dist", filename),
100
+ ];
101
+
102
+ try {
103
+ const pkgJson = require.resolve("web-vitals/package.json");
104
+ candidates.push(path.join(path.dirname(pkgJson), "dist", filename));
105
+ } catch {
106
+ // not resolvable from this module — fine, we have other candidates
107
+ }
108
+
109
+ for (const candidate of candidates) {
110
+ if (fs.existsSync(candidate)) {
111
+ console.log(` ✅ web-vitals loaded from disk (offline-safe)`);
112
+ return fs.readFileSync(candidate, "utf-8");
113
+ }
114
+ }
115
+
116
+ const searched = candidates.map(c => `\n ${c}`).join("");
117
+ throw new Error(
118
+ `❌ web-vitals not found.\n\n Searched in:${searched}\n\n` +
119
+ ` Fix: run "npm install web-vitals" inside react-tool/\n`,
120
+ );
121
+ }