react-doctor 0.2.9 → 0.2.11-dev.15e5fec

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.
@@ -2272,22 +2272,100 @@ const FRAMEWORK_DISPLAY_NAMES = {
2272
2272
  gatsby: "Gatsby",
2273
2273
  expo: "Expo",
2274
2274
  "react-native": "React Native",
2275
+ preact: "Preact",
2275
2276
  unknown: "React"
2276
2277
  };
2277
2278
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2278
2279
  const detectFramework = (dependencies) => {
2279
2280
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2281
+ if (dependencies.preact && !dependencies.react) return "preact";
2280
2282
  return "unknown";
2281
2283
  };
2282
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2283
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2284
- const OR_SEPARATOR = /\s*\|\|\s*/;
2285
2284
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2286
2285
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2287
2286
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2288
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2289
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2290
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
+ };
2291
2369
  const normalizeDependencyVersion = (version) => {
2292
2370
  const trimmed = version.trim();
2293
2371
  if (trimmed.length === 0) return null;
@@ -2297,17 +2375,29 @@ const normalizeDependencyVersion = (version) => {
2297
2375
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2298
2376
  return normalizedVersion;
2299
2377
  };
2300
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2301
- 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
+ };
2302
2383
  const getBranchLowestMajor = (branch) => {
2303
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2304
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2384
+ if (hasNonLowerBoundComparator(branch)) return null;
2385
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2305
2386
  if (lowerBoundComparators.length === 0) return null;
2306
2387
  let branchLowestMajor = null;
2307
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2308
- const major = Number.parseInt(match[1], 10);
2309
- if (!Number.isFinite(major) || major <= 0) continue;
2310
- 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;
2311
2401
  }
2312
2402
  return branchLowestMajor;
2313
2403
  };
@@ -2476,6 +2566,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2476
2566
  const EMPTY_DEPENDENCY_INFO = {
2477
2567
  reactVersion: null,
2478
2568
  tailwindVersion: null,
2569
+ zodVersion: null,
2479
2570
  framework: "unknown"
2480
2571
  };
2481
2572
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2504,6 +2595,11 @@ const extractDependencyInfo = (packageJson) => {
2504
2595
  "devDependencies",
2505
2596
  "peerDependencies"
2506
2597
  ]),
2598
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2599
+ "dependencies",
2600
+ "devDependencies",
2601
+ "peerDependencies"
2602
+ ]),
2507
2603
  framework: detectFramework(allDependencies)
2508
2604
  };
2509
2605
  };
@@ -2648,8 +2744,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2648
2744
  workspaceDirectory,
2649
2745
  workspacePackageJson
2650
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
+ });
2651
2760
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2652
2761
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2762
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2653
2763
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2654
2764
  const resultReactMajor = parseReactMajor(result.reactVersion);
2655
2765
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2684,17 +2794,44 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2684
2794
  "peerDependencies"
2685
2795
  ]
2686
2796
  }) : null;
2797
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2798
+ packageJson: leafPackageJson,
2799
+ packageName: "zod",
2800
+ sections: [
2801
+ "dependencies",
2802
+ "devDependencies",
2803
+ "peerDependencies"
2804
+ ]
2805
+ }) : null;
2687
2806
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2688
2807
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2808
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2689
2809
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2690
2810
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2811
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2691
2812
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2692
2813
  return {
2693
2814
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2694
2815
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2816
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2695
2817
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2696
2818
  };
2697
2819
  };
2820
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2821
+ if (predicate(rootPackageJson)) return true;
2822
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2823
+ if (patterns.length === 0) return false;
2824
+ const visitedDirectories = /* @__PURE__ */ new Set();
2825
+ for (const pattern of patterns) {
2826
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2827
+ for (const workspaceDirectory of directories) {
2828
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2829
+ visitedDirectories.add(workspaceDirectory);
2830
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2831
+ }
2832
+ }
2833
+ return false;
2834
+ };
2698
2835
  const NAMES = new Set([
2699
2836
  "react-native",
2700
2837
  "react-native-tvos",
@@ -2725,20 +2862,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2725
2862
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2726
2863
  return false;
2727
2864
  };
2728
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2729
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2730
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2731
- if (patterns.length === 0) return false;
2732
- const visitedDirectories = /* @__PURE__ */ new Set();
2733
- for (const pattern of patterns) {
2734
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2735
- for (const workspaceDirectory of directories) {
2736
- if (visitedDirectories.has(workspaceDirectory)) continue;
2737
- visitedDirectories.add(workspaceDirectory);
2738
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2739
- }
2740
- }
2741
- return false;
2865
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2866
+ const getPreactVersion = (packageJson) => {
2867
+ return {
2868
+ ...packageJson.peerDependencies,
2869
+ ...packageJson.dependencies,
2870
+ ...packageJson.devDependencies
2871
+ }.preact ?? null;
2742
2872
  };
2743
2873
  const TANSTACK_QUERY_PACKAGES = new Set([
2744
2874
  "@tanstack/react-query",
@@ -2753,6 +2883,20 @@ const hasTanStackQuery = (packageJson) => {
2753
2883
  };
2754
2884
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2755
2885
  };
2886
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2887
+ const isPackageJsonReanimatedAware = (packageJson) => {
2888
+ const allDependencies = {
2889
+ ...packageJson.peerDependencies,
2890
+ ...packageJson.dependencies,
2891
+ ...packageJson.devDependencies,
2892
+ ...packageJson.optionalDependencies
2893
+ };
2894
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2895
+ };
2896
+ const parseZodMajor = (zodVersion) => {
2897
+ if (typeof zodVersion !== "string") return null;
2898
+ return getLowestDependencyMajor(zodVersion);
2899
+ };
2756
2900
  const hasUpperBoundOnlyPeerRange = (range) => {
2757
2901
  if (typeof range !== "string") return false;
2758
2902
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2777,7 +2921,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2777
2921
  const REACT_DEPENDENCY_NAMES = new Set([
2778
2922
  "react",
2779
2923
  "react-native",
2780
- "next"
2924
+ "next",
2925
+ "preact"
2781
2926
  ]);
2782
2927
  const hasReactDependency = (packageJson) => {
2783
2928
  const allDependencies = {
@@ -2838,12 +2983,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2838
2983
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2839
2984
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2840
2985
  };
2986
+ const NON_PROJECT_DIRECTORIES = new Set([
2987
+ "AppData",
2988
+ "Application Data",
2989
+ "Library"
2990
+ ]);
2991
+ const MAX_SCAN_DEPTH = 6;
2841
2992
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2842
2993
  const packages = [];
2843
- const pendingDirectories = [rootDirectory];
2994
+ const pendingDirectories = [{
2995
+ directory: rootDirectory,
2996
+ depth: 0
2997
+ }];
2844
2998
  while (pendingDirectories.length > 0) {
2845
- const currentDirectory = pendingDirectories.pop();
2846
- if (!currentDirectory) continue;
2999
+ const current = pendingDirectories.pop();
3000
+ if (!current) continue;
3001
+ const { directory: currentDirectory, depth } = current;
2847
3002
  const packageJsonPath = path.join(currentDirectory, "package.json");
2848
3003
  if (isFile(packageJsonPath)) {
2849
3004
  const packageJson = readPackageJson(packageJsonPath);
@@ -2855,10 +3010,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2855
3010
  });
2856
3011
  }
2857
3012
  }
3013
+ if (depth >= MAX_SCAN_DEPTH) continue;
2858
3014
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2859
3015
  for (const entry of entries) {
2860
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2861
- pendingDirectories.push(path.join(currentDirectory, entry.name));
3016
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
3017
+ pendingDirectories.push({
3018
+ directory: path.join(currentDirectory, entry.name),
3019
+ depth: depth + 1
3020
+ });
2862
3021
  }
2863
3022
  }
2864
3023
  return packages;
@@ -2876,7 +3035,7 @@ const discoverProject = (directory) => {
2876
3035
  const packageJsonPath = path.join(directory, "package.json");
2877
3036
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2878
3037
  const packageJson = readPackageJson(packageJsonPath);
2879
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3038
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2880
3039
  const reactDeclaration = getDependencyDeclaration({
2881
3040
  packageJson,
2882
3041
  packageName: "react",
@@ -2895,9 +3054,19 @@ const discoverProject = (directory) => {
2895
3054
  "peerDependencies"
2896
3055
  ]
2897
3056
  });
3057
+ const zodDeclaration = getDependencyDeclaration({
3058
+ packageJson,
3059
+ packageName: "zod",
3060
+ sections: [
3061
+ "dependencies",
3062
+ "devDependencies",
3063
+ "peerDependencies"
3064
+ ]
3065
+ });
2898
3066
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2899
3067
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2900
- if (!reactVersion || !tailwindVersion) {
3068
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3069
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2901
3070
  const monorepoRoot = findMonorepoRoot(directory);
2902
3071
  if (monorepoRoot) {
2903
3072
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2905,6 +3074,7 @@ const discoverProject = (directory) => {
2905
3074
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2906
3075
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2907
3076
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3077
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2908
3078
  }
2909
3079
  }
2910
3080
  }
@@ -2912,36 +3082,81 @@ const discoverProject = (directory) => {
2912
3082
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2913
3083
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2914
3084
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3085
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2915
3086
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2916
3087
  }
2917
3088
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2918
3089
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2919
3090
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2920
3091
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3092
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2921
3093
  if (framework === "unknown") framework = monorepoInfo.framework;
2922
3094
  }
2923
3095
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2924
3096
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3097
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2925
3098
  const projectName = packageJson.name ?? path.basename(directory);
2926
3099
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2927
3100
  const sourceFileCount = countSourceFiles(directory);
2928
3101
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
3102
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3103
+ const preactVersion = getPreactVersion(packageJson);
2929
3104
  const projectInfo = {
2930
3105
  rootDirectory: directory,
2931
3106
  projectName,
2932
3107
  reactVersion,
2933
3108
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2934
3109
  tailwindVersion,
3110
+ zodVersion,
3111
+ zodMajorVersion: parseZodMajor(zodVersion),
2935
3112
  framework,
2936
3113
  hasTypeScript,
2937
3114
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2938
3115
  hasTanStackQuery: hasTanStackQuery(packageJson),
3116
+ preactVersion,
3117
+ preactMajorVersion: parseReactMajor(preactVersion),
2939
3118
  hasReactNativeWorkspace,
3119
+ hasReanimated,
2940
3120
  sourceFileCount
2941
3121
  };
2942
3122
  cachedProjectInfos.set(directory, projectInfo);
2943
3123
  return projectInfo;
2944
3124
  };
3125
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
3126
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
3127
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
3128
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
3129
+ const parseReactMajorMinor = (reactVersion) => {
3130
+ if (typeof reactVersion !== "string") return null;
3131
+ const trimmed = reactVersion.trim();
3132
+ if (trimmed.length === 0) return null;
3133
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
3134
+ if (lowerBoundsOnly.length === 0) return null;
3135
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
3136
+ if (majorMinorMatch) {
3137
+ const major = Number.parseInt(majorMinorMatch[1], 10);
3138
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
3139
+ if (!Number.isFinite(major) || major <= 0) return null;
3140
+ if (!Number.isFinite(minor) || minor < 0) return null;
3141
+ return {
3142
+ major,
3143
+ minor
3144
+ };
3145
+ }
3146
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
3147
+ if (!majorOnlyMatch) return null;
3148
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
3149
+ if (!Number.isFinite(major) || major <= 0) return null;
3150
+ return {
3151
+ major,
3152
+ minor: 0
3153
+ };
3154
+ };
3155
+ const isReactAtLeast = (detected, required) => {
3156
+ if (detected === null) return true;
3157
+ if (detected.major !== required.major) return detected.major > required.major;
3158
+ return detected.minor >= required.minor;
3159
+ };
2945
3160
  const parseTailwindMajorMinor = (tailwindVersion) => {
2946
3161
  if (typeof tailwindVersion !== "string") return null;
2947
3162
  const trimmed = tailwindVersion.trim();
@@ -2972,8 +3187,10 @@ const isTailwindAtLeast = (detected, required) => {
2972
3187
  return detected.minor >= required.minor;
2973
3188
  };
2974
3189
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3190
+ const MILLISECONDS_PER_SECOND = 1e3;
2975
3191
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2976
3192
  const SHARE_BASE_URL = "https://www.react.doctor/share";
3193
+ const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
2977
3194
  const FETCH_TIMEOUT_MS = 1e4;
2978
3195
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
2979
3196
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
@@ -3876,17 +4093,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3876
4093
  headers
3877
4094
  }).pipe(Layer.provide(FetchHttpClient.layer));
3878
4095
  }).pipe(Effect.orDie));
3879
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3880
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3881
- Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
4096
+ /**
4097
+ * Per-batch oxlint wall-clock budget. Reads from the env var on
4098
+ * startup so the eval harness can raise the budget under sandbox
4099
+ * microVMs without recompiling react-doctor. Tests override via
4100
+ * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
4101
+ */
4102
+ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3882
4103
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3883
4104
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3884
4105
  const parsed = Number(raw);
3885
4106
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3886
4107
  return parsed;
3887
- } });
3888
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3889
- Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
4108
+ } }) {};
4109
+ /**
4110
+ * Hard cap on combined stdout+stderr bytes per oxlint batch. The
4111
+ * subprocess gets SIGKILL'd if it produces more; the recovery path
4112
+ * suggests narrowing the scan with --diff. Override via Layer in
4113
+ * tests that exercise the cap behavior.
4114
+ */
4115
+ var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
3890
4116
  const DIAGNOSTIC_SURFACES = [
3891
4117
  "cli",
3892
4118
  "prComment",
@@ -4491,6 +4717,59 @@ const collectIgnorePatterns = (rootDirectory) => {
4491
4717
  return patterns;
4492
4718
  };
4493
4719
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4720
+ const DEAD_CODE_WORKER_SCRIPT = `
4721
+ const inputChunks = [];
4722
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4723
+ process.stdin.on("end", () => {
4724
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4725
+
4726
+ const normalizeResult = (result) => ({
4727
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4728
+ path: unusedFile.path,
4729
+ })),
4730
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4731
+ path: unusedExport.path,
4732
+ name: unusedExport.name,
4733
+ line: unusedExport.line,
4734
+ column: unusedExport.column,
4735
+ isTypeOnly: unusedExport.isTypeOnly,
4736
+ })),
4737
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4738
+ name: unusedDependency.name,
4739
+ isDevDependency: unusedDependency.isDevDependency,
4740
+ })),
4741
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4742
+ files: cycle.files,
4743
+ })),
4744
+ });
4745
+
4746
+ const serializeError = (error) =>
4747
+ error instanceof Error
4748
+ ? { name: error.name, message: error.message, stack: error.stack }
4749
+ : { message: String(error) };
4750
+
4751
+ const emit = (message) => {
4752
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4753
+ };
4754
+
4755
+ (async () => {
4756
+ try {
4757
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4758
+ const config = {
4759
+ rootDir: workerInput.rootDirectory,
4760
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4761
+ ...(workerInput.ignorePatterns.length > 0
4762
+ ? { ignorePatterns: workerInput.ignorePatterns }
4763
+ : {}),
4764
+ };
4765
+ const result = await analyze(defineConfig(config));
4766
+ emit({ ok: true, result: normalizeResult(result) });
4767
+ } catch (error) {
4768
+ emit({ ok: false, error: serializeError(error) });
4769
+ }
4770
+ })();
4771
+ });
4772
+ `;
4494
4773
  const resolveTsConfigPath = (rootDirectory) => {
4495
4774
  for (const filename of TSCONFIG_FILENAMES$1) {
4496
4775
  const candidate = path.join(rootDirectory, filename);
@@ -4511,16 +4790,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4511
4790
  const relative = toRelativePath(filePath, rootDirectory);
4512
4791
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4513
4792
  };
4793
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4794
+ const parseArray = (value, label) => {
4795
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4796
+ return value;
4797
+ };
4798
+ const parseString = (value, label) => {
4799
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4800
+ return value;
4801
+ };
4802
+ const parseNumber = (value, label) => {
4803
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4804
+ return value;
4805
+ };
4806
+ const parseBoolean = (value, label) => {
4807
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4808
+ return value;
4809
+ };
4810
+ const parseStringArray = (value, label) => {
4811
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4812
+ };
4813
+ const parseUnusedFiles = (value) => {
4814
+ const values = parseArray(value, "unusedFiles");
4815
+ const unusedFiles = [];
4816
+ for (const [index, entry] of values.entries()) {
4817
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4818
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4819
+ }
4820
+ return unusedFiles;
4821
+ };
4822
+ const parseUnusedExports = (value) => {
4823
+ const values = parseArray(value, "unusedExports");
4824
+ const unusedExports = [];
4825
+ for (const [index, entry] of values.entries()) {
4826
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4827
+ unusedExports.push({
4828
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4829
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4830
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4831
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4832
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4833
+ });
4834
+ }
4835
+ return unusedExports;
4836
+ };
4837
+ const parseUnusedDependencies = (value) => {
4838
+ const values = parseArray(value, "unusedDependencies");
4839
+ const unusedDependencies = [];
4840
+ for (const [index, entry] of values.entries()) {
4841
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4842
+ unusedDependencies.push({
4843
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4844
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4845
+ });
4846
+ }
4847
+ return unusedDependencies;
4848
+ };
4849
+ const parseCircularDependencies = (value) => {
4850
+ const values = parseArray(value, "circularDependencies");
4851
+ const circularDependencies = [];
4852
+ for (const [index, entry] of values.entries()) {
4853
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4854
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4855
+ }
4856
+ return circularDependencies;
4857
+ };
4858
+ const parseDeadCodeWorkerResult = (value) => {
4859
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4860
+ return {
4861
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4862
+ unusedExports: parseUnusedExports(value.unusedExports),
4863
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4864
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4865
+ };
4866
+ };
4867
+ const parseDeadCodeWorkerError = (value) => {
4868
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4869
+ return {
4870
+ ...typeof value.name === "string" ? { name: value.name } : {},
4871
+ message: value.message,
4872
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4873
+ };
4874
+ };
4875
+ const parseDeadCodeWorkerMessage = (value) => {
4876
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4877
+ if (value.ok === true) return {
4878
+ ok: true,
4879
+ result: value.result
4880
+ };
4881
+ if (value.ok === false) return {
4882
+ ok: false,
4883
+ error: parseDeadCodeWorkerError(value.error)
4884
+ };
4885
+ throw new Error("Dead-code worker returned an invalid status.");
4886
+ };
4887
+ const buildDeadCodeWorkerError = (workerError) => {
4888
+ const error = new Error(workerError.message);
4889
+ if (workerError.name !== void 0) error.name = workerError.name;
4890
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4891
+ return error;
4892
+ };
4893
+ const createDeadCodeWorker = (input) => {
4894
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4895
+ stdio: [
4896
+ "pipe",
4897
+ "pipe",
4898
+ "pipe"
4899
+ ],
4900
+ windowsHide: true
4901
+ });
4902
+ const stdoutChunks = [];
4903
+ const stderrChunks = [];
4904
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4905
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4906
+ let didSettle = false;
4907
+ const result = new Promise((resolve, reject) => {
4908
+ const settle = (callback) => {
4909
+ if (didSettle) return;
4910
+ didSettle = true;
4911
+ callback();
4912
+ };
4913
+ child.once("error", (error) => {
4914
+ settle(() => reject(error));
4915
+ });
4916
+ child.once("close", (exitCode) => {
4917
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4918
+ if (stdout.length === 0) {
4919
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4920
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4921
+ return;
4922
+ }
4923
+ try {
4924
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4925
+ if (parsedMessage.ok) {
4926
+ settle(() => resolve(parsedMessage.result));
4927
+ return;
4928
+ }
4929
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4930
+ } catch (error) {
4931
+ settle(() => reject(error));
4932
+ }
4933
+ });
4934
+ });
4935
+ child.stdin.on("error", () => {});
4936
+ child.stdin.end(JSON.stringify(input));
4937
+ return {
4938
+ result,
4939
+ terminate: () => {
4940
+ didSettle = true;
4941
+ child.kill("SIGKILL");
4942
+ }
4943
+ };
4944
+ };
4945
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4946
+ let didSettle = false;
4947
+ const timeoutHandle = setTimeout(() => {
4948
+ if (didSettle) return;
4949
+ didSettle = true;
4950
+ handle.terminate?.();
4951
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4952
+ }, timeoutMs);
4953
+ timeoutHandle.unref?.();
4954
+ handle.result.then((value) => {
4955
+ if (didSettle) return;
4956
+ didSettle = true;
4957
+ clearTimeout(timeoutHandle);
4958
+ handle.terminate?.();
4959
+ resolve(value);
4960
+ }, (error) => {
4961
+ if (didSettle) return;
4962
+ didSettle = true;
4963
+ clearTimeout(timeoutHandle);
4964
+ handle.terminate?.();
4965
+ reject(error);
4966
+ });
4967
+ });
4514
4968
  const checkDeadCode = async (options) => {
4515
4969
  const { rootDirectory, userConfig } = options;
4516
4970
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4517
- const { analyze, defineConfig } = await import("deslop-js");
4518
4971
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4519
- const result = await analyze(defineConfig({
4520
- rootDir: rootDirectory,
4972
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4973
+ rootDirectory,
4521
4974
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4522
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4523
- }));
4975
+ ignorePatterns,
4976
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4977
+ }), options.workerTimeoutMs ?? 12e4));
4524
4978
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4525
4979
  const diagnostics = [];
4526
4980
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -4726,8 +5180,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4726
5180
  env: input.env,
4727
5181
  extendEnv: true
4728
5182
  }));
5183
+ const maxStdoutBytes = input.maxStdoutBytes;
5184
+ const stdoutByteCount = yield* Ref.make(0);
5185
+ const stdoutStream = maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(Stream.tap((chunk) => Ref.updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(Effect.flatMap((total) => total > maxStdoutBytes ? Effect.fail(new ReactDoctorError({ reason: new GitInvocationFailed({
5186
+ args: [...input.args],
5187
+ directory: input.directory,
5188
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
5189
+ }) })) : Effect.void))));
4729
5190
  const [stdout, stderr, status] = yield* Effect.all([
4730
- Stream.mkString(Stream.decodeText(handle.stdout)),
5191
+ Stream.mkString(Stream.decodeText(stdoutStream)),
4731
5192
  Stream.mkString(Stream.decodeText(handle.stderr)),
4732
5193
  handle.exitCode
4733
5194
  ], { concurrency: 3 });
@@ -4889,7 +5350,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4889
5350
  if (result.status !== 0) return [];
4890
5351
  return splitNullSeparated(result.stdout);
4891
5352
  })),
4892
- showStagedContent: (directory, relativePath) => runGit(directory, ["show", `:${relativePath}`]).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5353
+ showStagedContent: (directory, relativePath, options) => runCommand({
5354
+ command: "git",
5355
+ args: ["show", `:${relativePath}`],
5356
+ directory,
5357
+ maxStdoutBytes: options?.maxBufferBytes
5358
+ }).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
4893
5359
  grep: (input) => Effect.gen(function* () {
4894
5360
  const args = ["grep"];
4895
5361
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -4897,7 +5363,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4897
5363
  if (input.extendedRegexp ?? false) args.push("-E");
4898
5364
  args.push(input.pattern);
4899
5365
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
4900
- const result = yield* runGit(input.directory, args);
5366
+ const result = yield* runCommand({
5367
+ command: "git",
5368
+ args,
5369
+ directory: input.directory,
5370
+ maxStdoutBytes: input.maxBufferBytes
5371
+ });
4901
5372
  if (result.status === 128) return null;
4902
5373
  return {
4903
5374
  status: result.status,
@@ -5144,7 +5615,16 @@ const buildCapabilities = (project) => {
5144
5615
  capabilities.add(project.framework);
5145
5616
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5146
5617
  const reactMajor = project.reactMajorVersion;
5147
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5618
+ if (reactMajor !== null) {
5619
+ const cappedReactMajor = Math.min(reactMajor, 30);
5620
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5621
+ if (reactMajor >= 19) {
5622
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5623
+ major: 19,
5624
+ minor: 2
5625
+ })) capabilities.add("react:19.2");
5626
+ }
5627
+ }
5148
5628
  if (project.tailwindVersion !== null) {
5149
5629
  capabilities.add("tailwind");
5150
5630
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5152,9 +5632,22 @@ const buildCapabilities = (project) => {
5152
5632
  minor: 4
5153
5633
  })) capabilities.add("tailwind:3.4");
5154
5634
  }
5635
+ if (project.zodVersion !== null) {
5636
+ capabilities.add("zod");
5637
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5638
+ }
5155
5639
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5156
5640
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5157
5641
  if (project.hasTypeScript) capabilities.add("typescript");
5642
+ if (project.preactVersion !== null) {
5643
+ capabilities.add("preact");
5644
+ const preactMajor = project.preactMajorVersion;
5645
+ if (preactMajor !== null) {
5646
+ const cappedPreactMajor = Math.min(preactMajor, 20);
5647
+ for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
5648
+ }
5649
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5650
+ }
5158
5651
  return capabilities;
5159
5652
  };
5160
5653
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -5409,6 +5902,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5409
5902
  if (!publicEnvPrefix) return fallbackRecommendation;
5410
5903
  return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
5411
5904
  };
5905
+ const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
5906
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5907
+ if (rule !== "immutability") return help;
5908
+ if (!project.hasReanimated) return help;
5909
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5910
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5911
+ };
5412
5912
  const REACT_MODULE_SOURCE = "react";
5413
5913
  const REQUIRE_IDENTIFIER = "require";
5414
5914
  const USE_IDENTIFIER = "use";
@@ -5732,7 +6232,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5732
6232
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5733
6233
  if (plugin === "react-hooks-js") return {
5734
6234
  message: REACT_COMPILER_MESSAGE,
5735
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6235
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
5736
6236
  };
5737
6237
  return {
5738
6238
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -5801,13 +6301,6 @@ const SANITIZED_ENV = (() => {
5801
6301
  }
5802
6302
  return sanitized;
5803
6303
  })();
5804
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5805
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
5806
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
5807
- const parsed = Number(raw);
5808
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
5809
- return parsed;
5810
- })();
5811
6304
  /**
5812
6305
  * Spawn one oxlint subprocess with hard ceilings on wall time and
5813
6306
  * output size. Returns stdout on success; raises a tagged
@@ -5824,7 +6317,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5824
6317
  * The first three are splittable (the caller's binary-split retry
5825
6318
  * shrinks the batch and re-spawns); the fourth isn't.
5826
6319
  */
5827
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
6320
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
5828
6321
  const child = spawn(nodeBinaryPath, args, {
5829
6322
  cwd: rootDirectory,
5830
6323
  env: SANITIZED_ENV
@@ -5833,9 +6326,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5833
6326
  child.kill("SIGKILL");
5834
6327
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5835
6328
  kind: "timeout",
5836
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6329
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
5837
6330
  }) }));
5838
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6331
+ }, spawnTimeoutMs);
5839
6332
  timeoutHandle.unref?.();
5840
6333
  const stdoutBuffers = [];
5841
6334
  const stderrBuffers = [];
@@ -5845,7 +6338,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5845
6338
  const killIfTooLarge = (incomingBytes, isStdout) => {
5846
6339
  if (isStdout) stdoutByteCount += incomingBytes;
5847
6340
  else stderrByteCount += incomingBytes;
5848
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6341
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
5849
6342
  didKillForSize = true;
5850
6343
  child.kill("SIGKILL");
5851
6344
  return true;
@@ -5871,7 +6364,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5871
6364
  if (didKillForSize) {
5872
6365
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5873
6366
  kind: "output-too-large",
5874
- detail: `exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`
6367
+ detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
5875
6368
  }) }));
5876
6369
  return;
5877
6370
  }
@@ -5912,7 +6405,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5912
6405
  * with a slimmer config in that case.
5913
6406
  */
5914
6407
  const spawnLintBatches = async (input) => {
5915
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6408
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
5916
6409
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
5917
6410
  const allDiagnostics = [];
5918
6411
  const droppedFiles = [];
@@ -5920,7 +6413,7 @@ const spawnLintBatches = async (input) => {
5920
6413
  const spawnLintBatch = async (batch) => {
5921
6414
  const batchArgs = [...baseArgs, ...batch];
5922
6415
  try {
5923
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6416
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
5924
6417
  } catch (error) {
5925
6418
  if (!isSplittableReactDoctorError(error)) throw error;
5926
6419
  if (batch.length <= 1) {
@@ -6023,13 +6516,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6023
6516
  * 6. always restore disable directives + clean up the temp dir
6024
6517
  */
6025
6518
  const runOxlint = async (options) => {
6026
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
6519
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
6027
6520
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6028
6521
  const severityControls = buildRuleSeverityControls(userConfig);
6029
6522
  validateRuleRegistration();
6030
6523
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6031
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6032
- const configPath = path.join(configDirectory, "oxlintrc.json");
6033
6524
  const pluginPath = resolvePluginPath();
6034
6525
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6035
6526
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6044,6 +6535,8 @@ const runOxlint = async (options) => {
6044
6535
  userPlugins
6045
6536
  });
6046
6537
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
6538
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6539
+ const configPath = path.join(configDirectory, "oxlintrc.json");
6047
6540
  try {
6048
6541
  const baseArgs = [
6049
6542
  resolveOxlintBinary(),
@@ -6070,7 +6563,9 @@ const runOxlint = async (options) => {
6070
6563
  nodeBinaryPath,
6071
6564
  project,
6072
6565
  onPartialFailure,
6073
- onFileProgress: options.onFileProgress
6566
+ onFileProgress: options.onFileProgress,
6567
+ spawnTimeoutMs,
6568
+ outputMaxBytes
6074
6569
  });
6075
6570
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6076
6571
  try {
@@ -6136,6 +6631,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6136
6631
  */
6137
6632
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6138
6633
  const partialFailures = yield* LintPartialFailures;
6634
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6635
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6139
6636
  const collectedFailures = [];
6140
6637
  const diagnostics = yield* Effect.tryPromise({
6141
6638
  try: () => runOxlint({
@@ -6152,7 +6649,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6152
6649
  onPartialFailure: (reason) => {
6153
6650
  collectedFailures.push(reason);
6154
6651
  },
6155
- onFileProgress: input.onFileProgress
6652
+ onFileProgress: input.onFileProgress,
6653
+ spawnTimeoutMs,
6654
+ outputMaxBytes
6156
6655
  }),
6157
6656
  catch: ensureReactDoctorError
6158
6657
  });
@@ -6450,7 +6949,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6450
6949
  const resolvedConfig = yield* configService.resolve(input.directory);
6451
6950
  const scanDirectory = resolvedConfig.resolvedDirectory;
6452
6951
  const project = yield* projectService.discover(scanDirectory);
6453
- if (project.reactVersion === null) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6952
+ if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6454
6953
  const [repo, sha, defaultBranch] = yield* Effect.all([
6455
6954
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6456
6955
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6478,23 +6977,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6478
6977
  const lintFailure = yield* Ref.make({
6479
6978
  didFail: false,
6480
6979
  reason: null,
6481
- reasonTag: null
6980
+ reasonTag: null,
6981
+ reasonKind: null
6482
6982
  });
6483
6983
  const deadCodeFailure = yield* Ref.make({
6484
6984
  didFail: false,
6485
6985
  reason: null
6486
6986
  });
6487
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6488
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6489
- rootDirectory: scanDirectory,
6490
- userConfig: resolvedConfig.config
6491
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6492
- yield* Ref.set(deadCodeFailure, {
6493
- didFail: true,
6494
- reason: error.message
6495
- });
6496
- return Stream.empty;
6497
- })))))) : Effect.succeed([]));
6498
6987
  const scanProgress = yield* progressService.start("Scanning...");
6499
6988
  const scanStartTime = Date.now();
6500
6989
  let lastReportedTotalFileCount = 0;
@@ -6511,24 +7000,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6511
7000
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6512
7001
  onFileProgress: (scannedFileCount, totalFileCount) => {
6513
7002
  lastReportedTotalFileCount = totalFileCount;
6514
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7003
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6515
7004
  }
6516
7005
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6517
7006
  yield* Ref.set(lintFailure, {
6518
7007
  didFail: true,
6519
7008
  reason: error.message,
6520
- reasonTag: error.reason._tag
7009
+ reasonTag: error.reason._tag,
7010
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6521
7011
  });
6522
7012
  return Stream.empty;
6523
7013
  }))));
6524
7014
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6525
7015
  const lintFailureState = yield* Ref.get(lintFailure);
6526
7016
  yield* afterLint(lintFailureState.didFail);
6527
- if (lintFailureState.didFail) {
6528
- yield* Fiber.interrupt(deadCodeFiber);
6529
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6530
- }
6531
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
7017
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
7018
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
7019
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
7020
+ rootDirectory: scanDirectory,
7021
+ userConfig: resolvedConfig.config
7022
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
7023
+ yield* Ref.set(deadCodeFailure, {
7024
+ didFail: true,
7025
+ reason: error.message
7026
+ });
7027
+ return Stream.empty;
7028
+ }))))))));
6532
7029
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6533
7030
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6534
7031
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
@@ -6570,6 +7067,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6570
7067
  didLintFail: lintFailureState.didFail,
6571
7068
  lintFailureReason: lintFailureState.reason,
6572
7069
  lintFailureReasonTag: lintFailureState.reasonTag,
7070
+ lintFailureReasonKind: lintFailureState.reasonKind,
6573
7071
  lintPartialFailures,
6574
7072
  didDeadCodeFail: deadCodeFailureState.didFail,
6575
7073
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7007,6 +7505,13 @@ const highlighter = {
7007
7505
  gray: import_picocolors.default.gray,
7008
7506
  bold: import_picocolors.default.bold
7009
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`;
7010
7515
  const groupBy = (items, keyFn) => {
7011
7516
  const groups = /* @__PURE__ */ new Map();
7012
7517
  for (const item of items) {
@@ -7023,7 +7528,7 @@ var cli_logger_exports = /* @__PURE__ */ __exportAll({ cliLogger: () => cliLogge
7023
7528
  /**
7024
7529
  * Thin synchronous façade over Effect's `Console` module. Used by
7025
7530
  * the imperative CLI helper files (`select-projects`, `run-explain`,
7026
- * `install-skill`, the legacy paths in `cli/commands/inspect.ts`)
7531
+ * `install-react-doctor`, the legacy paths in `cli/commands/inspect.ts`)
7027
7532
  * that aren't yet Effect-typed. Every call drains into a single
7028
7533
  * `Console.*` Effect via `Effect.runSync`, so the underlying logging
7029
7534
  * pipeline is identical to the canonical `yield* Console.log(...)`
@@ -7054,6 +7559,6 @@ const cliLogger = {
7054
7559
  }
7055
7560
  };
7056
7561
  //#endregion
7057
- 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 };
7058
7563
 
7059
- //# sourceMappingURL=cli-logger-BliQX9s8.js.map
7564
+ //# sourceMappingURL=cli-logger-CSZagq1E.js.map