react-doctor 0.2.11-dev.402c7ea → 0.2.11-dev.b2934f9

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
@@ -43,6 +43,32 @@ Works with Claude Code, Cursor, Codex, OpenCode, and many more.
43
43
 
44
44
  Add the reusable GitHub Action from Marketplace to scan every pull request, show inline annotations, and leave findings where reviewers already look.
45
45
 
46
+ ```yaml
47
+ name: React Doctor
48
+
49
+ on:
50
+ pull_request:
51
+ types: [opened, synchronize, reopened, ready_for_review]
52
+
53
+ permissions:
54
+ contents: read
55
+ pull-requests: write
56
+ issues: write
57
+
58
+ concurrency:
59
+ group: react-doctor-${{ github.event.pull_request.number || github.ref }}
60
+ cancel-in-progress: true
61
+
62
+ jobs:
63
+ react-doctor:
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v5
67
+ - uses: millionco/react-doctor@v1
68
+ ```
69
+
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.
71
+
46
72
  [Add GitHub Action →](https://github.com/marketplace/actions/react-doctor)
47
73
 
48
74
  ## Contributing
@@ -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,9 +1,9 @@
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";
6
- import { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
6
+ import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import process$1 from "node:process";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Layer from "effect/Layer";
@@ -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.402c7ea";
6677
+ const VERSION = "0.2.11-dev.b2934f9";
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) => {
@@ -7153,6 +7163,29 @@ const promptCopyIssues = async (input) => {
7153
7163
  else cliLogger.log(issuesSummary);
7154
7164
  };
7155
7165
  //#endregion
7166
+ //#region src/cli/utils/path-format.ts
7167
+ const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
7168
+ //#endregion
7169
+ //#region src/cli/utils/read-changed-files-from.ts
7170
+ const isSafeRelativePath = (filePath) => {
7171
+ if (filePath.length === 0) return false;
7172
+ if (filePath.includes("\0")) return false;
7173
+ if (path.isAbsolute(filePath)) return false;
7174
+ const normalized = path.posix.normalize(filePath);
7175
+ if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
7176
+ return normalized === filePath;
7177
+ };
7178
+ const readChangedFilesFrom = (filePath) => {
7179
+ const raw = fs.readFileSync(filePath, "utf8");
7180
+ const uniqueFiles = /* @__PURE__ */ new Set();
7181
+ for (const line of raw.split(/\r?\n/)) {
7182
+ const candidate = toForwardSlashes(line.trim());
7183
+ if (!isSafeRelativePath(candidate)) continue;
7184
+ uniqueFiles.add(candidate);
7185
+ }
7186
+ return [...uniqueFiles];
7187
+ };
7188
+ //#endregion
7156
7189
  //#region src/cli/utils/render-multi-project-summary.ts
7157
7190
  const SUMMARY_BAR_WIDTH_CHARS = 20;
7158
7191
  const buildMiniBar = (score) => {
@@ -7215,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
7215
7248
  };
7216
7249
  });
7217
7250
  const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
7251
+ yield* Console.log("");
7218
7252
  for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
7219
7253
  yield* Console.log("");
7220
7254
  });
@@ -7461,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
7461
7495
  return;
7462
7496
  }
7463
7497
  try {
7464
- 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);
7465
7499
  cliLogger.warn(message);
7466
7500
  } catch {}
7467
7501
  };
@@ -7611,7 +7645,6 @@ const resolveFailOnLevel = (flags, userConfig) => {
7611
7645
  };
7612
7646
  //#endregion
7613
7647
  //#region src/cli/utils/resolve-project-diff-include-paths.ts
7614
- const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
7615
7648
  const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
7616
7649
  const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
7617
7650
  const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
@@ -7810,7 +7843,7 @@ const validateModeFlags = (flags) => {
7810
7843
  if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
7811
7844
  if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
7812
7845
  if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
7813
- if (flags.annotations && (flags.json || flags.score)) throw new Error("--annotations cannot be combined with --json or --score.");
7846
+ if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
7814
7847
  if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
7815
7848
  if ((flags.explain ?? flags.why) !== void 0 && (flags.json || flags.score || flags.annotations || flags.staged)) throw new Error("--explain cannot be combined with --json, --score, --annotations, or --staged.");
7816
7849
  };
@@ -7836,6 +7869,12 @@ const finalizeScans = (input) => {
7836
7869
  const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
7837
7870
  if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
7838
7871
  };
7872
+ const buildChangedFilesDiffInfo = (changedFiles) => ({
7873
+ currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
7874
+ baseBranch: process.env.GITHUB_BASE_REF?.trim() || "pull request target",
7875
+ changedFiles,
7876
+ isCurrentChanges: false
7877
+ });
7839
7878
  const inspectAction = async (directory, flags) => {
7840
7879
  const isScoreOnly = Boolean(flags.score);
7841
7880
  const isJsonMode = Boolean(flags.json);
@@ -7938,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
7938
7977
  return;
7939
7978
  }
7940
7979
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
7980
+ const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
7941
7981
  const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
7942
- const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
7943
- const isDiffMode = await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
7982
+ const diffInfo = changedFilesDiffInfo ?? (changedFilesDiffInfo === null && (effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet) ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null);
7983
+ const isDiffMode = changedFilesDiffInfo !== null || await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
7944
7984
  setJsonReportMode(isDiffMode ? "diff" : "full");
7945
7985
  if (isDiffMode && diffInfo && !isQuiet) {
7946
7986
  if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
@@ -8838,21 +8878,23 @@ const buildWorkflowContent = () => [
8838
8878
  "",
8839
8879
  "on:",
8840
8880
  " pull_request:",
8841
- " branches: [main]",
8881
+ " types: [opened, synchronize, reopened, ready_for_review]",
8842
8882
  "",
8843
8883
  "permissions:",
8844
8884
  " contents: read",
8845
8885
  " pull-requests: write",
8886
+ " issues: write",
8887
+ "",
8888
+ "concurrency:",
8889
+ " group: react-doctor-${{ github.event.pull_request.number || github.ref }}",
8890
+ " cancel-in-progress: true",
8846
8891
  "",
8847
8892
  "jobs:",
8848
8893
  " react-doctor:",
8849
8894
  " runs-on: ubuntu-latest",
8850
8895
  " steps:",
8851
- " - uses: actions/checkout@v4",
8852
- " - uses: millionco/react-doctor@main",
8853
- " with:",
8854
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8855
- " diff: main",
8896
+ " - uses: actions/checkout@v5",
8897
+ " - uses: millionco/react-doctor@v1",
8856
8898
  ""
8857
8899
  ].join("\n");
8858
8900
  const runInstallReactDoctor = async (options = {}) => {
@@ -9050,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
9050
9092
  "--yes"
9051
9093
  ]),
9052
9094
  longOptionsWithRequiredValues: new Set([
9095
+ "--changed-files-from",
9053
9096
  "--explain",
9054
9097
  "--fail-on",
9055
9098
  "--project",
@@ -9143,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
9143
9186
  ];
9144
9187
  };
9145
9188
  //#endregion
9189
+ //#region src/cli/utils/unref-stdin.ts
9190
+ const unrefStdin = () => {
9191
+ process.stdin.unref?.();
9192
+ };
9193
+ //#endregion
9146
9194
  //#region src/cli/index.ts
9147
9195
  process.on("SIGINT", exitGracefully);
9148
9196
  process.on("SIGTERM", exitGracefully);
9149
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
9197
+ unrefStdin();
9198
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
9150
9199
  ${highlighter.dim("Configuration:")}
9151
9200
  Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
9152
9201
  CLI flags always override config values. See the README for the full schema.
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.402c7ea",
3
+ "version": "0.2.11-dev.b2934f9",
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.402c7ea"
61
+ "oxlint-plugin-react-doctor": "0.2.11-dev.b2934f9"
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.11",
68
+ "@react-doctor/core": "0.2.11"
69
69
  },
70
70
  "engines": {
71
71
  "node": "^20.19.0 || >=22.12.0"