react-doctor 0.0.34 → 0.0.36
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/README.md +5 -3
- package/dist/cli.js +400 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +124 -8
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +1116 -17
- package/dist/react-doctor-plugin.js.map +1 -1
- package/dist/skills/react-doctor/SKILL.md +19 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,281 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import fs, { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import fs, { accessSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import os, { tmpdir } from "node:os";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import basePrompts from "prompts";
|
|
10
|
+
import ora from "ora";
|
|
7
11
|
import { randomUUID } from "node:crypto";
|
|
8
12
|
import { performance } from "node:perf_hooks";
|
|
9
13
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import pc from "picocolors";
|
|
11
|
-
import basePrompts from "prompts";
|
|
12
14
|
import { main } from "knip";
|
|
13
15
|
import { createOptions } from "knip/session";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import ora from "ora";
|
|
16
16
|
|
|
17
|
+
//#region src/utils/detect-agents.ts
|
|
18
|
+
const SUPPORTED_AGENTS = {
|
|
19
|
+
claude: {
|
|
20
|
+
binaries: ["claude"],
|
|
21
|
+
displayName: "Claude Code",
|
|
22
|
+
skillDir: ".claude/skills"
|
|
23
|
+
},
|
|
24
|
+
codex: {
|
|
25
|
+
binaries: ["codex"],
|
|
26
|
+
displayName: "Codex",
|
|
27
|
+
skillDir: ".codex/skills"
|
|
28
|
+
},
|
|
29
|
+
copilot: {
|
|
30
|
+
binaries: ["copilot"],
|
|
31
|
+
displayName: "GitHub Copilot",
|
|
32
|
+
skillDir: ".github/copilot/skills"
|
|
33
|
+
},
|
|
34
|
+
gemini: {
|
|
35
|
+
binaries: ["gemini"],
|
|
36
|
+
displayName: "Gemini CLI",
|
|
37
|
+
skillDir: ".gemini/skills"
|
|
38
|
+
},
|
|
39
|
+
cursor: {
|
|
40
|
+
binaries: ["cursor", "agent"],
|
|
41
|
+
displayName: "Cursor",
|
|
42
|
+
skillDir: ".cursor/skills"
|
|
43
|
+
},
|
|
44
|
+
opencode: {
|
|
45
|
+
binaries: ["opencode"],
|
|
46
|
+
displayName: "OpenCode",
|
|
47
|
+
skillDir: ".opencode/skills"
|
|
48
|
+
},
|
|
49
|
+
droid: {
|
|
50
|
+
binaries: ["droid"],
|
|
51
|
+
displayName: "Factory Droid",
|
|
52
|
+
skillDir: ".droid/skills"
|
|
53
|
+
},
|
|
54
|
+
pi: {
|
|
55
|
+
binaries: ["pi", "omegon"],
|
|
56
|
+
displayName: "Pi",
|
|
57
|
+
skillDir: ".pi/skills"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const ALL_SUPPORTED_AGENTS = Object.keys(SUPPORTED_AGENTS);
|
|
61
|
+
const isCommandAvailable = (command) => {
|
|
62
|
+
const pathDirectories = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
63
|
+
for (const directory of pathDirectories) {
|
|
64
|
+
const binaryPath = path.join(directory, command);
|
|
65
|
+
try {
|
|
66
|
+
if (statSync(binaryPath).isFile()) {
|
|
67
|
+
accessSync(binaryPath, constants.X_OK);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
const detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
|
|
75
|
+
const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
|
|
76
|
+
const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/utils/highlighter.ts
|
|
80
|
+
const highlighter = {
|
|
81
|
+
error: pc.red,
|
|
82
|
+
warn: pc.yellow,
|
|
83
|
+
info: pc.cyan,
|
|
84
|
+
success: pc.green,
|
|
85
|
+
dim: pc.dim
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/utils/install-skill-for-agent.ts
|
|
90
|
+
const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName) => {
|
|
91
|
+
const installedSkillDirectory = path.join(projectRoot, toSkillDir(agent), skillName);
|
|
92
|
+
rmSync(installedSkillDirectory, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
mkdirSync(path.dirname(installedSkillDirectory), { recursive: true });
|
|
97
|
+
cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
|
|
98
|
+
return installedSkillDirectory;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/utils/logger.ts
|
|
103
|
+
const logger = {
|
|
104
|
+
error(...args) {
|
|
105
|
+
console.log(highlighter.error(args.join(" ")));
|
|
106
|
+
},
|
|
107
|
+
warn(...args) {
|
|
108
|
+
console.log(highlighter.warn(args.join(" ")));
|
|
109
|
+
},
|
|
110
|
+
info(...args) {
|
|
111
|
+
console.log(highlighter.info(args.join(" ")));
|
|
112
|
+
},
|
|
113
|
+
success(...args) {
|
|
114
|
+
console.log(highlighter.success(args.join(" ")));
|
|
115
|
+
},
|
|
116
|
+
dim(...args) {
|
|
117
|
+
console.log(highlighter.dim(args.join(" ")));
|
|
118
|
+
},
|
|
119
|
+
log(...args) {
|
|
120
|
+
console.log(args.join(" "));
|
|
121
|
+
},
|
|
122
|
+
break() {
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/utils/should-auto-select-current-choice.ts
|
|
129
|
+
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
130
|
+
if (choiceStates.some((choiceState) => choiceState.selected)) return false;
|
|
131
|
+
const currentChoice = choiceStates[cursor];
|
|
132
|
+
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/utils/should-select-all-choices.ts
|
|
137
|
+
const shouldSelectAllChoices = (choiceStates) => {
|
|
138
|
+
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/utils/prompts.ts
|
|
143
|
+
const require = createRequire(import.meta.url);
|
|
144
|
+
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
145
|
+
const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
|
|
146
|
+
let didPatchMultiselectToggleAll = false;
|
|
147
|
+
let didPatchMultiselectSubmit = false;
|
|
148
|
+
let didPatchSelectBanner = false;
|
|
149
|
+
const selectBannerMap = /* @__PURE__ */ new Map();
|
|
150
|
+
const onCancel = () => {
|
|
151
|
+
logger.break();
|
|
152
|
+
logger.log("Cancelled.");
|
|
153
|
+
logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
|
|
154
|
+
logger.break();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
};
|
|
157
|
+
const patchMultiselectToggleAll = () => {
|
|
158
|
+
if (didPatchMultiselectToggleAll) return;
|
|
159
|
+
didPatchMultiselectToggleAll = true;
|
|
160
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
161
|
+
multiselectPromptConstructor.prototype.toggleAll = function() {
|
|
162
|
+
const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
|
|
163
|
+
if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
|
|
164
|
+
this.bell();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
|
|
168
|
+
for (const choiceState of this.value) {
|
|
169
|
+
if (choiceState.disabled) continue;
|
|
170
|
+
choiceState.selected = shouldSelectAllEnabledChoices;
|
|
171
|
+
}
|
|
172
|
+
this.render();
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
const patchMultiselectSubmit = () => {
|
|
176
|
+
if (didPatchMultiselectSubmit) return;
|
|
177
|
+
didPatchMultiselectSubmit = true;
|
|
178
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
179
|
+
const originalSubmit = multiselectPromptConstructor.prototype.submit;
|
|
180
|
+
multiselectPromptConstructor.prototype.submit = function() {
|
|
181
|
+
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
|
|
182
|
+
originalSubmit.call(this);
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
const patchSelectBanner = () => {
|
|
186
|
+
if (didPatchSelectBanner) return;
|
|
187
|
+
didPatchSelectBanner = true;
|
|
188
|
+
const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
|
|
189
|
+
const promptsClear = require("prompts/lib/util/clear");
|
|
190
|
+
const originalRender = selectConstructor.prototype.render;
|
|
191
|
+
selectConstructor.prototype.render = function() {
|
|
192
|
+
originalRender.call(this);
|
|
193
|
+
const banner = selectBannerMap.get(this.cursor);
|
|
194
|
+
if (!banner || this.closed || this.done) return;
|
|
195
|
+
this.out.write(promptsClear(this.outputText, this.out.columns));
|
|
196
|
+
this.outputText = `${banner}\n\n${this.outputText}`;
|
|
197
|
+
this.out.write(this.outputText);
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const prompts = (questions) => {
|
|
201
|
+
patchMultiselectToggleAll();
|
|
202
|
+
patchMultiselectSubmit();
|
|
203
|
+
patchSelectBanner();
|
|
204
|
+
return basePrompts(questions, { onCancel });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/utils/spinner.ts
|
|
209
|
+
let sharedInstance = null;
|
|
210
|
+
let activeCount = 0;
|
|
211
|
+
const pendingTexts = /* @__PURE__ */ new Set();
|
|
212
|
+
const finalize = (method, originalText, displayText) => {
|
|
213
|
+
pendingTexts.delete(originalText);
|
|
214
|
+
activeCount--;
|
|
215
|
+
if (activeCount <= 0 || !sharedInstance) {
|
|
216
|
+
sharedInstance?.[method](displayText);
|
|
217
|
+
sharedInstance = null;
|
|
218
|
+
activeCount = 0;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
sharedInstance.stop();
|
|
222
|
+
ora(displayText).start()[method](displayText);
|
|
223
|
+
const [remainingText] = pendingTexts;
|
|
224
|
+
if (remainingText) sharedInstance.text = remainingText;
|
|
225
|
+
sharedInstance.start();
|
|
226
|
+
};
|
|
227
|
+
const spinner = (text) => ({ start() {
|
|
228
|
+
activeCount++;
|
|
229
|
+
pendingTexts.add(text);
|
|
230
|
+
if (!sharedInstance) sharedInstance = ora({ text }).start();
|
|
231
|
+
else sharedInstance.text = text;
|
|
232
|
+
return {
|
|
233
|
+
succeed: (displayText) => finalize("succeed", text, displayText),
|
|
234
|
+
fail: (displayText) => finalize("fail", text, displayText)
|
|
235
|
+
};
|
|
236
|
+
} });
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/install-skill.ts
|
|
240
|
+
const SKILL_NAME = "react-doctor";
|
|
241
|
+
const getSkillSourceDirectory = () => {
|
|
242
|
+
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
243
|
+
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
244
|
+
};
|
|
245
|
+
const runInstallSkill = async (options = {}) => {
|
|
246
|
+
const projectRoot = process.cwd();
|
|
247
|
+
const sourceDir = getSkillSourceDirectory();
|
|
248
|
+
if (!existsSync(path.join(sourceDir, "SKILL.md"))) {
|
|
249
|
+
logger.error(`Could not locate the ${SKILL_NAME} skill bundled with this package.`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const detectedAgents = detectAvailableAgents();
|
|
254
|
+
if (detectedAgents.length === 0) {
|
|
255
|
+
logger.error("No supported coding agents detected on your PATH.");
|
|
256
|
+
logger.dim(" Supported: Claude Code, Codex, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Factory Droid, Pi.");
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const selectedAgents = Boolean(options.yes) || !process.stdin.isTTY ? detectedAgents : (await prompts({
|
|
261
|
+
type: "multiselect",
|
|
262
|
+
name: "agents",
|
|
263
|
+
message: `Install the ${highlighter.info(SKILL_NAME)} skill for:`,
|
|
264
|
+
choices: detectedAgents.map((agent) => ({
|
|
265
|
+
title: toDisplayName(agent),
|
|
266
|
+
value: agent,
|
|
267
|
+
selected: true
|
|
268
|
+
})),
|
|
269
|
+
instructions: false,
|
|
270
|
+
min: 1
|
|
271
|
+
})).agents ?? [];
|
|
272
|
+
if (selectedAgents.length === 0) return;
|
|
273
|
+
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
274
|
+
for (const agent of selectedAgents) installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME);
|
|
275
|
+
installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
17
279
|
//#region src/constants.ts
|
|
18
280
|
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
19
281
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
@@ -140,16 +402,6 @@ const calculateScore = async (diagnostics) => {
|
|
|
140
402
|
}
|
|
141
403
|
};
|
|
142
404
|
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/utils/highlighter.ts
|
|
145
|
-
const highlighter = {
|
|
146
|
-
error: pc.red,
|
|
147
|
-
warn: pc.yellow,
|
|
148
|
-
info: pc.cyan,
|
|
149
|
-
success: pc.green,
|
|
150
|
-
dim: pc.dim
|
|
151
|
-
};
|
|
152
|
-
|
|
153
405
|
//#endregion
|
|
154
406
|
//#region src/utils/colorize-by-score.ts
|
|
155
407
|
const colorizeByScore = (text, score) => {
|
|
@@ -414,9 +666,11 @@ const EXPO_APP_CONFIG_FILENAMES = [
|
|
|
414
666
|
"app.config.js",
|
|
415
667
|
"app.config.ts"
|
|
416
668
|
];
|
|
417
|
-
const
|
|
669
|
+
const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
|
|
670
|
+
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*true\b/;
|
|
418
671
|
const FRAMEWORK_PACKAGES = {
|
|
419
672
|
next: "nextjs",
|
|
673
|
+
"@tanstack/react-start": "tanstack-start",
|
|
420
674
|
vite: "vite",
|
|
421
675
|
"react-scripts": "cra",
|
|
422
676
|
"@remix-run/react": "remix",
|
|
@@ -426,6 +680,7 @@ const FRAMEWORK_PACKAGES = {
|
|
|
426
680
|
};
|
|
427
681
|
const FRAMEWORK_DISPLAY_NAMES = {
|
|
428
682
|
nextjs: "Next.js",
|
|
683
|
+
"tanstack-start": "TanStack Start",
|
|
429
684
|
vite: "Vite",
|
|
430
685
|
cra: "Create React App",
|
|
431
686
|
remix: "Remix",
|
|
@@ -740,12 +995,12 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
740
995
|
const allDependencies = collectAllDependencies(packageJson);
|
|
741
996
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
742
997
|
};
|
|
743
|
-
const
|
|
998
|
+
const hasCompilerInConfigFile = (filePath) => {
|
|
744
999
|
if (!isFile(filePath)) return false;
|
|
745
1000
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
746
|
-
return
|
|
1001
|
+
return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
|
|
747
1002
|
};
|
|
748
|
-
const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) =>
|
|
1003
|
+
const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(path.join(directory, filename)));
|
|
749
1004
|
const detectReactCompiler = (directory, packageJson) => {
|
|
750
1005
|
if (hasCompilerPackage(packageJson)) return true;
|
|
751
1006
|
if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
|
|
@@ -800,32 +1055,6 @@ const discoverProject = (directory) => {
|
|
|
800
1055
|
};
|
|
801
1056
|
};
|
|
802
1057
|
|
|
803
|
-
//#endregion
|
|
804
|
-
//#region src/utils/logger.ts
|
|
805
|
-
const logger = {
|
|
806
|
-
error(...args) {
|
|
807
|
-
console.log(highlighter.error(args.join(" ")));
|
|
808
|
-
},
|
|
809
|
-
warn(...args) {
|
|
810
|
-
console.log(highlighter.warn(args.join(" ")));
|
|
811
|
-
},
|
|
812
|
-
info(...args) {
|
|
813
|
-
console.log(highlighter.info(args.join(" ")));
|
|
814
|
-
},
|
|
815
|
-
success(...args) {
|
|
816
|
-
console.log(highlighter.success(args.join(" ")));
|
|
817
|
-
},
|
|
818
|
-
dim(...args) {
|
|
819
|
-
console.log(highlighter.dim(args.join(" ")));
|
|
820
|
-
},
|
|
821
|
-
log(...args) {
|
|
822
|
-
console.log(args.join(" "));
|
|
823
|
-
},
|
|
824
|
-
break() {
|
|
825
|
-
console.log("");
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
|
|
829
1058
|
//#endregion
|
|
830
1059
|
//#region src/utils/framed-box.ts
|
|
831
1060
|
const createFramedLine = (plainText, renderedText = plainText) => ({
|
|
@@ -906,86 +1135,6 @@ const loadConfig = (rootDirectory) => {
|
|
|
906
1135
|
return null;
|
|
907
1136
|
};
|
|
908
1137
|
|
|
909
|
-
//#endregion
|
|
910
|
-
//#region src/utils/should-auto-select-current-choice.ts
|
|
911
|
-
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
912
|
-
if (choiceStates.some((choiceState) => choiceState.selected)) return false;
|
|
913
|
-
const currentChoice = choiceStates[cursor];
|
|
914
|
-
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
915
|
-
};
|
|
916
|
-
|
|
917
|
-
//#endregion
|
|
918
|
-
//#region src/utils/should-select-all-choices.ts
|
|
919
|
-
const shouldSelectAllChoices = (choiceStates) => {
|
|
920
|
-
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region src/utils/prompts.ts
|
|
925
|
-
const require = createRequire(import.meta.url);
|
|
926
|
-
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
927
|
-
const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
|
|
928
|
-
let didPatchMultiselectToggleAll = false;
|
|
929
|
-
let didPatchMultiselectSubmit = false;
|
|
930
|
-
let didPatchSelectBanner = false;
|
|
931
|
-
const selectBannerMap = /* @__PURE__ */ new Map();
|
|
932
|
-
const onCancel = () => {
|
|
933
|
-
logger.break();
|
|
934
|
-
logger.log("Cancelled.");
|
|
935
|
-
logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
|
|
936
|
-
logger.break();
|
|
937
|
-
process.exit(0);
|
|
938
|
-
};
|
|
939
|
-
const patchMultiselectToggleAll = () => {
|
|
940
|
-
if (didPatchMultiselectToggleAll) return;
|
|
941
|
-
didPatchMultiselectToggleAll = true;
|
|
942
|
-
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
943
|
-
multiselectPromptConstructor.prototype.toggleAll = function() {
|
|
944
|
-
const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
|
|
945
|
-
if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
|
|
946
|
-
this.bell();
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
|
|
950
|
-
for (const choiceState of this.value) {
|
|
951
|
-
if (choiceState.disabled) continue;
|
|
952
|
-
choiceState.selected = shouldSelectAllEnabledChoices;
|
|
953
|
-
}
|
|
954
|
-
this.render();
|
|
955
|
-
};
|
|
956
|
-
};
|
|
957
|
-
const patchMultiselectSubmit = () => {
|
|
958
|
-
if (didPatchMultiselectSubmit) return;
|
|
959
|
-
didPatchMultiselectSubmit = true;
|
|
960
|
-
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
961
|
-
const originalSubmit = multiselectPromptConstructor.prototype.submit;
|
|
962
|
-
multiselectPromptConstructor.prototype.submit = function() {
|
|
963
|
-
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
|
|
964
|
-
originalSubmit.call(this);
|
|
965
|
-
};
|
|
966
|
-
};
|
|
967
|
-
const patchSelectBanner = () => {
|
|
968
|
-
if (didPatchSelectBanner) return;
|
|
969
|
-
didPatchSelectBanner = true;
|
|
970
|
-
const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
|
|
971
|
-
const promptsClear = require("prompts/lib/util/clear");
|
|
972
|
-
const originalRender = selectConstructor.prototype.render;
|
|
973
|
-
selectConstructor.prototype.render = function() {
|
|
974
|
-
originalRender.call(this);
|
|
975
|
-
const banner = selectBannerMap.get(this.cursor);
|
|
976
|
-
if (!banner || this.closed || this.done) return;
|
|
977
|
-
this.out.write(promptsClear(this.outputText, this.out.columns));
|
|
978
|
-
this.outputText = `${banner}\n\n${this.outputText}`;
|
|
979
|
-
this.out.write(this.outputText);
|
|
980
|
-
};
|
|
981
|
-
};
|
|
982
|
-
const prompts = (questions) => {
|
|
983
|
-
patchMultiselectToggleAll();
|
|
984
|
-
patchMultiselectSubmit();
|
|
985
|
-
patchSelectBanner();
|
|
986
|
-
return basePrompts(questions, { onCancel });
|
|
987
|
-
};
|
|
988
|
-
|
|
989
1138
|
//#endregion
|
|
990
1139
|
//#region src/utils/resolve-compatible-node.ts
|
|
991
1140
|
const parseNodeVersion = (versionString) => {
|
|
@@ -1252,6 +1401,22 @@ const REACT_NATIVE_RULES = {
|
|
|
1252
1401
|
"react-doctor/rn-prefer-reanimated": "warn",
|
|
1253
1402
|
"react-doctor/rn-no-single-element-style-array": "warn"
|
|
1254
1403
|
};
|
|
1404
|
+
const TANSTACK_START_RULES = {
|
|
1405
|
+
"react-doctor/tanstack-start-route-property-order": "error",
|
|
1406
|
+
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "warn",
|
|
1407
|
+
"react-doctor/tanstack-start-server-fn-validate-input": "warn",
|
|
1408
|
+
"react-doctor/tanstack-start-no-useeffect-fetch": "warn",
|
|
1409
|
+
"react-doctor/tanstack-start-missing-head-content": "warn",
|
|
1410
|
+
"react-doctor/tanstack-start-no-anchor-element": "warn",
|
|
1411
|
+
"react-doctor/tanstack-start-server-fn-method-order": "error",
|
|
1412
|
+
"react-doctor/tanstack-start-no-navigate-in-render": "warn",
|
|
1413
|
+
"react-doctor/tanstack-start-no-dynamic-server-fn-import": "error",
|
|
1414
|
+
"react-doctor/tanstack-start-no-use-server-in-handler": "error",
|
|
1415
|
+
"react-doctor/tanstack-start-no-secrets-in-loader": "error",
|
|
1416
|
+
"react-doctor/tanstack-start-get-mutation": "warn",
|
|
1417
|
+
"react-doctor/tanstack-start-redirect-in-try-catch": "warn",
|
|
1418
|
+
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
1419
|
+
};
|
|
1255
1420
|
const REACT_COMPILER_RULES = {
|
|
1256
1421
|
"react-hooks-js/set-state-in-render": "error",
|
|
1257
1422
|
"react-hooks-js/immutability": "error",
|
|
@@ -1341,12 +1506,14 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1341
1506
|
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
1342
1507
|
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
1343
1508
|
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
1509
|
+
"react-doctor/rendering-script-defer-async": "warn",
|
|
1344
1510
|
"react-doctor/no-transition-all": "warn",
|
|
1345
1511
|
"react-doctor/no-global-css-variable-animation": "error",
|
|
1346
1512
|
"react-doctor/no-large-animated-blur": "warn",
|
|
1347
1513
|
"react-doctor/no-scale-from-zero": "warn",
|
|
1348
1514
|
"react-doctor/no-permanent-will-change": "warn",
|
|
1349
1515
|
"react-doctor/no-secrets-in-client-code": "error",
|
|
1516
|
+
"react-doctor/js-flatmap-filter": "warn",
|
|
1350
1517
|
"react-doctor/no-barrel-import": "warn",
|
|
1351
1518
|
"react-doctor/no-full-lodash-import": "warn",
|
|
1352
1519
|
"react-doctor/no-moment": "warn",
|
|
@@ -1359,9 +1526,31 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1359
1526
|
"react-doctor/server-auth-actions": "error",
|
|
1360
1527
|
"react-doctor/server-after-nonblocking": "warn",
|
|
1361
1528
|
"react-doctor/client-passive-event-listeners": "warn",
|
|
1529
|
+
"react-doctor/query-stable-query-client": "error",
|
|
1530
|
+
"react-doctor/query-no-rest-destructuring": "warn",
|
|
1531
|
+
"react-doctor/query-no-void-query-fn": "warn",
|
|
1532
|
+
"react-doctor/query-no-query-in-effect": "warn",
|
|
1533
|
+
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1534
|
+
"react-doctor/query-no-usequery-for-mutation": "warn",
|
|
1535
|
+
"react-doctor/no-inline-bounce-easing": "warn",
|
|
1536
|
+
"react-doctor/no-z-index-9999": "warn",
|
|
1537
|
+
"react-doctor/no-inline-exhaustive-style": "warn",
|
|
1538
|
+
"react-doctor/no-side-tab-border": "warn",
|
|
1539
|
+
"react-doctor/no-pure-black-background": "warn",
|
|
1540
|
+
"react-doctor/no-gradient-text": "warn",
|
|
1541
|
+
"react-doctor/no-dark-mode-glow": "warn",
|
|
1542
|
+
"react-doctor/no-justified-text": "warn",
|
|
1543
|
+
"react-doctor/no-tiny-text": "warn",
|
|
1544
|
+
"react-doctor/no-wide-letter-spacing": "warn",
|
|
1545
|
+
"react-doctor/no-gray-on-colored-background": "warn",
|
|
1546
|
+
"react-doctor/no-layout-transition-inline": "warn",
|
|
1547
|
+
"react-doctor/no-disabled-zoom": "error",
|
|
1548
|
+
"react-doctor/no-outline-none": "warn",
|
|
1549
|
+
"react-doctor/no-long-transition-duration": "warn",
|
|
1362
1550
|
"react-doctor/async-parallel": "warn",
|
|
1363
1551
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1364
|
-
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {}
|
|
1552
|
+
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1553
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
|
|
1365
1554
|
}
|
|
1366
1555
|
});
|
|
1367
1556
|
|
|
@@ -1438,6 +1627,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
1438
1627
|
"react-doctor/rendering-animate-svg-wrapper": "Performance",
|
|
1439
1628
|
"react-doctor/rendering-usetransition-loading": "Performance",
|
|
1440
1629
|
"react-doctor/rendering-hydration-no-flicker": "Performance",
|
|
1630
|
+
"react-doctor/rendering-script-defer-async": "Performance",
|
|
1441
1631
|
"react-doctor/no-transition-all": "Performance",
|
|
1442
1632
|
"react-doctor/no-global-css-variable-animation": "Performance",
|
|
1443
1633
|
"react-doctor/no-large-animated-blur": "Performance",
|
|
@@ -1472,6 +1662,28 @@ const RULE_CATEGORY_MAP = {
|
|
|
1472
1662
|
"react-doctor/server-auth-actions": "Server",
|
|
1473
1663
|
"react-doctor/server-after-nonblocking": "Server",
|
|
1474
1664
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
1665
|
+
"react-doctor/query-stable-query-client": "TanStack Query",
|
|
1666
|
+
"react-doctor/query-no-rest-destructuring": "TanStack Query",
|
|
1667
|
+
"react-doctor/query-no-void-query-fn": "TanStack Query",
|
|
1668
|
+
"react-doctor/query-no-query-in-effect": "TanStack Query",
|
|
1669
|
+
"react-doctor/query-mutation-missing-invalidation": "TanStack Query",
|
|
1670
|
+
"react-doctor/query-no-usequery-for-mutation": "TanStack Query",
|
|
1671
|
+
"react-doctor/no-inline-bounce-easing": "Performance",
|
|
1672
|
+
"react-doctor/no-z-index-9999": "Architecture",
|
|
1673
|
+
"react-doctor/no-inline-exhaustive-style": "Architecture",
|
|
1674
|
+
"react-doctor/no-side-tab-border": "Architecture",
|
|
1675
|
+
"react-doctor/no-pure-black-background": "Architecture",
|
|
1676
|
+
"react-doctor/no-gradient-text": "Architecture",
|
|
1677
|
+
"react-doctor/no-dark-mode-glow": "Architecture",
|
|
1678
|
+
"react-doctor/no-justified-text": "Accessibility",
|
|
1679
|
+
"react-doctor/no-tiny-text": "Accessibility",
|
|
1680
|
+
"react-doctor/no-wide-letter-spacing": "Architecture",
|
|
1681
|
+
"react-doctor/no-gray-on-colored-background": "Accessibility",
|
|
1682
|
+
"react-doctor/no-layout-transition-inline": "Performance",
|
|
1683
|
+
"react-doctor/no-disabled-zoom": "Accessibility",
|
|
1684
|
+
"react-doctor/no-outline-none": "Accessibility",
|
|
1685
|
+
"react-doctor/no-long-transition-duration": "Performance",
|
|
1686
|
+
"react-doctor/js-flatmap-filter": "Performance",
|
|
1475
1687
|
"react-doctor/async-parallel": "Performance",
|
|
1476
1688
|
"react-doctor/rn-no-raw-text": "React Native",
|
|
1477
1689
|
"react-doctor/rn-no-deprecated-modules": "React Native",
|
|
@@ -1480,7 +1692,21 @@ const RULE_CATEGORY_MAP = {
|
|
|
1480
1692
|
"react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
|
|
1481
1693
|
"react-doctor/rn-no-legacy-shadow-styles": "React Native",
|
|
1482
1694
|
"react-doctor/rn-prefer-reanimated": "React Native",
|
|
1483
|
-
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
1695
|
+
"react-doctor/rn-no-single-element-style-array": "React Native",
|
|
1696
|
+
"react-doctor/tanstack-start-route-property-order": "TanStack Start",
|
|
1697
|
+
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
|
|
1698
|
+
"react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
|
|
1699
|
+
"react-doctor/tanstack-start-no-useeffect-fetch": "TanStack Start",
|
|
1700
|
+
"react-doctor/tanstack-start-missing-head-content": "TanStack Start",
|
|
1701
|
+
"react-doctor/tanstack-start-no-anchor-element": "TanStack Start",
|
|
1702
|
+
"react-doctor/tanstack-start-server-fn-method-order": "TanStack Start",
|
|
1703
|
+
"react-doctor/tanstack-start-no-navigate-in-render": "TanStack Start",
|
|
1704
|
+
"react-doctor/tanstack-start-no-dynamic-server-fn-import": "TanStack Start",
|
|
1705
|
+
"react-doctor/tanstack-start-no-use-server-in-handler": "TanStack Start",
|
|
1706
|
+
"react-doctor/tanstack-start-no-secrets-in-loader": "Security",
|
|
1707
|
+
"react-doctor/tanstack-start-get-mutation": "Security",
|
|
1708
|
+
"react-doctor/tanstack-start-redirect-in-try-catch": "TanStack Start",
|
|
1709
|
+
"react-doctor/tanstack-start-loader-parallel-fetch": "Performance"
|
|
1484
1710
|
};
|
|
1485
1711
|
const RULE_HELP_MAP = {
|
|
1486
1712
|
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
@@ -1502,6 +1728,7 @@ const RULE_HELP_MAP = {
|
|
|
1502
1728
|
"rendering-animate-svg-wrapper": "Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
|
|
1503
1729
|
"rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
|
|
1504
1730
|
"rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
|
|
1731
|
+
"rendering-script-defer-async": "Add `defer` for DOM-dependent scripts or `async` for independent ones (analytics). In Next.js, use `<Script strategy=\"afterInteractive\" />` instead",
|
|
1505
1732
|
"no-transition-all": "List specific properties: `transition: \"opacity 200ms, transform 200ms\"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
1506
1733
|
"no-global-css-variable-animation": "Set the variable on the nearest element instead of a parent, or use `@property` with `inherits: false` to prevent cascade. Better yet, use targeted `element.style.transform` updates",
|
|
1507
1734
|
"no-large-animated-blur": "Keep blur radius under 10px, or apply blur to a smaller element. Large blurs multiply GPU memory usage with layer size",
|
|
@@ -1514,6 +1741,21 @@ const RULE_HELP_MAP = {
|
|
|
1514
1741
|
"prefer-dynamic-import": "Use `const Component = dynamic(() => import('library'), { ssr: false })` from next/dynamic or React.lazy()",
|
|
1515
1742
|
"use-lazy-motion": "Use `import { LazyMotion, m } from \"framer-motion\"` with `domAnimation` features — saves ~30kb",
|
|
1516
1743
|
"no-undeferred-third-party": "Use `next/script` with `strategy=\"lazyOnload\"` or add the `defer` attribute",
|
|
1744
|
+
"no-inline-bounce-easing": "Use `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out-expo) for natural deceleration — objects in the real world don't bounce",
|
|
1745
|
+
"no-z-index-9999": "Define a z-index scale in your design tokens (e.g. dropdown: 10, modal: 20, toast: 30). Create a new stacking context with `isolation: isolate` instead of escalating values",
|
|
1746
|
+
"no-inline-exhaustive-style": "Move styles to a CSS class, CSS module, Tailwind utilities, or a styled component — inline objects with many properties hurt readability and create new references every render",
|
|
1747
|
+
"no-side-tab-border": "Use a subtler accent (box-shadow inset, background gradient, or border-bottom) instead of a thick one-sided border",
|
|
1748
|
+
"no-pure-black-background": "Tint the background slightly toward your brand hue — e.g. `#0a0a0f` or Tailwind's `bg-gray-950`. Pure black looks harsh on modern displays",
|
|
1749
|
+
"no-gradient-text": "Use solid text colors for readability. If you need emphasis, use font weight, size, or a distinct color instead of gradients",
|
|
1750
|
+
"no-dark-mode-glow": "Use a subtle `box-shadow` with neutral colors for depth, or `border` with low opacity. Colored glows on dark backgrounds are the default AI-generated aesthetic",
|
|
1751
|
+
"no-justified-text": "Use `text-align: left` for body text, or add `hyphens: auto` and `overflow-wrap: break-word` if you must justify",
|
|
1752
|
+
"no-tiny-text": "Use at least 12px for body content, 16px is ideal. Small text is hard to read, especially on high-DPI mobile screens",
|
|
1753
|
+
"no-wide-letter-spacing": "Reserve wide tracking (letter-spacing > 0.05em) for short uppercase labels, navigation items, and buttons — not body text",
|
|
1754
|
+
"no-gray-on-colored-background": "Use a darker shade of the background color for text, or white/near-white for contrast. Gray text on colored backgrounds looks washed out",
|
|
1755
|
+
"no-layout-transition-inline": "Use `transform` and `opacity` for transitions — they run on the compositor thread. For height animations, use `grid-template-rows: 0fr → 1fr`",
|
|
1756
|
+
"no-disabled-zoom": "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If your layout breaks at 200% zoom, fix the layout — don't punish users with disabilities",
|
|
1757
|
+
"no-outline-none": "Use `:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px }` to show focus only for keyboard users while hiding it for mouse clicks",
|
|
1758
|
+
"no-long-transition-duration": "Keep UI transitions under 1s — 100-150ms for instant feedback, 200-300ms for state changes, 300-500ms for layout changes. Use longer durations only for page-load hero animations",
|
|
1517
1759
|
"no-array-index-as-key": "Use a stable unique identifier: `key={item.id}` or `key={item.slug}` — index keys break on reorder/filter",
|
|
1518
1760
|
"rendering-conditional-render": "Change to `{items.length > 0 && <List />}` or use a ternary: `{items.length ? <List /> : null}`",
|
|
1519
1761
|
"no-prevent-default": "Use `<form action={serverAction}>` (works without JS) or `<button>` instead of `<a>` with preventDefault",
|
|
@@ -1523,7 +1765,7 @@ const RULE_HELP_MAP = {
|
|
|
1523
1765
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
1524
1766
|
"nextjs-no-client-fetch-for-server-data": "Remove 'use client' and fetch directly in the Server Component — no API round-trip, secrets stay on server",
|
|
1525
1767
|
"nextjs-missing-metadata": "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
1526
|
-
"nextjs-no-client-side-redirect": "
|
|
1768
|
+
"nextjs-no-client-side-redirect": "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
1527
1769
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
1528
1770
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
1529
1771
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -1536,6 +1778,13 @@ const RULE_HELP_MAP = {
|
|
|
1536
1778
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
1537
1779
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
1538
1780
|
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
|
|
1781
|
+
"query-stable-query-client": "Move `new QueryClient()` to module scope or wrap in `useState(() => new QueryClient())` — recreating it on every render resets the entire cache",
|
|
1782
|
+
"query-no-rest-destructuring": "Destructure only the fields you need: `const { data, isLoading } = useQuery(...)` — rest destructuring subscribes to all fields and causes extra re-renders",
|
|
1783
|
+
"query-no-void-query-fn": "queryFn must return a value for the cache. Use the `enabled` option to conditionally disable the query instead of returning undefined",
|
|
1784
|
+
"query-no-query-in-effect": "React Query manages refetching automatically via queryKey dependencies and the `enabled` option — manual refetch() in useEffect is usually unnecessary",
|
|
1785
|
+
"query-mutation-missing-invalidation": "Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation",
|
|
1786
|
+
"query-no-usequery-for-mutation": "Use `useMutation()` for POST/PUT/DELETE — it provides onSuccess/onError callbacks, doesn't auto-refetch, and correctly models write operations",
|
|
1787
|
+
"js-flatmap-filter": "Use `.flatMap(item => condition ? [value] : [])` — transforms and filters in a single pass instead of creating an intermediate array",
|
|
1539
1788
|
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
1540
1789
|
"rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
|
|
1541
1790
|
"rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
|
|
@@ -1544,7 +1793,21 @@ const RULE_HELP_MAP = {
|
|
|
1544
1793
|
"rn-no-inline-flatlist-renderitem": "Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every render",
|
|
1545
1794
|
"rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
|
|
1546
1795
|
"rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
|
|
1547
|
-
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation"
|
|
1796
|
+
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation",
|
|
1797
|
+
"tanstack-start-route-property-order": "Follow the order: params/validateSearch → loaderDeps → context → beforeLoad → loader → head. See https://tanstack.com/router/latest/docs/eslint/create-route-property-order",
|
|
1798
|
+
"tanstack-start-no-direct-fetch-in-loader": "Use `createServerFn()` from @tanstack/react-start — provides type-safe RPC, input validation, and proper server/client code splitting",
|
|
1799
|
+
"tanstack-start-server-fn-validate-input": "Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime",
|
|
1800
|
+
"tanstack-start-no-useeffect-fetch": "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
1801
|
+
"tanstack-start-missing-head-content": "Add `<HeadContent />` inside `<head>` in your __root route — without it, route `head()` meta tags are silently dropped",
|
|
1802
|
+
"tanstack-start-no-anchor-element": "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
1803
|
+
"tanstack-start-server-fn-method-order": "Chain methods in order: .middleware() → .inputValidator() → .client() → .server() → .handler() — types depend on this sequence",
|
|
1804
|
+
"tanstack-start-no-navigate-in-render": "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader` instead — navigate() during render causes hydration issues",
|
|
1805
|
+
"tanstack-start-no-dynamic-server-fn-import": "Use `import { myFn } from '~/utils/my.functions'` — the bundler replaces server code with RPC stubs only for static imports",
|
|
1806
|
+
"tanstack-start-no-use-server-in-handler": "TanStack Start handles server boundaries automatically via the Vite plugin — \"use server\" inside createServerFn causes compilation errors",
|
|
1807
|
+
"tanstack-start-no-secrets-in-loader": "Loaders are isomorphic (run on both server and client). Wrap secret access in `createServerFn()` so it stays server-only",
|
|
1808
|
+
"tanstack-start-get-mutation": "Use `createServerFn({ method: 'POST' })` for data modifications — GET requests can be triggered by prefetching and are vulnerable to CSRF",
|
|
1809
|
+
"tanstack-start-redirect-in-try-catch": "TanStack Router's `redirect()` and `notFound()` throw special errors caught by the router. Move them outside the try block or re-throw in the catch",
|
|
1810
|
+
"tanstack-start-loader-parallel-fetch": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to avoid request waterfalls in route loaders"
|
|
1548
1811
|
};
|
|
1549
1812
|
const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
|
|
1550
1813
|
const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
|
|
@@ -1690,37 +1953,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1690
1953
|
}
|
|
1691
1954
|
};
|
|
1692
1955
|
|
|
1693
|
-
//#endregion
|
|
1694
|
-
//#region src/utils/spinner.ts
|
|
1695
|
-
let sharedInstance = null;
|
|
1696
|
-
let activeCount = 0;
|
|
1697
|
-
const pendingTexts = /* @__PURE__ */ new Set();
|
|
1698
|
-
const finalize = (method, originalText, displayText) => {
|
|
1699
|
-
pendingTexts.delete(originalText);
|
|
1700
|
-
activeCount--;
|
|
1701
|
-
if (activeCount <= 0 || !sharedInstance) {
|
|
1702
|
-
sharedInstance?.[method](displayText);
|
|
1703
|
-
sharedInstance = null;
|
|
1704
|
-
activeCount = 0;
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
sharedInstance.stop();
|
|
1708
|
-
ora(displayText).start()[method](displayText);
|
|
1709
|
-
const [remainingText] = pendingTexts;
|
|
1710
|
-
if (remainingText) sharedInstance.text = remainingText;
|
|
1711
|
-
sharedInstance.start();
|
|
1712
|
-
};
|
|
1713
|
-
const spinner = (text) => ({ start() {
|
|
1714
|
-
activeCount++;
|
|
1715
|
-
pendingTexts.add(text);
|
|
1716
|
-
if (!sharedInstance) sharedInstance = ora({ text }).start();
|
|
1717
|
-
else sharedInstance.text = text;
|
|
1718
|
-
return {
|
|
1719
|
-
succeed: (displayText) => finalize("succeed", text, displayText),
|
|
1720
|
-
fail: (displayText) => finalize("fail", text, displayText)
|
|
1721
|
-
};
|
|
1722
|
-
} });
|
|
1723
|
-
|
|
1724
1956
|
//#endregion
|
|
1725
1957
|
//#region src/scan.ts
|
|
1726
1958
|
const SEVERITY_ORDER = {
|
|
@@ -2280,7 +2512,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
2280
2512
|
|
|
2281
2513
|
//#endregion
|
|
2282
2514
|
//#region src/cli.ts
|
|
2283
|
-
const VERSION = "0.0.
|
|
2515
|
+
const VERSION = "0.0.36";
|
|
2284
2516
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2285
2517
|
"error",
|
|
2286
2518
|
"warning",
|
|
@@ -2438,6 +2670,13 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
2438
2670
|
${highlighter.dim("Learn more:")}
|
|
2439
2671
|
${highlighter.info("https://github.com/millionco/react-doctor")}
|
|
2440
2672
|
`);
|
|
2673
|
+
program.command("install").description("Install the react-doctor skill into your coding agents").option("-y, --yes", "skip prompts, install for all detected agents").action(async (options) => {
|
|
2674
|
+
try {
|
|
2675
|
+
await runInstallSkill({ yes: options.yes });
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
handleError(error);
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2441
2680
|
const main$1 = async () => {
|
|
2442
2681
|
await program.parseAsync();
|
|
2443
2682
|
};
|