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/{cli-logger-BliQX9s8.js → cli-logger-pbFEieEc.js} +342 -40
- package/dist/cli.js +113 -85
- package/dist/index.d.ts +21 -2
- package/dist/index.js +340 -38
- package/package.json +4 -4
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
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
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
|
|
4551
|
-
|
|
4836
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4837
|
+
rootDirectory,
|
|
4552
4838
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4553
|
-
|
|
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)
|
|
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
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
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.
|
|
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"
|