react-doctor 0.0.5 → 0.0.7
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/dist/cli.js +146 -12
- package/dist/cli.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +123 -6
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import path, { join } from "node:path";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
import pc from "picocolors";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import fs, { mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import os, { tmpdir } from "node:os";
|
|
8
|
+
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import os, { homedir, tmpdir } from "node:os";
|
|
9
10
|
import { performance } from "node:perf_hooks";
|
|
10
|
-
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { main } from "knip";
|
|
12
12
|
import { createOptions } from "knip/session";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -493,7 +493,8 @@ const NEXTJS_RULES = {
|
|
|
493
493
|
"react-doctor/nextjs-no-font-link": "warn",
|
|
494
494
|
"react-doctor/nextjs-no-css-link": "warn",
|
|
495
495
|
"react-doctor/nextjs-no-polyfill-script": "warn",
|
|
496
|
-
"react-doctor/nextjs-no-head-import": "error"
|
|
496
|
+
"react-doctor/nextjs-no-head-import": "error",
|
|
497
|
+
"react-doctor/nextjs-no-side-effect-in-get-handler": "error"
|
|
497
498
|
};
|
|
498
499
|
const REACT_COMPILER_RULES = {
|
|
499
500
|
"react-hooks-js/set-state-in-render": "error",
|
|
@@ -670,6 +671,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
670
671
|
"react-doctor/nextjs-no-css-link": "Next.js",
|
|
671
672
|
"react-doctor/nextjs-no-polyfill-script": "Next.js",
|
|
672
673
|
"react-doctor/nextjs-no-head-import": "Next.js",
|
|
674
|
+
"react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
|
|
673
675
|
"react-doctor/server-auth-actions": "Server",
|
|
674
676
|
"react-doctor/server-after-nonblocking": "Server",
|
|
675
677
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
@@ -725,6 +727,7 @@ const RULE_HELP_MAP = {
|
|
|
725
727
|
"nextjs-no-css-link": "Import CSS directly: `import './styles.css'` or use CSS Modules: `import styles from './Button.module.css'`",
|
|
726
728
|
"nextjs-no-polyfill-script": "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
727
729
|
"nextjs-no-head-import": "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
730
|
+
"nextjs-no-side-effect-in-get-handler": "Move the side effect to a POST handler and use a <form> or fetch with method POST — GET requests can be triggered by prefetching and are vulnerable to CSRF",
|
|
728
731
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
729
732
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
730
733
|
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
|
|
@@ -954,6 +957,23 @@ const printScoreGauge = (score, label) => {
|
|
|
954
957
|
logger.log(` ${buildScoreBar(score)}`);
|
|
955
958
|
logger.break();
|
|
956
959
|
};
|
|
960
|
+
const getDoctorFace = (score) => {
|
|
961
|
+
if (score >= SCORE_GOOD_THRESHOLD) return ["◠ ◠", " ▽ "];
|
|
962
|
+
if (score >= SCORE_OK_THRESHOLD) return ["• •", " ─ "];
|
|
963
|
+
return ["x x", " ▽ "];
|
|
964
|
+
};
|
|
965
|
+
const printBranding = (score) => {
|
|
966
|
+
if (score !== void 0) {
|
|
967
|
+
const [eyes, mouth] = getDoctorFace(score);
|
|
968
|
+
const colorize = (text) => colorizeByScore(text, score);
|
|
969
|
+
logger.log(colorize(" ┌─────┐"));
|
|
970
|
+
logger.log(colorize(` │ ${eyes} │`));
|
|
971
|
+
logger.log(colorize(` │ ${mouth} │`));
|
|
972
|
+
logger.log(colorize(" └─────┘"));
|
|
973
|
+
}
|
|
974
|
+
logger.log(` React Doctor ${highlighter.dim("(www.react.doctor)")}`);
|
|
975
|
+
logger.break();
|
|
976
|
+
};
|
|
957
977
|
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
|
|
958
978
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
959
979
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
@@ -961,6 +981,7 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
|
|
|
961
981
|
const elapsed = formatElapsedTime(elapsedMilliseconds);
|
|
962
982
|
logger.log("─".repeat(SEPARATOR_LENGTH_CHARS));
|
|
963
983
|
logger.break();
|
|
984
|
+
printBranding(scoreResult?.score);
|
|
964
985
|
if (scoreResult) printScoreGauge(scoreResult.score, scoreResult.label);
|
|
965
986
|
else {
|
|
966
987
|
logger.dim(` ${OFFLINE_MESSAGE}`);
|
|
@@ -1035,8 +1056,10 @@ const scan = async (directory, options) => {
|
|
|
1035
1056
|
if (diagnostics.length === 0) {
|
|
1036
1057
|
logger.success("No issues found!");
|
|
1037
1058
|
logger.break();
|
|
1038
|
-
if (scoreResult)
|
|
1039
|
-
|
|
1059
|
+
if (scoreResult) {
|
|
1060
|
+
printBranding(scoreResult.score);
|
|
1061
|
+
printScoreGauge(scoreResult.score, scoreResult.label);
|
|
1062
|
+
} else logger.dim(` ${OFFLINE_MESSAGE}`);
|
|
1040
1063
|
return;
|
|
1041
1064
|
}
|
|
1042
1065
|
printDiagnostics(diagnostics, options.verbose);
|
|
@@ -1058,8 +1081,8 @@ const prompts = (questions) => {
|
|
|
1058
1081
|
//#endregion
|
|
1059
1082
|
//#region src/utils/select-projects.ts
|
|
1060
1083
|
const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
1061
|
-
|
|
1062
|
-
if (packages.length === 0) packages
|
|
1084
|
+
let packages = listWorkspacePackages(rootDirectory);
|
|
1085
|
+
if (packages.length === 0) packages = discoverReactSubprojects(rootDirectory);
|
|
1063
1086
|
if (packages.length === 0) return [rootDirectory];
|
|
1064
1087
|
if (projectFlag) return resolveProjectFlag(projectFlag, packages);
|
|
1065
1088
|
if (skipPrompts) {
|
|
@@ -1087,7 +1110,7 @@ const printDiscoveredProjects = (packages) => {
|
|
|
1087
1110
|
for (const workspacePackage of packages) logger.log(` ${highlighter.dim("─")} ${workspacePackage.directory}`);
|
|
1088
1111
|
logger.break();
|
|
1089
1112
|
logger.dim(`Run with a specific path to scan a project:`);
|
|
1090
|
-
logger.dim(` npx react-doctor@latest <path>`);
|
|
1113
|
+
logger.dim(` npx -y react-doctor@latest <path>`);
|
|
1091
1114
|
logger.break();
|
|
1092
1115
|
};
|
|
1093
1116
|
const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
@@ -1105,12 +1128,70 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
1105
1128
|
return selectedDirectories;
|
|
1106
1129
|
};
|
|
1107
1130
|
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/utils/skill-prompt.ts
|
|
1133
|
+
const CONFIG_DIRECTORY = join(homedir(), ".react-doctor");
|
|
1134
|
+
const CONFIG_FILE = join(CONFIG_DIRECTORY, "config.json");
|
|
1135
|
+
const SKILL_REPO = "aidenybai/react-doctor";
|
|
1136
|
+
const readConfig = () => {
|
|
1137
|
+
try {
|
|
1138
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
1139
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
1140
|
+
} catch {
|
|
1141
|
+
return {};
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
const writeConfig = (config) => {
|
|
1145
|
+
try {
|
|
1146
|
+
if (!existsSync(CONFIG_DIRECTORY)) mkdirSync(CONFIG_DIRECTORY, { recursive: true });
|
|
1147
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
1148
|
+
} catch {}
|
|
1149
|
+
};
|
|
1150
|
+
const installSkill = () => {
|
|
1151
|
+
try {
|
|
1152
|
+
execSync(`npx -y skills add ${SKILL_REPO}`, { stdio: "inherit" });
|
|
1153
|
+
return true;
|
|
1154
|
+
} catch {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
1159
|
+
const config = readConfig();
|
|
1160
|
+
if (config.skillPromptDismissed) return;
|
|
1161
|
+
if (shouldSkipPrompts) return;
|
|
1162
|
+
logger.break();
|
|
1163
|
+
logger.log(`${highlighter.info("💡")} Install the ${highlighter.info("react-doctor")} skill for your coding agent?`);
|
|
1164
|
+
logger.dim(" Adds React best practices to Cursor, Claude Code, Copilot, and more.");
|
|
1165
|
+
logger.break();
|
|
1166
|
+
const { shouldInstall } = await prompts({
|
|
1167
|
+
type: "confirm",
|
|
1168
|
+
name: "shouldInstall",
|
|
1169
|
+
message: "Install skill?",
|
|
1170
|
+
initial: true
|
|
1171
|
+
});
|
|
1172
|
+
if (shouldInstall) {
|
|
1173
|
+
logger.break();
|
|
1174
|
+
if (installSkill()) {
|
|
1175
|
+
logger.break();
|
|
1176
|
+
logger.success("Skill installed!");
|
|
1177
|
+
} else {
|
|
1178
|
+
logger.break();
|
|
1179
|
+
logger.dim("Skill install failed. You can install manually:");
|
|
1180
|
+
logger.dim(` npx skills add ${SKILL_REPO}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
writeConfig({
|
|
1184
|
+
...config,
|
|
1185
|
+
skillPromptDismissed: true
|
|
1186
|
+
});
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1108
1189
|
//#endregion
|
|
1109
1190
|
//#region src/cli.ts
|
|
1110
|
-
const VERSION = "0.0.
|
|
1191
|
+
const VERSION = "0.0.7";
|
|
1111
1192
|
process.on("SIGINT", () => process.exit(0));
|
|
1112
1193
|
process.on("SIGTERM", () => process.exit(0));
|
|
1113
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--
|
|
1194
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
1114
1195
|
try {
|
|
1115
1196
|
const resolvedDirectory = path.resolve(directory);
|
|
1116
1197
|
const isScoreOnly = flags.score;
|
|
@@ -1121,7 +1202,7 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1121
1202
|
const scanOptions = {
|
|
1122
1203
|
lint: flags.lint,
|
|
1123
1204
|
deadCode: flags.deadCode,
|
|
1124
|
-
verbose: flags.verbose,
|
|
1205
|
+
verbose: Boolean(flags.verbose),
|
|
1125
1206
|
scoreOnly: isScoreOnly
|
|
1126
1207
|
};
|
|
1127
1208
|
const shouldSkipPrompts = flags.yes || Boolean(process.env.CI) || Boolean(process.env.CLAUDECODE) || Boolean(process.env.AMI) || !process.stdin.isTTY;
|
|
@@ -1134,6 +1215,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1134
1215
|
await scan(projectDirectory, scanOptions);
|
|
1135
1216
|
if (!isScoreOnly) logger.break();
|
|
1136
1217
|
}
|
|
1218
|
+
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
1219
|
+
if (!isScoreOnly) await maybePromptSkillInstall(shouldSkipPrompts);
|
|
1137
1220
|
} catch (error) {
|
|
1138
1221
|
handleError(error);
|
|
1139
1222
|
}
|
|
@@ -1141,6 +1224,57 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1141
1224
|
${highlighter.dim("Learn more:")}
|
|
1142
1225
|
${highlighter.info("https://github.com/aidenybai/react-doctor")}
|
|
1143
1226
|
`);
|
|
1227
|
+
const AMI_INSTALL_URL = "https://ami.dev/install.sh";
|
|
1228
|
+
const AMI_FIX_PROMPT = "Run npx -y react-doctor@latest . --verbose, read every diagnostic, then fix all issues one by one. After fixing, re-run react-doctor to verify the score improved.";
|
|
1229
|
+
const OPEN_PROJECT_DELAY_S = 2;
|
|
1230
|
+
const isAmiInstalled = () => {
|
|
1231
|
+
try {
|
|
1232
|
+
execSync("ls /Applications/Ami.app", { stdio: "ignore" });
|
|
1233
|
+
return true;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
const installAmi = () => {
|
|
1239
|
+
logger.log("Ami not found. Installing...");
|
|
1240
|
+
logger.break();
|
|
1241
|
+
try {
|
|
1242
|
+
execSync(`curl -fsSL ${AMI_INSTALL_URL} | bash`, { stdio: "inherit" });
|
|
1243
|
+
} catch {
|
|
1244
|
+
logger.error("Failed to install Ami. Visit https://ami.dev to install manually.");
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
logger.break();
|
|
1248
|
+
};
|
|
1249
|
+
const openAmiToFix = (directory) => {
|
|
1250
|
+
const resolvedDirectory = path.resolve(directory);
|
|
1251
|
+
if (!isAmiInstalled()) installAmi();
|
|
1252
|
+
logger.log("Opening Ami to fix react-doctor issues...");
|
|
1253
|
+
const encodedDirectory = encodeURIComponent(resolvedDirectory);
|
|
1254
|
+
const encodedPrompt = encodeURIComponent(AMI_FIX_PROMPT);
|
|
1255
|
+
const openProjectDeeplink = `ami://open-project?cwd=${encodedDirectory}`;
|
|
1256
|
+
const newChatDeeplink = `ami://new-chat?prompt=${encodedPrompt}&mode=agent&send=true`;
|
|
1257
|
+
try {
|
|
1258
|
+
execSync(`open "${openProjectDeeplink}" && sleep ${OPEN_PROJECT_DELAY_S} && open "${newChatDeeplink}"`, { stdio: "ignore" });
|
|
1259
|
+
logger.success("Opened Ami with react-doctor fix prompt.");
|
|
1260
|
+
} catch {
|
|
1261
|
+
logger.break();
|
|
1262
|
+
logger.dim("Could not open Ami automatically. Open these URLs manually:");
|
|
1263
|
+
logger.info(openProjectDeeplink);
|
|
1264
|
+
logger.info(newChatDeeplink);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
const fixAction = (directory) => {
|
|
1268
|
+
try {
|
|
1269
|
+
openAmiToFix(directory);
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
handleError(error);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
const fixCommand = new Command("fix").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1275
|
+
const installAmiCommand = new Command("install-ami").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1276
|
+
program.addCommand(fixCommand);
|
|
1277
|
+
program.addCommand(installAmiCommand);
|
|
1144
1278
|
const main$1 = async () => {
|
|
1145
1279
|
await program.parseAsync();
|
|
1146
1280
|
};
|