react-doctor 0.2.10 → 0.2.11-dev.2e369e2

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
@@ -21,7 +21,6 @@ import * as Option from "effect/Option";
21
21
  import * as Ref from "effect/Ref";
22
22
  import * as Stream from "effect/Stream";
23
23
  import * as Cache from "effect/Cache";
24
- import { Worker } from "node:worker_threads";
25
24
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
26
25
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
27
26
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2308,15 +2307,91 @@ const detectFramework = (dependencies) => {
2308
2307
  if (dependencies.preact && !dependencies.react) return "preact";
2309
2308
  return "unknown";
2310
2309
  };
2311
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2312
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2313
- const OR_SEPARATOR = /\s*\|\|\s*/;
2314
2310
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2315
2311
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2316
2312
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2317
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2318
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2319
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
+ };
2320
2395
  const normalizeDependencyVersion = (version) => {
2321
2396
  const trimmed = version.trim();
2322
2397
  if (trimmed.length === 0) return null;
@@ -2326,17 +2401,29 @@ const normalizeDependencyVersion = (version) => {
2326
2401
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2327
2402
  return normalizedVersion;
2328
2403
  };
2329
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2330
- 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
+ };
2331
2409
  const getBranchLowestMajor = (branch) => {
2332
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2333
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2410
+ if (hasNonLowerBoundComparator(branch)) return null;
2411
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2334
2412
  if (lowerBoundComparators.length === 0) return null;
2335
2413
  let branchLowestMajor = null;
2336
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2337
- const major = Number.parseInt(match[1], 10);
2338
- if (!Number.isFinite(major) || major <= 0) continue;
2339
- 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;
2340
2427
  }
2341
2428
  return branchLowestMajor;
2342
2429
  };
@@ -2505,6 +2592,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2505
2592
  const EMPTY_DEPENDENCY_INFO = {
2506
2593
  reactVersion: null,
2507
2594
  tailwindVersion: null,
2595
+ zodVersion: null,
2508
2596
  framework: "unknown"
2509
2597
  };
2510
2598
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2533,6 +2621,11 @@ const extractDependencyInfo = (packageJson) => {
2533
2621
  "devDependencies",
2534
2622
  "peerDependencies"
2535
2623
  ]),
2624
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2625
+ "dependencies",
2626
+ "devDependencies",
2627
+ "peerDependencies"
2628
+ ]),
2536
2629
  framework: detectFramework(allDependencies)
2537
2630
  };
2538
2631
  };
@@ -2677,8 +2770,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2677
2770
  workspaceDirectory,
2678
2771
  workspacePackageJson
2679
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
+ });
2680
2786
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2681
2787
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2788
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2682
2789
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2683
2790
  const resultReactMajor = parseReactMajor(result.reactVersion);
2684
2791
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2713,17 +2820,44 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2713
2820
  "peerDependencies"
2714
2821
  ]
2715
2822
  }) : null;
2823
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2824
+ packageJson: leafPackageJson,
2825
+ packageName: "zod",
2826
+ sections: [
2827
+ "dependencies",
2828
+ "devDependencies",
2829
+ "peerDependencies"
2830
+ ]
2831
+ }) : null;
2716
2832
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2717
2833
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2834
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2718
2835
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2719
2836
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2837
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2720
2838
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2721
2839
  return {
2722
2840
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2723
2841
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2842
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2724
2843
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2725
2844
  };
2726
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
+ };
2727
2861
  const NAMES = new Set([
2728
2862
  "react-native",
2729
2863
  "react-native-tvos",
@@ -2754,27 +2888,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2754
2888
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2755
2889
  return false;
2756
2890
  };
2757
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2758
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2759
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2760
- if (patterns.length === 0) return false;
2761
- const visitedDirectories = /* @__PURE__ */ new Set();
2762
- for (const pattern of patterns) {
2763
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2764
- for (const workspaceDirectory of directories) {
2765
- if (visitedDirectories.has(workspaceDirectory)) continue;
2766
- visitedDirectories.add(workspaceDirectory);
2767
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2768
- }
2769
- }
2770
- return false;
2771
- };
2772
- const hasPreact = (packageJson) => {
2773
- return "preact" in {
2891
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2892
+ const getPreactVersion = (packageJson) => {
2893
+ return {
2774
2894
  ...packageJson.peerDependencies,
2775
2895
  ...packageJson.dependencies,
2776
2896
  ...packageJson.devDependencies
2777
- };
2897
+ }.preact ?? null;
2778
2898
  };
2779
2899
  const TANSTACK_QUERY_PACKAGES = new Set([
2780
2900
  "@tanstack/react-query",
@@ -2789,6 +2909,20 @@ const hasTanStackQuery = (packageJson) => {
2789
2909
  };
2790
2910
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2791
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
+ };
2792
2926
  const hasUpperBoundOnlyPeerRange = (range) => {
2793
2927
  if (typeof range !== "string") return false;
2794
2928
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2875,12 +3009,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2875
3009
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2876
3010
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2877
3011
  };
3012
+ const NON_PROJECT_DIRECTORIES = new Set([
3013
+ "AppData",
3014
+ "Application Data",
3015
+ "Library"
3016
+ ]);
3017
+ const MAX_SCAN_DEPTH = 6;
2878
3018
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2879
3019
  const packages = [];
2880
- const pendingDirectories = [rootDirectory];
3020
+ const pendingDirectories = [{
3021
+ directory: rootDirectory,
3022
+ depth: 0
3023
+ }];
2881
3024
  while (pendingDirectories.length > 0) {
2882
- const currentDirectory = pendingDirectories.pop();
2883
- if (!currentDirectory) continue;
3025
+ const current = pendingDirectories.pop();
3026
+ if (!current) continue;
3027
+ const { directory: currentDirectory, depth } = current;
2884
3028
  const packageJsonPath = path.join(currentDirectory, "package.json");
2885
3029
  if (isFile(packageJsonPath)) {
2886
3030
  const packageJson = readPackageJson(packageJsonPath);
@@ -2892,10 +3036,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2892
3036
  });
2893
3037
  }
2894
3038
  }
3039
+ if (depth >= MAX_SCAN_DEPTH) continue;
2895
3040
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2896
3041
  for (const entry of entries) {
2897
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2898
- 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
+ });
2899
3047
  }
2900
3048
  }
2901
3049
  return packages;
@@ -2916,7 +3064,7 @@ const discoverProject = (directory) => {
2916
3064
  const packageJsonPath = path.join(directory, "package.json");
2917
3065
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2918
3066
  const packageJson = readPackageJson(packageJsonPath);
2919
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3067
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2920
3068
  const reactDeclaration = getDependencyDeclaration({
2921
3069
  packageJson,
2922
3070
  packageName: "react",
@@ -2935,9 +3083,19 @@ const discoverProject = (directory) => {
2935
3083
  "peerDependencies"
2936
3084
  ]
2937
3085
  });
3086
+ const zodDeclaration = getDependencyDeclaration({
3087
+ packageJson,
3088
+ packageName: "zod",
3089
+ sections: [
3090
+ "dependencies",
3091
+ "devDependencies",
3092
+ "peerDependencies"
3093
+ ]
3094
+ });
2938
3095
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2939
3096
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2940
- if (!reactVersion || !tailwindVersion) {
3097
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3098
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2941
3099
  const monorepoRoot = findMonorepoRoot(directory);
2942
3100
  if (monorepoRoot) {
2943
3101
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2945,6 +3103,7 @@ const discoverProject = (directory) => {
2945
3103
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2946
3104
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2947
3105
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3106
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2948
3107
  }
2949
3108
  }
2950
3109
  }
@@ -2952,37 +3111,47 @@ const discoverProject = (directory) => {
2952
3111
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2953
3112
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2954
3113
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3114
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2955
3115
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2956
3116
  }
2957
3117
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2958
3118
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2959
3119
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2960
3120
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3121
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2961
3122
  if (framework === "unknown") framework = monorepoInfo.framework;
2962
3123
  }
2963
3124
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2964
3125
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3126
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2965
3127
  const projectName = packageJson.name ?? path.basename(directory);
2966
3128
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2967
3129
  const sourceFileCount = countSourceFiles(directory);
2968
3130
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
3131
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3132
+ const preactVersion = getPreactVersion(packageJson);
2969
3133
  const projectInfo = {
2970
3134
  rootDirectory: directory,
2971
3135
  projectName,
2972
3136
  reactVersion,
2973
3137
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2974
3138
  tailwindVersion,
3139
+ zodVersion,
3140
+ zodMajorVersion: parseZodMajor(zodVersion),
2975
3141
  framework,
2976
3142
  hasTypeScript,
2977
3143
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2978
3144
  hasTanStackQuery: hasTanStackQuery(packageJson),
2979
- hasPreact: hasPreact(packageJson),
3145
+ preactVersion,
3146
+ preactMajorVersion: parseReactMajor(preactVersion),
2980
3147
  hasReactNativeWorkspace,
3148
+ hasReanimated,
2981
3149
  sourceFileCount
2982
3150
  };
2983
3151
  cachedProjectInfos.set(directory, projectInfo);
2984
3152
  return projectInfo;
2985
3153
  };
3154
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2986
3155
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2987
3156
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2988
3157
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3948,17 +4117,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3948
4117
  headers
3949
4118
  }).pipe(Layer.provide(FetchHttpClient.layer));
3950
4119
  }).pipe(Effect.orDie));
3951
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3952
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3953
- 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: () => {
3954
4127
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3955
4128
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3956
4129
  const parsed = Number(raw);
3957
4130
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3958
4131
  return parsed;
3959
- } });
3960
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3961
- 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 }) {};
3962
4140
  const DIAGNOSTIC_SURFACES = [
3963
4141
  "cli",
3964
4142
  "prComment",
@@ -4570,47 +4748,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4570
4748
  };
4571
4749
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4572
4750
  const DEAD_CODE_WORKER_SCRIPT = `
4573
- const { parentPort, workerData } = require("node:worker_threads");
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"));
4574
4755
 
4575
- const normalizeResult = (result) => ({
4576
- unusedFiles: result.unusedFiles.map((unusedFile) => ({
4577
- path: unusedFile.path,
4578
- })),
4579
- unusedExports: result.unusedExports.map((unusedExport) => ({
4580
- path: unusedExport.path,
4581
- name: unusedExport.name,
4582
- line: unusedExport.line,
4583
- column: unusedExport.column,
4584
- isTypeOnly: unusedExport.isTypeOnly,
4585
- })),
4586
- unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4587
- name: unusedDependency.name,
4588
- isDevDependency: unusedDependency.isDevDependency,
4589
- })),
4590
- circularDependencies: result.circularDependencies.map((cycle) => ({
4591
- files: cycle.files,
4592
- })),
4593
- });
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
+ });
4594
4775
 
4595
- const serializeError = (error) =>
4596
- error instanceof Error
4597
- ? { name: error.name, message: error.message, stack: error.stack }
4598
- : { message: String(error) };
4776
+ const serializeError = (error) =>
4777
+ error instanceof Error
4778
+ ? { name: error.name, message: error.message, stack: error.stack }
4779
+ : { message: String(error) };
4599
4780
 
4600
- (async () => {
4601
- try {
4602
- const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4603
- const config = {
4604
- rootDir: workerData.rootDirectory,
4605
- ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4606
- ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4607
- };
4608
- const result = await analyze(defineConfig(config));
4609
- parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4610
- } catch (error) {
4611
- parentPort.postMessage({ ok: false, error: serializeError(error) });
4612
- }
4613
- })();
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
+ });
4614
4802
  `;
4615
4803
  const resolveTsConfigPath = (rootDirectory) => {
4616
4804
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4733,43 +4921,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4733
4921
  return error;
4734
4922
  };
4735
4923
  const createDeadCodeWorker = (input) => {
4736
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4737
- eval: true,
4738
- workerData: input
4924
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4925
+ stdio: [
4926
+ "pipe",
4927
+ "pipe",
4928
+ "pipe"
4929
+ ],
4930
+ windowsHide: true
4739
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));
4740
4936
  let didSettle = false;
4741
- return {
4742
- result: new Promise((resolve, reject) => {
4743
- const settle = (callback) => {
4744
- if (didSettle) return;
4745
- didSettle = true;
4746
- worker.removeAllListeners();
4747
- callback();
4748
- };
4749
- worker.once("message", (message) => {
4750
- try {
4751
- const parsedMessage = parseDeadCodeWorkerMessage(message);
4752
- if (parsedMessage.ok) {
4753
- settle(() => resolve(parsedMessage.result));
4754
- return;
4755
- }
4756
- settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4757
- } catch (error) {
4758
- settle(() => reject(error));
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;
4759
4958
  }
4760
- });
4761
- worker.once("error", (error) => {
4959
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4960
+ } catch (error) {
4762
4961
  settle(() => reject(error));
4763
- });
4764
- worker.once("exit", (exitCode) => {
4765
- if (exitCode === 0) return;
4766
- settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4767
- });
4768
- }),
4962
+ }
4963
+ });
4964
+ });
4965
+ child.stdin.on("error", () => {});
4966
+ child.stdin.end(JSON.stringify(input));
4967
+ return {
4968
+ result,
4769
4969
  terminate: () => {
4770
4970
  didSettle = true;
4771
- worker.removeAllListeners();
4772
- return worker.terminate();
4971
+ child.kill("SIGKILL");
4773
4972
  }
4774
4973
  };
4775
4974
  };
@@ -5011,8 +5210,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5011
5210
  env: input.env,
5012
5211
  extendEnv: true
5013
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))));
5014
5220
  const [stdout, stderr, status] = yield* Effect.all([
5015
- Stream.mkString(Stream.decodeText(handle.stdout)),
5221
+ Stream.mkString(Stream.decodeText(stdoutStream)),
5016
5222
  Stream.mkString(Stream.decodeText(handle.stderr)),
5017
5223
  handle.exitCode
5018
5224
  ], { concurrency: 3 });
@@ -5174,7 +5380,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5174
5380
  if (result.status !== 0) return [];
5175
5381
  return splitNullSeparated(result.stdout);
5176
5382
  })),
5177
- 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)),
5178
5389
  grep: (input) => Effect.gen(function* () {
5179
5390
  const args = ["grep"];
5180
5391
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5182,7 +5393,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5182
5393
  if (input.extendedRegexp ?? false) args.push("-E");
5183
5394
  args.push(input.pattern);
5184
5395
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5185
- 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
+ });
5186
5402
  if (result.status === 128) return null;
5187
5403
  return {
5188
5404
  status: result.status,
@@ -5430,7 +5646,8 @@ const buildCapabilities = (project) => {
5430
5646
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5431
5647
  const reactMajor = project.reactMajorVersion;
5432
5648
  if (reactMajor !== null) {
5433
- for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5649
+ const cappedReactMajor = Math.min(reactMajor, 30);
5650
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5434
5651
  if (reactMajor >= 19) {
5435
5652
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5436
5653
  major: 19,
@@ -5445,11 +5662,20 @@ const buildCapabilities = (project) => {
5445
5662
  minor: 4
5446
5663
  })) capabilities.add("tailwind:3.4");
5447
5664
  }
5665
+ if (project.zodVersion !== null) {
5666
+ capabilities.add("zod");
5667
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5668
+ }
5448
5669
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5449
5670
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5450
5671
  if (project.hasTypeScript) capabilities.add("typescript");
5451
- if (project.hasPreact) {
5672
+ if (project.preactVersion !== null) {
5452
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
+ }
5453
5679
  if (project.reactVersion === null) capabilities.add("pure-preact");
5454
5680
  }
5455
5681
  return capabilities;
@@ -5706,6 +5932,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5706
5932
  if (!publicEnvPrefix) return fallbackRecommendation;
5707
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`;
5708
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
+ };
5709
5942
  const REACT_MODULE_SOURCE = "react";
5710
5943
  const REQUIRE_IDENTIFIER = "require";
5711
5944
  const USE_IDENTIFIER = "use";
@@ -6029,7 +6262,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
6029
6262
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
6030
6263
  if (plugin === "react-hooks-js") return {
6031
6264
  message: REACT_COMPILER_MESSAGE,
6032
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6265
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6033
6266
  };
6034
6267
  return {
6035
6268
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -6098,13 +6331,6 @@ const SANITIZED_ENV = (() => {
6098
6331
  }
6099
6332
  return sanitized;
6100
6333
  })();
6101
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6102
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6103
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6104
- const parsed = Number(raw);
6105
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6106
- return parsed;
6107
- })();
6108
6334
  /**
6109
6335
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6110
6336
  * output size. Returns stdout on success; raises a tagged
@@ -6121,7 +6347,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6121
6347
  * The first three are splittable (the caller's binary-split retry
6122
6348
  * shrinks the batch and re-spawns); the fourth isn't.
6123
6349
  */
6124
- 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) => {
6125
6351
  const child = spawn(nodeBinaryPath, args, {
6126
6352
  cwd: rootDirectory,
6127
6353
  env: SANITIZED_ENV
@@ -6130,9 +6356,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6130
6356
  child.kill("SIGKILL");
6131
6357
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6132
6358
  kind: "timeout",
6133
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6359
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6134
6360
  }) }));
6135
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6361
+ }, spawnTimeoutMs);
6136
6362
  timeoutHandle.unref?.();
6137
6363
  const stdoutBuffers = [];
6138
6364
  const stderrBuffers = [];
@@ -6142,7 +6368,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6142
6368
  const killIfTooLarge = (incomingBytes, isStdout) => {
6143
6369
  if (isStdout) stdoutByteCount += incomingBytes;
6144
6370
  else stderrByteCount += incomingBytes;
6145
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6371
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6146
6372
  didKillForSize = true;
6147
6373
  child.kill("SIGKILL");
6148
6374
  return true;
@@ -6168,7 +6394,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6168
6394
  if (didKillForSize) {
6169
6395
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6170
6396
  kind: "output-too-large",
6171
- 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`
6172
6398
  }) }));
6173
6399
  return;
6174
6400
  }
@@ -6209,7 +6435,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6209
6435
  * with a slimmer config in that case.
6210
6436
  */
6211
6437
  const spawnLintBatches = async (input) => {
6212
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6438
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6213
6439
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6214
6440
  const allDiagnostics = [];
6215
6441
  const droppedFiles = [];
@@ -6217,7 +6443,7 @@ const spawnLintBatches = async (input) => {
6217
6443
  const spawnLintBatch = async (batch) => {
6218
6444
  const batchArgs = [...baseArgs, ...batch];
6219
6445
  try {
6220
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6446
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6221
6447
  } catch (error) {
6222
6448
  if (!isSplittableReactDoctorError(error)) throw error;
6223
6449
  if (batch.length <= 1) {
@@ -6320,13 +6546,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6320
6546
  * 6. always restore disable directives + clean up the temp dir
6321
6547
  */
6322
6548
  const runOxlint = async (options) => {
6323
- 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;
6324
6550
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6325
6551
  const severityControls = buildRuleSeverityControls(userConfig);
6326
6552
  validateRuleRegistration();
6327
6553
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6328
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6329
- const configPath = path.join(configDirectory, "oxlintrc.json");
6330
6554
  const pluginPath = resolvePluginPath();
6331
6555
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6332
6556
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6341,6 +6565,8 @@ const runOxlint = async (options) => {
6341
6565
  userPlugins
6342
6566
  });
6343
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");
6344
6570
  try {
6345
6571
  const baseArgs = [
6346
6572
  resolveOxlintBinary(),
@@ -6367,7 +6593,9 @@ const runOxlint = async (options) => {
6367
6593
  nodeBinaryPath,
6368
6594
  project,
6369
6595
  onPartialFailure,
6370
- onFileProgress: options.onFileProgress
6596
+ onFileProgress: options.onFileProgress,
6597
+ spawnTimeoutMs,
6598
+ outputMaxBytes
6371
6599
  });
6372
6600
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6373
6601
  try {
@@ -6433,6 +6661,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6433
6661
  */
6434
6662
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6435
6663
  const partialFailures = yield* LintPartialFailures;
6664
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6665
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6436
6666
  const collectedFailures = [];
6437
6667
  const diagnostics = yield* Effect.tryPromise({
6438
6668
  try: () => runOxlint({
@@ -6449,7 +6679,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6449
6679
  onPartialFailure: (reason) => {
6450
6680
  collectedFailures.push(reason);
6451
6681
  },
6452
- onFileProgress: input.onFileProgress
6682
+ onFileProgress: input.onFileProgress,
6683
+ spawnTimeoutMs,
6684
+ outputMaxBytes
6453
6685
  }),
6454
6686
  catch: ensureReactDoctorError
6455
6687
  });
@@ -6747,7 +6979,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6747
6979
  const resolvedConfig = yield* configService.resolve(input.directory);
6748
6980
  const scanDirectory = resolvedConfig.resolvedDirectory;
6749
6981
  const project = yield* projectService.discover(scanDirectory);
6750
- 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 }) });
6751
6983
  const [repo, sha, defaultBranch] = yield* Effect.all([
6752
6984
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6753
6985
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6775,7 +7007,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6775
7007
  const lintFailure = yield* Ref.make({
6776
7008
  didFail: false,
6777
7009
  reason: null,
6778
- reasonTag: null
7010
+ reasonTag: null,
7011
+ reasonKind: null
6779
7012
  });
6780
7013
  const deadCodeFailure = yield* Ref.make({
6781
7014
  didFail: false,
@@ -6797,13 +7030,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6797
7030
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6798
7031
  onFileProgress: (scannedFileCount, totalFileCount) => {
6799
7032
  lastReportedTotalFileCount = totalFileCount;
6800
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7033
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6801
7034
  }
6802
7035
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6803
7036
  yield* Ref.set(lintFailure, {
6804
7037
  didFail: true,
6805
7038
  reason: error.message,
6806
- reasonTag: error.reason._tag
7039
+ reasonTag: error.reason._tag,
7040
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6807
7041
  });
6808
7042
  return Stream.empty;
6809
7043
  }))));
@@ -6863,6 +7097,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6863
7097
  didLintFail: lintFailureState.didFail,
6864
7098
  lintFailureReason: lintFailureState.reason,
6865
7099
  lintFailureReasonTag: lintFailureState.reasonTag,
7100
+ lintFailureReasonKind: lintFailureState.reasonKind,
6866
7101
  lintPartialFailures,
6867
7102
  didDeadCodeFail: deadCodeFailureState.didFail,
6868
7103
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7297,11 +7532,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7297
7532
  const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7298
7533
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7299
7534
  const skippedChecks = [];
7535
+ if (output.didLintFail) skippedChecks.push("lint");
7536
+ if (output.didDeadCodeFail) skippedChecks.push("dead-code");
7300
7537
  const skippedCheckReasons = {};
7301
- if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) {
7302
- skippedChecks.push("dead-code");
7303
- skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7304
- }
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;
7305
7541
  return {
7306
7542
  diagnostics: [...output.diagnostics],
7307
7543
  score: output.score,