react-doctor 0.2.11-dev.f4035fc → 0.2.12-dev.269ca17

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/README.md CHANGED
@@ -64,7 +64,7 @@ jobs:
64
64
  runs-on: ubuntu-latest
65
65
  steps:
66
66
  - uses: actions/checkout@v5
67
- - uses: millionco/react-doctor@v1
67
+ - uses: millionco/react-doctor@main
68
68
  ```
69
69
 
70
70
  React Doctor scans the files changed in the pull request, emits inline annotations, blocks on error-level findings, and updates one sticky PR comment with the score and issue summary. The built-in GitHub token is used automatically; no secret or PAT is required. On forked PRs where GitHub withholds write permissions, the scan and annotations still run, but the sticky comment may be skipped.
@@ -2281,15 +2281,91 @@ const detectFramework = (dependencies) => {
2281
2281
  if (dependencies.preact && !dependencies.react) return "preact";
2282
2282
  return "unknown";
2283
2283
  };
2284
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2285
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2286
- const OR_SEPARATOR = /\s*\|\|\s*/;
2287
2284
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2288
2285
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2289
2286
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2290
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2291
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2292
2287
  const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
2288
+ const isDigit = (value) => value !== void 0 && value >= "0" && value <= "9";
2289
+ const isWhitespace = (value) => value === " " || value === " " || value === "\n" || value === "\r" || value === "\f" || value === "\v";
2290
+ const isSeparator = (value) => isWhitespace(value) || value === "," || value === "|";
2291
+ const skipWhitespace = (value, start) => {
2292
+ let index = start;
2293
+ while (isWhitespace(value[index])) index += 1;
2294
+ return index;
2295
+ };
2296
+ const skipSeparators = (value, start) => {
2297
+ let index = start;
2298
+ while (isSeparator(value[index])) index += 1;
2299
+ return index;
2300
+ };
2301
+ const readDigits = (value, start) => {
2302
+ let index = start;
2303
+ while (isDigit(value[index])) index += 1;
2304
+ return index;
2305
+ };
2306
+ const getUpperBoundComparatorEnd = (version, start) => {
2307
+ if (version[start] !== "<") return null;
2308
+ let index = skipWhitespace(version, start + 1);
2309
+ if (version[index] === "=") index = skipWhitespace(version, index + 1);
2310
+ const majorStart = index;
2311
+ index = readDigits(version, index);
2312
+ if (index === majorStart) return null;
2313
+ for (let segments = 0; segments < 2 && version[index] === "."; segments += 1) {
2314
+ const segmentStart = index + 1;
2315
+ const segmentEnd = readDigits(version, segmentStart);
2316
+ if (segmentEnd === segmentStart) break;
2317
+ index = segmentEnd;
2318
+ }
2319
+ if (version[index] === "-") {
2320
+ index += 1;
2321
+ while (index < version.length && !isSeparator(version[index])) index += 1;
2322
+ }
2323
+ return index;
2324
+ };
2325
+ const stripUpperBoundComparators = (version) => {
2326
+ let stripped = "";
2327
+ let index = 0;
2328
+ while (index < version.length) {
2329
+ const comparatorEnd = getUpperBoundComparatorEnd(version, index);
2330
+ if (comparatorEnd === null) {
2331
+ stripped += version[index];
2332
+ index += 1;
2333
+ continue;
2334
+ }
2335
+ stripped += " ";
2336
+ index = comparatorEnd;
2337
+ }
2338
+ return stripped;
2339
+ };
2340
+ const hasNonLowerBoundComparator = (branch) => {
2341
+ for (let index = 0; index < branch.length; index += 1) {
2342
+ if (index > 0 && !isSeparator(branch[index - 1])) continue;
2343
+ if (branch[index] === ">" && branch[index + 1] !== "=") {
2344
+ if (isDigit(branch[skipWhitespace(branch, index + 1)])) return true;
2345
+ continue;
2346
+ }
2347
+ if (branch[index] !== "!") continue;
2348
+ let valueIndex = index + 1;
2349
+ if (branch[valueIndex] === "=") valueIndex += 1;
2350
+ if (branch[valueIndex] === "=") valueIndex += 1;
2351
+ valueIndex = skipWhitespace(branch, valueIndex);
2352
+ if (isDigit(branch[valueIndex])) return true;
2353
+ }
2354
+ return false;
2355
+ };
2356
+ const isMajorTerminator = (value) => value === void 0 || isSeparator(value) || value === "." || value === "*" || value === "x" || value === "X" || value === "-";
2357
+ const getLowerBoundMajorAt = (branch, start) => {
2358
+ let index = start;
2359
+ if (branch[index] === ">" && branch[index + 1] === "=") index = skipWhitespace(branch, index + 2);
2360
+ else if (branch[index] === "~" || branch[index] === "^" || branch[index] === "=" || branch[index] === "v") index = skipWhitespace(branch, index + 1);
2361
+ const majorStart = index;
2362
+ const majorEnd = readDigits(branch, majorStart);
2363
+ if (majorEnd === majorStart || !isMajorTerminator(branch[majorEnd])) return null;
2364
+ return {
2365
+ end: majorEnd,
2366
+ major: Number.parseInt(branch.slice(majorStart, majorEnd), 10)
2367
+ };
2368
+ };
2293
2369
  const normalizeDependencyVersion = (version) => {
2294
2370
  const trimmed = version.trim();
2295
2371
  if (trimmed.length === 0) return null;
@@ -2299,17 +2375,29 @@ const normalizeDependencyVersion = (version) => {
2299
2375
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2300
2376
  return normalizedVersion;
2301
2377
  };
2302
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2303
- const hasUpperBoundComparator = (version) => HAS_UPPER_BOUND_COMPARATOR.test(version);
2378
+ const splitDependencyVersionBranches = (version) => version.split("||").map((branch) => branch.trim()).filter(Boolean);
2379
+ const hasUpperBoundComparator = (version) => {
2380
+ for (let index = 0; index < version.length; index += 1) if (getUpperBoundComparatorEnd(version, index) !== null) return true;
2381
+ return false;
2382
+ };
2304
2383
  const getBranchLowestMajor = (branch) => {
2305
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2306
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2384
+ if (hasNonLowerBoundComparator(branch)) return null;
2385
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2307
2386
  if (lowerBoundComparators.length === 0) return null;
2308
2387
  let branchLowestMajor = null;
2309
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2310
- const major = Number.parseInt(match[1], 10);
2311
- if (!Number.isFinite(major) || major <= 0) continue;
2312
- if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2388
+ let index = 0;
2389
+ while (index < lowerBoundComparators.length) {
2390
+ const lowerBoundStart = skipSeparators(lowerBoundComparators, index);
2391
+ if (lowerBoundStart > 0 && !isSeparator(lowerBoundComparators[lowerBoundStart - 1])) {
2392
+ index = lowerBoundStart + 1;
2393
+ continue;
2394
+ }
2395
+ const lowerBoundMajor = getLowerBoundMajorAt(lowerBoundComparators, lowerBoundStart);
2396
+ if (lowerBoundMajor !== null && Number.isFinite(lowerBoundMajor.major) && lowerBoundMajor.major > 0) {
2397
+ const major = lowerBoundMajor.major;
2398
+ if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2399
+ }
2400
+ index = lowerBoundMajor?.end ?? lowerBoundStart + 1;
2313
2401
  }
2314
2402
  return branchLowestMajor;
2315
2403
  };
@@ -2478,6 +2566,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2478
2566
  const EMPTY_DEPENDENCY_INFO = {
2479
2567
  reactVersion: null,
2480
2568
  tailwindVersion: null,
2569
+ zodVersion: null,
2481
2570
  framework: "unknown"
2482
2571
  };
2483
2572
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2506,6 +2595,11 @@ const extractDependencyInfo = (packageJson) => {
2506
2595
  "devDependencies",
2507
2596
  "peerDependencies"
2508
2597
  ]),
2598
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2599
+ "dependencies",
2600
+ "devDependencies",
2601
+ "peerDependencies"
2602
+ ]),
2509
2603
  framework: detectFramework(allDependencies)
2510
2604
  };
2511
2605
  };
@@ -2650,8 +2744,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2650
2744
  workspaceDirectory,
2651
2745
  workspacePackageJson
2652
2746
  });
2747
+ const zodVersion = resolveWorkspaceDependencyVersion({
2748
+ concreteVersion: info.zodVersion,
2749
+ packageName: "zod",
2750
+ rootDirectory,
2751
+ rootPackageJson: packageJson,
2752
+ sections: [
2753
+ "dependencies",
2754
+ "devDependencies",
2755
+ "peerDependencies"
2756
+ ],
2757
+ workspaceDirectory,
2758
+ workspacePackageJson
2759
+ });
2653
2760
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2654
2761
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2762
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2655
2763
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2656
2764
  const resultReactMajor = parseReactMajor(result.reactVersion);
2657
2765
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2686,14 +2794,26 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2686
2794
  "peerDependencies"
2687
2795
  ]
2688
2796
  }) : null;
2797
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2798
+ packageJson: leafPackageJson,
2799
+ packageName: "zod",
2800
+ sections: [
2801
+ "dependencies",
2802
+ "devDependencies",
2803
+ "peerDependencies"
2804
+ ]
2805
+ }) : null;
2689
2806
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2690
2807
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2808
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2691
2809
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2692
2810
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2811
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2693
2812
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2694
2813
  return {
2695
2814
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2696
2815
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2816
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2697
2817
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2698
2818
  };
2699
2819
  };
@@ -2773,6 +2893,10 @@ const isPackageJsonReanimatedAware = (packageJson) => {
2773
2893
  };
2774
2894
  return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2775
2895
  };
2896
+ const parseZodMajor = (zodVersion) => {
2897
+ if (typeof zodVersion !== "string") return null;
2898
+ return getLowestDependencyMajor(zodVersion);
2899
+ };
2776
2900
  const hasUpperBoundOnlyPeerRange = (range) => {
2777
2901
  if (typeof range !== "string") return false;
2778
2902
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2911,7 +3035,7 @@ const discoverProject = (directory) => {
2911
3035
  const packageJsonPath = path.join(directory, "package.json");
2912
3036
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2913
3037
  const packageJson = readPackageJson(packageJsonPath);
2914
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3038
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2915
3039
  const reactDeclaration = getDependencyDeclaration({
2916
3040
  packageJson,
2917
3041
  packageName: "react",
@@ -2930,9 +3054,19 @@ const discoverProject = (directory) => {
2930
3054
  "peerDependencies"
2931
3055
  ]
2932
3056
  });
3057
+ const zodDeclaration = getDependencyDeclaration({
3058
+ packageJson,
3059
+ packageName: "zod",
3060
+ sections: [
3061
+ "dependencies",
3062
+ "devDependencies",
3063
+ "peerDependencies"
3064
+ ]
3065
+ });
2933
3066
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2934
3067
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2935
- if (!reactVersion || !tailwindVersion) {
3068
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3069
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2936
3070
  const monorepoRoot = findMonorepoRoot(directory);
2937
3071
  if (monorepoRoot) {
2938
3072
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2940,6 +3074,7 @@ const discoverProject = (directory) => {
2940
3074
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2941
3075
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2942
3076
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3077
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2943
3078
  }
2944
3079
  }
2945
3080
  }
@@ -2947,16 +3082,19 @@ const discoverProject = (directory) => {
2947
3082
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2948
3083
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2949
3084
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3085
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2950
3086
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2951
3087
  }
2952
3088
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2953
3089
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2954
3090
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2955
3091
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3092
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2956
3093
  if (framework === "unknown") framework = monorepoInfo.framework;
2957
3094
  }
2958
3095
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2959
3096
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3097
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2960
3098
  const projectName = packageJson.name ?? path.basename(directory);
2961
3099
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2962
3100
  const sourceFileCount = countSourceFiles(directory);
@@ -2969,6 +3107,8 @@ const discoverProject = (directory) => {
2969
3107
  reactVersion,
2970
3108
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2971
3109
  tailwindVersion,
3110
+ zodVersion,
3111
+ zodMajorVersion: parseZodMajor(zodVersion),
2972
3112
  framework,
2973
3113
  hasTypeScript,
2974
3114
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -3050,6 +3190,7 @@ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3050
3190
  const MILLISECONDS_PER_SECOND = 1e3;
3051
3191
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3052
3192
  const SHARE_BASE_URL = "https://www.react.doctor/share";
3193
+ const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
3053
3194
  const FETCH_TIMEOUT_MS = 1e4;
3054
3195
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
3055
3196
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
@@ -5491,6 +5632,10 @@ const buildCapabilities = (project) => {
5491
5632
  minor: 4
5492
5633
  })) capabilities.add("tailwind:3.4");
5493
5634
  }
5635
+ if (project.zodVersion !== null) {
5636
+ capabilities.add("zod");
5637
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5638
+ }
5494
5639
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5495
5640
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5496
5641
  if (project.hasTypeScript) capabilities.add("typescript");
@@ -7360,6 +7505,13 @@ const highlighter = {
7360
7505
  gray: import_picocolors.default.gray,
7361
7506
  bold: import_picocolors.default.bold
7362
7507
  };
7508
+ /**
7509
+ * Canonical URL for a rule's reviewer-tested fix recipe, served at
7510
+ * `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. The
7511
+ * `/doctor` playbook fetches it on demand so each fix follows the
7512
+ * canonical recipe instead of being improvised per diagnostic.
7513
+ */
7514
+ const buildRulePromptUrl = (plugin, rule) => `${PROMPTS_RULES_BASE_URL}/${plugin}/${rule}.md`;
7363
7515
  const groupBy = (items, keyFn) => {
7364
7516
  const groups = /* @__PURE__ */ new Map();
7365
7517
  for (const item of items) {
@@ -7407,6 +7559,6 @@ const cliLogger = {
7407
7559
  }
7408
7560
  };
7409
7561
  //#endregion
7410
- export { isReactDoctorError as A, filterSourceFiles as C, groupBy as D, getDiffInfo as E, runInspect as F, toRelativePath as I, listWorkspacePackages as M, resolveScanTarget as N, highlighter as O, restoreLegacyThrow as P, filterDiagnosticsForSurface as S, formatReactDoctorError as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, layerOtlp as j, isMonorepoRoot as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, formatErrorChain as w, discoverReactSubprojects as x, buildJsonReport as y };
7562
+ export { isMonorepoRoot as A, filterDiagnosticsForSurface as C, getDiffInfo as D, formatReactDoctorError as E, restoreLegacyThrow as F, runInspect as I, toRelativePath as L, layerOtlp as M, listWorkspacePackages as N, groupBy as O, resolveScanTarget as P, discoverReactSubprojects as S, formatErrorChain as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, isReactDoctorError as j, highlighter as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, filterSourceFiles as w, buildRulePromptUrl as x, buildJsonReport as y };
7411
7563
 
7412
- //# sourceMappingURL=cli-logger-Df45H6Lw.js.map
7564
+ //# sourceMappingURL=cli-logger-CSZagq1E.js.map
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
2
- import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-Df45H6Lw.js";
2
+ import { A as isMonorepoRoot, C as filterDiagnosticsForSurface, D as getDiffInfo, E as formatReactDoctorError, F as restoreLegacyThrow, I as runInspect, L as toRelativePath, M as layerOtlp, N as listWorkspacePackages, O as groupBy, P as resolveScanTarget, S as discoverReactSubprojects, T as formatErrorChain, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as isReactDoctorError, k as highlighter, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as filterSourceFiles, x as buildRulePromptUrl, y as buildJsonReport } from "./cli-logger-CSZagq1E.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, execSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
@@ -6220,6 +6220,8 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
6220
6220
  return ruleName + " ".repeat(columnWidth - ruleName.length);
6221
6221
  };
6222
6222
  const grayLine = (text) => highlighter.gray(text);
6223
+ const FETCH_FIX_RECIPE_LABEL = "Fetch & follow the canonical fix recipe before fixing";
6224
+ const formatFixRecipeLine = (diagnostic) => `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}`;
6223
6225
  const buildCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
6224
6226
  const firstDiagnostic = ruleDiagnostics[0];
6225
6227
  const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
@@ -6255,6 +6257,7 @@ const buildVerboseRuleGroupLines = (ruleKey, ruleDiagnostics, ruleNameColumnWidt
6255
6257
  const firstDiagnostic = ruleDiagnostics[0];
6256
6258
  lines.push(grayLine(indentMultilineText(firstDiagnostic.message, " ")));
6257
6259
  if (firstDiagnostic.help) lines.push(grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " ")));
6260
+ lines.push(grayLine(` ${formatFixRecipeLine(firstDiagnostic)}`));
6258
6261
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
6259
6262
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
6260
6263
  lines.push(grayLine(` ${filePath}:${site.line}`));
@@ -6299,6 +6302,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
6299
6302
  ];
6300
6303
  if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
6301
6304
  if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
6305
+ sections.push("", formatFixRecipeLine(firstDiagnostic));
6302
6306
  sections.push("", "Files:");
6303
6307
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
6304
6308
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
@@ -6539,7 +6543,11 @@ const printCountsSummaryLine = (diagnostics, isVerbose) => Effect.gen(function*
6539
6543
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
6540
6544
  const issueText = (errorCount > 0 ? highlighter.error : warningCount > 0 ? highlighter.warn : highlighter.dim)(`${totalIssueCount} ${totalIssueCount === 1 ? "issue" : "issues"}`);
6541
6545
  yield* Console.log(` ${issueText}`);
6542
- if (!isVerbose && totalIssueCount > 0) yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to see details`));
6546
+ if (!isVerbose && totalIssueCount > 0) {
6547
+ const exampleDiagnostic = diagnostics.find((diagnostic) => diagnostic.severity === "error") ?? diagnostics[0];
6548
+ yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to list every issue with its fix-recipe URL`));
6549
+ yield* Console.log(highlighter.dim(` Each rule links a canonical fix recipe to fetch & follow before fixing, e.g. ${highlighter.info(buildRulePromptUrl(exampleDiagnostic.plugin, exampleDiagnostic.rule))}`));
6550
+ }
6543
6551
  });
6544
6552
  const printSummary = (input) => Effect.gen(function* () {
6545
6553
  if (input.scoreResult) yield* printScoreHeader(input.scoreResult, input.projectName);
@@ -6666,7 +6674,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6666
6674
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6667
6675
  //#endregion
6668
6676
  //#region src/cli/utils/version.ts
6669
- const VERSION = "0.2.11-dev.f4035fc";
6677
+ const VERSION = "0.2.12-dev.269ca17";
6670
6678
  //#endregion
6671
6679
  //#region src/inspect.ts
6672
6680
  const silentConsole = makeNoopConsole();
@@ -7066,15 +7074,16 @@ const buildIssuesSummary = (input) => {
7066
7074
  if (input.score) lines.push(`Score: ${input.score.score}/100`);
7067
7075
  lines.push(`${input.diagnostics.length} issues found`);
7068
7076
  lines.push("");
7069
- const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => diagnostic.rule).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
7077
+ const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
7070
7078
  const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
7071
- for (const [rule, ruleDiagnostics] of visibleRules) {
7079
+ for (const [ruleKey, ruleDiagnostics] of visibleRules) {
7072
7080
  const severity = ruleDiagnostics[0].severity;
7073
7081
  const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
7074
7082
  const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
7075
7083
  const remainingFileCount = uniqueFiles.length - shownFiles.length;
7076
- lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${rule} (×${ruleDiagnostics.length})`);
7084
+ lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
7077
7085
  lines.push(` ${ruleDiagnostics[0].message}`);
7086
+ lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
7078
7087
  for (const filePath of shownFiles) {
7079
7088
  const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
7080
7089
  lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
@@ -7094,11 +7103,12 @@ const buildIssuesSummary = (input) => {
7094
7103
  lines.push("");
7095
7104
  lines.push("## How to fix");
7096
7105
  lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
7097
- lines.push("2. Fix errors first, then warnings. Start with high-count rules.");
7098
- lines.push("3. Read the code before acting. Treat findings as hypotheses, not commands.");
7099
- lines.push("4. Fix root causes, not symptoms. Don't suppress rules without evidence.");
7100
- lines.push("5. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
7101
- lines.push("6. Split unrelated fixes into separate PRs.");
7106
+ lines.push("2. For each rule above, fetch & follow its canonical fix recipe URL before fixing.");
7107
+ lines.push("3. Fix errors first, then warnings. Start with high-count rules.");
7108
+ lines.push("4. Read the code before acting. Treat findings as hypotheses, not commands.");
7109
+ lines.push("5. Fix root causes, not symptoms. Don't suppress rules without evidence.");
7110
+ lines.push("6. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
7111
+ lines.push("7. Split unrelated fixes into separate PRs.");
7102
7112
  return lines.join("\n");
7103
7113
  };
7104
7114
  const copyToClipboard = (text) => {
@@ -7238,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
7238
7248
  };
7239
7249
  });
7240
7250
  const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
7251
+ yield* Console.log("");
7241
7252
  for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
7242
7253
  yield* Console.log("");
7243
7254
  });
@@ -7484,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
7484
7495
  return;
7485
7496
  }
7486
7497
  try {
7487
- const { cliLogger } = await import("./cli-logger-Df45H6Lw.js").then((n) => n.n);
7498
+ const { cliLogger } = await import("./cli-logger-CSZagq1E.js").then((n) => n.n);
7488
7499
  cliLogger.warn(message);
7489
7500
  } catch {}
7490
7501
  };
@@ -8883,7 +8894,7 @@ const buildWorkflowContent = () => [
8883
8894
  " runs-on: ubuntu-latest",
8884
8895
  " steps:",
8885
8896
  " - uses: actions/checkout@v5",
8886
- " - uses: millionco/react-doctor@v1",
8897
+ " - uses: millionco/react-doctor@main",
8887
8898
  ""
8888
8899
  ].join("\n");
8889
8900
  const runInstallReactDoctor = async (options = {}) => {
package/dist/index.d.ts CHANGED
@@ -305,6 +305,9 @@ interface ProjectInfo {
305
305
  reactVersion: string | null;
306
306
  reactMajorVersion: number | null;
307
307
  tailwindVersion: string | null;
308
+ zodVersion: string | null;
309
+ /** Parsed major from `zodVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
310
+ zodMajorVersion: number | null;
308
311
  framework: Framework;
309
312
  hasTypeScript: boolean;
310
313
  hasReactCompiler: boolean;
package/dist/index.js CHANGED
@@ -2307,15 +2307,91 @@ const detectFramework = (dependencies) => {
2307
2307
  if (dependencies.preact && !dependencies.react) return "preact";
2308
2308
  return "unknown";
2309
2309
  };
2310
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2311
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2312
- const OR_SEPARATOR = /\s*\|\|\s*/;
2313
2310
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2314
2311
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2315
2312
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2316
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2317
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2318
2313
  const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
2314
+ const isDigit = (value) => value !== void 0 && value >= "0" && value <= "9";
2315
+ const isWhitespace = (value) => value === " " || value === " " || value === "\n" || value === "\r" || value === "\f" || value === "\v";
2316
+ const isSeparator = (value) => isWhitespace(value) || value === "," || value === "|";
2317
+ const skipWhitespace = (value, start) => {
2318
+ let index = start;
2319
+ while (isWhitespace(value[index])) index += 1;
2320
+ return index;
2321
+ };
2322
+ const skipSeparators = (value, start) => {
2323
+ let index = start;
2324
+ while (isSeparator(value[index])) index += 1;
2325
+ return index;
2326
+ };
2327
+ const readDigits = (value, start) => {
2328
+ let index = start;
2329
+ while (isDigit(value[index])) index += 1;
2330
+ return index;
2331
+ };
2332
+ const getUpperBoundComparatorEnd = (version, start) => {
2333
+ if (version[start] !== "<") return null;
2334
+ let index = skipWhitespace(version, start + 1);
2335
+ if (version[index] === "=") index = skipWhitespace(version, index + 1);
2336
+ const majorStart = index;
2337
+ index = readDigits(version, index);
2338
+ if (index === majorStart) return null;
2339
+ for (let segments = 0; segments < 2 && version[index] === "."; segments += 1) {
2340
+ const segmentStart = index + 1;
2341
+ const segmentEnd = readDigits(version, segmentStart);
2342
+ if (segmentEnd === segmentStart) break;
2343
+ index = segmentEnd;
2344
+ }
2345
+ if (version[index] === "-") {
2346
+ index += 1;
2347
+ while (index < version.length && !isSeparator(version[index])) index += 1;
2348
+ }
2349
+ return index;
2350
+ };
2351
+ const stripUpperBoundComparators = (version) => {
2352
+ let stripped = "";
2353
+ let index = 0;
2354
+ while (index < version.length) {
2355
+ const comparatorEnd = getUpperBoundComparatorEnd(version, index);
2356
+ if (comparatorEnd === null) {
2357
+ stripped += version[index];
2358
+ index += 1;
2359
+ continue;
2360
+ }
2361
+ stripped += " ";
2362
+ index = comparatorEnd;
2363
+ }
2364
+ return stripped;
2365
+ };
2366
+ const hasNonLowerBoundComparator = (branch) => {
2367
+ for (let index = 0; index < branch.length; index += 1) {
2368
+ if (index > 0 && !isSeparator(branch[index - 1])) continue;
2369
+ if (branch[index] === ">" && branch[index + 1] !== "=") {
2370
+ if (isDigit(branch[skipWhitespace(branch, index + 1)])) return true;
2371
+ continue;
2372
+ }
2373
+ if (branch[index] !== "!") continue;
2374
+ let valueIndex = index + 1;
2375
+ if (branch[valueIndex] === "=") valueIndex += 1;
2376
+ if (branch[valueIndex] === "=") valueIndex += 1;
2377
+ valueIndex = skipWhitespace(branch, valueIndex);
2378
+ if (isDigit(branch[valueIndex])) return true;
2379
+ }
2380
+ return false;
2381
+ };
2382
+ const isMajorTerminator = (value) => value === void 0 || isSeparator(value) || value === "." || value === "*" || value === "x" || value === "X" || value === "-";
2383
+ const getLowerBoundMajorAt = (branch, start) => {
2384
+ let index = start;
2385
+ if (branch[index] === ">" && branch[index + 1] === "=") index = skipWhitespace(branch, index + 2);
2386
+ else if (branch[index] === "~" || branch[index] === "^" || branch[index] === "=" || branch[index] === "v") index = skipWhitespace(branch, index + 1);
2387
+ const majorStart = index;
2388
+ const majorEnd = readDigits(branch, majorStart);
2389
+ if (majorEnd === majorStart || !isMajorTerminator(branch[majorEnd])) return null;
2390
+ return {
2391
+ end: majorEnd,
2392
+ major: Number.parseInt(branch.slice(majorStart, majorEnd), 10)
2393
+ };
2394
+ };
2319
2395
  const normalizeDependencyVersion = (version) => {
2320
2396
  const trimmed = version.trim();
2321
2397
  if (trimmed.length === 0) return null;
@@ -2325,17 +2401,29 @@ const normalizeDependencyVersion = (version) => {
2325
2401
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2326
2402
  return normalizedVersion;
2327
2403
  };
2328
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2329
- const hasUpperBoundComparator = (version) => HAS_UPPER_BOUND_COMPARATOR.test(version);
2404
+ const splitDependencyVersionBranches = (version) => version.split("||").map((branch) => branch.trim()).filter(Boolean);
2405
+ const hasUpperBoundComparator = (version) => {
2406
+ for (let index = 0; index < version.length; index += 1) if (getUpperBoundComparatorEnd(version, index) !== null) return true;
2407
+ return false;
2408
+ };
2330
2409
  const getBranchLowestMajor = (branch) => {
2331
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2332
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2410
+ if (hasNonLowerBoundComparator(branch)) return null;
2411
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2333
2412
  if (lowerBoundComparators.length === 0) return null;
2334
2413
  let branchLowestMajor = null;
2335
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2336
- const major = Number.parseInt(match[1], 10);
2337
- if (!Number.isFinite(major) || major <= 0) continue;
2338
- if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2414
+ let index = 0;
2415
+ while (index < lowerBoundComparators.length) {
2416
+ const lowerBoundStart = skipSeparators(lowerBoundComparators, index);
2417
+ if (lowerBoundStart > 0 && !isSeparator(lowerBoundComparators[lowerBoundStart - 1])) {
2418
+ index = lowerBoundStart + 1;
2419
+ continue;
2420
+ }
2421
+ const lowerBoundMajor = getLowerBoundMajorAt(lowerBoundComparators, lowerBoundStart);
2422
+ if (lowerBoundMajor !== null && Number.isFinite(lowerBoundMajor.major) && lowerBoundMajor.major > 0) {
2423
+ const major = lowerBoundMajor.major;
2424
+ if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2425
+ }
2426
+ index = lowerBoundMajor?.end ?? lowerBoundStart + 1;
2339
2427
  }
2340
2428
  return branchLowestMajor;
2341
2429
  };
@@ -2504,6 +2592,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2504
2592
  const EMPTY_DEPENDENCY_INFO = {
2505
2593
  reactVersion: null,
2506
2594
  tailwindVersion: null,
2595
+ zodVersion: null,
2507
2596
  framework: "unknown"
2508
2597
  };
2509
2598
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2532,6 +2621,11 @@ const extractDependencyInfo = (packageJson) => {
2532
2621
  "devDependencies",
2533
2622
  "peerDependencies"
2534
2623
  ]),
2624
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2625
+ "dependencies",
2626
+ "devDependencies",
2627
+ "peerDependencies"
2628
+ ]),
2535
2629
  framework: detectFramework(allDependencies)
2536
2630
  };
2537
2631
  };
@@ -2676,8 +2770,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2676
2770
  workspaceDirectory,
2677
2771
  workspacePackageJson
2678
2772
  });
2773
+ const zodVersion = resolveWorkspaceDependencyVersion({
2774
+ concreteVersion: info.zodVersion,
2775
+ packageName: "zod",
2776
+ rootDirectory,
2777
+ rootPackageJson: packageJson,
2778
+ sections: [
2779
+ "dependencies",
2780
+ "devDependencies",
2781
+ "peerDependencies"
2782
+ ],
2783
+ workspaceDirectory,
2784
+ workspacePackageJson
2785
+ });
2679
2786
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2680
2787
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2788
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2681
2789
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2682
2790
  const resultReactMajor = parseReactMajor(result.reactVersion);
2683
2791
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2712,14 +2820,26 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2712
2820
  "peerDependencies"
2713
2821
  ]
2714
2822
  }) : null;
2823
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2824
+ packageJson: leafPackageJson,
2825
+ packageName: "zod",
2826
+ sections: [
2827
+ "dependencies",
2828
+ "devDependencies",
2829
+ "peerDependencies"
2830
+ ]
2831
+ }) : null;
2715
2832
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2716
2833
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2834
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2717
2835
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2718
2836
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2837
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2719
2838
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2720
2839
  return {
2721
2840
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2722
2841
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2842
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2723
2843
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2724
2844
  };
2725
2845
  };
@@ -2799,6 +2919,10 @@ const isPackageJsonReanimatedAware = (packageJson) => {
2799
2919
  };
2800
2920
  return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2801
2921
  };
2922
+ const parseZodMajor = (zodVersion) => {
2923
+ if (typeof zodVersion !== "string") return null;
2924
+ return getLowestDependencyMajor(zodVersion);
2925
+ };
2802
2926
  const hasUpperBoundOnlyPeerRange = (range) => {
2803
2927
  if (typeof range !== "string") return false;
2804
2928
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2940,7 +3064,7 @@ const discoverProject = (directory) => {
2940
3064
  const packageJsonPath = path.join(directory, "package.json");
2941
3065
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2942
3066
  const packageJson = readPackageJson(packageJsonPath);
2943
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3067
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2944
3068
  const reactDeclaration = getDependencyDeclaration({
2945
3069
  packageJson,
2946
3070
  packageName: "react",
@@ -2959,9 +3083,19 @@ const discoverProject = (directory) => {
2959
3083
  "peerDependencies"
2960
3084
  ]
2961
3085
  });
3086
+ const zodDeclaration = getDependencyDeclaration({
3087
+ packageJson,
3088
+ packageName: "zod",
3089
+ sections: [
3090
+ "dependencies",
3091
+ "devDependencies",
3092
+ "peerDependencies"
3093
+ ]
3094
+ });
2962
3095
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2963
3096
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2964
- if (!reactVersion || !tailwindVersion) {
3097
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3098
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2965
3099
  const monorepoRoot = findMonorepoRoot(directory);
2966
3100
  if (monorepoRoot) {
2967
3101
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2969,6 +3103,7 @@ const discoverProject = (directory) => {
2969
3103
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2970
3104
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2971
3105
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3106
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2972
3107
  }
2973
3108
  }
2974
3109
  }
@@ -2976,16 +3111,19 @@ const discoverProject = (directory) => {
2976
3111
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2977
3112
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2978
3113
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3114
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2979
3115
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2980
3116
  }
2981
3117
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2982
3118
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2983
3119
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2984
3120
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3121
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2985
3122
  if (framework === "unknown") framework = monorepoInfo.framework;
2986
3123
  }
2987
3124
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2988
3125
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3126
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2989
3127
  const projectName = packageJson.name ?? path.basename(directory);
2990
3128
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2991
3129
  const sourceFileCount = countSourceFiles(directory);
@@ -2998,6 +3136,8 @@ const discoverProject = (directory) => {
2998
3136
  reactVersion,
2999
3137
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
3000
3138
  tailwindVersion,
3139
+ zodVersion,
3140
+ zodMajorVersion: parseZodMajor(zodVersion),
3001
3141
  framework,
3002
3142
  hasTypeScript,
3003
3143
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -5522,6 +5662,10 @@ const buildCapabilities = (project) => {
5522
5662
  minor: 4
5523
5663
  })) capabilities.add("tailwind:3.4");
5524
5664
  }
5665
+ if (project.zodVersion !== null) {
5666
+ capabilities.add("zod");
5667
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5668
+ }
5525
5669
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5526
5670
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5527
5671
  if (project.hasTypeScript) capabilities.add("typescript");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.11-dev.f4035fc",
3
+ "version": "0.2.12-dev.269ca17",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -58,14 +58,14 @@
58
58
  "oxlint": "^1.66.0",
59
59
  "prompts": "^2.4.2",
60
60
  "typescript": ">=5.0.4 <7",
61
- "oxlint-plugin-react-doctor": "0.2.11-dev.f4035fc"
61
+ "oxlint-plugin-react-doctor": "0.2.12-dev.269ca17"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/prompts": "^2.4.9",
65
65
  "commander": "^14.0.3",
66
66
  "ora": "^9.4.0",
67
- "@react-doctor/core": "0.2.11",
68
- "@react-doctor/api": "0.2.11"
67
+ "@react-doctor/api": "0.2.12",
68
+ "@react-doctor/core": "0.2.12"
69
69
  },
70
70
  "engines": {
71
71
  "node": "^20.19.0 || >=22.12.0"