react-doctor 0.0.6 → 0.0.8
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 +112 -10
- package/dist/cli.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +104 -0
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import path, { join } from "node:path";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import fs, { mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
import os, { tmpdir } from "node:os";
|
|
8
|
+
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import os, { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { performance } from "node:perf_hooks";
|
|
11
11
|
import { main } from "knip";
|
|
12
12
|
import { createOptions } from "knip/session";
|
|
@@ -73,6 +73,7 @@ const SCORE_GOOD_THRESHOLD = 75;
|
|
|
73
73
|
const SCORE_OK_THRESHOLD = 50;
|
|
74
74
|
const SCORE_BAR_WIDTH_CHARS = 50;
|
|
75
75
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
76
|
+
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
76
77
|
const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
|
|
77
78
|
|
|
78
79
|
//#endregion
|
|
@@ -493,7 +494,8 @@ const NEXTJS_RULES = {
|
|
|
493
494
|
"react-doctor/nextjs-no-font-link": "warn",
|
|
494
495
|
"react-doctor/nextjs-no-css-link": "warn",
|
|
495
496
|
"react-doctor/nextjs-no-polyfill-script": "warn",
|
|
496
|
-
"react-doctor/nextjs-no-head-import": "error"
|
|
497
|
+
"react-doctor/nextjs-no-head-import": "error",
|
|
498
|
+
"react-doctor/nextjs-no-side-effect-in-get-handler": "error"
|
|
497
499
|
};
|
|
498
500
|
const REACT_COMPILER_RULES = {
|
|
499
501
|
"react-hooks-js/set-state-in-render": "error",
|
|
@@ -670,6 +672,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
670
672
|
"react-doctor/nextjs-no-css-link": "Next.js",
|
|
671
673
|
"react-doctor/nextjs-no-polyfill-script": "Next.js",
|
|
672
674
|
"react-doctor/nextjs-no-head-import": "Next.js",
|
|
675
|
+
"react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
|
|
673
676
|
"react-doctor/server-auth-actions": "Server",
|
|
674
677
|
"react-doctor/server-after-nonblocking": "Server",
|
|
675
678
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
@@ -725,6 +728,7 @@ const RULE_HELP_MAP = {
|
|
|
725
728
|
"nextjs-no-css-link": "Import CSS directly: `import './styles.css'` or use CSS Modules: `import styles from './Button.module.css'`",
|
|
726
729
|
"nextjs-no-polyfill-script": "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
727
730
|
"nextjs-no-head-import": "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
731
|
+
"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
732
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
729
733
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
730
734
|
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
|
|
@@ -971,6 +975,17 @@ const printBranding = (score) => {
|
|
|
971
975
|
logger.log(` React Doctor ${highlighter.dim("(www.react.doctor)")}`);
|
|
972
976
|
logger.break();
|
|
973
977
|
};
|
|
978
|
+
const buildShareUrl = (diagnostics, scoreResult) => {
|
|
979
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
980
|
+
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
981
|
+
const affectedFileCount = collectAffectedFiles(diagnostics).size;
|
|
982
|
+
const params = new URLSearchParams();
|
|
983
|
+
if (scoreResult) params.set("s", String(scoreResult.score));
|
|
984
|
+
if (errorCount > 0) params.set("e", String(errorCount));
|
|
985
|
+
if (warningCount > 0) params.set("w", String(warningCount));
|
|
986
|
+
if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
|
|
987
|
+
return `${SHARE_BASE_URL}?${params.toString()}`;
|
|
988
|
+
};
|
|
974
989
|
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
|
|
975
990
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
976
991
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
@@ -997,6 +1012,9 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
|
|
|
997
1012
|
} catch {
|
|
998
1013
|
logger.break();
|
|
999
1014
|
}
|
|
1015
|
+
const shareUrl = buildShareUrl(diagnostics, scoreResult);
|
|
1016
|
+
logger.break();
|
|
1017
|
+
logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
|
|
1000
1018
|
};
|
|
1001
1019
|
const scan = async (directory, options) => {
|
|
1002
1020
|
const startTime = performance.now();
|
|
@@ -1125,9 +1143,68 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
1125
1143
|
return selectedDirectories;
|
|
1126
1144
|
};
|
|
1127
1145
|
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/utils/skill-prompt.ts
|
|
1148
|
+
const CONFIG_DIRECTORY = join(homedir(), ".react-doctor");
|
|
1149
|
+
const CONFIG_FILE = join(CONFIG_DIRECTORY, "config.json");
|
|
1150
|
+
const SKILL_REPO = "aidenybai/react-doctor";
|
|
1151
|
+
const readConfig = () => {
|
|
1152
|
+
try {
|
|
1153
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
1154
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
1155
|
+
} catch {
|
|
1156
|
+
return {};
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
const writeConfig = (config) => {
|
|
1160
|
+
try {
|
|
1161
|
+
if (!existsSync(CONFIG_DIRECTORY)) mkdirSync(CONFIG_DIRECTORY, { recursive: true });
|
|
1162
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
1163
|
+
} catch {}
|
|
1164
|
+
};
|
|
1165
|
+
const installSkill = () => {
|
|
1166
|
+
try {
|
|
1167
|
+
execSync(`npx -y skills add ${SKILL_REPO}`, { stdio: "inherit" });
|
|
1168
|
+
return true;
|
|
1169
|
+
} catch {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
1174
|
+
const config = readConfig();
|
|
1175
|
+
if (config.skillPromptDismissed) return;
|
|
1176
|
+
if (shouldSkipPrompts) return;
|
|
1177
|
+
logger.break();
|
|
1178
|
+
logger.log(`${highlighter.info("💡")} Have your coding agent fix these issues automatically?`);
|
|
1179
|
+
logger.dim(` Install the ${highlighter.info("react-doctor")} skill to teach Cursor, Claude Code, Copilot,`);
|
|
1180
|
+
logger.dim(" Ami, and other AI agents how to diagnose and fix these React issues.");
|
|
1181
|
+
logger.break();
|
|
1182
|
+
const { shouldInstall } = await prompts({
|
|
1183
|
+
type: "confirm",
|
|
1184
|
+
name: "shouldInstall",
|
|
1185
|
+
message: "Install skill?",
|
|
1186
|
+
initial: true
|
|
1187
|
+
});
|
|
1188
|
+
if (shouldInstall) {
|
|
1189
|
+
logger.break();
|
|
1190
|
+
if (installSkill()) {
|
|
1191
|
+
logger.break();
|
|
1192
|
+
logger.success("Skill installed!");
|
|
1193
|
+
} else {
|
|
1194
|
+
logger.break();
|
|
1195
|
+
logger.dim("Skill install failed. You can install manually:");
|
|
1196
|
+
logger.dim(` npx skills add ${SKILL_REPO}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
writeConfig({
|
|
1200
|
+
...config,
|
|
1201
|
+
skillPromptDismissed: true
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1128
1205
|
//#endregion
|
|
1129
1206
|
//#region src/cli.ts
|
|
1130
|
-
const VERSION = "0.0.
|
|
1207
|
+
const VERSION = "0.0.8";
|
|
1131
1208
|
process.on("SIGINT", () => process.exit(0));
|
|
1132
1209
|
process.on("SIGTERM", () => process.exit(0));
|
|
1133
1210
|
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) => {
|
|
@@ -1144,7 +1221,17 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1144
1221
|
verbose: Boolean(flags.verbose),
|
|
1145
1222
|
scoreOnly: isScoreOnly
|
|
1146
1223
|
};
|
|
1147
|
-
const
|
|
1224
|
+
const isAutomatedEnvironment = [
|
|
1225
|
+
process.env.CI,
|
|
1226
|
+
process.env.CLAUDECODE,
|
|
1227
|
+
process.env.CURSOR_TRACE_ID,
|
|
1228
|
+
process.env.CURSOR_AGENT,
|
|
1229
|
+
process.env.CODEX_CI,
|
|
1230
|
+
process.env.OPENCODE,
|
|
1231
|
+
process.env.AMP_HOME,
|
|
1232
|
+
process.env.AMI
|
|
1233
|
+
].some(Boolean);
|
|
1234
|
+
const shouldSkipPrompts = flags.yes || isAutomatedEnvironment || !process.stdin.isTTY;
|
|
1148
1235
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, shouldSkipPrompts);
|
|
1149
1236
|
for (const projectDirectory of projectDirectories) {
|
|
1150
1237
|
if (!isScoreOnly) {
|
|
@@ -1155,6 +1242,10 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1155
1242
|
if (!isScoreOnly) logger.break();
|
|
1156
1243
|
}
|
|
1157
1244
|
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
1245
|
+
if (!isScoreOnly) {
|
|
1246
|
+
await maybePromptSkillInstall(shouldSkipPrompts);
|
|
1247
|
+
if (!shouldSkipPrompts && !flags.fix) await maybePromptAmiFix(resolvedDirectory);
|
|
1248
|
+
}
|
|
1158
1249
|
} catch (error) {
|
|
1159
1250
|
handleError(error);
|
|
1160
1251
|
}
|
|
@@ -1164,9 +1255,6 @@ ${highlighter.dim("Learn more:")}
|
|
|
1164
1255
|
`);
|
|
1165
1256
|
const AMI_INSTALL_URL = "https://ami.dev/install.sh";
|
|
1166
1257
|
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.";
|
|
1167
|
-
const buildAmiDeeplink = (projectDirectory) => {
|
|
1168
|
-
return `ami://new-chat?cwd=${encodeURIComponent(projectDirectory)}&prompt=${encodeURIComponent(AMI_FIX_PROMPT)}&mode=agent`;
|
|
1169
|
-
};
|
|
1170
1258
|
const isAmiInstalled = () => {
|
|
1171
1259
|
try {
|
|
1172
1260
|
execSync("ls /Applications/Ami.app", { stdio: "ignore" });
|
|
@@ -1190,7 +1278,7 @@ const openAmiToFix = (directory) => {
|
|
|
1190
1278
|
const resolvedDirectory = path.resolve(directory);
|
|
1191
1279
|
if (!isAmiInstalled()) installAmi();
|
|
1192
1280
|
logger.log("Opening Ami to fix react-doctor issues...");
|
|
1193
|
-
const deeplink =
|
|
1281
|
+
const deeplink = `ami://open-project?cwd=${encodeURIComponent(resolvedDirectory)}&prompt=${encodeURIComponent(AMI_FIX_PROMPT)}&mode=agent`;
|
|
1194
1282
|
try {
|
|
1195
1283
|
execSync(`open "${deeplink}"`, { stdio: "ignore" });
|
|
1196
1284
|
logger.success("Opened Ami with react-doctor fix prompt.");
|
|
@@ -1200,6 +1288,20 @@ const openAmiToFix = (directory) => {
|
|
|
1200
1288
|
logger.info(deeplink);
|
|
1201
1289
|
}
|
|
1202
1290
|
};
|
|
1291
|
+
const maybePromptAmiFix = async (directory) => {
|
|
1292
|
+
logger.break();
|
|
1293
|
+
logger.log(`Fix these issues with ${highlighter.info("Ami")}?`);
|
|
1294
|
+
logger.dim(" Ami is a coding agent built to understand your codebase and fix issues");
|
|
1295
|
+
logger.dim(` automatically. Learn more at ${highlighter.info("https://ami.dev")}`);
|
|
1296
|
+
logger.break();
|
|
1297
|
+
const { shouldFix } = await prompts({
|
|
1298
|
+
type: "confirm",
|
|
1299
|
+
name: "shouldFix",
|
|
1300
|
+
message: "Open Ami to fix?",
|
|
1301
|
+
initial: true
|
|
1302
|
+
});
|
|
1303
|
+
if (shouldFix) openAmiToFix(directory);
|
|
1304
|
+
};
|
|
1203
1305
|
const fixAction = (directory) => {
|
|
1204
1306
|
try {
|
|
1205
1307
|
openAmiToFix(directory);
|
|
@@ -1208,7 +1310,7 @@ const fixAction = (directory) => {
|
|
|
1208
1310
|
}
|
|
1209
1311
|
};
|
|
1210
1312
|
const fixCommand = new Command("fix").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1211
|
-
const installAmiCommand = new Command("install-ami").description("
|
|
1313
|
+
const installAmiCommand = new Command("install-ami").description("Install Ami and open it to auto-fix issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1212
1314
|
program.addCommand(fixCommand);
|
|
1213
1315
|
program.addCommand(installAmiCommand);
|
|
1214
1316
|
const main$1 = async () => {
|