react-doctor 0.0.40 → 0.0.42
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/{process-browser-diagnostics-Cahx3_oy.d.ts → browser-DFbjNpPb.d.ts} +1 -1
- package/dist/browser-DFbjNpPb.d.ts.map +1 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -3
- package/dist/cli.js +110 -116
- package/dist/cli.js.map +1 -1
- package/dist/index.js +71 -65
- package/dist/index.js.map +1 -1
- package/dist/{process-browser-diagnostics-DpaZeYLI.js → process-browser-diagnostics-BHiLPUJT.js} +5 -25
- package/dist/process-browser-diagnostics-BHiLPUJT.js.map +1 -0
- package/dist/react-doctor-plugin.js +25 -63
- package/dist/react-doctor-plugin.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +2 -3
- package/package.json +1 -1
- package/dist/process-browser-diagnostics-Cahx3_oy.d.ts.map +0 -1
- package/dist/process-browser-diagnostics-DpaZeYLI.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -13,8 +13,8 @@ import { performance } from "node:perf_hooks";
|
|
|
13
13
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { main } from "knip";
|
|
15
15
|
import { createOptions } from "knip/session";
|
|
16
|
-
|
|
17
16
|
//#region src/utils/detect-agents.ts
|
|
17
|
+
const AGENTS_SKILL_DIR = ".agents/skills";
|
|
18
18
|
const SUPPORTED_AGENTS = {
|
|
19
19
|
claude: {
|
|
20
20
|
binaries: ["claude"],
|
|
@@ -24,37 +24,37 @@ const SUPPORTED_AGENTS = {
|
|
|
24
24
|
codex: {
|
|
25
25
|
binaries: ["codex"],
|
|
26
26
|
displayName: "Codex",
|
|
27
|
-
skillDir:
|
|
27
|
+
skillDir: AGENTS_SKILL_DIR
|
|
28
28
|
},
|
|
29
29
|
copilot: {
|
|
30
30
|
binaries: ["copilot"],
|
|
31
31
|
displayName: "GitHub Copilot",
|
|
32
|
-
skillDir:
|
|
32
|
+
skillDir: AGENTS_SKILL_DIR
|
|
33
33
|
},
|
|
34
34
|
gemini: {
|
|
35
35
|
binaries: ["gemini"],
|
|
36
36
|
displayName: "Gemini CLI",
|
|
37
|
-
skillDir:
|
|
37
|
+
skillDir: AGENTS_SKILL_DIR
|
|
38
38
|
},
|
|
39
39
|
cursor: {
|
|
40
40
|
binaries: ["cursor", "agent"],
|
|
41
41
|
displayName: "Cursor",
|
|
42
|
-
skillDir:
|
|
42
|
+
skillDir: AGENTS_SKILL_DIR
|
|
43
43
|
},
|
|
44
44
|
opencode: {
|
|
45
45
|
binaries: ["opencode"],
|
|
46
46
|
displayName: "OpenCode",
|
|
47
|
-
skillDir:
|
|
47
|
+
skillDir: AGENTS_SKILL_DIR
|
|
48
48
|
},
|
|
49
49
|
droid: {
|
|
50
50
|
binaries: ["droid"],
|
|
51
51
|
displayName: "Factory Droid",
|
|
52
|
-
skillDir: ".
|
|
52
|
+
skillDir: ".factory/skills"
|
|
53
53
|
},
|
|
54
54
|
pi: {
|
|
55
55
|
binaries: ["pi", "omegon"],
|
|
56
56
|
displayName: "Pi",
|
|
57
|
-
skillDir:
|
|
57
|
+
skillDir: AGENTS_SKILL_DIR
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
const ALL_SUPPORTED_AGENTS = Object.keys(SUPPORTED_AGENTS);
|
|
@@ -74,7 +74,6 @@ const isCommandAvailable = (command) => {
|
|
|
74
74
|
const detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
|
|
75
75
|
const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
|
|
76
76
|
const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
|
|
77
|
-
|
|
78
77
|
//#endregion
|
|
79
78
|
//#region src/utils/highlighter.ts
|
|
80
79
|
const highlighter = {
|
|
@@ -84,11 +83,11 @@ const highlighter = {
|
|
|
84
83
|
success: pc.green,
|
|
85
84
|
dim: pc.dim
|
|
86
85
|
};
|
|
87
|
-
|
|
88
86
|
//#endregion
|
|
89
87
|
//#region src/utils/install-skill-for-agent.ts
|
|
90
|
-
const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName) => {
|
|
88
|
+
const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName, alreadyInstalledDirectories) => {
|
|
91
89
|
const installedSkillDirectory = path.join(projectRoot, toSkillDir(agent), skillName);
|
|
90
|
+
if (alreadyInstalledDirectories?.has(installedSkillDirectory)) return installedSkillDirectory;
|
|
92
91
|
rmSync(installedSkillDirectory, {
|
|
93
92
|
recursive: true,
|
|
94
93
|
force: true
|
|
@@ -97,7 +96,6 @@ const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillNam
|
|
|
97
96
|
cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
|
|
98
97
|
return installedSkillDirectory;
|
|
99
98
|
};
|
|
100
|
-
|
|
101
99
|
//#endregion
|
|
102
100
|
//#region src/utils/logger.ts
|
|
103
101
|
const logger = {
|
|
@@ -123,7 +121,6 @@ const logger = {
|
|
|
123
121
|
console.log("");
|
|
124
122
|
}
|
|
125
123
|
};
|
|
126
|
-
|
|
127
124
|
//#endregion
|
|
128
125
|
//#region src/utils/should-auto-select-current-choice.ts
|
|
129
126
|
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
@@ -131,13 +128,11 @@ const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
|
131
128
|
const currentChoice = choiceStates[cursor];
|
|
132
129
|
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
133
130
|
};
|
|
134
|
-
|
|
135
131
|
//#endregion
|
|
136
132
|
//#region src/utils/should-select-all-choices.ts
|
|
137
133
|
const shouldSelectAllChoices = (choiceStates) => {
|
|
138
134
|
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
139
135
|
};
|
|
140
|
-
|
|
141
136
|
//#endregion
|
|
142
137
|
//#region src/utils/prompts.ts
|
|
143
138
|
const require = createRequire(import.meta.url);
|
|
@@ -203,7 +198,6 @@ const prompts = (questions) => {
|
|
|
203
198
|
patchSelectBanner();
|
|
204
199
|
return basePrompts(questions, { onCancel });
|
|
205
200
|
};
|
|
206
|
-
|
|
207
201
|
//#endregion
|
|
208
202
|
//#region src/utils/spinner.ts
|
|
209
203
|
let sharedInstance = null;
|
|
@@ -234,7 +228,6 @@ const spinner = (text) => ({ start() {
|
|
|
234
228
|
fail: (displayText) => finalize("fail", text, displayText)
|
|
235
229
|
};
|
|
236
230
|
} });
|
|
237
|
-
|
|
238
231
|
//#endregion
|
|
239
232
|
//#region src/install-skill.ts
|
|
240
233
|
const SKILL_NAME = "react-doctor";
|
|
@@ -271,35 +264,37 @@ const runInstallSkill = async (options = {}) => {
|
|
|
271
264
|
})).agents ?? [];
|
|
272
265
|
if (selectedAgents.length === 0) return;
|
|
273
266
|
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
274
|
-
|
|
267
|
+
const installedDirectories = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const agent of selectedAgents) {
|
|
269
|
+
const installedDirectory = installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME, installedDirectories);
|
|
270
|
+
installedDirectories.add(installedDirectory);
|
|
271
|
+
}
|
|
275
272
|
installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
|
|
276
273
|
};
|
|
277
|
-
|
|
278
274
|
//#endregion
|
|
279
275
|
//#region src/constants.ts
|
|
280
276
|
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
281
277
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
282
278
|
const MILLISECONDS_PER_SECOND = 1e3;
|
|
283
|
-
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
284
|
-
const PERFECT_SCORE = 100;
|
|
285
|
-
const SCORE_GOOD_THRESHOLD = 75;
|
|
286
|
-
const SCORE_OK_THRESHOLD = 50;
|
|
287
|
-
const SCORE_BAR_WIDTH_CHARS = 50;
|
|
288
|
-
const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
|
|
289
|
-
const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
|
|
290
279
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
291
280
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
292
281
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
293
282
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
294
|
-
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
295
|
-
const OXLINT_MAX_FILES_PER_BATCH = 500;
|
|
296
283
|
const OFFLINE_MESSAGE = "Score calculated locally (offline mode).";
|
|
297
284
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
298
285
|
const ERROR_RULE_PENALTY = 1.5;
|
|
299
286
|
const WARNING_RULE_PENALTY = .75;
|
|
300
|
-
const
|
|
287
|
+
const KNIP_CONFIG_LOCATIONS = [
|
|
288
|
+
"knip.json",
|
|
289
|
+
"knip.jsonc",
|
|
290
|
+
".knip.json",
|
|
291
|
+
".knip.jsonc",
|
|
292
|
+
"knip.ts",
|
|
293
|
+
"knip.js",
|
|
294
|
+
"knip.config.ts",
|
|
295
|
+
"knip.config.js"
|
|
296
|
+
];
|
|
301
297
|
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
302
|
-
const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
|
|
303
298
|
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
304
299
|
const IGNORED_DIRECTORIES = new Set([
|
|
305
300
|
"node_modules",
|
|
@@ -307,12 +302,11 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
307
302
|
"build",
|
|
308
303
|
"coverage"
|
|
309
304
|
]);
|
|
310
|
-
|
|
311
305
|
//#endregion
|
|
312
306
|
//#region src/core/calculate-score-locally.ts
|
|
313
307
|
const getScoreLabel = (score) => {
|
|
314
|
-
if (score >=
|
|
315
|
-
if (score >=
|
|
308
|
+
if (score >= 75) return "Great";
|
|
309
|
+
if (score >= 50) return "Needs work";
|
|
316
310
|
return "Critical";
|
|
317
311
|
};
|
|
318
312
|
const countUniqueRules = (diagnostics) => {
|
|
@@ -330,7 +324,7 @@ const countUniqueRules = (diagnostics) => {
|
|
|
330
324
|
};
|
|
331
325
|
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
332
326
|
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
333
|
-
return Math.max(0, Math.round(
|
|
327
|
+
return Math.max(0, Math.round(100 - penalty));
|
|
334
328
|
};
|
|
335
329
|
const calculateScoreLocally = (diagnostics) => {
|
|
336
330
|
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
@@ -340,7 +334,6 @@ const calculateScoreLocally = (diagnostics) => {
|
|
|
340
334
|
label: getScoreLabel(score)
|
|
341
335
|
};
|
|
342
336
|
};
|
|
343
|
-
|
|
344
337
|
//#endregion
|
|
345
338
|
//#region src/core/try-score-from-api.ts
|
|
346
339
|
const parseScoreResult = (value) => {
|
|
@@ -372,7 +365,6 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
|
|
|
372
365
|
clearTimeout(timeoutId);
|
|
373
366
|
}
|
|
374
367
|
};
|
|
375
|
-
|
|
376
368
|
//#endregion
|
|
377
369
|
//#region src/utils/proxy-fetch.ts
|
|
378
370
|
const getGlobalProcess = () => {
|
|
@@ -415,7 +407,6 @@ const proxyFetch = async (url, init) => {
|
|
|
415
407
|
clearTimeout(timeoutId);
|
|
416
408
|
}
|
|
417
409
|
};
|
|
418
|
-
|
|
419
410
|
//#endregion
|
|
420
411
|
//#region src/utils/calculate-score-node.ts
|
|
421
412
|
const calculateScore = async (diagnostics) => {
|
|
@@ -423,19 +414,16 @@ const calculateScore = async (diagnostics) => {
|
|
|
423
414
|
if (apiScore) return apiScore;
|
|
424
415
|
return calculateScoreLocally(diagnostics);
|
|
425
416
|
};
|
|
426
|
-
|
|
427
417
|
//#endregion
|
|
428
418
|
//#region src/utils/colorize-by-score.ts
|
|
429
419
|
const colorizeByScore = (text, score) => {
|
|
430
|
-
if (score >=
|
|
431
|
-
if (score >=
|
|
420
|
+
if (score >= 75) return highlighter.success(text);
|
|
421
|
+
if (score >= 50) return highlighter.warn(text);
|
|
432
422
|
return highlighter.error(text);
|
|
433
423
|
};
|
|
434
|
-
|
|
435
424
|
//#endregion
|
|
436
425
|
//#region src/plugin/constants.ts
|
|
437
426
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
438
|
-
|
|
439
427
|
//#endregion
|
|
440
428
|
//#region src/utils/is-file.ts
|
|
441
429
|
const isFile = (filePath) => {
|
|
@@ -445,7 +433,6 @@ const isFile = (filePath) => {
|
|
|
445
433
|
return false;
|
|
446
434
|
}
|
|
447
435
|
};
|
|
448
|
-
|
|
449
436
|
//#endregion
|
|
450
437
|
//#region src/utils/read-package-json.ts
|
|
451
438
|
const readPackageJson = (packageJsonPath) => {
|
|
@@ -460,7 +447,6 @@ const readPackageJson = (packageJsonPath) => {
|
|
|
460
447
|
throw error;
|
|
461
448
|
}
|
|
462
449
|
};
|
|
463
|
-
|
|
464
450
|
//#endregion
|
|
465
451
|
//#region src/utils/check-reduced-motion.ts
|
|
466
452
|
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
@@ -502,7 +488,6 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
502
488
|
return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
503
489
|
}
|
|
504
490
|
};
|
|
505
|
-
|
|
506
491
|
//#endregion
|
|
507
492
|
//#region src/utils/read-file-lines-node.ts
|
|
508
493
|
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
@@ -515,7 +500,6 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
|
515
500
|
}
|
|
516
501
|
};
|
|
517
502
|
};
|
|
518
|
-
|
|
519
503
|
//#endregion
|
|
520
504
|
//#region src/utils/match-glob-pattern.ts
|
|
521
505
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
@@ -543,7 +527,6 @@ const compileGlobPattern = (pattern) => {
|
|
|
543
527
|
regexSource += "$";
|
|
544
528
|
return new RegExp(regexSource);
|
|
545
529
|
};
|
|
546
|
-
|
|
547
530
|
//#endregion
|
|
548
531
|
//#region src/utils/is-ignored-file.ts
|
|
549
532
|
const toRelativePath = (filePath, rootDirectory) => {
|
|
@@ -558,7 +541,6 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
|
558
541
|
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
559
542
|
return patterns.some((pattern) => pattern.test(relativePath));
|
|
560
543
|
};
|
|
561
|
-
|
|
562
544
|
//#endregion
|
|
563
545
|
//#region src/utils/filter-diagnostics.ts
|
|
564
546
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
@@ -632,17 +614,14 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
|
|
|
632
614
|
return true;
|
|
633
615
|
});
|
|
634
616
|
};
|
|
635
|
-
|
|
636
617
|
//#endregion
|
|
637
618
|
//#region src/utils/merge-and-filter-diagnostics.ts
|
|
638
619
|
const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
|
|
639
620
|
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
|
|
640
621
|
};
|
|
641
|
-
|
|
642
622
|
//#endregion
|
|
643
623
|
//#region src/utils/jsx-include-paths.ts
|
|
644
624
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
645
|
-
|
|
646
625
|
//#endregion
|
|
647
626
|
//#region src/utils/combine-diagnostics.ts
|
|
648
627
|
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true) => {
|
|
@@ -653,7 +632,6 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
|
|
|
653
632
|
...extraDiagnostics
|
|
654
633
|
], directory, userConfig, readFileLinesSync);
|
|
655
634
|
};
|
|
656
|
-
|
|
657
635
|
//#endregion
|
|
658
636
|
//#region src/utils/find-monorepo-root.ts
|
|
659
637
|
const isMonorepoRoot = (directory) => {
|
|
@@ -672,11 +650,9 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
672
650
|
}
|
|
673
651
|
return null;
|
|
674
652
|
};
|
|
675
|
-
|
|
676
653
|
//#endregion
|
|
677
654
|
//#region src/utils/is-plain-object.ts
|
|
678
655
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
679
|
-
|
|
680
656
|
//#endregion
|
|
681
657
|
//#region src/utils/discover-project.ts
|
|
682
658
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -1097,7 +1073,22 @@ const discoverProject = (directory) => {
|
|
|
1097
1073
|
sourceFileCount
|
|
1098
1074
|
};
|
|
1099
1075
|
};
|
|
1100
|
-
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region src/utils/format-error-chain.ts
|
|
1078
|
+
const collectErrorChain = (rootError) => {
|
|
1079
|
+
const errorChain = [];
|
|
1080
|
+
const visitedErrors = /* @__PURE__ */ new Set();
|
|
1081
|
+
let currentError = rootError;
|
|
1082
|
+
while (currentError !== void 0 && !visitedErrors.has(currentError)) {
|
|
1083
|
+
visitedErrors.add(currentError);
|
|
1084
|
+
errorChain.push(currentError);
|
|
1085
|
+
currentError = currentError instanceof Error ? currentError.cause : void 0;
|
|
1086
|
+
}
|
|
1087
|
+
return errorChain;
|
|
1088
|
+
};
|
|
1089
|
+
const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
|
|
1090
|
+
const formatErrorChain = (rootError) => collectErrorChain(rootError).map(formatErrorMessage).join(" → ");
|
|
1091
|
+
const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
|
|
1101
1092
|
//#endregion
|
|
1102
1093
|
//#region src/utils/framed-box.ts
|
|
1103
1094
|
const createFramedLine = (plainText, renderedText = plainText) => ({
|
|
@@ -1107,10 +1098,10 @@ const createFramedLine = (plainText, renderedText = plainText) => ({
|
|
|
1107
1098
|
const renderFramedBoxString = (framedLines) => {
|
|
1108
1099
|
if (framedLines.length === 0) return "";
|
|
1109
1100
|
const borderColorizer = highlighter.dim;
|
|
1110
|
-
const outerIndent = " ".repeat(
|
|
1111
|
-
const horizontalPadding = " ".repeat(
|
|
1101
|
+
const outerIndent = " ".repeat(2);
|
|
1102
|
+
const horizontalPadding = " ".repeat(1);
|
|
1112
1103
|
const maximumLineLength = Math.max(...framedLines.map((framedLine) => framedLine.plainText.length));
|
|
1113
|
-
const borderLine = "─".repeat(maximumLineLength +
|
|
1104
|
+
const borderLine = "─".repeat(maximumLineLength + 2);
|
|
1114
1105
|
const lines = [];
|
|
1115
1106
|
lines.push(`${outerIndent}${borderColorizer(`┌${borderLine}┐`)}`);
|
|
1116
1107
|
for (const framedLine of framedLines) {
|
|
@@ -1124,7 +1115,6 @@ const printFramedBox = (framedLines) => {
|
|
|
1124
1115
|
const rendered = renderFramedBoxString(framedLines);
|
|
1125
1116
|
if (rendered) logger.log(rendered);
|
|
1126
1117
|
};
|
|
1127
|
-
|
|
1128
1118
|
//#endregion
|
|
1129
1119
|
//#region src/utils/group-by.ts
|
|
1130
1120
|
const groupBy = (items, keyFn) => {
|
|
@@ -1137,11 +1127,9 @@ const groupBy = (items, keyFn) => {
|
|
|
1137
1127
|
}
|
|
1138
1128
|
return groups;
|
|
1139
1129
|
};
|
|
1140
|
-
|
|
1141
1130
|
//#endregion
|
|
1142
1131
|
//#region src/utils/indent-multiline-text.ts
|
|
1143
1132
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
1144
|
-
|
|
1145
1133
|
//#endregion
|
|
1146
1134
|
//#region src/utils/load-config.ts
|
|
1147
1135
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
@@ -1177,7 +1165,6 @@ const loadConfig = (rootDirectory) => {
|
|
|
1177
1165
|
}
|
|
1178
1166
|
return null;
|
|
1179
1167
|
};
|
|
1180
|
-
|
|
1181
1168
|
//#endregion
|
|
1182
1169
|
//#region src/utils/resolve-compatible-node.ts
|
|
1183
1170
|
const parseNodeVersion = (versionString) => {
|
|
@@ -1230,7 +1217,7 @@ const installNodeViaNvm = () => {
|
|
|
1230
1217
|
const nvmScript = path.join(nvmDirectory, "nvm.sh");
|
|
1231
1218
|
if (!existsSync(nvmScript)) return false;
|
|
1232
1219
|
try {
|
|
1233
|
-
execSync(`bash -c ". '${nvmScript}' && nvm install
|
|
1220
|
+
execSync(`bash -c ". '${nvmScript}' && nvm install 24"`, { stdio: "inherit" });
|
|
1234
1221
|
return findCompatibleNvmBinary() !== null;
|
|
1235
1222
|
} catch {
|
|
1236
1223
|
return false;
|
|
@@ -1252,7 +1239,6 @@ const resolveNodeForOxlint = () => {
|
|
|
1252
1239
|
version
|
|
1253
1240
|
};
|
|
1254
1241
|
};
|
|
1255
|
-
|
|
1256
1242
|
//#endregion
|
|
1257
1243
|
//#region src/utils/resolve-lint-include-paths.ts
|
|
1258
1244
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
@@ -1295,7 +1281,6 @@ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
|
1295
1281
|
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
1296
1282
|
});
|
|
1297
1283
|
};
|
|
1298
|
-
|
|
1299
1284
|
//#endregion
|
|
1300
1285
|
//#region src/utils/collect-unused-file-paths.ts
|
|
1301
1286
|
const collectUnusedFilePaths = (filesIssues) => {
|
|
@@ -1309,7 +1294,19 @@ const collectUnusedFilePaths = (filesIssues) => {
|
|
|
1309
1294
|
}
|
|
1310
1295
|
return unusedFilePaths;
|
|
1311
1296
|
};
|
|
1312
|
-
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/utils/extract-failed-plugin-name.ts
|
|
1299
|
+
const PLUGIN_CONFIG_PATTERN = /(?:^|[/\\\s])([a-z][a-z0-9-]*)\.config\./i;
|
|
1300
|
+
const extractFailedPluginName = (error) => {
|
|
1301
|
+
for (const errorMessage of getErrorChainMessages(error)) {
|
|
1302
|
+
const pluginNameMatch = errorMessage.match(PLUGIN_CONFIG_PATTERN);
|
|
1303
|
+
if (pluginNameMatch?.[1]) return pluginNameMatch[1].toLowerCase();
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
};
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/utils/has-knip-config.ts
|
|
1309
|
+
const hasKnipConfig = (directory) => KNIP_CONFIG_LOCATIONS.some((configFilename) => isFile(path.join(directory, configFilename)));
|
|
1313
1310
|
//#endregion
|
|
1314
1311
|
//#region src/utils/run-knip.ts
|
|
1315
1312
|
const KNIP_CATEGORY_MAP = {
|
|
@@ -1364,12 +1361,15 @@ const silenced = async (fn) => {
|
|
|
1364
1361
|
console.error = originalError;
|
|
1365
1362
|
}
|
|
1366
1363
|
};
|
|
1367
|
-
const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
|
|
1368
|
-
const extractFailedPluginName = (error) => {
|
|
1369
|
-
return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
|
|
1370
|
-
};
|
|
1371
1364
|
const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
|
|
1372
1365
|
const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
|
|
1366
|
+
const tryDisableFailedPlugin = (error, parsedConfig, disabledPlugins) => {
|
|
1367
|
+
const failedPlugin = extractFailedPluginName(error);
|
|
1368
|
+
if (!failedPlugin || !(failedPlugin in parsedConfig) || disabledPlugins.has(failedPlugin)) return false;
|
|
1369
|
+
disabledPlugins.add(failedPlugin);
|
|
1370
|
+
parsedConfig[failedPlugin] = false;
|
|
1371
|
+
return true;
|
|
1372
|
+
};
|
|
1373
1373
|
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
1374
1374
|
const tsConfigFile = resolveTsConfigFile(knipCwd);
|
|
1375
1375
|
const options = await silenced(() => createOptions({
|
|
@@ -1379,33 +1379,36 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
|
1379
1379
|
...tsConfigFile ? { tsConfigFile } : {}
|
|
1380
1380
|
}));
|
|
1381
1381
|
const parsedConfig = options.parsedConfig;
|
|
1382
|
-
|
|
1382
|
+
const disabledPlugins = /* @__PURE__ */ new Set();
|
|
1383
|
+
let lastKnipError;
|
|
1384
|
+
for (let attempt = 0; attempt <= 5; attempt++) try {
|
|
1383
1385
|
return await silenced(() => main(options));
|
|
1384
1386
|
} catch (error) {
|
|
1385
|
-
|
|
1386
|
-
if (!
|
|
1387
|
-
parsedConfig[failedPlugin] = false;
|
|
1387
|
+
lastKnipError = error;
|
|
1388
|
+
if (!tryDisableFailedPlugin(error, parsedConfig, disabledPlugins)) throw error;
|
|
1388
1389
|
}
|
|
1389
|
-
throw
|
|
1390
|
+
throw lastKnipError;
|
|
1390
1391
|
};
|
|
1391
1392
|
const hasNodeModules = (directory) => {
|
|
1392
1393
|
const nodeModulesPath = path.join(directory, "node_modules");
|
|
1393
1394
|
return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
|
|
1394
1395
|
};
|
|
1396
|
+
const resolveWorkspaceName = (rootDirectory) => {
|
|
1397
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
1398
|
+
return (isFile(packageJsonPath) ? readPackageJson(packageJsonPath) : {}).name ?? path.basename(rootDirectory);
|
|
1399
|
+
};
|
|
1400
|
+
const runKnipForProject = async (rootDirectory, monorepoRoot) => {
|
|
1401
|
+
if (!monorepoRoot || hasKnipConfig(rootDirectory)) return runKnipWithOptions(rootDirectory);
|
|
1402
|
+
try {
|
|
1403
|
+
return await runKnipWithOptions(monorepoRoot, resolveWorkspaceName(rootDirectory));
|
|
1404
|
+
} catch {
|
|
1405
|
+
return runKnipWithOptions(rootDirectory);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1395
1408
|
const runKnip = async (rootDirectory) => {
|
|
1396
1409
|
const monorepoRoot = findMonorepoRoot(rootDirectory);
|
|
1397
1410
|
if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
|
|
1398
|
-
|
|
1399
|
-
if (monorepoRoot) {
|
|
1400
|
-
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
1401
|
-
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
1402
|
-
try {
|
|
1403
|
-
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
1404
|
-
} catch {
|
|
1405
|
-
knipResult = await runKnipWithOptions(rootDirectory);
|
|
1406
|
-
}
|
|
1407
|
-
} else knipResult = await runKnipWithOptions(rootDirectory);
|
|
1408
|
-
const { issues } = knipResult;
|
|
1411
|
+
const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
|
|
1409
1412
|
const diagnostics = [];
|
|
1410
1413
|
for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
|
|
1411
1414
|
filePath: path.relative(rootDirectory, unusedFilePath),
|
|
@@ -1426,7 +1429,6 @@ const runKnip = async (rootDirectory) => {
|
|
|
1426
1429
|
]) diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));
|
|
1427
1430
|
return diagnostics;
|
|
1428
1431
|
};
|
|
1429
|
-
|
|
1430
1432
|
//#endregion
|
|
1431
1433
|
//#region src/oxlint-config.ts
|
|
1432
1434
|
const esmRequire$1 = createRequire(import.meta.url);
|
|
@@ -1610,7 +1612,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1610
1612
|
...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
|
|
1611
1613
|
}
|
|
1612
1614
|
});
|
|
1613
|
-
|
|
1614
1615
|
//#endregion
|
|
1615
1616
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1616
1617
|
const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
|
|
@@ -1653,7 +1654,6 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
|
1653
1654
|
for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
|
|
1654
1655
|
};
|
|
1655
1656
|
};
|
|
1656
|
-
|
|
1657
1657
|
//#endregion
|
|
1658
1658
|
//#region src/utils/run-oxlint.ts
|
|
1659
1659
|
const esmRequire = createRequire(import.meta.url);
|
|
@@ -1913,8 +1913,8 @@ const batchIncludePaths = (baseArgs, includePaths) => {
|
|
|
1913
1913
|
let currentBatchLength = baseArgsLength;
|
|
1914
1914
|
for (const filePath of includePaths) {
|
|
1915
1915
|
const entryLength = filePath.length + 1;
|
|
1916
|
-
const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength >
|
|
1917
|
-
const exceedsFileCount = currentBatch.length >=
|
|
1916
|
+
const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
|
|
1917
|
+
const exceedsFileCount = currentBatch.length >= 500;
|
|
1918
1918
|
if (exceedsArgLength || exceedsFileCount) {
|
|
1919
1919
|
batches.push(currentBatch);
|
|
1920
1920
|
currentBatch = [];
|
|
@@ -1958,7 +1958,7 @@ const parseOxlintOutput = (stdout) => {
|
|
|
1958
1958
|
try {
|
|
1959
1959
|
output = JSON.parse(stdout);
|
|
1960
1960
|
} catch {
|
|
1961
|
-
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0,
|
|
1961
|
+
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
|
|
1962
1962
|
}
|
|
1963
1963
|
return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
|
|
1964
1964
|
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
@@ -2009,7 +2009,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
2009
2009
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
2010
2010
|
}
|
|
2011
2011
|
};
|
|
2012
|
-
|
|
2013
2012
|
//#endregion
|
|
2014
2013
|
//#region src/scan.ts
|
|
2015
2014
|
const SEVERITY_ORDER = {
|
|
@@ -2048,7 +2047,7 @@ const printDiagnostics = (diagnostics, isVerbose) => {
|
|
|
2048
2047
|
}
|
|
2049
2048
|
};
|
|
2050
2049
|
const formatElapsedTime = (elapsedMilliseconds) => {
|
|
2051
|
-
if (elapsedMilliseconds <
|
|
2050
|
+
if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
|
|
2052
2051
|
return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
2053
2052
|
};
|
|
2054
2053
|
const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
@@ -2077,8 +2076,8 @@ const writeDiagnosticsDirectory = (diagnostics) => {
|
|
|
2077
2076
|
return outputDirectory;
|
|
2078
2077
|
};
|
|
2079
2078
|
const buildScoreBarSegments = (score) => {
|
|
2080
|
-
const filledCount = Math.round(score /
|
|
2081
|
-
const emptyCount =
|
|
2079
|
+
const filledCount = Math.round(score / 100 * 50);
|
|
2080
|
+
const emptyCount = 50 - filledCount;
|
|
2082
2081
|
return {
|
|
2083
2082
|
filledSegment: "█".repeat(filledCount),
|
|
2084
2083
|
emptySegment: "░".repeat(emptyCount)
|
|
@@ -2095,14 +2094,14 @@ const buildScoreBar = (score) => {
|
|
|
2095
2094
|
const printScoreGauge = (score, label) => {
|
|
2096
2095
|
const scoreDisplay = colorizeByScore(`${score}`, score);
|
|
2097
2096
|
const labelDisplay = colorizeByScore(label, score);
|
|
2098
|
-
logger.log(` ${scoreDisplay} /
|
|
2097
|
+
logger.log(` ${scoreDisplay} / 100 ${labelDisplay}`);
|
|
2099
2098
|
logger.break();
|
|
2100
2099
|
logger.log(` ${buildScoreBar(score)}`);
|
|
2101
2100
|
logger.break();
|
|
2102
2101
|
};
|
|
2103
2102
|
const getDoctorFace = (score) => {
|
|
2104
|
-
if (score >=
|
|
2105
|
-
if (score >=
|
|
2103
|
+
if (score >= 75) return ["◠ ◠", " ▽ "];
|
|
2104
|
+
if (score >= 50) return ["• •", " ─ "];
|
|
2106
2105
|
return ["x x", " ▽ "];
|
|
2107
2106
|
};
|
|
2108
2107
|
const printBranding = (score) => {
|
|
@@ -2140,8 +2139,8 @@ const buildBrandingLines = (scoreResult, noScoreMessage) => {
|
|
|
2140
2139
|
lines.push(createFramedLine("└─────┘", scoreColorizer("└─────┘")));
|
|
2141
2140
|
lines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
|
|
2142
2141
|
lines.push(createFramedLine(""));
|
|
2143
|
-
const scoreLinePlainText = `${scoreResult.score} /
|
|
2144
|
-
const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} /
|
|
2142
|
+
const scoreLinePlainText = `${scoreResult.score} / 100 ${scoreResult.label}`;
|
|
2143
|
+
const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / 100 ${colorizeByScore(scoreResult.label, scoreResult.score)}`;
|
|
2145
2144
|
lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));
|
|
2146
2145
|
lines.push(createFramedLine(""));
|
|
2147
2146
|
lines.push(createFramedLine(buildPlainScoreBar(scoreResult.score), buildScoreBar(scoreResult.score)));
|
|
@@ -2208,7 +2207,7 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
|
|
|
2208
2207
|
const { shouldInstallNode } = await prompts({
|
|
2209
2208
|
type: "confirm",
|
|
2210
2209
|
name: "shouldInstallNode",
|
|
2211
|
-
message: `Install Node
|
|
2210
|
+
message: `Install Node 24 via nvm to enable lint checks?`,
|
|
2212
2211
|
initial: true
|
|
2213
2212
|
});
|
|
2214
2213
|
if (shouldInstallNode) {
|
|
@@ -2225,8 +2224,8 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
|
|
|
2225
2224
|
logger.break();
|
|
2226
2225
|
return null;
|
|
2227
2226
|
}
|
|
2228
|
-
} else if (isNvmInstalled()) logger.dim(` Run: nvm install
|
|
2229
|
-
else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install
|
|
2227
|
+
} else if (isNvmInstalled()) logger.dim(` Run: nvm install 24`);
|
|
2228
|
+
else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
|
|
2230
2229
|
logger.break();
|
|
2231
2230
|
return null;
|
|
2232
2231
|
};
|
|
@@ -2279,13 +2278,13 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
2279
2278
|
} catch (error) {
|
|
2280
2279
|
didLintFail = true;
|
|
2281
2280
|
if (!options.scoreOnly) {
|
|
2282
|
-
const
|
|
2283
|
-
if (
|
|
2281
|
+
const lintErrorChain = formatErrorChain(error);
|
|
2282
|
+
if (lintErrorChain.includes("native binding")) {
|
|
2284
2283
|
lintSpinner?.fail(`Lint checks failed — oxlint native binding not found (Node ${process.version}).`);
|
|
2285
2284
|
logger.dim(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`);
|
|
2286
2285
|
} else {
|
|
2287
2286
|
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
|
|
2288
|
-
logger.error(
|
|
2287
|
+
logger.error(lintErrorChain);
|
|
2289
2288
|
}
|
|
2290
2289
|
}
|
|
2291
2290
|
return [];
|
|
@@ -2301,7 +2300,7 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
2301
2300
|
didDeadCodeFail = true;
|
|
2302
2301
|
if (!options.scoreOnly) {
|
|
2303
2302
|
deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
|
|
2304
|
-
logger.error(
|
|
2303
|
+
logger.error(formatErrorChain(error));
|
|
2305
2304
|
}
|
|
2306
2305
|
return [];
|
|
2307
2306
|
}
|
|
@@ -2358,7 +2357,6 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
2358
2357
|
skippedChecks
|
|
2359
2358
|
};
|
|
2360
2359
|
};
|
|
2361
|
-
|
|
2362
2360
|
//#endregion
|
|
2363
2361
|
//#region src/utils/get-diff-files.ts
|
|
2364
2362
|
const getCurrentBranch = (directory) => {
|
|
@@ -2438,7 +2436,6 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
|
2438
2436
|
};
|
|
2439
2437
|
};
|
|
2440
2438
|
const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
|
|
2441
|
-
|
|
2442
2439
|
//#endregion
|
|
2443
2440
|
//#region src/utils/get-staged-files.ts
|
|
2444
2441
|
const getStagedFilePaths = (directory) => {
|
|
@@ -2500,7 +2497,6 @@ const materializeStagedFiles = (directory, stagedFiles, tempDirectory) => {
|
|
|
2500
2497
|
}
|
|
2501
2498
|
};
|
|
2502
2499
|
};
|
|
2503
|
-
|
|
2504
2500
|
//#endregion
|
|
2505
2501
|
//#region src/utils/handle-error.ts
|
|
2506
2502
|
const DEFAULT_HANDLE_ERROR_OPTIONS = { shouldExit: true };
|
|
@@ -2514,7 +2510,6 @@ const handleError = (error, options = DEFAULT_HANDLE_ERROR_OPTIONS) => {
|
|
|
2514
2510
|
if (options.shouldExit) process.exit(1);
|
|
2515
2511
|
process.exitCode = 1;
|
|
2516
2512
|
};
|
|
2517
|
-
|
|
2518
2513
|
//#endregion
|
|
2519
2514
|
//#region src/utils/select-projects.ts
|
|
2520
2515
|
const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
@@ -2562,10 +2557,9 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
2562
2557
|
});
|
|
2563
2558
|
return selectedDirectories;
|
|
2564
2559
|
};
|
|
2565
|
-
|
|
2566
2560
|
//#endregion
|
|
2567
2561
|
//#region src/cli.ts
|
|
2568
|
-
const VERSION = "0.0.
|
|
2562
|
+
const VERSION = "0.0.42";
|
|
2569
2563
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2570
2564
|
"error",
|
|
2571
2565
|
"warning",
|
|
@@ -2734,7 +2728,7 @@ const main$1 = async () => {
|
|
|
2734
2728
|
await program.parseAsync();
|
|
2735
2729
|
};
|
|
2736
2730
|
main$1();
|
|
2737
|
-
|
|
2738
2731
|
//#endregion
|
|
2739
|
-
export {
|
|
2732
|
+
export {};
|
|
2733
|
+
|
|
2740
2734
|
//# sourceMappingURL=cli.js.map
|