react-doctor 0.0.6 → 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 +75 -11
- 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";
|
|
@@ -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 })`",
|
|
@@ -1125,9 +1128,67 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
1125
1128
|
return selectedDirectories;
|
|
1126
1129
|
};
|
|
1127
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
|
+
|
|
1128
1189
|
//#endregion
|
|
1129
1190
|
//#region src/cli.ts
|
|
1130
|
-
const VERSION = "0.0.
|
|
1191
|
+
const VERSION = "0.0.7";
|
|
1131
1192
|
process.on("SIGINT", () => process.exit(0));
|
|
1132
1193
|
process.on("SIGTERM", () => process.exit(0));
|
|
1133
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) => {
|
|
@@ -1155,6 +1216,7 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1155
1216
|
if (!isScoreOnly) logger.break();
|
|
1156
1217
|
}
|
|
1157
1218
|
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
1219
|
+
if (!isScoreOnly) await maybePromptSkillInstall(shouldSkipPrompts);
|
|
1158
1220
|
} catch (error) {
|
|
1159
1221
|
handleError(error);
|
|
1160
1222
|
}
|
|
@@ -1164,9 +1226,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
1164
1226
|
`);
|
|
1165
1227
|
const AMI_INSTALL_URL = "https://ami.dev/install.sh";
|
|
1166
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.";
|
|
1167
|
-
const
|
|
1168
|
-
return `ami://new-chat?cwd=${encodeURIComponent(projectDirectory)}&prompt=${encodeURIComponent(AMI_FIX_PROMPT)}&mode=agent`;
|
|
1169
|
-
};
|
|
1229
|
+
const OPEN_PROJECT_DELAY_S = 2;
|
|
1170
1230
|
const isAmiInstalled = () => {
|
|
1171
1231
|
try {
|
|
1172
1232
|
execSync("ls /Applications/Ami.app", { stdio: "ignore" });
|
|
@@ -1190,14 +1250,18 @@ const openAmiToFix = (directory) => {
|
|
|
1190
1250
|
const resolvedDirectory = path.resolve(directory);
|
|
1191
1251
|
if (!isAmiInstalled()) installAmi();
|
|
1192
1252
|
logger.log("Opening Ami to fix react-doctor issues...");
|
|
1193
|
-
const
|
|
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`;
|
|
1194
1257
|
try {
|
|
1195
|
-
execSync(`open "${
|
|
1258
|
+
execSync(`open "${openProjectDeeplink}" && sleep ${OPEN_PROJECT_DELAY_S} && open "${newChatDeeplink}"`, { stdio: "ignore" });
|
|
1196
1259
|
logger.success("Opened Ami with react-doctor fix prompt.");
|
|
1197
1260
|
} catch {
|
|
1198
1261
|
logger.break();
|
|
1199
|
-
logger.dim("Could not open Ami automatically. Open
|
|
1200
|
-
logger.info(
|
|
1262
|
+
logger.dim("Could not open Ami automatically. Open these URLs manually:");
|
|
1263
|
+
logger.info(openProjectDeeplink);
|
|
1264
|
+
logger.info(newChatDeeplink);
|
|
1201
1265
|
}
|
|
1202
1266
|
};
|
|
1203
1267
|
const fixAction = (directory) => {
|