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.
package/dist/index.js CHANGED
@@ -2307,15 +2307,91 @@ const detectFramework = (dependencies) => {
2307
2307
  if (dependencies.preact && !dependencies.react) return "preact";
2308
2308
  return "unknown";
2309
2309
  };
2310
- const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
2311
- const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
2312
- const OR_SEPARATOR = /\s*\|\|\s*/;
2313
2310
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
2314
2311
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
2315
2312
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
2316
- const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
2317
- const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
2318
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
+ };
2319
2395
  const normalizeDependencyVersion = (version) => {
2320
2396
  const trimmed = version.trim();
2321
2397
  if (trimmed.length === 0) return null;
@@ -2325,17 +2401,29 @@ const normalizeDependencyVersion = (version) => {
2325
2401
  if (WILDCARD_VERSION.test(normalizedVersion)) return null;
2326
2402
  return normalizedVersion;
2327
2403
  };
2328
- const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
2329
- 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
+ };
2330
2409
  const getBranchLowestMajor = (branch) => {
2331
- if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
2332
- const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
2410
+ if (hasNonLowerBoundComparator(branch)) return null;
2411
+ const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
2333
2412
  if (lowerBoundComparators.length === 0) return null;
2334
2413
  let branchLowestMajor = null;
2335
- for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
2336
- const major = Number.parseInt(match[1], 10);
2337
- if (!Number.isFinite(major) || major <= 0) continue;
2338
- 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;
2339
2427
  }
2340
2428
  return branchLowestMajor;
2341
2429
  };
@@ -2504,6 +2592,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
2504
2592
  const EMPTY_DEPENDENCY_INFO = {
2505
2593
  reactVersion: null,
2506
2594
  tailwindVersion: null,
2595
+ zodVersion: null,
2507
2596
  framework: "unknown"
2508
2597
  };
2509
2598
  const pickConcreteVersion = (packageJson, packageName, sections) => {
@@ -2532,6 +2621,11 @@ const extractDependencyInfo = (packageJson) => {
2532
2621
  "devDependencies",
2533
2622
  "peerDependencies"
2534
2623
  ]),
2624
+ zodVersion: pickConcreteVersion(packageJson, "zod", [
2625
+ "dependencies",
2626
+ "devDependencies",
2627
+ "peerDependencies"
2628
+ ]),
2535
2629
  framework: detectFramework(allDependencies)
2536
2630
  };
2537
2631
  };
@@ -2676,8 +2770,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
2676
2770
  workspaceDirectory,
2677
2771
  workspacePackageJson
2678
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
+ });
2679
2786
  if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
2680
2787
  if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
2788
+ if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
2681
2789
  if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
2682
2790
  const resultReactMajor = parseReactMajor(result.reactVersion);
2683
2791
  if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
@@ -2712,14 +2820,26 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2712
2820
  "peerDependencies"
2713
2821
  ]
2714
2822
  }) : null;
2823
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
2824
+ packageJson: leafPackageJson,
2825
+ packageName: "zod",
2826
+ sections: [
2827
+ "dependencies",
2828
+ "devDependencies",
2829
+ "peerDependencies"
2830
+ ]
2831
+ }) : null;
2715
2832
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
2716
2833
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
2834
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
2717
2835
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
2718
2836
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
2837
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
2719
2838
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
2720
2839
  return {
2721
2840
  reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
2722
2841
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
2842
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
2723
2843
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2724
2844
  };
2725
2845
  };
@@ -2769,12 +2889,12 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2769
2889
  return false;
2770
2890
  };
2771
2891
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2772
- const hasPreact = (packageJson) => {
2773
- return "preact" in {
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",
@@ -2799,6 +2919,10 @@ const isPackageJsonReanimatedAware = (packageJson) => {
2799
2919
  };
2800
2920
  return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2801
2921
  };
2922
+ const parseZodMajor = (zodVersion) => {
2923
+ if (typeof zodVersion !== "string") return null;
2924
+ return getLowestDependencyMajor(zodVersion);
2925
+ };
2802
2926
  const hasUpperBoundOnlyPeerRange = (range) => {
2803
2927
  if (typeof range !== "string") return false;
2804
2928
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2885,12 +3009,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2885
3009
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2886
3010
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2887
3011
  };
3012
+ const NON_PROJECT_DIRECTORIES = new Set([
3013
+ "AppData",
3014
+ "Application Data",
3015
+ "Library"
3016
+ ]);
3017
+ const MAX_SCAN_DEPTH = 6;
2888
3018
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2889
3019
  const packages = [];
2890
- const pendingDirectories = [rootDirectory];
3020
+ const pendingDirectories = [{
3021
+ directory: rootDirectory,
3022
+ depth: 0
3023
+ }];
2891
3024
  while (pendingDirectories.length > 0) {
2892
- const currentDirectory = pendingDirectories.pop();
2893
- if (!currentDirectory) continue;
3025
+ const current = pendingDirectories.pop();
3026
+ if (!current) continue;
3027
+ const { directory: currentDirectory, depth } = current;
2894
3028
  const packageJsonPath = path.join(currentDirectory, "package.json");
2895
3029
  if (isFile(packageJsonPath)) {
2896
3030
  const packageJson = readPackageJson(packageJsonPath);
@@ -2902,10 +3036,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2902
3036
  });
2903
3037
  }
2904
3038
  }
3039
+ if (depth >= MAX_SCAN_DEPTH) continue;
2905
3040
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2906
3041
  for (const entry of entries) {
2907
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2908
- 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
+ });
2909
3047
  }
2910
3048
  }
2911
3049
  return packages;
@@ -2926,7 +3064,7 @@ const discoverProject = (directory) => {
2926
3064
  const packageJsonPath = path.join(directory, "package.json");
2927
3065
  if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
2928
3066
  const packageJson = readPackageJson(packageJsonPath);
2929
- let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
3067
+ let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
2930
3068
  const reactDeclaration = getDependencyDeclaration({
2931
3069
  packageJson,
2932
3070
  packageName: "react",
@@ -2945,9 +3083,19 @@ const discoverProject = (directory) => {
2945
3083
  "peerDependencies"
2946
3084
  ]
2947
3085
  });
3086
+ const zodDeclaration = getDependencyDeclaration({
3087
+ packageJson,
3088
+ packageName: "zod",
3089
+ sections: [
3090
+ "dependencies",
3091
+ "devDependencies",
3092
+ "peerDependencies"
3093
+ ]
3094
+ });
2948
3095
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
2949
3096
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
2950
- if (!reactVersion || !tailwindVersion) {
3097
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
3098
+ if (!reactVersion || !tailwindVersion || !zodVersion) {
2951
3099
  const monorepoRoot = findMonorepoRoot(directory);
2952
3100
  if (monorepoRoot) {
2953
3101
  const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
@@ -2955,6 +3103,7 @@ const discoverProject = (directory) => {
2955
3103
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
2956
3104
  if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
2957
3105
  if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
3106
+ if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
2958
3107
  }
2959
3108
  }
2960
3109
  }
@@ -2962,32 +3111,39 @@ const discoverProject = (directory) => {
2962
3111
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
2963
3112
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
2964
3113
  if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
3114
+ if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
2965
3115
  if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
2966
3116
  }
2967
3117
  if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
2968
3118
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
2969
3119
  if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
2970
3120
  if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
3121
+ if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
2971
3122
  if (framework === "unknown") framework = monorepoInfo.framework;
2972
3123
  }
2973
3124
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
2974
3125
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
3126
+ if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
2975
3127
  const projectName = packageJson.name ?? path.basename(directory);
2976
3128
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2977
3129
  const sourceFileCount = countSourceFiles(directory);
2978
3130
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2979
3131
  const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
3132
+ const preactVersion = getPreactVersion(packageJson);
2980
3133
  const projectInfo = {
2981
3134
  rootDirectory: directory,
2982
3135
  projectName,
2983
3136
  reactVersion,
2984
3137
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
2985
3138
  tailwindVersion,
3139
+ zodVersion,
3140
+ zodMajorVersion: parseZodMajor(zodVersion),
2986
3141
  framework,
2987
3142
  hasTypeScript,
2988
3143
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2989
3144
  hasTanStackQuery: hasTanStackQuery(packageJson),
2990
- hasPreact: hasPreact(packageJson),
3145
+ preactVersion,
3146
+ preactMajorVersion: parseReactMajor(preactVersion),
2991
3147
  hasReactNativeWorkspace,
2992
3148
  hasReanimated,
2993
3149
  sourceFileCount
@@ -2995,6 +3151,7 @@ const discoverProject = (directory) => {
2995
3151
  cachedProjectInfos.set(directory, projectInfo);
2996
3152
  return projectInfo;
2997
3153
  };
3154
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2998
3155
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2999
3156
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
3000
3157
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3960,17 +4117,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3960
4117
  headers
3961
4118
  }).pipe(Layer.provide(FetchHttpClient.layer));
3962
4119
  }).pipe(Effect.orDie));
3963
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3964
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3965
- 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: () => {
3966
4127
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3967
4128
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3968
4129
  const parsed = Number(raw);
3969
4130
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3970
4131
  return parsed;
3971
- } });
3972
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3973
- 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 }) {};
3974
4140
  const DIAGNOSTIC_SURFACES = [
3975
4141
  "cli",
3976
4142
  "prComment",
@@ -5044,8 +5210,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5044
5210
  env: input.env,
5045
5211
  extendEnv: true
5046
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))));
5047
5220
  const [stdout, stderr, status] = yield* Effect.all([
5048
- Stream.mkString(Stream.decodeText(handle.stdout)),
5221
+ Stream.mkString(Stream.decodeText(stdoutStream)),
5049
5222
  Stream.mkString(Stream.decodeText(handle.stderr)),
5050
5223
  handle.exitCode
5051
5224
  ], { concurrency: 3 });
@@ -5207,7 +5380,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5207
5380
  if (result.status !== 0) return [];
5208
5381
  return splitNullSeparated(result.stdout);
5209
5382
  })),
5210
- 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)),
5211
5389
  grep: (input) => Effect.gen(function* () {
5212
5390
  const args = ["grep"];
5213
5391
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5215,7 +5393,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5215
5393
  if (input.extendedRegexp ?? false) args.push("-E");
5216
5394
  args.push(input.pattern);
5217
5395
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5218
- 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
+ });
5219
5402
  if (result.status === 128) return null;
5220
5403
  return {
5221
5404
  status: result.status,
@@ -5463,7 +5646,8 @@ const buildCapabilities = (project) => {
5463
5646
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5464
5647
  const reactMajor = project.reactMajorVersion;
5465
5648
  if (reactMajor !== null) {
5466
- 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}`);
5467
5651
  if (reactMajor >= 19) {
5468
5652
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5469
5653
  major: 19,
@@ -5478,11 +5662,20 @@ const buildCapabilities = (project) => {
5478
5662
  minor: 4
5479
5663
  })) capabilities.add("tailwind:3.4");
5480
5664
  }
5665
+ if (project.zodVersion !== null) {
5666
+ capabilities.add("zod");
5667
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
5668
+ }
5481
5669
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5482
5670
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5483
5671
  if (project.hasTypeScript) capabilities.add("typescript");
5484
- if (project.hasPreact) {
5672
+ if (project.preactVersion !== null) {
5485
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
+ }
5486
5679
  if (project.reactVersion === null) capabilities.add("pure-preact");
5487
5680
  }
5488
5681
  return capabilities;
@@ -6138,13 +6331,6 @@ const SANITIZED_ENV = (() => {
6138
6331
  }
6139
6332
  return sanitized;
6140
6333
  })();
6141
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6142
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6143
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6144
- const parsed = Number(raw);
6145
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6146
- return parsed;
6147
- })();
6148
6334
  /**
6149
6335
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6150
6336
  * output size. Returns stdout on success; raises a tagged
@@ -6161,7 +6347,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6161
6347
  * The first three are splittable (the caller's binary-split retry
6162
6348
  * shrinks the batch and re-spawns); the fourth isn't.
6163
6349
  */
6164
- 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) => {
6165
6351
  const child = spawn(nodeBinaryPath, args, {
6166
6352
  cwd: rootDirectory,
6167
6353
  env: SANITIZED_ENV
@@ -6170,9 +6356,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6170
6356
  child.kill("SIGKILL");
6171
6357
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6172
6358
  kind: "timeout",
6173
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6359
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6174
6360
  }) }));
6175
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6361
+ }, spawnTimeoutMs);
6176
6362
  timeoutHandle.unref?.();
6177
6363
  const stdoutBuffers = [];
6178
6364
  const stderrBuffers = [];
@@ -6182,7 +6368,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6182
6368
  const killIfTooLarge = (incomingBytes, isStdout) => {
6183
6369
  if (isStdout) stdoutByteCount += incomingBytes;
6184
6370
  else stderrByteCount += incomingBytes;
6185
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6371
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6186
6372
  didKillForSize = true;
6187
6373
  child.kill("SIGKILL");
6188
6374
  return true;
@@ -6208,7 +6394,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6208
6394
  if (didKillForSize) {
6209
6395
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6210
6396
  kind: "output-too-large",
6211
- 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`
6212
6398
  }) }));
6213
6399
  return;
6214
6400
  }
@@ -6249,7 +6435,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6249
6435
  * with a slimmer config in that case.
6250
6436
  */
6251
6437
  const spawnLintBatches = async (input) => {
6252
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6438
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6253
6439
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6254
6440
  const allDiagnostics = [];
6255
6441
  const droppedFiles = [];
@@ -6257,7 +6443,7 @@ const spawnLintBatches = async (input) => {
6257
6443
  const spawnLintBatch = async (batch) => {
6258
6444
  const batchArgs = [...baseArgs, ...batch];
6259
6445
  try {
6260
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6446
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6261
6447
  } catch (error) {
6262
6448
  if (!isSplittableReactDoctorError(error)) throw error;
6263
6449
  if (batch.length <= 1) {
@@ -6360,13 +6546,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6360
6546
  * 6. always restore disable directives + clean up the temp dir
6361
6547
  */
6362
6548
  const runOxlint = async (options) => {
6363
- 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;
6364
6550
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6365
6551
  const severityControls = buildRuleSeverityControls(userConfig);
6366
6552
  validateRuleRegistration();
6367
6553
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6368
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6369
- const configPath = path.join(configDirectory, "oxlintrc.json");
6370
6554
  const pluginPath = resolvePluginPath();
6371
6555
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6372
6556
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6381,6 +6565,8 @@ const runOxlint = async (options) => {
6381
6565
  userPlugins
6382
6566
  });
6383
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");
6384
6570
  try {
6385
6571
  const baseArgs = [
6386
6572
  resolveOxlintBinary(),
@@ -6407,7 +6593,9 @@ const runOxlint = async (options) => {
6407
6593
  nodeBinaryPath,
6408
6594
  project,
6409
6595
  onPartialFailure,
6410
- onFileProgress: options.onFileProgress
6596
+ onFileProgress: options.onFileProgress,
6597
+ spawnTimeoutMs,
6598
+ outputMaxBytes
6411
6599
  });
6412
6600
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6413
6601
  try {
@@ -6473,6 +6661,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6473
6661
  */
6474
6662
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6475
6663
  const partialFailures = yield* LintPartialFailures;
6664
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6665
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6476
6666
  const collectedFailures = [];
6477
6667
  const diagnostics = yield* Effect.tryPromise({
6478
6668
  try: () => runOxlint({
@@ -6489,7 +6679,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6489
6679
  onPartialFailure: (reason) => {
6490
6680
  collectedFailures.push(reason);
6491
6681
  },
6492
- onFileProgress: input.onFileProgress
6682
+ onFileProgress: input.onFileProgress,
6683
+ spawnTimeoutMs,
6684
+ outputMaxBytes
6493
6685
  }),
6494
6686
  catch: ensureReactDoctorError
6495
6687
  });
@@ -6787,7 +6979,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6787
6979
  const resolvedConfig = yield* configService.resolve(input.directory);
6788
6980
  const scanDirectory = resolvedConfig.resolvedDirectory;
6789
6981
  const project = yield* projectService.discover(scanDirectory);
6790
- 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 }) });
6791
6983
  const [repo, sha, defaultBranch] = yield* Effect.all([
6792
6984
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6793
6985
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6815,7 +7007,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6815
7007
  const lintFailure = yield* Ref.make({
6816
7008
  didFail: false,
6817
7009
  reason: null,
6818
- reasonTag: null
7010
+ reasonTag: null,
7011
+ reasonKind: null
6819
7012
  });
6820
7013
  const deadCodeFailure = yield* Ref.make({
6821
7014
  didFail: false,
@@ -6837,13 +7030,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6837
7030
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6838
7031
  onFileProgress: (scannedFileCount, totalFileCount) => {
6839
7032
  lastReportedTotalFileCount = totalFileCount;
6840
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
7033
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6841
7034
  }
6842
7035
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6843
7036
  yield* Ref.set(lintFailure, {
6844
7037
  didFail: true,
6845
7038
  reason: error.message,
6846
- reasonTag: error.reason._tag
7039
+ reasonTag: error.reason._tag,
7040
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6847
7041
  });
6848
7042
  return Stream.empty;
6849
7043
  }))));
@@ -6903,6 +7097,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6903
7097
  didLintFail: lintFailureState.didFail,
6904
7098
  lintFailureReason: lintFailureState.reason,
6905
7099
  lintFailureReasonTag: lintFailureState.reasonTag,
7100
+ lintFailureReasonKind: lintFailureState.reasonKind,
6906
7101
  lintPartialFailures,
6907
7102
  didDeadCodeFail: deadCodeFailureState.didFail,
6908
7103
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7337,11 +7532,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7337
7532
  const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7338
7533
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7339
7534
  const skippedChecks = [];
7535
+ if (output.didLintFail) skippedChecks.push("lint");
7536
+ if (output.didDeadCodeFail) skippedChecks.push("dead-code");
7340
7537
  const skippedCheckReasons = {};
7341
- if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) {
7342
- skippedChecks.push("dead-code");
7343
- skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7344
- }
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;
7345
7541
  return {
7346
7542
  diagnostics: [...output.diagnostics],
7347
7543
  score: output.score,