react-doctor 0.2.11 → 0.2.12-dev.269ca17

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.
@@ -2281,15 +2281,91 @@ const detectFramework = (dependencies) => {
2281
2281
  if (dependencies.preact && !dependencies.react) return "preact";
2282
2282
  return "unknown";
2283
2283
  };
2284
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2285
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2286
- const OR_SEPARATOR = /\s*\|\|\s*/;
2287
2284
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2288
2285
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2289
2286
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2290
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2291
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2292
2287
  const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
2288
+ const isDigit = (value) => value !== void 0 && value >= "0" && value <= "9";
2289
+ const isWhitespace = (value) => value === " " || value === " " || value === "\n" || value === "\r" || value === "\f" || value === "\v";
2290
+ const isSeparator = (value) => isWhitespace(value) || value === "," || value === "|";
2291
+ const skipWhitespace = (value, start) => {
2292
+ let index = start;
2293
+ while (isWhitespace(value[index])) index += 1;
2294
+ return index;
2295
+ };
2296
+ const skipSeparators = (value, start) => {
2297
+ let index = start;
2298
+ while (isSeparator(value[index])) index += 1;
2299
+ return index;
2300
+ };
2301
+ const readDigits = (value, start) => {
2302
+ let index = start;
2303
+ while (isDigit(value[index])) index += 1;
2304
+ return index;
2305
+ };
2306
+ const getUpperBoundComparatorEnd = (version, start) => {
2307
+ if (version[start] !== "<") return null;
2308
+ let index = skipWhitespace(version, start + 1);
2309
+ if (version[index] === "=") index = skipWhitespace(version, index + 1);
2310
+ const majorStart = index;
2311
+ index = readDigits(version, index);
2312
+ if (index === majorStart) return null;
2313
+ for (let segments = 0; segments < 2 && version[index] === "."; segments += 1) {
2314
+ const segmentStart = index + 1;
2315
+ const segmentEnd = readDigits(version, segmentStart);
2316
+ if (segmentEnd === segmentStart) break;
2317
+ index = segmentEnd;
2318
+ }
2319
+ if (version[index] === "-") {
2320
+ index += 1;
2321
+ while (index < version.length && !isSeparator(version[index])) index += 1;
2322
+ }
2323
+ return index;
2324
+ };
2325
+ const stripUpperBoundComparators = (version) => {
2326
+ let stripped = "";
2327
+ let index = 0;
2328
+ while (index < version.length) {
2329
+ const comparatorEnd = getUpperBoundComparatorEnd(version, index);
2330
+ if (comparatorEnd === null) {
2331
+ stripped += version[index];
2332
+ index += 1;
2333
+ continue;
2334
+ }
2335
+ stripped += " ";
2336
+ index = comparatorEnd;
2337
+ }
2338
+ return stripped;
2339
+ };
2340
+ const hasNonLowerBoundComparator = (branch) => {
2341
+ for (let index = 0; index < branch.length; index += 1) {
2342
+ if (index > 0 && !isSeparator(branch[index - 1])) continue;
2343
+ if (branch[index] === ">" && branch[index + 1] !== "=") {
2344
+ if (isDigit(branch[skipWhitespace(branch, index + 1)])) return true;
2345
+ continue;
2346
+ }
2347
+ if (branch[index] !== "!") continue;
2348
+ let valueIndex = index + 1;
2349
+ if (branch[valueIndex] === "=") valueIndex += 1;
2350
+ if (branch[valueIndex] === "=") valueIndex += 1;
2351
+ valueIndex = skipWhitespace(branch, valueIndex);
2352
+ if (isDigit(branch[valueIndex])) return true;
2353
+ }
2354
+ return false;
2355
+ };
2356
+ const isMajorTerminator = (value) => value === void 0 || isSeparator(value) || value === "." || value === "*" || value === "x" || value === "X" || value === "-";
2357
+ const getLowerBoundMajorAt = (branch, start) => {
2358
+ let index = start;
2359
+ if (branch[index] === ">" && branch[index + 1] === "=") index = skipWhitespace(branch, index + 2);
2360
+ else if (branch[index] === "~" || branch[index] === "^" || branch[index] === "=" || branch[index] === "v") index = skipWhitespace(branch, index + 1);
2361
+ const majorStart = index;
2362
+ const majorEnd = readDigits(branch, majorStart);
2363
+ if (majorEnd === majorStart || !isMajorTerminator(branch[majorEnd])) return null;
2364
+ return {
2365
+ end: majorEnd,
2366
+ major: Number.parseInt(branch.slice(majorStart, majorEnd), 10)
2367
+ };
2368
+ };
2293
2369
  const normalizeDependencyVersion = (version) => {
2294
2370
  const trimmed = version.trim();
2295
2371
  if (trimmed.length === 0) return null;
@@ -2299,17 +2375,29 @@ const normalizeDependencyVersion = (version) => {
2299
2375
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2300
2376
  return normalizedVersion;
2301
2377
  };
2302
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2303
- const hasUpperBoundComparator = (version) => HAS_UPPER_BOUND_COMPARATOR.test(version);
2378
+ const splitDependencyVersionBranches = (version) => version.split("||").map((branch) => branch.trim()).filter(Boolean);
2379
+ const hasUpperBoundComparator = (version) => {
2380
+ for (let index = 0; index < version.length; index += 1) if (getUpperBoundComparatorEnd(version, index) !== null) return true;
2381
+ return false;
2382
+ };
2304
2383
  const getBranchLowestMajor = (branch) => {
2305
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2306
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2384
+ if (hasNonLowerBoundComparator(branch)) return null;
2385
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2307
2386
  if (lowerBoundComparators.length === 0) return null;
2308
2387
  let branchLowestMajor = null;
2309
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2310
- const major = Number.parseInt(match[1], 10);
2311
- if (!Number.isFinite(major) || major <= 0) continue;
2312
- if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2388
+ let index = 0;
2389
+ while (index < lowerBoundComparators.length) {
2390
+ const lowerBoundStart = skipSeparators(lowerBoundComparators, index);
2391
+ if (lowerBoundStart > 0 && !isSeparator(lowerBoundComparators[lowerBoundStart - 1])) {
2392
+ index = lowerBoundStart + 1;
2393
+ continue;
2394
+ }
2395
+ const lowerBoundMajor = getLowerBoundMajorAt(lowerBoundComparators, lowerBoundStart);
2396
+ if (lowerBoundMajor !== null && Number.isFinite(lowerBoundMajor.major) && lowerBoundMajor.major > 0) {
2397
+ const major = lowerBoundMajor.major;
2398
+ if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
2399
+ }
2400
+ index = lowerBoundMajor?.end ?? lowerBoundStart + 1;
2313
2401
  }
2314
2402
  return branchLowestMajor;
2315
2403
  };
@@ -2478,6 +2566,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2478
2566
  const EMPTY_DEPENDENCY_INFO = {
2479
2567
  reactVersion: null,
2480
2568
  tailwindVersion: null,
2569
+ zodVersion: null,
2481
2570
  framework: "unknown"
2482
2571
  };
2483
2572
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2506,6 +2595,11 @@ const extractDependencyInfo = (packageJson) => {
2506
2595
  "devDependencies",
2507
2596
  "peerDependencies"
2508
2597
  ]),
2598
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2599
+ "dependencies",
2600
+ "devDependencies",
2601
+ "peerDependencies"
2602
+ ]),
2509
2603
  framework: detectFramework(allDependencies)
2510
2604
  };
2511
2605
  };
@@ -2650,8 +2744,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2650
2744
  workspaceDirectory,
2651
2745
  workspacePackageJson
2652
2746
  });
2747
+ const zodVersion = resolveWorkspaceDependencyVersion({
2748
+ concreteVersion: info.zodVersion,
2749
+ packageName: "zod",
2750
+ rootDirectory,
2751
+ rootPackageJson: packageJson,
2752
+ sections: [
2753
+ "dependencies",
2754
+ "devDependencies",
2755
+ "peerDependencies"
2756
+ ],
2757
+ workspaceDirectory,
2758
+ workspacePackageJson
2759
+ });
2653
2760
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2654
2761
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2762
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2655
2763
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2656
2764
  const resultReactMajor = parseReactMajor(result.reactVersion);
2657
2765
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2686,14 +2794,26 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2686
2794
  "peerDependencies"
2687
2795
  ]
2688
2796
  }) : null;
2797
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2798
+ packageJson: leafPackageJson,
2799
+ packageName: "zod",
2800
+ sections: [
2801
+ "dependencies",
2802
+ "devDependencies",
2803
+ "peerDependencies"
2804
+ ]
2805
+ }) : null;
2689
2806
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2690
2807
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2808
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2691
2809
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2692
2810
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2811
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2693
2812
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2694
2813
  return {
2695
2814
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2696
2815
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2816
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2697
2817
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2698
2818
  };
2699
2819
  };
@@ -2743,12 +2863,12 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2743
2863
  return false;
2744
2864
  };
2745
2865
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2746
- const hasPreact = (packageJson) => {
2747
- return "preact" in {
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",
@@ -2773,6 +2893,10 @@ const isPackageJsonReanimatedAware = (packageJson) => {
2773
2893
  };
2774
2894
  return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2775
2895
  };
2896
+ const parseZodMajor = (zodVersion) => {
2897
+ if (typeof zodVersion !== "string") return null;
2898
+ return getLowestDependencyMajor(zodVersion);
2899
+ };
2776
2900
  const hasUpperBoundOnlyPeerRange = (range) => {
2777
2901
  if (typeof range !== "string") return false;
2778
2902
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2859,12 +2983,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2859
2983
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2860
2984
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2861
2985
  };
2986
+ const NON_PROJECT_DIRECTORIES = new Set([
2987
+ "AppData",
2988
+ "Application Data",
2989
+ "Library"
2990
+ ]);
2991
+ const MAX_SCAN_DEPTH = 6;
2862
2992
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2863
2993
  const packages = [];
2864
- const pendingDirectories = [rootDirectory];
2994
+ const pendingDirectories = [{
2995
+ directory: rootDirectory,
2996
+ depth: 0
2997
+ }];
2865
2998
  while (pendingDirectories.length > 0) {
2866
- const currentDirectory = pendingDirectories.pop();
2867
- if (!currentDirectory) continue;
2999
+ const current = pendingDirectories.pop();
3000
+ if (!current) continue;
3001
+ const { directory: currentDirectory, depth } = current;
2868
3002
  const packageJsonPath = path.join(currentDirectory, "package.json");
2869
3003
  if (isFile(packageJsonPath)) {
2870
3004
  const packageJson = readPackageJson(packageJsonPath);
@@ -2876,10 +3010,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2876
3010
  });
2877
3011
  }
2878
3012
  }
3013
+ if (depth >= MAX_SCAN_DEPTH) continue;
2879
3014
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2880
3015
  for (const entry of entries) {
2881
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2882
- 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
+ });
2883
3021
  }
2884
3022
  }
2885
3023
  return packages;
@@ -2897,7 +3035,7 @@ const discoverProject = (directory) => {
2897
3035
  const packageJsonPath = path.join(directory, "package.json");
2898
3036
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2899
3037
  const packageJson = readPackageJson(packageJsonPath);
2900
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3038
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2901
3039
  const reactDeclaration = getDependencyDeclaration({
2902
3040
  packageJson,
2903
3041
  packageName: "react",
@@ -2916,9 +3054,19 @@ const discoverProject = (directory) => {
2916
3054
  "peerDependencies"
2917
3055
  ]
2918
3056
  });
3057
+ const zodDeclaration = getDependencyDeclaration({
3058
+ packageJson,
3059
+ packageName: "zod",
3060
+ sections: [
3061
+ "dependencies",
3062
+ "devDependencies",
3063
+ "peerDependencies"
3064
+ ]
3065
+ });
2919
3066
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2920
3067
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2921
- if (!reactVersion || !tailwindVersion) {
3068
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3069
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2922
3070
  const monorepoRoot = findMonorepoRoot(directory);
2923
3071
  if (monorepoRoot) {
2924
3072
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2926,6 +3074,7 @@ const discoverProject = (directory) => {
2926
3074
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2927
3075
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2928
3076
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3077
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2929
3078
  }
2930
3079
  }
2931
3080
  }
@@ -2933,32 +3082,39 @@ const discoverProject = (directory) => {
2933
3082
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2934
3083
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2935
3084
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3085
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2936
3086
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2937
3087
  }
2938
3088
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2939
3089
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2940
3090
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2941
3091
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3092
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2942
3093
  if (framework === "unknown") framework = monorepoInfo.framework;
2943
3094
  }
2944
3095
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2945
3096
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3097
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2946
3098
  const projectName = packageJson.name ?? path.basename(directory);
2947
3099
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2948
3100
  const sourceFileCount = countSourceFiles(directory);
2949
3101
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2950
3102
  const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3103
+ const preactVersion = getPreactVersion(packageJson);
2951
3104
  const projectInfo = {
2952
3105
  rootDirectory: directory,
2953
3106
  projectName,
2954
3107
  reactVersion,
2955
3108
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2956
3109
  tailwindVersion,
3110
+ zodVersion,
3111
+ zodMajorVersion: parseZodMajor(zodVersion),
2957
3112
  framework,
2958
3113
  hasTypeScript,
2959
3114
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2960
3115
  hasTanStackQuery: hasTanStackQuery(packageJson),
2961
- hasPreact: hasPreact(packageJson),
3116
+ preactVersion,
3117
+ preactMajorVersion: parseReactMajor(preactVersion),
2962
3118
  hasReactNativeWorkspace,
2963
3119
  hasReanimated,
2964
3120
  sourceFileCount
@@ -2966,6 +3122,7 @@ const discoverProject = (directory) => {
2966
3122
  cachedProjectInfos.set(directory, projectInfo);
2967
3123
  return projectInfo;
2968
3124
  };
3125
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2969
3126
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2970
3127
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2971
3128
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3033,6 +3190,7 @@ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3033
3190
  const MILLISECONDS_PER_SECOND = 1e3;
3034
3191
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3035
3192
  const SHARE_BASE_URL = "https://www.react.doctor/share";
3193
+ const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
3036
3194
  const FETCH_TIMEOUT_MS = 1e4;
3037
3195
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
3038
3196
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
@@ -3935,17 +4093,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3935
4093
  headers
3936
4094
  }).pipe(Layer.provide(FetchHttpClient.layer));
3937
4095
  }).pipe(Effect.orDie));
3938
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3939
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3940
- 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: () => {
3941
4103
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3942
4104
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3943
4105
  const parsed = Number(raw);
3944
4106
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3945
4107
  return parsed;
3946
- } });
3947
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3948
- 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 }) {};
3949
4116
  const DIAGNOSTIC_SURFACES = [
3950
4117
  "cli",
3951
4118
  "prComment",
@@ -5013,8 +5180,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5013
5180
  env: input.env,
5014
5181
  extendEnv: true
5015
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))));
5016
5190
  const [stdout, stderr, status] = yield* Effect.all([
5017
- Stream.mkString(Stream.decodeText(handle.stdout)),
5191
+ Stream.mkString(Stream.decodeText(stdoutStream)),
5018
5192
  Stream.mkString(Stream.decodeText(handle.stderr)),
5019
5193
  handle.exitCode
5020
5194
  ], { concurrency: 3 });
@@ -5176,7 +5350,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5176
5350
  if (result.status !== 0) return [];
5177
5351
  return splitNullSeparated(result.stdout);
5178
5352
  })),
5179
- 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)),
5180
5359
  grep: (input) => Effect.gen(function* () {
5181
5360
  const args = ["grep"];
5182
5361
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5184,7 +5363,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5184
5363
  if (input.extendedRegexp ?? false) args.push("-E");
5185
5364
  args.push(input.pattern);
5186
5365
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5187
- 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
+ });
5188
5372
  if (result.status === 128) return null;
5189
5373
  return {
5190
5374
  status: result.status,
@@ -5432,7 +5616,8 @@ const buildCapabilities = (project) => {
5432
5616
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5433
5617
  const reactMajor = project.reactMajorVersion;
5434
5618
  if (reactMajor !== null) {
5435
- 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}`);
5436
5621
  if (reactMajor >= 19) {
5437
5622
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5438
5623
  major: 19,
@@ -5447,11 +5632,20 @@ const buildCapabilities = (project) => {
5447
5632
  minor: 4
5448
5633
  })) capabilities.add("tailwind:3.4");
5449
5634
  }
5635
+ if (project.zodVersion !== null) {
5636
+ capabilities.add("zod");
5637
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5638
+ }
5450
5639
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5451
5640
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5452
5641
  if (project.hasTypeScript) capabilities.add("typescript");
5453
- if (project.hasPreact) {
5642
+ if (project.preactVersion !== null) {
5454
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
+ }
5455
5649
  if (project.reactVersion === null) capabilities.add("pure-preact");
5456
5650
  }
5457
5651
  return capabilities;
@@ -6107,13 +6301,6 @@ const SANITIZED_ENV = (() => {
6107
6301
  }
6108
6302
  return sanitized;
6109
6303
  })();
6110
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6111
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6112
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6113
- const parsed = Number(raw);
6114
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6115
- return parsed;
6116
- })();
6117
6304
  /**
6118
6305
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6119
6306
  * output size. Returns stdout on success; raises a tagged
@@ -6130,7 +6317,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6130
6317
  * The first three are splittable (the caller's binary-split retry
6131
6318
  * shrinks the batch and re-spawns); the fourth isn't.
6132
6319
  */
6133
- 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) => {
6134
6321
  const child = spawn(nodeBinaryPath, args, {
6135
6322
  cwd: rootDirectory,
6136
6323
  env: SANITIZED_ENV
@@ -6139,9 +6326,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6139
6326
  child.kill("SIGKILL");
6140
6327
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6141
6328
  kind: "timeout",
6142
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6329
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6143
6330
  }) }));
6144
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6331
+ }, spawnTimeoutMs);
6145
6332
  timeoutHandle.unref?.();
6146
6333
  const stdoutBuffers = [];
6147
6334
  const stderrBuffers = [];
@@ -6151,7 +6338,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6151
6338
  const killIfTooLarge = (incomingBytes, isStdout) => {
6152
6339
  if (isStdout) stdoutByteCount += incomingBytes;
6153
6340
  else stderrByteCount += incomingBytes;
6154
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6341
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6155
6342
  didKillForSize = true;
6156
6343
  child.kill("SIGKILL");
6157
6344
  return true;
@@ -6177,7 +6364,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6177
6364
  if (didKillForSize) {
6178
6365
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6179
6366
  kind: "output-too-large",
6180
- 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`
6181
6368
  }) }));
6182
6369
  return;
6183
6370
  }
@@ -6218,7 +6405,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6218
6405
  * with a slimmer config in that case.
6219
6406
  */
6220
6407
  const spawnLintBatches = async (input) => {
6221
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6408
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6222
6409
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6223
6410
  const allDiagnostics = [];
6224
6411
  const droppedFiles = [];
@@ -6226,7 +6413,7 @@ const spawnLintBatches = async (input) => {
6226
6413
  const spawnLintBatch = async (batch) => {
6227
6414
  const batchArgs = [...baseArgs, ...batch];
6228
6415
  try {
6229
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6416
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6230
6417
  } catch (error) {
6231
6418
  if (!isSplittableReactDoctorError(error)) throw error;
6232
6419
  if (batch.length <= 1) {
@@ -6329,13 +6516,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6329
6516
  * 6. always restore disable directives + clean up the temp dir
6330
6517
  */
6331
6518
  const runOxlint = async (options) => {
6332
- 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;
6333
6520
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6334
6521
  const severityControls = buildRuleSeverityControls(userConfig);
6335
6522
  validateRuleRegistration();
6336
6523
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6337
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6338
- const configPath = path.join(configDirectory, "oxlintrc.json");
6339
6524
  const pluginPath = resolvePluginPath();
6340
6525
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6341
6526
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6350,6 +6535,8 @@ const runOxlint = async (options) => {
6350
6535
  userPlugins
6351
6536
  });
6352
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");
6353
6540
  try {
6354
6541
  const baseArgs = [
6355
6542
  resolveOxlintBinary(),
@@ -6376,7 +6563,9 @@ const runOxlint = async (options) => {
6376
6563
  nodeBinaryPath,
6377
6564
  project,
6378
6565
  onPartialFailure,
6379
- onFileProgress: options.onFileProgress
6566
+ onFileProgress: options.onFileProgress,
6567
+ spawnTimeoutMs,
6568
+ outputMaxBytes
6380
6569
  });
6381
6570
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6382
6571
  try {
@@ -6442,6 +6631,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6442
6631
  */
6443
6632
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6444
6633
  const partialFailures = yield* LintPartialFailures;
6634
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6635
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6445
6636
  const collectedFailures = [];
6446
6637
  const diagnostics = yield* Effect.tryPromise({
6447
6638
  try: () => runOxlint({
@@ -6458,7 +6649,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6458
6649
  onPartialFailure: (reason) => {
6459
6650
  collectedFailures.push(reason);
6460
6651
  },
6461
- onFileProgress: input.onFileProgress
6652
+ onFileProgress: input.onFileProgress,
6653
+ spawnTimeoutMs,
6654
+ outputMaxBytes
6462
6655
  }),
6463
6656
  catch: ensureReactDoctorError
6464
6657
  });
@@ -6756,7 +6949,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6756
6949
  const resolvedConfig = yield* configService.resolve(input.directory);
6757
6950
  const scanDirectory = resolvedConfig.resolvedDirectory;
6758
6951
  const project = yield* projectService.discover(scanDirectory);
6759
- 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 }) });
6760
6953
  const [repo, sha, defaultBranch] = yield* Effect.all([
6761
6954
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6762
6955
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6784,7 +6977,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6784
6977
  const lintFailure = yield* Ref.make({
6785
6978
  didFail: false,
6786
6979
  reason: null,
6787
- reasonTag: null
6980
+ reasonTag: null,
6981
+ reasonKind: null
6788
6982
  });
6789
6983
  const deadCodeFailure = yield* Ref.make({
6790
6984
  didFail: false,
@@ -6806,13 +7000,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6806
7000
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6807
7001
  onFileProgress: (scannedFileCount, totalFileCount) => {
6808
7002
  lastReportedTotalFileCount = totalFileCount;
6809
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7003
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6810
7004
  }
6811
7005
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6812
7006
  yield* Ref.set(lintFailure, {
6813
7007
  didFail: true,
6814
7008
  reason: error.message,
6815
- reasonTag: error.reason._tag
7009
+ reasonTag: error.reason._tag,
7010
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6816
7011
  });
6817
7012
  return Stream.empty;
6818
7013
  }))));
@@ -6872,6 +7067,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6872
7067
  didLintFail: lintFailureState.didFail,
6873
7068
  lintFailureReason: lintFailureState.reason,
6874
7069
  lintFailureReasonTag: lintFailureState.reasonTag,
7070
+ lintFailureReasonKind: lintFailureState.reasonKind,
6875
7071
  lintPartialFailures,
6876
7072
  didDeadCodeFail: deadCodeFailureState.didFail,
6877
7073
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7309,6 +7505,13 @@ const highlighter = {
7309
7505
  gray: import_picocolors.default.gray,
7310
7506
  bold: import_picocolors.default.bold
7311
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`;
7312
7515
  const groupBy = (items, keyFn) => {
7313
7516
  const groups = /* @__PURE__ */ new Map();
7314
7517
  for (const item of items) {
@@ -7356,6 +7559,6 @@ const cliLogger = {
7356
7559
  }
7357
7560
  };
7358
7561
  //#endregion
7359
- 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 };
7360
7563
 
7361
- //# sourceMappingURL=cli-logger-pbFEieEc.js.map
7564
+ //# sourceMappingURL=cli-logger-CSZagq1E.js.map