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,366 @@
1
+ "use strict";
2
+ // ─────────────────────────────────────────────────────────────
3
+ // cli/src/commands/full.ts
4
+ //
5
+ // react-doctor full <projectPath>
6
+ //
7
+ // The main command. Runs the entire React Doctor pipeline:
8
+ //
9
+ // 1. FileScanner → finds all JSX/TSX files
10
+ // 2. StaticAnalyzer → detects bad code patterns
11
+ // 3. RuntimeProfiler → measures live browser performance
12
+ // 4. RuleEngine → combines both reports into suggestions
13
+ // 5. ReportCompiler → merges everything into finalreport.json
14
+ // 6. Upload → sends the report to the backend API
15
+ //
16
+ // HOW IMPORTS WORK:
17
+ // The CLI imports core modules directly as TypeScript classes.
18
+ // No shell commands, no spawning child processes, no ts-node
19
+ // inside ts-node. Everything runs in the same Node.js process,
20
+ // which means objects are passed between steps in memory —
21
+ // fast, clean, and no disk reads between steps.
22
+ //
23
+ // The core folder is 2 levels up from cli/src/commands/:
24
+ // cli/src/commands/full.ts
25
+ // → ../../.. = react-tool root
26
+ // → ../../../core = core folder
27
+ // ─────────────────────────────────────────────────────────────
28
+ var __importDefault = (this && this.__importDefault) || function (mod) {
29
+ return (mod && mod.__esModule) ? mod : { "default": mod };
30
+ };
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.registerFullCommand = registerFullCommand;
33
+ exports.runFullCommand = runFullCommand;
34
+ const path_1 = __importDefault(require("path"));
35
+ const fs_1 = __importDefault(require("fs"));
36
+ const chalk_1 = __importDefault(require("chalk"));
37
+ const axios_1 = __importDefault(require("axios"));
38
+ const child_process_1 = require("child_process");
39
+ const ui_1 = require("../ui");
40
+ // ── Core imports ──────────────────────────────────────────────
41
+ // These are the actual classes from the core folder.
42
+ // We use require() with a resolved path so they work whether
43
+ // the CLI is run from its own folder or from the project root.
44
+ function getCoreModule(relativePath) {
45
+ // __dirname = cli/src/commands/
46
+ // 3 levels up = react-tool root
47
+ // then into core/
48
+ return require(path_1.default.resolve(__dirname, "..", "..", "..", "core", relativePath));
49
+ }
50
+ // ─────────────────────────────────────────────────────────────
51
+ // REGISTER COMMAND
52
+ // ─────────────────────────────────────────────────────────────
53
+ function registerFullCommand(program) {
54
+ program
55
+ .command("full")
56
+ .description("Run the complete React Doctor diagnostic (static + runtime + rules)")
57
+ .argument("[projectPath]", "Path to the React project (defaults to current directory)", process.cwd())
58
+ .option("--desktop", "Profile on desktop viewport 1280x720 (default if neither flag is passed)", false)
59
+ .option("--mobile", "Profile on mobile viewport — iPhone 12 Pro 390x844", false)
60
+ .option("--cpu <rate>", "CPU throttle rate for profiler: 1 (real speed) | 4 (Lighthouse mobile) | 6 (low-end)", (v) => parseInt(v), 1)
61
+ .option("--throttle <preset>", "Network throttle: none | slow4g | 3g (only meaningful against deployed URLs)", "none")
62
+ .option("--upload", "Upload the final report to the React Doctor backend API", false)
63
+ .option("--api-url <url>", "Backend API URL to upload to", "http://localhost:3000")
64
+ // ✅ Single --api-key option (defined BEFORE .action)
65
+ .option("--api-key <key>", "API key for backend authentication (overrides REACT_DOCTOR_API_KEY env var)", process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this")
66
+ .option("--no-banner", "Skip the banner")
67
+ // ✅ .action() comes LAST, with no trailing semicolon/comment
68
+ .action(async (projectPath, options) => {
69
+ await runFullCommand(projectPath, options);
70
+ });
71
+ }
72
+ // ─────────────────────────────────────────────────────────────
73
+ // MAIN RUNNER
74
+ // Exported so other commands (analyze --full) can call it too.
75
+ // ─────────────────────────────────────────────────────────────
76
+ async function runFullCommand(projectPath, options = {}) {
77
+ const resolvedPath = path_1.default.resolve(projectPath);
78
+ if (!options.noBanner)
79
+ (0, ui_1.printBanner)();
80
+ // ── Validate that target is a React project ────────────────
81
+ if (!fs_1.default.existsSync(path_1.default.join(resolvedPath, "package.json"))) {
82
+ (0, ui_1.printFail)(`No package.json found at: ${resolvedPath}\n\n` +
83
+ ` Make sure you point to the root of a React project.\n` +
84
+ ` Example: react-doctor full ./my-react-app`);
85
+ process.exit(1);
86
+ }
87
+ // ── Determine device configuration ──────────────────────────
88
+ // --desktop and --mobile are independent flags.
89
+ // If neither is passed, desktop is the default.
90
+ // If only --mobile is passed, only mobile runs.
91
+ // If both are passed, both run in one pass.
92
+ const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
93
+ const wantMobile = options.mobile ?? false;
94
+ const devices = wantDesktop && wantMobile
95
+ ? ["desktop", "mobile"]
96
+ : wantMobile
97
+ ? "mobile"
98
+ : "desktop";
99
+ const deviceLabel = wantDesktop && wantMobile
100
+ ? "desktop + mobile"
101
+ : wantMobile
102
+ ? "mobile"
103
+ : "desktop";
104
+ const throttleLabel = options.throttle ?? "none";
105
+ const cpuLabel = options.cpu ?? 1;
106
+ (0, ui_1.printSection)("Full Diagnostic");
107
+ (0, ui_1.printInfo)("Project", resolvedPath);
108
+ (0, ui_1.printInfo)("Device", deviceLabel);
109
+ (0, ui_1.printInfo)("CPU", `${cpuLabel}x`);
110
+ (0, ui_1.printInfo)("Network", throttleLabel);
111
+ // ── Output directory ───────────────────────────────────────
112
+ // Reports are saved inside the user's project in a hidden
113
+ // .react-doctor/ folder — easy to find, easy to gitignore.
114
+ const outputDir = path_1.default.join(resolvedPath, ".react-doctor");
115
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
116
+ // ════════════════════════════════════════════════════════════
117
+ // STEP 1 — STATIC ANALYSIS
118
+ // ════════════════════════════════════════════════════════════
119
+ (0, ui_1.printSection)("Step 1 / 4 — Static Analysis");
120
+ let staticReport;
121
+ const staticSpin = (0, ui_1.spinner)("Scanning JSX/TSX source files...");
122
+ try {
123
+ const { FileScanner } = getCoreModule("static-ana/static/scanner");
124
+ const { StaticAnalyzer } = getCoreModule("static-ana/static/analyzer");
125
+ const scanner = new FileScanner();
126
+ const analyzer = new StaticAnalyzer();
127
+ const files = await scanner.findFiles(resolvedPath);
128
+ staticSpin.text = ` Analyzing ${files.length} file(s)...`;
129
+ staticReport = await analyzer.analyze(files);
130
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, "staticreport.json"), JSON.stringify(staticReport, null, 2));
131
+ const critical = staticReport.issues?.filter((i) => i.severity === "critical")
132
+ .length ?? 0;
133
+ const warnings = staticReport.issues?.filter((i) => i.severity === "warning")
134
+ .length ?? 0;
135
+ const infos = staticReport.issues?.filter((i) => i.severity === "info").length ??
136
+ 0;
137
+ const total = staticReport.issues?.length ?? 0;
138
+ staticSpin.succeed(chalk_1.default.green(`Static analysis complete — ${files.length} files scanned`));
139
+ (0, ui_1.printResult)("Files analyzed", String(staticReport.filesAnalyzed ?? 0), "info");
140
+ (0, ui_1.printResult)("Total issues", String(total), total > 0 ? "warn" : "good");
141
+ (0, ui_1.printResult)("Critical", String(critical), critical > 0 ? "poor" : "good");
142
+ (0, ui_1.printResult)("Warnings", String(warnings), warnings > 0 ? "warn" : "good");
143
+ (0, ui_1.printResult)("Info", String(infos), "info");
144
+ (0, ui_1.printResult)("Health grade", staticReport.grade ?? "N/A", "info");
145
+ }
146
+ catch (err) {
147
+ staticSpin.fail(chalk_1.default.red("Static analysis failed"));
148
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
149
+ staticReport = null;
150
+ }
151
+ // ════════════════════════════════════════════════════════════
152
+ // STEP 2 — RUNTIME PROFILING
153
+ // ════════════════════════════════════════════════════════════
154
+ (0, ui_1.printSection)("Step 2 / 4 — Runtime Profiler");
155
+ let runtimeReports = {};
156
+ const profilingSpin = (0, ui_1.spinner)("Starting dev server and launching Chrome...");
157
+ try {
158
+ const { RuntimeProfiler } = getCoreModule("runtime/profiler/index");
159
+ const profiler = new RuntimeProfiler(resolvedPath, outputDir);
160
+ profilingSpin.text = " Profiling... (this takes ~30 seconds per route)";
161
+ runtimeReports = await profiler.profile([], {
162
+ device: devices,
163
+ throttle: throttleLabel,
164
+ cpuThrottle: cpuLabel,
165
+ });
166
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, "runtimereport.json"), JSON.stringify(runtimeReports, null, 2));
167
+ const routeKeys = Object.keys(runtimeReports);
168
+ profilingSpin.succeed(chalk_1.default.green(`Profiling complete — ${routeKeys.length} route/device combination(s)`));
169
+ // Print results for each route
170
+ for (const [key, report] of Object.entries(runtimeReports)) {
171
+ const [route, device] = key.includes("::")
172
+ ? key.split("::")
173
+ : [key, "desktop"];
174
+ console.log();
175
+ console.log(` ${chalk_1.default.bold(route)} ${chalk_1.default.gray(`[${device}]`)} Score: ${(0, ui_1.scoreBadge)(report.performanceScore)}`);
176
+ // ── Device / CPU / Network line ──────────────────────────
177
+ console.log(` ${chalk_1.default.gray("Device:")} ${device} ` +
178
+ `${chalk_1.default.gray("CPU:")} ${report.cpuThrottling ?? cpuLabel}x ` +
179
+ `${chalk_1.default.gray("Network:")} ${throttleLabel}`);
180
+ (0, ui_1.printResult)("LCP", `${report.metrics.lcp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("lcp", report.metrics.lcp));
181
+ (0, ui_1.printResult)("FCP", `${report.metrics.fcp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("fcp", report.metrics.fcp));
182
+ (0, ui_1.printResult)("TTFB", `${report.metrics.ttfb.toFixed(0)}ms`, (0, ui_1.vitalStatus)("ttfb", report.metrics.ttfb));
183
+ (0, ui_1.printResult)("CLS", report.metrics.cls.toFixed(3), (0, ui_1.vitalStatus)("cls", report.metrics.cls));
184
+ (0, ui_1.printResult)("INP", `${report.metrics.inp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("inp", report.metrics.inp));
185
+ (0, ui_1.printResult)("Render time", `${report.renderTime}ms`, report.renderTime <= 2000
186
+ ? "good"
187
+ : report.renderTime <= 4000
188
+ ? "warn"
189
+ : "poor");
190
+ if ((report.errors ?? []).length > 0) {
191
+ const errs = report.errors.filter((e) => e.type === "error").length;
192
+ const warn = report.errors.filter((e) => e.type === "warning").length;
193
+ (0, ui_1.printResult)("Issues", `${errs} error(s) ${warn} warning(s)`, errs > 0 ? "poor" : "warn");
194
+ }
195
+ else {
196
+ (0, ui_1.printResult)("Issues", "None detected", "good");
197
+ }
198
+ }
199
+ }
200
+ catch (err) {
201
+ profilingSpin.fail(chalk_1.default.red("Runtime profiling failed"));
202
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
203
+ // Profiling failure is not fatal — rule engine can still run on static data
204
+ }
205
+ // ════════════════════════════════════════════════════════════
206
+ // STEP 3 — RULE ENGINE
207
+ // ════════════════════════════════════════════════════════════
208
+ (0, ui_1.printSection)("Step 3 / 4 — Rule Engine");
209
+ let ruleResults = [];
210
+ const ruleSpin = (0, ui_1.spinner)("Evaluating rules against both reports...");
211
+ try {
212
+ const { RuleEngine } = getCoreModule("rule-engine/index");
213
+ const engine = new RuleEngine(outputDir);
214
+ ruleResults = await engine.run(staticReport, runtimeReports);
215
+ const allSuggestions = ruleResults.flatMap((r) => r.suggestions);
216
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, "suggestions.json"), JSON.stringify(ruleResults, null, 2));
217
+ const total = allSuggestions.length;
218
+ const critical = allSuggestions.filter((s) => s.severity === "critical").length;
219
+ const warnings = allSuggestions.filter((s) => s.severity === "warning").length;
220
+ const infos = allSuggestions.filter((s) => s.severity === "info").length;
221
+ ruleSpin.succeed(chalk_1.default.green(`Rule Engine complete — ${total} suggestion(s) generated`));
222
+ (0, ui_1.printResult)("Critical", String(critical), critical > 0 ? "poor" : "good");
223
+ (0, ui_1.printResult)("Warnings", String(warnings), warnings > 0 ? "warn" : "good");
224
+ (0, ui_1.printResult)("Info", String(infos), "info");
225
+ if (total > 0) {
226
+ console.log();
227
+ console.log(chalk_1.default.gray(" Top suggestions:"));
228
+ allSuggestions.slice(0, 5).forEach((s) => {
229
+ const icon = (0, ui_1.severityIcon)(s.severity);
230
+ const comp = s.affectedComponent
231
+ ? chalk_1.default.cyan(` [${s.affectedComponent}]`)
232
+ : "";
233
+ console.log(` ${icon} ${s.title}${comp}`);
234
+ });
235
+ if (total > 5) {
236
+ console.log(chalk_1.default.gray(`\n ... and ${total - 5} more in the full report.`));
237
+ }
238
+ }
239
+ }
240
+ catch (err) {
241
+ ruleSpin.fail(chalk_1.default.red("Rule Engine failed"));
242
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
243
+ }
244
+ // ════════════════════════════════════════════════════════════
245
+ // STEP 4 — REPORT COMPILER
246
+ // ════════════════════════════════════════════════════════════
247
+ (0, ui_1.printSection)("Step 4 / 4 — Report Compiler");
248
+ let finalReport = null;
249
+ const compilerSpin = (0, ui_1.spinner)("Compiling final report...");
250
+ try {
251
+ const { ReportCompiler } = getCoreModule("report-compiler/index");
252
+ const compiler = new ReportCompiler(outputDir);
253
+ finalReport = await compiler.compile(staticReport, runtimeReports, ruleResults);
254
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, "finalreport.json"), JSON.stringify(finalReport, null, 2));
255
+ compilerSpin.succeed(chalk_1.default.green("Final report compiled"));
256
+ (0, ui_1.printResult)("Overall score", (0, ui_1.scoreBadge)(finalReport.performanceScore), "none");
257
+ (0, ui_1.printResult)("Report saved", path_1.default.join(outputDir, "finalreport.json"), "info");
258
+ }
259
+ catch (err) {
260
+ compilerSpin.fail(chalk_1.default.red("Report Compiler failed"));
261
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
262
+ }
263
+ // ════════════════════════════════════════════════════════════
264
+ // OPTIONAL — UPLOAD TO BACKEND API
265
+ // ════════════════════════════════════════════════════════════
266
+ if (options.upload && finalReport) {
267
+ (0, ui_1.printSection)("Uploading to Backend");
268
+ const uploadSpin = (0, ui_1.spinner)(`Connecting to ${options.apiUrl}...`);
269
+ try {
270
+ // Ensure apiUrl has a default
271
+ const apiUrl = options.apiUrl ?? "http://localhost:3000";
272
+ // 1. Check if backend is already running
273
+ try {
274
+ await axios_1.default.get(`${apiUrl}/health`, { timeout: 2000 });
275
+ uploadSpin.text = "Backend detected. Preparing upload...";
276
+ }
277
+ catch (err) {
278
+ // 2. If not running, start it automatically
279
+ // Determine the project root directory (where cli/ and backend/ are siblings)
280
+ const projectRoot = path_1.default.resolve(__dirname, "..", "..", "..");
281
+ // Backend is a sibling folder to cli at projectRoot
282
+ const backendRoot = path_1.default.resolve(projectRoot, "backend");
283
+ // Check for compiled JS first (for installed packages)
284
+ const backendDist = path_1.default.join(backendRoot, "dist", "index.js");
285
+ // Check for TS source (for local dev)
286
+ const backendSrc = path_1.default.join(backendRoot, "src", "index.ts");
287
+ let command;
288
+ let args;
289
+ if (fs_1.default.existsSync(backendDist)) {
290
+ command = "node";
291
+ args = [backendDist];
292
+ }
293
+ else if (fs_1.default.existsSync(backendSrc)) {
294
+ command = "npx";
295
+ args = ["ts-node", backendSrc];
296
+ }
297
+ else {
298
+ throw new Error(`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`);
299
+ }
300
+ uploadSpin.text = "Backend not found. Starting local server automatically...";
301
+ // Extract port safely
302
+ const port = new URL(apiUrl).port || "3000";
303
+ // Spawn the backend process
304
+ // Create backend data directory in the target project
305
+ const backendDataDir = path_1.default.join(outputDir, "backend-data");
306
+ fs_1.default.mkdirSync(backendDataDir, { recursive: true });
307
+ const backendProcess = (0, child_process_1.spawn)(command, args, {
308
+ stdio: "inherit",
309
+ env: {
310
+ ...process.env,
311
+ API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
312
+ PORT: port,
313
+ DB_PATH: path_1.default.join(backendDataDir, "reports.db"),
314
+ },
315
+ cwd: backendRoot
316
+ });
317
+ // 3. Wait for backend to be ready
318
+ let isReady = false;
319
+ let retries = 0;
320
+ while (!isReady && retries < 15) {
321
+ try {
322
+ await axios_1.default.get(`${apiUrl}/health`, { timeout: 1000 });
323
+ isReady = true;
324
+ }
325
+ catch {
326
+ await new Promise((r) => setTimeout(r, 1000));
327
+ retries++;
328
+ }
329
+ }
330
+ if (!isReady)
331
+ throw new Error("Backend failed to start after 15 seconds.");
332
+ uploadSpin.text = "Backend started successfully!";
333
+ }
334
+ // 4. Perform the actual upload
335
+ uploadSpin.text = "Uploading report...";
336
+ await axios_1.default.post(`${apiUrl}/api/reports/upload`, finalReport, {
337
+ headers: {
338
+ "Content-Type": "application/json",
339
+ "x-api-key": options.apiKey || "react-doctor-secret-key-change-this",
340
+ },
341
+ timeout: 10000,
342
+ });
343
+ uploadSpin.succeed(chalk_1.default.green("Report uploaded successfully"));
344
+ }
345
+ catch (err) {
346
+ uploadSpin.fail(chalk_1.default.yellow("Upload failed — report saved locally"));
347
+ console.log(chalk_1.default.gray(` ${err.message}`));
348
+ }
349
+ }
350
+ // ════════════════════════════════════════════════════════════
351
+ // FINAL SUMMARY
352
+ // ════════════════════════════════════════════════════════════
353
+ (0, ui_1.printSection)("Summary");
354
+ console.log(chalk_1.default.gray(" Reports saved to:"));
355
+ console.log(chalk_1.default.cyan(` ${path_1.default.join(outputDir, "staticreport.json")}`));
356
+ console.log(chalk_1.default.cyan(` ${path_1.default.join(outputDir, "runtimereport.json")}`));
357
+ console.log(chalk_1.default.cyan(` ${path_1.default.join(outputDir, "suggestions.json")}`));
358
+ console.log(chalk_1.default.cyan(` ${path_1.default.join(outputDir, "finalreport.json")}`));
359
+ console.log();
360
+ if (!options.upload) {
361
+ console.log(chalk_1.default.gray(" Tip: add ") +
362
+ chalk_1.default.cyan("--upload") +
363
+ chalk_1.default.gray(" to send results to the dashboard."));
364
+ }
365
+ (0, ui_1.printDone)("Full diagnostic finished.");
366
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ // ─────────────────────────────────────────────────────────────
3
+ // cli/src/commands/install.ts
4
+ //
5
+ // react-doctor install
6
+ //
7
+ // Installs React Doctor from GitHub into a React project.
8
+ // Runs the equivalent of:
9
+ //
10
+ // npm install --save-dev softar-dev/React_Doctor
11
+ //
12
+ // WHY THIS COMMAND EXISTS:
13
+ // When React Doctor is published and another developer wants
14
+ // to use it, they can run "npx react-doctor install" instead
15
+ // of having to remember the GitHub package name. It also
16
+ // detects their package manager (npm/yarn/pnpm) automatically.
17
+ //
18
+ // NOTE: This command is only useful once the GitHub repo is
19
+ // public and the project is ready for other developers to use.
20
+ // During development you run the tool directly from the
21
+ // react-tool/ folder with ts-node, so this command is not
22
+ // needed for your own workflow.
23
+ // ─────────────────────────────────────────────────────────────
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.registerInstallCommand = registerInstallCommand;
29
+ const child_process_1 = require("child_process");
30
+ const path_1 = __importDefault(require("path"));
31
+ const fs_1 = __importDefault(require("fs"));
32
+ const chalk_1 = __importDefault(require("chalk"));
33
+ const ui_1 = require("../ui");
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
+ // REGISTER COMMAND
39
+ // ─────────────────────────────────────────────────────────────
40
+ function registerInstallCommand(program) {
41
+ program
42
+ .command("install")
43
+ .description(`Install React Doctor from GitHub (${GITHUB_PACKAGE})`)
44
+ .option("-p, --path <projectPath>", "Path to the React project to install into (defaults to current directory)", process.cwd())
45
+ .option("--no-banner", "Skip the banner")
46
+ .action(async (options) => {
47
+ const projectPath = path_1.default.resolve(options.path);
48
+ if (!options.noBanner)
49
+ (0, ui_1.printBanner)();
50
+ // ── Validate this is a React project ────────────────────
51
+ const pkgJsonPath = path_1.default.join(projectPath, "package.json");
52
+ if (!fs_1.default.existsSync(pkgJsonPath)) {
53
+ (0, ui_1.printFail)(`No package.json found at: ${projectPath}\n\n` +
54
+ ` Make sure you are inside a React project, or pass the path:\n` +
55
+ ` react-doctor install --path ./my-react-app`);
56
+ process.exit(1);
57
+ }
58
+ // Warn if React is not in the project's dependencies
59
+ try {
60
+ const pkgJson = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, "utf-8"));
61
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
62
+ if (!deps["react"]) {
63
+ console.log(chalk_1.default.yellow(" ⚠️ React not found in this project's dependencies.\n" +
64
+ " Installing anyway — make sure this is a React project.\n"));
65
+ }
66
+ }
67
+ catch {
68
+ // If we can't read package.json, just continue
69
+ }
70
+ // ── Detect package manager ───────────────────────────────
71
+ // Check for lock files to know which package manager the
72
+ // project uses. Order matters — check yarn and pnpm first
73
+ // since npm is the fallback.
74
+ const pkgManager = fs_1.default.existsSync(path_1.default.join(projectPath, "yarn.lock")) ? "yarn" :
75
+ fs_1.default.existsSync(path_1.default.join(projectPath, "pnpm-lock.yaml")) ? "pnpm" :
76
+ "npm";
77
+ (0, ui_1.printSection)("Installing React Doctor");
78
+ (0, ui_1.printInfo)("Project", projectPath);
79
+ (0, ui_1.printInfo)("Manager", pkgManager);
80
+ (0, ui_1.printInfo)("Source", `github.com/${GITHUB_PACKAGE}`);
81
+ console.log();
82
+ // ── Build the install command ────────────────────────────
83
+ // The "owner/repo" format is understood natively by all
84
+ // three package managers — no npm registry needed.
85
+ let args;
86
+ if (pkgManager === "yarn") {
87
+ args = ["add", "--dev", GITHUB_PACKAGE];
88
+ }
89
+ else if (pkgManager === "pnpm") {
90
+ args = ["add", "--save-dev", GITHUB_PACKAGE];
91
+ }
92
+ else {
93
+ args = ["install", "--save-dev", GITHUB_PACKAGE];
94
+ }
95
+ console.log(chalk_1.default.gray(` Running: `) + chalk_1.default.cyan(`${pkgManager} ${args.join(" ")}\n`));
96
+ const spin = (0, ui_1.spinner)("Downloading from GitHub...");
97
+ await new Promise((resolve, reject) => {
98
+ const isWin = process.platform === "win32";
99
+ const proc = (0, child_process_1.spawn)(pkgManager, args, {
100
+ cwd: projectPath,
101
+ shell: isWin ? true : "/bin/bash",
102
+ stdio: ["ignore", "pipe", "pipe"],
103
+ });
104
+ let output = "";
105
+ proc.stdout?.on("data", (d) => { output += d.toString(); });
106
+ proc.stderr?.on("data", (d) => { output += d.toString(); });
107
+ proc.on("close", (code) => {
108
+ if (code === 0) {
109
+ spin.succeed(chalk_1.default.green("Installed successfully!"));
110
+ resolve();
111
+ }
112
+ else {
113
+ spin.fail(chalk_1.default.red("Installation failed."));
114
+ if (output)
115
+ console.log(chalk_1.default.gray(`\n${output}`));
116
+ reject(new Error(`${pkgManager} exited with code ${code}`));
117
+ }
118
+ });
119
+ proc.on("error", reject);
120
+ });
121
+ // ── Show usage after install ─────────────────────────────
122
+ (0, ui_1.printSection)("You are ready");
123
+ console.log(" Run these commands from inside your React project:\n");
124
+ const commands = [
125
+ ["npx react-doctor full ./", "Run the full diagnostic (recommended)"],
126
+ ["npx react-doctor full ./ --mobile", "Include mobile viewport profiling"],
127
+ ["npx react-doctor analyze ./", "Static code analysis only (no Chrome needed)"],
128
+ ["npx react-doctor profile ./", "Runtime profiling only"],
129
+ ["npx react-doctor --help", "See all available options"],
130
+ ];
131
+ commands.forEach(([cmd, desc]) => {
132
+ console.log(` ${chalk_1.default.cyan(cmd.padEnd(44))}` +
133
+ chalk_1.default.gray(desc));
134
+ });
135
+ console.log();
136
+ (0, ui_1.printDone)("React Doctor is installed and ready.");
137
+ });
138
+ }