react-doctor 0.0.16 → 0.0.18

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 CHANGED
@@ -242,23 +242,34 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
242
242
  if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
243
243
  return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry)).filter((entryPath) => fs.statSync(entryPath).isDirectory() && fs.existsSync(path.join(entryPath, "package.json")));
244
244
  };
245
- const findDependencyInfoFromAncestors = (startDirectory) => {
245
+ const isMonorepoRoot = (directory) => {
246
+ if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
247
+ const packageJsonPath = path.join(directory, "package.json");
248
+ if (!fs.existsSync(packageJsonPath)) return false;
249
+ const packageJson = readPackageJson(packageJsonPath);
250
+ return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
251
+ };
252
+ const findMonorepoRoot$1 = (startDirectory) => {
246
253
  let currentDirectory = path.dirname(startDirectory);
247
- const result = {
248
- reactVersion: null,
249
- framework: "unknown"
250
- };
251
254
  while (currentDirectory !== path.dirname(currentDirectory)) {
252
- const packageJsonPath = path.join(currentDirectory, "package.json");
253
- if (fs.existsSync(packageJsonPath)) {
254
- const info = extractDependencyInfo(readPackageJson(packageJsonPath));
255
- if (!result.reactVersion && info.reactVersion) result.reactVersion = info.reactVersion;
256
- if (result.framework === "unknown" && info.framework !== "unknown") result.framework = info.framework;
257
- if (result.reactVersion && result.framework !== "unknown") return result;
258
- }
255
+ if (isMonorepoRoot(currentDirectory)) return currentDirectory;
259
256
  currentDirectory = path.dirname(currentDirectory);
260
257
  }
261
- return result;
258
+ return null;
259
+ };
260
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
261
+ const monorepoRoot = findMonorepoRoot$1(directory);
262
+ if (!monorepoRoot) return {
263
+ reactVersion: null,
264
+ framework: "unknown"
265
+ };
266
+ const rootPackageJson = readPackageJson(path.join(monorepoRoot, "package.json"));
267
+ const rootInfo = extractDependencyInfo(rootPackageJson);
268
+ const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
269
+ return {
270
+ reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
271
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
272
+ };
262
273
  };
263
274
  const findReactInWorkspaces = (rootDirectory, packageJson) => {
264
275
  const patterns = getWorkspacePatterns(rootDirectory, packageJson);
@@ -355,10 +366,10 @@ const discoverProject = (directory) => {
355
366
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
356
367
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
357
368
  }
358
- if (!reactVersion || framework === "unknown") {
359
- const ancestorInfo = findDependencyInfoFromAncestors(directory);
360
- if (!reactVersion) reactVersion = ancestorInfo.reactVersion;
361
- if (framework === "unknown") framework = ancestorInfo.framework;
369
+ if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
370
+ const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
371
+ if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
372
+ if (framework === "unknown") framework = monorepoInfo.framework;
362
373
  }
363
374
  const projectName = packageJson.name ?? path.basename(directory);
364
375
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
@@ -474,15 +485,18 @@ const silenced = async (fn) => {
474
485
  const originalLog = console.log;
475
486
  const originalInfo = console.info;
476
487
  const originalWarn = console.warn;
488
+ const originalError = console.error;
477
489
  console.log = () => {};
478
490
  console.info = () => {};
479
491
  console.warn = () => {};
492
+ console.error = () => {};
480
493
  try {
481
494
  return await fn();
482
495
  } finally {
483
496
  console.log = originalLog;
484
497
  console.info = originalInfo;
485
498
  console.warn = originalWarn;
499
+ console.error = originalError;
486
500
  }
487
501
  };
488
502
  const findMonorepoRoot = (directory) => {
@@ -498,13 +512,26 @@ const findMonorepoRoot = (directory) => {
498
512
  }
499
513
  return null;
500
514
  };
515
+ const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
516
+ const extractFailedPluginName = (error) => {
517
+ return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
518
+ };
519
+ const MAX_KNIP_RETRIES = 5;
501
520
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
502
521
  const options = await silenced(() => createOptions({
503
522
  cwd: knipCwd,
504
523
  isShowProgress: false,
505
524
  ...workspaceName ? { workspace: workspaceName } : {}
506
525
  }));
507
- return await silenced(() => main(options));
526
+ const parsedConfig = options.parsedConfig;
527
+ for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
528
+ return await silenced(() => main(options));
529
+ } catch (error) {
530
+ const failedPlugin = extractFailedPluginName(error);
531
+ if (!failedPlugin || attempt === MAX_KNIP_RETRIES) throw error;
532
+ parsedConfig[failedPlugin] = false;
533
+ }
534
+ throw new Error("Unreachable");
508
535
  };
509
536
  const hasNodeModules = (directory) => {
510
537
  const nodeModulesPath = path.join(directory, "node_modules");
@@ -1161,8 +1188,9 @@ const scan = async (directory, options) => {
1161
1188
  const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler);
1162
1189
  lintSpinner?.succeed("Running lint checks.");
1163
1190
  return lintDiagnostics;
1164
- } catch {
1191
+ } catch (error) {
1165
1192
  lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
1193
+ logger.error(String(error));
1166
1194
  return [];
1167
1195
  }
1168
1196
  })() : Promise.resolve([]);
@@ -1172,8 +1200,9 @@ const scan = async (directory, options) => {
1172
1200
  const knipDiagnostics = await runKnip(directory);
1173
1201
  deadCodeSpinner?.succeed("Detecting dead code.");
1174
1202
  return knipDiagnostics;
1175
- } catch {
1203
+ } catch (error) {
1176
1204
  deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
1205
+ logger.error(String(error));
1177
1206
  return [];
1178
1207
  }
1179
1208
  })() : Promise.resolve([]);
@@ -1203,6 +1232,14 @@ const scan = async (directory, options) => {
1203
1232
  printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, projectInfo.sourceFileCount);
1204
1233
  };
1205
1234
 
1235
+ //#endregion
1236
+ //#region src/utils/should-auto-select-current-choice.ts
1237
+ const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
1238
+ if (choiceStates.some((choiceState) => choiceState.selected)) return false;
1239
+ const currentChoice = choiceStates[cursor];
1240
+ return Boolean(currentChoice) && !currentChoice.disabled;
1241
+ };
1242
+
1206
1243
  //#endregion
1207
1244
  //#region src/utils/should-select-all-choices.ts
1208
1245
  const shouldSelectAllChoices = (choiceStates) => {
@@ -1214,6 +1251,7 @@ const shouldSelectAllChoices = (choiceStates) => {
1214
1251
  const require = createRequire(import.meta.url);
1215
1252
  const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
1216
1253
  let didPatchMultiselectToggleAll = false;
1254
+ let didPatchMultiselectSubmit = false;
1217
1255
  const onCancel = () => {
1218
1256
  logger.break();
1219
1257
  logger.log("Cancelled.");
@@ -1238,8 +1276,19 @@ const patchMultiselectToggleAll = () => {
1238
1276
  this.render();
1239
1277
  };
1240
1278
  };
1279
+ const patchMultiselectSubmit = () => {
1280
+ if (didPatchMultiselectSubmit) return;
1281
+ didPatchMultiselectSubmit = true;
1282
+ const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
1283
+ const originalSubmit = multiselectPromptConstructor.prototype.submit;
1284
+ multiselectPromptConstructor.prototype.submit = function() {
1285
+ if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
1286
+ originalSubmit.call(this);
1287
+ };
1288
+ };
1241
1289
  const prompts = (questions) => {
1242
1290
  patchMultiselectToggleAll();
1291
+ patchMultiselectSubmit();
1243
1292
  return basePrompts(questions, { onCancel });
1244
1293
  };
1245
1294
 
@@ -1414,7 +1463,7 @@ const copyToClipboard = (text) => {
1414
1463
 
1415
1464
  //#endregion
1416
1465
  //#region src/cli.ts
1417
- const VERSION = "0.0.16";
1466
+ const VERSION = "0.0.18";
1418
1467
  process.on("SIGINT", () => process.exit(0));
1419
1468
  process.on("SIGTERM", () => process.exit(0));
1420
1469
  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").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {