react-doctor 0.0.40 → 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/index.js CHANGED
@@ -6,7 +6,6 @@ import { main } from "knip";
6
6
  import { createOptions } from "knip/session";
7
7
  import os from "node:os";
8
8
  import { fileURLToPath } from "node:url";
9
-
10
9
  //#region src/core/build-diagnose-result.ts
11
10
  const buildDiagnoseResult = (params) => ({
12
11
  diagnostics: params.diagnostics,
@@ -14,7 +13,6 @@ const buildDiagnoseResult = (params) => ({
14
13
  project: params.project,
15
14
  elapsedMilliseconds: params.elapsedMilliseconds
16
15
  });
17
-
18
16
  //#endregion
19
17
  //#region src/utils/match-glob-pattern.ts
20
18
  const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
@@ -42,7 +40,6 @@ const compileGlobPattern = (pattern) => {
42
40
  regexSource += "$";
43
41
  return new RegExp(regexSource);
44
42
  };
45
-
46
43
  //#endregion
47
44
  //#region src/utils/is-ignored-file.ts
48
45
  const toRelativePath = (filePath, rootDirectory) => {
@@ -57,7 +54,6 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
57
54
  const relativePath = toRelativePath(filePath, rootDirectory);
58
55
  return patterns.some((pattern) => pattern.test(relativePath));
59
56
  };
60
-
61
57
  //#endregion
62
58
  //#region src/utils/filter-diagnostics.ts
63
59
  const resolveCandidateReadPath = (rootDirectory, filePath) => {
@@ -131,13 +127,11 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
131
127
  return true;
132
128
  });
133
129
  };
134
-
135
130
  //#endregion
136
131
  //#region src/utils/merge-and-filter-diagnostics.ts
137
132
  const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
138
133
  return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
139
134
  };
140
-
141
135
  //#endregion
142
136
  //#region src/core/build-result.ts
143
137
  const buildDiagnoseTimedResult = async (input) => {
@@ -149,36 +143,35 @@ const buildDiagnoseTimedResult = async (input) => {
149
143
  elapsedMilliseconds
150
144
  };
151
145
  };
152
-
153
146
  //#endregion
154
147
  //#region src/constants.ts
155
148
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
156
149
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
157
- const ERROR_PREVIEW_LENGTH_CHARS = 200;
158
- const PERFECT_SCORE = 100;
159
- const SCORE_GOOD_THRESHOLD = 75;
160
- const SCORE_OK_THRESHOLD = 50;
161
150
  const SCORE_API_URL = "https://www.react.doctor/api/score";
162
151
  const FETCH_TIMEOUT_MS = 1e4;
163
152
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
164
- const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
165
- const OXLINT_MAX_FILES_PER_BATCH = 500;
166
153
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
167
154
  const ERROR_RULE_PENALTY = 1.5;
168
155
  const WARNING_RULE_PENALTY = .75;
169
- const MAX_KNIP_RETRIES = 5;
170
- const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
156
+ const KNIP_CONFIG_LOCATIONS = [
157
+ "knip.json",
158
+ "knip.jsonc",
159
+ ".knip.json",
160
+ ".knip.jsonc",
161
+ "knip.ts",
162
+ "knip.js",
163
+ "knip.config.ts",
164
+ "knip.config.js"
165
+ ];
171
166
  const IGNORED_DIRECTORIES = new Set([
172
167
  "node_modules",
173
168
  "dist",
174
169
  "build",
175
170
  "coverage"
176
171
  ]);
177
-
178
172
  //#endregion
179
173
  //#region src/utils/jsx-include-paths.ts
180
174
  const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
181
-
182
175
  //#endregion
183
176
  //#region src/core/diagnose-core.ts
184
177
  const diagnoseCore = async (deps, options = {}) => {
@@ -229,11 +222,9 @@ const diagnoseCore = async (deps, options = {}) => {
229
222
  elapsedMilliseconds: timed.elapsedMilliseconds
230
223
  });
231
224
  };
232
-
233
225
  //#endregion
234
226
  //#region src/plugin/constants.ts
235
227
  const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
236
-
237
228
  //#endregion
238
229
  //#region src/utils/is-file.ts
239
230
  const isFile = (filePath) => {
@@ -243,7 +234,6 @@ const isFile = (filePath) => {
243
234
  return false;
244
235
  }
245
236
  };
246
-
247
237
  //#endregion
248
238
  //#region src/utils/read-package-json.ts
249
239
  const readPackageJson = (packageJsonPath) => {
@@ -258,7 +248,6 @@ const readPackageJson = (packageJsonPath) => {
258
248
  throw error;
259
249
  }
260
250
  };
261
-
262
251
  //#endregion
263
252
  //#region src/utils/check-reduced-motion.ts
264
253
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
@@ -300,7 +289,6 @@ const checkReducedMotion = (rootDirectory) => {
300
289
  return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
301
290
  }
302
291
  };
303
-
304
292
  //#endregion
305
293
  //#region src/utils/find-monorepo-root.ts
306
294
  const isMonorepoRoot = (directory) => {
@@ -319,11 +307,9 @@ const findMonorepoRoot = (startDirectory) => {
319
307
  }
320
308
  return null;
321
309
  };
322
-
323
310
  //#endregion
324
311
  //#region src/utils/is-plain-object.ts
325
312
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
326
-
327
313
  //#endregion
328
314
  //#region src/utils/discover-project.ts
329
315
  const REACT_COMPILER_PACKAGES = new Set([
@@ -665,7 +651,6 @@ const discoverProject = (directory) => {
665
651
  sourceFileCount
666
652
  };
667
653
  };
668
-
669
654
  //#endregion
670
655
  //#region src/utils/load-config.ts
671
656
  const CONFIG_FILENAME = "react-doctor.config.json";
@@ -701,7 +686,6 @@ const loadConfig = (rootDirectory) => {
701
686
  }
702
687
  return null;
703
688
  };
704
-
705
689
  //#endregion
706
690
  //#region src/utils/read-file-lines-node.ts
707
691
  const createNodeReadFileLinesSync = (rootDirectory) => {
@@ -714,7 +698,6 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
714
698
  }
715
699
  };
716
700
  };
717
-
718
701
  //#endregion
719
702
  //#region src/utils/resolve-lint-include-paths.ts
720
703
  const listSourceFilesViaGit = (rootDirectory) => {
@@ -757,12 +740,11 @@ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
757
740
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
758
741
  });
759
742
  };
760
-
761
743
  //#endregion
762
744
  //#region src/core/calculate-score-locally.ts
763
745
  const getScoreLabel = (score) => {
764
- if (score >= SCORE_GOOD_THRESHOLD) return "Great";
765
- if (score >= SCORE_OK_THRESHOLD) return "Needs work";
746
+ if (score >= 75) return "Great";
747
+ if (score >= 50) return "Needs work";
766
748
  return "Critical";
767
749
  };
768
750
  const countUniqueRules = (diagnostics) => {
@@ -780,7 +762,7 @@ const countUniqueRules = (diagnostics) => {
780
762
  };
781
763
  const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
782
764
  const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
783
- return Math.max(0, Math.round(PERFECT_SCORE - penalty));
765
+ return Math.max(0, Math.round(100 - penalty));
784
766
  };
785
767
  const calculateScoreLocally = (diagnostics) => {
786
768
  const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
@@ -790,7 +772,6 @@ const calculateScoreLocally = (diagnostics) => {
790
772
  label: getScoreLabel(score)
791
773
  };
792
774
  };
793
-
794
775
  //#endregion
795
776
  //#region src/core/try-score-from-api.ts
796
777
  const parseScoreResult = (value) => {
@@ -822,7 +803,6 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
822
803
  clearTimeout(timeoutId);
823
804
  }
824
805
  };
825
-
826
806
  //#endregion
827
807
  //#region src/utils/proxy-fetch.ts
828
808
  const getGlobalProcess = () => {
@@ -865,7 +845,6 @@ const proxyFetch = async (url, init) => {
865
845
  clearTimeout(timeoutId);
866
846
  }
867
847
  };
868
-
869
848
  //#endregion
870
849
  //#region src/utils/calculate-score-node.ts
871
850
  const calculateScore = async (diagnostics) => {
@@ -873,7 +852,6 @@ const calculateScore = async (diagnostics) => {
873
852
  if (apiScore) return apiScore;
874
853
  return calculateScoreLocally(diagnostics);
875
854
  };
876
-
877
855
  //#endregion
878
856
  //#region src/utils/collect-unused-file-paths.ts
879
857
  const collectUnusedFilePaths = (filesIssues) => {
@@ -887,7 +865,34 @@ const collectUnusedFilePaths = (filesIssues) => {
887
865
  }
888
866
  return unusedFilePaths;
889
867
  };
890
-
868
+ //#endregion
869
+ //#region src/utils/format-error-chain.ts
870
+ const collectErrorChain = (rootError) => {
871
+ const errorChain = [];
872
+ const visitedErrors = /* @__PURE__ */ new Set();
873
+ let currentError = rootError;
874
+ while (currentError !== void 0 && !visitedErrors.has(currentError)) {
875
+ visitedErrors.add(currentError);
876
+ errorChain.push(currentError);
877
+ currentError = currentError instanceof Error ? currentError.cause : void 0;
878
+ }
879
+ return errorChain;
880
+ };
881
+ const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
882
+ const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
883
+ //#endregion
884
+ //#region src/utils/extract-failed-plugin-name.ts
885
+ const PLUGIN_CONFIG_PATTERN = /(?:^|[/\\\s])([a-z][a-z0-9-]*)\.config\./i;
886
+ const extractFailedPluginName = (error) => {
887
+ for (const errorMessage of getErrorChainMessages(error)) {
888
+ const pluginNameMatch = errorMessage.match(PLUGIN_CONFIG_PATTERN);
889
+ if (pluginNameMatch?.[1]) return pluginNameMatch[1].toLowerCase();
890
+ }
891
+ return null;
892
+ };
893
+ //#endregion
894
+ //#region src/utils/has-knip-config.ts
895
+ const hasKnipConfig = (directory) => KNIP_CONFIG_LOCATIONS.some((configFilename) => isFile(path.join(directory, configFilename)));
891
896
  //#endregion
892
897
  //#region src/utils/run-knip.ts
893
898
  const KNIP_CATEGORY_MAP = {
@@ -942,12 +947,15 @@ const silenced = async (fn) => {
942
947
  console.error = originalError;
943
948
  }
944
949
  };
945
- const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
946
- const extractFailedPluginName = (error) => {
947
- return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
948
- };
949
950
  const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
950
951
  const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
952
+ const tryDisableFailedPlugin = (error, parsedConfig, disabledPlugins) => {
953
+ const failedPlugin = extractFailedPluginName(error);
954
+ if (!failedPlugin || !(failedPlugin in parsedConfig) || disabledPlugins.has(failedPlugin)) return false;
955
+ disabledPlugins.add(failedPlugin);
956
+ parsedConfig[failedPlugin] = false;
957
+ return true;
958
+ };
951
959
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
952
960
  const tsConfigFile = resolveTsConfigFile(knipCwd);
953
961
  const options = await silenced(() => createOptions({
@@ -957,33 +965,36 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
957
965
  ...tsConfigFile ? { tsConfigFile } : {}
958
966
  }));
959
967
  const parsedConfig = options.parsedConfig;
960
- for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
968
+ const disabledPlugins = /* @__PURE__ */ new Set();
969
+ let lastKnipError;
970
+ for (let attempt = 0; attempt <= 5; attempt++) try {
961
971
  return await silenced(() => main(options));
962
972
  } catch (error) {
963
- const failedPlugin = extractFailedPluginName(error);
964
- if (!failedPlugin || attempt === MAX_KNIP_RETRIES) throw error;
965
- parsedConfig[failedPlugin] = false;
973
+ lastKnipError = error;
974
+ if (!tryDisableFailedPlugin(error, parsedConfig, disabledPlugins)) throw error;
966
975
  }
967
- throw new Error("Unreachable");
976
+ throw lastKnipError;
968
977
  };
969
978
  const hasNodeModules = (directory) => {
970
979
  const nodeModulesPath = path.join(directory, "node_modules");
971
980
  return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
972
981
  };
982
+ const resolveWorkspaceName = (rootDirectory) => {
983
+ const packageJsonPath = path.join(rootDirectory, "package.json");
984
+ return (isFile(packageJsonPath) ? readPackageJson(packageJsonPath) : {}).name ?? path.basename(rootDirectory);
985
+ };
986
+ const runKnipForProject = async (rootDirectory, monorepoRoot) => {
987
+ if (!monorepoRoot || hasKnipConfig(rootDirectory)) return runKnipWithOptions(rootDirectory);
988
+ try {
989
+ return await runKnipWithOptions(monorepoRoot, resolveWorkspaceName(rootDirectory));
990
+ } catch {
991
+ return runKnipWithOptions(rootDirectory);
992
+ }
993
+ };
973
994
  const runKnip = async (rootDirectory) => {
974
995
  const monorepoRoot = findMonorepoRoot(rootDirectory);
975
996
  if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
976
- let knipResult;
977
- if (monorepoRoot) {
978
- const packageJsonPath = path.join(rootDirectory, "package.json");
979
- const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
980
- try {
981
- knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
982
- } catch {
983
- knipResult = await runKnipWithOptions(rootDirectory);
984
- }
985
- } else knipResult = await runKnipWithOptions(rootDirectory);
986
- const { issues } = knipResult;
997
+ const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
987
998
  const diagnostics = [];
988
999
  for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
989
1000
  filePath: path.relative(rootDirectory, unusedFilePath),
@@ -1004,7 +1015,6 @@ const runKnip = async (rootDirectory) => {
1004
1015
  ]) diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));
1005
1016
  return diagnostics;
1006
1017
  };
1007
-
1008
1018
  //#endregion
1009
1019
  //#region src/oxlint-config.ts
1010
1020
  const esmRequire$1 = createRequire(import.meta.url);
@@ -1188,7 +1198,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
1188
1198
  ...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
1189
1199
  }
1190
1200
  });
1191
-
1192
1201
  //#endregion
1193
1202
  //#region src/utils/neutralize-disable-directives.ts
1194
1203
  const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
@@ -1231,7 +1240,6 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
1231
1240
  for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
1232
1241
  };
1233
1242
  };
1234
-
1235
1243
  //#endregion
1236
1244
  //#region src/utils/run-oxlint.ts
1237
1245
  const esmRequire = createRequire(import.meta.url);
@@ -1491,8 +1499,8 @@ const batchIncludePaths = (baseArgs, includePaths) => {
1491
1499
  let currentBatchLength = baseArgsLength;
1492
1500
  for (const filePath of includePaths) {
1493
1501
  const entryLength = filePath.length + 1;
1494
- const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS;
1495
- const exceedsFileCount = currentBatch.length >= OXLINT_MAX_FILES_PER_BATCH;
1502
+ const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
1503
+ const exceedsFileCount = currentBatch.length >= 500;
1496
1504
  if (exceedsArgLength || exceedsFileCount) {
1497
1505
  batches.push(currentBatch);
1498
1506
  currentBatch = [];
@@ -1536,7 +1544,7 @@ const parseOxlintOutput = (stdout) => {
1536
1544
  try {
1537
1545
  output = JSON.parse(stdout);
1538
1546
  } catch {
1539
- throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
1547
+ throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
1540
1548
  }
1541
1549
  return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
1542
1550
  const { plugin, rule } = parseRuleCode(diagnostic.code);
@@ -1587,7 +1595,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
1587
1595
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
1588
1596
  }
1589
1597
  };
1590
-
1591
1598
  //#endregion
1592
1599
  //#region src/utils/get-diff-files.ts
1593
1600
  const getCurrentBranch = (directory) => {
@@ -1667,7 +1674,6 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
1667
1674
  };
1668
1675
  };
1669
1676
  const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
1670
-
1671
1677
  //#endregion
1672
1678
  //#region src/index.ts
1673
1679
  const diagnose = async (directory, options = {}) => {
@@ -1692,7 +1698,7 @@ const diagnose = async (directory, options = {}) => {
1692
1698
  lintIncludePaths
1693
1699
  });
1694
1700
  };
1695
-
1696
1701
  //#endregion
1697
1702
  export { diagnose, filterSourceFiles, getDiffInfo };
1703
+
1698
1704
  //# sourceMappingURL=index.js.map