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.
@@ -22,7 +22,6 @@ import * as Option from "effect/Option";
22
22
  import * as Ref from "effect/Ref";
23
23
  import * as Stream from "effect/Stream";
24
24
  import * as Cache from "effect/Cache";
25
- import { Worker } from "node:worker_threads";
26
25
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
27
26
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
28
27
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2282,15 +2281,91 @@ const detectFramework = (dependencies) => {
2282
2281
  if (dependencies.preact && !dependencies.react) return "preact";
2283
2282
  return "unknown";
2284
2283
  };
2285
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2286
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2287
- const OR_SEPARATOR = /\s*\|\|\s*/;
2288
2284
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2289
2285
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2290
2286
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2291
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2292
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2293
2287
  const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
2288
+ const isDigit = (value) => value !== void 0 && value >= "0" && value <= "9";
2289
+ const isWhitespace = (value) => value === " " || value === " " || value === "\n" || value === "\r" || value === "\f" || value === "\v";
2290
+ const isSeparator = (value) => isWhitespace(value) || value === "," || value === "|";
2291
+ const skipWhitespace = (value, start) => {
2292
+ let index = start;
2293
+ while (isWhitespace(value[index])) index += 1;
2294
+ return index;
2295
+ };
2296
+ const skipSeparators = (value, start) => {
2297
+ let index = start;
2298
+ while (isSeparator(value[index])) index += 1;
2299
+ return index;
2300
+ };
2301
+ const readDigits = (value, start) => {
2302
+ let index = start;
2303
+ while (isDigit(value[index])) index += 1;
2304
+ return index;
2305
+ };
2306
+ const getUpperBoundComparatorEnd = (version, start) => {
2307
+ if (version[start] !== "<") return null;
2308
+ let index = skipWhitespace(version, start + 1);
2309
+ if (version[index] === "=") index = skipWhitespace(version, index + 1);
2310
+ const majorStart = index;
2311
+ index = readDigits(version, index);
2312
+ if (index === majorStart) return null;
2313
+ for (let segments = 0; segments < 2 && version[index] === "."; segments += 1) {
2314
+ const segmentStart = index + 1;
2315
+ const segmentEnd = readDigits(version, segmentStart);
2316
+ if (segmentEnd === segmentStart) break;
2317
+ index = segmentEnd;
2318
+ }
2319
+ if (version[index] === "-") {
2320
+ index += 1;
2321
+ while (index < version.length && !isSeparator(version[index])) index += 1;
2322
+ }
2323
+ return index;
2324
+ };
2325
+ const stripUpperBoundComparators = (version) => {
2326
+ let stripped = "";
2327
+ let index = 0;
2328
+ while (index < version.length) {
2329
+ const comparatorEnd = getUpperBoundComparatorEnd(version, index);
2330
+ if (comparatorEnd === null) {
2331
+ stripped += version[index];
2332
+ index += 1;
2333
+ continue;
2334
+ }
2335
+ stripped += " ";
2336
+ index = comparatorEnd;
2337
+ }
2338
+ return stripped;
2339
+ };
2340
+ const hasNonLowerBoundComparator = (branch) => {
2341
+ for (let index = 0; index < branch.length; index += 1) {
2342
+ if (index > 0 && !isSeparator(branch[index - 1])) continue;
2343
+ if (branch[index] === ">" && branch[index + 1] !== "=") {
2344
+ if (isDigit(branch[skipWhitespace(branch, index + 1)])) return true;
2345
+ continue;
2346
+ }
2347
+ if (branch[index] !== "!") continue;
2348
+ let valueIndex = index + 1;
2349
+ if (branch[valueIndex] === "=") valueIndex += 1;
2350
+ if (branch[valueIndex] === "=") valueIndex += 1;
2351
+ valueIndex = skipWhitespace(branch, valueIndex);
2352
+ if (isDigit(branch[valueIndex])) return true;
2353
+ }
2354
+ return false;
2355
+ };
2356
+ const isMajorTerminator = (value) => value === void 0 || isSeparator(value) || value === "." || value === "*" || value === "x" || value === "X" || value === "-";
2357
+ const getLowerBoundMajorAt = (branch, start) => {
2358
+ let index = start;
2359
+ if (branch[index] === ">" && branch[index + 1] === "=") index = skipWhitespace(branch, index + 2);
2360
+ else if (branch[index] === "~" || branch[index] === "^" || branch[index] === "=" || branch[index] === "v") index = skipWhitespace(branch, index + 1);
2361
+ const majorStart = index;
2362
+ const majorEnd = readDigits(branch, majorStart);
2363
+ if (majorEnd === majorStart || !isMajorTerminator(branch[majorEnd])) return null;
2364
+ return {
2365
+ end: majorEnd,
2366
+ major: Number.parseInt(branch.slice(majorStart, majorEnd), 10)
2367
+ };
2368
+ };
2294
2369
  const normalizeDependencyVersion = (version) => {
2295
2370
  const trimmed = version.trim();
2296
2371
  if (trimmed.length === 0) return null;
@@ -2300,17 +2375,29 @@ const normalizeDependencyVersion = (version) => {
2300
2375
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2301
2376
  return normalizedVersion;
2302
2377
  };
2303
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2304
- const hasUpperBoundComparator = (version) => HAS_UPPER_BOUND_COMPARATOR.test(version);
2378
+ const splitDependencyVersionBranches = (version) => version.split("||").map((branch) => branch.trim()).filter(Boolean);
2379
+ const hasUpperBoundComparator = (version) => {
2380
+ for (let index = 0; index < version.length; index += 1) if (getUpperBoundComparatorEnd(version, index) !== null) return true;
2381
+ return false;
2382
+ };
2305
2383
  const getBranchLowestMajor = (branch) => {
2306
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2307
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2384
+ if (hasNonLowerBoundComparator(branch)) return null;
2385
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2308
2386
  if (lowerBoundComparators.length === 0) return null;
2309
2387
  let branchLowestMajor = null;
2310
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2311
- const major = Number.parseInt(match[1], 10);
2312
- if (!Number.isFinite(major) || major <= 0) continue;
2313
- if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2388
+ let index = 0;
2389
+ while (index < lowerBoundComparators.length) {
2390
+ const lowerBoundStart = skipSeparators(lowerBoundComparators, index);
2391
+ if (lowerBoundStart > 0 && !isSeparator(lowerBoundComparators[lowerBoundStart - 1])) {
2392
+ index = lowerBoundStart + 1;
2393
+ continue;
2394
+ }
2395
+ const lowerBoundMajor = getLowerBoundMajorAt(lowerBoundComparators, lowerBoundStart);
2396
+ if (lowerBoundMajor !== null && Number.isFinite(lowerBoundMajor.major) && lowerBoundMajor.major > 0) {
2397
+ const major = lowerBoundMajor.major;
2398
+ if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2399
+ }
2400
+ index = lowerBoundMajor?.end ?? lowerBoundStart + 1;
2314
2401
  }
2315
2402
  return branchLowestMajor;
2316
2403
  };
@@ -2479,6 +2566,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2479
2566
  const EMPTY_DEPENDENCY_INFO = {
2480
2567
  reactVersion: null,
2481
2568
  tailwindVersion: null,
2569
+ zodVersion: null,
2482
2570
  framework: "unknown"
2483
2571
  };
2484
2572
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2507,6 +2595,11 @@ const extractDependencyInfo = (packageJson) => {
2507
2595
  "devDependencies",
2508
2596
  "peerDependencies"
2509
2597
  ]),
2598
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2599
+ "dependencies",
2600
+ "devDependencies",
2601
+ "peerDependencies"
2602
+ ]),
2510
2603
  framework: detectFramework(allDependencies)
2511
2604
  };
2512
2605
  };
@@ -2651,8 +2744,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2651
2744
  workspaceDirectory,
2652
2745
  workspacePackageJson
2653
2746
  });
2747
+ const zodVersion = resolveWorkspaceDependencyVersion({
2748
+ concreteVersion: info.zodVersion,
2749
+ packageName: "zod",
2750
+ rootDirectory,
2751
+ rootPackageJson: packageJson,
2752
+ sections: [
2753
+ "dependencies",
2754
+ "devDependencies",
2755
+ "peerDependencies"
2756
+ ],
2757
+ workspaceDirectory,
2758
+ workspacePackageJson
2759
+ });
2654
2760
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2655
2761
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2762
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2656
2763
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2657
2764
  const resultReactMajor = parseReactMajor(result.reactVersion);
2658
2765
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2687,17 +2794,44 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2687
2794
  "peerDependencies"
2688
2795
  ]
2689
2796
  }) : null;
2797
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2798
+ packageJson: leafPackageJson,
2799
+ packageName: "zod",
2800
+ sections: [
2801
+ "dependencies",
2802
+ "devDependencies",
2803
+ "peerDependencies"
2804
+ ]
2805
+ }) : null;
2690
2806
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2691
2807
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2808
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2692
2809
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2693
2810
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2811
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2694
2812
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2695
2813
  return {
2696
2814
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2697
2815
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2816
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2698
2817
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2699
2818
  };
2700
2819
  };
2820
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2821
+ if (predicate(rootPackageJson)) return true;
2822
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2823
+ if (patterns.length === 0) return false;
2824
+ const visitedDirectories = /* @__PURE__ */ new Set();
2825
+ for (const pattern of patterns) {
2826
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2827
+ for (const workspaceDirectory of directories) {
2828
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2829
+ visitedDirectories.add(workspaceDirectory);
2830
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2831
+ }
2832
+ }
2833
+ return false;
2834
+ };
2701
2835
  const NAMES = new Set([
2702
2836
  "react-native",
2703
2837
  "react-native-tvos",
@@ -2728,27 +2862,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2728
2862
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2729
2863
  return false;
2730
2864
  };
2731
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2732
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2733
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2734
- if (patterns.length === 0) return false;
2735
- const visitedDirectories = /* @__PURE__ */ new Set();
2736
- for (const pattern of patterns) {
2737
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2738
- for (const workspaceDirectory of directories) {
2739
- if (visitedDirectories.has(workspaceDirectory)) continue;
2740
- visitedDirectories.add(workspaceDirectory);
2741
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2742
- }
2743
- }
2744
- return false;
2745
- };
2746
- const hasPreact = (packageJson) => {
2747
- return "preact" in {
2865
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2866
+ const getPreactVersion = (packageJson) => {
2867
+ return {
2748
2868
  ...packageJson.peerDependencies,
2749
2869
  ...packageJson.dependencies,
2750
2870
  ...packageJson.devDependencies
2751
- };
2871
+ }.preact ?? null;
2752
2872
  };
2753
2873
  const TANSTACK_QUERY_PACKAGES = new Set([
2754
2874
  "@tanstack/react-query",
@@ -2763,6 +2883,20 @@ const hasTanStackQuery = (packageJson) => {
2763
2883
  };
2764
2884
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2765
2885
  };
2886
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2887
+ const isPackageJsonReanimatedAware = (packageJson) => {
2888
+ const allDependencies = {
2889
+ ...packageJson.peerDependencies,
2890
+ ...packageJson.dependencies,
2891
+ ...packageJson.devDependencies,
2892
+ ...packageJson.optionalDependencies
2893
+ };
2894
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2895
+ };
2896
+ const parseZodMajor = (zodVersion) => {
2897
+ if (typeof zodVersion !== "string") return null;
2898
+ return getLowestDependencyMajor(zodVersion);
2899
+ };
2766
2900
  const hasUpperBoundOnlyPeerRange = (range) => {
2767
2901
  if (typeof range !== "string") return false;
2768
2902
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2849,12 +2983,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2849
2983
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2850
2984
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2851
2985
  };
2986
+ const NON_PROJECT_DIRECTORIES = new Set([
2987
+ "AppData",
2988
+ "Application Data",
2989
+ "Library"
2990
+ ]);
2991
+ const MAX_SCAN_DEPTH = 6;
2852
2992
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2853
2993
  const packages = [];
2854
- const pendingDirectories = [rootDirectory];
2994
+ const pendingDirectories = [{
2995
+ directory: rootDirectory,
2996
+ depth: 0
2997
+ }];
2855
2998
  while (pendingDirectories.length > 0) {
2856
- const currentDirectory = pendingDirectories.pop();
2857
- if (!currentDirectory) continue;
2999
+ const current = pendingDirectories.pop();
3000
+ if (!current) continue;
3001
+ const { directory: currentDirectory, depth } = current;
2858
3002
  const packageJsonPath = path.join(currentDirectory, "package.json");
2859
3003
  if (isFile(packageJsonPath)) {
2860
3004
  const packageJson = readPackageJson(packageJsonPath);
@@ -2866,10 +3010,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2866
3010
  });
2867
3011
  }
2868
3012
  }
3013
+ if (depth >= MAX_SCAN_DEPTH) continue;
2869
3014
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2870
3015
  for (const entry of entries) {
2871
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2872
- pendingDirectories.push(path.join(currentDirectory, entry.name));
3016
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
3017
+ pendingDirectories.push({
3018
+ directory: path.join(currentDirectory, entry.name),
3019
+ depth: depth + 1
3020
+ });
2873
3021
  }
2874
3022
  }
2875
3023
  return packages;
@@ -2887,7 +3035,7 @@ const discoverProject = (directory) => {
2887
3035
  const packageJsonPath = path.join(directory, "package.json");
2888
3036
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2889
3037
  const packageJson = readPackageJson(packageJsonPath);
2890
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3038
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2891
3039
  const reactDeclaration = getDependencyDeclaration({
2892
3040
  packageJson,
2893
3041
  packageName: "react",
@@ -2906,9 +3054,19 @@ const discoverProject = (directory) => {
2906
3054
  "peerDependencies"
2907
3055
  ]
2908
3056
  });
3057
+ const zodDeclaration = getDependencyDeclaration({
3058
+ packageJson,
3059
+ packageName: "zod",
3060
+ sections: [
3061
+ "dependencies",
3062
+ "devDependencies",
3063
+ "peerDependencies"
3064
+ ]
3065
+ });
2909
3066
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2910
3067
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2911
- if (!reactVersion || !tailwindVersion) {
3068
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3069
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2912
3070
  const monorepoRoot = findMonorepoRoot(directory);
2913
3071
  if (monorepoRoot) {
2914
3072
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2916,6 +3074,7 @@ const discoverProject = (directory) => {
2916
3074
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2917
3075
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2918
3076
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3077
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2919
3078
  }
2920
3079
  }
2921
3080
  }
@@ -2923,37 +3082,47 @@ const discoverProject = (directory) => {
2923
3082
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2924
3083
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2925
3084
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3085
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2926
3086
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2927
3087
  }
2928
3088
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2929
3089
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2930
3090
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2931
3091
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3092
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2932
3093
  if (framework === "unknown") framework = monorepoInfo.framework;
2933
3094
  }
2934
3095
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2935
3096
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3097
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2936
3098
  const projectName = packageJson.name ?? path.basename(directory);
2937
3099
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2938
3100
  const sourceFileCount = countSourceFiles(directory);
2939
3101
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
3102
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3103
+ const preactVersion = getPreactVersion(packageJson);
2940
3104
  const projectInfo = {
2941
3105
  rootDirectory: directory,
2942
3106
  projectName,
2943
3107
  reactVersion,
2944
3108
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2945
3109
  tailwindVersion,
3110
+ zodVersion,
3111
+ zodMajorVersion: parseZodMajor(zodVersion),
2946
3112
  framework,
2947
3113
  hasTypeScript,
2948
3114
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2949
3115
  hasTanStackQuery: hasTanStackQuery(packageJson),
2950
- hasPreact: hasPreact(packageJson),
3116
+ preactVersion,
3117
+ preactMajorVersion: parseReactMajor(preactVersion),
2951
3118
  hasReactNativeWorkspace,
3119
+ hasReanimated,
2952
3120
  sourceFileCount
2953
3121
  };
2954
3122
  cachedProjectInfos.set(directory, projectInfo);
2955
3123
  return projectInfo;
2956
3124
  };
3125
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2957
3126
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2958
3127
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2959
3128
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3021,6 +3190,7 @@ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3021
3190
  const MILLISECONDS_PER_SECOND = 1e3;
3022
3191
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3023
3192
  const SHARE_BASE_URL = "https://www.react.doctor/share";
3193
+ const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
3024
3194
  const FETCH_TIMEOUT_MS = 1e4;
3025
3195
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
3026
3196
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
@@ -3923,17 +4093,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3923
4093
  headers
3924
4094
  }).pipe(Layer.provide(FetchHttpClient.layer));
3925
4095
  }).pipe(Effect.orDie));
3926
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3927
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3928
- Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
4096
+ /**
4097
+ * Per-batch oxlint wall-clock budget. Reads from the env var on
4098
+ * startup so the eval harness can raise the budget under sandbox
4099
+ * microVMs without recompiling react-doctor. Tests override via
4100
+ * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
4101
+ */
4102
+ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3929
4103
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3930
4104
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3931
4105
  const parsed = Number(raw);
3932
4106
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3933
4107
  return parsed;
3934
- } });
3935
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3936
- Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
4108
+ } }) {};
4109
+ /**
4110
+ * Hard cap on combined stdout+stderr bytes per oxlint batch. The
4111
+ * subprocess gets SIGKILL'd if it produces more; the recovery path
4112
+ * suggests narrowing the scan with --diff. Override via Layer in
4113
+ * tests that exercise the cap behavior.
4114
+ */
4115
+ var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
3937
4116
  const DIAGNOSTIC_SURFACES = [
3938
4117
  "cli",
3939
4118
  "prComment",
@@ -4539,47 +4718,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4539
4718
  };
4540
4719
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4541
4720
  const DEAD_CODE_WORKER_SCRIPT = `
4542
- const { parentPort, workerData } = require("node:worker_threads");
4721
+ const inputChunks = [];
4722
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4723
+ process.stdin.on("end", () => {
4724
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4543
4725
 
4544
- const normalizeResult = (result) => ({
4545
- unusedFiles: result.unusedFiles.map((unusedFile) => ({
4546
- path: unusedFile.path,
4547
- })),
4548
- unusedExports: result.unusedExports.map((unusedExport) => ({
4549
- path: unusedExport.path,
4550
- name: unusedExport.name,
4551
- line: unusedExport.line,
4552
- column: unusedExport.column,
4553
- isTypeOnly: unusedExport.isTypeOnly,
4554
- })),
4555
- unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4556
- name: unusedDependency.name,
4557
- isDevDependency: unusedDependency.isDevDependency,
4558
- })),
4559
- circularDependencies: result.circularDependencies.map((cycle) => ({
4560
- files: cycle.files,
4561
- })),
4562
- });
4726
+ const normalizeResult = (result) => ({
4727
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4728
+ path: unusedFile.path,
4729
+ })),
4730
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4731
+ path: unusedExport.path,
4732
+ name: unusedExport.name,
4733
+ line: unusedExport.line,
4734
+ column: unusedExport.column,
4735
+ isTypeOnly: unusedExport.isTypeOnly,
4736
+ })),
4737
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4738
+ name: unusedDependency.name,
4739
+ isDevDependency: unusedDependency.isDevDependency,
4740
+ })),
4741
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4742
+ files: cycle.files,
4743
+ })),
4744
+ });
4563
4745
 
4564
- const serializeError = (error) =>
4565
- error instanceof Error
4566
- ? { name: error.name, message: error.message, stack: error.stack }
4567
- : { message: String(error) };
4746
+ const serializeError = (error) =>
4747
+ error instanceof Error
4748
+ ? { name: error.name, message: error.message, stack: error.stack }
4749
+ : { message: String(error) };
4568
4750
 
4569
- (async () => {
4570
- try {
4571
- const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4572
- const config = {
4573
- rootDir: workerData.rootDirectory,
4574
- ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4575
- ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4576
- };
4577
- const result = await analyze(defineConfig(config));
4578
- parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4579
- } catch (error) {
4580
- parentPort.postMessage({ ok: false, error: serializeError(error) });
4581
- }
4582
- })();
4751
+ const emit = (message) => {
4752
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4753
+ };
4754
+
4755
+ (async () => {
4756
+ try {
4757
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4758
+ const config = {
4759
+ rootDir: workerInput.rootDirectory,
4760
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4761
+ ...(workerInput.ignorePatterns.length > 0
4762
+ ? { ignorePatterns: workerInput.ignorePatterns }
4763
+ : {}),
4764
+ };
4765
+ const result = await analyze(defineConfig(config));
4766
+ emit({ ok: true, result: normalizeResult(result) });
4767
+ } catch (error) {
4768
+ emit({ ok: false, error: serializeError(error) });
4769
+ }
4770
+ })();
4771
+ });
4583
4772
  `;
4584
4773
  const resolveTsConfigPath = (rootDirectory) => {
4585
4774
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4702,43 +4891,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4702
4891
  return error;
4703
4892
  };
4704
4893
  const createDeadCodeWorker = (input) => {
4705
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4706
- eval: true,
4707
- workerData: input
4894
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4895
+ stdio: [
4896
+ "pipe",
4897
+ "pipe",
4898
+ "pipe"
4899
+ ],
4900
+ windowsHide: true
4708
4901
  });
4902
+ const stdoutChunks = [];
4903
+ const stderrChunks = [];
4904
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4905
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4709
4906
  let didSettle = false;
4710
- return {
4711
- result: new Promise((resolve, reject) => {
4712
- const settle = (callback) => {
4713
- if (didSettle) return;
4714
- didSettle = true;
4715
- worker.removeAllListeners();
4716
- callback();
4717
- };
4718
- worker.once("message", (message) => {
4719
- try {
4720
- const parsedMessage = parseDeadCodeWorkerMessage(message);
4721
- if (parsedMessage.ok) {
4722
- settle(() => resolve(parsedMessage.result));
4723
- return;
4724
- }
4725
- settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4726
- } catch (error) {
4727
- settle(() => reject(error));
4907
+ const result = new Promise((resolve, reject) => {
4908
+ const settle = (callback) => {
4909
+ if (didSettle) return;
4910
+ didSettle = true;
4911
+ callback();
4912
+ };
4913
+ child.once("error", (error) => {
4914
+ settle(() => reject(error));
4915
+ });
4916
+ child.once("close", (exitCode) => {
4917
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4918
+ if (stdout.length === 0) {
4919
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4920
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4921
+ return;
4922
+ }
4923
+ try {
4924
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4925
+ if (parsedMessage.ok) {
4926
+ settle(() => resolve(parsedMessage.result));
4927
+ return;
4728
4928
  }
4729
- });
4730
- worker.once("error", (error) => {
4929
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4930
+ } catch (error) {
4731
4931
  settle(() => reject(error));
4732
- });
4733
- worker.once("exit", (exitCode) => {
4734
- if (exitCode === 0) return;
4735
- settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4736
- });
4737
- }),
4932
+ }
4933
+ });
4934
+ });
4935
+ child.stdin.on("error", () => {});
4936
+ child.stdin.end(JSON.stringify(input));
4937
+ return {
4938
+ result,
4738
4939
  terminate: () => {
4739
4940
  didSettle = true;
4740
- worker.removeAllListeners();
4741
- return worker.terminate();
4941
+ child.kill("SIGKILL");
4742
4942
  }
4743
4943
  };
4744
4944
  };
@@ -4980,8 +5180,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4980
5180
  env: input.env,
4981
5181
  extendEnv: true
4982
5182
  }));
5183
+ const maxStdoutBytes = input.maxStdoutBytes;
5184
+ const stdoutByteCount = yield* Ref.make(0);
5185
+ const stdoutStream = maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(Stream.tap((chunk) => Ref.updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(Effect.flatMap((total) => total > maxStdoutBytes ? Effect.fail(new ReactDoctorError({ reason: new GitInvocationFailed({
5186
+ args: [...input.args],
5187
+ directory: input.directory,
5188
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
5189
+ }) })) : Effect.void))));
4983
5190
  const [stdout, stderr, status] = yield* Effect.all([
4984
- Stream.mkString(Stream.decodeText(handle.stdout)),
5191
+ Stream.mkString(Stream.decodeText(stdoutStream)),
4985
5192
  Stream.mkString(Stream.decodeText(handle.stderr)),
4986
5193
  handle.exitCode
4987
5194
  ], { concurrency: 3 });
@@ -5143,7 +5350,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5143
5350
  if (result.status !== 0) return [];
5144
5351
  return splitNullSeparated(result.stdout);
5145
5352
  })),
5146
- showStagedContent: (directory, relativePath) => runGit(directory, ["show", `:${relativePath}`]).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5353
+ showStagedContent: (directory, relativePath, options) => runCommand({
5354
+ command: "git",
5355
+ args: ["show", `:${relativePath}`],
5356
+ directory,
5357
+ maxStdoutBytes: options?.maxBufferBytes
5358
+ }).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5147
5359
  grep: (input) => Effect.gen(function* () {
5148
5360
  const args = ["grep"];
5149
5361
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5151,7 +5363,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5151
5363
  if (input.extendedRegexp ?? false) args.push("-E");
5152
5364
  args.push(input.pattern);
5153
5365
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5154
- const result = yield* runGit(input.directory, args);
5366
+ const result = yield* runCommand({
5367
+ command: "git",
5368
+ args,
5369
+ directory: input.directory,
5370
+ maxStdoutBytes: input.maxBufferBytes
5371
+ });
5155
5372
  if (result.status === 128) return null;
5156
5373
  return {
5157
5374
  status: result.status,
@@ -5399,7 +5616,8 @@ const buildCapabilities = (project) => {
5399
5616
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5400
5617
  const reactMajor = project.reactMajorVersion;
5401
5618
  if (reactMajor !== null) {
5402
- for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5619
+ const cappedReactMajor = Math.min(reactMajor, 30);
5620
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5403
5621
  if (reactMajor >= 19) {
5404
5622
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5405
5623
  major: 19,
@@ -5414,11 +5632,20 @@ const buildCapabilities = (project) => {
5414
5632
  minor: 4
5415
5633
  })) capabilities.add("tailwind:3.4");
5416
5634
  }
5635
+ if (project.zodVersion !== null) {
5636
+ capabilities.add("zod");
5637
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5638
+ }
5417
5639
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5418
5640
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5419
5641
  if (project.hasTypeScript) capabilities.add("typescript");
5420
- if (project.hasPreact) {
5642
+ if (project.preactVersion !== null) {
5421
5643
  capabilities.add("preact");
5644
+ const preactMajor = project.preactMajorVersion;
5645
+ if (preactMajor !== null) {
5646
+ const cappedPreactMajor = Math.min(preactMajor, 20);
5647
+ for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
5648
+ }
5422
5649
  if (project.reactVersion === null) capabilities.add("pure-preact");
5423
5650
  }
5424
5651
  return capabilities;
@@ -5675,6 +5902,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5675
5902
  if (!publicEnvPrefix) return fallbackRecommendation;
5676
5903
  return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
5677
5904
  };
5905
+ const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
5906
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5907
+ if (rule !== "immutability") return help;
5908
+ if (!project.hasReanimated) return help;
5909
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5910
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5911
+ };
5678
5912
  const REACT_MODULE_SOURCE = "react";
5679
5913
  const REQUIRE_IDENTIFIER = "require";
5680
5914
  const USE_IDENTIFIER = "use";
@@ -5998,7 +6232,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5998
6232
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5999
6233
  if (plugin === "react-hooks-js") return {
6000
6234
  message: REACT_COMPILER_MESSAGE,
6001
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6235
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6002
6236
  };
6003
6237
  return {
6004
6238
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -6067,13 +6301,6 @@ const SANITIZED_ENV = (() => {
6067
6301
  }
6068
6302
  return sanitized;
6069
6303
  })();
6070
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6071
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6072
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6073
- const parsed = Number(raw);
6074
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6075
- return parsed;
6076
- })();
6077
6304
  /**
6078
6305
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6079
6306
  * output size. Returns stdout on success; raises a tagged
@@ -6090,7 +6317,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6090
6317
  * The first three are splittable (the caller's binary-split retry
6091
6318
  * shrinks the batch and re-spawns); the fourth isn't.
6092
6319
  */
6093
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
6320
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
6094
6321
  const child = spawn(nodeBinaryPath, args, {
6095
6322
  cwd: rootDirectory,
6096
6323
  env: SANITIZED_ENV
@@ -6099,9 +6326,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6099
6326
  child.kill("SIGKILL");
6100
6327
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6101
6328
  kind: "timeout",
6102
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6329
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6103
6330
  }) }));
6104
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6331
+ }, spawnTimeoutMs);
6105
6332
  timeoutHandle.unref?.();
6106
6333
  const stdoutBuffers = [];
6107
6334
  const stderrBuffers = [];
@@ -6111,7 +6338,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6111
6338
  const killIfTooLarge = (incomingBytes, isStdout) => {
6112
6339
  if (isStdout) stdoutByteCount += incomingBytes;
6113
6340
  else stderrByteCount += incomingBytes;
6114
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6341
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6115
6342
  didKillForSize = true;
6116
6343
  child.kill("SIGKILL");
6117
6344
  return true;
@@ -6137,7 +6364,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6137
6364
  if (didKillForSize) {
6138
6365
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6139
6366
  kind: "output-too-large",
6140
- detail: `exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`
6367
+ detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
6141
6368
  }) }));
6142
6369
  return;
6143
6370
  }
@@ -6178,7 +6405,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6178
6405
  * with a slimmer config in that case.
6179
6406
  */
6180
6407
  const spawnLintBatches = async (input) => {
6181
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6408
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6182
6409
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6183
6410
  const allDiagnostics = [];
6184
6411
  const droppedFiles = [];
@@ -6186,7 +6413,7 @@ const spawnLintBatches = async (input) => {
6186
6413
  const spawnLintBatch = async (batch) => {
6187
6414
  const batchArgs = [...baseArgs, ...batch];
6188
6415
  try {
6189
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6416
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6190
6417
  } catch (error) {
6191
6418
  if (!isSplittableReactDoctorError(error)) throw error;
6192
6419
  if (batch.length <= 1) {
@@ -6289,13 +6516,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6289
6516
  * 6. always restore disable directives + clean up the temp dir
6290
6517
  */
6291
6518
  const runOxlint = async (options) => {
6292
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
6519
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
6293
6520
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6294
6521
  const severityControls = buildRuleSeverityControls(userConfig);
6295
6522
  validateRuleRegistration();
6296
6523
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6297
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6298
- const configPath = path.join(configDirectory, "oxlintrc.json");
6299
6524
  const pluginPath = resolvePluginPath();
6300
6525
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6301
6526
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6310,6 +6535,8 @@ const runOxlint = async (options) => {
6310
6535
  userPlugins
6311
6536
  });
6312
6537
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
6538
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6539
+ const configPath = path.join(configDirectory, "oxlintrc.json");
6313
6540
  try {
6314
6541
  const baseArgs = [
6315
6542
  resolveOxlintBinary(),
@@ -6336,7 +6563,9 @@ const runOxlint = async (options) => {
6336
6563
  nodeBinaryPath,
6337
6564
  project,
6338
6565
  onPartialFailure,
6339
- onFileProgress: options.onFileProgress
6566
+ onFileProgress: options.onFileProgress,
6567
+ spawnTimeoutMs,
6568
+ outputMaxBytes
6340
6569
  });
6341
6570
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6342
6571
  try {
@@ -6402,6 +6631,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6402
6631
  */
6403
6632
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6404
6633
  const partialFailures = yield* LintPartialFailures;
6634
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6635
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6405
6636
  const collectedFailures = [];
6406
6637
  const diagnostics = yield* Effect.tryPromise({
6407
6638
  try: () => runOxlint({
@@ -6418,7 +6649,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6418
6649
  onPartialFailure: (reason) => {
6419
6650
  collectedFailures.push(reason);
6420
6651
  },
6421
- onFileProgress: input.onFileProgress
6652
+ onFileProgress: input.onFileProgress,
6653
+ spawnTimeoutMs,
6654
+ outputMaxBytes
6422
6655
  }),
6423
6656
  catch: ensureReactDoctorError
6424
6657
  });
@@ -6716,7 +6949,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6716
6949
  const resolvedConfig = yield* configService.resolve(input.directory);
6717
6950
  const scanDirectory = resolvedConfig.resolvedDirectory;
6718
6951
  const project = yield* projectService.discover(scanDirectory);
6719
- if (project.reactVersion === null) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6952
+ if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6720
6953
  const [repo, sha, defaultBranch] = yield* Effect.all([
6721
6954
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6722
6955
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6744,7 +6977,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6744
6977
  const lintFailure = yield* Ref.make({
6745
6978
  didFail: false,
6746
6979
  reason: null,
6747
- reasonTag: null
6980
+ reasonTag: null,
6981
+ reasonKind: null
6748
6982
  });
6749
6983
  const deadCodeFailure = yield* Ref.make({
6750
6984
  didFail: false,
@@ -6766,13 +7000,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6766
7000
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6767
7001
  onFileProgress: (scannedFileCount, totalFileCount) => {
6768
7002
  lastReportedTotalFileCount = totalFileCount;
6769
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7003
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6770
7004
  }
6771
7005
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6772
7006
  yield* Ref.set(lintFailure, {
6773
7007
  didFail: true,
6774
7008
  reason: error.message,
6775
- reasonTag: error.reason._tag
7009
+ reasonTag: error.reason._tag,
7010
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6776
7011
  });
6777
7012
  return Stream.empty;
6778
7013
  }))));
@@ -6832,6 +7067,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6832
7067
  didLintFail: lintFailureState.didFail,
6833
7068
  lintFailureReason: lintFailureState.reason,
6834
7069
  lintFailureReasonTag: lintFailureState.reasonTag,
7070
+ lintFailureReasonKind: lintFailureState.reasonKind,
6835
7071
  lintPartialFailures,
6836
7072
  didDeadCodeFail: deadCodeFailureState.didFail,
6837
7073
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7269,6 +7505,13 @@ const highlighter = {
7269
7505
  gray: import_picocolors.default.gray,
7270
7506
  bold: import_picocolors.default.bold
7271
7507
  };
7508
+ /**
7509
+ * Canonical URL for a rule's reviewer-tested fix recipe, served at
7510
+ * `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. The
7511
+ * `/doctor` playbook fetches it on demand so each fix follows the
7512
+ * canonical recipe instead of being improvised per diagnostic.
7513
+ */
7514
+ const buildRulePromptUrl = (plugin, rule) => `${PROMPTS_RULES_BASE_URL}/${plugin}/${rule}.md`;
7272
7515
  const groupBy = (items, keyFn) => {
7273
7516
  const groups = /* @__PURE__ */ new Map();
7274
7517
  for (const item of items) {
@@ -7316,6 +7559,6 @@ const cliLogger = {
7316
7559
  }
7317
7560
  };
7318
7561
  //#endregion
7319
- export { isReactDoctorError as A, filterSourceFiles as C, groupBy as D, getDiffInfo as E, runInspect as F, toRelativePath as I, listWorkspacePackages as M, resolveScanTarget as N, highlighter as O, restoreLegacyThrow as P, filterDiagnosticsForSurface as S, formatReactDoctorError as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, layerOtlp as j, isMonorepoRoot as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, formatErrorChain as w, discoverReactSubprojects as x, buildJsonReport as y };
7562
+ export { isMonorepoRoot as A, filterDiagnosticsForSurface as C, getDiffInfo as D, formatReactDoctorError as E, restoreLegacyThrow as F, runInspect as I, toRelativePath as L, layerOtlp as M, listWorkspacePackages as N, groupBy as O, resolveScanTarget as P, discoverReactSubprojects as S, formatErrorChain as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, isReactDoctorError as j, highlighter as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, filterSourceFiles as w, buildRulePromptUrl as x, buildJsonReport as y };
7320
7563
 
7321
- //# sourceMappingURL=cli-logger-BRBUS1pE.js.map
7564
+ //# sourceMappingURL=cli-logger-CSZagq1E.js.map