react-doctor 0.2.9 → 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.
package/dist/index.js CHANGED
@@ -2298,11 +2298,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
2298
2298
  gatsby: "Gatsby",
2299
2299
  expo: "Expo",
2300
2300
  "react-native": "React Native",
2301
+ preact: "Preact",
2301
2302
  unknown: "React"
2302
2303
  };
2303
2304
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2304
2305
  const detectFramework = (dependencies) => {
2305
2306
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2307
+ if (dependencies.preact && !dependencies.react) return "preact";
2306
2308
  return "unknown";
2307
2309
  };
2308
2310
  const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
@@ -2721,6 +2723,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
2721
2723
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
2722
2724
  };
2723
2725
  };
2726
+ const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
2727
+ if (predicate(rootPackageJson)) return true;
2728
+ const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2729
+ if (patterns.length === 0) return false;
2730
+ const visitedDirectories = /* @__PURE__ */ new Set();
2731
+ for (const pattern of patterns) {
2732
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2733
+ for (const workspaceDirectory of directories) {
2734
+ if (visitedDirectories.has(workspaceDirectory)) continue;
2735
+ visitedDirectories.add(workspaceDirectory);
2736
+ if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2737
+ }
2738
+ }
2739
+ return false;
2740
+ };
2724
2741
  const NAMES = new Set([
2725
2742
  "react-native",
2726
2743
  "react-native-tvos",
@@ -2751,20 +2768,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
2751
2768
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
2752
2769
  return false;
2753
2770
  };
2754
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2755
- if (isPackageJsonReactNativeAware(rootPackageJson)) return true;
2756
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
2757
- if (patterns.length === 0) return false;
2758
- const visitedDirectories = /* @__PURE__ */ new Set();
2759
- for (const pattern of patterns) {
2760
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
2761
- for (const workspaceDirectory of directories) {
2762
- if (visitedDirectories.has(workspaceDirectory)) continue;
2763
- visitedDirectories.add(workspaceDirectory);
2764
- if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
2765
- }
2766
- }
2767
- return false;
2771
+ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
2772
+ const hasPreact = (packageJson) => {
2773
+ return "preact" in {
2774
+ ...packageJson.peerDependencies,
2775
+ ...packageJson.dependencies,
2776
+ ...packageJson.devDependencies
2777
+ };
2768
2778
  };
2769
2779
  const TANSTACK_QUERY_PACKAGES = new Set([
2770
2780
  "@tanstack/react-query",
@@ -2779,6 +2789,16 @@ const hasTanStackQuery = (packageJson) => {
2779
2789
  };
2780
2790
  return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
2781
2791
  };
2792
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
2793
+ const isPackageJsonReanimatedAware = (packageJson) => {
2794
+ const allDependencies = {
2795
+ ...packageJson.peerDependencies,
2796
+ ...packageJson.dependencies,
2797
+ ...packageJson.devDependencies,
2798
+ ...packageJson.optionalDependencies
2799
+ };
2800
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
2801
+ };
2782
2802
  const hasUpperBoundOnlyPeerRange = (range) => {
2783
2803
  if (typeof range !== "string") return false;
2784
2804
  const normalizedRange = normalizeDependencyVersion(range);
@@ -2803,7 +2823,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2803
2823
  const REACT_DEPENDENCY_NAMES = new Set([
2804
2824
  "react",
2805
2825
  "react-native",
2806
- "next"
2826
+ "next",
2827
+ "preact"
2807
2828
  ]);
2808
2829
  const hasReactDependency = (packageJson) => {
2809
2830
  const allDependencies = {
@@ -2955,6 +2976,7 @@ const discoverProject = (directory) => {
2955
2976
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
2956
2977
  const sourceFileCount = countSourceFiles(directory);
2957
2978
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
2979
+ const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
2958
2980
  const projectInfo = {
2959
2981
  rootDirectory: directory,
2960
2982
  projectName,
@@ -2965,12 +2987,48 @@ const discoverProject = (directory) => {
2965
2987
  hasTypeScript,
2966
2988
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2967
2989
  hasTanStackQuery: hasTanStackQuery(packageJson),
2990
+ hasPreact: hasPreact(packageJson),
2968
2991
  hasReactNativeWorkspace,
2992
+ hasReanimated,
2969
2993
  sourceFileCount
2970
2994
  };
2971
2995
  cachedProjectInfos.set(directory, projectInfo);
2972
2996
  return projectInfo;
2973
2997
  };
2998
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2999
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
3000
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
3001
+ const parseReactMajorMinor = (reactVersion) => {
3002
+ if (typeof reactVersion !== "string") return null;
3003
+ const trimmed = reactVersion.trim();
3004
+ if (trimmed.length === 0) return null;
3005
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
3006
+ if (lowerBoundsOnly.length === 0) return null;
3007
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
3008
+ if (majorMinorMatch) {
3009
+ const major = Number.parseInt(majorMinorMatch[1], 10);
3010
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
3011
+ if (!Number.isFinite(major) || major <= 0) return null;
3012
+ if (!Number.isFinite(minor) || minor < 0) return null;
3013
+ return {
3014
+ major,
3015
+ minor
3016
+ };
3017
+ }
3018
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
3019
+ if (!majorOnlyMatch) return null;
3020
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
3021
+ if (!Number.isFinite(major) || major <= 0) return null;
3022
+ return {
3023
+ major,
3024
+ minor: 0
3025
+ };
3026
+ };
3027
+ const isReactAtLeast = (detected, required) => {
3028
+ if (detected === null) return true;
3029
+ if (detected.major !== required.major) return detected.major > required.major;
3030
+ return detected.minor >= required.minor;
3031
+ };
2974
3032
  const parseTailwindMajorMinor = (tailwindVersion) => {
2975
3033
  if (typeof tailwindVersion !== "string") return null;
2976
3034
  const trimmed = tailwindVersion.trim();
@@ -3001,6 +3059,7 @@ const isTailwindAtLeast = (detected, required) => {
3001
3059
  return detected.minor >= required.minor;
3002
3060
  };
3003
3061
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3062
+ const MILLISECONDS_PER_SECOND = 1e3;
3004
3063
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3005
3064
  const FETCH_TIMEOUT_MS = 1e4;
3006
3065
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
@@ -4522,6 +4581,59 @@ const collectIgnorePatterns = (rootDirectory) => {
4522
4581
  return patterns;
4523
4582
  };
4524
4583
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4584
+ const DEAD_CODE_WORKER_SCRIPT = `
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"));
4589
+
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
+ });
4609
+
4610
+ const serializeError = (error) =>
4611
+ error instanceof Error
4612
+ ? { name: error.name, message: error.message, stack: error.stack }
4613
+ : { message: String(error) };
4614
+
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
+ });
4636
+ `;
4525
4637
  const resolveTsConfigPath = (rootDirectory) => {
4526
4638
  for (const filename of TSCONFIG_FILENAMES$1) {
4527
4639
  const candidate = path.join(rootDirectory, filename);
@@ -4542,16 +4654,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4542
4654
  const relative = toRelativePath(filePath, rootDirectory);
4543
4655
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4544
4656
  };
4657
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4658
+ const parseArray = (value, label) => {
4659
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4660
+ return value;
4661
+ };
4662
+ const parseString = (value, label) => {
4663
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4664
+ return value;
4665
+ };
4666
+ const parseNumber = (value, label) => {
4667
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4668
+ return value;
4669
+ };
4670
+ const parseBoolean = (value, label) => {
4671
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4672
+ return value;
4673
+ };
4674
+ const parseStringArray = (value, label) => {
4675
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4676
+ };
4677
+ const parseUnusedFiles = (value) => {
4678
+ const values = parseArray(value, "unusedFiles");
4679
+ const unusedFiles = [];
4680
+ for (const [index, entry] of values.entries()) {
4681
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4682
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4683
+ }
4684
+ return unusedFiles;
4685
+ };
4686
+ const parseUnusedExports = (value) => {
4687
+ const values = parseArray(value, "unusedExports");
4688
+ const unusedExports = [];
4689
+ for (const [index, entry] of values.entries()) {
4690
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4691
+ unusedExports.push({
4692
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4693
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4694
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4695
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4696
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4697
+ });
4698
+ }
4699
+ return unusedExports;
4700
+ };
4701
+ const parseUnusedDependencies = (value) => {
4702
+ const values = parseArray(value, "unusedDependencies");
4703
+ const unusedDependencies = [];
4704
+ for (const [index, entry] of values.entries()) {
4705
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4706
+ unusedDependencies.push({
4707
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4708
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4709
+ });
4710
+ }
4711
+ return unusedDependencies;
4712
+ };
4713
+ const parseCircularDependencies = (value) => {
4714
+ const values = parseArray(value, "circularDependencies");
4715
+ const circularDependencies = [];
4716
+ for (const [index, entry] of values.entries()) {
4717
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4718
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4719
+ }
4720
+ return circularDependencies;
4721
+ };
4722
+ const parseDeadCodeWorkerResult = (value) => {
4723
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4724
+ return {
4725
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4726
+ unusedExports: parseUnusedExports(value.unusedExports),
4727
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4728
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4729
+ };
4730
+ };
4731
+ const parseDeadCodeWorkerError = (value) => {
4732
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4733
+ return {
4734
+ ...typeof value.name === "string" ? { name: value.name } : {},
4735
+ message: value.message,
4736
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4737
+ };
4738
+ };
4739
+ const parseDeadCodeWorkerMessage = (value) => {
4740
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4741
+ if (value.ok === true) return {
4742
+ ok: true,
4743
+ result: value.result
4744
+ };
4745
+ if (value.ok === false) return {
4746
+ ok: false,
4747
+ error: parseDeadCodeWorkerError(value.error)
4748
+ };
4749
+ throw new Error("Dead-code worker returned an invalid status.");
4750
+ };
4751
+ const buildDeadCodeWorkerError = (workerError) => {
4752
+ const error = new Error(workerError.message);
4753
+ if (workerError.name !== void 0) error.name = workerError.name;
4754
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4755
+ return error;
4756
+ };
4757
+ const createDeadCodeWorker = (input) => {
4758
+ const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4759
+ stdio: [
4760
+ "pipe",
4761
+ "pipe",
4762
+ "pipe"
4763
+ ],
4764
+ windowsHide: true
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));
4770
+ let didSettle = false;
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;
4792
+ }
4793
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4794
+ } catch (error) {
4795
+ settle(() => reject(error));
4796
+ }
4797
+ });
4798
+ });
4799
+ child.stdin.on("error", () => {});
4800
+ child.stdin.end(JSON.stringify(input));
4801
+ return {
4802
+ result,
4803
+ terminate: () => {
4804
+ didSettle = true;
4805
+ child.kill("SIGKILL");
4806
+ }
4807
+ };
4808
+ };
4809
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4810
+ let didSettle = false;
4811
+ const timeoutHandle = setTimeout(() => {
4812
+ if (didSettle) return;
4813
+ didSettle = true;
4814
+ handle.terminate?.();
4815
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4816
+ }, timeoutMs);
4817
+ timeoutHandle.unref?.();
4818
+ handle.result.then((value) => {
4819
+ if (didSettle) return;
4820
+ didSettle = true;
4821
+ clearTimeout(timeoutHandle);
4822
+ handle.terminate?.();
4823
+ resolve(value);
4824
+ }, (error) => {
4825
+ if (didSettle) return;
4826
+ didSettle = true;
4827
+ clearTimeout(timeoutHandle);
4828
+ handle.terminate?.();
4829
+ reject(error);
4830
+ });
4831
+ });
4545
4832
  const checkDeadCode = async (options) => {
4546
4833
  const { rootDirectory, userConfig } = options;
4547
4834
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4548
- const { analyze, defineConfig } = await import("deslop-js");
4549
4835
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4550
- const result = await analyze(defineConfig({
4551
- rootDir: rootDirectory,
4836
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4837
+ rootDirectory,
4552
4838
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4553
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4554
- }));
4839
+ ignorePatterns,
4840
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4841
+ }), options.workerTimeoutMs ?? 12e4));
4555
4842
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4556
4843
  const diagnostics = [];
4557
4844
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -5175,7 +5462,15 @@ const buildCapabilities = (project) => {
5175
5462
  capabilities.add(project.framework);
5176
5463
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5177
5464
  const reactMajor = project.reactMajorVersion;
5178
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5465
+ if (reactMajor !== null) {
5466
+ for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5467
+ if (reactMajor >= 19) {
5468
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5469
+ major: 19,
5470
+ minor: 2
5471
+ })) capabilities.add("react:19.2");
5472
+ }
5473
+ }
5179
5474
  if (project.tailwindVersion !== null) {
5180
5475
  capabilities.add("tailwind");
5181
5476
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5186,6 +5481,10 @@ const buildCapabilities = (project) => {
5186
5481
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5187
5482
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5188
5483
  if (project.hasTypeScript) capabilities.add("typescript");
5484
+ if (project.hasPreact) {
5485
+ capabilities.add("preact");
5486
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5487
+ }
5189
5488
  return capabilities;
5190
5489
  };
5191
5490
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -5440,6 +5739,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
5440
5739
  if (!publicEnvPrefix) return fallbackRecommendation;
5441
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`;
5442
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
+ };
5443
5749
  const REACT_MODULE_SOURCE = "react";
5444
5750
  const REQUIRE_IDENTIFIER = "require";
5445
5751
  const USE_IDENTIFIER = "use";
@@ -5763,7 +6069,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
5763
6069
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
5764
6070
  if (plugin === "react-hooks-js") return {
5765
6071
  message: REACT_COMPILER_MESSAGE,
5766
- help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
6072
+ help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
5767
6073
  };
5768
6074
  return {
5769
6075
  message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
@@ -6515,17 +6821,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6515
6821
  didFail: false,
6516
6822
  reason: null
6517
6823
  });
6518
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6519
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6520
- rootDirectory: scanDirectory,
6521
- userConfig: resolvedConfig.config
6522
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6523
- yield* Ref.set(deadCodeFailure, {
6524
- didFail: true,
6525
- reason: error.message
6526
- });
6527
- return Stream.empty;
6528
- })))))) : Effect.succeed([]));
6529
6824
  const scanProgress = yield* progressService.start("Scanning...");
6530
6825
  const scanStartTime = Date.now();
6531
6826
  let lastReportedTotalFileCount = 0;
@@ -6555,11 +6850,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6555
6850
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6556
6851
  const lintFailureState = yield* Ref.get(lintFailure);
6557
6852
  yield* afterLint(lintFailureState.didFail);
6558
- if (lintFailureState.didFail) {
6559
- yield* Fiber.interrupt(deadCodeFiber);
6560
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6561
- }
6562
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
6853
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6854
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6855
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6856
+ rootDirectory: scanDirectory,
6857
+ userConfig: resolvedConfig.config
6858
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6859
+ yield* Ref.set(deadCodeFailure, {
6860
+ didFail: true,
6861
+ reason: error.message
6862
+ });
6863
+ return Stream.empty;
6864
+ }))))))));
6563
6865
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6564
6866
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6565
6867
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.9",
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.9"
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.9",
68
- "@react-doctor/core": "0.2.9"
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"