react-doctor 0.2.14-dev.6e59f10 → 0.2.14-dev.8b313ba

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
@@ -7726,6 +7726,28 @@ const collectIgnorePatterns = (rootDirectory) => {
7726
7726
  cachedPatternsByRoot.set(rootDirectory, patterns);
7727
7727
  return patterns;
7728
7728
  };
7729
+ /**
7730
+ * Resolves a path to its canonical, symlink-free form, falling back to
7731
+ * the input when it cannot be realpath'd (broken symlink, permission
7732
+ * error) so a best-effort normalization never throws.
7733
+ *
7734
+ * deslop's dead-code module graph is collected with `fast-glob` (which
7735
+ * keeps the scan root's symlinks intact) while imports are resolved
7736
+ * through `oxc-resolver` (which returns realpath'd targets). When the
7737
+ * project root sits behind a symlink — e.g. macOS iCloud-synced
7738
+ * `~/Documents` / `~/Desktop`, or a symlinked checkout — those two path
7739
+ * spaces diverge: every resolved import misses the graph and the files
7740
+ * they point at (commonly every `@/…` alias target) are mis-reported as
7741
+ * unreachable. Canonicalizing the root before the scan keeps both path
7742
+ * spaces in agreement.
7743
+ */
7744
+ const toCanonicalPath = (filePath) => {
7745
+ try {
7746
+ return fs.realpathSync(filePath);
7747
+ } catch {
7748
+ return filePath;
7749
+ }
7750
+ };
7729
7751
  const DEAD_CODE_PLUGIN = "deslop";
7730
7752
  const DEAD_CODE_CATEGORY = "Maintainability";
7731
7753
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
@@ -7982,7 +8004,8 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
7982
8004
  });
7983
8005
  });
7984
8006
  const checkDeadCode = async (options) => {
7985
- const { rootDirectory, userConfig } = options;
8007
+ const { userConfig } = options;
8008
+ const rootDirectory = toCanonicalPath(options.rootDirectory);
7986
8009
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
7987
8010
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
7988
8011
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
@@ -10824,6 +10847,20 @@ const groupBy = (items, keyFn) => {
10824
10847
  }
10825
10848
  return groups;
10826
10849
  };
10850
+ /**
10851
+ * Whether a diagnostic's rule has a published per-rule fix recipe at
10852
+ * `${PROMPTS_RULES_BASE_URL}/react-doctor/<rule>.md`
10853
+ * (see `buildRulePromptUrl`).
10854
+ *
10855
+ * Recipes are generated from react-doctor's own engine rules, so only
10856
+ * those resolve. Dead-code (`deslop`), the synthetic environment and
10857
+ * supply-chain checks (`require-reduced-motion`, `require-pnpm-hardening`
10858
+ * — `react-doctor`-namespaced but not engine rules), and adopted
10859
+ * third-party plugins (`eslint`, `unicorn`, `react-hooks-js`, …) have no
10860
+ * recipe, so advertising "fetch the fix recipe" for them sends agents to
10861
+ * a 404. Gate the directive on this predicate.
10862
+ */
10863
+ const hasPublishedFixRecipe = (diagnostic) => diagnostic.plugin === "react-doctor" && Object.hasOwn(reactDoctorPlugin.rules, diagnostic.rule);
10827
10864
  //#endregion
10828
10865
  //#region ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
10829
10866
  const ANSI_BACKGROUND_OFFSET = 10;
@@ -14012,7 +14049,7 @@ const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
14012
14049
  };
14013
14050
  const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
14014
14051
  const FETCH_FIX_RECIPE_LABEL = "Fetch & follow the canonical fix recipe before fixing";
14015
- const formatFixRecipeLine = (diagnostic) => `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}`;
14052
+ const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}` : null;
14016
14053
  //#endregion
14017
14054
  //#region src/cli/utils/box-text.ts
14018
14055
  const ESCAPE = String.fromCharCode(27);
@@ -14296,7 +14333,8 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
14296
14333
  ];
14297
14334
  if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
14298
14335
  if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
14299
- sections.push("", formatFixRecipeLine(firstDiagnostic));
14336
+ const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
14337
+ if (fixRecipeLine) sections.push("", fixRecipeLine);
14300
14338
  sections.push("", "Files:");
14301
14339
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
14302
14340
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
@@ -14715,7 +14753,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
14715
14753
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
14716
14754
  //#endregion
14717
14755
  //#region src/cli/utils/version.ts
14718
- const VERSION = "0.2.14-dev.6e59f10";
14756
+ const VERSION = "0.2.14-dev.8b313ba";
14719
14757
  //#endregion
14720
14758
  //#region src/inspect.ts
14721
14759
  const silentConsole = makeNoopConsole();
@@ -15029,7 +15067,9 @@ const buildHandoffPayload = (input) => {
15029
15067
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
15030
15068
  const representative = ruleDiagnostics[0];
15031
15069
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
15032
- lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (×${ruleDiagnostics.length})`, ` ${representative.message}`, ` ${formatFixRecipeLine(representative)}`);
15070
+ lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (×${ruleDiagnostics.length})`, ` ${representative.message}`);
15071
+ const fixRecipeLine = formatFixRecipeLine(representative);
15072
+ if (fixRecipeLine) lines.push(` ${fixRecipeLine}`);
15033
15073
  const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
15034
15074
  for (const filePath of uniqueFiles.slice(0, 3)) {
15035
15075
  const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
@@ -15040,7 +15080,7 @@ const buildHandoffPayload = (input) => {
15040
15080
  });
15041
15081
  lines.push("");
15042
15082
  if (diagnosticsDirectory) lines.push(`Full results for all ${input.diagnostics.length} issues (diagnostics.json + a .txt per rule): ${diagnosticsDirectory}`, "");
15043
- lines.push("Read each file and fix the root cause — don't suppress. When the top issues are done, run `npx react-doctor@latest --verbose` to verify, then work through the rest from the full results above.");
15083
+ lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", "", "Then work through the rest from the full results above.");
15044
15084
  return lines.join("\n");
15045
15085
  };
15046
15086
  //#endregion
package/dist/index.js CHANGED
@@ -4711,6 +4711,28 @@ const collectIgnorePatterns = (rootDirectory) => {
4711
4711
  cachedPatternsByRoot.set(rootDirectory, patterns);
4712
4712
  return patterns;
4713
4713
  };
4714
+ /**
4715
+ * Resolves a path to its canonical, symlink-free form, falling back to
4716
+ * the input when it cannot be realpath'd (broken symlink, permission
4717
+ * error) so a best-effort normalization never throws.
4718
+ *
4719
+ * deslop's dead-code module graph is collected with `fast-glob` (which
4720
+ * keeps the scan root's symlinks intact) while imports are resolved
4721
+ * through `oxc-resolver` (which returns realpath'd targets). When the
4722
+ * project root sits behind a symlink — e.g. macOS iCloud-synced
4723
+ * `~/Documents` / `~/Desktop`, or a symlinked checkout — those two path
4724
+ * spaces diverge: every resolved import misses the graph and the files
4725
+ * they point at (commonly every `@/…` alias target) are mis-reported as
4726
+ * unreachable. Canonicalizing the root before the scan keeps both path
4727
+ * spaces in agreement.
4728
+ */
4729
+ const toCanonicalPath = (filePath) => {
4730
+ try {
4731
+ return fs.realpathSync(filePath);
4732
+ } catch {
4733
+ return filePath;
4734
+ }
4735
+ };
4714
4736
  const DEAD_CODE_PLUGIN = "deslop";
4715
4737
  const DEAD_CODE_CATEGORY = "Maintainability";
4716
4738
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
@@ -4967,7 +4989,8 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
4967
4989
  });
4968
4990
  });
4969
4991
  const checkDeadCode = async (options) => {
4970
- const { rootDirectory, userConfig } = options;
4992
+ const { userConfig } = options;
4993
+ const rootDirectory = toCanonicalPath(options.rootDirectory);
4971
4994
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4972
4995
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4973
4996
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.14-dev.6e59f10",
3
+ "version": "0.2.14-dev.8b313ba",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -59,7 +59,7 @@
59
59
  "oxlint": "^1.66.0",
60
60
  "prompts": "^2.4.2",
61
61
  "typescript": ">=5.0.4 <7",
62
- "oxlint-plugin-react-doctor": "0.2.14-dev.6e59f10"
62
+ "oxlint-plugin-react-doctor": "0.2.14-dev.8b313ba"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/babel__code-frame": "^7.27.0",
@@ -67,8 +67,8 @@
67
67
  "@xterm/headless": "^6.0.0",
68
68
  "commander": "^14.0.3",
69
69
  "ora": "^9.4.0",
70
- "@react-doctor/api": "0.2.14",
71
- "@react-doctor/core": "0.2.14"
70
+ "@react-doctor/core": "0.2.14",
71
+ "@react-doctor/api": "0.2.14"
72
72
  },
73
73
  "engines": {
74
74
  "node": "^20.19.0 || >=22.12.0"