react-doctor 0.2.10 → 0.2.11-dev.d917f62

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
@@ -21,7 +21,6 @@ import * as Option from "effect/Option";
21
21
  import * as Ref from "effect/Ref";
22
22
  import * as Stream from "effect/Stream";
23
23
  import * as Cache from "effect/Cache";
24
- import { Worker } from "node:worker_threads";
25
24
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
26
25
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
27
26
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2724,6 +2723,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2724
2723
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2725
2724
  };
2726
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
+ };
2727
2741
  const NAMES = new Set([
2728
2742
  "react-native",
2729
2743
  "react-native-tvos",
@@ -2754,27 +2768,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2754
2768
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2755
2769
  return false;
2756
2770
  };
2757
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2758
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2759
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2760
- if (patterns.length === 0) return false;
2761
- const visitedDirectories = /* @__PURE__ */ new Set();
2762
- for (const pattern of patterns) {
2763
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2764
- for (const workspaceDirectory of directories) {
2765
- if (visitedDirectories.has(workspaceDirectory)) continue;
2766
- visitedDirectories.add(workspaceDirectory);
2767
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2768
- }
2769
- }
2770
- return false;
2771
- };
2772
- const hasPreact = (packageJson) => {
2773
- return "preact" in {
2771
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2772
+ const getPreactVersion = (packageJson) => {
2773
+ return {
2774
2774
  ...packageJson.peerDependencies,
2775
2775
  ...packageJson.dependencies,
2776
2776
  ...packageJson.devDependencies
2777
- };
2777
+ }.preact ?? null;
2778
2778
  };
2779
2779
  const TANSTACK_QUERY_PACKAGES = new Set([
2780
2780
  "@tanstack/react-query",
@@ -2789,6 +2789,16 @@ const hasTanStackQuery = (packageJson) => {
2789
2789
  };
2790
2790
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2791
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
+ };
2792
2802
  const hasUpperBoundOnlyPeerRange = (range) => {
2793
2803
  if (typeof range !== "string") return false;
2794
2804
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2875,12 +2885,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
2875
2885
  const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
2876
2886
  return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
2877
2887
  };
2888
+ const NON_PROJECT_DIRECTORIES = new Set([
2889
+ "AppData",
2890
+ "Application Data",
2891
+ "Library"
2892
+ ]);
2893
+ const MAX_SCAN_DEPTH = 6;
2878
2894
  const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2879
2895
  const packages = [];
2880
- const pendingDirectories = [rootDirectory];
2896
+ const pendingDirectories = [{
2897
+ directory: rootDirectory,
2898
+ depth: 0
2899
+ }];
2881
2900
  while (pendingDirectories.length > 0) {
2882
- const currentDirectory = pendingDirectories.pop();
2883
- if (!currentDirectory) continue;
2901
+ const current = pendingDirectories.pop();
2902
+ if (!current) continue;
2903
+ const { directory: currentDirectory, depth } = current;
2884
2904
  const packageJsonPath = path.join(currentDirectory, "package.json");
2885
2905
  if (isFile(packageJsonPath)) {
2886
2906
  const packageJson = readPackageJson(packageJsonPath);
@@ -2892,10 +2912,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
2892
2912
  });
2893
2913
  }
2894
2914
  }
2915
+ if (depth >= MAX_SCAN_DEPTH) continue;
2895
2916
  const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
2896
2917
  for (const entry of entries) {
2897
- if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2898
- 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
+ });
2899
2923
  }
2900
2924
  }
2901
2925
  return packages;
@@ -2966,6 +2990,8 @@ const discoverProject = (directory) => {
2966
2990
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2967
2991
  const sourceFileCount = countSourceFiles(directory);
2968
2992
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2993
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2994
+ const preactVersion = getPreactVersion(packageJson);
2969
2995
  const projectInfo = {
2970
2996
  rootDirectory: directory,
2971
2997
  projectName,
@@ -2976,13 +3002,16 @@ const discoverProject = (directory) => {
2976
3002
  hasTypeScript,
2977
3003
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2978
3004
  hasTanStackQuery: hasTanStackQuery(packageJson),
2979
- hasPreact: hasPreact(packageJson),
3005
+ preactVersion,
3006
+ preactMajorVersion: parseReactMajor(preactVersion),
2980
3007
  hasReactNativeWorkspace,
3008
+ hasReanimated,
2981
3009
  sourceFileCount
2982
3010
  };
2983
3011
  cachedProjectInfos.set(directory, projectInfo);
2984
3012
  return projectInfo;
2985
3013
  };
3014
+ const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
2986
3015
  const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2987
3016
  const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2988
3017
  const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
@@ -3948,17 +3977,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3948
3977
  headers
3949
3978
  }).pipe(Layer.provide(FetchHttpClient.layer));
3950
3979
  }).pipe(Effect.orDie));
3951
- Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3952
- Schema.String.pipe(Schema.brand("NodeBinaryPath"));
3953
- 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: () => {
3954
3987
  const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
3955
3988
  if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
3956
3989
  const parsed = Number(raw);
3957
3990
  if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
3958
3991
  return parsed;
3959
- } });
3960
- Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3961
- 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 }) {};
3962
4000
  const DIAGNOSTIC_SURFACES = [
3963
4001
  "cli",
3964
4002
  "prComment",
@@ -4570,47 +4608,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4570
4608
  };
4571
4609
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4572
4610
  const DEAD_CODE_WORKER_SCRIPT = `
4573
- const { parentPort, workerData } = require("node:worker_threads");
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"));
4574
4615
 
4575
- const normalizeResult = (result) => ({
4576
- unusedFiles: result.unusedFiles.map((unusedFile) => ({
4577
- path: unusedFile.path,
4578
- })),
4579
- unusedExports: result.unusedExports.map((unusedExport) => ({
4580
- path: unusedExport.path,
4581
- name: unusedExport.name,
4582
- line: unusedExport.line,
4583
- column: unusedExport.column,
4584
- isTypeOnly: unusedExport.isTypeOnly,
4585
- })),
4586
- unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4587
- name: unusedDependency.name,
4588
- isDevDependency: unusedDependency.isDevDependency,
4589
- })),
4590
- circularDependencies: result.circularDependencies.map((cycle) => ({
4591
- files: cycle.files,
4592
- })),
4593
- });
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
+ });
4594
4635
 
4595
- const serializeError = (error) =>
4596
- error instanceof Error
4597
- ? { name: error.name, message: error.message, stack: error.stack }
4598
- : { message: String(error) };
4636
+ const serializeError = (error) =>
4637
+ error instanceof Error
4638
+ ? { name: error.name, message: error.message, stack: error.stack }
4639
+ : { message: String(error) };
4599
4640
 
4600
- (async () => {
4601
- try {
4602
- const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4603
- const config = {
4604
- rootDir: workerData.rootDirectory,
4605
- ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4606
- ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4607
- };
4608
- const result = await analyze(defineConfig(config));
4609
- parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4610
- } catch (error) {
4611
- parentPort.postMessage({ ok: false, error: serializeError(error) });
4612
- }
4613
- })();
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
+ });
4614
4662
  `;
4615
4663
  const resolveTsConfigPath = (rootDirectory) => {
4616
4664
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4733,43 +4781,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4733
4781
  return error;
4734
4782
  };
4735
4783
  const createDeadCodeWorker = (input) => {
4736
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4737
- eval: true,
4738
- workerData: input
4784
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4785
+ stdio: [
4786
+ "pipe",
4787
+ "pipe",
4788
+ "pipe"
4789
+ ],
4790
+ windowsHide: true
4739
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));
4740
4796
  let didSettle = false;
4741
- return {
4742
- result: new Promise((resolve, reject) => {
4743
- const settle = (callback) => {
4744
- if (didSettle) return;
4745
- didSettle = true;
4746
- worker.removeAllListeners();
4747
- callback();
4748
- };
4749
- worker.once("message", (message) => {
4750
- try {
4751
- const parsedMessage = parseDeadCodeWorkerMessage(message);
4752
- if (parsedMessage.ok) {
4753
- settle(() => resolve(parsedMessage.result));
4754
- return;
4755
- }
4756
- settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4757
- } catch (error) {
4758
- settle(() => reject(error));
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;
4759
4818
  }
4760
- });
4761
- worker.once("error", (error) => {
4819
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4820
+ } catch (error) {
4762
4821
  settle(() => reject(error));
4763
- });
4764
- worker.once("exit", (exitCode) => {
4765
- if (exitCode === 0) return;
4766
- settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4767
- });
4768
- }),
4822
+ }
4823
+ });
4824
+ });
4825
+ child.stdin.on("error", () => {});
4826
+ child.stdin.end(JSON.stringify(input));
4827
+ return {
4828
+ result,
4769
4829
  terminate: () => {
4770
4830
  didSettle = true;
4771
- worker.removeAllListeners();
4772
- return worker.terminate();
4831
+ child.kill("SIGKILL");
4773
4832
  }
4774
4833
  };
4775
4834
  };
@@ -5011,8 +5070,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5011
5070
  env: input.env,
5012
5071
  extendEnv: true
5013
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))));
5014
5080
  const [stdout, stderr, status] = yield* Effect.all([
5015
- Stream.mkString(Stream.decodeText(handle.stdout)),
5081
+ Stream.mkString(Stream.decodeText(stdoutStream)),
5016
5082
  Stream.mkString(Stream.decodeText(handle.stderr)),
5017
5083
  handle.exitCode
5018
5084
  ], { concurrency: 3 });
@@ -5174,7 +5240,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5174
5240
  if (result.status !== 0) return [];
5175
5241
  return splitNullSeparated(result.stdout);
5176
5242
  })),
5177
- 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)),
5178
5249
  grep: (input) => Effect.gen(function* () {
5179
5250
  const args = ["grep"];
5180
5251
  if (input.listMatchingFiles ?? true) args.push("-l");
@@ -5182,7 +5253,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
5182
5253
  if (input.extendedRegexp ?? false) args.push("-E");
5183
5254
  args.push(input.pattern);
5184
5255
  if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
5185
- 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
+ });
5186
5262
  if (result.status === 128) return null;
5187
5263
  return {
5188
5264
  status: result.status,
@@ -5430,7 +5506,8 @@ const buildCapabilities = (project) => {
5430
5506
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5431
5507
  const reactMajor = project.reactMajorVersion;
5432
5508
  if (reactMajor !== null) {
5433
- for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5509
+ const cappedReactMajor = Math.min(reactMajor, 30);
5510
+ for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
5434
5511
  if (reactMajor >= 19) {
5435
5512
  if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5436
5513
  major: 19,
@@ -5448,8 +5525,13 @@ const buildCapabilities = (project) => {
5448
5525
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5449
5526
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5450
5527
  if (project.hasTypeScript) capabilities.add("typescript");
5451
- if (project.hasPreact) {
5528
+ if (project.preactVersion !== null) {
5452
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
+ }
5453
5535
  if (project.reactVersion === null) capabilities.add("pure-preact");
5454
5536
  }
5455
5537
  return capabilities;
@@ -5706,6 +5788,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5706
5788
  if (!publicEnvPrefix) return fallbackRecommendation;
5707
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`;
5708
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
+ };
5709
5798
  const REACT_MODULE_SOURCE = "react";
5710
5799
  const REQUIRE_IDENTIFIER = "require";
5711
5800
  const USE_IDENTIFIER = "use";
@@ -6029,7 +6118,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
6029
6118
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
6030
6119
  if (plugin === "react-hooks-js") return {
6031
6120
  message: REACT_COMPILER_MESSAGE,
6032
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6121
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6033
6122
  };
6034
6123
  return {
6035
6124
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -6098,13 +6187,6 @@ const SANITIZED_ENV = (() => {
6098
6187
  }
6099
6188
  return sanitized;
6100
6189
  })();
6101
- const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6102
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
6103
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
6104
- const parsed = Number(raw);
6105
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
6106
- return parsed;
6107
- })();
6108
6190
  /**
6109
6191
  * Spawn one oxlint subprocess with hard ceilings on wall time and
6110
6192
  * output size. Returns stdout on success; raises a tagged
@@ -6121,7 +6203,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
6121
6203
  * The first three are splittable (the caller's binary-split retry
6122
6204
  * shrinks the batch and re-spawns); the fourth isn't.
6123
6205
  */
6124
- 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) => {
6125
6207
  const child = spawn(nodeBinaryPath, args, {
6126
6208
  cwd: rootDirectory,
6127
6209
  env: SANITIZED_ENV
@@ -6130,9 +6212,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6130
6212
  child.kill("SIGKILL");
6131
6213
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6132
6214
  kind: "timeout",
6133
- detail: `${OXLINT_SPAWN_TIMEOUT_MS$1 / 1e3}s budget exceeded`
6215
+ detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
6134
6216
  }) }));
6135
- }, OXLINT_SPAWN_TIMEOUT_MS$1);
6217
+ }, spawnTimeoutMs);
6136
6218
  timeoutHandle.unref?.();
6137
6219
  const stdoutBuffers = [];
6138
6220
  const stderrBuffers = [];
@@ -6142,7 +6224,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6142
6224
  const killIfTooLarge = (incomingBytes, isStdout) => {
6143
6225
  if (isStdout) stdoutByteCount += incomingBytes;
6144
6226
  else stderrByteCount += incomingBytes;
6145
- if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
6227
+ if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
6146
6228
  didKillForSize = true;
6147
6229
  child.kill("SIGKILL");
6148
6230
  return true;
@@ -6168,7 +6250,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6168
6250
  if (didKillForSize) {
6169
6251
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
6170
6252
  kind: "output-too-large",
6171
- 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`
6172
6254
  }) }));
6173
6255
  return;
6174
6256
  }
@@ -6209,7 +6291,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
6209
6291
  * with a slimmer config in that case.
6210
6292
  */
6211
6293
  const spawnLintBatches = async (input) => {
6212
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
6294
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
6213
6295
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
6214
6296
  const allDiagnostics = [];
6215
6297
  const droppedFiles = [];
@@ -6217,7 +6299,7 @@ const spawnLintBatches = async (input) => {
6217
6299
  const spawnLintBatch = async (batch) => {
6218
6300
  const batchArgs = [...baseArgs, ...batch];
6219
6301
  try {
6220
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
6302
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
6221
6303
  } catch (error) {
6222
6304
  if (!isSplittableReactDoctorError(error)) throw error;
6223
6305
  if (batch.length <= 1) {
@@ -6320,13 +6402,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
6320
6402
  * 6. always restore disable directives + clean up the temp dir
6321
6403
  */
6322
6404
  const runOxlint = async (options) => {
6323
- 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;
6324
6406
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
6325
6407
  const severityControls = buildRuleSeverityControls(userConfig);
6326
6408
  validateRuleRegistration();
6327
6409
  if (includePaths !== void 0 && includePaths.length === 0) return [];
6328
- const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
6329
- const configPath = path.join(configDirectory, "oxlintrc.json");
6330
6410
  const pluginPath = resolvePluginPath();
6331
6411
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
6332
6412
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
@@ -6341,6 +6421,8 @@ const runOxlint = async (options) => {
6341
6421
  userPlugins
6342
6422
  });
6343
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");
6344
6426
  try {
6345
6427
  const baseArgs = [
6346
6428
  resolveOxlintBinary(),
@@ -6367,7 +6449,9 @@ const runOxlint = async (options) => {
6367
6449
  nodeBinaryPath,
6368
6450
  project,
6369
6451
  onPartialFailure,
6370
- onFileProgress: options.onFileProgress
6452
+ onFileProgress: options.onFileProgress,
6453
+ spawnTimeoutMs,
6454
+ outputMaxBytes
6371
6455
  });
6372
6456
  writeOxlintConfig(configPath, buildConfig(extendsPaths));
6373
6457
  try {
@@ -6433,6 +6517,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6433
6517
  */
6434
6518
  static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
6435
6519
  const partialFailures = yield* LintPartialFailures;
6520
+ const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
6521
+ const outputMaxBytes = yield* OxlintOutputMaxBytes;
6436
6522
  const collectedFailures = [];
6437
6523
  const diagnostics = yield* Effect.tryPromise({
6438
6524
  try: () => runOxlint({
@@ -6449,7 +6535,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
6449
6535
  onPartialFailure: (reason) => {
6450
6536
  collectedFailures.push(reason);
6451
6537
  },
6452
- onFileProgress: input.onFileProgress
6538
+ onFileProgress: input.onFileProgress,
6539
+ spawnTimeoutMs,
6540
+ outputMaxBytes
6453
6541
  }),
6454
6542
  catch: ensureReactDoctorError
6455
6543
  });
@@ -6747,7 +6835,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6747
6835
  const resolvedConfig = yield* configService.resolve(input.directory);
6748
6836
  const scanDirectory = resolvedConfig.resolvedDirectory;
6749
6837
  const project = yield* projectService.discover(scanDirectory);
6750
- 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 }) });
6751
6839
  const [repo, sha, defaultBranch] = yield* Effect.all([
6752
6840
  gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6753
6841
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
@@ -6775,7 +6863,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6775
6863
  const lintFailure = yield* Ref.make({
6776
6864
  didFail: false,
6777
6865
  reason: null,
6778
- reasonTag: null
6866
+ reasonTag: null,
6867
+ reasonKind: null
6779
6868
  });
6780
6869
  const deadCodeFailure = yield* Ref.make({
6781
6870
  didFail: false,
@@ -6797,13 +6886,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6797
6886
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
6798
6887
  onFileProgress: (scannedFileCount, totalFileCount) => {
6799
6888
  lastReportedTotalFileCount = totalFileCount;
6800
- Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
6889
+ Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
6801
6890
  }
6802
6891
  }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6803
6892
  yield* Ref.set(lintFailure, {
6804
6893
  didFail: true,
6805
6894
  reason: error.message,
6806
- reasonTag: error.reason._tag
6895
+ reasonTag: error.reason._tag,
6896
+ reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
6807
6897
  });
6808
6898
  return Stream.empty;
6809
6899
  }))));
@@ -6863,6 +6953,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6863
6953
  didLintFail: lintFailureState.didFail,
6864
6954
  lintFailureReason: lintFailureState.reason,
6865
6955
  lintFailureReasonTag: lintFailureState.reasonTag,
6956
+ lintFailureReasonKind: lintFailureState.reasonKind,
6866
6957
  lintPartialFailures,
6867
6958
  didDeadCodeFail: deadCodeFailureState.didFail,
6868
6959
  deadCodeFailureReason: deadCodeFailureState.reason
@@ -7297,11 +7388,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7297
7388
  const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7298
7389
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7299
7390
  const skippedChecks = [];
7391
+ if (output.didLintFail) skippedChecks.push("lint");
7392
+ if (output.didDeadCodeFail) skippedChecks.push("dead-code");
7300
7393
  const skippedCheckReasons = {};
7301
- if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) {
7302
- skippedChecks.push("dead-code");
7303
- skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
7304
- }
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;
7305
7397
  return {
7306
7398
  diagnostics: [...output.diagnostics],
7307
7399
  score: output.score,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.10",
3
+ "version": "0.2.11-dev.d917f62",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -58,14 +58,14 @@
58
58
  "oxlint": "^1.66.0",
59
59
  "prompts": "^2.4.2",
60
60
  "typescript": ">=5.0.4 <7",
61
- "oxlint-plugin-react-doctor": "0.2.10"
61
+ "oxlint-plugin-react-doctor": "0.2.11-dev.d917f62"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/prompts": "^2.4.9",
65
65
  "commander": "^14.0.3",
66
66
  "ora": "^9.4.0",
67
- "@react-doctor/api": "0.2.10",
68
- "@react-doctor/core": "0.2.10"
67
+ "@react-doctor/api": "0.2.11",
68
+ "@react-doctor/core": "0.2.11"
69
69
  },
70
70
  "engines": {
71
71
  "node": "^20.19.0 || >=22.12.0"