react-doctor 0.0.41 → 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/cli.js CHANGED
@@ -13,7 +13,6 @@ 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
18
17
  const AGENTS_SKILL_DIR = ".agents/skills";
19
18
  const SUPPORTED_AGENTS = {
@@ -75,7 +74,6 @@ const isCommandAvailable = (command) => {
75
74
  const detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
76
75
  const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
77
76
  const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
78
-
79
77
  //#endregion
80
78
  //#region src/utils/highlighter.ts
81
79
  const highlighter = {
@@ -85,7 +83,6 @@ const highlighter = {
85
83
  success: pc.green,
86
84
  dim: pc.dim
87
85
  };
88
-
89
86
  //#endregion
90
87
  //#region src/utils/install-skill-for-agent.ts
91
88
  const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName, alreadyInstalledDirectories) => {
@@ -99,7 +96,6 @@ const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillNam
99
96
  cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
100
97
  return installedSkillDirectory;
101
98
  };
102
-
103
99
  //#endregion
104
100
  //#region src/utils/logger.ts
105
101
  const logger = {
@@ -125,7 +121,6 @@ const logger = {
125
121
  console.log("");
126
122
  }
127
123
  };
128
-
129
124
  //#endregion
130
125
  //#region src/utils/should-auto-select-current-choice.ts
131
126
  const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
@@ -133,13 +128,11 @@ const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
133
128
  const currentChoice = choiceStates[cursor];
134
129
  return Boolean(currentChoice) && !currentChoice.disabled;
135
130
  };
136
-
137
131
  //#endregion
138
132
  //#region src/utils/should-select-all-choices.ts
139
133
  const shouldSelectAllChoices = (choiceStates) => {
140
134
  return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
141
135
  };
142
-
143
136
  //#endregion
144
137
  //#region src/utils/prompts.ts
145
138
  const require = createRequire(import.meta.url);
@@ -205,7 +198,6 @@ const prompts = (questions) => {
205
198
  patchSelectBanner();
206
199
  return basePrompts(questions, { onCancel });
207
200
  };
208
-
209
201
  //#endregion
210
202
  //#region src/utils/spinner.ts
211
203
  let sharedInstance = null;
@@ -236,7 +228,6 @@ const spinner = (text) => ({ start() {
236
228
  fail: (displayText) => finalize("fail", text, displayText)
237
229
  };
238
230
  } });
239
-
240
231
  //#endregion
241
232
  //#region src/install-skill.ts
242
233
  const SKILL_NAME = "react-doctor";
@@ -280,32 +271,30 @@ const runInstallSkill = async (options = {}) => {
280
271
  }
281
272
  installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
282
273
  };
283
-
284
274
  //#endregion
285
275
  //#region src/constants.ts
286
276
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
287
277
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
288
278
  const MILLISECONDS_PER_SECOND = 1e3;
289
- const ERROR_PREVIEW_LENGTH_CHARS = 200;
290
- const PERFECT_SCORE = 100;
291
- const SCORE_GOOD_THRESHOLD = 75;
292
- const SCORE_OK_THRESHOLD = 50;
293
- const SCORE_BAR_WIDTH_CHARS = 50;
294
- const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
295
- const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
296
279
  const SCORE_API_URL = "https://www.react.doctor/api/score";
297
280
  const SHARE_BASE_URL = "https://www.react.doctor/share";
298
281
  const FETCH_TIMEOUT_MS = 1e4;
299
282
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
300
- const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
301
- const OXLINT_MAX_FILES_PER_BATCH = 500;
302
283
  const OFFLINE_MESSAGE = "Score calculated locally (offline mode).";
303
284
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
304
285
  const ERROR_RULE_PENALTY = 1.5;
305
286
  const WARNING_RULE_PENALTY = .75;
306
- const MAX_KNIP_RETRIES = 5;
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
+ ];
307
297
  const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
308
- const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
309
298
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
310
299
  const IGNORED_DIRECTORIES = new Set([
311
300
  "node_modules",
@@ -313,12 +302,11 @@ const IGNORED_DIRECTORIES = new Set([
313
302
  "build",
314
303
  "coverage"
315
304
  ]);
316
-
317
305
  //#endregion
318
306
  //#region src/core/calculate-score-locally.ts
319
307
  const getScoreLabel = (score) => {
320
- if (score >= SCORE_GOOD_THRESHOLD) return "Great";
321
- if (score >= SCORE_OK_THRESHOLD) return "Needs work";
308
+ if (score >= 75) return "Great";
309
+ if (score >= 50) return "Needs work";
322
310
  return "Critical";
323
311
  };
324
312
  const countUniqueRules = (diagnostics) => {
@@ -336,7 +324,7 @@ const countUniqueRules = (diagnostics) => {
336
324
  };
337
325
  const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
338
326
  const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
339
- return Math.max(0, Math.round(PERFECT_SCORE - penalty));
327
+ return Math.max(0, Math.round(100 - penalty));
340
328
  };
341
329
  const calculateScoreLocally = (diagnostics) => {
342
330
  const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
@@ -346,7 +334,6 @@ const calculateScoreLocally = (diagnostics) => {
346
334
  label: getScoreLabel(score)
347
335
  };
348
336
  };
349
-
350
337
  //#endregion
351
338
  //#region src/core/try-score-from-api.ts
352
339
  const parseScoreResult = (value) => {
@@ -378,7 +365,6 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
378
365
  clearTimeout(timeoutId);
379
366
  }
380
367
  };
381
-
382
368
  //#endregion
383
369
  //#region src/utils/proxy-fetch.ts
384
370
  const getGlobalProcess = () => {
@@ -421,7 +407,6 @@ const proxyFetch = async (url, init) => {
421
407
  clearTimeout(timeoutId);
422
408
  }
423
409
  };
424
-
425
410
  //#endregion
426
411
  //#region src/utils/calculate-score-node.ts
427
412
  const calculateScore = async (diagnostics) => {
@@ -429,19 +414,16 @@ const calculateScore = async (diagnostics) => {
429
414
  if (apiScore) return apiScore;
430
415
  return calculateScoreLocally(diagnostics);
431
416
  };
432
-
433
417
  //#endregion
434
418
  //#region src/utils/colorize-by-score.ts
435
419
  const colorizeByScore = (text, score) => {
436
- if (score >= SCORE_GOOD_THRESHOLD) return highlighter.success(text);
437
- if (score >= SCORE_OK_THRESHOLD) return highlighter.warn(text);
420
+ if (score >= 75) return highlighter.success(text);
421
+ if (score >= 50) return highlighter.warn(text);
438
422
  return highlighter.error(text);
439
423
  };
440
-
441
424
  //#endregion
442
425
  //#region src/plugin/constants.ts
443
426
  const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
444
-
445
427
  //#endregion
446
428
  //#region src/utils/is-file.ts
447
429
  const isFile = (filePath) => {
@@ -451,7 +433,6 @@ const isFile = (filePath) => {
451
433
  return false;
452
434
  }
453
435
  };
454
-
455
436
  //#endregion
456
437
  //#region src/utils/read-package-json.ts
457
438
  const readPackageJson = (packageJsonPath) => {
@@ -466,7 +447,6 @@ const readPackageJson = (packageJsonPath) => {
466
447
  throw error;
467
448
  }
468
449
  };
469
-
470
450
  //#endregion
471
451
  //#region src/utils/check-reduced-motion.ts
472
452
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
@@ -508,7 +488,6 @@ const checkReducedMotion = (rootDirectory) => {
508
488
  return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
509
489
  }
510
490
  };
511
-
512
491
  //#endregion
513
492
  //#region src/utils/read-file-lines-node.ts
514
493
  const createNodeReadFileLinesSync = (rootDirectory) => {
@@ -521,7 +500,6 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
521
500
  }
522
501
  };
523
502
  };
524
-
525
503
  //#endregion
526
504
  //#region src/utils/match-glob-pattern.ts
527
505
  const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
@@ -549,7 +527,6 @@ const compileGlobPattern = (pattern) => {
549
527
  regexSource += "$";
550
528
  return new RegExp(regexSource);
551
529
  };
552
-
553
530
  //#endregion
554
531
  //#region src/utils/is-ignored-file.ts
555
532
  const toRelativePath = (filePath, rootDirectory) => {
@@ -564,7 +541,6 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
564
541
  const relativePath = toRelativePath(filePath, rootDirectory);
565
542
  return patterns.some((pattern) => pattern.test(relativePath));
566
543
  };
567
-
568
544
  //#endregion
569
545
  //#region src/utils/filter-diagnostics.ts
570
546
  const resolveCandidateReadPath = (rootDirectory, filePath) => {
@@ -638,17 +614,14 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
638
614
  return true;
639
615
  });
640
616
  };
641
-
642
617
  //#endregion
643
618
  //#region src/utils/merge-and-filter-diagnostics.ts
644
619
  const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
645
620
  return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
646
621
  };
647
-
648
622
  //#endregion
649
623
  //#region src/utils/jsx-include-paths.ts
650
624
  const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
651
-
652
625
  //#endregion
653
626
  //#region src/utils/combine-diagnostics.ts
654
627
  const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true) => {
@@ -659,7 +632,6 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
659
632
  ...extraDiagnostics
660
633
  ], directory, userConfig, readFileLinesSync);
661
634
  };
662
-
663
635
  //#endregion
664
636
  //#region src/utils/find-monorepo-root.ts
665
637
  const isMonorepoRoot = (directory) => {
@@ -678,11 +650,9 @@ const findMonorepoRoot = (startDirectory) => {
678
650
  }
679
651
  return null;
680
652
  };
681
-
682
653
  //#endregion
683
654
  //#region src/utils/is-plain-object.ts
684
655
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
685
-
686
656
  //#endregion
687
657
  //#region src/utils/discover-project.ts
688
658
  const REACT_COMPILER_PACKAGES = new Set([
@@ -1103,7 +1073,22 @@ const discoverProject = (directory) => {
1103
1073
  sourceFileCount
1104
1074
  };
1105
1075
  };
1106
-
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);
1107
1092
  //#endregion
1108
1093
  //#region src/utils/framed-box.ts
1109
1094
  const createFramedLine = (plainText, renderedText = plainText) => ({
@@ -1113,10 +1098,10 @@ const createFramedLine = (plainText, renderedText = plainText) => ({
1113
1098
  const renderFramedBoxString = (framedLines) => {
1114
1099
  if (framedLines.length === 0) return "";
1115
1100
  const borderColorizer = highlighter.dim;
1116
- const outerIndent = " ".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);
1117
- const horizontalPadding = " ".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);
1101
+ const outerIndent = " ".repeat(2);
1102
+ const horizontalPadding = " ".repeat(1);
1118
1103
  const maximumLineLength = Math.max(...framedLines.map((framedLine) => framedLine.plainText.length));
1119
- const borderLine = "─".repeat(maximumLineLength + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2);
1104
+ const borderLine = "─".repeat(maximumLineLength + 2);
1120
1105
  const lines = [];
1121
1106
  lines.push(`${outerIndent}${borderColorizer(`┌${borderLine}┐`)}`);
1122
1107
  for (const framedLine of framedLines) {
@@ -1130,7 +1115,6 @@ const printFramedBox = (framedLines) => {
1130
1115
  const rendered = renderFramedBoxString(framedLines);
1131
1116
  if (rendered) logger.log(rendered);
1132
1117
  };
1133
-
1134
1118
  //#endregion
1135
1119
  //#region src/utils/group-by.ts
1136
1120
  const groupBy = (items, keyFn) => {
@@ -1143,11 +1127,9 @@ const groupBy = (items, keyFn) => {
1143
1127
  }
1144
1128
  return groups;
1145
1129
  };
1146
-
1147
1130
  //#endregion
1148
1131
  //#region src/utils/indent-multiline-text.ts
1149
1132
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
1150
-
1151
1133
  //#endregion
1152
1134
  //#region src/utils/load-config.ts
1153
1135
  const CONFIG_FILENAME = "react-doctor.config.json";
@@ -1183,7 +1165,6 @@ const loadConfig = (rootDirectory) => {
1183
1165
  }
1184
1166
  return null;
1185
1167
  };
1186
-
1187
1168
  //#endregion
1188
1169
  //#region src/utils/resolve-compatible-node.ts
1189
1170
  const parseNodeVersion = (versionString) => {
@@ -1236,7 +1217,7 @@ const installNodeViaNvm = () => {
1236
1217
  const nvmScript = path.join(nvmDirectory, "nvm.sh");
1237
1218
  if (!existsSync(nvmScript)) return false;
1238
1219
  try {
1239
- execSync(`bash -c ". '${nvmScript}' && nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}"`, { stdio: "inherit" });
1220
+ execSync(`bash -c ". '${nvmScript}' && nvm install 24"`, { stdio: "inherit" });
1240
1221
  return findCompatibleNvmBinary() !== null;
1241
1222
  } catch {
1242
1223
  return false;
@@ -1258,7 +1239,6 @@ const resolveNodeForOxlint = () => {
1258
1239
  version
1259
1240
  };
1260
1241
  };
1261
-
1262
1242
  //#endregion
1263
1243
  //#region src/utils/resolve-lint-include-paths.ts
1264
1244
  const listSourceFilesViaGit = (rootDirectory) => {
@@ -1301,7 +1281,6 @@ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
1301
1281
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
1302
1282
  });
1303
1283
  };
1304
-
1305
1284
  //#endregion
1306
1285
  //#region src/utils/collect-unused-file-paths.ts
1307
1286
  const collectUnusedFilePaths = (filesIssues) => {
@@ -1315,7 +1294,19 @@ const collectUnusedFilePaths = (filesIssues) => {
1315
1294
  }
1316
1295
  return unusedFilePaths;
1317
1296
  };
1318
-
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)));
1319
1310
  //#endregion
1320
1311
  //#region src/utils/run-knip.ts
1321
1312
  const KNIP_CATEGORY_MAP = {
@@ -1370,12 +1361,15 @@ const silenced = async (fn) => {
1370
1361
  console.error = originalError;
1371
1362
  }
1372
1363
  };
1373
- const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
1374
- const extractFailedPluginName = (error) => {
1375
- return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
1376
- };
1377
1364
  const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
1378
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
+ };
1379
1373
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
1380
1374
  const tsConfigFile = resolveTsConfigFile(knipCwd);
1381
1375
  const options = await silenced(() => createOptions({
@@ -1385,33 +1379,36 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
1385
1379
  ...tsConfigFile ? { tsConfigFile } : {}
1386
1380
  }));
1387
1381
  const parsedConfig = options.parsedConfig;
1388
- for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
1382
+ const disabledPlugins = /* @__PURE__ */ new Set();
1383
+ let lastKnipError;
1384
+ for (let attempt = 0; attempt <= 5; attempt++) try {
1389
1385
  return await silenced(() => main(options));
1390
1386
  } catch (error) {
1391
- const failedPlugin = extractFailedPluginName(error);
1392
- if (!failedPlugin || attempt === MAX_KNIP_RETRIES) throw error;
1393
- parsedConfig[failedPlugin] = false;
1387
+ lastKnipError = error;
1388
+ if (!tryDisableFailedPlugin(error, parsedConfig, disabledPlugins)) throw error;
1394
1389
  }
1395
- throw new Error("Unreachable");
1390
+ throw lastKnipError;
1396
1391
  };
1397
1392
  const hasNodeModules = (directory) => {
1398
1393
  const nodeModulesPath = path.join(directory, "node_modules");
1399
1394
  return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
1400
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
+ };
1401
1408
  const runKnip = async (rootDirectory) => {
1402
1409
  const monorepoRoot = findMonorepoRoot(rootDirectory);
1403
1410
  if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
1404
- let knipResult;
1405
- if (monorepoRoot) {
1406
- const packageJsonPath = path.join(rootDirectory, "package.json");
1407
- const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
1408
- try {
1409
- knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
1410
- } catch {
1411
- knipResult = await runKnipWithOptions(rootDirectory);
1412
- }
1413
- } else knipResult = await runKnipWithOptions(rootDirectory);
1414
- const { issues } = knipResult;
1411
+ const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
1415
1412
  const diagnostics = [];
1416
1413
  for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
1417
1414
  filePath: path.relative(rootDirectory, unusedFilePath),
@@ -1432,7 +1429,6 @@ const runKnip = async (rootDirectory) => {
1432
1429
  ]) diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));
1433
1430
  return diagnostics;
1434
1431
  };
1435
-
1436
1432
  //#endregion
1437
1433
  //#region src/oxlint-config.ts
1438
1434
  const esmRequire$1 = createRequire(import.meta.url);
@@ -1616,7 +1612,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
1616
1612
  ...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
1617
1613
  }
1618
1614
  });
1619
-
1620
1615
  //#endregion
1621
1616
  //#region src/utils/neutralize-disable-directives.ts
1622
1617
  const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
@@ -1659,7 +1654,6 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
1659
1654
  for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
1660
1655
  };
1661
1656
  };
1662
-
1663
1657
  //#endregion
1664
1658
  //#region src/utils/run-oxlint.ts
1665
1659
  const esmRequire = createRequire(import.meta.url);
@@ -1919,8 +1913,8 @@ const batchIncludePaths = (baseArgs, includePaths) => {
1919
1913
  let currentBatchLength = baseArgsLength;
1920
1914
  for (const filePath of includePaths) {
1921
1915
  const entryLength = filePath.length + 1;
1922
- const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS;
1923
- const exceedsFileCount = currentBatch.length >= OXLINT_MAX_FILES_PER_BATCH;
1916
+ const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
1917
+ const exceedsFileCount = currentBatch.length >= 500;
1924
1918
  if (exceedsArgLength || exceedsFileCount) {
1925
1919
  batches.push(currentBatch);
1926
1920
  currentBatch = [];
@@ -1964,7 +1958,7 @@ const parseOxlintOutput = (stdout) => {
1964
1958
  try {
1965
1959
  output = JSON.parse(stdout);
1966
1960
  } catch {
1967
- throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
1961
+ throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
1968
1962
  }
1969
1963
  return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
1970
1964
  const { plugin, rule } = parseRuleCode(diagnostic.code);
@@ -2015,7 +2009,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
2015
2009
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
2016
2010
  }
2017
2011
  };
2018
-
2019
2012
  //#endregion
2020
2013
  //#region src/scan.ts
2021
2014
  const SEVERITY_ORDER = {
@@ -2054,7 +2047,7 @@ const printDiagnostics = (diagnostics, isVerbose) => {
2054
2047
  }
2055
2048
  };
2056
2049
  const formatElapsedTime = (elapsedMilliseconds) => {
2057
- if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) return `${Math.round(elapsedMilliseconds)}ms`;
2050
+ if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
2058
2051
  return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
2059
2052
  };
2060
2053
  const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
@@ -2083,8 +2076,8 @@ const writeDiagnosticsDirectory = (diagnostics) => {
2083
2076
  return outputDirectory;
2084
2077
  };
2085
2078
  const buildScoreBarSegments = (score) => {
2086
- const filledCount = Math.round(score / PERFECT_SCORE * SCORE_BAR_WIDTH_CHARS);
2087
- const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;
2079
+ const filledCount = Math.round(score / 100 * 50);
2080
+ const emptyCount = 50 - filledCount;
2088
2081
  return {
2089
2082
  filledSegment: "█".repeat(filledCount),
2090
2083
  emptySegment: "░".repeat(emptyCount)
@@ -2101,14 +2094,14 @@ const buildScoreBar = (score) => {
2101
2094
  const printScoreGauge = (score, label) => {
2102
2095
  const scoreDisplay = colorizeByScore(`${score}`, score);
2103
2096
  const labelDisplay = colorizeByScore(label, score);
2104
- logger.log(` ${scoreDisplay} / ${PERFECT_SCORE} ${labelDisplay}`);
2097
+ logger.log(` ${scoreDisplay} / 100 ${labelDisplay}`);
2105
2098
  logger.break();
2106
2099
  logger.log(` ${buildScoreBar(score)}`);
2107
2100
  logger.break();
2108
2101
  };
2109
2102
  const getDoctorFace = (score) => {
2110
- if (score >= SCORE_GOOD_THRESHOLD) return ["◠ ◠", " ▽ "];
2111
- if (score >= SCORE_OK_THRESHOLD) return ["• •", " ─ "];
2103
+ if (score >= 75) return ["◠ ◠", " ▽ "];
2104
+ if (score >= 50) return ["• •", " ─ "];
2112
2105
  return ["x x", " ▽ "];
2113
2106
  };
2114
2107
  const printBranding = (score) => {
@@ -2146,8 +2139,8 @@ const buildBrandingLines = (scoreResult, noScoreMessage) => {
2146
2139
  lines.push(createFramedLine("└─────┘", scoreColorizer("└─────┘")));
2147
2140
  lines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
2148
2141
  lines.push(createFramedLine(""));
2149
- const scoreLinePlainText = `${scoreResult.score} / ${PERFECT_SCORE} ${scoreResult.label}`;
2150
- const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / ${PERFECT_SCORE} ${colorizeByScore(scoreResult.label, 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)}`;
2151
2144
  lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));
2152
2145
  lines.push(createFramedLine(""));
2153
2146
  lines.push(createFramedLine(buildPlainScoreBar(scoreResult.score), buildScoreBar(scoreResult.score)));
@@ -2214,7 +2207,7 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
2214
2207
  const { shouldInstallNode } = await prompts({
2215
2208
  type: "confirm",
2216
2209
  name: "shouldInstallNode",
2217
- message: `Install Node ${OXLINT_RECOMMENDED_NODE_MAJOR} via nvm to enable lint checks?`,
2210
+ message: `Install Node 24 via nvm to enable lint checks?`,
2218
2211
  initial: true
2219
2212
  });
2220
2213
  if (shouldInstallNode) {
@@ -2231,8 +2224,8 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
2231
2224
  logger.break();
2232
2225
  return null;
2233
2226
  }
2234
- } else if (isNvmInstalled()) logger.dim(` Run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
2235
- else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
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`);
2236
2229
  logger.break();
2237
2230
  return null;
2238
2231
  };
@@ -2285,13 +2278,13 @@ const scan = async (directory, inputOptions = {}) => {
2285
2278
  } catch (error) {
2286
2279
  didLintFail = true;
2287
2280
  if (!options.scoreOnly) {
2288
- const errorMessage = error instanceof Error ? error.message : String(error);
2289
- if (errorMessage.includes("native binding")) {
2281
+ const lintErrorChain = formatErrorChain(error);
2282
+ if (lintErrorChain.includes("native binding")) {
2290
2283
  lintSpinner?.fail(`Lint checks failed — oxlint native binding not found (Node ${process.version}).`);
2291
2284
  logger.dim(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`);
2292
2285
  } else {
2293
2286
  lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
2294
- logger.error(errorMessage);
2287
+ logger.error(lintErrorChain);
2295
2288
  }
2296
2289
  }
2297
2290
  return [];
@@ -2307,7 +2300,7 @@ const scan = async (directory, inputOptions = {}) => {
2307
2300
  didDeadCodeFail = true;
2308
2301
  if (!options.scoreOnly) {
2309
2302
  deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
2310
- logger.error(String(error));
2303
+ logger.error(formatErrorChain(error));
2311
2304
  }
2312
2305
  return [];
2313
2306
  }
@@ -2364,7 +2357,6 @@ const scan = async (directory, inputOptions = {}) => {
2364
2357
  skippedChecks
2365
2358
  };
2366
2359
  };
2367
-
2368
2360
  //#endregion
2369
2361
  //#region src/utils/get-diff-files.ts
2370
2362
  const getCurrentBranch = (directory) => {
@@ -2444,7 +2436,6 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
2444
2436
  };
2445
2437
  };
2446
2438
  const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
2447
-
2448
2439
  //#endregion
2449
2440
  //#region src/utils/get-staged-files.ts
2450
2441
  const getStagedFilePaths = (directory) => {
@@ -2506,7 +2497,6 @@ const materializeStagedFiles = (directory, stagedFiles, tempDirectory) => {
2506
2497
  }
2507
2498
  };
2508
2499
  };
2509
-
2510
2500
  //#endregion
2511
2501
  //#region src/utils/handle-error.ts
2512
2502
  const DEFAULT_HANDLE_ERROR_OPTIONS = { shouldExit: true };
@@ -2520,7 +2510,6 @@ const handleError = (error, options = DEFAULT_HANDLE_ERROR_OPTIONS) => {
2520
2510
  if (options.shouldExit) process.exit(1);
2521
2511
  process.exitCode = 1;
2522
2512
  };
2523
-
2524
2513
  //#endregion
2525
2514
  //#region src/utils/select-projects.ts
2526
2515
  const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
@@ -2568,10 +2557,9 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
2568
2557
  });
2569
2558
  return selectedDirectories;
2570
2559
  };
2571
-
2572
2560
  //#endregion
2573
2561
  //#region src/cli.ts
2574
- const VERSION = "0.0.41";
2562
+ const VERSION = "0.0.42";
2575
2563
  const VALID_FAIL_ON_LEVELS = new Set([
2576
2564
  "error",
2577
2565
  "warning",
@@ -2740,7 +2728,7 @@ const main$1 = async () => {
2740
2728
  await program.parseAsync();
2741
2729
  };
2742
2730
  main$1();
2743
-
2744
2731
  //#endregion
2745
- export { };
2732
+ export {};
2733
+
2746
2734
  //# sourceMappingURL=cli.js.map