react-doctor 0.0.19 → 0.0.21

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
@@ -28,8 +28,10 @@ const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
28
28
  const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
29
29
  const SCORE_API_URL = "https://www.react.doctor/api/score";
30
30
  const SHARE_BASE_URL = "https://www.react.doctor/share";
31
+ const OPEN_BASE_URL = "https://www.react.doctor/open";
31
32
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32
33
  const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
34
+ const OFFLINE_FLAG_MESSAGE = "Score not calculated. Remove --offline to calculate score.";
33
35
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
34
36
 
35
37
  //#endregion
@@ -765,6 +767,46 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
765
767
  }
766
768
  });
767
769
 
770
+ //#endregion
771
+ //#region src/utils/neutralize-disable-directives.ts
772
+ const findFilesWithDisableDirectives = (rootDirectory) => {
773
+ const result = spawnSync("git", [
774
+ "grep",
775
+ "-l",
776
+ "--untracked",
777
+ "-E",
778
+ "(eslint|oxlint)-disable"
779
+ ], {
780
+ cwd: rootDirectory,
781
+ encoding: "utf-8",
782
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
783
+ });
784
+ if (result.error || result.status === null) return [];
785
+ return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
786
+ };
787
+ const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
788
+ const neutralizeDisableDirectives = (rootDirectory) => {
789
+ const filePaths = findFilesWithDisableDirectives(rootDirectory);
790
+ const originalContents = /* @__PURE__ */ new Map();
791
+ for (const relativePath of filePaths) {
792
+ const absolutePath = path.join(rootDirectory, relativePath);
793
+ let originalContent;
794
+ try {
795
+ originalContent = fs.readFileSync(absolutePath, "utf-8");
796
+ } catch {
797
+ continue;
798
+ }
799
+ const neutralizedContent = neutralizeContent(originalContent);
800
+ if (neutralizedContent !== originalContent) {
801
+ originalContents.set(absolutePath, originalContent);
802
+ fs.writeFileSync(absolutePath, neutralizedContent);
803
+ }
804
+ }
805
+ return () => {
806
+ for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
807
+ };
808
+ };
809
+
768
810
  //#endregion
769
811
  //#region src/utils/run-oxlint.ts
770
812
  const esmRequire = createRequire(import.meta.url);
@@ -832,7 +874,7 @@ const RULE_CATEGORY_MAP = {
832
874
  "react-doctor/async-parallel": "Performance"
833
875
  };
834
876
  const RULE_HELP_MAP = {
835
- "no-derived-state-effect": "Compute during render: `const derived = computeFrom(dep1, dep2)` no useEffect needed",
877
+ "no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`",
836
878
  "no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
837
879
  "no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
838
880
  "no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
@@ -934,6 +976,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
934
976
  framework,
935
977
  hasReactCompiler
936
978
  });
979
+ const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
937
980
  try {
938
981
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
939
982
  const args = [
@@ -989,6 +1032,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
989
1032
  };
990
1033
  });
991
1034
  } finally {
1035
+ restoreDisableDirectives();
992
1036
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
993
1037
  }
994
1038
  };
@@ -1170,7 +1214,7 @@ const buildShareUrl = (diagnostics, scoreResult, projectName) => {
1170
1214
  if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
1171
1215
  return `${SHARE_BASE_URL}?${params.toString()}`;
1172
1216
  };
1173
- const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount) => {
1217
+ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage) => {
1174
1218
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1175
1219
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
1176
1220
  const affectedFileCount = collectAffectedFiles(diagnostics).size;
@@ -1212,7 +1256,7 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName
1212
1256
  } else {
1213
1257
  summaryFramedLines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
1214
1258
  summaryFramedLines.push(createFramedLine(""));
1215
- summaryFramedLines.push(createFramedLine(OFFLINE_MESSAGE, highlighter.dim(OFFLINE_MESSAGE)));
1259
+ summaryFramedLines.push(createFramedLine(noScoreMessage, highlighter.dim(noScoreMessage)));
1216
1260
  summaryFramedLines.push(createFramedLine(""));
1217
1261
  }
1218
1262
  summaryFramedLines.push(createFramedLine(summaryLinePartsPlain.join(" "), summaryLineParts.join(" ")));
@@ -1237,6 +1281,7 @@ const scan = async (directory, inputOptions = {}) => {
1237
1281
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
1238
1282
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
1239
1283
  scoreOnly: inputOptions.scoreOnly ?? false,
1284
+ offline: inputOptions.offline ?? false,
1240
1285
  includePaths: inputOptions.includePaths
1241
1286
  };
1242
1287
  const includePaths = options.includePaths ?? [];
@@ -1266,7 +1311,10 @@ const scan = async (directory, inputOptions = {}) => {
1266
1311
  return lintDiagnostics;
1267
1312
  } catch (error) {
1268
1313
  lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
1269
- logger.error(String(error));
1314
+ if (error instanceof Error) {
1315
+ logger.error(error.message);
1316
+ if (error.stack) logger.dim(error.stack);
1317
+ } else logger.error(String(error));
1270
1318
  return [];
1271
1319
  }
1272
1320
  })() : Promise.resolve([]);
@@ -1290,10 +1338,11 @@ const scan = async (directory, inputOptions = {}) => {
1290
1338
  ];
1291
1339
  const diagnostics = userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;
1292
1340
  const elapsedMilliseconds = performance.now() - startTime;
1293
- const scoreResult = await calculateScore(diagnostics);
1341
+ const scoreResult = options.offline ? null : await calculateScore(diagnostics);
1342
+ const noScoreMessage = options.offline ? OFFLINE_FLAG_MESSAGE : OFFLINE_MESSAGE;
1294
1343
  if (options.scoreOnly) {
1295
1344
  if (scoreResult) logger.log(`${scoreResult.score}`);
1296
- else logger.dim(OFFLINE_MESSAGE);
1345
+ else logger.dim(noScoreMessage);
1297
1346
  return;
1298
1347
  }
1299
1348
  if (diagnostics.length === 0) {
@@ -1302,12 +1351,12 @@ const scan = async (directory, inputOptions = {}) => {
1302
1351
  if (scoreResult) {
1303
1352
  printBranding(scoreResult.score);
1304
1353
  printScoreGauge(scoreResult.score, scoreResult.label);
1305
- } else logger.dim(` ${OFFLINE_MESSAGE}`);
1354
+ } else logger.dim(` ${noScoreMessage}`);
1306
1355
  return;
1307
1356
  }
1308
1357
  printDiagnostics(diagnostics, options.verbose);
1309
1358
  const displayedSourceFileCount = isDiffMode ? includePaths.length : projectInfo.sourceFileCount;
1310
- printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount);
1359
+ printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage);
1311
1360
  };
1312
1361
 
1313
1362
  //#endregion
@@ -1614,7 +1663,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
1614
1663
 
1615
1664
  //#endregion
1616
1665
  //#region src/cli.ts
1617
- const VERSION = "0.0.19";
1666
+ const VERSION = "0.0.21";
1618
1667
  process.on("SIGINT", () => process.exit(0));
1619
1668
  process.on("SIGTERM", () => process.exit(0));
1620
1669
  const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly) => {
@@ -1639,7 +1688,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
1639
1688
  });
1640
1689
  return Boolean(shouldScanBranchOnly);
1641
1690
  };
1642
- 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("--diff [base]", "scan only files changed vs base branch").option("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
1691
+ 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("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
1643
1692
  const isScoreOnly = flags.score && !flags.prompt;
1644
1693
  const shouldCopyPromptOutput = flags.prompt;
1645
1694
  if (shouldCopyPromptOutput) startLoggerCapture();
@@ -1655,7 +1704,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
1655
1704
  lint: isCliOverride("lint") ? flags.lint : userConfig?.lint ?? flags.lint,
1656
1705
  deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ?? flags.deadCode,
1657
1706
  verbose: flags.prompt || (isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false),
1658
- scoreOnly: isScoreOnly
1707
+ scoreOnly: isScoreOnly,
1708
+ offline: flags.offline
1659
1709
  };
1660
1710
  const isAutomatedEnvironment = [
1661
1711
  process.env.CI,
@@ -1753,12 +1803,20 @@ const openUrl = (url) => {
1753
1803
  }
1754
1804
  execSync(process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`, { stdio: "ignore" });
1755
1805
  };
1756
- const buildDeeplink = (directory) => {
1757
- return `ami://open-project?cwd=${encodeURIComponent(path.resolve(directory))}&prompt=${encodeURIComponent(DEEPLINK_FIX_PROMPT)}&mode=agent&autoSubmit=true`;
1758
- };
1806
+ const buildDeeplinkParams = (directory) => {
1807
+ const params = new URLSearchParams();
1808
+ params.set("cwd", path.resolve(directory));
1809
+ params.set("prompt", DEEPLINK_FIX_PROMPT);
1810
+ params.set("mode", "agent");
1811
+ params.set("autoSubmit", "true");
1812
+ return params;
1813
+ };
1814
+ const buildDeeplink = (directory) => `ami://open-project?${buildDeeplinkParams(directory).toString()}`;
1815
+ const buildWebDeeplink = (directory) => `${OPEN_BASE_URL}?${buildDeeplinkParams(directory).toString()}`;
1759
1816
  const openAmiToFix = (directory) => {
1760
1817
  const isInstalled = isAmiInstalled();
1761
1818
  const deeplink = buildDeeplink(directory);
1819
+ const webDeeplink = buildWebDeeplink(directory);
1762
1820
  if (!isInstalled) {
1763
1821
  if (process.platform === "darwin") {
1764
1822
  installAmi();
@@ -1769,7 +1827,7 @@ const openAmiToFix = (directory) => {
1769
1827
  }
1770
1828
  logger.break();
1771
1829
  logger.dim("Once Ami is running, open this link to start fixing:");
1772
- logger.info(deeplink);
1830
+ logger.info(webDeeplink);
1773
1831
  return;
1774
1832
  }
1775
1833
  logger.log("Opening Ami to fix react-doctor issues...");
@@ -1779,7 +1837,7 @@ const openAmiToFix = (directory) => {
1779
1837
  } catch {
1780
1838
  logger.break();
1781
1839
  logger.dim("Could not open Ami automatically. Open this URL manually:");
1782
- logger.info(deeplink);
1840
+ logger.info(webDeeplink);
1783
1841
  }
1784
1842
  };
1785
1843
  const buildPromptWithOutput = (reactDoctorOutput) => {