react-doctor 0.2.9 → 0.2.11-dev.f036b0f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2298,11 +2298,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
2298
2298
  gatsby: "Gatsby",
2299
2299
  expo: "Expo",
2300
2300
  "react-native": "React Native",
2301
+ preact: "Preact",
2301
2302
  unknown: "React"
2302
2303
  };
2303
2304
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2304
2305
  const detectFramework = (dependencies) => {
2305
2306
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2307
+ if (dependencies.preact && !dependencies.react) return "preact";
2306
2308
  return "unknown";
2307
2309
  };
2308
2310
  const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
@@ -2721,6 +2723,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2721
2723
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2722
2724
  };
2723
2725
  };
2726
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2727
+ if (predicate(rootPackageJson)) return true;
2728
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2729
+ if (patterns.length === 0) return false;
2730
+ const visitedDirectories = /* @__PURE__ */ new Set();
2731
+ for (const pattern of patterns) {
2732
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2733
+ for (const workspaceDirectory of directories) {
2734
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2735
+ visitedDirectories.add(workspaceDirectory);
2736
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2737
+ }
2738
+ }
2739
+ return false;
2740
+ };
2724
2741
  const NAMES = new Set([
2725
2742
  "react-native",
2726
2743
  "react-native-tvos",
@@ -2751,20 +2768,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2751
2768
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2752
2769
  return false;
2753
2770
  };
2754
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2755
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2756
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2757
- if (patterns.length === 0) return false;
2758
- const visitedDirectories = /* @__PURE__ */ new Set();
2759
- for (const pattern of patterns) {
2760
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2761
- for (const workspaceDirectory of directories) {
2762
- if (visitedDirectories.has(workspaceDirectory)) continue;
2763
- visitedDirectories.add(workspaceDirectory);
2764
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2765
- }
2766
- }
2767
- return false;
2771
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2772
+ const getPreactVersion = (packageJson) => {
2773
+ return {
2774
+ ...packageJson.peerDependencies,
2775
+ ...packageJson.dependencies,
2776
+ ...packageJson.devDependencies
2777
+ }.preact ?? null;
2768
2778
  };
2769
2779
  const TANSTACK_QUERY_PACKAGES = new Set([
2770
2780
  "@tanstack/react-query",
@@ -2779,6 +2789,16 @@ const hasTanStackQuery = (packageJson) => {
2779
2789
  };
2780
2790
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2781
2791
  };
2792
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2793
+ const isPackageJsonReanimatedAware = (packageJson) => {
2794
+ const allDependencies = {
2795
+ ...packageJson.peerDependencies,
2796
+ ...packageJson.dependencies,
2797
+ ...packageJson.devDependencies,
2798
+ ...packageJson.optionalDependencies
2799
+ };
2800
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2801
+ };
2782
2802
  const hasUpperBoundOnlyPeerRange = (range) => {
2783
2803
  if (typeof range !== "string") return false;
2784
2804
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2803,7 +2823,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2803
2823
  const REACT_DEPENDENCY_NAMES = new Set([
2804
2824
  "react",
2805
2825
  "react-native",
2806
- "next"
2826
+ "next",
2827
+ "preact"
2807
2828
  ]);
2808
2829
  const hasReactDependency = (packageJson) => {
2809
2830
  const allDependencies = {
@@ -2864,12 +2885,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2864
2885
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2865
2886
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2866
2887
  };
2888
+ const NON_PROJECT_DIRECTORIES = new Set([
2889
+ "AppData",
2890
+ "Application Data",
2891
+ "Library"
2892
+ ]);
2893
+ const MAX_SCAN_DEPTH = 6;
2867
2894
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2868
2895
  const packages = [];
2869
- const pendingDirectories = [rootDirectory];
2896
+ const pendingDirectories = [{
2897
+ directory: rootDirectory,
2898
+ depth: 0
2899
+ }];
2870
2900
  while (pendingDirectories.length > 0) {
2871
- const currentDirectory = pendingDirectories.pop();
2872
- if (!currentDirectory) continue;
2901
+ const current = pendingDirectories.pop();
2902
+ if (!current) continue;
2903
+ const { directory: currentDirectory, depth } = current;
2873
2904
  const packageJsonPath = path.join(currentDirectory, "package.json");
2874
2905
  if (isFile(packageJsonPath)) {
2875
2906
  const packageJson = readPackageJson(packageJsonPath);
@@ -2881,10 +2912,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2881
2912
  });
2882
2913
  }
2883
2914
  }
2915
+ if (depth >= MAX_SCAN_DEPTH) continue;
2884
2916
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2885
2917
  for (const entry of entries) {
2886
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2887
- pendingDirectories.push(path.join(currentDirectory, entry.name));
2918
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
2919
+ pendingDirectories.push({
2920
+ directory: path.join(currentDirectory, entry.name),
2921
+ depth: depth + 1
2922
+ });
2888
2923
  }
2889
2924
  }
2890
2925
  return packages;
@@ -2955,6 +2990,8 @@ const discoverProject = (directory) => {
2955
2990
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2956
2991
  const sourceFileCount = countSourceFiles(directory);
2957
2992
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2993
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2994
+ const preactVersion = getPreactVersion(packageJson);
2958
2995
  const projectInfo = {
2959
2996
  rootDirectory: directory,
2960
2997
  projectName,
@@ -2965,12 +3002,50 @@ const discoverProject = (directory) => {
2965
3002
  hasTypeScript,
2966
3003
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2967
3004
  hasTanStackQuery: hasTanStackQuery(packageJson),
3005
+ preactVersion,
3006
+ preactMajorVersion: parseReactMajor(preactVersion),
2968
3007
  hasReactNativeWorkspace,
3008
+ hasReanimated,
2969
3009
  sourceFileCount
2970
3010
  };
2971
3011
  cachedProjectInfos.set(directory, projectInfo);
2972
3012
  return projectInfo;
2973
3013
  };
3014
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
3015
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
3016
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
3017
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
3018
+ const parseReactMajorMinor = (reactVersion) => {
3019
+ if (typeof reactVersion !== "string") return null;
3020
+ const trimmed = reactVersion.trim();
3021
+ if (trimmed.length === 0) return null;
3022
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
3023
+ if (lowerBoundsOnly.length === 0) return null;
3024
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
3025
+ if (majorMinorMatch) {
3026
+ const major = Number.parseInt(majorMinorMatch[1], 10);
3027
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
3028
+ if (!Number.isFinite(major) || major <= 0) return null;
3029
+ if (!Number.isFinite(minor) || minor < 0) return null;
3030
+ return {
3031
+ major,
3032
+ minor
3033
+ };
3034
+ }
3035
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
3036
+ if (!majorOnlyMatch) return null;
3037
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
3038
+ if (!Number.isFinite(major) || major <= 0) return null;
3039
+ return {
3040
+ major,
3041
+ minor: 0
3042
+ };
3043
+ };
3044
+ const isReactAtLeast = (detected, required) => {
3045
+ if (detected === null) return true;
3046
+ if (detected.major !== required.major) return detected.major > required.major;
3047
+ return detected.minor >= required.minor;
3048
+ };
2974
3049
  const parseTailwindMajorMinor = (tailwindVersion) => {
2975
3050
  if (typeof tailwindVersion !== "string") return null;
2976
3051
  const trimmed = tailwindVersion.trim();
@@ -3001,6 +3076,7 @@ const isTailwindAtLeast = (detected, required) => {
3001
3076
  return detected.minor >= required.minor;
3002
3077
  };
3003
3078
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3079
+ const MILLISECONDS_PER_SECOND = 1e3;
3004
3080
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3005
3081
  const FETCH_TIMEOUT_MS = 1e4;
3006
3082
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
@@ -3901,17 +3977,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3901
3977
  headers
3902
3978
  }).pipe(Layer.provide(FetchHttpClient.layer));
3903
3979
  }).pipe(Effect.orDie));
3904
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3905
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3906
- Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3980
+ /**
3981
+ * Per-batch oxlint wall-clock budget. Reads from the env var on
3982
+ * startup so the eval harness can raise the budget under sandbox
3983
+ * microVMs without recompiling react-doctor. Tests override via
3984
+ * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
3985
+ */
3986
+ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3907
3987
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3908
3988
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3909
3989
  const parsed = Number(raw);
3910
3990
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3911
3991
  return parsed;
3912
- } });
3913
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3914
- Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
3992
+ } }) {};
3993
+ /**
3994
+ * Hard cap on combined stdout+stderr bytes per oxlint batch. The
3995
+ * subprocess gets SIGKILL'd if it produces more; the recovery path
3996
+ * suggests narrowing the scan with --diff. Override via Layer in
3997
+ * tests that exercise the cap behavior.
3998
+ */
3999
+ var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
3915
4000
  const DIAGNOSTIC_SURFACES = [
3916
4001
  "cli",
3917
4002
  "prComment",
@@ -4522,6 +4607,59 @@ const collectIgnorePatterns = (rootDirectory) => {
4522
4607
  return patterns;
4523
4608
  };
4524
4609
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4610
+ const DEAD_CODE_WORKER_SCRIPT = `
4611
+ const inputChunks = [];
4612
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4613
+ process.stdin.on("end", () => {
4614
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4615
+
4616
+ const normalizeResult = (result) => ({
4617
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4618
+ path: unusedFile.path,
4619
+ })),
4620
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4621
+ path: unusedExport.path,
4622
+ name: unusedExport.name,
4623
+ line: unusedExport.line,
4624
+ column: unusedExport.column,
4625
+ isTypeOnly: unusedExport.isTypeOnly,
4626
+ })),
4627
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4628
+ name: unusedDependency.name,
4629
+ isDevDependency: unusedDependency.isDevDependency,
4630
+ })),
4631
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4632
+ files: cycle.files,
4633
+ })),
4634
+ });
4635
+
4636
+ const serializeError = (error) =>
4637
+ error instanceof Error
4638
+ ? { name: error.name, message: error.message, stack: error.stack }
4639
+ : { message: String(error) };
4640
+
4641
+ const emit = (message) => {
4642
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4643
+ };
4644
+
4645
+ (async () => {
4646
+ try {
4647
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4648
+ const config = {
4649
+ rootDir: workerInput.rootDirectory,
4650
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4651
+ ...(workerInput.ignorePatterns.length > 0
4652
+ ? { ignorePatterns: workerInput.ignorePatterns }
4653
+ : {}),
4654
+ };
4655
+ const result = await analyze(defineConfig(config));
4656
+ emit({ ok: true, result: normalizeResult(result) });
4657
+ } catch (error) {
4658
+ emit({ ok: false, error: serializeError(error) });
4659
+ }
4660
+ })();
4661
+ });
4662
+ `;
4525
4663
  const resolveTsConfigPath = (rootDirectory) => {
4526
4664
  for (const filename of TSCONFIG_FILENAMES$1) {
4527
4665
  const candidate = path.join(rootDirectory, filename);
@@ -4542,16 +4680,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4542
4680
  const relative = toRelativePath(filePath, rootDirectory);
4543
4681
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4544
4682
  };
4683
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4684
+ const parseArray = (value, label) => {
4685
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4686
+ return value;
4687
+ };
4688
+ const parseString = (value, label) => {
4689
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4690
+ return value;
4691
+ };
4692
+ const parseNumber = (value, label) => {
4693
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4694
+ return value;
4695
+ };
4696
+ const parseBoolean = (value, label) => {
4697
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4698
+ return value;
4699
+ };
4700
+ const parseStringArray = (value, label) => {
4701
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4702
+ };
4703
+ const parseUnusedFiles = (value) => {
4704
+ const values = parseArray(value, "unusedFiles");
4705
+ const unusedFiles = [];
4706
+ for (const [index, entry] of values.entries()) {
4707
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4708
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4709
+ }
4710
+ return unusedFiles;
4711
+ };
4712
+ const parseUnusedExports = (value) => {
4713
+ const values = parseArray(value, "unusedExports");
4714
+ const unusedExports = [];
4715
+ for (const [index, entry] of values.entries()) {
4716
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4717
+ unusedExports.push({
4718
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4719
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4720
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4721
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4722
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4723
+ });
4724
+ }
4725
+ return unusedExports;
4726
+ };
4727
+ const parseUnusedDependencies = (value) => {
4728
+ const values = parseArray(value, "unusedDependencies");
4729
+ const unusedDependencies = [];
4730
+ for (const [index, entry] of values.entries()) {
4731
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4732
+ unusedDependencies.push({
4733
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4734
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4735
+ });
4736
+ }
4737
+ return unusedDependencies;
4738
+ };
4739
+ const parseCircularDependencies = (value) => {
4740
+ const values = parseArray(value, "circularDependencies");
4741
+ const circularDependencies = [];
4742
+ for (const [index, entry] of values.entries()) {
4743
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4744
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4745
+ }
4746
+ return circularDependencies;
4747
+ };
4748
+ const parseDeadCodeWorkerResult = (value) => {
4749
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4750
+ return {
4751
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4752
+ unusedExports: parseUnusedExports(value.unusedExports),
4753
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4754
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4755
+ };
4756
+ };
4757
+ const parseDeadCodeWorkerError = (value) => {
4758
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4759
+ return {
4760
+ ...typeof value.name === "string" ? { name: value.name } : {},
4761
+ message: value.message,
4762
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4763
+ };
4764
+ };
4765
+ const parseDeadCodeWorkerMessage = (value) => {
4766
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4767
+ if (value.ok === true) return {
4768
+ ok: true,
4769
+ result: value.result
4770
+ };
4771
+ if (value.ok === false) return {
4772
+ ok: false,
4773
+ error: parseDeadCodeWorkerError(value.error)
4774
+ };
4775
+ throw new Error("Dead-code worker returned an invalid status.");
4776
+ };
4777
+ const buildDeadCodeWorkerError = (workerError) => {
4778
+ const error = new Error(workerError.message);
4779
+ if (workerError.name !== void 0) error.name = workerError.name;
4780
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4781
+ return error;
4782
+ };
4783
+ const createDeadCodeWorker = (input) => {
4784
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4785
+ stdio: [
4786
+ "pipe",
4787
+ "pipe",
4788
+ "pipe"
4789
+ ],
4790
+ windowsHide: true
4791
+ });
4792
+ const stdoutChunks = [];
4793
+ const stderrChunks = [];
4794
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4795
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4796
+ let didSettle = false;
4797
+ const result = new Promise((resolve, reject) => {
4798
+ const settle = (callback) => {
4799
+ if (didSettle) return;
4800
+ didSettle = true;
4801
+ callback();
4802
+ };
4803
+ child.once("error", (error) => {
4804
+ settle(() => reject(error));
4805
+ });
4806
+ child.once("close", (exitCode) => {
4807
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4808
+ if (stdout.length === 0) {
4809
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4810
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4811
+ return;
4812
+ }
4813
+ try {
4814
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4815
+ if (parsedMessage.ok) {
4816
+ settle(() => resolve(parsedMessage.result));
4817
+ return;
4818
+ }
4819
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4820
+ } catch (error) {
4821
+ settle(() => reject(error));
4822
+ }
4823
+ });
4824
+ });
4825
+ child.stdin.on("error", () => {});
4826
+ child.stdin.end(JSON.stringify(input));
4827
+ return {
4828
+ result,
4829
+ terminate: () => {
4830
+ didSettle = true;
4831
+ child.kill("SIGKILL");
4832
+ }
4833
+ };
4834
+ };
4835
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4836
+ let didSettle = false;
4837
+ const timeoutHandle = setTimeout(() => {
4838
+ if (didSettle) return;
4839
+ didSettle = true;
4840
+ handle.terminate?.();
4841
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4842
+ }, timeoutMs);
4843
+ timeoutHandle.unref?.();
4844
+ handle.result.then((value) => {
4845
+ if (didSettle) return;
4846
+ didSettle = true;
4847
+ clearTimeout(timeoutHandle);
4848
+ handle.terminate?.();
4849
+ resolve(value);
4850
+ }, (error) => {
4851
+ if (didSettle) return;
4852
+ didSettle = true;
4853
+ clearTimeout(timeoutHandle);
4854
+ handle.terminate?.();
4855
+ reject(error);
4856
+ });
4857
+ });
4545
4858
  const checkDeadCode = async (options) => {
4546
4859
  const { rootDirectory, userConfig } = options;
4547
4860
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4548
- const { analyze, defineConfig } = await import("deslop-js");
4549
4861
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4550
- const result = await analyze(defineConfig({
4551
- rootDir: rootDirectory,
4862
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4863
+ rootDirectory,
4552
4864
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4553
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4554
- }));
4865
+ ignorePatterns,
4866
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4867
+ }), options.workerTimeoutMs ?? 12e4));
4555
4868
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4556
4869
  const diagnostics = [];
4557
4870
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -4757,8 +5070,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4757
5070
  env: input.env,
4758
5071
  extendEnv: true
4759
5072
  }));
5073
+ const maxStdoutBytes = input.maxStdoutBytes;
5074
+ const stdoutByteCount = yield* Ref.make(0);
5075
+ 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({
5076
+ args: [...input.args],
5077
+ directory: input.directory,
5078
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
5079
+ }) })) : Effect.void))));
4760
5080
  const [stdout, stderr, status] = yield* Effect.all([
4761
- Stream.mkString(Stream.decodeText(handle.stdout)),
5081
+ Stream.mkString(Stream.decodeText(stdoutStream)),
4762
5082
  Stream.mkString(Stream.decodeText(handle.stderr)),
4763
5083
  handle.exitCode
4764
5084
  ], { concurrency: 3 });
@@ -4920,7 +5240,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4920
5240
  if (result.status !== 0) return [];
4921
5241
  return splitNullSeparated(result.stdout);
4922
5242
  })),
4923
- showStagedContent: (directory, relativePath) => runGit(directory, ["show", `:${relativePath}`]).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5243
+ showStagedContent: (directory, relativePath, options) => runCommand({
5244
+ command: "git",
5245
+ args: ["show", `:${relativePath}`],
5246
+ directory,
5247
+ maxStdoutBytes: options?.maxBufferBytes
5248
+ }).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
4924
5249
  grep: (input) => Effect.gen(function* () {
4925
5250
  const args = ["grep"];
4926
5251
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -4928,7 +5253,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4928
5253
  if (input.extendedRegexp ?? false) args.push("-E");
4929
5254
  args.push(input.pattern);
4930
5255
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
4931
- const result = yield* runGit(input.directory, args);
5256
+ const result = yield* runCommand({
5257
+ command: "git",
5258
+ args,
5259
+ directory: input.directory,
5260
+ maxStdoutBytes: input.maxBufferBytes
5261
+ });
4932
5262
  if (result.status === 128) return null;
4933
5263
  return {
4934
5264
  status: result.status,
@@ -5175,7 +5505,16 @@ const buildCapabilities = (project) => {
5175
5505
  capabilities.add(project.framework);
5176
5506
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5177
5507
  const reactMajor = project.reactMajorVersion;
5178
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5508
+ if (reactMajor !== null) {
5509
+ const cappedReactMajor = Math.min(reactMajor, 30);
5510
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5511
+ if (reactMajor >= 19) {
5512
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5513
+ major: 19,
5514
+ minor: 2
5515
+ })) capabilities.add("react:19.2");
5516
+ }
5517
+ }
5179
5518
  if (project.tailwindVersion !== null) {
5180
5519
  capabilities.add("tailwind");
5181
5520
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5186,6 +5525,15 @@ const buildCapabilities = (project) => {
5186
5525
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5187
5526
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5188
5527
  if (project.hasTypeScript) capabilities.add("typescript");
5528
+ if (project.preactVersion !== null) {
5529
+ capabilities.add("preact");
5530
+ const preactMajor = project.preactMajorVersion;
5531
+ if (preactMajor !== null) {
5532
+ const cappedPreactMajor = Math.min(preactMajor, 20);
5533
+ for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
5534
+ }
5535
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5536
+ }
5189
5537
  return capabilities;
5190
5538
  };
5191
5539
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -5440,6 +5788,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5440
5788
  if (!publicEnvPrefix) return fallbackRecommendation;
5441
5789
  return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
5442
5790
  };
5791
+ 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";
5792
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5793
+ if (rule !== "immutability") return help;
5794
+ if (!project.hasReanimated) return help;
5795
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5796
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5797
+ };
5443
5798
  const REACT_MODULE_SOURCE = "react";
5444
5799
  const REQUIRE_IDENTIFIER = "require";
5445
5800
  const USE_IDENTIFIER = "use";
@@ -5763,7 +6118,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5763
6118
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5764
6119
  if (plugin === "react-hooks-js") return {
5765
6120
  message: REACT_COMPILER_MESSAGE,
5766
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6121
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
5767
6122
  };
5768
6123
  return {
5769
6124
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -5832,13 +6187,6 @@ const SANITIZED_ENV = (() => {
5832
6187
  }
5833
6188
  return sanitized;
5834
6189
  })();
5835
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5836
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
5837
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
5838
- const parsed = Number(raw);
5839
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
5840
- return parsed;
5841
- })();
5842
6190
  /**
5843
6191
  * Spawn one oxlint subprocess with hard ceilings on wall time and
5844
6192
  * output size. Returns stdout on success; raises a tagged
@@ -5855,7 +6203,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
5855
6203
  * The first three are splittable (the caller's binary-split retry
5856
6204
  * shrinks the batch and re-spawns); the fourth isn't.
5857
6205
  */
5858
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
6206
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
5859
6207
  const child = spawn(nodeBinaryPath, args, {
5860
6208
  cwd: rootDirectory,
5861
6209
  env: SANITIZED_ENV
@@ -5864,9 +6212,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5864
6212
  child.kill("SIGKILL");
5865
6213
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5866
6214
  kind: "timeout",
5867
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6215
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
5868
6216
  }) }));
5869
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6217
+ }, spawnTimeoutMs);
5870
6218
  timeoutHandle.unref?.();
5871
6219
  const stdoutBuffers = [];
5872
6220
  const stderrBuffers = [];
@@ -5876,7 +6224,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5876
6224
  const killIfTooLarge = (incomingBytes, isStdout) => {
5877
6225
  if (isStdout) stdoutByteCount += incomingBytes;
5878
6226
  else stderrByteCount += incomingBytes;
5879
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6227
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
5880
6228
  didKillForSize = true;
5881
6229
  child.kill("SIGKILL");
5882
6230
  return true;
@@ -5902,7 +6250,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5902
6250
  if (didKillForSize) {
5903
6251
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
5904
6252
  kind: "output-too-large",
5905
- detail: `exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`
6253
+ detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
5906
6254
  }) }));
5907
6255
  return;
5908
6256
  }
@@ -5943,7 +6291,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
5943
6291
  * with a slimmer config in that case.
5944
6292
  */
5945
6293
  const spawnLintBatches = async (input) => {
5946
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6294
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
5947
6295
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
5948
6296
  const allDiagnostics = [];
5949
6297
  const droppedFiles = [];
@@ -5951,7 +6299,7 @@ const spawnLintBatches = async (input) => {
5951
6299
  const spawnLintBatch = async (batch) => {
5952
6300
  const batchArgs = [...baseArgs, ...batch];
5953
6301
  try {
5954
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6302
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
5955
6303
  } catch (error) {
5956
6304
  if (!isSplittableReactDoctorError(error)) throw error;
5957
6305
  if (batch.length <= 1) {
@@ -6054,13 +6402,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6054
6402
  * 6. always restore disable directives + clean up the temp dir
6055
6403
  */
6056
6404
  const runOxlint = async (options) => {
6057
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
6405
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
6058
6406
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6059
6407
  const severityControls = buildRuleSeverityControls(userConfig);
6060
6408
  validateRuleRegistration();
6061
6409
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6062
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6063
- const configPath = path.join(configDirectory, "oxlintrc.json");
6064
6410
  const pluginPath = resolvePluginPath();
6065
6411
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6066
6412
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6075,6 +6421,8 @@ const runOxlint = async (options) => {
6075
6421
  userPlugins
6076
6422
  });
6077
6423
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
6424
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6425
+ const configPath = path.join(configDirectory, "oxlintrc.json");
6078
6426
  try {
6079
6427
  const baseArgs = [
6080
6428
  resolveOxlintBinary(),
@@ -6101,7 +6449,9 @@ const runOxlint = async (options) => {
6101
6449
  nodeBinaryPath,
6102
6450
  project,
6103
6451
  onPartialFailure,
6104
- onFileProgress: options.onFileProgress
6452
+ onFileProgress: options.onFileProgress,
6453
+ spawnTimeoutMs,
6454
+ outputMaxBytes
6105
6455
  });
6106
6456
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6107
6457
  try {
@@ -6167,6 +6517,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6167
6517
  */
6168
6518
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6169
6519
  const partialFailures = yield* LintPartialFailures;
6520
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6521
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6170
6522
  const collectedFailures = [];
6171
6523
  const diagnostics = yield* Effect.tryPromise({
6172
6524
  try: () => runOxlint({
@@ -6183,7 +6535,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6183
6535
  onPartialFailure: (reason) => {
6184
6536
  collectedFailures.push(reason);
6185
6537
  },
6186
- onFileProgress: input.onFileProgress
6538
+ onFileProgress: input.onFileProgress,
6539
+ spawnTimeoutMs,
6540
+ outputMaxBytes
6187
6541
  }),
6188
6542
  catch: ensureReactDoctorError
6189
6543
  });
@@ -6481,7 +6835,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6481
6835
  const resolvedConfig = yield* configService.resolve(input.directory);
6482
6836
  const scanDirectory = resolvedConfig.resolvedDirectory;
6483
6837
  const project = yield* projectService.discover(scanDirectory);
6484
- if (project.reactVersion === null) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6838
+ if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6485
6839
  const [repo, sha, defaultBranch] = yield* Effect.all([
6486
6840
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6487
6841
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6509,23 +6863,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6509
6863
  const lintFailure = yield* Ref.make({
6510
6864
  didFail: false,
6511
6865
  reason: null,
6512
- reasonTag: null
6866
+ reasonTag: null,
6867
+ reasonKind: null
6513
6868
  });
6514
6869
  const deadCodeFailure = yield* Ref.make({
6515
6870
  didFail: false,
6516
6871
  reason: null
6517
6872
  });
6518
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6519
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6520
- rootDirectory: scanDirectory,
6521
- userConfig: resolvedConfig.config
6522
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6523
- yield* Ref.set(deadCodeFailure, {
6524
- didFail: true,
6525
- reason: error.message
6526
- });
6527
- return Stream.empty;
6528
- })))))) : Effect.succeed([]));
6529
6873
  const scanProgress = yield* progressService.start("Scanning...");
6530
6874
  const scanStartTime = Date.now();
6531
6875
  let lastReportedTotalFileCount = 0;
@@ -6542,24 +6886,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6542
6886
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6543
6887
  onFileProgress: (scannedFileCount, totalFileCount) => {
6544
6888
  lastReportedTotalFileCount = totalFileCount;
6545
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
6889
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6546
6890
  }
6547
6891
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6548
6892
  yield* Ref.set(lintFailure, {
6549
6893
  didFail: true,
6550
6894
  reason: error.message,
6551
- reasonTag: error.reason._tag
6895
+ reasonTag: error.reason._tag,
6896
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6552
6897
  });
6553
6898
  return Stream.empty;
6554
6899
  }))));
6555
6900
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6556
6901
  const lintFailureState = yield* Ref.get(lintFailure);
6557
6902
  yield* afterLint(lintFailureState.didFail);
6558
- if (lintFailureState.didFail) {
6559
- yield* Fiber.interrupt(deadCodeFiber);
6560
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6561
- }
6562
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
6903
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6904
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6905
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6906
+ rootDirectory: scanDirectory,
6907
+ userConfig: resolvedConfig.config
6908
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6909
+ yield* Ref.set(deadCodeFailure, {
6910
+ didFail: true,
6911
+ reason: error.message
6912
+ });
6913
+ return Stream.empty;
6914
+ }))))))));
6563
6915
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6564
6916
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6565
6917
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
@@ -6601,6 +6953,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6601
6953
  didLintFail: lintFailureState.didFail,
6602
6954
  lintFailureReason: lintFailureState.reason,
6603
6955
  lintFailureReasonTag: lintFailureState.reasonTag,
6956
+ lintFailureReasonKind: lintFailureState.reasonKind,
6604
6957
  lintPartialFailures,
6605
6958
  didDeadCodeFail: deadCodeFailureState.didFail,
6606
6959
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7035,11 +7388,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7035
7388
  const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7036
7389
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7037
7390
  const skippedChecks = [];
7391
+ if (output.didLintFail) skippedChecks.push("lint");
7392
+ if (output.didDeadCodeFail) skippedChecks.push("dead-code");
7038
7393
  const skippedCheckReasons = {};
7039
- if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) {
7040
- skippedChecks.push("dead-code");
7041
- skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7042
- }
7394
+ if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
7395
+ else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
7396
+ if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7043
7397
  return {
7044
7398
  diagnostics: [...output.diagnostics],
7045
7399
  score: output.score,