react-doctor-cli-dev 1.0.7 → 1.0.9
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/data/screenshots/4--fcp.png +0 -0
- package/backend/data/screenshots/4--fullLoad.png +0 -0
- package/backend/data/screenshots/4-docs-fullLoad.png +0 -0
- package/backend/data/screenshots/4-white-fullLoad.png +0 -0
- package/backend/dist/db.js +33 -11
- package/backend/dist/index.js +29 -3
- package/backend/dist/routes/reports.js +106 -55
- package/backend/public/assets/index-BpODc0fS.css +1 -0
- package/backend/public/assets/index-zKyZPsv1.js +118 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/backend/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/backend/public/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/backend/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/backend/public/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/backend/public/assets/tajawal-arabic-300-normal-Bq0yWa0Z.woff +0 -0
- package/backend/public/assets/tajawal-arabic-300-normal-By07C9pa.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-400-normal-CyCXRvzh.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-400-normal-DCQxawbB.woff +0 -0
- package/backend/public/assets/tajawal-arabic-500-normal-BZ8ojJNu.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-500-normal-CbVEaYEW.woff +0 -0
- package/backend/public/assets/tajawal-arabic-700-normal-9L7Zusdl.woff +0 -0
- package/backend/public/assets/tajawal-arabic-700-normal-D2-eand5.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-300-normal-C0-xR3ms.woff +0 -0
- package/backend/public/assets/tajawal-latin-300-normal-CeEKeOxZ.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-400-normal-BVNSOH3d.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-400-normal-BdYcZznU.woff +0 -0
- package/backend/public/assets/tajawal-latin-500-normal-CoYeBiSI.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-500-normal-DU9v6xgj.woff +0 -0
- package/backend/public/assets/tajawal-latin-700-normal-BypgxfGb.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-700-normal-CV3bxpHe.woff +0 -0
- package/backend/public/favicon.svg +1 -0
- package/backend/public/icons.svg +24 -0
- package/backend/public/index.html +254 -0
- package/backend/src/db.ts +42 -18
- package/backend/src/index.ts +31 -3
- package/backend/src/routes/reports.ts +141 -52
- package/cli/dist/commands/full.js +82 -48
- package/cli/src/commands/full.ts +161 -115
- package/package.json +25 -4
- package/shared/dist/index.d.ts +0 -2
- package/shared/dist/index.js +0 -19
- package/shared/dist/schemas.d.ts +0 -91
- package/shared/dist/schemas.js +0 -82
- package/shared/dist/types.d.ts +0 -44
- package/shared/dist/types.js +0 -2
- package/shared/package-lock.json +0 -47
- package/shared/package.json +0 -21
- package/shared/src/index.ts +0 -4
- package/shared/src/schemas.ts +0 -136
- package/shared/src/types.ts +0 -137
- package/shared/tsconfig.json +0 -15
- package/tsconfig.json +0 -25
package/cli/src/commands/full.ts
CHANGED
|
@@ -11,18 +11,7 @@
|
|
|
11
11
|
// 4. RuleEngine → combines both reports into suggestions
|
|
12
12
|
// 5. ReportCompiler → merges everything into finalreport.json
|
|
13
13
|
// 6. Upload → sends the report to the backend API
|
|
14
|
-
//
|
|
15
|
-
// HOW IMPORTS WORK:
|
|
16
|
-
// The CLI imports core modules directly as TypeScript classes.
|
|
17
|
-
// No shell commands, no spawning child processes, no ts-node
|
|
18
|
-
// inside ts-node. Everything runs in the same Node.js process,
|
|
19
|
-
// which means objects are passed between steps in memory —
|
|
20
|
-
// fast, clean, and no disk reads between steps.
|
|
21
|
-
//
|
|
22
|
-
// The core folder is 2 levels up from cli/src/commands/:
|
|
23
|
-
// cli/src/commands/full.ts
|
|
24
|
-
// → ../../.. = react-tool root
|
|
25
|
-
// → ../../../core = core folder
|
|
14
|
+
// and opens the dashboard to that report
|
|
26
15
|
// ─────────────────────────────────────────────────────────────
|
|
27
16
|
|
|
28
17
|
import { Command } from "commander";
|
|
@@ -45,14 +34,7 @@ import {
|
|
|
45
34
|
} from "../ui";
|
|
46
35
|
|
|
47
36
|
// ── Core imports ──────────────────────────────────────────────
|
|
48
|
-
// These are the actual classes from the core folder.
|
|
49
|
-
// We use require() with a resolved path so they work whether
|
|
50
|
-
// the CLI is run from its own folder or from the project root.
|
|
51
|
-
|
|
52
37
|
function getCoreModule(relativePath: string) {
|
|
53
|
-
// __dirname = cli/src/commands/
|
|
54
|
-
// 3 levels up = react-tool root
|
|
55
|
-
// then into core/
|
|
56
38
|
return require(
|
|
57
39
|
path.resolve(__dirname, "..", "..", "..", "core", relativePath),
|
|
58
40
|
);
|
|
@@ -104,21 +86,19 @@ export function registerFullCommand(program: Command): void {
|
|
|
104
86
|
"Backend API URL to upload to",
|
|
105
87
|
"http://localhost:3000",
|
|
106
88
|
)
|
|
107
|
-
// ✅ Single --api-key option (defined BEFORE .action)
|
|
108
89
|
.option(
|
|
109
90
|
"--api-key <key>",
|
|
110
91
|
"API key for backend authentication (overrides REACT_DOCTOR_API_KEY env var)",
|
|
111
92
|
process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this",
|
|
112
93
|
)
|
|
113
94
|
.option("--no-banner", "Skip the banner")
|
|
114
|
-
// ✅ .action() comes LAST, with no trailing semicolon/comment
|
|
115
95
|
.action(async (projectPath: string, options) => {
|
|
116
96
|
await runFullCommand(projectPath, options);
|
|
117
97
|
});
|
|
118
98
|
}
|
|
99
|
+
|
|
119
100
|
// ─────────────────────────────────────────────────────────────
|
|
120
101
|
// MAIN RUNNER
|
|
121
|
-
// Exported so other commands (analyze --full) can call it too.
|
|
122
102
|
// ─────────────────────────────────────────────────────────────
|
|
123
103
|
|
|
124
104
|
export async function runFullCommand(
|
|
@@ -149,10 +129,6 @@ export async function runFullCommand(
|
|
|
149
129
|
}
|
|
150
130
|
|
|
151
131
|
// ── Determine device configuration ──────────────────────────
|
|
152
|
-
// --desktop and --mobile are independent flags.
|
|
153
|
-
// If neither is passed, desktop is the default.
|
|
154
|
-
// If only --mobile is passed, only mobile runs.
|
|
155
|
-
// If both are passed, both run in one pass.
|
|
156
132
|
const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
|
|
157
133
|
const wantMobile = options.mobile ?? false;
|
|
158
134
|
|
|
@@ -180,8 +156,6 @@ export async function runFullCommand(
|
|
|
180
156
|
printInfo("Network", throttleLabel);
|
|
181
157
|
|
|
182
158
|
// ── Output directory ───────────────────────────────────────
|
|
183
|
-
// Reports are saved inside the user's project in a hidden
|
|
184
|
-
// .react-doctor/ folder — easy to find, easy to gitignore.
|
|
185
159
|
const outputDir = path.join(resolvedPath, ".react-doctor");
|
|
186
160
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
187
161
|
|
|
@@ -275,7 +249,6 @@ export async function runFullCommand(
|
|
|
275
249
|
),
|
|
276
250
|
);
|
|
277
251
|
|
|
278
|
-
// Print results for each route
|
|
279
252
|
for (const [key, report] of Object.entries(runtimeReports)) {
|
|
280
253
|
const [route, device] = key.includes("::")
|
|
281
254
|
? key.split("::")
|
|
@@ -285,7 +258,6 @@ export async function runFullCommand(
|
|
|
285
258
|
console.log(
|
|
286
259
|
` ${chalk.bold(route)} ${chalk.gray(`[${device}]`)} Score: ${scoreBadge(report.performanceScore)}`,
|
|
287
260
|
);
|
|
288
|
-
// ── Device / CPU / Network line ──────────────────────────
|
|
289
261
|
console.log(
|
|
290
262
|
` ${chalk.gray("Device:")} ${device} ` +
|
|
291
263
|
`${chalk.gray("CPU:")} ${report.cpuThrottling ?? cpuLabel}x ` +
|
|
@@ -346,7 +318,6 @@ export async function runFullCommand(
|
|
|
346
318
|
} catch (err: any) {
|
|
347
319
|
profilingSpin.fail(chalk.red("Runtime profiling failed"));
|
|
348
320
|
console.log(chalk.red(`\n ${err.message}\n`));
|
|
349
|
-
// Profiling failure is not fatal — rule engine can still run on static data
|
|
350
321
|
}
|
|
351
322
|
|
|
352
323
|
// ════════════════════════════════════════════════════════════
|
|
@@ -452,103 +423,178 @@ export async function runFullCommand(
|
|
|
452
423
|
}
|
|
453
424
|
|
|
454
425
|
// ════════════════════════════════════════════════════════════
|
|
455
|
-
// OPTIONAL — UPLOAD TO BACKEND API
|
|
426
|
+
// OPTIONAL — UPLOAD TO BACKEND API (WITH SCREENSHOTS)
|
|
456
427
|
// ════════════════════════════════════════════════════════════
|
|
457
428
|
|
|
458
429
|
if (options.upload && finalReport) {
|
|
459
|
-
|
|
430
|
+
printSection("Uploading to Backend");
|
|
460
431
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
// Ensure apiUrl has a default
|
|
465
|
-
const apiUrl = options.apiUrl ?? "http://localhost:3000";
|
|
466
|
-
|
|
467
|
-
// 1. Check if backend is already running
|
|
468
|
-
try {
|
|
469
|
-
await axios.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
470
|
-
uploadSpin.text = "Backend detected. Preparing upload...";
|
|
471
|
-
} catch (err) {
|
|
472
|
-
// 2. If not running, start it automatically
|
|
473
|
-
|
|
474
|
-
// Determine the project root directory (where cli/ and backend/ are siblings)
|
|
475
|
-
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
476
|
-
|
|
477
|
-
// Backend is a sibling folder to cli at projectRoot
|
|
478
|
-
const backendRoot = path.resolve(projectRoot, "backend");
|
|
479
|
-
|
|
480
|
-
// Check for compiled JS first (for installed packages)
|
|
481
|
-
const backendDist = path.join(backendRoot, "dist", "index.js");
|
|
482
|
-
// Check for TS source (for local dev)
|
|
483
|
-
const backendSrc = path.join(backendRoot, "src", "index.ts");
|
|
484
|
-
|
|
485
|
-
let command: string;
|
|
486
|
-
let args: string[];
|
|
487
|
-
|
|
488
|
-
if (fs.existsSync(backendDist)) {
|
|
489
|
-
command = "node";
|
|
490
|
-
args = [backendDist];
|
|
491
|
-
} else if (fs.existsSync(backendSrc)) {
|
|
492
|
-
command = "npx";
|
|
493
|
-
args = ["ts-node", backendSrc];
|
|
494
|
-
} else {
|
|
495
|
-
throw new Error(`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
uploadSpin.text = "Backend not found. Starting local server automatically...";
|
|
432
|
+
const uploadSpin = spinner(`Connecting to ${options.apiUrl}...`);
|
|
499
433
|
|
|
500
|
-
|
|
501
|
-
const
|
|
434
|
+
try {
|
|
435
|
+
const apiUrl = options.apiUrl ?? "http://localhost:3000";
|
|
436
|
+
|
|
437
|
+
// 1. Check if backend is already running
|
|
438
|
+
try {
|
|
439
|
+
await axios.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
440
|
+
uploadSpin.text = "Backend detected. Preparing upload...";
|
|
441
|
+
} catch (err) {
|
|
442
|
+
// 2. If not running, start it automatically
|
|
443
|
+
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
444
|
+
const backendRoot = path.resolve(projectRoot, "backend");
|
|
445
|
+
const backendDist = path.join(backendRoot, "dist", "index.js");
|
|
446
|
+
const backendSrc = path.join(backendRoot, "src", "index.ts");
|
|
447
|
+
|
|
448
|
+
let command: string;
|
|
449
|
+
let args: string[];
|
|
450
|
+
|
|
451
|
+
if (fs.existsSync(backendDist)) {
|
|
452
|
+
command = "node";
|
|
453
|
+
args = [backendDist];
|
|
454
|
+
} else if (fs.existsSync(backendSrc)) {
|
|
455
|
+
command = "npx";
|
|
456
|
+
args = ["ts-node", backendSrc];
|
|
457
|
+
} else {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
502
462
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
463
|
+
uploadSpin.text =
|
|
464
|
+
"Backend not found. Starting local server automatically...";
|
|
465
|
+
|
|
466
|
+
const port = new URL(apiUrl).port || "3000";
|
|
467
|
+
const backendDataDir = path.join(outputDir, "backend-data");
|
|
468
|
+
fs.mkdirSync(backendDataDir, { recursive: true });
|
|
469
|
+
|
|
470
|
+
spawn(command, args, {
|
|
471
|
+
stdio: "inherit",
|
|
472
|
+
env: {
|
|
473
|
+
...process.env,
|
|
474
|
+
API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
|
|
475
|
+
PORT: port,
|
|
476
|
+
DB_PATH: path.join(backendDataDir, "reports.db"),
|
|
477
|
+
},
|
|
478
|
+
cwd: backendRoot,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// 3. Wait for backend to be ready (up to 15s)
|
|
482
|
+
let isReady = false;
|
|
483
|
+
let retries = 0;
|
|
484
|
+
while (!isReady && retries < 15) {
|
|
485
|
+
try {
|
|
486
|
+
await axios.get(`${apiUrl}/health`, { timeout: 1000 });
|
|
487
|
+
isReady = true;
|
|
488
|
+
} catch {
|
|
489
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
490
|
+
retries++;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
507
493
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
|
|
513
|
-
PORT: port,
|
|
514
|
-
DB_PATH: path.join(backendDataDir, "reports.db"),
|
|
515
|
-
},
|
|
516
|
-
cwd: backendRoot
|
|
517
|
-
});
|
|
494
|
+
if (!isReady)
|
|
495
|
+
throw new Error("Backend failed to start after 15 seconds.");
|
|
496
|
+
uploadSpin.text = "Backend started successfully!";
|
|
497
|
+
}
|
|
518
498
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
499
|
+
// ── 4. Read and encode screenshots ──────────────────────
|
|
500
|
+
uploadSpin.text = "Processing screenshots...";
|
|
501
|
+
const screenshots: any[] = [];
|
|
502
|
+
const runtime = finalReport.runtime || {};
|
|
503
|
+
|
|
504
|
+
for (const [routeKey, routeData] of Object.entries(runtime)) {
|
|
505
|
+
if ((routeData as any).screenshots && (routeData as any).screenshots.length > 0) {
|
|
506
|
+
for (const screenshot of (routeData as any).screenshots) {
|
|
507
|
+
try {
|
|
508
|
+
// Check if this is a file path or already a data URL
|
|
509
|
+
if (screenshot.dataUrl && !screenshot.dataUrl.startsWith('data:')) {
|
|
510
|
+
// It's a file path - read and encode it
|
|
511
|
+
const screenshotPath = path.join(outputDir, screenshot.dataUrl);
|
|
512
|
+
if (fs.existsSync(screenshotPath)) {
|
|
513
|
+
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
514
|
+
const base64Image = imageBuffer.toString('base64');
|
|
515
|
+
screenshots.push({
|
|
516
|
+
route: routeKey,
|
|
517
|
+
label: screenshot.label || 'screenshot',
|
|
518
|
+
takenAt: screenshot.takenAt || 0,
|
|
519
|
+
dataUrl: `data:image/png;base64,${base64Image}`,
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
// File doesn't exist - store as placeholder
|
|
523
|
+
console.log(chalk.yellow(` ⚠️ Screenshot not found: ${screenshotPath}`));
|
|
524
|
+
screenshots.push({
|
|
525
|
+
route: routeKey,
|
|
526
|
+
label: screenshot.label || 'screenshot',
|
|
527
|
+
takenAt: screenshot.takenAt || 0,
|
|
528
|
+
dataUrl: null,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} else if (screenshot.dataUrl && screenshot.dataUrl.startsWith('data:')) {
|
|
532
|
+
// Already a data URL - use it directly
|
|
533
|
+
screenshots.push({
|
|
534
|
+
route: routeKey,
|
|
535
|
+
label: screenshot.label || 'screenshot',
|
|
536
|
+
takenAt: screenshot.takenAt || 0,
|
|
537
|
+
dataUrl: screenshot.dataUrl,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} catch (err: any) {
|
|
541
|
+
console.log(chalk.yellow(` ⚠️ Failed to process screenshot: ${err.message}`));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
529
544
|
}
|
|
530
545
|
}
|
|
531
546
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
547
|
+
// ── 5. Prepare upload data with screenshots ─────────────
|
|
548
|
+
const uploadData = {
|
|
549
|
+
...finalReport,
|
|
550
|
+
screenshots: screenshots,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
uploadSpin.text = `Uploading report and ${screenshots.length} screenshots...`;
|
|
554
|
+
|
|
555
|
+
// ── 6. Perform the upload ──────────────────────────────
|
|
556
|
+
const response = await axios.post(
|
|
557
|
+
`${apiUrl}/api/reports/upload`,
|
|
558
|
+
uploadData,
|
|
559
|
+
{
|
|
560
|
+
headers: {
|
|
561
|
+
"Content-Type": "application/json",
|
|
562
|
+
"x-api-key":
|
|
563
|
+
options.apiKey || "react-doctor-secret-key-change-this",
|
|
564
|
+
} as Record<string, string>,
|
|
565
|
+
timeout: 30000, // Longer timeout for images
|
|
566
|
+
},
|
|
567
|
+
);
|
|
535
568
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"x-api-key": options.apiKey || "react-doctor-secret-key-change-this",
|
|
542
|
-
} as Record<string, string>,
|
|
543
|
-
timeout: 10000,
|
|
544
|
-
});
|
|
569
|
+
const reportId = response.data?.id;
|
|
570
|
+
const uploadedCount = response.data?.screenshots || 0;
|
|
571
|
+
uploadSpin.succeed(
|
|
572
|
+
chalk.green(`Report uploaded successfully (${uploadedCount} screenshots)`)
|
|
573
|
+
);
|
|
545
574
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
575
|
+
// 7. Open the dashboard directly to this report
|
|
576
|
+
if (reportId) {
|
|
577
|
+
const dashboardUrl = `${apiUrl}/report/${reportId}`;
|
|
578
|
+
printInfo("Opening dashboard", dashboardUrl);
|
|
579
|
+
|
|
580
|
+
const openCmd: [string, string[]] =
|
|
581
|
+
process.platform === "win32"
|
|
582
|
+
? ["cmd", ["/c", "start", "", dashboardUrl]]
|
|
583
|
+
: process.platform === "darwin"
|
|
584
|
+
? ["open", [dashboardUrl]]
|
|
585
|
+
: ["xdg-open", [dashboardUrl]];
|
|
586
|
+
|
|
587
|
+
spawn(openCmd[0], openCmd[1], {
|
|
588
|
+
stdio: "ignore",
|
|
589
|
+
detached: true,
|
|
590
|
+
}).unref();
|
|
591
|
+
}
|
|
592
|
+
} catch (err: any) {
|
|
593
|
+
uploadSpin.fail(chalk.yellow("Upload failed — report saved locally"));
|
|
594
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
595
|
+
}
|
|
550
596
|
}
|
|
551
|
-
|
|
597
|
+
|
|
552
598
|
// ════════════════════════════════════════════════════════════
|
|
553
599
|
// FINAL SUMMARY
|
|
554
600
|
// ════════════════════════════════════════════════════════════
|
|
@@ -571,4 +617,4 @@ export async function runFullCommand(
|
|
|
571
617
|
}
|
|
572
618
|
|
|
573
619
|
printDone("Full diagnostic finished.");
|
|
574
|
-
}
|
|
620
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor-cli-dev",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "React performance analyzer with static analysis, runtime profiling, rule engine, and dashboard
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "React performance analyzer with static analysis, runtime profiling, rule engine, and dashboard",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"react-doctor": "./cli/bin/react-doctor.js"
|
|
9
9
|
},
|
|
10
|
-
"scripts": {
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node scripts/postinstall.js",
|
|
12
|
+
"build": "node scripts/build.js",
|
|
13
|
+
"dev": "node scripts/dev.js"
|
|
14
|
+
},
|
|
11
15
|
"keywords": [
|
|
12
16
|
"react",
|
|
13
17
|
"performance",
|
|
14
18
|
"profiler",
|
|
15
19
|
"cli",
|
|
16
|
-
"analyzer"
|
|
20
|
+
"analyzer",
|
|
21
|
+
"dashboard",
|
|
22
|
+
"web-vitals"
|
|
17
23
|
],
|
|
18
24
|
"author": "Ozma",
|
|
19
25
|
"license": "ISC",
|
|
@@ -21,9 +27,12 @@
|
|
|
21
27
|
"@babel/parser": "^7.29.0",
|
|
22
28
|
"@babel/traverse": "^7.29.0",
|
|
23
29
|
"@babel/types": "^7.29.0",
|
|
30
|
+
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
31
|
+
"@fontsource/tajawal": "^5.2.7",
|
|
24
32
|
"axios": "^1.6.0",
|
|
25
33
|
"better-sqlite3": "^12.10.0",
|
|
26
34
|
"chalk": "^4.1.2",
|
|
35
|
+
"chart.js": "^4.5.1",
|
|
27
36
|
"commander": "^12.0.0",
|
|
28
37
|
"cors": "^2.8.6",
|
|
29
38
|
"dotenv": "^17.4.2",
|
|
@@ -45,5 +54,17 @@
|
|
|
45
54
|
"@types/fs-extra": "^11.0.4",
|
|
46
55
|
"@types/node": "^25.9.1",
|
|
47
56
|
"nodemon": "^3.1.14"
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"cli/**/*",
|
|
60
|
+
"backend/**/*",
|
|
61
|
+
"frontend/dist/**/*",
|
|
62
|
+
"core/**/*",
|
|
63
|
+
"scripts/**/*",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE"
|
|
66
|
+
],
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18.0.0"
|
|
48
69
|
}
|
|
49
70
|
}
|
package/shared/dist/index.d.ts
DELETED
package/shared/dist/index.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
// Exporting all types in one common used file
|
|
18
|
-
__exportStar(require("./types"), exports);
|
|
19
|
-
__exportStar(require("./schemas"), exports);
|
package/shared/dist/schemas.d.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import * as v from 'valibot';
|
|
2
|
-
|
|
3
|
-
export declare const WebVitalsSchema: v.ObjectSchema<{
|
|
4
|
-
readonly lcp: v.NumberSchema<undefined>;
|
|
5
|
-
readonly fcp: v.NumberSchema<undefined>;
|
|
6
|
-
readonly cls: v.NumberSchema<undefined>;
|
|
7
|
-
readonly inp: v.NumberSchema<undefined>;
|
|
8
|
-
readonly ttfb: v.NumberSchema<undefined>;
|
|
9
|
-
}, undefined>;
|
|
10
|
-
export declare const ComponentIssueSchema: v.ObjectSchema<{
|
|
11
|
-
readonly id: v.StringSchema<undefined>;
|
|
12
|
-
readonly component: v.StringSchema<undefined>;
|
|
13
|
-
readonly file: v.StringSchema<undefined>;
|
|
14
|
-
readonly line: v.NumberSchema<undefined>;
|
|
15
|
-
readonly severity: v.PicklistSchema<["critical", "warning", "info"], undefined>;
|
|
16
|
-
readonly message: v.StringSchema<undefined>;
|
|
17
|
-
readonly suggestion: v.StringSchema<undefined>;
|
|
18
|
-
}, undefined>;
|
|
19
|
-
export declare const StaticReportSchema: v.ObjectSchema<{
|
|
20
|
-
readonly timestamp: v.StringSchema<undefined>;
|
|
21
|
-
readonly issues: v.ArraySchema<v.ObjectSchema<{
|
|
22
|
-
readonly id: v.StringSchema<undefined>;
|
|
23
|
-
readonly component: v.StringSchema<undefined>;
|
|
24
|
-
readonly file: v.StringSchema<undefined>;
|
|
25
|
-
readonly line: v.NumberSchema<undefined>;
|
|
26
|
-
readonly severity: v.PicklistSchema<["critical", "warning", "info"], undefined>;
|
|
27
|
-
readonly message: v.StringSchema<undefined>;
|
|
28
|
-
readonly suggestion: v.StringSchema<undefined>;
|
|
29
|
-
}, undefined>, undefined>;
|
|
30
|
-
readonly bundleSize: v.NumberSchema<undefined>;
|
|
31
|
-
readonly componentCount: v.NumberSchema<undefined>;
|
|
32
|
-
}, undefined>;
|
|
33
|
-
export declare const RuntimeReportSchema: v.ObjectSchema<{
|
|
34
|
-
readonly timestamp: v.StringSchema<undefined>;
|
|
35
|
-
readonly metrics: v.ObjectSchema<{
|
|
36
|
-
readonly lcp: v.NumberSchema<undefined>;
|
|
37
|
-
readonly fcp: v.NumberSchema<undefined>;
|
|
38
|
-
readonly cls: v.NumberSchema<undefined>;
|
|
39
|
-
readonly inp: v.NumberSchema<undefined>;
|
|
40
|
-
readonly ttfb: v.NumberSchema<undefined>;
|
|
41
|
-
}, undefined>;
|
|
42
|
-
readonly rerenders: v.RecordSchema<v.StringSchema<undefined>, v.NumberSchema<undefined>, undefined>;
|
|
43
|
-
readonly commitDurations: v.ArraySchema<v.NumberSchema<undefined>, undefined>;
|
|
44
|
-
}, undefined>;
|
|
45
|
-
export declare const SuggestionSchema: v.ObjectSchema<{
|
|
46
|
-
readonly id: v.StringSchema<undefined>;
|
|
47
|
-
readonly title: v.StringSchema<undefined>;
|
|
48
|
-
readonly description: v.StringSchema<undefined>;
|
|
49
|
-
readonly severity: v.PicklistSchema<["critical", "warning", "info"], undefined>;
|
|
50
|
-
readonly affectedComponent: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
51
|
-
readonly fix: v.StringSchema<undefined>;
|
|
52
|
-
}, undefined>;
|
|
53
|
-
export declare const FinalReportSchema: v.ObjectSchema<{
|
|
54
|
-
readonly projectName: v.StringSchema<undefined>;
|
|
55
|
-
readonly analyzedAt: v.StringSchema<undefined>;
|
|
56
|
-
readonly static: v.ObjectSchema<{
|
|
57
|
-
readonly timestamp: v.StringSchema<undefined>;
|
|
58
|
-
readonly issues: v.ArraySchema<v.ObjectSchema<{
|
|
59
|
-
readonly id: v.StringSchema<undefined>;
|
|
60
|
-
readonly component: v.StringSchema<undefined>;
|
|
61
|
-
readonly file: v.StringSchema<undefined>;
|
|
62
|
-
readonly line: v.NumberSchema<undefined>;
|
|
63
|
-
readonly severity: v.PicklistSchema<["critical", "warning", "info"], undefined>;
|
|
64
|
-
readonly message: v.StringSchema<undefined>;
|
|
65
|
-
readonly suggestion: v.StringSchema<undefined>;
|
|
66
|
-
}, undefined>, undefined>;
|
|
67
|
-
readonly bundleSize: v.NumberSchema<undefined>;
|
|
68
|
-
readonly componentCount: v.NumberSchema<undefined>;
|
|
69
|
-
}, undefined>;
|
|
70
|
-
readonly runtime: v.ObjectSchema<{
|
|
71
|
-
readonly timestamp: v.StringSchema<undefined>;
|
|
72
|
-
readonly metrics: v.ObjectSchema<{
|
|
73
|
-
readonly lcp: v.NumberSchema<undefined>;
|
|
74
|
-
readonly fcp: v.NumberSchema<undefined>;
|
|
75
|
-
readonly cls: v.NumberSchema<undefined>;
|
|
76
|
-
readonly inp: v.NumberSchema<undefined>;
|
|
77
|
-
readonly ttfb: v.NumberSchema<undefined>;
|
|
78
|
-
}, undefined>;
|
|
79
|
-
readonly rerenders: v.RecordSchema<v.StringSchema<undefined>, v.NumberSchema<undefined>, undefined>;
|
|
80
|
-
readonly commitDurations: v.ArraySchema<v.NumberSchema<undefined>, undefined>;
|
|
81
|
-
}, undefined>;
|
|
82
|
-
readonly suggestions: v.ArraySchema<v.ObjectSchema<{
|
|
83
|
-
readonly id: v.StringSchema<undefined>;
|
|
84
|
-
readonly title: v.StringSchema<undefined>;
|
|
85
|
-
readonly description: v.StringSchema<undefined>;
|
|
86
|
-
readonly severity: v.PicklistSchema<["critical", "warning", "info"], undefined>;
|
|
87
|
-
readonly affectedComponent: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
88
|
-
readonly fix: v.StringSchema<undefined>;
|
|
89
|
-
}, undefined>, undefined>;
|
|
90
|
-
readonly performanceScore: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 100, undefined>]>;
|
|
91
|
-
}, undefined>;
|
package/shared/dist/schemas.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.FinalReportSchema = exports.SuggestionSchema = exports.RuntimeReportSchema = exports.StaticReportSchema = exports.ComponentIssueSchema = exports.WebVitalsSchema = void 0;
|
|
37
|
-
const v = __importStar(require("valibot"));
|
|
38
|
-
// Valibot schemas for validation
|
|
39
|
-
exports.WebVitalsSchema = v.object({
|
|
40
|
-
lcp: v.number(),
|
|
41
|
-
fcp: v.number(),
|
|
42
|
-
cls: v.number(),
|
|
43
|
-
inp: v.number(),
|
|
44
|
-
ttfb: v.number(),
|
|
45
|
-
});
|
|
46
|
-
exports.ComponentIssueSchema = v.object({
|
|
47
|
-
id: v.string(),
|
|
48
|
-
component: v.string(),
|
|
49
|
-
file: v.string(),
|
|
50
|
-
line: v.number(),
|
|
51
|
-
severity: v.picklist(['critical', 'warning', 'info']),
|
|
52
|
-
message: v.string(),
|
|
53
|
-
suggestion: v.string(),
|
|
54
|
-
});
|
|
55
|
-
exports.StaticReportSchema = v.object({
|
|
56
|
-
timestamp: v.string(),
|
|
57
|
-
issues: v.array(exports.ComponentIssueSchema),
|
|
58
|
-
bundleSize: v.number(),
|
|
59
|
-
componentCount: v.number(),
|
|
60
|
-
});
|
|
61
|
-
exports.RuntimeReportSchema = v.object({
|
|
62
|
-
timestamp: v.string(),
|
|
63
|
-
metrics: exports.WebVitalsSchema,
|
|
64
|
-
rerenders: v.record(v.string(), v.number()),
|
|
65
|
-
commitDurations: v.array(v.number()),
|
|
66
|
-
});
|
|
67
|
-
exports.SuggestionSchema = v.object({
|
|
68
|
-
id: v.string(),
|
|
69
|
-
title: v.string(),
|
|
70
|
-
description: v.string(),
|
|
71
|
-
severity: v.picklist(['critical', 'warning', 'info']),
|
|
72
|
-
affectedComponent: v.optional(v.string()),
|
|
73
|
-
fix: v.string(),
|
|
74
|
-
});
|
|
75
|
-
exports.FinalReportSchema = v.object({
|
|
76
|
-
projectName: v.string(),
|
|
77
|
-
analyzedAt: v.string(),
|
|
78
|
-
static: exports.StaticReportSchema,
|
|
79
|
-
runtime: exports.RuntimeReportSchema,
|
|
80
|
-
suggestions: v.array(exports.SuggestionSchema),
|
|
81
|
-
performanceScore: v.pipe(v.number(), v.minValue(0), v.maxValue(100)),
|
|
82
|
-
});
|
package/shared/dist/types.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export interface WebVitals {
|
|
2
|
-
lcp: number;
|
|
3
|
-
fcp: number;
|
|
4
|
-
cls: number;
|
|
5
|
-
inp: number;
|
|
6
|
-
ttfb: number;
|
|
7
|
-
}
|
|
8
|
-
export interface ComponentIssue {
|
|
9
|
-
id: string;
|
|
10
|
-
component: string;
|
|
11
|
-
file: string;
|
|
12
|
-
line: number;
|
|
13
|
-
severity: "critical" | "warning" | "info";
|
|
14
|
-
message: string;
|
|
15
|
-
suggestion: string;
|
|
16
|
-
}
|
|
17
|
-
export interface StaticReport {
|
|
18
|
-
timestamp: string;
|
|
19
|
-
issues: ComponentIssue[];
|
|
20
|
-
bundleSize: number;
|
|
21
|
-
componentCount: number;
|
|
22
|
-
}
|
|
23
|
-
export interface RuntimeReport {
|
|
24
|
-
timestamp: string;
|
|
25
|
-
metrics: WebVitals;
|
|
26
|
-
rerenders: Record<string, number>;
|
|
27
|
-
commitDurations: number[];
|
|
28
|
-
}
|
|
29
|
-
export interface Suggestion {
|
|
30
|
-
id: string;
|
|
31
|
-
title: string;
|
|
32
|
-
description: string;
|
|
33
|
-
severity: "critical" | "warning" | "info";
|
|
34
|
-
affectedComponent?: string;
|
|
35
|
-
fix: string;
|
|
36
|
-
}
|
|
37
|
-
export interface FinalReport {
|
|
38
|
-
projectName: string;
|
|
39
|
-
analyzedAt: string;
|
|
40
|
-
static: StaticReport;
|
|
41
|
-
runtime: RuntimeReport;
|
|
42
|
-
suggestions: Suggestion[];
|
|
43
|
-
performanceScore: number;
|
|
44
|
-
}
|
package/shared/dist/types.js
DELETED