react-doctor-cli-dev 1.0.2 → 1.0.4
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/reports.db +0 -0
- package/backend/data/screenshots/1--fcp.png +0 -0
- package/backend/data/screenshots/1--fullLoad.png +0 -0
- package/backend/data/screenshots/2--fcp.png +0 -0
- package/backend/data/screenshots/2--fullLoad.png +0 -0
- package/backend/data/screenshots/3--fcp.png +0 -0
- package/backend/data/screenshots/3--fullLoad.png +0 -0
- package/backend/data/screenshots/4--fcp.png +0 -0
- package/backend/data/screenshots/4--fullLoad.png +0 -0
- package/backend/dist/db.js +13 -56
- package/backend/dist/index.js +13 -20
- package/backend/dist/routes/reports.js +3 -35
- package/backend/src/db.ts +16 -61
- package/backend/src/index.ts +25 -32
- package/backend/src/routes/reports.ts +3 -1
- package/cli/dist/commands/dashboard.js +152 -0
- package/cli/dist/index.js +24 -50
- package/cli/src/commands/dashboard.ts +179 -0
- package/cli/src/index.ts +33 -60
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/backend/dist/db.js
CHANGED
|
@@ -1,69 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ─────────────────────────────────────────────────────────────
|
|
3
|
-
// backend/src/db.ts
|
|
4
|
-
//
|
|
5
|
-
// SQLite database via better-sqlite3 (synchronous, fast).
|
|
6
|
-
//
|
|
7
|
-
// SCHEMA:
|
|
8
|
-
//
|
|
9
|
-
// reports — one row per analysis run
|
|
10
|
-
// id — auto-increment PK
|
|
11
|
-
// project — project name / hostname
|
|
12
|
-
// score — 0-100 overall performance score
|
|
13
|
-
// grade — A+, A, B, C, D, F
|
|
14
|
-
// analyzed_at — ISO timestamp from the CLI
|
|
15
|
-
// created_at — when the row was inserted
|
|
16
|
-
// static_json — full StaticReport as JSON string
|
|
17
|
-
// runtime_json — full RuntimeReport map as JSON string
|
|
18
|
-
// (screenshots stripped — stored as files)
|
|
19
|
-
// suggestions — full Suggestion[] as JSON string
|
|
20
|
-
//
|
|
21
|
-
// WHY SPLIT COLUMNS INSTEAD OF ONE payload COLUMN?
|
|
22
|
-
// The old schema stored everything in a single `payload` TEXT
|
|
23
|
-
// column. This meant the dashboard couldn't query individual
|
|
24
|
-
// fields without parsing the whole blob. Splitting into three
|
|
25
|
-
// typed columns lets us:
|
|
26
|
-
// • SELECT only the part we need (e.g. just static_json for
|
|
27
|
-
// the issues page)
|
|
28
|
-
// • Keep the list endpoint fast (no giant blobs per row)
|
|
29
|
-
// • Add indices on score/grade later without changing schema
|
|
30
|
-
//
|
|
31
|
-
// SCREENSHOT STORAGE:
|
|
32
|
-
// Screenshots are base64 PNGs — up to 200KB each. Storing them
|
|
33
|
-
// in SQLite bloats the DB and makes every query slower. Instead:
|
|
34
|
-
// • The upload route strips dataUrls from runtime_json
|
|
35
|
-
// • Saves each screenshot as a .png file in data/screenshots/
|
|
36
|
-
// • Replaces dataUrl with a relative path /screenshots/<file>
|
|
37
|
-
// The dashboard fetches screenshots via the static file route.
|
|
38
|
-
// ─────────────────────────────────────────────────────────────
|
|
39
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
4
|
};
|
|
42
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.screenshotsDir = void 0;
|
|
44
6
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
45
7
|
const path_1 = __importDefault(require("path"));
|
|
46
8
|
const fs_1 = __importDefault(require("fs"));
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const dbPath = process.env.DB_PATH || DEFAULT_DB;
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
// Store DB in user's home directory — works regardless of where process spawns from
|
|
11
|
+
const dbDir = path_1.default.join(os_1.default.homedir(), ".react-doctor");
|
|
12
|
+
const dbPath = process.env.DB_PATH || path_1.default.join(dbDir, "reports.db");
|
|
52
13
|
fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
|
|
53
|
-
fs_1.default.mkdirSync(path_1.default.join(path_1.default.dirname(dbPath), "screenshots"), { recursive: true });
|
|
54
14
|
const db = new better_sqlite3_1.default(dbPath);
|
|
55
15
|
db.exec(`
|
|
56
|
-
|
|
57
|
-
id
|
|
58
|
-
project
|
|
59
|
-
score
|
|
60
|
-
grade
|
|
61
|
-
analyzed_at
|
|
62
|
-
created_at
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
suggestions TEXT NOT NULL
|
|
66
|
-
);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
project TEXT NOT NULL,
|
|
19
|
+
score INTEGER NOT NULL,
|
|
20
|
+
grade TEXT NOT NULL,
|
|
21
|
+
analyzed_at TEXT NOT NULL,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
payload TEXT NOT NULL
|
|
24
|
+
);
|
|
67
25
|
`);
|
|
68
|
-
exports.screenshotsDir = path_1.default.join(path_1.default.dirname(dbPath), "screenshots");
|
|
69
26
|
exports.default = db;
|
package/backend/dist/index.js
CHANGED
|
@@ -9,34 +9,27 @@ const helmet_1 = __importDefault(require("helmet"));
|
|
|
9
9
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const reports_1 = __importDefault(require("./routes/reports"));
|
|
12
|
-
|
|
13
|
-
dotenv_1.default.config({ path: path_1.default.join(__dirname,
|
|
12
|
+
// Load .env from the backend folder, not from cwd
|
|
13
|
+
dotenv_1.default.config({ path: path_1.default.join(__dirname, '..', '.env') });
|
|
14
14
|
const app = (0, express_1.default)();
|
|
15
15
|
const PORT = process.env.PORT || 3000;
|
|
16
|
-
|
|
16
|
+
const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
|
|
17
|
+
// Make API_KEY available globally so auth middleware can use it
|
|
18
|
+
process.env.API_KEY = API_KEY;
|
|
17
19
|
app.use((0, helmet_1.default)());
|
|
18
20
|
app.use((0, cors_1.default)());
|
|
19
|
-
app.use(express_1.default.json({ limit:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
app.use("/screenshots", express_1.default.static(db_1.screenshotsDir));
|
|
24
|
-
// ── Routes ────────────────────────────────────────────────────
|
|
25
|
-
app.use("/api/reports", reports_1.default);
|
|
26
|
-
// ── Health check ──────────────────────────────────────────────
|
|
27
|
-
app.get("/health", (_req, res) => {
|
|
28
|
-
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
21
|
+
app.use(express_1.default.json({ limit: '50mb' }));
|
|
22
|
+
app.use('/api/reports', reports_1.default);
|
|
23
|
+
app.get('/health', (_req, res) => {
|
|
24
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
29
25
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
res.status(404).json({ message: "Route not found" });
|
|
26
|
+
app.use((req, res) => {
|
|
27
|
+
res.status(404).json({ message: 'Route not found' });
|
|
33
28
|
});
|
|
34
|
-
// ── Error handler ─────────────────────────────────────────────
|
|
35
29
|
app.use((err, _req, res, _next) => {
|
|
36
30
|
console.error(err.stack);
|
|
37
|
-
res.status(500).json({ message:
|
|
31
|
+
res.status(500).json({ message: 'Internal Server Error' });
|
|
38
32
|
});
|
|
39
|
-
// ── Start ─────────────────────────────────────────────────────
|
|
40
33
|
app.listen(PORT, () => {
|
|
41
|
-
console.log(
|
|
34
|
+
console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
|
|
42
35
|
});
|
|
@@ -31,39 +31,6 @@
|
|
|
31
31
|
// /screenshots/<filename>
|
|
32
32
|
// so the dashboard can load them as normal <img> tags.
|
|
33
33
|
// ─────────────────────────────────────────────────────────────
|
|
34
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
35
|
-
if (k2 === undefined) k2 = k;
|
|
36
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
37
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
38
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
39
|
-
}
|
|
40
|
-
Object.defineProperty(o, k2, desc);
|
|
41
|
-
}) : (function(o, m, k, k2) {
|
|
42
|
-
if (k2 === undefined) k2 = k;
|
|
43
|
-
o[k2] = m[k];
|
|
44
|
-
}));
|
|
45
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
46
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
47
|
-
}) : function(o, v) {
|
|
48
|
-
o["default"] = v;
|
|
49
|
-
});
|
|
50
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
51
|
-
var ownKeys = function(o) {
|
|
52
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
53
|
-
var ar = [];
|
|
54
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
55
|
-
return ar;
|
|
56
|
-
};
|
|
57
|
-
return ownKeys(o);
|
|
58
|
-
};
|
|
59
|
-
return function (mod) {
|
|
60
|
-
if (mod && mod.__esModule) return mod;
|
|
61
|
-
var result = {};
|
|
62
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
63
|
-
__setModuleDefault(result, mod);
|
|
64
|
-
return result;
|
|
65
|
-
};
|
|
66
|
-
})();
|
|
67
34
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
68
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
69
36
|
};
|
|
@@ -71,8 +38,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
71
38
|
const express_1 = require("express");
|
|
72
39
|
const path_1 = __importDefault(require("path"));
|
|
73
40
|
const fs_1 = __importDefault(require("fs"));
|
|
74
|
-
const db_1 =
|
|
41
|
+
const db_1 = __importDefault(require("../db"));
|
|
75
42
|
const auth_1 = require("../middleware/auth");
|
|
43
|
+
const screenshotsDir = "data/screenshots";
|
|
76
44
|
const router = (0, express_1.Router)();
|
|
77
45
|
// ── GET /api/reports ─────────────────────────────────────────
|
|
78
46
|
// Summary list — no blobs, just the columns the history page needs.
|
|
@@ -241,7 +209,7 @@ function saveScreenshots(reportId, pending) {
|
|
|
241
209
|
const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
|
|
242
210
|
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
243
211
|
const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
|
|
244
|
-
const fullPath = path_1.default.join(
|
|
212
|
+
const fullPath = path_1.default.join(screenshotsDir, filename);
|
|
245
213
|
try {
|
|
246
214
|
fs_1.default.writeFileSync(fullPath, shot.buffer);
|
|
247
215
|
saved.push({
|
package/backend/src/db.ts
CHANGED
|
@@ -1,70 +1,25 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// backend/src/db.ts
|
|
3
|
-
//
|
|
4
|
-
// SQLite database via better-sqlite3 (synchronous, fast).
|
|
5
|
-
//
|
|
6
|
-
// SCHEMA:
|
|
7
|
-
//
|
|
8
|
-
// reports — one row per analysis run
|
|
9
|
-
// id — auto-increment PK
|
|
10
|
-
// project — project name / hostname
|
|
11
|
-
// score — 0-100 overall performance score
|
|
12
|
-
// grade — A+, A, B, C, D, F
|
|
13
|
-
// analyzed_at — ISO timestamp from the CLI
|
|
14
|
-
// created_at — when the row was inserted
|
|
15
|
-
// static_json — full StaticReport as JSON string
|
|
16
|
-
// runtime_json — full RuntimeReport map as JSON string
|
|
17
|
-
// (screenshots stripped — stored as files)
|
|
18
|
-
// suggestions — full Suggestion[] as JSON string
|
|
19
|
-
//
|
|
20
|
-
// WHY SPLIT COLUMNS INSTEAD OF ONE payload COLUMN?
|
|
21
|
-
// The old schema stored everything in a single `payload` TEXT
|
|
22
|
-
// column. This meant the dashboard couldn't query individual
|
|
23
|
-
// fields without parsing the whole blob. Splitting into three
|
|
24
|
-
// typed columns lets us:
|
|
25
|
-
// • SELECT only the part we need (e.g. just static_json for
|
|
26
|
-
// the issues page)
|
|
27
|
-
// • Keep the list endpoint fast (no giant blobs per row)
|
|
28
|
-
// • Add indices on score/grade later without changing schema
|
|
29
|
-
//
|
|
30
|
-
// SCREENSHOT STORAGE:
|
|
31
|
-
// Screenshots are base64 PNGs — up to 200KB each. Storing them
|
|
32
|
-
// in SQLite bloats the DB and makes every query slower. Instead:
|
|
33
|
-
// • The upload route strips dataUrls from runtime_json
|
|
34
|
-
// • Saves each screenshot as a .png file in data/screenshots/
|
|
35
|
-
// • Replaces dataUrl with a relative path /screenshots/<file>
|
|
36
|
-
// The dashboard fetches screenshots via the static file route.
|
|
37
|
-
// ─────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
1
|
import Database from "better-sqlite3";
|
|
40
|
-
import path
|
|
41
|
-
import fs
|
|
42
|
-
import
|
|
43
|
-
|
|
44
|
-
dotenv.config({ path: path.join(__dirname, "..", ".env") });
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
45
5
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const dbPath
|
|
6
|
+
// Store DB in user's home directory — works regardless of where process spawns from
|
|
7
|
+
const dbDir = path.join(os.homedir(), ".react-doctor");
|
|
8
|
+
const dbPath = process.env.DB_PATH || path.join(dbDir, "reports.db");
|
|
49
9
|
|
|
50
10
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
51
|
-
fs.mkdirSync(path.join(path.dirname(dbPath), "screenshots"), { recursive: true });
|
|
52
11
|
|
|
53
12
|
const db = new Database(dbPath);
|
|
54
|
-
|
|
55
13
|
db.exec(`
|
|
56
|
-
|
|
57
|
-
id
|
|
58
|
-
project
|
|
59
|
-
score
|
|
60
|
-
grade
|
|
61
|
-
analyzed_at
|
|
62
|
-
created_at
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
suggestions TEXT NOT NULL
|
|
66
|
-
);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
project TEXT NOT NULL,
|
|
17
|
+
score INTEGER NOT NULL,
|
|
18
|
+
grade TEXT NOT NULL,
|
|
19
|
+
analyzed_at TEXT NOT NULL,
|
|
20
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
21
|
+
payload TEXT NOT NULL
|
|
22
|
+
);
|
|
67
23
|
`);
|
|
68
24
|
|
|
69
|
-
export
|
|
70
|
-
export default db;
|
|
25
|
+
export default db;
|
package/backend/src/index.ts
CHANGED
|
@@ -1,46 +1,39 @@
|
|
|
1
|
-
import express, { Request, Response, NextFunction } from
|
|
2
|
-
import cors
|
|
3
|
-
import helmet
|
|
4
|
-
import dotenv
|
|
5
|
-
import path
|
|
6
|
-
import reportRoutes
|
|
7
|
-
import db
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import helmet from 'helmet';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import reportRoutes from './routes/reports';
|
|
7
|
+
import db from './db';
|
|
8
|
+
|
|
9
|
+
// Load .env from the backend folder, not from cwd
|
|
10
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
|
11
|
+
|
|
12
|
+
const app = express();
|
|
12
13
|
const PORT = process.env.PORT || 3000;
|
|
14
|
+
const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
|
|
15
|
+
|
|
16
|
+
// Make API_KEY available globally so auth middleware can use it
|
|
17
|
+
process.env.API_KEY = API_KEY;
|
|
13
18
|
|
|
14
|
-
// ── Middleware ────────────────────────────────────────────────
|
|
15
19
|
app.use(helmet());
|
|
16
20
|
app.use(cors());
|
|
17
|
-
app.use(express.json({ limit:
|
|
21
|
+
app.use(express.json({ limit: '50mb' }));
|
|
22
|
+
app.use('/api/reports', reportRoutes);
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// The dashboard uses these as <img src="/screenshots/...">
|
|
22
|
-
app.use("/screenshots", express.static(screenshotsDir));
|
|
23
|
-
|
|
24
|
-
// ── Routes ────────────────────────────────────────────────────
|
|
25
|
-
app.use("/api/reports", reportRoutes);
|
|
26
|
-
|
|
27
|
-
// ── Health check ──────────────────────────────────────────────
|
|
28
|
-
app.get("/health", (_req: Request, res: Response) => {
|
|
29
|
-
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
24
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
25
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
30
26
|
});
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
res.status(404).json({ message: "Route not found" });
|
|
28
|
+
app.use((req: Request, res: Response) => {
|
|
29
|
+
res.status(404).json({ message: 'Route not found' });
|
|
35
30
|
});
|
|
36
31
|
|
|
37
|
-
// ── Error handler ─────────────────────────────────────────────
|
|
38
32
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
39
33
|
console.error(err.stack);
|
|
40
|
-
res.status(500).json({ message:
|
|
34
|
+
res.status(500).json({ message: 'Internal Server Error' });
|
|
41
35
|
});
|
|
42
36
|
|
|
43
|
-
// ── Start ─────────────────────────────────────────────────────
|
|
44
37
|
app.listen(PORT, () => {
|
|
45
|
-
console.log(
|
|
46
|
-
});
|
|
38
|
+
console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
|
|
39
|
+
});
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
import { Router, Request, Response, RequestHandler } from "express";
|
|
35
35
|
import path from "path";
|
|
36
36
|
import fs from "fs";
|
|
37
|
-
import db
|
|
37
|
+
import db from "../db";
|
|
38
38
|
import { requireApiKey } from "../middleware/auth";
|
|
39
39
|
|
|
40
|
+
const screenshotsDir = "data/screenshots";
|
|
41
|
+
|
|
40
42
|
const router = Router();
|
|
41
43
|
|
|
42
44
|
// ── GET /api/reports ─────────────────────────────────────────
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// cli/src/commands/dashboard.ts
|
|
4
|
+
//
|
|
5
|
+
// react-doctor dashboard
|
|
6
|
+
//
|
|
7
|
+
// Opens the React Doctor dashboard in the browser.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IT DOES:
|
|
10
|
+
// 1. Checks if the backend is already running on the port
|
|
11
|
+
// 2. If not — starts it automatically (same logic as --upload)
|
|
12
|
+
// 3. Opens http://localhost:PORT in the default browser
|
|
13
|
+
//
|
|
14
|
+
// This command is the natural companion to --upload.
|
|
15
|
+
// Workflow:
|
|
16
|
+
// react-doctor full ./my-app --upload ← runs analysis + saves report
|
|
17
|
+
// react-doctor dashboard ← opens the dashboard to view it
|
|
18
|
+
//
|
|
19
|
+
// Or in one shot:
|
|
20
|
+
// react-doctor full ./my-app --upload && react-doctor dashboard
|
|
21
|
+
// ─────────────────────────────────────────────────────────────
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.registerDashboardCommand = registerDashboardCommand;
|
|
27
|
+
const path_1 = __importDefault(require("path"));
|
|
28
|
+
const fs_1 = __importDefault(require("fs"));
|
|
29
|
+
const axios_1 = __importDefault(require("axios"));
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
32
|
+
const ui_1 = require("../ui");
|
|
33
|
+
function registerDashboardCommand(program) {
|
|
34
|
+
program
|
|
35
|
+
.command("dashboard")
|
|
36
|
+
.description("Open the React Doctor dashboard (auto-starts backend if needed)")
|
|
37
|
+
.option("--port <port>", "Port the backend runs on", "3000")
|
|
38
|
+
.option("--api-key <key>", "API key for the backend", process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this")
|
|
39
|
+
.option("--no-banner", "Skip the banner")
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
if (!options.noBanner)
|
|
42
|
+
(0, ui_1.printBanner)();
|
|
43
|
+
const port = options.port;
|
|
44
|
+
const apiUrl = `http://localhost:${port}`;
|
|
45
|
+
(0, ui_1.printSection)("Dashboard");
|
|
46
|
+
(0, ui_1.printInfo)("Backend URL", apiUrl);
|
|
47
|
+
console.log();
|
|
48
|
+
const spin = (0, ui_1.spinner)("Checking backend status...");
|
|
49
|
+
try {
|
|
50
|
+
// ── 1. Check if backend is already up ─────────────────
|
|
51
|
+
let backendRunning = false;
|
|
52
|
+
try {
|
|
53
|
+
await axios_1.default.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
54
|
+
backendRunning = true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
backendRunning = false;
|
|
58
|
+
}
|
|
59
|
+
// ── 2. Start backend if not running ───────────────────
|
|
60
|
+
if (!backendRunning) {
|
|
61
|
+
spin.text = " Backend not running — starting automatically...";
|
|
62
|
+
// Locate backend folder (sibling of cli/)
|
|
63
|
+
const projectRoot = path_1.default.resolve(__dirname, "..", "..", "..");
|
|
64
|
+
const backendRoot = path_1.default.resolve(projectRoot, "backend");
|
|
65
|
+
const backendDist = path_1.default.join(backendRoot, "dist", "index.js");
|
|
66
|
+
const backendSrc = path_1.default.join(backendRoot, "src", "index.ts");
|
|
67
|
+
let command;
|
|
68
|
+
let args;
|
|
69
|
+
if (fs_1.default.existsSync(backendDist)) {
|
|
70
|
+
command = "node";
|
|
71
|
+
args = [backendDist];
|
|
72
|
+
}
|
|
73
|
+
else if (fs_1.default.existsSync(backendSrc)) {
|
|
74
|
+
command = "npx";
|
|
75
|
+
args = ["ts-node", backendSrc];
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
spin.fail(chalk_1.default.red("Backend not found"));
|
|
79
|
+
(0, ui_1.printFail)(`Could not find backend at: ${backendRoot}\n\n` +
|
|
80
|
+
` Make sure the 'backend/' folder exists next to 'cli/'.`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
// Create data dir inside npm global cache for the backend DB
|
|
84
|
+
const dataDir = path_1.default.join(backendRoot, "data");
|
|
85
|
+
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
86
|
+
(0, child_process_1.spawn)(command, args, {
|
|
87
|
+
stdio: "ignore",
|
|
88
|
+
detached: true,
|
|
89
|
+
env: {
|
|
90
|
+
...process.env,
|
|
91
|
+
API_KEY: options.apiKey,
|
|
92
|
+
PORT: port,
|
|
93
|
+
DB_PATH: path_1.default.join(dataDir, "reports.db"),
|
|
94
|
+
},
|
|
95
|
+
cwd: backendRoot,
|
|
96
|
+
}).unref(); // let CLI exit without killing the server
|
|
97
|
+
// Wait for backend to be ready (up to 15 seconds)
|
|
98
|
+
let ready = false;
|
|
99
|
+
let retries = 0;
|
|
100
|
+
while (!ready && retries < 15) {
|
|
101
|
+
try {
|
|
102
|
+
await axios_1.default.get(`${apiUrl}/health`, { timeout: 1000 });
|
|
103
|
+
ready = true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
107
|
+
retries++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!ready) {
|
|
111
|
+
spin.fail(chalk_1.default.red("Backend failed to start"));
|
|
112
|
+
(0, ui_1.printFail)("Backend did not respond after 15 seconds.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
spin.succeed(chalk_1.default.green("Backend started successfully"));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
spin.succeed(chalk_1.default.green("Backend already running"));
|
|
119
|
+
}
|
|
120
|
+
// ── 3. Open dashboard in default browser ──────────────
|
|
121
|
+
const dashboardUrl = apiUrl;
|
|
122
|
+
console.log();
|
|
123
|
+
(0, ui_1.printInfo)("Opening", dashboardUrl);
|
|
124
|
+
console.log();
|
|
125
|
+
// Cross-platform browser open
|
|
126
|
+
const openCmd = process.platform === "win32" ? ["cmd", ["/c", "start", dashboardUrl]] :
|
|
127
|
+
process.platform === "darwin" ? ["open", [dashboardUrl]] :
|
|
128
|
+
["xdg-open", [dashboardUrl]];
|
|
129
|
+
(0, child_process_1.spawn)(openCmd[0], openCmd[1], {
|
|
130
|
+
stdio: "ignore",
|
|
131
|
+
detached: true,
|
|
132
|
+
}).unref();
|
|
133
|
+
(0, ui_1.printDone)(`Dashboard opened at ${chalk_1.default.cyan(dashboardUrl)}`);
|
|
134
|
+
// ── 4. Show quick API reference ───────────────────────
|
|
135
|
+
console.log(chalk_1.default.gray(" Available endpoints:"));
|
|
136
|
+
console.log(chalk_1.default.cyan(` GET ${apiUrl}/health`));
|
|
137
|
+
console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports`));
|
|
138
|
+
console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports/:id`));
|
|
139
|
+
console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports/project/:name`));
|
|
140
|
+
console.log(chalk_1.default.cyan(` POST ${apiUrl}/api/reports/upload`));
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk_1.default.gray(" Tip: run ") +
|
|
143
|
+
chalk_1.default.cyan("react-doctor full ./ --upload") +
|
|
144
|
+
chalk_1.default.gray(" to add a new report.\n"));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
spin.fail(chalk_1.default.red("Dashboard failed to open"));
|
|
148
|
+
console.log(chalk_1.default.red(`\n ${err.message}\n`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
package/cli/dist/index.js
CHANGED
|
@@ -1,77 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
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.
|
|
4
|
+
// cli/src/index.ts — CLI entry point
|
|
27
5
|
// ─────────────────────────────────────────────────────────────
|
|
28
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
7
|
const commander_1 = require("commander");
|
|
30
8
|
const analyze_1 = require("./commands/analyze");
|
|
31
9
|
const profile_1 = require("./commands/profile");
|
|
32
10
|
const full_1 = require("./commands/full");
|
|
33
|
-
const
|
|
11
|
+
const dashboard_1 = require("./commands/dashboard");
|
|
34
12
|
const program = new commander_1.Command();
|
|
35
13
|
// ── Program metadata ──────────────────────────────────────────
|
|
36
14
|
program
|
|
37
15
|
.name("react-doctor")
|
|
38
16
|
.description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
|
|
39
|
-
.version("1.0.
|
|
40
|
-
// ── Register
|
|
41
|
-
// Each function adds one command to the program.
|
|
42
|
-
// The order here is the order they appear in --help output.
|
|
17
|
+
.version("1.0.2");
|
|
18
|
+
// ── Register commands ─────────────────────────────────────────
|
|
43
19
|
(0, full_1.registerFullCommand)(program); // react-doctor full
|
|
44
20
|
(0, analyze_1.registerAnalyzeCommand)(program); // react-doctor analyze
|
|
45
21
|
(0, profile_1.registerProfileCommand)(program); // react-doctor profile
|
|
46
|
-
(0,
|
|
47
|
-
// ── Usage examples
|
|
22
|
+
(0, dashboard_1.registerDashboardCommand)(program); // react-doctor dashboard
|
|
23
|
+
// ── Usage examples ────────────────────────────────────────────
|
|
48
24
|
program.addHelpText("after", `
|
|
49
25
|
Examples:
|
|
50
|
-
$ react-doctor full ./my-app
|
|
51
|
-
$ react-doctor full ./my-app --mobile
|
|
52
|
-
$ react-doctor full ./my-app --desktop --mobile
|
|
53
|
-
$ react-doctor full ./my-app --cpu 4
|
|
54
|
-
$ react-doctor full ./my-app --throttle slow4g
|
|
55
|
-
$ react-doctor full ./my-app --throttle 3g
|
|
56
|
-
$ react-doctor full ./my-app --cpu 4 --throttle 3g
|
|
57
|
-
$ react-doctor full ./my-app --upload
|
|
26
|
+
$ react-doctor full ./my-app Run full diagnostic (desktop)
|
|
27
|
+
$ react-doctor full ./my-app --mobile Include mobile viewport
|
|
28
|
+
$ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
|
|
29
|
+
$ react-doctor full ./my-app --cpu 4 Simulate slow Android device
|
|
30
|
+
$ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
|
|
31
|
+
$ react-doctor full ./my-app --throttle 3g Simulate 3G network
|
|
32
|
+
$ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
|
|
33
|
+
$ react-doctor full ./my-app --upload Run + save report to dashboard
|
|
58
34
|
|
|
59
|
-
$ react-doctor analyze ./my-app
|
|
60
|
-
$ react-doctor analyze ./my-app --full
|
|
35
|
+
$ react-doctor analyze ./my-app Static code analysis only
|
|
36
|
+
$ react-doctor analyze ./my-app --full Static + runtime + rules
|
|
61
37
|
|
|
62
|
-
$ react-doctor profile ./my-app
|
|
63
|
-
$ react-doctor profile ./my-app --mobile
|
|
64
|
-
$ react-doctor profile ./my-app --desktop --mobile
|
|
65
|
-
$ react-doctor profile ./my-app --cpu 4
|
|
66
|
-
$ react-doctor profile ./my-app --throttle slow4g
|
|
67
|
-
$ react-doctor profile ./my-app --throttle 3g
|
|
38
|
+
$ react-doctor profile ./my-app Runtime profiling only (desktop)
|
|
39
|
+
$ react-doctor profile ./my-app --mobile Mobile viewport
|
|
40
|
+
$ react-doctor profile ./my-app --desktop --mobile Both devices
|
|
41
|
+
$ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
|
|
42
|
+
$ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
|
|
43
|
+
$ react-doctor profile ./my-app --throttle 3g Simulate 3G network
|
|
68
44
|
|
|
69
|
-
$ react-doctor
|
|
70
|
-
$ react-doctor
|
|
45
|
+
$ react-doctor dashboard Open dashboard (auto-starts backend)
|
|
46
|
+
$ react-doctor dashboard --port 4000 Use custom port
|
|
71
47
|
`);
|
|
72
48
|
// ── 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
49
|
if (process.argv.length < 3) {
|
|
76
50
|
program.help();
|
|
77
51
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// cli/src/commands/dashboard.ts
|
|
3
|
+
//
|
|
4
|
+
// react-doctor dashboard
|
|
5
|
+
//
|
|
6
|
+
// Opens the React Doctor dashboard in the browser.
|
|
7
|
+
//
|
|
8
|
+
// WHAT IT DOES:
|
|
9
|
+
// 1. Checks if the backend is already running on the port
|
|
10
|
+
// 2. If not — starts it automatically (same logic as --upload)
|
|
11
|
+
// 3. Opens http://localhost:PORT in the default browser
|
|
12
|
+
//
|
|
13
|
+
// This command is the natural companion to --upload.
|
|
14
|
+
// Workflow:
|
|
15
|
+
// react-doctor full ./my-app --upload ← runs analysis + saves report
|
|
16
|
+
// react-doctor dashboard ← opens the dashboard to view it
|
|
17
|
+
//
|
|
18
|
+
// Or in one shot:
|
|
19
|
+
// react-doctor full ./my-app --upload && react-doctor dashboard
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
import { Command } from "commander";
|
|
23
|
+
import path from "path";
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
import axios from "axios";
|
|
26
|
+
import { spawn } from "child_process";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
import {
|
|
29
|
+
printBanner, printSection,
|
|
30
|
+
printDone, printFail, printInfo, spinner,
|
|
31
|
+
} from "../ui";
|
|
32
|
+
|
|
33
|
+
export function registerDashboardCommand(program: Command): void {
|
|
34
|
+
program
|
|
35
|
+
.command("dashboard")
|
|
36
|
+
.description("Open the React Doctor dashboard (auto-starts backend if needed)")
|
|
37
|
+
.option(
|
|
38
|
+
"--port <port>",
|
|
39
|
+
"Port the backend runs on",
|
|
40
|
+
"3000",
|
|
41
|
+
)
|
|
42
|
+
.option(
|
|
43
|
+
"--api-key <key>",
|
|
44
|
+
"API key for the backend",
|
|
45
|
+
process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this",
|
|
46
|
+
)
|
|
47
|
+
.option("--no-banner", "Skip the banner")
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
|
|
50
|
+
if (!options.noBanner) printBanner();
|
|
51
|
+
|
|
52
|
+
const port = options.port;
|
|
53
|
+
const apiUrl = `http://localhost:${port}`;
|
|
54
|
+
|
|
55
|
+
printSection("Dashboard");
|
|
56
|
+
printInfo("Backend URL", apiUrl);
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
const spin = spinner("Checking backend status...");
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// ── 1. Check if backend is already up ─────────────────
|
|
63
|
+
let backendRunning = false;
|
|
64
|
+
try {
|
|
65
|
+
await axios.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
66
|
+
backendRunning = true;
|
|
67
|
+
} catch {
|
|
68
|
+
backendRunning = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 2. Start backend if not running ───────────────────
|
|
72
|
+
if (!backendRunning) {
|
|
73
|
+
spin.text = " Backend not running — starting automatically...";
|
|
74
|
+
|
|
75
|
+
// Locate backend folder (sibling of cli/)
|
|
76
|
+
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
77
|
+
const backendRoot = path.resolve(projectRoot, "backend");
|
|
78
|
+
const backendDist = path.join(backendRoot, "dist", "index.js");
|
|
79
|
+
const backendSrc = path.join(backendRoot, "src", "index.ts");
|
|
80
|
+
|
|
81
|
+
let command: string;
|
|
82
|
+
let args: string[];
|
|
83
|
+
|
|
84
|
+
if (fs.existsSync(backendDist)) {
|
|
85
|
+
command = "node";
|
|
86
|
+
args = [backendDist];
|
|
87
|
+
} else if (fs.existsSync(backendSrc)) {
|
|
88
|
+
command = "npx";
|
|
89
|
+
args = ["ts-node", backendSrc];
|
|
90
|
+
} else {
|
|
91
|
+
spin.fail(chalk.red("Backend not found"));
|
|
92
|
+
printFail(
|
|
93
|
+
`Could not find backend at: ${backendRoot}\n\n` +
|
|
94
|
+
` Make sure the 'backend/' folder exists next to 'cli/'.`,
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create data dir inside npm global cache for the backend DB
|
|
100
|
+
const dataDir = path.join(backendRoot, "data");
|
|
101
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
spawn(command, args, {
|
|
104
|
+
stdio: "ignore",
|
|
105
|
+
detached: true,
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
API_KEY: options.apiKey,
|
|
109
|
+
PORT: port,
|
|
110
|
+
DB_PATH: path.join(dataDir, "reports.db"),
|
|
111
|
+
},
|
|
112
|
+
cwd: backendRoot,
|
|
113
|
+
}).unref(); // let CLI exit without killing the server
|
|
114
|
+
|
|
115
|
+
// Wait for backend to be ready (up to 15 seconds)
|
|
116
|
+
let ready = false;
|
|
117
|
+
let retries = 0;
|
|
118
|
+
while (!ready && retries < 15) {
|
|
119
|
+
try {
|
|
120
|
+
await axios.get(`${apiUrl}/health`, { timeout: 1000 });
|
|
121
|
+
ready = true;
|
|
122
|
+
} catch {
|
|
123
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
124
|
+
retries++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!ready) {
|
|
129
|
+
spin.fail(chalk.red("Backend failed to start"));
|
|
130
|
+
printFail("Backend did not respond after 15 seconds.");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
spin.succeed(chalk.green("Backend started successfully"));
|
|
135
|
+
} else {
|
|
136
|
+
spin.succeed(chalk.green("Backend already running"));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── 3. Open dashboard in default browser ──────────────
|
|
140
|
+
const dashboardUrl = apiUrl;
|
|
141
|
+
|
|
142
|
+
console.log();
|
|
143
|
+
printInfo("Opening", dashboardUrl);
|
|
144
|
+
console.log();
|
|
145
|
+
|
|
146
|
+
// Cross-platform browser open
|
|
147
|
+
const openCmd =
|
|
148
|
+
process.platform === "win32" ? ["cmd", ["/c", "start", dashboardUrl]] :
|
|
149
|
+
process.platform === "darwin" ? ["open", [dashboardUrl]] :
|
|
150
|
+
["xdg-open", [dashboardUrl]];
|
|
151
|
+
|
|
152
|
+
spawn(openCmd[0] as string, openCmd[1] as string[], {
|
|
153
|
+
stdio: "ignore",
|
|
154
|
+
detached: true,
|
|
155
|
+
}).unref();
|
|
156
|
+
|
|
157
|
+
printDone(`Dashboard opened at ${chalk.cyan(dashboardUrl)}`);
|
|
158
|
+
|
|
159
|
+
// ── 4. Show quick API reference ───────────────────────
|
|
160
|
+
console.log(chalk.gray(" Available endpoints:"));
|
|
161
|
+
console.log(chalk.cyan(` GET ${apiUrl}/health`));
|
|
162
|
+
console.log(chalk.cyan(` GET ${apiUrl}/api/reports`));
|
|
163
|
+
console.log(chalk.cyan(` GET ${apiUrl}/api/reports/:id`));
|
|
164
|
+
console.log(chalk.cyan(` GET ${apiUrl}/api/reports/project/:name`));
|
|
165
|
+
console.log(chalk.cyan(` POST ${apiUrl}/api/reports/upload`));
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.gray(" Tip: run ") +
|
|
169
|
+
chalk.cyan("react-doctor full ./ --upload") +
|
|
170
|
+
chalk.gray(" to add a new report.\n"),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
} catch (err: any) {
|
|
174
|
+
spin.fail(chalk.red("Dashboard failed to open"));
|
|
175
|
+
console.log(chalk.red(`\n ${err.message}\n`));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
package/cli/src/index.ts
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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.
|
|
3
|
+
// cli/src/index.ts — CLI entry point
|
|
26
4
|
// ─────────────────────────────────────────────────────────────
|
|
27
5
|
|
|
28
6
|
import { Command } from "commander";
|
|
29
|
-
import { registerAnalyzeCommand }
|
|
30
|
-
import { registerProfileCommand }
|
|
31
|
-
import { registerFullCommand }
|
|
32
|
-
import {
|
|
7
|
+
import { registerAnalyzeCommand } from "./commands/analyze";
|
|
8
|
+
import { registerProfileCommand } from "./commands/profile";
|
|
9
|
+
import { registerFullCommand } from "./commands/full";
|
|
10
|
+
import { registerDashboardCommand } from "./commands/dashboard";
|
|
33
11
|
|
|
34
12
|
const program = new Command();
|
|
35
13
|
|
|
@@ -37,46 +15,41 @@ const program = new Command();
|
|
|
37
15
|
program
|
|
38
16
|
.name("react-doctor")
|
|
39
17
|
.description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
|
|
40
|
-
.version("1.0.
|
|
18
|
+
.version("1.0.2");
|
|
41
19
|
|
|
42
|
-
// ── Register
|
|
43
|
-
//
|
|
44
|
-
//
|
|
20
|
+
// ── Register commands ─────────────────────────────────────────
|
|
21
|
+
registerFullCommand(program); // react-doctor full
|
|
22
|
+
registerAnalyzeCommand(program); // react-doctor analyze
|
|
23
|
+
registerProfileCommand(program); // react-doctor profile
|
|
24
|
+
registerDashboardCommand(program); // react-doctor dashboard
|
|
45
25
|
|
|
46
|
-
|
|
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 ─────────────────
|
|
26
|
+
// ── Usage examples ────────────────────────────────────────────
|
|
52
27
|
program.addHelpText("after", `
|
|
53
28
|
Examples:
|
|
54
|
-
$ react-doctor full ./my-app
|
|
55
|
-
$ react-doctor full ./my-app --mobile
|
|
56
|
-
$ react-doctor full ./my-app --desktop --mobile
|
|
57
|
-
$ react-doctor full ./my-app --cpu 4
|
|
58
|
-
$ react-doctor full ./my-app --throttle slow4g
|
|
59
|
-
$ react-doctor full ./my-app --throttle 3g
|
|
60
|
-
$ react-doctor full ./my-app --cpu 4 --throttle 3g
|
|
61
|
-
$ react-doctor full ./my-app --upload
|
|
62
|
-
|
|
63
|
-
$ react-doctor analyze ./my-app
|
|
64
|
-
$ react-doctor analyze ./my-app --full
|
|
65
|
-
|
|
66
|
-
$ react-doctor profile ./my-app
|
|
67
|
-
$ react-doctor profile ./my-app --mobile
|
|
68
|
-
$ react-doctor profile ./my-app --desktop --mobile
|
|
69
|
-
$ react-doctor profile ./my-app --cpu 4
|
|
70
|
-
$ react-doctor profile ./my-app --throttle slow4g
|
|
71
|
-
$ react-doctor profile ./my-app --throttle 3g
|
|
72
|
-
|
|
73
|
-
$ react-doctor
|
|
74
|
-
$ react-doctor
|
|
29
|
+
$ react-doctor full ./my-app Run full diagnostic (desktop)
|
|
30
|
+
$ react-doctor full ./my-app --mobile Include mobile viewport
|
|
31
|
+
$ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
|
|
32
|
+
$ react-doctor full ./my-app --cpu 4 Simulate slow Android device
|
|
33
|
+
$ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
|
|
34
|
+
$ react-doctor full ./my-app --throttle 3g Simulate 3G network
|
|
35
|
+
$ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
|
|
36
|
+
$ react-doctor full ./my-app --upload Run + save report to dashboard
|
|
37
|
+
|
|
38
|
+
$ react-doctor analyze ./my-app Static code analysis only
|
|
39
|
+
$ react-doctor analyze ./my-app --full Static + runtime + rules
|
|
40
|
+
|
|
41
|
+
$ react-doctor profile ./my-app Runtime profiling only (desktop)
|
|
42
|
+
$ react-doctor profile ./my-app --mobile Mobile viewport
|
|
43
|
+
$ react-doctor profile ./my-app --desktop --mobile Both devices
|
|
44
|
+
$ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
|
|
45
|
+
$ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
|
|
46
|
+
$ react-doctor profile ./my-app --throttle 3g Simulate 3G network
|
|
47
|
+
|
|
48
|
+
$ react-doctor dashboard Open dashboard (auto-starts backend)
|
|
49
|
+
$ react-doctor dashboard --port 4000 Use custom port
|
|
75
50
|
`);
|
|
76
51
|
|
|
77
52
|
// ── 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
53
|
if (process.argv.length < 3) {
|
|
81
54
|
program.help();
|
|
82
55
|
}
|
package/package.json
CHANGED