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,163 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // cli/src/commands/install.ts
3
+ //
4
+ // react-doctor install
5
+ //
6
+ // Installs React Doctor from GitHub into a React project.
7
+ // Runs the equivalent of:
8
+ //
9
+ // npm install --save-dev softar-dev/React_Doctor
10
+ //
11
+ // WHY THIS COMMAND EXISTS:
12
+ // When React Doctor is published and another developer wants
13
+ // to use it, they can run "npx react-doctor install" instead
14
+ // of having to remember the GitHub package name. It also
15
+ // detects their package manager (npm/yarn/pnpm) automatically.
16
+ //
17
+ // NOTE: This command is only useful once the GitHub repo is
18
+ // public and the project is ready for other developers to use.
19
+ // During development you run the tool directly from the
20
+ // react-tool/ folder with ts-node, so this command is not
21
+ // needed for your own workflow.
22
+ // ─────────────────────────────────────────────────────────────
23
+
24
+ import { Command } from "commander";
25
+ import { spawn } from "child_process";
26
+ import path from "path";
27
+ import fs from "fs";
28
+ import chalk from "chalk";
29
+ import {
30
+ printBanner, printSection,
31
+ printDone, printFail, printInfo, spinner,
32
+ } from "../ui";
33
+
34
+ // The GitHub package identifier — owner/repo format.
35
+ // npm, yarn, and pnpm all understand this without any registry.
36
+ const GITHUB_PACKAGE = "softar-dev/React_Doctor";
37
+
38
+ // ─────────────────────────────────────────────────────────────
39
+ // REGISTER COMMAND
40
+ // ─────────────────────────────────────────────────────────────
41
+
42
+ export function registerInstallCommand(program: Command): void {
43
+ program
44
+ .command("install")
45
+ .description(`Install React Doctor from GitHub (${GITHUB_PACKAGE})`)
46
+ .option(
47
+ "-p, --path <projectPath>",
48
+ "Path to the React project to install into (defaults to current directory)",
49
+ process.cwd(),
50
+ )
51
+ .option("--no-banner", "Skip the banner")
52
+ .action(async (options) => {
53
+
54
+ const projectPath = path.resolve(options.path);
55
+
56
+ if (!options.noBanner) printBanner();
57
+
58
+ // ── Validate this is a React project ────────────────────
59
+ const pkgJsonPath = path.join(projectPath, "package.json");
60
+ if (!fs.existsSync(pkgJsonPath)) {
61
+ printFail(
62
+ `No package.json found at: ${projectPath}\n\n` +
63
+ ` Make sure you are inside a React project, or pass the path:\n` +
64
+ ` react-doctor install --path ./my-react-app`,
65
+ );
66
+ process.exit(1);
67
+ }
68
+
69
+ // Warn if React is not in the project's dependencies
70
+ try {
71
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
72
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
73
+ if (!deps["react"]) {
74
+ console.log(chalk.yellow(
75
+ " ⚠️ React not found in this project's dependencies.\n" +
76
+ " Installing anyway — make sure this is a React project.\n",
77
+ ));
78
+ }
79
+ } catch {
80
+ // If we can't read package.json, just continue
81
+ }
82
+
83
+ // ── Detect package manager ───────────────────────────────
84
+ // Check for lock files to know which package manager the
85
+ // project uses. Order matters — check yarn and pnpm first
86
+ // since npm is the fallback.
87
+ const pkgManager =
88
+ fs.existsSync(path.join(projectPath, "yarn.lock")) ? "yarn" :
89
+ fs.existsSync(path.join(projectPath, "pnpm-lock.yaml")) ? "pnpm" :
90
+ "npm";
91
+
92
+ printSection("Installing React Doctor");
93
+ printInfo("Project", projectPath);
94
+ printInfo("Manager", pkgManager);
95
+ printInfo("Source", `github.com/${GITHUB_PACKAGE}`);
96
+ console.log();
97
+
98
+ // ── Build the install command ────────────────────────────
99
+ // The "owner/repo" format is understood natively by all
100
+ // three package managers — no npm registry needed.
101
+ let args: string[];
102
+ if (pkgManager === "yarn") {
103
+ args = ["add", "--dev", GITHUB_PACKAGE];
104
+ } else if (pkgManager === "pnpm") {
105
+ args = ["add", "--save-dev", GITHUB_PACKAGE];
106
+ } else {
107
+ args = ["install", "--save-dev", GITHUB_PACKAGE];
108
+ }
109
+
110
+ console.log(chalk.gray(` Running: `) + chalk.cyan(`${pkgManager} ${args.join(" ")}\n`));
111
+
112
+ const spin = spinner("Downloading from GitHub...");
113
+
114
+ await new Promise<void>((resolve, reject) => {
115
+ const isWin = process.platform === "win32";
116
+
117
+ const proc = spawn(pkgManager, args, {
118
+ cwd: projectPath,
119
+ shell: isWin ? true : "/bin/bash",
120
+ stdio: ["ignore", "pipe", "pipe"],
121
+ });
122
+
123
+ let output = "";
124
+ proc.stdout?.on("data", (d: Buffer) => { output += d.toString(); });
125
+ proc.stderr?.on("data", (d: Buffer) => { output += d.toString(); });
126
+
127
+ proc.on("close", (code) => {
128
+ if (code === 0) {
129
+ spin.succeed(chalk.green("Installed successfully!"));
130
+ resolve();
131
+ } else {
132
+ spin.fail(chalk.red("Installation failed."));
133
+ if (output) console.log(chalk.gray(`\n${output}`));
134
+ reject(new Error(`${pkgManager} exited with code ${code}`));
135
+ }
136
+ });
137
+
138
+ proc.on("error", reject);
139
+ });
140
+
141
+ // ── Show usage after install ─────────────────────────────
142
+ printSection("You are ready");
143
+ console.log(" Run these commands from inside your React project:\n");
144
+
145
+ const commands: [string, string][] = [
146
+ ["npx react-doctor full ./", "Run the full diagnostic (recommended)"],
147
+ ["npx react-doctor full ./ --mobile", "Include mobile viewport profiling"],
148
+ ["npx react-doctor analyze ./", "Static code analysis only (no Chrome needed)"],
149
+ ["npx react-doctor profile ./", "Runtime profiling only"],
150
+ ["npx react-doctor --help", "See all available options"],
151
+ ];
152
+
153
+ commands.forEach(([cmd, desc]) => {
154
+ console.log(
155
+ ` ${chalk.cyan(cmd.padEnd(44))}` +
156
+ chalk.gray(desc),
157
+ );
158
+ });
159
+
160
+ console.log();
161
+ printDone("React Doctor is installed and ready.");
162
+ });
163
+ }
@@ -0,0 +1,246 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // cli/src/commands/profile.ts
3
+ //
4
+ // react-doctor profile <projectPath>
5
+ //
6
+ // Runs the Runtime Profiler only — needs Chrome.
7
+ // Boots the React dev server, opens headless Chrome,
8
+ // measures Web Vitals + React profiler data, saves
9
+ // runtimereport.json to .react-doctor/
10
+ //
11
+ // Use this when:
12
+ // - You already ran static analysis and want performance data
13
+ // - You want a quick runtime check without code analysis
14
+ // - You want to compare desktop vs mobile performance
15
+ //
16
+ // Options:
17
+ // --mobile also profile on mobile viewport
18
+ // --cpu 4 simulate 4x CPU slowdown (Lighthouse preset)
19
+ // --throttle slow4g simulate slow network (deployed URLs only)
20
+ //
21
+ // Use "react-doctor full" to run everything together.
22
+ // ─────────────────────────────────────────────────────────────
23
+
24
+ import { Command } from "commander";
25
+ import path from "path";
26
+ import fs from "fs";
27
+ import chalk from "chalk";
28
+ import {
29
+ printBanner, printSection, printResult,
30
+ printDone, printFail, printInfo,
31
+ scoreBadge, vitalStatus, spinner,
32
+ } from "../ui";
33
+
34
+ function getCoreModule(relativePath: string) {
35
+ return require(path.resolve(__dirname, "..", "..", "..", "core", relativePath));
36
+ }
37
+
38
+ // ─────────────────────────────────────────────────────────────
39
+ // REGISTER COMMAND
40
+ // ─────────────────────────────────────────────────────────────
41
+
42
+ export function registerProfileCommand(program: Command): void {
43
+ program
44
+ .command("profile")
45
+ .description("Run the runtime profiler only (requires Chrome)")
46
+ .argument(
47
+ "[projectPath]",
48
+ "Path to the React project (defaults to current directory)",
49
+ process.cwd(),
50
+ )
51
+ .option(
52
+ "--desktop",
53
+ "Profile on desktop viewport 1280x720 (default if neither flag is passed)",
54
+ false,
55
+ )
56
+ .option(
57
+ "--mobile",
58
+ "Profile on mobile viewport — iPhone 12 Pro 390x844",
59
+ false,
60
+ )
61
+ .option(
62
+ "--cpu <rate>",
63
+ "CPU throttle: 1 (real speed) | 4 (Lighthouse mobile) | 6 (low-end)",
64
+ (v: string) => parseInt(v) as 1 | 4 | 6,
65
+ 1,
66
+ )
67
+ .option(
68
+ "--throttle <preset>",
69
+ "Network throttle: none | slow4g | 3g (only meaningful against deployed URLs)",
70
+ "none",
71
+ )
72
+ .option("--no-banner", "Skip the banner")
73
+ .action(async (projectPath: string, options) => {
74
+
75
+ const resolvedPath = path.resolve(projectPath);
76
+
77
+ if (!options.noBanner) printBanner();
78
+
79
+ // ── Validate project ────────────────────────────────────
80
+ if (!fs.existsSync(path.join(resolvedPath, "package.json"))) {
81
+ printFail(
82
+ `No package.json found at: ${resolvedPath}\n\n` +
83
+ ` Pass the path to your React project:\n` +
84
+ ` react-doctor profile ./my-react-app`,
85
+ );
86
+ process.exit(1);
87
+ }
88
+
89
+ // ── Determine devices ────────────────────────────────────
90
+ // --desktop and --mobile are independent flags.
91
+ // If neither is passed, desktop is the default.
92
+ // If only --mobile is passed, only mobile runs.
93
+ // If both are passed, both run in one pass.
94
+ const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
95
+ const wantMobile = options.mobile ?? false;
96
+
97
+ const devices: ("desktop" | "mobile")[] | "desktop" | "mobile" =
98
+ wantDesktop && wantMobile ? ["desktop", "mobile"] :
99
+ wantMobile ? "mobile" :
100
+ "desktop";
101
+
102
+ const deviceLabel =
103
+ wantDesktop && wantMobile ? "desktop + mobile" :
104
+ wantMobile ? "mobile" :
105
+ "desktop";
106
+
107
+ printSection("Runtime Profiler");
108
+ printInfo("Project", resolvedPath);
109
+ printInfo("Device", deviceLabel);
110
+ printInfo("CPU", `${options.cpu}x`);
111
+ printInfo("Network", options.throttle);
112
+ console.log();
113
+
114
+ // ── Run the profiler ────────────────────────────────────
115
+ const spin = spinner("Starting dev server...");
116
+
117
+ try {
118
+ const { RuntimeProfiler } = getCoreModule("runtime/profiler/index");
119
+
120
+ const profiler = new RuntimeProfiler(resolvedPath);
121
+ spin.text = " Launching headless Chrome...";
122
+
123
+ const runtimeReports = await profiler.profile([], {
124
+ device: devices,
125
+ throttle: options.throttle as "none" | "slow4g" | "3g",
126
+ cpuThrottle: options.cpu as 1 | 4 | 6,
127
+ });
128
+
129
+ // Save to .react-doctor/
130
+ const outputDir = path.join(resolvedPath, ".react-doctor");
131
+ fs.mkdirSync(outputDir, { recursive: true });
132
+ fs.writeFileSync(
133
+ path.join(outputDir, "runtimereport.json"),
134
+ JSON.stringify(runtimeReports, null, 2),
135
+ );
136
+
137
+ const routeKeys = Object.keys(runtimeReports);
138
+ spin.succeed(chalk.green(`Profiling complete — ${routeKeys.length} route/device combination(s)`));
139
+
140
+ // ── Results ─────────────────────────────────────────────
141
+ printSection("Results");
142
+
143
+ for (const [key, report] of Object.entries(runtimeReports) as [string, any][]) {
144
+ const [route, device] = key.includes("::") ? key.split("::") : [key, "desktop"];
145
+
146
+ console.log();
147
+ console.log(
148
+ ` ${chalk.bold("Route:")} ${route} ` +
149
+ `${chalk.gray(`[${device}]`)} ` +
150
+ `Score: ${scoreBadge(report.performanceScore)}`,
151
+ );
152
+ console.log();
153
+
154
+ // Web vitals
155
+ printResult("LCP", `${report.metrics.lcp.toFixed(0)}ms`, vitalStatus("lcp", report.metrics.lcp));
156
+ printResult("FCP", `${report.metrics.fcp.toFixed(0)}ms`, vitalStatus("fcp", report.metrics.fcp));
157
+ printResult("TTFB", `${report.metrics.ttfb.toFixed(0)}ms`, vitalStatus("ttfb", report.metrics.ttfb));
158
+ printResult("CLS", report.metrics.cls.toFixed(3), vitalStatus("cls", report.metrics.cls));
159
+ printResult("INP", `${report.metrics.inp.toFixed(0)}ms`, vitalStatus("inp", report.metrics.inp));
160
+ printResult("Render time", `${report.renderTime}ms`,
161
+ report.renderTime <= 2000 ? "good" : report.renderTime <= 4000 ? "warn" : "poor");
162
+
163
+ // React profiler
164
+ if (report.commitDurations?.length > 0) {
165
+ const avg = (report.commitDurations.reduce((a: number, b: number) => a + b, 0) /
166
+ report.commitDurations.length).toFixed(1);
167
+ const slow = report.commitDurations.filter((d: number) => d > 16).length;
168
+ printResult(
169
+ "React commits",
170
+ `${report.commitDurations.length} total, avg ${avg}ms`,
171
+ slow > 0 ? "warn" : "good",
172
+ );
173
+ }
174
+
175
+ // Top re-render component
176
+ const rerenderEntries = Object.entries(report.rerenders ?? {})
177
+ .sort(([, a], [, b]) => (b as number) - (a as number));
178
+ if (rerenderEntries.length > 0) {
179
+ const [topName, topCount] = rerenderEntries[0];
180
+ printResult(
181
+ "Most re-renders",
182
+ `${topName} (${topCount}x)`,
183
+ (topCount as number) >= 10 ? "poor" : (topCount as number) >= 5 ? "warn" : "good",
184
+ );
185
+ }
186
+
187
+ // System stats
188
+ printResult("Page weight", `${report.stats.payloadMB} MB`, "info");
189
+ printResult("JS heap", `${report.stats.jsHeapMB} MB`, "info");
190
+ printResult("DOM nodes", String(report.stats.domNodes), "info");
191
+
192
+ if (report.stats.topOffender) {
193
+ printResult(
194
+ "Heaviest file",
195
+ `${report.stats.topOffender.name} (${report.stats.topOffender.size.toFixed(2)} MB)`,
196
+ "warn",
197
+ );
198
+ }
199
+
200
+ // Errors
201
+ const errorCount = (report.errors ?? []).filter((e: any) => e.type === "error").length;
202
+ const warningCount = (report.errors ?? []).filter((e: any) => e.type === "warning").length;
203
+ if (errorCount > 0 || warningCount > 0) {
204
+ printResult(
205
+ "Issues",
206
+ `${errorCount} error(s) ${warningCount} warning(s)`,
207
+ errorCount > 0 ? "poor" : "warn",
208
+ );
209
+ (report.errors ?? []).slice(0, 3).forEach((e: any) => {
210
+ const icon = e.type === "error" ? chalk.red(" ✗") : chalk.yellow(" !");
211
+ console.log(`${icon} ${chalk.gray(e.message.slice(0, 90))}`);
212
+ });
213
+ } else {
214
+ printResult("Issues", "None detected", "good");
215
+ }
216
+
217
+ // Screenshots
218
+ if (report.screenshots?.length > 0) {
219
+ const labels = report.screenshots.map((s: any) => `${s.label}@${s.takenAt}ms`).join(" ");
220
+ printResult("Screenshots", labels, "info");
221
+ }
222
+
223
+ console.log();
224
+ }
225
+
226
+ printResult(
227
+ "Report saved",
228
+ path.join(resolvedPath, ".react-doctor", "runtimereport.json"),
229
+ "info",
230
+ );
231
+ console.log();
232
+
233
+ printDone("Runtime profiling finished.");
234
+ console.log(
235
+ chalk.gray(" Tip: run ") +
236
+ chalk.cyan("react-doctor full ./") +
237
+ chalk.gray(" to also get static analysis and improvement suggestions.\n"),
238
+ );
239
+
240
+ } catch (err: any) {
241
+ spin.fail(chalk.red("Runtime profiling failed"));
242
+ console.log(chalk.red(`\n ${err.message}\n`));
243
+ process.exit(1);
244
+ }
245
+ });
246
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────
3
+ // cli/src/index.ts
4
+ //
5
+ // The CLI entry point. This is the file that runs when the
6
+ // user types "react-doctor" in their terminal.
7
+ //
8
+ // HOW IT WORKS:
9
+ // 1. Commander.js parses the command and flags from argv
10
+ // 2. The matching command handler is called
11
+ // 3. The handler imports core modules and runs the pipeline
12
+ //
13
+ // HOW THE BINARY REGISTRATION WORKS:
14
+ // package.json has a "bin" field:
15
+ // "bin": { "react-doctor": "./dist/index.js" }
16
+ //
17
+ // After "npm link" (dev) or "npm install" (production),
18
+ // npm creates a symlink from the system's bin directory
19
+ // to this file. That's what makes "react-doctor" a real
20
+ // terminal command available anywhere.
21
+ //
22
+ // THE SHEBANG (#!/usr/bin/env node) on line 1:
23
+ // This tells the OS to run this file with Node.js when
24
+ // called directly as a script. Without it, the OS doesn't
25
+ // know which interpreter to use.
26
+ // ─────────────────────────────────────────────────────────────
27
+
28
+ import { Command } from "commander";
29
+ import { registerAnalyzeCommand } from "./commands/analyze";
30
+ import { registerProfileCommand } from "./commands/profile";
31
+ import { registerFullCommand } from "./commands/full";
32
+ import { registerInstallCommand } from "./commands/install";
33
+
34
+ const program = new Command();
35
+
36
+ // ── Program metadata ──────────────────────────────────────────
37
+ program
38
+ .name("react-doctor")
39
+ .description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
40
+ .version("1.0.0");
41
+
42
+ // ── Register all commands ─────────────────────────────────────
43
+ // Each function adds one command to the program.
44
+ // The order here is the order they appear in --help output.
45
+
46
+ registerFullCommand(program); // react-doctor full
47
+ registerAnalyzeCommand(program); // react-doctor analyze
48
+ registerProfileCommand(program); // react-doctor profile
49
+ registerInstallCommand(program); // react-doctor install
50
+
51
+ // ── Usage examples shown at bottom of --help ─────────────────
52
+ program.addHelpText("after", `
53
+ Examples:
54
+ $ react-doctor full ./my-app Desktop only (default)
55
+ $ react-doctor full ./my-app --mobile Mobile only
56
+ $ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
57
+ $ react-doctor full ./my-app --cpu 4 Simulate slow Android device
58
+ $ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
59
+ $ react-doctor full ./my-app --throttle 3g Simulate 3G network
60
+ $ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
61
+ $ react-doctor full ./my-app --upload Upload results to dashboard
62
+
63
+ $ react-doctor analyze ./my-app Static code analysis only
64
+ $ react-doctor analyze ./my-app --full Static + runtime + rules
65
+
66
+ $ react-doctor profile ./my-app Desktop only (default)
67
+ $ react-doctor profile ./my-app --mobile Mobile only
68
+ $ react-doctor profile ./my-app --desktop --mobile Both devices
69
+ $ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
70
+ $ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
71
+ $ react-doctor profile ./my-app --throttle 3g Simulate 3G network
72
+
73
+ $ react-doctor install Install from GitHub into a project
74
+ $ react-doctor install --path ./my-app Install into a specific folder
75
+ `);
76
+
77
+ // ── Show help if called with no arguments ─────────────────────
78
+ // Without this, calling "react-doctor" with no command just
79
+ // exits silently, which is confusing. This prints help instead.
80
+ if (process.argv.length < 3) {
81
+ program.help();
82
+ }
83
+
84
+ program.parse(process.argv);
package/cli/src/ui.ts ADDED
@@ -0,0 +1,120 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // cli/src/ui.ts
3
+ //
4
+ // Shared terminal output helpers used by every command.
5
+ // All chalk colors, ora spinners, and print functions live
6
+ // here so the visual style is consistent across the CLI.
7
+ //
8
+ // WHY ONE FILE FOR ALL UI:
9
+ // If each command file had its own chalk/ora setup, changing
10
+ // a color or spacing would require editing every file. This
11
+ // way you change it once here and it applies everywhere.
12
+ // ─────────────────────────────────────────────────────────────
13
+
14
+ import chalk from "chalk";
15
+ import ora, { Ora } from "ora";
16
+
17
+ // ── Banner ────────────────────────────────────────────────────
18
+ // Printed once at the start of every command run.
19
+
20
+ export function printBanner(): void {
21
+ console.log();
22
+ console.log(chalk.cyan.bold(" ┌─────────────────────────────────┐"));
23
+ console.log(chalk.cyan.bold(" │ 🩺 React Doctor │"));
24
+ console.log(chalk.cyan.bold(" │ React Performance Analyzer │"));
25
+ console.log(chalk.cyan.bold(" └─────────────────────────────────┘"));
26
+ console.log();
27
+ }
28
+
29
+ // ── Section headers ───────────────────────────────────────────
30
+ // Visually separates sections within a command's output.
31
+
32
+ export function printSection(title: string): void {
33
+ console.log();
34
+ console.log(chalk.cyan.bold(` ── ${title} `).padEnd(58, "─"));
35
+ console.log();
36
+ }
37
+
38
+ // ── Spinner ───────────────────────────────────────────────────
39
+ // Returns a running ora spinner.
40
+ // Caller calls .succeed() or .fail() when done.
41
+
42
+ export function spinner(text: string): Ora {
43
+ return ora({ text: ` ${text}`, color: "cyan", spinner: "dots" }).start();
44
+ }
45
+
46
+ // ── Single result line ────────────────────────────────────────
47
+ // Prints one labeled metric with a colored status dot.
48
+
49
+ export function printResult(
50
+ label: string,
51
+ value: string,
52
+ status: "good" | "warn" | "poor" | "info" | "none" = "none",
53
+ ): void {
54
+ const dot =
55
+ status === "good" ? chalk.green("●") :
56
+ status === "warn" ? chalk.yellow("●") :
57
+ status === "poor" ? chalk.red("●") :
58
+ status === "info" ? chalk.blue("●") :
59
+ chalk.gray("·");
60
+
61
+ console.log(` ${dot} ${chalk.gray(label.padEnd(24))} ${value}`);
62
+ }
63
+
64
+ // ── Score badge ───────────────────────────────────────────────
65
+ // Formats a 0-100 score with a color band.
66
+
67
+ export function scoreBadge(score: number): string {
68
+ if (score >= 90) return chalk.green.bold(`${score}/100`) + chalk.green(" Excellent");
69
+ if (score >= 70) return chalk.yellow.bold(`${score}/100`) + chalk.yellow(" Good");
70
+ if (score >= 50) return chalk.hex("#FFA500").bold(`${score}/100`) + chalk.hex("#FFA500")(" Needs Work");
71
+ return chalk.red.bold(`${score}/100`) + chalk.red(" Poor");
72
+ }
73
+
74
+ // ── Severity icon ─────────────────────────────────────────────
75
+
76
+ export function severityIcon(s: string): string {
77
+ if (s === "critical") return chalk.red("❌");
78
+ if (s === "warning") return chalk.yellow("⚠️ ");
79
+ return chalk.blue("ℹ️ ");
80
+ }
81
+
82
+ // ── Done / fail ───────────────────────────────────────────────
83
+
84
+ export function printDone(message: string): void {
85
+ console.log();
86
+ console.log(chalk.green.bold(` ✅ ${message}`));
87
+ console.log();
88
+ }
89
+
90
+ export function printFail(message: string): void {
91
+ console.log();
92
+ console.log(chalk.red.bold(` ❌ ${message}`));
93
+ console.log();
94
+ }
95
+
96
+ // ── Muted info line ───────────────────────────────────────────
97
+
98
+ export function printInfo(label: string, value: string): void {
99
+ console.log(` ${chalk.gray(label.padEnd(16))} ${chalk.white(value)}`);
100
+ }
101
+
102
+ // ── Web vital threshold helper ────────────────────────────────
103
+ // Maps a metric value to good/warn/poor for colored output.
104
+
105
+ export function vitalStatus(
106
+ metric: "lcp" | "fcp" | "ttfb" | "inp" | "cls",
107
+ value: number,
108
+ ): "good" | "warn" | "poor" {
109
+ const thresholds: Record<string, { good: number; poor: number }> = {
110
+ lcp: { good: 2500, poor: 4000 },
111
+ fcp: { good: 1800, poor: 3000 },
112
+ ttfb: { good: 800, poor: 1800 },
113
+ inp: { good: 200, poor: 500 },
114
+ cls: { good: 0.1, poor: 0.25 },
115
+ };
116
+ const t = thresholds[metric];
117
+ if (value <= t.good) return "good";
118
+ if (value <= t.poor) return "warn";
119
+ return "poor";
120
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }