Skip to content

Commit 007f755

Browse files
committed
fix
1 parent 64d837b commit 007f755

File tree

5 files changed

+148
-24
lines changed

5 files changed

+148
-24
lines changed

packages/react-doctor/src/utils/discover-project.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,32 +165,38 @@ const resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): st
165165
);
166166
};
167167

168-
const findDependencyInfoFromAncestors = (startDirectory: string): DependencyInfo => {
168+
const isMonorepoRoot = (directory: string): boolean => {
169+
if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
170+
const packageJsonPath = path.join(directory, "package.json");
171+
if (!fs.existsSync(packageJsonPath)) return false;
172+
const packageJson = readPackageJson(packageJsonPath);
173+
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
174+
};
175+
176+
const findMonorepoRoot = (startDirectory: string): string | null => {
169177
let currentDirectory = path.dirname(startDirectory);
170-
const result: DependencyInfo = { reactVersion: null, framework: "unknown" };
171178

172179
while (currentDirectory !== path.dirname(currentDirectory)) {
173-
const packageJsonPath = path.join(currentDirectory, "package.json");
174-
if (fs.existsSync(packageJsonPath)) {
175-
const packageJson = readPackageJson(packageJsonPath);
176-
const info = extractDependencyInfo(packageJson);
180+
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
181+
currentDirectory = path.dirname(currentDirectory);
182+
}
177183

178-
if (!result.reactVersion && info.reactVersion) {
179-
result.reactVersion = info.reactVersion;
180-
}
181-
if (result.framework === "unknown" && info.framework !== "unknown") {
182-
result.framework = info.framework;
183-
}
184+
return null;
185+
};
184186

185-
if (result.reactVersion && result.framework !== "unknown") {
186-
return result;
187-
}
188-
}
187+
const findDependencyInfoFromMonorepoRoot = (directory: string): DependencyInfo => {
188+
const monorepoRoot = findMonorepoRoot(directory);
189+
if (!monorepoRoot) return { reactVersion: null, framework: "unknown" };
189190

190-
currentDirectory = path.dirname(currentDirectory);
191-
}
191+
const rootPackageJson = readPackageJson(path.join(monorepoRoot, "package.json"));
192+
const rootInfo = extractDependencyInfo(rootPackageJson);
193+
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
192194

193-
return result;
195+
return {
196+
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
197+
framework:
198+
rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework,
199+
};
194200
};
195201

196202
const findReactInWorkspaces = (rootDirectory: string, packageJson: PackageJson): DependencyInfo => {
@@ -334,13 +340,13 @@ export const discoverProject = (directory: string): ProjectInfo => {
334340
}
335341
}
336342

337-
if (!reactVersion || framework === "unknown") {
338-
const ancestorInfo = findDependencyInfoFromAncestors(directory);
343+
if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
344+
const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
339345
if (!reactVersion) {
340-
reactVersion = ancestorInfo.reactVersion;
346+
reactVersion = monorepoInfo.reactVersion;
341347
}
342348
if (framework === "unknown") {
343-
framework = ancestorInfo.framework;
349+
framework = monorepoInfo.framework;
344350
}
345351
}
346352

packages/react-doctor/src/utils/prompts.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { createRequire } from "node:module";
22
import basePrompts, { type PromptObject, type Answers } from "prompts";
33
import type { PromptMultiselectContext } from "../types.js";
44
import { logger } from "./logger.js";
5+
import { shouldAutoSelectCurrentChoice } from "./should-auto-select-current-choice.js";
56
import { shouldSelectAllChoices } from "./should-select-all-choices.js";
67

78
const require = createRequire(import.meta.url);
89
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
910
let didPatchMultiselectToggleAll = false;
11+
let didPatchMultiselectSubmit = false;
1012

1113
const onCancel = () => {
1214
logger.break();
@@ -41,9 +43,25 @@ const patchMultiselectToggleAll = (): void => {
4143
};
4244
};
4345

46+
const patchMultiselectSubmit = (): void => {
47+
if (didPatchMultiselectSubmit) return;
48+
didPatchMultiselectSubmit = true;
49+
50+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
51+
const originalSubmit = multiselectPromptConstructor.prototype.submit;
52+
53+
multiselectPromptConstructor.prototype.submit = function (this: PromptMultiselectContext): void {
54+
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) {
55+
this.value[this.cursor].selected = true;
56+
}
57+
originalSubmit.call(this);
58+
};
59+
};
60+
4461
export const prompts = <T extends string = string>(
4562
questions: PromptObject<T> | PromptObject<T>[],
4663
): Promise<Answers<T>> => {
4764
patchMultiselectToggleAll();
65+
patchMultiselectSubmit();
4866
return basePrompts(questions, { onCancel });
4967
};

packages/react-doctor/src/utils/run-knip.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,18 @@ const silenced = async <T>(fn: () => Promise<T>): Promise<T> => {
5757
const originalLog = console.log;
5858
const originalInfo = console.info;
5959
const originalWarn = console.warn;
60+
const originalError = console.error;
6061
console.log = () => {};
6162
console.info = () => {};
6263
console.warn = () => {};
64+
console.error = () => {};
6365
try {
6466
return await fn();
6567
} finally {
6668
console.log = originalLog;
6769
console.info = originalInfo;
6870
console.warn = originalWarn;
71+
console.error = originalError;
6972
}
7073
};
7174

@@ -89,6 +92,15 @@ const findMonorepoRoot = (directory: string): string | null => {
8992
return null;
9093
};
9194

95+
const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
96+
97+
const extractFailedPluginName = (error: unknown): string | null => {
98+
const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);
99+
return match?.[1] ?? null;
100+
};
101+
102+
const MAX_KNIP_RETRIES = 5;
103+
92104
const runKnipWithOptions = async (
93105
knipCwd: string,
94106
workspaceName?: string,
@@ -100,7 +112,22 @@ const runKnipWithOptions = async (
100112
...(workspaceName ? { workspace: workspaceName } : {}),
101113
}),
102114
);
103-
return (await silenced(() => main(options))) as KnipResults;
115+
116+
const parsedConfig = options.parsedConfig as Record<string, unknown>;
117+
118+
for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {
119+
try {
120+
return (await silenced(() => main(options))) as KnipResults;
121+
} catch (error) {
122+
const failedPlugin = extractFailedPluginName(error);
123+
if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {
124+
throw error;
125+
}
126+
parsedConfig[failedPlugin] = false;
127+
}
128+
}
129+
130+
throw new Error("Unreachable");
104131
};
105132

106133
const hasNodeModules = (directory: string): boolean => {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { PromptMultiselectChoiceState } from "../types.js";
2+
3+
export const shouldAutoSelectCurrentChoice = (
4+
choiceStates: PromptMultiselectChoiceState[],
5+
cursor: number,
6+
): boolean => {
7+
const hasSelection = choiceStates.some((choiceState) => choiceState.selected);
8+
if (hasSelection) return false;
9+
10+
const currentChoice = choiceStates[cursor];
11+
return Boolean(currentChoice) && !currentChoice.disabled;
12+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it } from "vitest";
2+
import { shouldAutoSelectCurrentChoice } from "../src/utils/should-auto-select-current-choice.js";
3+
4+
describe("shouldAutoSelectCurrentChoice", () => {
5+
it("returns true when nothing is selected and cursor is on an enabled choice", () => {
6+
const result = shouldAutoSelectCurrentChoice(
7+
[{ selected: false }, { selected: false }, { selected: false }],
8+
1,
9+
);
10+
11+
expect(result).toBe(true);
12+
});
13+
14+
it("returns false when a choice is already selected", () => {
15+
const result = shouldAutoSelectCurrentChoice(
16+
[{ selected: true }, { selected: false }, { selected: false }],
17+
1,
18+
);
19+
20+
expect(result).toBe(false);
21+
});
22+
23+
it("returns false when cursor is on a disabled choice", () => {
24+
const result = shouldAutoSelectCurrentChoice(
25+
[{ selected: false }, { selected: false, disabled: true }],
26+
1,
27+
);
28+
29+
expect(result).toBe(false);
30+
});
31+
32+
it("returns false when all choices are disabled and nothing is selected", () => {
33+
const result = shouldAutoSelectCurrentChoice([{ disabled: true }, { disabled: true }], 0);
34+
35+
expect(result).toBe(false);
36+
});
37+
38+
it("returns false when cursor is out of bounds", () => {
39+
const result = shouldAutoSelectCurrentChoice([{ selected: false }, { selected: false }], 5);
40+
41+
expect(result).toBe(false);
42+
});
43+
44+
it("returns false when choice states array is empty", () => {
45+
const result = shouldAutoSelectCurrentChoice([], 0);
46+
47+
expect(result).toBe(false);
48+
});
49+
50+
it("returns true when selected is undefined on all choices", () => {
51+
const result = shouldAutoSelectCurrentChoice([{}, {}, {}], 0);
52+
53+
expect(result).toBe(true);
54+
});
55+
56+
it("returns false when cursor is negative", () => {
57+
const result = shouldAutoSelectCurrentChoice([{ selected: false }, { selected: false }], -1);
58+
59+
expect(result).toBe(false);
60+
});
61+
});

0 commit comments

Comments
 (0)