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.
- package/dist/{cli-logger-BRBUS1pE.js → cli-logger-pbFEieEc.js} +127 -87
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +126 -86
- package/package.json +4 -4
|
@@ -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
|
|
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
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
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
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
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
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
|
4706
|
-
|
|
4707
|
-
|
|
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
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
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
|
-
|
|
4762
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4763
|
+
} catch (error) {
|
|
4731
4764
|
settle(() => reject(error));
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
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-
|
|
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
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
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
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
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
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
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
|
|
4737
|
-
|
|
4738
|
-
|
|
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
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
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
|
-
|
|
4793
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4794
|
+
} catch (error) {
|
|
4762
4795
|
settle(() => reject(error));
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
-
"@react-doctor/core": "0.2.
|
|
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"
|