react-doctor 0.1.3 → 0.1.5

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.d.ts CHANGED
@@ -18,6 +18,7 @@ interface Diagnostic {
18
18
  severity: "error" | "warning";
19
19
  message: string;
20
20
  help: string;
21
+ url?: string;
21
22
  line: number;
22
23
  column: number;
23
24
  category: string;
@@ -75,6 +76,24 @@ interface ReactDoctorConfig {
75
76
  failOn?: FailOnLevel;
76
77
  customRulesOnly?: boolean;
77
78
  share?: boolean;
79
+ /**
80
+ * Redirect react-doctor at a different project directory than the one
81
+ * it was invoked against. Resolved relative to the location of the
82
+ * config file that declared this field (NOT relative to the CWD), so
83
+ * the redirect is stable no matter where the CLI / `diagnose()` is
84
+ * run from. Absolute paths are used as-is.
85
+ *
86
+ * Typical use: a monorepo root holds the only `react-doctor.config.json`
87
+ * (so editor tooling and child commands all find it), but the React
88
+ * app lives in `apps/web`. Setting `"rootDir": "apps/web"` makes
89
+ * every invocation that loads this config scan that subproject
90
+ * without anyone needing to `cd` first or pass an explicit path.
91
+ *
92
+ * Ignored if the resolved path does not exist or is not a directory
93
+ * (a warning is emitted and react-doctor falls back to the originally
94
+ * requested directory).
95
+ */
96
+ rootDir?: string;
78
97
  textComponents?: string[];
79
98
  /**
80
99
  * Names of components that safely route string-only children through a
@@ -211,6 +230,34 @@ declare const filterSourceFiles: (filePaths: string[]) => string[];
211
230
  //#region src/utils/summarize-diagnostics.d.ts
212
231
  declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: number | null, worstScoreLabel?: string | null) => JsonReportSummary;
213
232
  //#endregion
233
+ //#region src/errors.d.ts
234
+ declare class ReactDoctorError extends Error {
235
+ readonly name: string;
236
+ constructor(message: string, options?: ErrorOptions);
237
+ }
238
+ declare class ProjectNotFoundError extends ReactDoctorError {
239
+ readonly name = "ProjectNotFoundError";
240
+ readonly directory: string;
241
+ constructor(directory: string, options?: ErrorOptions);
242
+ }
243
+ declare class NoReactDependencyError extends ReactDoctorError {
244
+ readonly name = "NoReactDependencyError";
245
+ readonly directory: string;
246
+ constructor(directory: string, options?: ErrorOptions);
247
+ }
248
+ declare class PackageJsonNotFoundError extends ReactDoctorError {
249
+ readonly name = "PackageJsonNotFoundError";
250
+ readonly directory: string;
251
+ constructor(directory: string, options?: ErrorOptions);
252
+ }
253
+ declare class AmbiguousProjectError extends ReactDoctorError {
254
+ readonly name = "AmbiguousProjectError";
255
+ readonly directory: string;
256
+ readonly candidates: readonly string[];
257
+ constructor(directory: string, candidates: readonly string[], options?: ErrorOptions);
258
+ }
259
+ declare const isReactDoctorError: (value: unknown) => value is ReactDoctorError;
260
+ //#endregion
214
261
  //#region src/index.d.ts
215
262
  declare const clearCaches: () => void;
216
263
  interface ToJsonReportOptions {
@@ -221,5 +268,5 @@ interface ToJsonReportOptions {
221
268
  declare const toJsonReport: (result: DiagnoseResult, options: ToJsonReportOptions) => JsonReport;
222
269
  declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>;
223
270
  //#endregion
224
- export { type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, type ProjectInfo, type ReactDoctorConfig, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, summarizeDiagnostics, toJsonReport };
271
+ export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, PackageJsonNotFoundError, type ProjectInfo, ProjectNotFoundError, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isReactDoctorError, summarizeDiagnostics, toJsonReport };
225
272
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -36,6 +36,50 @@ const IGNORED_DIRECTORIES = new Set([
36
36
  const PROXY_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
37
37
  const buildNoReactDependencyError = (directory) => `No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`;
38
38
  //#endregion
39
+ //#region src/errors.ts
40
+ var ReactDoctorError = class extends Error {
41
+ name = "ReactDoctorError";
42
+ constructor(message, options) {
43
+ super(message, options);
44
+ Object.setPrototypeOf(this, new.target.prototype);
45
+ }
46
+ };
47
+ var ProjectNotFoundError = class extends ReactDoctorError {
48
+ name = "ProjectNotFoundError";
49
+ directory;
50
+ constructor(directory, options) {
51
+ super(`No React project found in ${directory}. Expected a package.json at the directory root or a nested package.json with a React dependency.`, options);
52
+ this.directory = directory;
53
+ }
54
+ };
55
+ var NoReactDependencyError = class extends ReactDoctorError {
56
+ name = "NoReactDependencyError";
57
+ directory;
58
+ constructor(directory, options) {
59
+ super(buildNoReactDependencyError(directory), options);
60
+ this.directory = directory;
61
+ }
62
+ };
63
+ var PackageJsonNotFoundError = class extends ReactDoctorError {
64
+ name = "PackageJsonNotFoundError";
65
+ directory;
66
+ constructor(directory, options) {
67
+ super(`No package.json found in ${directory}`, options);
68
+ this.directory = directory;
69
+ }
70
+ };
71
+ var AmbiguousProjectError = class extends ReactDoctorError {
72
+ name = "AmbiguousProjectError";
73
+ directory;
74
+ candidates;
75
+ constructor(directory, candidates, options) {
76
+ super(`Multiple React projects found under ${directory} (${candidates.length} candidates): ${candidates.join(", ")}. Re-run diagnose() with one of those subdirectories, or iterate them yourself.`, options);
77
+ this.directory = directory;
78
+ this.candidates = candidates;
79
+ }
80
+ };
81
+ const isReactDoctorError = (value) => value instanceof ReactDoctorError;
82
+ //#endregion
39
83
  //#region src/utils/summarize-diagnostics.ts
40
84
  const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel = null) => {
41
85
  let errorCount = 0;
@@ -770,9 +814,9 @@ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogRefer
770
814
  for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
771
815
  return null;
772
816
  };
773
- const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
817
+ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicitCatalogReference) => {
774
818
  const rawVersion = collectAllDependencies(packageJson)[packageName];
775
- const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
819
+ const catalogName = explicitCatalogReference ?? (rawVersion ? extractCatalogName(rawVersion) : null);
776
820
  if (isPlainObject(packageJson.catalog)) {
777
821
  const version = resolveVersionFromCatalog(packageJson.catalog, packageName);
778
822
  if (version) return version;
@@ -789,9 +833,22 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
789
833
  }
790
834
  }
791
835
  const workspaces = packageJson.workspaces;
792
- if (workspaces && !Array.isArray(workspaces) && isPlainObject(workspaces.catalog)) {
793
- const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
794
- if (version) return version;
836
+ if (workspaces && !Array.isArray(workspaces)) {
837
+ if (isPlainObject(workspaces.catalog)) {
838
+ const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
839
+ if (version) return version;
840
+ }
841
+ if (isPlainObject(workspaces.catalogs)) {
842
+ const namedCatalog = catalogName ? workspaces.catalogs[catalogName] : void 0;
843
+ if (namedCatalog && isPlainObject(namedCatalog)) {
844
+ const version = resolveVersionFromCatalog(namedCatalog, packageName);
845
+ if (version) return version;
846
+ }
847
+ for (const catalogEntries of Object.values(workspaces.catalogs)) if (isPlainObject(catalogEntries)) {
848
+ const version = resolveVersionFromCatalog(catalogEntries, packageName);
849
+ if (version) return version;
850
+ }
851
+ }
795
852
  }
796
853
  if (rootDirectory) {
797
854
  const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
@@ -878,7 +935,8 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
878
935
  };
879
936
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
880
937
  const rootInfo = extractDependencyInfo(rootPackageJson);
881
- const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot);
938
+ const leafPackageJsonPath = path.join(directory, "package.json");
939
+ const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, isFile(leafPackageJsonPath) ? extractCatalogName(collectAllDependencies(readPackageJson(leafPackageJsonPath)).react ?? "") ?? null : null);
882
940
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
883
941
  return {
884
942
  reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
@@ -902,6 +960,45 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
902
960
  }
903
961
  return result;
904
962
  };
963
+ const REACT_DEPENDENCY_NAMES = new Set([
964
+ "react",
965
+ "react-native",
966
+ "next"
967
+ ]);
968
+ const hasReactDependency = (packageJson) => {
969
+ const allDependencies = collectAllDependencies(packageJson);
970
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
971
+ };
972
+ const discoverReactSubprojects = (rootDirectory) => {
973
+ if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
974
+ const packages = [];
975
+ const rootPackageJsonPath = path.join(rootDirectory, "package.json");
976
+ if (isFile(rootPackageJsonPath)) {
977
+ const rootPackageJson = readPackageJson(rootPackageJsonPath);
978
+ if (hasReactDependency(rootPackageJson)) {
979
+ const name = rootPackageJson.name ?? path.basename(rootDirectory);
980
+ packages.push({
981
+ name,
982
+ directory: rootDirectory
983
+ });
984
+ }
985
+ }
986
+ const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
987
+ for (const entry of entries) {
988
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
989
+ const subdirectory = path.join(rootDirectory, entry.name);
990
+ const packageJsonPath = path.join(subdirectory, "package.json");
991
+ if (!isFile(packageJsonPath)) continue;
992
+ const packageJson = readPackageJson(packageJsonPath);
993
+ if (!hasReactDependency(packageJson)) continue;
994
+ const name = packageJson.name ?? entry.name;
995
+ packages.push({
996
+ name,
997
+ directory: subdirectory
998
+ });
999
+ }
1000
+ return packages;
1001
+ };
905
1002
  const hasCompilerPackage = (packageJson) => {
906
1003
  const allDependencies = collectAllDependencies(packageJson);
907
1004
  return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
@@ -942,15 +1039,16 @@ const discoverProject = (directory) => {
942
1039
  const cached = cachedProjectInfos.get(directory);
943
1040
  if (cached !== void 0) return cached;
944
1041
  const packageJsonPath = path.join(directory, "package.json");
945
- if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
1042
+ if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
946
1043
  const packageJson = readPackageJson(packageJsonPath);
947
1044
  let { reactVersion, framework } = extractDependencyInfo(packageJson);
948
- if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory);
1045
+ const leafCatalogReference = extractCatalogName(collectAllDependencies(packageJson).react ?? "") ?? null;
1046
+ if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory, leafCatalogReference);
949
1047
  if (!reactVersion) {
950
1048
  const monorepoRoot = findMonorepoRoot(directory);
951
1049
  if (monorepoRoot) {
952
1050
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
953
- if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
1051
+ if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot, leafCatalogReference);
954
1052
  }
955
1053
  }
956
1054
  if (!reactVersion || framework === "unknown") {
@@ -996,6 +1094,7 @@ const BOOLEAN_FIELD_NAMES = [
996
1094
  "respectInlineDisables",
997
1095
  "adoptExistingLintConfig"
998
1096
  ];
1097
+ const STRING_FIELD_NAMES = ["rootDir"];
999
1098
  const warnConfigField$1 = (message) => {
1000
1099
  process.stderr.write(`[react-doctor] ${message}\n`);
1001
1100
  };
@@ -1011,6 +1110,10 @@ const coerceMaybeBooleanString = (fieldName, value) => {
1011
1110
  }
1012
1111
  warnConfigField$1(`config field "${fieldName}" must be a boolean (got ${typeof value}); ignoring this field.`);
1013
1112
  };
1113
+ const validateString = (fieldName, value) => {
1114
+ if (typeof value === "string") return value;
1115
+ warnConfigField$1(`config field "${fieldName}" must be a string (got ${typeof value}); ignoring this field.`);
1116
+ };
1014
1117
  const validateConfigTypes = (config) => {
1015
1118
  const validated = { ...config };
1016
1119
  for (const fieldName of BOOLEAN_FIELD_NAMES) {
@@ -1020,6 +1123,13 @@ const validateConfigTypes = (config) => {
1020
1123
  if (coerced === void 0) delete validated[fieldName];
1021
1124
  else validated[fieldName] = coerced;
1022
1125
  }
1126
+ for (const fieldName of STRING_FIELD_NAMES) {
1127
+ const original = config[fieldName];
1128
+ if (original === void 0) continue;
1129
+ const validatedString = validateString(fieldName, original);
1130
+ if (validatedString === void 0) delete validated[fieldName];
1131
+ else validated[fieldName] = validatedString;
1132
+ }
1023
1133
  return validated;
1024
1134
  };
1025
1135
  //#endregion
@@ -1031,7 +1141,10 @@ const loadConfigFromDirectory = (directory) => {
1031
1141
  if (isFile(configFilePath)) try {
1032
1142
  const fileContent = fs.readFileSync(configFilePath, "utf-8");
1033
1143
  const parsed = JSON.parse(fileContent);
1034
- if (isPlainObject(parsed)) return validateConfigTypes(parsed);
1144
+ if (isPlainObject(parsed)) return {
1145
+ config: validateConfigTypes(parsed),
1146
+ sourceDirectory: directory
1147
+ };
1035
1148
  logger.warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
1036
1149
  } catch (error) {
1037
1150
  logger.warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1042,7 +1155,10 @@ const loadConfigFromDirectory = (directory) => {
1042
1155
  const packageJson = JSON.parse(fileContent);
1043
1156
  if (isPlainObject(packageJson)) {
1044
1157
  const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
1045
- if (isPlainObject(embeddedConfig)) return validateConfigTypes(embeddedConfig);
1158
+ if (isPlainObject(embeddedConfig)) return {
1159
+ config: validateConfigTypes(embeddedConfig),
1160
+ sourceDirectory: directory
1161
+ };
1046
1162
  }
1047
1163
  } catch {
1048
1164
  return null;
@@ -1054,7 +1170,7 @@ const cachedConfigs = /* @__PURE__ */ new Map();
1054
1170
  const clearConfigCache = () => {
1055
1171
  cachedConfigs.clear();
1056
1172
  };
1057
- const loadConfig = (rootDirectory) => {
1173
+ const loadConfigWithSource = (rootDirectory) => {
1058
1174
  const cached = cachedConfigs.get(rootDirectory);
1059
1175
  if (cached !== void 0) return cached;
1060
1176
  const localConfig = loadConfigFromDirectory(rootDirectory);
@@ -1261,7 +1377,7 @@ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
1261
1377
  };
1262
1378
  //#endregion
1263
1379
  //#region src/utils/find-stacked-disable-comments.ts
1264
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([\w/\-.,\s]+?))?\s*(?:\*\/)?\s*\}?\s*$/;
1380
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
1265
1381
  const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
1266
1382
  const collected = [];
1267
1383
  let isStillInChain = true;
@@ -1283,13 +1399,21 @@ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
1283
1399
  };
1284
1400
  //#endregion
1285
1401
  //#region src/utils/is-rule-listed-in-comment.ts
1402
+ const stripDescriptionTail = (ruleList) => {
1403
+ const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
1404
+ if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
1405
+ return ruleList.slice(0, descriptionMatch.index);
1406
+ };
1286
1407
  const isRuleListedInComment = (ruleList, ruleId) => {
1287
- if (!ruleList?.trim()) return true;
1288
- return ruleList.split(/[,\s]+/).some((token) => token.trim() === ruleId);
1408
+ const trimmed = ruleList?.trim();
1409
+ if (!trimmed) return true;
1410
+ const ruleSection = stripDescriptionTail(trimmed).trim();
1411
+ if (!ruleSection) return true;
1412
+ return ruleSection.split(/[,\s]+/).some((token) => token.trim() === ruleId);
1289
1413
  };
1290
1414
  //#endregion
1291
1415
  //#region src/utils/evaluate-suppression.ts
1292
- const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([\w/\-.,\s]+?))?\s*(?:\*\/)?\s*\}?\s*$/;
1416
+ const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
1293
1417
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
1294
1418
  const hasChainSuppressor = (comments, ruleId) => comments.some((comment) => comment.isInChain && isRuleListedInComment(comment.ruleList, ruleId));
1295
1419
  const findAdjacentRuleListMismatch = (comments, ruleId) => comments.find((comment) => comment.isInChain && Boolean(comment.ruleList?.trim()) && !isRuleListedInComment(comment.ruleList, ruleId));
@@ -1514,6 +1638,31 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
1514
1638
  };
1515
1639
  };
1516
1640
  //#endregion
1641
+ //#region src/utils/resolve-config-root-dir.ts
1642
+ const resolveConfigRootDir = (config, configSourceDirectory) => {
1643
+ if (!config || !configSourceDirectory) return null;
1644
+ const rawRootDir = config.rootDir;
1645
+ if (typeof rawRootDir !== "string") return null;
1646
+ const trimmedRootDir = rawRootDir.trim();
1647
+ if (trimmedRootDir.length === 0) return null;
1648
+ const resolvedRootDir = path.isAbsolute(trimmedRootDir) ? trimmedRootDir : path.resolve(configSourceDirectory, trimmedRootDir);
1649
+ if (resolvedRootDir === configSourceDirectory) return null;
1650
+ if (!fs.existsSync(resolvedRootDir) || !fs.statSync(resolvedRootDir).isDirectory()) {
1651
+ logger.warn(`react-doctor config "rootDir" points to "${rawRootDir}" (resolved to ${resolvedRootDir}), which is not a directory. Ignoring.`);
1652
+ return null;
1653
+ }
1654
+ return resolvedRootDir;
1655
+ };
1656
+ //#endregion
1657
+ //#region src/utils/resolve-diagnose-target.ts
1658
+ const resolveDiagnoseTarget = (directory) => {
1659
+ if (isFile(path.join(directory, "package.json"))) return directory;
1660
+ const reactSubprojects = discoverReactSubprojects(directory);
1661
+ if (reactSubprojects.length === 0) return null;
1662
+ if (reactSubprojects.length === 1) return reactSubprojects[0].directory;
1663
+ throw new AmbiguousProjectError(directory, reactSubprojects.map((subproject) => path.relative(directory, subproject.directory)));
1664
+ };
1665
+ //#endregion
1517
1666
  //#region src/utils/resolve-lint-include-paths.ts
1518
1667
  const listSourceFilesViaGit = (rootDirectory) => {
1519
1668
  const result = spawnSync("git", [
@@ -1923,6 +2072,16 @@ const TANSTACK_START_RULES = {
1923
2072
  "react-doctor/tanstack-start-redirect-in-try-catch": "warn",
1924
2073
  "react-doctor/tanstack-start-loader-parallel-fetch": "warn"
1925
2074
  };
2075
+ const YOU_MIGHT_NOT_NEED_EFFECT_RULES = {
2076
+ "effect/no-derived-state": "warn",
2077
+ "effect/no-chain-state-updates": "warn",
2078
+ "effect/no-event-handler": "warn",
2079
+ "effect/no-adjust-state-on-prop-change": "warn",
2080
+ "effect/no-reset-all-state-on-prop-change": "warn",
2081
+ "effect/no-pass-live-state-to-parent": "warn",
2082
+ "effect/no-pass-data-to-parent": "warn",
2083
+ "effect/no-initialize-state": "warn"
2084
+ };
1926
2085
  const REACT_COMPILER_RULES = {
1927
2086
  "react-hooks-js/set-state-in-render": "error",
1928
2087
  "react-hooks-js/immutability": "error",
@@ -1967,6 +2126,23 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
1967
2126
  availableRuleNames: readPluginRuleNames(pluginSpecifier)
1968
2127
  };
1969
2128
  };
2129
+ const YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE = "effect";
2130
+ const resolveYouMightNotNeedEffectPlugin = (customRulesOnly) => {
2131
+ if (customRulesOnly) return null;
2132
+ let pluginSpecifier;
2133
+ try {
2134
+ pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-you-might-not-need-an-effect");
2135
+ } catch {
2136
+ return null;
2137
+ }
2138
+ return {
2139
+ entry: {
2140
+ name: YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE,
2141
+ specifier: pluginSpecifier
2142
+ },
2143
+ availableRuleNames: readPluginRuleNames(pluginSpecifier)
2144
+ };
2145
+ };
1970
2146
  const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
1971
2147
  if (availableRuleNames.size === 0) return rules;
1972
2148
  const ruleKeyPrefix = `${pluginNamespace}/`;
@@ -2177,6 +2353,11 @@ const filterRulesByReactMajor = (rules, reactMajorVersion) => {
2177
2353
  const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false, reactMajorVersion = null, extendsPaths = [] }) => {
2178
2354
  const reactHooksJsPlugin = resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly);
2179
2355
  const reactCompilerRules = reactHooksJsPlugin ? filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames) : {};
2356
+ const youMightNotNeedEffectPlugin = resolveYouMightNotNeedEffectPlugin(customRulesOnly);
2357
+ const youMightNotNeedEffectRules = youMightNotNeedEffectPlugin ? filterRulesToAvailable(YOU_MIGHT_NOT_NEED_EFFECT_RULES, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE, youMightNotNeedEffectPlugin.availableRuleNames) : {};
2358
+ const jsPlugins = [];
2359
+ if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
2360
+ if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
2180
2361
  return {
2181
2362
  ...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
2182
2363
  categories: {
@@ -2189,11 +2370,12 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanSta
2189
2370
  nursery: "off"
2190
2371
  },
2191
2372
  plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
2192
- jsPlugins: reactHooksJsPlugin ? [reactHooksJsPlugin.entry, pluginPath] : [pluginPath],
2373
+ jsPlugins: [...jsPlugins, pluginPath],
2193
2374
  rules: {
2194
2375
  ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
2195
2376
  ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
2196
2377
  ...reactCompilerRules,
2378
+ ...youMightNotNeedEffectRules,
2197
2379
  ...filterRulesByReactMajor(GLOBAL_REACT_DOCTOR_RULES, reactMajorVersion),
2198
2380
  ...framework === "nextjs" ? NEXTJS_RULES : {},
2199
2381
  ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
@@ -2307,6 +2489,7 @@ const PLUGIN_CATEGORY_MAP = {
2307
2489
  "react-doctor": "Other",
2308
2490
  "jsx-a11y": "Accessibility",
2309
2491
  knip: "Dead Code",
2492
+ effect: "State & Effects",
2310
2493
  eslint: "Correctness",
2311
2494
  oxc: "Correctness",
2312
2495
  typescript: "Correctness",
@@ -2821,6 +3004,7 @@ const parseOxlintOutput = (stdout) => {
2821
3004
  severity: diagnostic.severity,
2822
3005
  message: cleaned.message,
2823
3006
  help: cleaned.help,
3007
+ url: diagnostic.url,
2824
3008
  line: primaryLabel?.span.line ?? 0,
2825
3009
  column: primaryLabel?.span.column ?? 0,
2826
3010
  category: resolveDiagnosticCategory(plugin, rule)
@@ -3077,12 +3261,16 @@ const settledOrEmpty = (settled, label) => {
3077
3261
  };
3078
3262
  const diagnose = async (directory, options = {}) => {
3079
3263
  const startTime = globalThis.performance.now();
3080
- const resolvedDirectory = path.resolve(directory);
3081
- const userConfig = loadConfig(resolvedDirectory);
3264
+ const requestedDirectory = path.resolve(directory);
3265
+ const initialLoadedConfig = loadConfigWithSource(requestedDirectory);
3266
+ const directoryAfterRedirect = resolveConfigRootDir(initialLoadedConfig?.config ?? null, initialLoadedConfig?.sourceDirectory ?? null) ?? requestedDirectory;
3267
+ const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect);
3268
+ if (!resolvedDirectory) throw new ProjectNotFoundError(directoryAfterRedirect);
3269
+ const userConfig = initialLoadedConfig?.config ?? loadConfigWithSource(resolvedDirectory)?.config ?? null;
3082
3270
  const includePaths = options.includePaths ?? [];
3083
3271
  const isDiffMode = includePaths.length > 0;
3084
3272
  const projectInfo = discoverProject(resolvedDirectory);
3085
- if (!projectInfo.reactVersion) throw new Error(buildNoReactDependencyError(resolvedDirectory));
3273
+ if (!projectInfo.reactVersion) throw new NoReactDependencyError(resolvedDirectory);
3086
3274
  const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
3087
3275
  const readFileLinesSync = createNodeReadFileLinesSync(resolvedDirectory);
3088
3276
  const effectiveLint = options.lint ?? userConfig?.lint ?? true;
@@ -3125,6 +3313,6 @@ const diagnose = async (directory, options = {}) => {
3125
3313
  };
3126
3314
  };
3127
3315
  //#endregion
3128
- export { buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, summarizeDiagnostics, toJsonReport };
3316
+ export { AmbiguousProjectError, NoReactDependencyError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isReactDoctorError, summarizeDiagnostics, toJsonReport };
3129
3317
 
3130
3318
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -69,14 +69,19 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/prompts": "^2.4.9",
72
- "eslint-plugin-react-hooks": "^7.1.1"
72
+ "eslint-plugin-react-hooks": "^7.1.1",
73
+ "eslint-plugin-react-you-might-not-need-an-effect": "^0.10.1"
73
74
  },
74
75
  "peerDependencies": {
75
- "eslint-plugin-react-hooks": "^6 || ^7"
76
+ "eslint-plugin-react-hooks": "^6 || ^7",
77
+ "eslint-plugin-react-you-might-not-need-an-effect": "^0.10"
76
78
  },
77
79
  "peerDependenciesMeta": {
78
80
  "eslint-plugin-react-hooks": {
79
81
  "optional": true
82
+ },
83
+ "eslint-plugin-react-you-might-not-need-an-effect": {
84
+ "optional": true
80
85
  }
81
86
  },
82
87
  "engines": {