react-doctor 0.2.10 → 0.2.11

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,21 +2742,7 @@ 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
- };
2745
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2746
2746
  const hasPreact = (packageJson) => {
2747
2747
  return "preact" in {
2748
2748
  ...packageJson.peerDependencies,
@@ -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);
@@ -2937,6 +2947,7 @@ const discoverProject = (directory) => {
2937
2947
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2938
2948
  const sourceFileCount = countSourceFiles(directory);
2939
2949
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2950
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2940
2951
  const projectInfo = {
2941
2952
  rootDirectory: directory,
2942
2953
  projectName,
@@ -2949,6 +2960,7 @@ const discoverProject = (directory) => {
2949
2960
  hasTanStackQuery: hasTanStackQuery(packageJson),
2950
2961
  hasPreact: hasPreact(packageJson),
2951
2962
  hasReactNativeWorkspace,
2963
+ hasReanimated,
2952
2964
  sourceFileCount
2953
2965
  };
2954
2966
  cachedProjectInfos.set(directory, projectInfo);
@@ -4539,47 +4551,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4539
4551
  };
4540
4552
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4541
4553
  const DEAD_CODE_WORKER_SCRIPT = `
4542
- const { parentPort, workerData } = require("node:worker_threads");
4554
+ const inputChunks = [];
4555
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4556
+ process.stdin.on("end", () => {
4557
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4543
4558
 
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
- });
4559
+ const normalizeResult = (result) => ({
4560
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4561
+ path: unusedFile.path,
4562
+ })),
4563
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4564
+ path: unusedExport.path,
4565
+ name: unusedExport.name,
4566
+ line: unusedExport.line,
4567
+ column: unusedExport.column,
4568
+ isTypeOnly: unusedExport.isTypeOnly,
4569
+ })),
4570
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4571
+ name: unusedDependency.name,
4572
+ isDevDependency: unusedDependency.isDevDependency,
4573
+ })),
4574
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4575
+ files: cycle.files,
4576
+ })),
4577
+ });
4563
4578
 
4564
- const serializeError = (error) =>
4565
- error instanceof Error
4566
- ? { name: error.name, message: error.message, stack: error.stack }
4567
- : { message: String(error) };
4579
+ const serializeError = (error) =>
4580
+ error instanceof Error
4581
+ ? { name: error.name, message: error.message, stack: error.stack }
4582
+ : { message: String(error) };
4568
4583
 
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
- })();
4584
+ const emit = (message) => {
4585
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4586
+ };
4587
+
4588
+ (async () => {
4589
+ try {
4590
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4591
+ const config = {
4592
+ rootDir: workerInput.rootDirectory,
4593
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4594
+ ...(workerInput.ignorePatterns.length > 0
4595
+ ? { ignorePatterns: workerInput.ignorePatterns }
4596
+ : {}),
4597
+ };
4598
+ const result = await analyze(defineConfig(config));
4599
+ emit({ ok: true, result: normalizeResult(result) });
4600
+ } catch (error) {
4601
+ emit({ ok: false, error: serializeError(error) });
4602
+ }
4603
+ })();
4604
+ });
4583
4605
  `;
4584
4606
  const resolveTsConfigPath = (rootDirectory) => {
4585
4607
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4702,43 +4724,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4702
4724
  return error;
4703
4725
  };
4704
4726
  const createDeadCodeWorker = (input) => {
4705
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4706
- eval: true,
4707
- workerData: input
4727
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4728
+ stdio: [
4729
+ "pipe",
4730
+ "pipe",
4731
+ "pipe"
4732
+ ],
4733
+ windowsHide: true
4708
4734
  });
4735
+ const stdoutChunks = [];
4736
+ const stderrChunks = [];
4737
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4738
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4709
4739
  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));
4740
+ const result = new Promise((resolve, reject) => {
4741
+ const settle = (callback) => {
4742
+ if (didSettle) return;
4743
+ didSettle = true;
4744
+ callback();
4745
+ };
4746
+ child.once("error", (error) => {
4747
+ settle(() => reject(error));
4748
+ });
4749
+ child.once("close", (exitCode) => {
4750
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4751
+ if (stdout.length === 0) {
4752
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4753
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4754
+ return;
4755
+ }
4756
+ try {
4757
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4758
+ if (parsedMessage.ok) {
4759
+ settle(() => resolve(parsedMessage.result));
4760
+ return;
4728
4761
  }
4729
- });
4730
- worker.once("error", (error) => {
4762
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4763
+ } catch (error) {
4731
4764
  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
- }),
4765
+ }
4766
+ });
4767
+ });
4768
+ child.stdin.on("error", () => {});
4769
+ child.stdin.end(JSON.stringify(input));
4770
+ return {
4771
+ result,
4738
4772
  terminate: () => {
4739
4773
  didSettle = true;
4740
- worker.removeAllListeners();
4741
- return worker.terminate();
4774
+ child.kill("SIGKILL");
4742
4775
  }
4743
4776
  };
4744
4777
  };
@@ -5675,6 +5708,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5675
5708
  if (!publicEnvPrefix) return fallbackRecommendation;
5676
5709
  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
5710
  };
5711
+ 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";
5712
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5713
+ if (rule !== "immutability") return help;
5714
+ if (!project.hasReanimated) return help;
5715
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5716
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5717
+ };
5678
5718
  const REACT_MODULE_SOURCE = "react";
5679
5719
  const REQUIRE_IDENTIFIER = "require";
5680
5720
  const USE_IDENTIFIER = "use";
@@ -5998,7 +6038,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5998
6038
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5999
6039
  if (plugin === "react-hooks-js") return {
6000
6040
  message: REACT_COMPILER_MESSAGE,
6001
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6041
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6002
6042
  };
6003
6043
  return {
6004
6044
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -7318,4 +7358,4 @@ const cliLogger = {
7318
7358
  //#endregion
7319
7359
  export { isReactDoctorError as A, filterSourceFiles as C, groupBy as D, getDiffInfo as E, runInspect as F, toRelativePath as I, listWorkspacePackages as M, resolveScanTarget as N, highlighter as O, restoreLegacyThrow as P, filterDiagnosticsForSurface as S, formatReactDoctorError as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, layerOtlp as j, isMonorepoRoot as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, formatErrorChain as w, discoverReactSubprojects as x, buildJsonReport as y };
7320
7360
 
7321
- //# sourceMappingURL=cli-logger-BRBUS1pE.js.map
7361
+ //# sourceMappingURL=cli-logger-pbFEieEc.js.map
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
2
- import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-BRBUS1pE.js";
2
+ import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-pbFEieEc.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, execSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
@@ -6666,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6666
6666
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6667
6667
  //#endregion
6668
6668
  //#region src/cli/utils/version.ts
6669
- const VERSION = "0.2.10";
6669
+ const VERSION = "0.2.11";
6670
6670
  //#endregion
6671
6671
  //#region src/inspect.ts
6672
6672
  const silentConsole = makeNoopConsole();
@@ -7463,7 +7463,7 @@ const warnSetupPromptFailure = async (options, error) => {
7463
7463
  return;
7464
7464
  }
7465
7465
  try {
7466
- const { cliLogger } = await import("./cli-logger-BRBUS1pE.js").then((n) => n.n);
7466
+ const { cliLogger } = await import("./cli-logger-pbFEieEc.js").then((n) => n.n);
7467
7467
  cliLogger.warn(message);
7468
7468
  } catch {}
7469
7469
  };
package/dist/index.d.ts CHANGED
@@ -333,6 +333,13 @@ interface ProjectInfo {
333
333
  * — no `rn-*` rules load for the project at all.
334
334
  */
335
335
  hasReactNativeWorkspace: boolean;
336
+ /**
337
+ * `true` when the project (or any of its workspace packages) declares
338
+ * `react-native-reanimated`. Lets diagnostics surface reanimated's
339
+ * Compiler-compatible `.get()` / `.set()` accessors only where they
340
+ * apply, instead of on every React Native project.
341
+ */
342
+ hasReanimated: boolean;
336
343
  sourceFileCount: number;
337
344
  }
338
345
  //#endregion
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,21 +2768,7 @@ 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
- };
2771
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2772
2772
  const hasPreact = (packageJson) => {
2773
2773
  return "preact" in {
2774
2774
  ...packageJson.peerDependencies,
@@ -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);
@@ -2966,6 +2976,7 @@ const discoverProject = (directory) => {
2966
2976
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2967
2977
  const sourceFileCount = countSourceFiles(directory);
2968
2978
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2979
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2969
2980
  const projectInfo = {
2970
2981
  rootDirectory: directory,
2971
2982
  projectName,
@@ -2978,6 +2989,7 @@ const discoverProject = (directory) => {
2978
2989
  hasTanStackQuery: hasTanStackQuery(packageJson),
2979
2990
  hasPreact: hasPreact(packageJson),
2980
2991
  hasReactNativeWorkspace,
2992
+ hasReanimated,
2981
2993
  sourceFileCount
2982
2994
  };
2983
2995
  cachedProjectInfos.set(directory, projectInfo);
@@ -4570,47 +4582,57 @@ const collectIgnorePatterns = (rootDirectory) => {
4570
4582
  };
4571
4583
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4572
4584
  const DEAD_CODE_WORKER_SCRIPT = `
4573
- const { parentPort, workerData } = require("node:worker_threads");
4585
+ const inputChunks = [];
4586
+ process.stdin.on("data", (chunk) => inputChunks.push(chunk));
4587
+ process.stdin.on("end", () => {
4588
+ const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
4574
4589
 
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
- });
4590
+ const normalizeResult = (result) => ({
4591
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4592
+ path: unusedFile.path,
4593
+ })),
4594
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4595
+ path: unusedExport.path,
4596
+ name: unusedExport.name,
4597
+ line: unusedExport.line,
4598
+ column: unusedExport.column,
4599
+ isTypeOnly: unusedExport.isTypeOnly,
4600
+ })),
4601
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4602
+ name: unusedDependency.name,
4603
+ isDevDependency: unusedDependency.isDevDependency,
4604
+ })),
4605
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4606
+ files: cycle.files,
4607
+ })),
4608
+ });
4594
4609
 
4595
- const serializeError = (error) =>
4596
- error instanceof Error
4597
- ? { name: error.name, message: error.message, stack: error.stack }
4598
- : { message: String(error) };
4610
+ const serializeError = (error) =>
4611
+ error instanceof Error
4612
+ ? { name: error.name, message: error.message, stack: error.stack }
4613
+ : { message: String(error) };
4599
4614
 
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
- })();
4615
+ const emit = (message) => {
4616
+ process.stdout.write(JSON.stringify(message), () => process.exit(0));
4617
+ };
4618
+
4619
+ (async () => {
4620
+ try {
4621
+ const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
4622
+ const config = {
4623
+ rootDir: workerInput.rootDirectory,
4624
+ ...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
4625
+ ...(workerInput.ignorePatterns.length > 0
4626
+ ? { ignorePatterns: workerInput.ignorePatterns }
4627
+ : {}),
4628
+ };
4629
+ const result = await analyze(defineConfig(config));
4630
+ emit({ ok: true, result: normalizeResult(result) });
4631
+ } catch (error) {
4632
+ emit({ ok: false, error: serializeError(error) });
4633
+ }
4634
+ })();
4635
+ });
4614
4636
  `;
4615
4637
  const resolveTsConfigPath = (rootDirectory) => {
4616
4638
  for (const filename of TSCONFIG_FILENAMES$1) {
@@ -4733,43 +4755,54 @@ const buildDeadCodeWorkerError = (workerError) => {
4733
4755
  return error;
4734
4756
  };
4735
4757
  const createDeadCodeWorker = (input) => {
4736
- const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4737
- eval: true,
4738
- workerData: input
4758
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4759
+ stdio: [
4760
+ "pipe",
4761
+ "pipe",
4762
+ "pipe"
4763
+ ],
4764
+ windowsHide: true
4739
4765
  });
4766
+ const stdoutChunks = [];
4767
+ const stderrChunks = [];
4768
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
4769
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
4740
4770
  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));
4771
+ const result = new Promise((resolve, reject) => {
4772
+ const settle = (callback) => {
4773
+ if (didSettle) return;
4774
+ didSettle = true;
4775
+ callback();
4776
+ };
4777
+ child.once("error", (error) => {
4778
+ settle(() => reject(error));
4779
+ });
4780
+ child.once("close", (exitCode) => {
4781
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
4782
+ if (stdout.length === 0) {
4783
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
4784
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
4785
+ return;
4786
+ }
4787
+ try {
4788
+ const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
4789
+ if (parsedMessage.ok) {
4790
+ settle(() => resolve(parsedMessage.result));
4791
+ return;
4759
4792
  }
4760
- });
4761
- worker.once("error", (error) => {
4793
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4794
+ } catch (error) {
4762
4795
  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
- }),
4796
+ }
4797
+ });
4798
+ });
4799
+ child.stdin.on("error", () => {});
4800
+ child.stdin.end(JSON.stringify(input));
4801
+ return {
4802
+ result,
4769
4803
  terminate: () => {
4770
4804
  didSettle = true;
4771
- worker.removeAllListeners();
4772
- return worker.terminate();
4805
+ child.kill("SIGKILL");
4773
4806
  }
4774
4807
  };
4775
4808
  };
@@ -5706,6 +5739,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5706
5739
  if (!publicEnvPrefix) return fallbackRecommendation;
5707
5740
  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
5741
  };
5742
+ 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";
5743
+ const appendReanimatedSharedValueHint = (help, rule, project) => {
5744
+ if (rule !== "immutability") return help;
5745
+ if (!project.hasReanimated) return help;
5746
+ if (!help) return REANIMATED_SHARED_VALUE_HINT;
5747
+ return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5748
+ };
5709
5749
  const REACT_MODULE_SOURCE = "react";
5710
5750
  const REQUIRE_IDENTIFIER = "require";
5711
5751
  const USE_IDENTIFIER = "use";
@@ -6029,7 +6069,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
6029
6069
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
6030
6070
  if (plugin === "react-hooks-js") return {
6031
6071
  message: REACT_COMPILER_MESSAGE,
6032
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6072
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
6033
6073
  };
6034
6074
  return {
6035
6075
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
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",
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"
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"