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.
package/dist/index.js CHANGED
@@ -2298,22 +2298,100 @@ const FRAMEWORK_DISPLAY_NAMES = {
2298
2298
  gatsby: "Gatsby",
2299
2299
  expo: "Expo",
2300
2300
  "react-native": "React Native",
2301
+ preact: "Preact",
2301
2302
  unknown: "React"
2302
2303
  };
2303
2304
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2304
2305
  const detectFramework = (dependencies) => {
2305
2306
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2307
+ if (dependencies.preact && !dependencies.react) return "preact";
2306
2308
  return "unknown";
2307
2309
  };
2308
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2309
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2310
- const OR_SEPARATOR = /\s*\|\|\s*/;
2311
2310
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2312
2311
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2313
2312
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2314
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2315
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2316
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
+ };
2317
2395
  const normalizeDependencyVersion = (version) => {
2318
2396
  const trimmed = version.trim();
2319
2397
  if (trimmed.length === 0) return null;
@@ -2323,17 +2401,29 @@ const normalizeDependencyVersion = (version) => {
2323
2401
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2324
2402
  return normalizedVersion;
2325
2403
  };
2326
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2327
- 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
+ };
2328
2409
  const getBranchLowestMajor = (branch) => {
2329
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2330
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2410
+ if (hasNonLowerBoundComparator(branch)) return null;
2411
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2331
2412
  if (lowerBoundComparators.length === 0) return null;
2332
2413
  let branchLowestMajor = null;
2333
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2334
- const major = Number.parseInt(match[1], 10);
2335
- if (!Number.isFinite(major) || major <= 0) continue;
2336
- 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;
2337
2427
  }
2338
2428
  return branchLowestMajor;
2339
2429
  };
@@ -2502,6 +2592,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2502
2592
  const EMPTY_DEPENDENCY_INFO = {
2503
2593
  reactVersion: null,
2504
2594
  tailwindVersion: null,
2595
+ zodVersion: null,
2505
2596
  framework: "unknown"
2506
2597
  };
2507
2598
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2530,6 +2621,11 @@ const extractDependencyInfo = (packageJson) => {
2530
2621
  "devDependencies",
2531
2622
  "peerDependencies"
2532
2623
  ]),
2624
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2625
+ "dependencies",
2626
+ "devDependencies",
2627
+ "peerDependencies"
2628
+ ]),
2533
2629
  framework: detectFramework(allDependencies)
2534
2630
  };
2535
2631
  };
@@ -2674,8 +2770,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2674
2770
  workspaceDirectory,
2675
2771
  workspacePackageJson
2676
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
+ });
2677
2786
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2678
2787
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2788
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2679
2789
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2680
2790
  const resultReactMajor = parseReactMajor(result.reactVersion);
2681
2791
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2710,17 +2820,44 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2710
2820
  "peerDependencies"
2711
2821
  ]
2712
2822
  }) : null;
2823
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2824
+ packageJson: leafPackageJson,
2825
+ packageName: "zod",
2826
+ sections: [
2827
+ "dependencies",
2828
+ "devDependencies",
2829
+ "peerDependencies"
2830
+ ]
2831
+ }) : null;
2713
2832
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2714
2833
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2834
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2715
2835
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2716
2836
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2837
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2717
2838
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2718
2839
  return {
2719
2840
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2720
2841
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2842
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2721
2843
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2722
2844
  };
2723
2845
  };
2846
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2847
+ if (predicate(rootPackageJson)) return true;
2848
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2849
+ if (patterns.length === 0) return false;
2850
+ const visitedDirectories = /* @__PURE__ */ new Set();
2851
+ for (const pattern of patterns) {
2852
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2853
+ for (const workspaceDirectory of directories) {
2854
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2855
+ visitedDirectories.add(workspaceDirectory);
2856
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2857
+ }
2858
+ }
2859
+ return false;
2860
+ };
2724
2861
  const NAMES = new Set([
2725
2862
  "react-native",
2726
2863
  "react-native-tvos",
@@ -2751,20 +2888,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2751
2888
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2752
2889
  return false;
2753
2890
  };
2754
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2755
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2756
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2757
- if (patterns.length === 0) return false;
2758
- const visitedDirectories = /* @__PURE__ */ new Set();
2759
- for (const pattern of patterns) {
2760
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2761
- for (const workspaceDirectory of directories) {
2762
- if (visitedDirectories.has(workspaceDirectory)) continue;
2763
- visitedDirectories.add(workspaceDirectory);
2764
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2765
- }
2766
- }
2767
- return false;
2891
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2892
+ const getPreactVersion = (packageJson) => {
2893
+ return {
2894
+ ...packageJson.peerDependencies,
2895
+ ...packageJson.dependencies,
2896
+ ...packageJson.devDependencies
2897
+ }.preact ?? null;
2768
2898
  };
2769
2899
  const TANSTACK_QUERY_PACKAGES = new Set([
2770
2900
  "@tanstack/react-query",
@@ -2779,6 +2909,20 @@ const hasTanStackQuery = (packageJson) => {
2779
2909
  };
2780
2910
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2781
2911
  };
2912
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2913
+ const isPackageJsonReanimatedAware = (packageJson) => {
2914
+ const allDependencies = {
2915
+ ...packageJson.peerDependencies,
2916
+ ...packageJson.dependencies,
2917
+ ...packageJson.devDependencies,
2918
+ ...packageJson.optionalDependencies
2919
+ };
2920
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2921
+ };
2922
+ const parseZodMajor = (zodVersion) => {
2923
+ if (typeof zodVersion !== "string") return null;
2924
+ return getLowestDependencyMajor(zodVersion);
2925
+ };
2782
2926
  const hasUpperBoundOnlyPeerRange = (range) => {
2783
2927
  if (typeof range !== "string") return false;
2784
2928
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2803,7 +2947,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2803
2947
  const REACT_DEPENDENCY_NAMES = new Set([
2804
2948
  "react",
2805
2949
  "react-native",
2806
- "next"
2950
+ "next",
2951
+ "preact"
2807
2952
  ]);
2808
2953
  const hasReactDependency = (packageJson) => {
2809
2954
  const allDependencies = {
@@ -2864,12 +3009,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2864
3009
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2865
3010
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2866
3011
  };
3012
+ const NON_PROJECT_DIRECTORIES = new Set([
3013
+ "AppData",
3014
+ "Application Data",
3015
+ "Library"
3016
+ ]);
3017
+ const MAX_SCAN_DEPTH = 6;
2867
3018
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2868
3019
  const packages = [];
2869
- const pendingDirectories = [rootDirectory];
3020
+ const pendingDirectories = [{
3021
+ directory: rootDirectory,
3022
+ depth: 0
3023
+ }];
2870
3024
  while (pendingDirectories.length > 0) {
2871
- const currentDirectory = pendingDirectories.pop();
2872
- if (!currentDirectory) continue;
3025
+ const current = pendingDirectories.pop();
3026
+ if (!current) continue;
3027
+ const { directory: currentDirectory, depth } = current;
2873
3028
  const packageJsonPath = path.join(currentDirectory, "package.json");
2874
3029
  if (isFile(packageJsonPath)) {
2875
3030
  const packageJson = readPackageJson(packageJsonPath);
@@ -2881,10 +3036,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2881
3036
  });
2882
3037
  }
2883
3038
  }
3039
+ if (depth >= MAX_SCAN_DEPTH) continue;
2884
3040
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2885
3041
  for (const entry of entries) {
2886
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2887
- pendingDirectories.push(path.join(currentDirectory, entry.name));
3042
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
3043
+ pendingDirectories.push({
3044
+ directory: path.join(currentDirectory, entry.name),
3045
+ depth: depth + 1
3046
+ });
2888
3047
  }
2889
3048
  }
2890
3049
  return packages;
@@ -2905,7 +3064,7 @@ const discoverProject = (directory) => {
2905
3064
  const packageJsonPath = path.join(directory, "package.json");
2906
3065
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2907
3066
  const packageJson = readPackageJson(packageJsonPath);
2908
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3067
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2909
3068
  const reactDeclaration = getDependencyDeclaration({
2910
3069
  packageJson,
2911
3070
  packageName: "react",
@@ -2924,9 +3083,19 @@ const discoverProject = (directory) => {
2924
3083
  "peerDependencies"
2925
3084
  ]
2926
3085
  });
3086
+ const zodDeclaration = getDependencyDeclaration({
3087
+ packageJson,
3088
+ packageName: "zod",
3089
+ sections: [
3090
+ "dependencies",
3091
+ "devDependencies",
3092
+ "peerDependencies"
3093
+ ]
3094
+ });
2927
3095
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2928
3096
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2929
- if (!reactVersion || !tailwindVersion) {
3097
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3098
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2930
3099
  const monorepoRoot = findMonorepoRoot(directory);
2931
3100
  if (monorepoRoot) {
2932
3101
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2934,6 +3103,7 @@ const discoverProject = (directory) => {
2934
3103
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2935
3104
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2936
3105
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3106
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2937
3107
  }
2938
3108
  }
2939
3109
  }
@@ -2941,36 +3111,81 @@ const discoverProject = (directory) => {
2941
3111
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2942
3112
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2943
3113
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3114
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2944
3115
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2945
3116
  }
2946
3117
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2947
3118
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2948
3119
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2949
3120
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3121
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2950
3122
  if (framework === "unknown") framework = monorepoInfo.framework;
2951
3123
  }
2952
3124
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2953
3125
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3126
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2954
3127
  const projectName = packageJson.name ?? path.basename(directory);
2955
3128
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2956
3129
  const sourceFileCount = countSourceFiles(directory);
2957
3130
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
3131
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3132
+ const preactVersion = getPreactVersion(packageJson);
2958
3133
  const projectInfo = {
2959
3134
  rootDirectory: directory,
2960
3135
  projectName,
2961
3136
  reactVersion,
2962
3137
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2963
3138
  tailwindVersion,
3139
+ zodVersion,
3140
+ zodMajorVersion: parseZodMajor(zodVersion),
2964
3141
  framework,
2965
3142
  hasTypeScript,
2966
3143
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2967
3144
  hasTanStackQuery: hasTanStackQuery(packageJson),
3145
+ preactVersion,
3146
+ preactMajorVersion: parseReactMajor(preactVersion),
2968
3147
  hasReactNativeWorkspace,
3148
+ hasReanimated,
2969
3149
  sourceFileCount
2970
3150
  };
2971
3151
  cachedProjectInfos.set(directory, projectInfo);
2972
3152
  return projectInfo;
2973
3153
  };
3154
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
3155
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
3156
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
3157
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
3158
+ const parseReactMajorMinor = (reactVersion) => {
3159
+ if (typeof reactVersion !== "string") return null;
3160
+ const trimmed = reactVersion.trim();
3161
+ if (trimmed.length === 0) return null;
3162
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
3163
+ if (lowerBoundsOnly.length === 0) return null;
3164
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
3165
+ if (majorMinorMatch) {
3166
+ const major = Number.parseInt(majorMinorMatch[1], 10);
3167
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
3168
+ if (!Number.isFinite(major) || major <= 0) return null;
3169
+ if (!Number.isFinite(minor) || minor < 0) return null;
3170
+ return {
3171
+ major,
3172
+ minor
3173
+ };
3174
+ }
3175
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
3176
+ if (!majorOnlyMatch) return null;
3177
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
3178
+ if (!Number.isFinite(major) || major <= 0) return null;
3179
+ return {
3180
+ major,
3181
+ minor: 0
3182
+ };
3183
+ };
3184
+ const isReactAtLeast = (detected, required) => {
3185
+ if (detected === null) return true;
3186
+ if (detected.major !== required.major) return detected.major > required.major;
3187
+ return detected.minor >= required.minor;
3188
+ };
2974
3189
  const parseTailwindMajorMinor = (tailwindVersion) => {
2975
3190
  if (typeof tailwindVersion !== "string") return null;
2976
3191
  const trimmed = tailwindVersion.trim();
@@ -3001,6 +3216,7 @@ const isTailwindAtLeast = (detected, required) => {
3001
3216
  return detected.minor >= required.minor;
3002
3217
  };
3003
3218
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3219
+ const MILLISECONDS_PER_SECOND = 1e3;
3004
3220
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3005
3221
  const FETCH_TIMEOUT_MS = 1e4;
3006
3222
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
@@ -3901,17 +4117,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3901
4117
  headers
3902
4118
  }).pipe(Layer.provide(FetchHttpClient.layer));
3903
4119
  }).pipe(Effect.orDie));
3904
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3905
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3906
- Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
4120
+ /**
4121
+ * Per-batch oxlint wall-clock budget. Reads from the env var on
4122
+ * startup so the eval harness can raise the budget under sandbox
4123
+ * microVMs without recompiling react-doctor. Tests override via
4124
+ * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
4125
+ */
4126
+ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3907
4127
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3908
4128
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3909
4129
  const parsed = Number(raw);
3910
4130
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3911
4131
  return parsed;
3912
- } });
3913
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3914
- Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
4132
+ } }) {};
4133
+ /**
4134
+ * Hard cap on combined stdout+stderr bytes per oxlint batch. The
4135
+ * subprocess gets SIGKILL'd if it produces more; the recovery path
4136
+ * suggests narrowing the scan with --diff. Override via Layer in
4137
+ * tests that exercise the cap behavior.
4138
+ */
4139
+ var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
3915
4140
  const DIAGNOSTIC_SURFACES = [
3916
4141
  "cli",
3917
4142
  "prComment",
@@ -4522,6 +4747,59 @@ const collectIgnorePatterns = (rootDirectory) => {
4522
4747
  return patterns;
4523
4748
  };
4524
4749
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4750
+ const DEAD_CODE_WORKER_SCRIPT = `
4751
+ const inputChunks = [];
4752
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4753
+ process.stdin.on("end", () => {
4754
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4755
+
4756
+ const normalizeResult = (result) => ({
4757
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4758
+ path: unusedFile.path,
4759
+ })),
4760
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4761
+ path: unusedExport.path,
4762
+ name: unusedExport.name,
4763
+ line: unusedExport.line,
4764
+ column: unusedExport.column,
4765
+ isTypeOnly: unusedExport.isTypeOnly,
4766
+ })),
4767
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4768
+ name: unusedDependency.name,
4769
+ isDevDependency: unusedDependency.isDevDependency,
4770
+ })),
4771
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4772
+ files: cycle.files,
4773
+ })),
4774
+ });
4775
+
4776
+ const serializeError = (error) =>
4777
+ error instanceof Error
4778
+ ? { name: error.name, message: error.message, stack: error.stack }
4779
+ : { message: String(error) };
4780
+
4781
+ const emit = (message) => {
4782
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4783
+ };
4784
+
4785
+ (async () => {
4786
+ try {
4787
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4788
+ const config = {
4789
+ rootDir: workerInput.rootDirectory,
4790
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4791
+ ...(workerInput.ignorePatterns.length > 0
4792
+ ? { ignorePatterns: workerInput.ignorePatterns }
4793
+ : {}),
4794
+ };
4795
+ const result = await analyze(defineConfig(config));
4796
+ emit({ ok: true, result: normalizeResult(result) });
4797
+ } catch (error) {
4798
+ emit({ ok: false, error: serializeError(error) });
4799
+ }
4800
+ })();
4801
+ });
4802
+ `;
4525
4803
  const resolveTsConfigPath = (rootDirectory) => {
4526
4804
  for (const filename of TSCONFIG_FILENAMES$1) {
4527
4805
  const candidate = path.join(rootDirectory, filename);
@@ -4542,16 +4820,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4542
4820
  const relative = toRelativePath(filePath, rootDirectory);
4543
4821
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4544
4822
  };
4823
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4824
+ const parseArray = (value, label) => {
4825
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4826
+ return value;
4827
+ };
4828
+ const parseString = (value, label) => {
4829
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4830
+ return value;
4831
+ };
4832
+ const parseNumber = (value, label) => {
4833
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4834
+ return value;
4835
+ };
4836
+ const parseBoolean = (value, label) => {
4837
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4838
+ return value;
4839
+ };
4840
+ const parseStringArray = (value, label) => {
4841
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4842
+ };
4843
+ const parseUnusedFiles = (value) => {
4844
+ const values = parseArray(value, "unusedFiles");
4845
+ const unusedFiles = [];
4846
+ for (const [index, entry] of values.entries()) {
4847
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4848
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4849
+ }
4850
+ return unusedFiles;
4851
+ };
4852
+ const parseUnusedExports = (value) => {
4853
+ const values = parseArray(value, "unusedExports");
4854
+ const unusedExports = [];
4855
+ for (const [index, entry] of values.entries()) {
4856
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4857
+ unusedExports.push({
4858
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4859
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4860
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4861
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4862
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4863
+ });
4864
+ }
4865
+ return unusedExports;
4866
+ };
4867
+ const parseUnusedDependencies = (value) => {
4868
+ const values = parseArray(value, "unusedDependencies");
4869
+ const unusedDependencies = [];
4870
+ for (const [index, entry] of values.entries()) {
4871
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4872
+ unusedDependencies.push({
4873
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4874
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4875
+ });
4876
+ }
4877
+ return unusedDependencies;
4878
+ };
4879
+ const parseCircularDependencies = (value) => {
4880
+ const values = parseArray(value, "circularDependencies");
4881
+ const circularDependencies = [];
4882
+ for (const [index, entry] of values.entries()) {
4883
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4884
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4885
+ }
4886
+ return circularDependencies;
4887
+ };
4888
+ const parseDeadCodeWorkerResult = (value) => {
4889
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4890
+ return {
4891
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4892
+ unusedExports: parseUnusedExports(value.unusedExports),
4893
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4894
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4895
+ };
4896
+ };
4897
+ const parseDeadCodeWorkerError = (value) => {
4898
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4899
+ return {
4900
+ ...typeof value.name === "string" ? { name: value.name } : {},
4901
+ message: value.message,
4902
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4903
+ };
4904
+ };
4905
+ const parseDeadCodeWorkerMessage = (value) => {
4906
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4907
+ if (value.ok === true) return {
4908
+ ok: true,
4909
+ result: value.result
4910
+ };
4911
+ if (value.ok === false) return {
4912
+ ok: false,
4913
+ error: parseDeadCodeWorkerError(value.error)
4914
+ };
4915
+ throw new Error("Dead-code worker returned an invalid status.");
4916
+ };
4917
+ const buildDeadCodeWorkerError = (workerError) => {
4918
+ const error = new Error(workerError.message);
4919
+ if (workerError.name !== void 0) error.name = workerError.name;
4920
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4921
+ return error;
4922
+ };
4923
+ const createDeadCodeWorker = (input) => {
4924
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4925
+ stdio: [
4926
+ "pipe",
4927
+ "pipe",
4928
+ "pipe"
4929
+ ],
4930
+ windowsHide: true
4931
+ });
4932
+ const stdoutChunks = [];
4933
+ const stderrChunks = [];
4934
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4935
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4936
+ let didSettle = false;
4937
+ const result = new Promise((resolve, reject) => {
4938
+ const settle = (callback) => {
4939
+ if (didSettle) return;
4940
+ didSettle = true;
4941
+ callback();
4942
+ };
4943
+ child.once("error", (error) => {
4944
+ settle(() => reject(error));
4945
+ });
4946
+ child.once("close", (exitCode) => {
4947
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4948
+ if (stdout.length === 0) {
4949
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4950
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4951
+ return;
4952
+ }
4953
+ try {
4954
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4955
+ if (parsedMessage.ok) {
4956
+ settle(() => resolve(parsedMessage.result));
4957
+ return;
4958
+ }
4959
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4960
+ } catch (error) {
4961
+ settle(() => reject(error));
4962
+ }
4963
+ });
4964
+ });
4965
+ child.stdin.on("error", () => {});
4966
+ child.stdin.end(JSON.stringify(input));
4967
+ return {
4968
+ result,
4969
+ terminate: () => {
4970
+ didSettle = true;
4971
+ child.kill("SIGKILL");
4972
+ }
4973
+ };
4974
+ };
4975
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4976
+ let didSettle = false;
4977
+ const timeoutHandle = setTimeout(() => {
4978
+ if (didSettle) return;
4979
+ didSettle = true;
4980
+ handle.terminate?.();
4981
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4982
+ }, timeoutMs);
4983
+ timeoutHandle.unref?.();
4984
+ handle.result.then((value) => {
4985
+ if (didSettle) return;
4986
+ didSettle = true;
4987
+ clearTimeout(timeoutHandle);
4988
+ handle.terminate?.();
4989
+ resolve(value);
4990
+ }, (error) => {
4991
+ if (didSettle) return;
4992
+ didSettle = true;
4993
+ clearTimeout(timeoutHandle);
4994
+ handle.terminate?.();
4995
+ reject(error);
4996
+ });
4997
+ });
4545
4998
  const checkDeadCode = async (options) => {
4546
4999
  const { rootDirectory, userConfig } = options;
4547
5000
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4548
- const { analyze, defineConfig } = await import("deslop-js");
4549
5001
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4550
- const result = await analyze(defineConfig({
4551
- rootDir: rootDirectory,
5002
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
5003
+ rootDirectory,
4552
5004
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4553
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4554
- }));
5005
+ ignorePatterns,
5006
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
5007
+ }), options.workerTimeoutMs ?? 12e4));
4555
5008
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4556
5009
  const diagnostics = [];
4557
5010
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -4757,8 +5210,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4757
5210
  env: input.env,
4758
5211
  extendEnv: true
4759
5212
  }));
5213
+ const maxStdoutBytes = input.maxStdoutBytes;
5214
+ const stdoutByteCount = yield* Ref.make(0);
5215
+ 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({
5216
+ args: [...input.args],
5217
+ directory: input.directory,
5218
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
5219
+ }) })) : Effect.void))));
4760
5220
  const [stdout, stderr, status] = yield* Effect.all([
4761
- Stream.mkString(Stream.decodeText(handle.stdout)),
5221
+ Stream.mkString(Stream.decodeText(stdoutStream)),
4762
5222
  Stream.mkString(Stream.decodeText(handle.stderr)),
4763
5223
  handle.exitCode
4764
5224
  ], { concurrency: 3 });
@@ -4920,7 +5380,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4920
5380
  if (result.status !== 0) return [];
4921
5381
  return splitNullSeparated(result.stdout);
4922
5382
  })),
4923
- showStagedContent: (directory, relativePath) => runGit(directory, ["show", `:${relativePath}`]).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5383
+ showStagedContent: (directory, relativePath, options) => runCommand({
5384
+ command: "git",
5385
+ args: ["show", `:${relativePath}`],
5386
+ directory,
5387
+ maxStdoutBytes: options?.maxBufferBytes
5388
+ }).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
4924
5389
  grep: (input) => Effect.gen(function* () {
4925
5390
  const args = ["grep"];
4926
5391
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -4928,7 +5393,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4928
5393
  if (input.extendedRegexp ?? false) args.push("-E");
4929
5394
  args.push(input.pattern);
4930
5395
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
4931
- const result = yield* runGit(input.directory, args);
5396
+ const result = yield* runCommand({
5397
+ command: "git",
5398
+ args,
5399
+ directory: input.directory,
5400
+ maxStdoutBytes: input.maxBufferBytes
5401
+ });
4932
5402
  if (result.status === 128) return null;
4933
5403
  return {
4934
5404
  status: result.status,
@@ -5175,7 +5645,16 @@ const buildCapabilities = (project) => {
5175
5645
  capabilities.add(project.framework);
5176
5646
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5177
5647
  const reactMajor = project.reactMajorVersion;
5178
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5648
+ if (reactMajor !== null) {
5649
+ const cappedReactMajor = Math.min(reactMajor, 30);
5650
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5651
+ if (reactMajor >= 19) {
5652
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5653
+ major: 19,
5654
+ minor: 2
5655
+ })) capabilities.add("react:19.2");
5656
+ }
5657
+ }
5179
5658
  if (project.tailwindVersion !== null) {
5180
5659
  capabilities.add("tailwind");
5181
5660
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5183,9 +5662,22 @@ const buildCapabilities = (project) => {
5183
5662
  minor: 4
5184
5663
  })) capabilities.add("tailwind:3.4");
5185
5664
  }
5665
+ if (project.zodVersion !== null) {
5666
+ capabilities.add("zod");
5667
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5668
+ }
5186
5669
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5187
5670
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5188
5671
  if (project.hasTypeScript) capabilities.add("typescript");
5672
+ if (project.preactVersion !== null) {
5673
+ capabilities.add("preact");
5674
+ const preactMajor = project.preactMajorVersion;
5675
+ if (preactMajor !== null) {
5676
+ const cappedPreactMajor = Math.min(preactMajor, 20);
5677
+ for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
5678
+ }
5679
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5680
+ }
5189
5681
  return capabilities;
5190
5682
  };
5191
5683
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -5440,6 +5932,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5440
5932
  if (!publicEnvPrefix) return fallbackRecommendation;
5441
5933
  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`;
5442
5934
  };
5935
+ 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";
5936
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5937
+ if (rule !== "immutability") return help;
5938
+ if (!project.hasReanimated) return help;
5939
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5940
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5941
+ };
5443
5942
  const REACT_MODULE_SOURCE = "react";
5444
5943
  const REQUIRE_IDENTIFIER = "require";
5445
5944
  const USE_IDENTIFIER = "use";
@@ -5763,7 +6262,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5763
6262
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5764
6263
  if (plugin === "react-hooks-js") return {
5765
6264
  message: REACT_COMPILER_MESSAGE,
5766
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6265
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
5767
6266
  };
5768
6267
  return {
5769
6268
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -5832,13 +6331,6 @@ const SANITIZED_ENV = (() => {
5832
6331
  }
5833
6332
  return sanitized;
5834
6333
  })();
5835
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5836
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
5837
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
5838
- const parsed = Number(raw);
5839
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
5840
- return parsed;
5841
- })();
5842
6334
  /**
5843
6335
  * Spawn one oxlint subprocess with hard ceilings on wall time and
5844
6336
  * output size. Returns stdout on success; raises a tagged
@@ -5855,7 +6347,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5855
6347
  * The first three are splittable (the caller's binary-split retry
5856
6348
  * shrinks the batch and re-spawns); the fourth isn't.
5857
6349
  */
5858
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
6350
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
5859
6351
  const child = spawn(nodeBinaryPath, args, {
5860
6352
  cwd: rootDirectory,
5861
6353
  env: SANITIZED_ENV
@@ -5864,9 +6356,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5864
6356
  child.kill("SIGKILL");
5865
6357
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5866
6358
  kind: "timeout",
5867
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6359
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
5868
6360
  }) }));
5869
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6361
+ }, spawnTimeoutMs);
5870
6362
  timeoutHandle.unref?.();
5871
6363
  const stdoutBuffers = [];
5872
6364
  const stderrBuffers = [];
@@ -5876,7 +6368,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5876
6368
  const killIfTooLarge = (incomingBytes, isStdout) => {
5877
6369
  if (isStdout) stdoutByteCount += incomingBytes;
5878
6370
  else stderrByteCount += incomingBytes;
5879
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6371
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
5880
6372
  didKillForSize = true;
5881
6373
  child.kill("SIGKILL");
5882
6374
  return true;
@@ -5902,7 +6394,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5902
6394
  if (didKillForSize) {
5903
6395
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5904
6396
  kind: "output-too-large",
5905
- detail: `exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`
6397
+ detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
5906
6398
  }) }));
5907
6399
  return;
5908
6400
  }
@@ -5943,7 +6435,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5943
6435
  * with a slimmer config in that case.
5944
6436
  */
5945
6437
  const spawnLintBatches = async (input) => {
5946
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6438
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
5947
6439
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
5948
6440
  const allDiagnostics = [];
5949
6441
  const droppedFiles = [];
@@ -5951,7 +6443,7 @@ const spawnLintBatches = async (input) => {
5951
6443
  const spawnLintBatch = async (batch) => {
5952
6444
  const batchArgs = [...baseArgs, ...batch];
5953
6445
  try {
5954
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6446
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
5955
6447
  } catch (error) {
5956
6448
  if (!isSplittableReactDoctorError(error)) throw error;
5957
6449
  if (batch.length <= 1) {
@@ -6054,13 +6546,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6054
6546
  * 6. always restore disable directives + clean up the temp dir
6055
6547
  */
6056
6548
  const runOxlint = async (options) => {
6057
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
6549
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
6058
6550
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6059
6551
  const severityControls = buildRuleSeverityControls(userConfig);
6060
6552
  validateRuleRegistration();
6061
6553
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6062
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6063
- const configPath = path.join(configDirectory, "oxlintrc.json");
6064
6554
  const pluginPath = resolvePluginPath();
6065
6555
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6066
6556
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6075,6 +6565,8 @@ const runOxlint = async (options) => {
6075
6565
  userPlugins
6076
6566
  });
6077
6567
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
6568
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6569
+ const configPath = path.join(configDirectory, "oxlintrc.json");
6078
6570
  try {
6079
6571
  const baseArgs = [
6080
6572
  resolveOxlintBinary(),
@@ -6101,7 +6593,9 @@ const runOxlint = async (options) => {
6101
6593
  nodeBinaryPath,
6102
6594
  project,
6103
6595
  onPartialFailure,
6104
- onFileProgress: options.onFileProgress
6596
+ onFileProgress: options.onFileProgress,
6597
+ spawnTimeoutMs,
6598
+ outputMaxBytes
6105
6599
  });
6106
6600
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6107
6601
  try {
@@ -6167,6 +6661,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6167
6661
  */
6168
6662
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6169
6663
  const partialFailures = yield* LintPartialFailures;
6664
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6665
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6170
6666
  const collectedFailures = [];
6171
6667
  const diagnostics = yield* Effect.tryPromise({
6172
6668
  try: () => runOxlint({
@@ -6183,7 +6679,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6183
6679
  onPartialFailure: (reason) => {
6184
6680
  collectedFailures.push(reason);
6185
6681
  },
6186
- onFileProgress: input.onFileProgress
6682
+ onFileProgress: input.onFileProgress,
6683
+ spawnTimeoutMs,
6684
+ outputMaxBytes
6187
6685
  }),
6188
6686
  catch: ensureReactDoctorError
6189
6687
  });
@@ -6481,7 +6979,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6481
6979
  const resolvedConfig = yield* configService.resolve(input.directory);
6482
6980
  const scanDirectory = resolvedConfig.resolvedDirectory;
6483
6981
  const project = yield* projectService.discover(scanDirectory);
6484
- if (project.reactVersion === null) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6982
+ if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6485
6983
  const [repo, sha, defaultBranch] = yield* Effect.all([
6486
6984
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6487
6985
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6509,23 +7007,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6509
7007
  const lintFailure = yield* Ref.make({
6510
7008
  didFail: false,
6511
7009
  reason: null,
6512
- reasonTag: null
7010
+ reasonTag: null,
7011
+ reasonKind: null
6513
7012
  });
6514
7013
  const deadCodeFailure = yield* Ref.make({
6515
7014
  didFail: false,
6516
7015
  reason: null
6517
7016
  });
6518
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6519
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6520
- rootDirectory: scanDirectory,
6521
- userConfig: resolvedConfig.config
6522
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6523
- yield* Ref.set(deadCodeFailure, {
6524
- didFail: true,
6525
- reason: error.message
6526
- });
6527
- return Stream.empty;
6528
- })))))) : Effect.succeed([]));
6529
7017
  const scanProgress = yield* progressService.start("Scanning...");
6530
7018
  const scanStartTime = Date.now();
6531
7019
  let lastReportedTotalFileCount = 0;
@@ -6542,24 +7030,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6542
7030
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6543
7031
  onFileProgress: (scannedFileCount, totalFileCount) => {
6544
7032
  lastReportedTotalFileCount = totalFileCount;
6545
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7033
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6546
7034
  }
6547
7035
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6548
7036
  yield* Ref.set(lintFailure, {
6549
7037
  didFail: true,
6550
7038
  reason: error.message,
6551
- reasonTag: error.reason._tag
7039
+ reasonTag: error.reason._tag,
7040
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6552
7041
  });
6553
7042
  return Stream.empty;
6554
7043
  }))));
6555
7044
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6556
7045
  const lintFailureState = yield* Ref.get(lintFailure);
6557
7046
  yield* afterLint(lintFailureState.didFail);
6558
- if (lintFailureState.didFail) {
6559
- yield* Fiber.interrupt(deadCodeFiber);
6560
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6561
- }
6562
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
7047
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
7048
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
7049
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
7050
+ rootDirectory: scanDirectory,
7051
+ userConfig: resolvedConfig.config
7052
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
7053
+ yield* Ref.set(deadCodeFailure, {
7054
+ didFail: true,
7055
+ reason: error.message
7056
+ });
7057
+ return Stream.empty;
7058
+ }))))))));
6563
7059
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6564
7060
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6565
7061
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
@@ -6601,6 +7097,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6601
7097
  didLintFail: lintFailureState.didFail,
6602
7098
  lintFailureReason: lintFailureState.reason,
6603
7099
  lintFailureReasonTag: lintFailureState.reasonTag,
7100
+ lintFailureReasonKind: lintFailureState.reasonKind,
6604
7101
  lintPartialFailures,
6605
7102
  didDeadCodeFail: deadCodeFailureState.didFail,
6606
7103
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7035,11 +7532,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7035
7532
  const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7036
7533
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7037
7534
  const skippedChecks = [];
7535
+ if (output.didLintFail) skippedChecks.push("lint");
7536
+ if (output.didDeadCodeFail) skippedChecks.push("dead-code");
7038
7537
  const skippedCheckReasons = {};
7039
- if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) {
7040
- skippedChecks.push("dead-code");
7041
- skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7042
- }
7538
+ if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
7539
+ else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
7540
+ if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7043
7541
  return {
7044
7542
  diagnostics: [...output.diagnostics],
7045
7543
  score: output.score,