react-doctor 0.2.10 → 0.2.11-dev.d0f5206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,6 @@ import * as Option from "effect/Option";
22
22
  import * as Ref from "effect/Ref";
23
23
  import * as Stream from "effect/Stream";
24
24
  import * as Cache from "effect/Cache";
25
- import { Worker } from "node:worker_threads";
26
25
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
27
26
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
28
27
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2698,6 +2697,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2698
2697
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2699
2698
  };
2700
2699
  };
2700
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2701
+ if (predicate(rootPackageJson)) return true;
2702
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2703
+ if (patterns.length === 0) return false;
2704
+ const visitedDirectories = /* @__PURE__ */ new Set();
2705
+ for (const pattern of patterns) {
2706
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2707
+ for (const workspaceDirectory of directories) {
2708
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2709
+ visitedDirectories.add(workspaceDirectory);
2710
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2711
+ }
2712
+ }
2713
+ return false;
2714
+ };
2701
2715
  const NAMES = new Set([
2702
2716
  "react-native",
2703
2717
  "react-native-tvos",
@@ -2728,27 +2742,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2728
2742
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2729
2743
  return false;
2730
2744
  };
2731
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2732
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2733
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2734
- if (patterns.length === 0) return false;
2735
- const visitedDirectories = /* @__PURE__ */ new Set();
2736
- for (const pattern of patterns) {
2737
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2738
- for (const workspaceDirectory of directories) {
2739
- if (visitedDirectories.has(workspaceDirectory)) continue;
2740
- visitedDirectories.add(workspaceDirectory);
2741
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2742
- }
2743
- }
2744
- return false;
2745
- };
2746
- const hasPreact = (packageJson) => {
2747
- return "preact" in {
2745
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2746
+ const getPreactVersion = (packageJson) => {
2747
+ return {
2748
2748
  ...packageJson.peerDependencies,
2749
2749
  ...packageJson.dependencies,
2750
2750
  ...packageJson.devDependencies
2751
- };
2751
+ }.preact ?? null;
2752
2752
  };
2753
2753
  const TANSTACK_QUERY_PACKAGES = new Set([
2754
2754
  "@tanstack/react-query",
@@ -2763,6 +2763,16 @@ const hasTanStackQuery = (packageJson) => {
2763
2763
  };
2764
2764
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2765
2765
  };
2766
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2767
+ const isPackageJsonReanimatedAware = (packageJson) => {
2768
+ const allDependencies = {
2769
+ ...packageJson.peerDependencies,
2770
+ ...packageJson.dependencies,
2771
+ ...packageJson.devDependencies,
2772
+ ...packageJson.optionalDependencies
2773
+ };
2774
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2775
+ };
2766
2776
  const hasUpperBoundOnlyPeerRange = (range) => {
2767
2777
  if (typeof range !== "string") return false;
2768
2778
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2849,12 +2859,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2849
2859
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2850
2860
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2851
2861
  };
2862
+ const NON_PROJECT_DIRECTORIES = new Set([
2863
+ "AppData",
2864
+ "Application Data",
2865
+ "Library"
2866
+ ]);
2867
+ const MAX_SCAN_DEPTH = 6;
2852
2868
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2853
2869
  const packages = [];
2854
- const pendingDirectories = [rootDirectory];
2870
+ const pendingDirectories = [{
2871
+ directory: rootDirectory,
2872
+ depth: 0
2873
+ }];
2855
2874
  while (pendingDirectories.length > 0) {
2856
- const currentDirectory = pendingDirectories.pop();
2857
- if (!currentDirectory) continue;
2875
+ const current = pendingDirectories.pop();
2876
+ if (!current) continue;
2877
+ const { directory: currentDirectory, depth } = current;
2858
2878
  const packageJsonPath = path.join(currentDirectory, "package.json");
2859
2879
  if (isFile(packageJsonPath)) {
2860
2880
  const packageJson = readPackageJson(packageJsonPath);
@@ -2866,10 +2886,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2866
2886
  });
2867
2887
  }
2868
2888
  }
2889
+ if (depth >= MAX_SCAN_DEPTH) continue;
2869
2890
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2870
2891
  for (const entry of entries) {
2871
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2872
- pendingDirectories.push(path.join(currentDirectory, entry.name));
2892
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
2893
+ pendingDirectories.push({
2894
+ directory: path.join(currentDirectory, entry.name),
2895
+ depth: depth + 1
2896
+ });
2873
2897
  }
2874
2898
  }
2875
2899
  return packages;
@@ -2937,6 +2961,8 @@ const discoverProject = (directory) => {
2937
2961
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2938
2962
  const sourceFileCount = countSourceFiles(directory);
2939
2963
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2964
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2965
+ const preactVersion = getPreactVersion(packageJson);
2940
2966
  const projectInfo = {
2941
2967
  rootDirectory: directory,
2942
2968
  projectName,
@@ -2947,13 +2973,16 @@ const discoverProject = (directory) => {
2947
2973
  hasTypeScript,
2948
2974
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2949
2975
  hasTanStackQuery: hasTanStackQuery(packageJson),
2950
- hasPreact: hasPreact(packageJson),
2976
+ preactVersion,
2977
+ preactMajorVersion: parseReactMajor(preactVersion),
2951
2978
  hasReactNativeWorkspace,
2979
+ hasReanimated,
2952
2980
  sourceFileCount
2953
2981
  };
2954
2982
  cachedProjectInfos.set(directory, projectInfo);
2955
2983
  return projectInfo;
2956
2984
  };
2985
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2957
2986
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2958
2987
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2959
2988
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3923,17 +3952,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3923
3952
  headers
3924
3953
  }).pipe(Layer.provide(FetchHttpClient.layer));
3925
3954
  }).pipe(Effect.orDie));
3926
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3927
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3928
- Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3955
+ /**
3956
+ * Per-batch oxlint wall-clock budget. Reads from the env var on
3957
+ * startup so the eval harness can raise the budget under sandbox
3958
+ * microVMs without recompiling react-doctor. Tests override via
3959
+ * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
3960
+ */
3961
+ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3929
3962
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3930
3963
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3931
3964
  const parsed = Number(raw);
3932
3965
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3933
3966
  return parsed;
3934
- } });
3935
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3936
- Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
3967
+ } }) {};
3968
+ /**
3969
+ * Hard cap on combined stdout+stderr bytes per oxlint batch. The
3970
+ * subprocess gets SIGKILL'd if it produces more; the recovery path
3971
+ * suggests narrowing the scan with --diff. Override via Layer in
3972
+ * tests that exercise the cap behavior.
3973
+ */
3974
+ var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
3937
3975
  const DIAGNOSTIC_SURFACES = [
3938
3976
  "cli",
3939
3977
  "prComment",
@@ -4539,47 +4577,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4539
4577
  };
4540
4578
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4541
4579
  const DEAD_CODE_WORKER_SCRIPT = `
4542
- const { parentPort, workerData } = require("node:worker_threads");
4580
+ const inputChunks = [];
4581
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4582
+ process.stdin.on("end", () => {
4583
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4543
4584
 
4544
- const normalizeResult = (result) => ({
4545
- unusedFiles: result.unusedFiles.map((unusedFile) => ({
4546
- path: unusedFile.path,
4547
- })),
4548
- unusedExports: result.unusedExports.map((unusedExport) => ({
4549
- path: unusedExport.path,
4550
- name: unusedExport.name,
4551
- line: unusedExport.line,
4552
- column: unusedExport.column,
4553
- isTypeOnly: unusedExport.isTypeOnly,
4554
- })),
4555
- unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4556
- name: unusedDependency.name,
4557
- isDevDependency: unusedDependency.isDevDependency,
4558
- })),
4559
- circularDependencies: result.circularDependencies.map((cycle) => ({
4560
- files: cycle.files,
4561
- })),
4562
- });
4585
+ const normalizeResult = (result) => ({
4586
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4587
+ path: unusedFile.path,
4588
+ })),
4589
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4590
+ path: unusedExport.path,
4591
+ name: unusedExport.name,
4592
+ line: unusedExport.line,
4593
+ column: unusedExport.column,
4594
+ isTypeOnly: unusedExport.isTypeOnly,
4595
+ })),
4596
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4597
+ name: unusedDependency.name,
4598
+ isDevDependency: unusedDependency.isDevDependency,
4599
+ })),
4600
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4601
+ files: cycle.files,
4602
+ })),
4603
+ });
4563
4604
 
4564
- const serializeError = (error) =>
4565
- error instanceof Error
4566
- ? { name: error.name, message: error.message, stack: error.stack }
4567
- : { message: String(error) };
4605
+ const serializeError = (error) =>
4606
+ error instanceof Error
4607
+ ? { name: error.name, message: error.message, stack: error.stack }
4608
+ : { message: String(error) };
4568
4609
 
4569
- (async () => {
4570
- try {
4571
- const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4572
- const config = {
4573
- rootDir: workerData.rootDirectory,
4574
- ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4575
- ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4576
- };
4577
- const result = await analyze(defineConfig(config));
4578
- parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4579
- } catch (error) {
4580
- parentPort.postMessage({ ok: false, error: serializeError(error) });
4581
- }
4582
- })();
4610
+ const emit = (message) => {
4611
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4612
+ };
4613
+
4614
+ (async () => {
4615
+ try {
4616
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4617
+ const config = {
4618
+ rootDir: workerInput.rootDirectory,
4619
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4620
+ ...(workerInput.ignorePatterns.length > 0
4621
+ ? { ignorePatterns: workerInput.ignorePatterns }
4622
+ : {}),
4623
+ };
4624
+ const result = await analyze(defineConfig(config));
4625
+ emit({ ok: true, result: normalizeResult(result) });
4626
+ } catch (error) {
4627
+ emit({ ok: false, error: serializeError(error) });
4628
+ }
4629
+ })();
4630
+ });
4583
4631
  `;
4584
4632
  const resolveTsConfigPath = (rootDirectory) => {
4585
4633
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4702,43 +4750,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4702
4750
  return error;
4703
4751
  };
4704
4752
  const createDeadCodeWorker = (input) => {
4705
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4706
- eval: true,
4707
- workerData: input
4753
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4754
+ stdio: [
4755
+ "pipe",
4756
+ "pipe",
4757
+ "pipe"
4758
+ ],
4759
+ windowsHide: true
4708
4760
  });
4761
+ const stdoutChunks = [];
4762
+ const stderrChunks = [];
4763
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4764
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4709
4765
  let didSettle = false;
4710
- return {
4711
- result: new Promise((resolve, reject) => {
4712
- const settle = (callback) => {
4713
- if (didSettle) return;
4714
- didSettle = true;
4715
- worker.removeAllListeners();
4716
- callback();
4717
- };
4718
- worker.once("message", (message) => {
4719
- try {
4720
- const parsedMessage = parseDeadCodeWorkerMessage(message);
4721
- if (parsedMessage.ok) {
4722
- settle(() => resolve(parsedMessage.result));
4723
- return;
4724
- }
4725
- settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4726
- } catch (error) {
4727
- settle(() => reject(error));
4766
+ const result = new Promise((resolve, reject) => {
4767
+ const settle = (callback) => {
4768
+ if (didSettle) return;
4769
+ didSettle = true;
4770
+ callback();
4771
+ };
4772
+ child.once("error", (error) => {
4773
+ settle(() => reject(error));
4774
+ });
4775
+ child.once("close", (exitCode) => {
4776
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4777
+ if (stdout.length === 0) {
4778
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4779
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4780
+ return;
4781
+ }
4782
+ try {
4783
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4784
+ if (parsedMessage.ok) {
4785
+ settle(() => resolve(parsedMessage.result));
4786
+ return;
4728
4787
  }
4729
- });
4730
- worker.once("error", (error) => {
4788
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4789
+ } catch (error) {
4731
4790
  settle(() => reject(error));
4732
- });
4733
- worker.once("exit", (exitCode) => {
4734
- if (exitCode === 0) return;
4735
- settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4736
- });
4737
- }),
4791
+ }
4792
+ });
4793
+ });
4794
+ child.stdin.on("error", () => {});
4795
+ child.stdin.end(JSON.stringify(input));
4796
+ return {
4797
+ result,
4738
4798
  terminate: () => {
4739
4799
  didSettle = true;
4740
- worker.removeAllListeners();
4741
- return worker.terminate();
4800
+ child.kill("SIGKILL");
4742
4801
  }
4743
4802
  };
4744
4803
  };
@@ -4980,8 +5039,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4980
5039
  env: input.env,
4981
5040
  extendEnv: true
4982
5041
  }));
5042
+ const maxStdoutBytes = input.maxStdoutBytes;
5043
+ const stdoutByteCount = yield* Ref.make(0);
5044
+ 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({
5045
+ args: [...input.args],
5046
+ directory: input.directory,
5047
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
5048
+ }) })) : Effect.void))));
4983
5049
  const [stdout, stderr, status] = yield* Effect.all([
4984
- Stream.mkString(Stream.decodeText(handle.stdout)),
5050
+ Stream.mkString(Stream.decodeText(stdoutStream)),
4985
5051
  Stream.mkString(Stream.decodeText(handle.stderr)),
4986
5052
  handle.exitCode
4987
5053
  ], { concurrency: 3 });
@@ -5143,7 +5209,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5143
5209
  if (result.status !== 0) return [];
5144
5210
  return splitNullSeparated(result.stdout);
5145
5211
  })),
5146
- showStagedContent: (directory, relativePath) => runGit(directory, ["show", `:${relativePath}`]).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5212
+ showStagedContent: (directory, relativePath, options) => runCommand({
5213
+ command: "git",
5214
+ args: ["show", `:${relativePath}`],
5215
+ directory,
5216
+ maxStdoutBytes: options?.maxBufferBytes
5217
+ }).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
5147
5218
  grep: (input) => Effect.gen(function* () {
5148
5219
  const args = ["grep"];
5149
5220
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5151,7 +5222,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5151
5222
  if (input.extendedRegexp ?? false) args.push("-E");
5152
5223
  args.push(input.pattern);
5153
5224
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5154
- const result = yield* runGit(input.directory, args);
5225
+ const result = yield* runCommand({
5226
+ command: "git",
5227
+ args,
5228
+ directory: input.directory,
5229
+ maxStdoutBytes: input.maxBufferBytes
5230
+ });
5155
5231
  if (result.status === 128) return null;
5156
5232
  return {
5157
5233
  status: result.status,
@@ -5399,7 +5475,8 @@ const buildCapabilities = (project) => {
5399
5475
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5400
5476
  const reactMajor = project.reactMajorVersion;
5401
5477
  if (reactMajor !== null) {
5402
- for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5478
+ const cappedReactMajor = Math.min(reactMajor, 30);
5479
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5403
5480
  if (reactMajor >= 19) {
5404
5481
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5405
5482
  major: 19,
@@ -5417,8 +5494,13 @@ const buildCapabilities = (project) => {
5417
5494
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5418
5495
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5419
5496
  if (project.hasTypeScript) capabilities.add("typescript");
5420
- if (project.hasPreact) {
5497
+ if (project.preactVersion !== null) {
5421
5498
  capabilities.add("preact");
5499
+ const preactMajor = project.preactMajorVersion;
5500
+ if (preactMajor !== null) {
5501
+ const cappedPreactMajor = Math.min(preactMajor, 20);
5502
+ for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
5503
+ }
5422
5504
  if (project.reactVersion === null) capabilities.add("pure-preact");
5423
5505
  }
5424
5506
  return capabilities;
@@ -5675,6 +5757,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5675
5757
  if (!publicEnvPrefix) return fallbackRecommendation;
5676
5758
  return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
5677
5759
  };
5760
+ 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";
5761
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5762
+ if (rule !== "immutability") return help;
5763
+ if (!project.hasReanimated) return help;
5764
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5765
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5766
+ };
5678
5767
  const REACT_MODULE_SOURCE = "react";
5679
5768
  const REQUIRE_IDENTIFIER = "require";
5680
5769
  const USE_IDENTIFIER = "use";
@@ -5998,7 +6087,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5998
6087
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5999
6088
  if (plugin === "react-hooks-js") return {
6000
6089
  message: REACT_COMPILER_MESSAGE,
6001
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6090
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6002
6091
  };
6003
6092
  return {
6004
6093
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -6067,13 +6156,6 @@ const SANITIZED_ENV = (() => {
6067
6156
  }
6068
6157
  return sanitized;
6069
6158
  })();
6070
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6071
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6072
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6073
- const parsed = Number(raw);
6074
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6075
- return parsed;
6076
- })();
6077
6159
  /**
6078
6160
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6079
6161
  * output size. Returns stdout on success; raises a tagged
@@ -6090,7 +6172,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6090
6172
  * The first three are splittable (the caller's binary-split retry
6091
6173
  * shrinks the batch and re-spawns); the fourth isn't.
6092
6174
  */
6093
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
6175
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
6094
6176
  const child = spawn(nodeBinaryPath, args, {
6095
6177
  cwd: rootDirectory,
6096
6178
  env: SANITIZED_ENV
@@ -6099,9 +6181,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6099
6181
  child.kill("SIGKILL");
6100
6182
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6101
6183
  kind: "timeout",
6102
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6184
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6103
6185
  }) }));
6104
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6186
+ }, spawnTimeoutMs);
6105
6187
  timeoutHandle.unref?.();
6106
6188
  const stdoutBuffers = [];
6107
6189
  const stderrBuffers = [];
@@ -6111,7 +6193,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6111
6193
  const killIfTooLarge = (incomingBytes, isStdout) => {
6112
6194
  if (isStdout) stdoutByteCount += incomingBytes;
6113
6195
  else stderrByteCount += incomingBytes;
6114
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6196
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6115
6197
  didKillForSize = true;
6116
6198
  child.kill("SIGKILL");
6117
6199
  return true;
@@ -6137,7 +6219,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6137
6219
  if (didKillForSize) {
6138
6220
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6139
6221
  kind: "output-too-large",
6140
- detail: `exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`
6222
+ detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
6141
6223
  }) }));
6142
6224
  return;
6143
6225
  }
@@ -6178,7 +6260,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6178
6260
  * with a slimmer config in that case.
6179
6261
  */
6180
6262
  const spawnLintBatches = async (input) => {
6181
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6263
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6182
6264
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6183
6265
  const allDiagnostics = [];
6184
6266
  const droppedFiles = [];
@@ -6186,7 +6268,7 @@ const spawnLintBatches = async (input) => {
6186
6268
  const spawnLintBatch = async (batch) => {
6187
6269
  const batchArgs = [...baseArgs, ...batch];
6188
6270
  try {
6189
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6271
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6190
6272
  } catch (error) {
6191
6273
  if (!isSplittableReactDoctorError(error)) throw error;
6192
6274
  if (batch.length <= 1) {
@@ -6289,13 +6371,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6289
6371
  * 6. always restore disable directives + clean up the temp dir
6290
6372
  */
6291
6373
  const runOxlint = async (options) => {
6292
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
6374
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
6293
6375
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6294
6376
  const severityControls = buildRuleSeverityControls(userConfig);
6295
6377
  validateRuleRegistration();
6296
6378
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6297
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6298
- const configPath = path.join(configDirectory, "oxlintrc.json");
6299
6379
  const pluginPath = resolvePluginPath();
6300
6380
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6301
6381
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6310,6 +6390,8 @@ const runOxlint = async (options) => {
6310
6390
  userPlugins
6311
6391
  });
6312
6392
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
6393
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6394
+ const configPath = path.join(configDirectory, "oxlintrc.json");
6313
6395
  try {
6314
6396
  const baseArgs = [
6315
6397
  resolveOxlintBinary(),
@@ -6336,7 +6418,9 @@ const runOxlint = async (options) => {
6336
6418
  nodeBinaryPath,
6337
6419
  project,
6338
6420
  onPartialFailure,
6339
- onFileProgress: options.onFileProgress
6421
+ onFileProgress: options.onFileProgress,
6422
+ spawnTimeoutMs,
6423
+ outputMaxBytes
6340
6424
  });
6341
6425
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6342
6426
  try {
@@ -6402,6 +6486,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6402
6486
  */
6403
6487
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6404
6488
  const partialFailures = yield* LintPartialFailures;
6489
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6490
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6405
6491
  const collectedFailures = [];
6406
6492
  const diagnostics = yield* Effect.tryPromise({
6407
6493
  try: () => runOxlint({
@@ -6418,7 +6504,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6418
6504
  onPartialFailure: (reason) => {
6419
6505
  collectedFailures.push(reason);
6420
6506
  },
6421
- onFileProgress: input.onFileProgress
6507
+ onFileProgress: input.onFileProgress,
6508
+ spawnTimeoutMs,
6509
+ outputMaxBytes
6422
6510
  }),
6423
6511
  catch: ensureReactDoctorError
6424
6512
  });
@@ -6716,7 +6804,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6716
6804
  const resolvedConfig = yield* configService.resolve(input.directory);
6717
6805
  const scanDirectory = resolvedConfig.resolvedDirectory;
6718
6806
  const project = yield* projectService.discover(scanDirectory);
6719
- if (project.reactVersion === null) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6807
+ if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
6720
6808
  const [repo, sha, defaultBranch] = yield* Effect.all([
6721
6809
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6722
6810
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6744,7 +6832,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6744
6832
  const lintFailure = yield* Ref.make({
6745
6833
  didFail: false,
6746
6834
  reason: null,
6747
- reasonTag: null
6835
+ reasonTag: null,
6836
+ reasonKind: null
6748
6837
  });
6749
6838
  const deadCodeFailure = yield* Ref.make({
6750
6839
  didFail: false,
@@ -6766,13 +6855,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6766
6855
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6767
6856
  onFileProgress: (scannedFileCount, totalFileCount) => {
6768
6857
  lastReportedTotalFileCount = totalFileCount;
6769
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
6858
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6770
6859
  }
6771
6860
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6772
6861
  yield* Ref.set(lintFailure, {
6773
6862
  didFail: true,
6774
6863
  reason: error.message,
6775
- reasonTag: error.reason._tag
6864
+ reasonTag: error.reason._tag,
6865
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6776
6866
  });
6777
6867
  return Stream.empty;
6778
6868
  }))));
@@ -6832,6 +6922,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6832
6922
  didLintFail: lintFailureState.didFail,
6833
6923
  lintFailureReason: lintFailureState.reason,
6834
6924
  lintFailureReasonTag: lintFailureState.reasonTag,
6925
+ lintFailureReasonKind: lintFailureState.reasonKind,
6835
6926
  lintPartialFailures,
6836
6927
  didDeadCodeFail: deadCodeFailureState.didFail,
6837
6928
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7318,4 +7409,4 @@ const cliLogger = {
7318
7409
  //#endregion
7319
7410
  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 };
7320
7411
 
7321
- //# sourceMappingURL=cli-logger-BRBUS1pE.js.map
7412
+ //# sourceMappingURL=cli-logger-Df45H6Lw.js.map