react-doctor 0.0.27 → 0.0.28

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
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { execSync, spawn, spawnSync } from "node:child_process";
4
- import fs, { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import fs, { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import os, { homedir, tmpdir } from "node:os";
6
6
  import path, { join } from "node:path";
7
7
  import { Command } from "commander";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { performance } from "node:perf_hooks";
10
10
  import pc from "picocolors";
11
+ import basePrompts from "prompts";
11
12
  import { main } from "knip";
12
13
  import { createOptions } from "knip/session";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import ora from "ora";
15
- import basePrompts from "prompts";
16
16
 
17
17
  //#region src/constants.ts
18
18
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
@@ -40,6 +40,8 @@ const WARNING_RULE_PENALTY = .75;
40
40
  const ERROR_ESTIMATED_FIX_RATE = .85;
41
41
  const WARNING_ESTIMATED_FIX_RATE = .8;
42
42
  const MAX_KNIP_RETRIES = 5;
43
+ const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
44
+ const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
43
45
  const AMI_WEBSITE_URL = "https://ami.dev";
44
46
  const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
45
47
  const AMI_RELEASES_URL = "https://github.com/millionco/ami-releases/releases";
@@ -641,6 +643,167 @@ const loadConfig = (rootDirectory) => {
641
643
  return null;
642
644
  };
643
645
 
646
+ //#endregion
647
+ //#region src/utils/should-auto-select-current-choice.ts
648
+ const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
649
+ if (choiceStates.some((choiceState) => choiceState.selected)) return false;
650
+ const currentChoice = choiceStates[cursor];
651
+ return Boolean(currentChoice) && !currentChoice.disabled;
652
+ };
653
+
654
+ //#endregion
655
+ //#region src/utils/should-select-all-choices.ts
656
+ const shouldSelectAllChoices = (choiceStates) => {
657
+ return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
658
+ };
659
+
660
+ //#endregion
661
+ //#region src/utils/prompts.ts
662
+ const require = createRequire(import.meta.url);
663
+ const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
664
+ const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
665
+ let didPatchMultiselectToggleAll = false;
666
+ let didPatchMultiselectSubmit = false;
667
+ let didPatchSelectBanner = false;
668
+ const selectBannerMap = /* @__PURE__ */ new Map();
669
+ const setSelectBanner = (banner, targetIndex) => {
670
+ selectBannerMap.set(targetIndex, banner);
671
+ };
672
+ const clearSelectBanner = () => {
673
+ selectBannerMap.clear();
674
+ };
675
+ const onCancel = () => {
676
+ logger.break();
677
+ logger.log("Cancelled.");
678
+ logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
679
+ logger.break();
680
+ process.exit(0);
681
+ };
682
+ const patchMultiselectToggleAll = () => {
683
+ if (didPatchMultiselectToggleAll) return;
684
+ didPatchMultiselectToggleAll = true;
685
+ const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
686
+ multiselectPromptConstructor.prototype.toggleAll = function() {
687
+ const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
688
+ if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
689
+ this.bell();
690
+ return;
691
+ }
692
+ const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
693
+ for (const choiceState of this.value) {
694
+ if (choiceState.disabled) continue;
695
+ choiceState.selected = shouldSelectAllEnabledChoices;
696
+ }
697
+ this.render();
698
+ };
699
+ };
700
+ const patchMultiselectSubmit = () => {
701
+ if (didPatchMultiselectSubmit) return;
702
+ didPatchMultiselectSubmit = true;
703
+ const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
704
+ const originalSubmit = multiselectPromptConstructor.prototype.submit;
705
+ multiselectPromptConstructor.prototype.submit = function() {
706
+ if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
707
+ originalSubmit.call(this);
708
+ };
709
+ };
710
+ const patchSelectBanner = () => {
711
+ if (didPatchSelectBanner) return;
712
+ didPatchSelectBanner = true;
713
+ const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
714
+ const promptsClear = require("prompts/lib/util/clear");
715
+ const originalRender = selectConstructor.prototype.render;
716
+ selectConstructor.prototype.render = function() {
717
+ originalRender.call(this);
718
+ const banner = selectBannerMap.get(this.cursor);
719
+ if (!banner || this.closed || this.done) return;
720
+ this.out.write(promptsClear(this.outputText, this.out.columns));
721
+ this.outputText = `${banner}\n\n${this.outputText}`;
722
+ this.out.write(this.outputText);
723
+ };
724
+ };
725
+ const prompts = (questions) => {
726
+ patchMultiselectToggleAll();
727
+ patchMultiselectSubmit();
728
+ patchSelectBanner();
729
+ return basePrompts(questions, { onCancel });
730
+ };
731
+
732
+ //#endregion
733
+ //#region src/utils/resolve-compatible-node.ts
734
+ const parseNodeVersion = (versionString) => {
735
+ const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
736
+ return {
737
+ major,
738
+ minor,
739
+ patch
740
+ };
741
+ };
742
+ const isNodeVersionCompatibleWithOxlint = ({ major, minor }) => {
743
+ if (major === 20 && minor >= 19) return true;
744
+ if (major === 22 && minor >= 12) return true;
745
+ if (major > 22) return true;
746
+ return false;
747
+ };
748
+ const isCurrentNodeCompatibleWithOxlint = () => isNodeVersionCompatibleWithOxlint(parseNodeVersion(process.version));
749
+ const getNvmDirectory = () => {
750
+ const envNvmDirectory = process.env.NVM_DIR;
751
+ if (envNvmDirectory && existsSync(envNvmDirectory)) return envNvmDirectory;
752
+ const defaultNvmDirectory = path.join(os.homedir(), ".nvm");
753
+ if (existsSync(defaultNvmDirectory)) return defaultNvmDirectory;
754
+ return null;
755
+ };
756
+ const isNvmInstalled = () => getNvmDirectory() !== null;
757
+ const findCompatibleNvmBinary = () => {
758
+ const nvmDirectory = getNvmDirectory();
759
+ if (!nvmDirectory) return null;
760
+ const versionsDirectory = path.join(nvmDirectory, "versions", "node");
761
+ if (!existsSync(versionsDirectory)) return null;
762
+ const compatibleVersions = readdirSync(versionsDirectory).filter((directoryName) => directoryName.startsWith("v")).map((directoryName) => ({
763
+ directoryName,
764
+ ...parseNodeVersion(directoryName)
765
+ })).filter((version) => isNodeVersionCompatibleWithOxlint(version)).sort((versionA, versionB) => versionB.major - versionA.major || versionB.minor - versionA.minor || versionB.patch - versionA.patch);
766
+ if (compatibleVersions.length === 0) return null;
767
+ const bestVersion = compatibleVersions[0];
768
+ const binaryPath = path.join(versionsDirectory, bestVersion.directoryName, "bin", "node");
769
+ return existsSync(binaryPath) ? binaryPath : null;
770
+ };
771
+ const getNodeVersionFromBinary = (binaryPath) => {
772
+ try {
773
+ return execSync(`"${binaryPath}" --version`, { encoding: "utf-8" }).trim();
774
+ } catch {
775
+ return null;
776
+ }
777
+ };
778
+ const installNodeViaNvm = () => {
779
+ const nvmDirectory = getNvmDirectory();
780
+ if (!nvmDirectory) return false;
781
+ const nvmScript = path.join(nvmDirectory, "nvm.sh");
782
+ if (!existsSync(nvmScript)) return false;
783
+ try {
784
+ execSync(`bash -c ". '${nvmScript}' && nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}"`, { stdio: "inherit" });
785
+ return findCompatibleNvmBinary() !== null;
786
+ } catch {
787
+ return false;
788
+ }
789
+ };
790
+ const resolveNodeForOxlint = () => {
791
+ if (isCurrentNodeCompatibleWithOxlint()) return {
792
+ binaryPath: process.execPath,
793
+ isCurrentNode: true,
794
+ version: process.version
795
+ };
796
+ const nvmBinaryPath = findCompatibleNvmBinary();
797
+ if (!nvmBinaryPath) return null;
798
+ const version = getNodeVersionFromBinary(nvmBinaryPath);
799
+ if (!version) return null;
800
+ return {
801
+ binaryPath: nvmBinaryPath,
802
+ isCurrentNode: false,
803
+ version
804
+ };
805
+ };
806
+
644
807
  //#endregion
645
808
  //#region src/utils/run-knip.ts
646
809
  const KNIP_CATEGORY_MAP = {
@@ -1102,8 +1265,8 @@ const batchIncludePaths = (baseArgs, includePaths) => {
1102
1265
  if (currentBatch.length > 0) batches.push(currentBatch);
1103
1266
  return batches;
1104
1267
  };
1105
- const spawnOxlint = (args, rootDirectory) => new Promise((resolve, reject) => {
1106
- const child = spawn(process.execPath, args, { cwd: rootDirectory });
1268
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
1269
+ const child = spawn(nodeBinaryPath, args, { cwd: rootDirectory });
1107
1270
  const stdoutBuffers = [];
1108
1271
  const stderrBuffers = [];
1109
1272
  child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
@@ -1146,7 +1309,7 @@ const parseOxlintOutput = (stdout) => {
1146
1309
  };
1147
1310
  });
1148
1311
  };
1149
- const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths) => {
1312
+ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths, nodeBinaryPath = process.execPath) => {
1150
1313
  if (includePaths !== void 0 && includePaths.length === 0) return [];
1151
1314
  const configPath = path.join(os.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
1152
1315
  const config = createOxlintConfig({
@@ -1168,7 +1331,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
1168
1331
  const fileBatches = includePaths !== void 0 ? batchIncludePaths(baseArgs, includePaths) : [["."]];
1169
1332
  const allDiagnostics = [];
1170
1333
  for (const batch of fileBatches) {
1171
- const stdout = await spawnOxlint([...baseArgs, ...batch], rootDirectory);
1334
+ const stdout = await spawnOxlint([...baseArgs, ...batch], rootDirectory, nodeBinaryPath);
1172
1335
  allDiagnostics.push(...parseOxlintOutput(stdout));
1173
1336
  }
1174
1337
  return allDiagnostics;
@@ -1393,6 +1556,44 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName
1393
1556
  logger.break();
1394
1557
  logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
1395
1558
  };
1559
+ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
1560
+ if (!isLintEnabled) return null;
1561
+ const nodeResolution = resolveNodeForOxlint();
1562
+ if (nodeResolution) {
1563
+ if (!nodeResolution.isCurrentNode && !isScoreOnly) {
1564
+ logger.warn(`Node ${process.version} is unsupported by oxlint. Using Node ${nodeResolution.version} from nvm.`);
1565
+ logger.break();
1566
+ }
1567
+ return nodeResolution.binaryPath;
1568
+ }
1569
+ if (isScoreOnly) return null;
1570
+ logger.warn(`Node ${process.version} is not compatible with oxlint (requires ${OXLINT_NODE_REQUIREMENT}). Lint checks will be skipped.`);
1571
+ if (isNvmInstalled() && process.stdin.isTTY) {
1572
+ const { shouldInstallNode } = await prompts({
1573
+ type: "confirm",
1574
+ name: "shouldInstallNode",
1575
+ message: `Install Node ${OXLINT_RECOMMENDED_NODE_MAJOR} via nvm to enable lint checks?`,
1576
+ initial: true
1577
+ });
1578
+ if (shouldInstallNode) {
1579
+ logger.break();
1580
+ const freshResolution = installNodeViaNvm() ? resolveNodeForOxlint() : null;
1581
+ if (freshResolution) {
1582
+ logger.break();
1583
+ logger.success(`Node ${freshResolution.version} installed. Using it for lint checks.`);
1584
+ logger.break();
1585
+ return freshResolution.binaryPath;
1586
+ }
1587
+ logger.break();
1588
+ logger.warn("Failed to install Node via nvm. Skipping lint checks.");
1589
+ logger.break();
1590
+ return null;
1591
+ }
1592
+ } else if (isNvmInstalled()) logger.dim(` Run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
1593
+ else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
1594
+ logger.break();
1595
+ return null;
1596
+ };
1396
1597
  const mergeScanOptions = (inputOptions, userConfig) => ({
1397
1598
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
1398
1599
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
@@ -1428,19 +1629,24 @@ const scan = async (directory, inputOptions = {}) => {
1428
1629
  const jsxIncludePaths = computeJsxIncludePaths(includePaths);
1429
1630
  let didLintFail = false;
1430
1631
  let didDeadCodeFail = false;
1431
- const lintPromise = options.lint ? (async () => {
1632
+ const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly);
1633
+ if (options.lint && !resolvedNodeBinaryPath) didLintFail = true;
1634
+ const lintPromise = resolvedNodeBinaryPath ? (async () => {
1432
1635
  const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
1433
1636
  try {
1434
- const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths);
1637
+ const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths, resolvedNodeBinaryPath);
1435
1638
  lintSpinner?.succeed("Running lint checks.");
1436
1639
  return lintDiagnostics;
1437
1640
  } catch (error) {
1438
1641
  didLintFail = true;
1439
- lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
1440
- if (error instanceof Error) {
1441
- logger.error(error.message);
1442
- if (error.stack) logger.dim(error.stack);
1443
- } else logger.error(String(error));
1642
+ const errorMessage = error instanceof Error ? error.message : String(error);
1643
+ if (errorMessage.includes("native binding")) {
1644
+ lintSpinner?.fail(`Lint checks failed — oxlint native binding not found (Node ${process.version}).`);
1645
+ logger.dim(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`);
1646
+ } else {
1647
+ lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
1648
+ logger.error(errorMessage);
1649
+ }
1444
1650
  return [];
1445
1651
  }
1446
1652
  })() : Promise.resolve([]);
@@ -1603,92 +1809,6 @@ const handleError = (error, options = DEFAULT_HANDLE_ERROR_OPTIONS) => {
1603
1809
  process.exitCode = 1;
1604
1810
  };
1605
1811
 
1606
- //#endregion
1607
- //#region src/utils/should-auto-select-current-choice.ts
1608
- const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
1609
- if (choiceStates.some((choiceState) => choiceState.selected)) return false;
1610
- const currentChoice = choiceStates[cursor];
1611
- return Boolean(currentChoice) && !currentChoice.disabled;
1612
- };
1613
-
1614
- //#endregion
1615
- //#region src/utils/should-select-all-choices.ts
1616
- const shouldSelectAllChoices = (choiceStates) => {
1617
- return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
1618
- };
1619
-
1620
- //#endregion
1621
- //#region src/utils/prompts.ts
1622
- const require = createRequire(import.meta.url);
1623
- const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
1624
- const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
1625
- let didPatchMultiselectToggleAll = false;
1626
- let didPatchMultiselectSubmit = false;
1627
- let didPatchSelectBanner = false;
1628
- const selectBannerMap = /* @__PURE__ */ new Map();
1629
- const setSelectBanner = (banner, targetIndex) => {
1630
- selectBannerMap.set(targetIndex, banner);
1631
- };
1632
- const clearSelectBanner = () => {
1633
- selectBannerMap.clear();
1634
- };
1635
- const onCancel = () => {
1636
- logger.break();
1637
- logger.log("Cancelled.");
1638
- logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
1639
- logger.break();
1640
- process.exit(0);
1641
- };
1642
- const patchMultiselectToggleAll = () => {
1643
- if (didPatchMultiselectToggleAll) return;
1644
- didPatchMultiselectToggleAll = true;
1645
- const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
1646
- multiselectPromptConstructor.prototype.toggleAll = function() {
1647
- const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
1648
- if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
1649
- this.bell();
1650
- return;
1651
- }
1652
- const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
1653
- for (const choiceState of this.value) {
1654
- if (choiceState.disabled) continue;
1655
- choiceState.selected = shouldSelectAllEnabledChoices;
1656
- }
1657
- this.render();
1658
- };
1659
- };
1660
- const patchMultiselectSubmit = () => {
1661
- if (didPatchMultiselectSubmit) return;
1662
- didPatchMultiselectSubmit = true;
1663
- const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
1664
- const originalSubmit = multiselectPromptConstructor.prototype.submit;
1665
- multiselectPromptConstructor.prototype.submit = function() {
1666
- if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
1667
- originalSubmit.call(this);
1668
- };
1669
- };
1670
- const patchSelectBanner = () => {
1671
- if (didPatchSelectBanner) return;
1672
- didPatchSelectBanner = true;
1673
- const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
1674
- const promptsClear = require("prompts/lib/util/clear");
1675
- const originalRender = selectConstructor.prototype.render;
1676
- selectConstructor.prototype.render = function() {
1677
- originalRender.call(this);
1678
- const banner = selectBannerMap.get(this.cursor);
1679
- if (!banner || this.closed || this.done) return;
1680
- this.out.write(promptsClear(this.outputText, this.out.columns));
1681
- this.outputText = `${banner}\n\n${this.outputText}`;
1682
- this.out.write(this.outputText);
1683
- };
1684
- };
1685
- const prompts = (questions) => {
1686
- patchMultiselectToggleAll();
1687
- patchMultiselectSubmit();
1688
- patchSelectBanner();
1689
- return basePrompts(questions, { onCancel });
1690
- };
1691
-
1692
1812
  //#endregion
1693
1813
  //#region src/utils/select-projects.ts
1694
1814
  const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
@@ -1909,7 +2029,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
1909
2029
 
1910
2030
  //#endregion
1911
2031
  //#region src/cli.ts
1912
- const VERSION = "0.0.27";
2032
+ const VERSION = "0.0.28";
1913
2033
  const exitWithFixHint = () => {
1914
2034
  logger.break();
1915
2035
  logger.log("Cancelled.");