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.
- package/backend/.env +3 -0
- package/backend/dist/index.js +43 -0
- package/backend/dist/middleware/auth.js +16 -0
- package/backend/dist/routes/reports.js +93 -0
- package/backend/package-lock.json +2000 -0
- package/backend/package.json +30 -0
- package/backend/src/db.ts +24 -0
- package/backend/src/index.ts +49 -0
- package/backend/src/middleware/auth.ts +21 -0
- package/backend/src/routes/reports.ts +110 -0
- package/backend/tsconfig.json +12 -0
- package/cli/bin/react-doctor.js +29 -0
- package/cli/dist/commands/analyze.js +125 -0
- package/cli/dist/commands/full.js +366 -0
- package/cli/dist/commands/install.js +138 -0
- package/cli/dist/commands/profile.js +166 -0
- package/cli/dist/index.js +78 -0
- package/cli/dist/ui.js +113 -0
- package/cli/package-lock.json +936 -0
- package/cli/package.json +34 -0
- package/cli/src/commands/analyze.ts +162 -0
- package/cli/src/commands/full.ts +574 -0
- package/cli/src/commands/install.ts +163 -0
- package/cli/src/commands/profile.ts +246 -0
- package/cli/src/index.ts +84 -0
- package/cli/src/ui.ts +120 -0
- package/cli/tsconfig.json +16 -0
- package/core/report-compiler/index.ts +359 -0
- package/core/report-compiler/test-report-compiler.ts +126 -0
- package/core/rule-engine/context-builder.ts +146 -0
- package/core/rule-engine/evaluator.ts +131 -0
- package/core/rule-engine/index.ts +222 -0
- package/core/rule-engine/rules.json +304 -0
- package/core/rule-engine/suggestion-builder.ts +209 -0
- package/core/rule-engine/test-rule-engine.ts +144 -0
- package/core/rule-engine/types.ts +202 -0
- package/core/runtime/profiler/browser.ts +121 -0
- package/core/runtime/profiler/collectors.ts +216 -0
- package/core/runtime/profiler/index.ts +311 -0
- package/core/runtime/profiler/porfiler.ts +967 -0
- package/core/runtime/profiler/route-scanner.ts +76 -0
- package/core/runtime/profiler/score.ts +59 -0
- package/core/runtime/profiler/server.ts +115 -0
- package/core/runtime/profiler/types.ts +65 -0
- package/core/runtime/test-runtime-profiler.ts +226 -0
- package/core/static-ana/static/analyzer.ts +145 -0
- package/core/static-ana/static/ast-parser.ts +31 -0
- package/core/static-ana/static/detectors/console-log.ts +49 -0
- package/core/static-ana/static/detectors/dead-code.ts +51 -0
- package/core/static-ana/static/detectors/effect-loop.ts +45 -0
- package/core/static-ana/static/detectors/index.ts +16 -0
- package/core/static-ana/static/detectors/inline-function.ts +59 -0
- package/core/static-ana/static/detectors/inline-style.ts +52 -0
- package/core/static-ana/static/detectors/large-component.ts +79 -0
- package/core/static-ana/static/detectors/missing-key.ts +56 -0
- package/core/static-ana/static/detectors/missing-memo.ts +59 -0
- package/core/static-ana/static/detectors/prop-drilling.ts +66 -0
- package/core/static-ana/static/helpers.ts +81 -0
- package/core/static-ana/static/scanner.ts +93 -0
- package/core/static-ana/test-analyzer.ts +115 -0
- package/core/static-ana/types.ts +25 -0
- package/core/tests/mock-react-project/src/app.tsx +22 -0
- package/core/tests/mock-react-project/src/components/Button.tsx +9 -0
- package/core/tests/mock-react-project/src/components/Header.tsx +3 -0
- package/core/tests/mock-react-project/src/components/ListTesting.tsx +51 -0
- package/core/tests/mock-react-project/src/components/UserDashboard.tsx +66 -0
- package/core/tests/mock-react-project/src/utils.ts +4 -0
- package/package.json +55 -0
- package/react-doctor-cli-dev-1.0.0.tgz +0 -0
- package/shared/dist/index.d.ts +2 -0
- package/shared/dist/index.js +19 -0
- package/shared/dist/schemas.d.ts +91 -0
- package/shared/dist/schemas.js +82 -0
- package/shared/dist/types.d.ts +44 -0
- package/shared/dist/types.js +2 -0
- package/shared/package-lock.json +47 -0
- package/shared/package.json +21 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/schemas.ts +136 -0
- package/shared/src/types.ts +137 -0
- package/shared/tsconfig.json +15 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// cli/src/commands/profile.ts
|
|
4
|
+
//
|
|
5
|
+
// react-doctor profile <projectPath>
|
|
6
|
+
//
|
|
7
|
+
// Runs the Runtime Profiler only — needs Chrome.
|
|
8
|
+
// Boots the React dev server, opens headless Chrome,
|
|
9
|
+
// measures Web Vitals + React profiler data, saves
|
|
10
|
+
// runtimereport.json to .react-doctor/
|
|
11
|
+
//
|
|
12
|
+
// Use this when:
|
|
13
|
+
// - You already ran static analysis and want performance data
|
|
14
|
+
// - You want a quick runtime check without code analysis
|
|
15
|
+
// - You want to compare desktop vs mobile performance
|
|
16
|
+
//
|
|
17
|
+
// Options:
|
|
18
|
+
// --mobile also profile on mobile viewport
|
|
19
|
+
// --cpu 4 simulate 4x CPU slowdown (Lighthouse preset)
|
|
20
|
+
// --throttle slow4g simulate slow network (deployed URLs only)
|
|
21
|
+
//
|
|
22
|
+
// Use "react-doctor full" to run everything together.
|
|
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.registerProfileCommand = registerProfileCommand;
|
|
29
|
+
const path_1 = __importDefault(require("path"));
|
|
30
|
+
const fs_1 = __importDefault(require("fs"));
|
|
31
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
32
|
+
const ui_1 = require("../ui");
|
|
33
|
+
function getCoreModule(relativePath) {
|
|
34
|
+
return require(path_1.default.resolve(__dirname, "..", "..", "..", "core", relativePath));
|
|
35
|
+
}
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
// REGISTER COMMAND
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
function registerProfileCommand(program) {
|
|
40
|
+
program
|
|
41
|
+
.command("profile")
|
|
42
|
+
.description("Run the runtime profiler only (requires Chrome)")
|
|
43
|
+
.argument("[projectPath]", "Path to the React project (defaults to current directory)", process.cwd())
|
|
44
|
+
.option("--desktop", "Profile on desktop viewport 1280x720 (default if neither flag is passed)", false)
|
|
45
|
+
.option("--mobile", "Profile on mobile viewport — iPhone 12 Pro 390x844", false)
|
|
46
|
+
.option("--cpu <rate>", "CPU throttle: 1 (real speed) | 4 (Lighthouse mobile) | 6 (low-end)", (v) => parseInt(v), 1)
|
|
47
|
+
.option("--throttle <preset>", "Network throttle: none | slow4g | 3g (only meaningful against deployed URLs)", "none")
|
|
48
|
+
.option("--no-banner", "Skip the banner")
|
|
49
|
+
.action(async (projectPath, options) => {
|
|
50
|
+
const resolvedPath = path_1.default.resolve(projectPath);
|
|
51
|
+
if (!options.noBanner)
|
|
52
|
+
(0, ui_1.printBanner)();
|
|
53
|
+
// ── Validate project ────────────────────────────────────
|
|
54
|
+
if (!fs_1.default.existsSync(path_1.default.join(resolvedPath, "package.json"))) {
|
|
55
|
+
(0, ui_1.printFail)(`No package.json found at: ${resolvedPath}\n\n` +
|
|
56
|
+
` Pass the path to your React project:\n` +
|
|
57
|
+
` react-doctor profile ./my-react-app`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
// ── Determine devices ────────────────────────────────────
|
|
61
|
+
// --desktop and --mobile are independent flags.
|
|
62
|
+
// If neither is passed, desktop is the default.
|
|
63
|
+
// If only --mobile is passed, only mobile runs.
|
|
64
|
+
// If both are passed, both run in one pass.
|
|
65
|
+
const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
|
|
66
|
+
const wantMobile = options.mobile ?? false;
|
|
67
|
+
const devices = wantDesktop && wantMobile ? ["desktop", "mobile"] :
|
|
68
|
+
wantMobile ? "mobile" :
|
|
69
|
+
"desktop";
|
|
70
|
+
const deviceLabel = wantDesktop && wantMobile ? "desktop + mobile" :
|
|
71
|
+
wantMobile ? "mobile" :
|
|
72
|
+
"desktop";
|
|
73
|
+
(0, ui_1.printSection)("Runtime Profiler");
|
|
74
|
+
(0, ui_1.printInfo)("Project", resolvedPath);
|
|
75
|
+
(0, ui_1.printInfo)("Device", deviceLabel);
|
|
76
|
+
(0, ui_1.printInfo)("CPU", `${options.cpu}x`);
|
|
77
|
+
(0, ui_1.printInfo)("Network", options.throttle);
|
|
78
|
+
console.log();
|
|
79
|
+
// ── Run the profiler ────────────────────────────────────
|
|
80
|
+
const spin = (0, ui_1.spinner)("Starting dev server...");
|
|
81
|
+
try {
|
|
82
|
+
const { RuntimeProfiler } = getCoreModule("runtime/profiler/index");
|
|
83
|
+
const profiler = new RuntimeProfiler(resolvedPath);
|
|
84
|
+
spin.text = " Launching headless Chrome...";
|
|
85
|
+
const runtimeReports = await profiler.profile([], {
|
|
86
|
+
device: devices,
|
|
87
|
+
throttle: options.throttle,
|
|
88
|
+
cpuThrottle: options.cpu,
|
|
89
|
+
});
|
|
90
|
+
// Save to .react-doctor/
|
|
91
|
+
const outputDir = path_1.default.join(resolvedPath, ".react-doctor");
|
|
92
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
93
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, "runtimereport.json"), JSON.stringify(runtimeReports, null, 2));
|
|
94
|
+
const routeKeys = Object.keys(runtimeReports);
|
|
95
|
+
spin.succeed(chalk_1.default.green(`Profiling complete — ${routeKeys.length} route/device combination(s)`));
|
|
96
|
+
// ── Results ─────────────────────────────────────────────
|
|
97
|
+
(0, ui_1.printSection)("Results");
|
|
98
|
+
for (const [key, report] of Object.entries(runtimeReports)) {
|
|
99
|
+
const [route, device] = key.includes("::") ? key.split("::") : [key, "desktop"];
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(` ${chalk_1.default.bold("Route:")} ${route} ` +
|
|
102
|
+
`${chalk_1.default.gray(`[${device}]`)} ` +
|
|
103
|
+
`Score: ${(0, ui_1.scoreBadge)(report.performanceScore)}`);
|
|
104
|
+
console.log();
|
|
105
|
+
// Web vitals
|
|
106
|
+
(0, ui_1.printResult)("LCP", `${report.metrics.lcp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("lcp", report.metrics.lcp));
|
|
107
|
+
(0, ui_1.printResult)("FCP", `${report.metrics.fcp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("fcp", report.metrics.fcp));
|
|
108
|
+
(0, ui_1.printResult)("TTFB", `${report.metrics.ttfb.toFixed(0)}ms`, (0, ui_1.vitalStatus)("ttfb", report.metrics.ttfb));
|
|
109
|
+
(0, ui_1.printResult)("CLS", report.metrics.cls.toFixed(3), (0, ui_1.vitalStatus)("cls", report.metrics.cls));
|
|
110
|
+
(0, ui_1.printResult)("INP", `${report.metrics.inp.toFixed(0)}ms`, (0, ui_1.vitalStatus)("inp", report.metrics.inp));
|
|
111
|
+
(0, ui_1.printResult)("Render time", `${report.renderTime}ms`, report.renderTime <= 2000 ? "good" : report.renderTime <= 4000 ? "warn" : "poor");
|
|
112
|
+
// React profiler
|
|
113
|
+
if (report.commitDurations?.length > 0) {
|
|
114
|
+
const avg = (report.commitDurations.reduce((a, b) => a + b, 0) /
|
|
115
|
+
report.commitDurations.length).toFixed(1);
|
|
116
|
+
const slow = report.commitDurations.filter((d) => d > 16).length;
|
|
117
|
+
(0, ui_1.printResult)("React commits", `${report.commitDurations.length} total, avg ${avg}ms`, slow > 0 ? "warn" : "good");
|
|
118
|
+
}
|
|
119
|
+
// Top re-render component
|
|
120
|
+
const rerenderEntries = Object.entries(report.rerenders ?? {})
|
|
121
|
+
.sort(([, a], [, b]) => b - a);
|
|
122
|
+
if (rerenderEntries.length > 0) {
|
|
123
|
+
const [topName, topCount] = rerenderEntries[0];
|
|
124
|
+
(0, ui_1.printResult)("Most re-renders", `${topName} (${topCount}x)`, topCount >= 10 ? "poor" : topCount >= 5 ? "warn" : "good");
|
|
125
|
+
}
|
|
126
|
+
// System stats
|
|
127
|
+
(0, ui_1.printResult)("Page weight", `${report.stats.payloadMB} MB`, "info");
|
|
128
|
+
(0, ui_1.printResult)("JS heap", `${report.stats.jsHeapMB} MB`, "info");
|
|
129
|
+
(0, ui_1.printResult)("DOM nodes", String(report.stats.domNodes), "info");
|
|
130
|
+
if (report.stats.topOffender) {
|
|
131
|
+
(0, ui_1.printResult)("Heaviest file", `${report.stats.topOffender.name} (${report.stats.topOffender.size.toFixed(2)} MB)`, "warn");
|
|
132
|
+
}
|
|
133
|
+
// Errors
|
|
134
|
+
const errorCount = (report.errors ?? []).filter((e) => e.type === "error").length;
|
|
135
|
+
const warningCount = (report.errors ?? []).filter((e) => e.type === "warning").length;
|
|
136
|
+
if (errorCount > 0 || warningCount > 0) {
|
|
137
|
+
(0, ui_1.printResult)("Issues", `${errorCount} error(s) ${warningCount} warning(s)`, errorCount > 0 ? "poor" : "warn");
|
|
138
|
+
(report.errors ?? []).slice(0, 3).forEach((e) => {
|
|
139
|
+
const icon = e.type === "error" ? chalk_1.default.red(" ✗") : chalk_1.default.yellow(" !");
|
|
140
|
+
console.log(`${icon} ${chalk_1.default.gray(e.message.slice(0, 90))}`);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
(0, ui_1.printResult)("Issues", "None detected", "good");
|
|
145
|
+
}
|
|
146
|
+
// Screenshots
|
|
147
|
+
if (report.screenshots?.length > 0) {
|
|
148
|
+
const labels = report.screenshots.map((s) => `${s.label}@${s.takenAt}ms`).join(" ");
|
|
149
|
+
(0, ui_1.printResult)("Screenshots", labels, "info");
|
|
150
|
+
}
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
(0, ui_1.printResult)("Report saved", path_1.default.join(resolvedPath, ".react-doctor", "runtimereport.json"), "info");
|
|
154
|
+
console.log();
|
|
155
|
+
(0, ui_1.printDone)("Runtime profiling finished.");
|
|
156
|
+
console.log(chalk_1.default.gray(" Tip: run ") +
|
|
157
|
+
chalk_1.default.cyan("react-doctor full ./") +
|
|
158
|
+
chalk_1.default.gray(" to also get static analysis and improvement suggestions.\n"));
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
spin.fail(chalk_1.default.red("Runtime profiling failed"));
|
|
162
|
+
console.log(chalk_1.default.red(`\n ${err.message}\n`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
// cli/src/index.ts
|
|
5
|
+
//
|
|
6
|
+
// The CLI entry point. This is the file that runs when the
|
|
7
|
+
// user types "react-doctor" in their terminal.
|
|
8
|
+
//
|
|
9
|
+
// HOW IT WORKS:
|
|
10
|
+
// 1. Commander.js parses the command and flags from argv
|
|
11
|
+
// 2. The matching command handler is called
|
|
12
|
+
// 3. The handler imports core modules and runs the pipeline
|
|
13
|
+
//
|
|
14
|
+
// HOW THE BINARY REGISTRATION WORKS:
|
|
15
|
+
// package.json has a "bin" field:
|
|
16
|
+
// "bin": { "react-doctor": "./dist/index.js" }
|
|
17
|
+
//
|
|
18
|
+
// After "npm link" (dev) or "npm install" (production),
|
|
19
|
+
// npm creates a symlink from the system's bin directory
|
|
20
|
+
// to this file. That's what makes "react-doctor" a real
|
|
21
|
+
// terminal command available anywhere.
|
|
22
|
+
//
|
|
23
|
+
// THE SHEBANG (#!/usr/bin/env node) on line 1:
|
|
24
|
+
// This tells the OS to run this file with Node.js when
|
|
25
|
+
// called directly as a script. Without it, the OS doesn't
|
|
26
|
+
// know which interpreter to use.
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const commander_1 = require("commander");
|
|
30
|
+
const analyze_1 = require("./commands/analyze");
|
|
31
|
+
const profile_1 = require("./commands/profile");
|
|
32
|
+
const full_1 = require("./commands/full");
|
|
33
|
+
const install_1 = require("./commands/install");
|
|
34
|
+
const program = new commander_1.Command();
|
|
35
|
+
// ── Program metadata ──────────────────────────────────────────
|
|
36
|
+
program
|
|
37
|
+
.name("react-doctor")
|
|
38
|
+
.description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
|
|
39
|
+
.version("1.0.0");
|
|
40
|
+
// ── Register all commands ─────────────────────────────────────
|
|
41
|
+
// Each function adds one command to the program.
|
|
42
|
+
// The order here is the order they appear in --help output.
|
|
43
|
+
(0, full_1.registerFullCommand)(program); // react-doctor full
|
|
44
|
+
(0, analyze_1.registerAnalyzeCommand)(program); // react-doctor analyze
|
|
45
|
+
(0, profile_1.registerProfileCommand)(program); // react-doctor profile
|
|
46
|
+
(0, install_1.registerInstallCommand)(program); // react-doctor install
|
|
47
|
+
// ── Usage examples shown at bottom of --help ─────────────────
|
|
48
|
+
program.addHelpText("after", `
|
|
49
|
+
Examples:
|
|
50
|
+
$ react-doctor full ./my-app Desktop only (default)
|
|
51
|
+
$ react-doctor full ./my-app --mobile Mobile only
|
|
52
|
+
$ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
|
|
53
|
+
$ react-doctor full ./my-app --cpu 4 Simulate slow Android device
|
|
54
|
+
$ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
|
|
55
|
+
$ react-doctor full ./my-app --throttle 3g Simulate 3G network
|
|
56
|
+
$ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
|
|
57
|
+
$ react-doctor full ./my-app --upload Upload results to dashboard
|
|
58
|
+
|
|
59
|
+
$ react-doctor analyze ./my-app Static code analysis only
|
|
60
|
+
$ react-doctor analyze ./my-app --full Static + runtime + rules
|
|
61
|
+
|
|
62
|
+
$ react-doctor profile ./my-app Desktop only (default)
|
|
63
|
+
$ react-doctor profile ./my-app --mobile Mobile only
|
|
64
|
+
$ react-doctor profile ./my-app --desktop --mobile Both devices
|
|
65
|
+
$ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
|
|
66
|
+
$ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
|
|
67
|
+
$ react-doctor profile ./my-app --throttle 3g Simulate 3G network
|
|
68
|
+
|
|
69
|
+
$ react-doctor install Install from GitHub into a project
|
|
70
|
+
$ react-doctor install --path ./my-app Install into a specific folder
|
|
71
|
+
`);
|
|
72
|
+
// ── Show help if called with no arguments ─────────────────────
|
|
73
|
+
// Without this, calling "react-doctor" with no command just
|
|
74
|
+
// exits silently, which is confusing. This prints help instead.
|
|
75
|
+
if (process.argv.length < 3) {
|
|
76
|
+
program.help();
|
|
77
|
+
}
|
|
78
|
+
program.parse(process.argv);
|
package/cli/dist/ui.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// cli/src/ui.ts
|
|
4
|
+
//
|
|
5
|
+
// Shared terminal output helpers used by every command.
|
|
6
|
+
// All chalk colors, ora spinners, and print functions live
|
|
7
|
+
// here so the visual style is consistent across the CLI.
|
|
8
|
+
//
|
|
9
|
+
// WHY ONE FILE FOR ALL UI:
|
|
10
|
+
// If each command file had its own chalk/ora setup, changing
|
|
11
|
+
// a color or spacing would require editing every file. This
|
|
12
|
+
// way you change it once here and it applies everywhere.
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.printBanner = printBanner;
|
|
19
|
+
exports.printSection = printSection;
|
|
20
|
+
exports.spinner = spinner;
|
|
21
|
+
exports.printResult = printResult;
|
|
22
|
+
exports.scoreBadge = scoreBadge;
|
|
23
|
+
exports.severityIcon = severityIcon;
|
|
24
|
+
exports.printDone = printDone;
|
|
25
|
+
exports.printFail = printFail;
|
|
26
|
+
exports.printInfo = printInfo;
|
|
27
|
+
exports.vitalStatus = vitalStatus;
|
|
28
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
29
|
+
const ora_1 = __importDefault(require("ora"));
|
|
30
|
+
// ── Banner ────────────────────────────────────────────────────
|
|
31
|
+
// Printed once at the start of every command run.
|
|
32
|
+
function printBanner() {
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(chalk_1.default.cyan.bold(" ┌─────────────────────────────────┐"));
|
|
35
|
+
console.log(chalk_1.default.cyan.bold(" │ 🩺 React Doctor │"));
|
|
36
|
+
console.log(chalk_1.default.cyan.bold(" │ React Performance Analyzer │"));
|
|
37
|
+
console.log(chalk_1.default.cyan.bold(" └─────────────────────────────────┘"));
|
|
38
|
+
console.log();
|
|
39
|
+
}
|
|
40
|
+
// ── Section headers ───────────────────────────────────────────
|
|
41
|
+
// Visually separates sections within a command's output.
|
|
42
|
+
function printSection(title) {
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(chalk_1.default.cyan.bold(` ── ${title} `).padEnd(58, "─"));
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
// ── Spinner ───────────────────────────────────────────────────
|
|
48
|
+
// Returns a running ora spinner.
|
|
49
|
+
// Caller calls .succeed() or .fail() when done.
|
|
50
|
+
function spinner(text) {
|
|
51
|
+
return (0, ora_1.default)({ text: ` ${text}`, color: "cyan", spinner: "dots" }).start();
|
|
52
|
+
}
|
|
53
|
+
// ── Single result line ────────────────────────────────────────
|
|
54
|
+
// Prints one labeled metric with a colored status dot.
|
|
55
|
+
function printResult(label, value, status = "none") {
|
|
56
|
+
const dot = status === "good" ? chalk_1.default.green("●") :
|
|
57
|
+
status === "warn" ? chalk_1.default.yellow("●") :
|
|
58
|
+
status === "poor" ? chalk_1.default.red("●") :
|
|
59
|
+
status === "info" ? chalk_1.default.blue("●") :
|
|
60
|
+
chalk_1.default.gray("·");
|
|
61
|
+
console.log(` ${dot} ${chalk_1.default.gray(label.padEnd(24))} ${value}`);
|
|
62
|
+
}
|
|
63
|
+
// ── Score badge ───────────────────────────────────────────────
|
|
64
|
+
// Formats a 0-100 score with a color band.
|
|
65
|
+
function scoreBadge(score) {
|
|
66
|
+
if (score >= 90)
|
|
67
|
+
return chalk_1.default.green.bold(`${score}/100`) + chalk_1.default.green(" Excellent");
|
|
68
|
+
if (score >= 70)
|
|
69
|
+
return chalk_1.default.yellow.bold(`${score}/100`) + chalk_1.default.yellow(" Good");
|
|
70
|
+
if (score >= 50)
|
|
71
|
+
return chalk_1.default.hex("#FFA500").bold(`${score}/100`) + chalk_1.default.hex("#FFA500")(" Needs Work");
|
|
72
|
+
return chalk_1.default.red.bold(`${score}/100`) + chalk_1.default.red(" Poor");
|
|
73
|
+
}
|
|
74
|
+
// ── Severity icon ─────────────────────────────────────────────
|
|
75
|
+
function severityIcon(s) {
|
|
76
|
+
if (s === "critical")
|
|
77
|
+
return chalk_1.default.red("❌");
|
|
78
|
+
if (s === "warning")
|
|
79
|
+
return chalk_1.default.yellow("⚠️ ");
|
|
80
|
+
return chalk_1.default.blue("ℹ️ ");
|
|
81
|
+
}
|
|
82
|
+
// ── Done / fail ───────────────────────────────────────────────
|
|
83
|
+
function printDone(message) {
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(chalk_1.default.green.bold(` ✅ ${message}`));
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
function printFail(message) {
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(chalk_1.default.red.bold(` ❌ ${message}`));
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
// ── Muted info line ───────────────────────────────────────────
|
|
94
|
+
function printInfo(label, value) {
|
|
95
|
+
console.log(` ${chalk_1.default.gray(label.padEnd(16))} ${chalk_1.default.white(value)}`);
|
|
96
|
+
}
|
|
97
|
+
// ── Web vital threshold helper ────────────────────────────────
|
|
98
|
+
// Maps a metric value to good/warn/poor for colored output.
|
|
99
|
+
function vitalStatus(metric, value) {
|
|
100
|
+
const thresholds = {
|
|
101
|
+
lcp: { good: 2500, poor: 4000 },
|
|
102
|
+
fcp: { good: 1800, poor: 3000 },
|
|
103
|
+
ttfb: { good: 800, poor: 1800 },
|
|
104
|
+
inp: { good: 200, poor: 500 },
|
|
105
|
+
cls: { good: 0.1, poor: 0.25 },
|
|
106
|
+
};
|
|
107
|
+
const t = thresholds[metric];
|
|
108
|
+
if (value <= t.good)
|
|
109
|
+
return "good";
|
|
110
|
+
if (value <= t.poor)
|
|
111
|
+
return "warn";
|
|
112
|
+
return "poor";
|
|
113
|
+
}
|