react-doctor 0.2.9 → 0.2.10
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-BRBUS1pE.js} +287 -25
- package/dist/cli.js +113 -85
- package/dist/index.d.ts +14 -2
- package/dist/index.js +285 -23
- package/package.json +4 -4
|
@@ -22,6 +22,7 @@ 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";
|
|
25
26
|
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
26
27
|
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
27
28
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
@@ -2272,11 +2273,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
2272
2273
|
gatsby: "Gatsby",
|
|
2273
2274
|
expo: "Expo",
|
|
2274
2275
|
"react-native": "React Native",
|
|
2276
|
+
preact: "Preact",
|
|
2275
2277
|
unknown: "React"
|
|
2276
2278
|
};
|
|
2277
2279
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
2278
2280
|
const detectFramework = (dependencies) => {
|
|
2279
2281
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
2282
|
+
if (dependencies.preact && !dependencies.react) return "preact";
|
|
2280
2283
|
return "unknown";
|
|
2281
2284
|
};
|
|
2282
2285
|
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -2740,6 +2743,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
|
|
|
2740
2743
|
}
|
|
2741
2744
|
return false;
|
|
2742
2745
|
};
|
|
2746
|
+
const hasPreact = (packageJson) => {
|
|
2747
|
+
return "preact" in {
|
|
2748
|
+
...packageJson.peerDependencies,
|
|
2749
|
+
...packageJson.dependencies,
|
|
2750
|
+
...packageJson.devDependencies
|
|
2751
|
+
};
|
|
2752
|
+
};
|
|
2743
2753
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2744
2754
|
"@tanstack/react-query",
|
|
2745
2755
|
"@tanstack/query-core",
|
|
@@ -2777,7 +2787,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2777
2787
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2778
2788
|
"react",
|
|
2779
2789
|
"react-native",
|
|
2780
|
-
"next"
|
|
2790
|
+
"next",
|
|
2791
|
+
"preact"
|
|
2781
2792
|
]);
|
|
2782
2793
|
const hasReactDependency = (packageJson) => {
|
|
2783
2794
|
const allDependencies = {
|
|
@@ -2936,12 +2947,47 @@ const discoverProject = (directory) => {
|
|
|
2936
2947
|
hasTypeScript,
|
|
2937
2948
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2938
2949
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
2950
|
+
hasPreact: hasPreact(packageJson),
|
|
2939
2951
|
hasReactNativeWorkspace,
|
|
2940
2952
|
sourceFileCount
|
|
2941
2953
|
};
|
|
2942
2954
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2943
2955
|
return projectInfo;
|
|
2944
2956
|
};
|
|
2957
|
+
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
2958
|
+
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
2959
|
+
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
2960
|
+
const parseReactMajorMinor = (reactVersion) => {
|
|
2961
|
+
if (typeof reactVersion !== "string") return null;
|
|
2962
|
+
const trimmed = reactVersion.trim();
|
|
2963
|
+
if (trimmed.length === 0) return null;
|
|
2964
|
+
const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
|
|
2965
|
+
if (lowerBoundsOnly.length === 0) return null;
|
|
2966
|
+
const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
|
|
2967
|
+
if (majorMinorMatch) {
|
|
2968
|
+
const major = Number.parseInt(majorMinorMatch[1], 10);
|
|
2969
|
+
const minor = Number.parseInt(majorMinorMatch[2], 10);
|
|
2970
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
2971
|
+
if (!Number.isFinite(minor) || minor < 0) return null;
|
|
2972
|
+
return {
|
|
2973
|
+
major,
|
|
2974
|
+
minor
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
|
|
2978
|
+
if (!majorOnlyMatch) return null;
|
|
2979
|
+
const major = Number.parseInt(majorOnlyMatch[1], 10);
|
|
2980
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
2981
|
+
return {
|
|
2982
|
+
major,
|
|
2983
|
+
minor: 0
|
|
2984
|
+
};
|
|
2985
|
+
};
|
|
2986
|
+
const isReactAtLeast = (detected, required) => {
|
|
2987
|
+
if (detected === null) return true;
|
|
2988
|
+
if (detected.major !== required.major) return detected.major > required.major;
|
|
2989
|
+
return detected.minor >= required.minor;
|
|
2990
|
+
};
|
|
2945
2991
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2946
2992
|
if (typeof tailwindVersion !== "string") return null;
|
|
2947
2993
|
const trimmed = tailwindVersion.trim();
|
|
@@ -2972,6 +3018,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
2972
3018
|
return detected.minor >= required.minor;
|
|
2973
3019
|
};
|
|
2974
3020
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3021
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
2975
3022
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
2976
3023
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
2977
3024
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -4491,6 +4538,49 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4491
4538
|
return patterns;
|
|
4492
4539
|
};
|
|
4493
4540
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4541
|
+
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4542
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
4543
|
+
|
|
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
|
+
});
|
|
4563
|
+
|
|
4564
|
+
const serializeError = (error) =>
|
|
4565
|
+
error instanceof Error
|
|
4566
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4567
|
+
: { message: String(error) };
|
|
4568
|
+
|
|
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
|
+
})();
|
|
4583
|
+
`;
|
|
4494
4584
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4495
4585
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4496
4586
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4511,16 +4601,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4511
4601
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4512
4602
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4513
4603
|
};
|
|
4604
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4605
|
+
const parseArray = (value, label) => {
|
|
4606
|
+
if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4607
|
+
return value;
|
|
4608
|
+
};
|
|
4609
|
+
const parseString = (value, label) => {
|
|
4610
|
+
if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4611
|
+
return value;
|
|
4612
|
+
};
|
|
4613
|
+
const parseNumber = (value, label) => {
|
|
4614
|
+
if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4615
|
+
return value;
|
|
4616
|
+
};
|
|
4617
|
+
const parseBoolean = (value, label) => {
|
|
4618
|
+
if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4619
|
+
return value;
|
|
4620
|
+
};
|
|
4621
|
+
const parseStringArray = (value, label) => {
|
|
4622
|
+
return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
|
|
4623
|
+
};
|
|
4624
|
+
const parseUnusedFiles = (value) => {
|
|
4625
|
+
const values = parseArray(value, "unusedFiles");
|
|
4626
|
+
const unusedFiles = [];
|
|
4627
|
+
for (const [index, entry] of values.entries()) {
|
|
4628
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
4629
|
+
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
4630
|
+
}
|
|
4631
|
+
return unusedFiles;
|
|
4632
|
+
};
|
|
4633
|
+
const parseUnusedExports = (value) => {
|
|
4634
|
+
const values = parseArray(value, "unusedExports");
|
|
4635
|
+
const unusedExports = [];
|
|
4636
|
+
for (const [index, entry] of values.entries()) {
|
|
4637
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
4638
|
+
unusedExports.push({
|
|
4639
|
+
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
4640
|
+
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
4641
|
+
line: parseNumber(entry.line, `unusedExports[${index}].line`),
|
|
4642
|
+
column: parseNumber(entry.column, `unusedExports[${index}].column`),
|
|
4643
|
+
isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4646
|
+
return unusedExports;
|
|
4647
|
+
};
|
|
4648
|
+
const parseUnusedDependencies = (value) => {
|
|
4649
|
+
const values = parseArray(value, "unusedDependencies");
|
|
4650
|
+
const unusedDependencies = [];
|
|
4651
|
+
for (const [index, entry] of values.entries()) {
|
|
4652
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
4653
|
+
unusedDependencies.push({
|
|
4654
|
+
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
4655
|
+
isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
return unusedDependencies;
|
|
4659
|
+
};
|
|
4660
|
+
const parseCircularDependencies = (value) => {
|
|
4661
|
+
const values = parseArray(value, "circularDependencies");
|
|
4662
|
+
const circularDependencies = [];
|
|
4663
|
+
for (const [index, entry] of values.entries()) {
|
|
4664
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
4665
|
+
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
4666
|
+
}
|
|
4667
|
+
return circularDependencies;
|
|
4668
|
+
};
|
|
4669
|
+
const parseDeadCodeWorkerResult = (value) => {
|
|
4670
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
4671
|
+
return {
|
|
4672
|
+
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
4673
|
+
unusedExports: parseUnusedExports(value.unusedExports),
|
|
4674
|
+
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
4675
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
4676
|
+
};
|
|
4677
|
+
};
|
|
4678
|
+
const parseDeadCodeWorkerError = (value) => {
|
|
4679
|
+
if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
4680
|
+
return {
|
|
4681
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
4682
|
+
message: value.message,
|
|
4683
|
+
...typeof value.stack === "string" ? { stack: value.stack } : {}
|
|
4684
|
+
};
|
|
4685
|
+
};
|
|
4686
|
+
const parseDeadCodeWorkerMessage = (value) => {
|
|
4687
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
4688
|
+
if (value.ok === true) return {
|
|
4689
|
+
ok: true,
|
|
4690
|
+
result: value.result
|
|
4691
|
+
};
|
|
4692
|
+
if (value.ok === false) return {
|
|
4693
|
+
ok: false,
|
|
4694
|
+
error: parseDeadCodeWorkerError(value.error)
|
|
4695
|
+
};
|
|
4696
|
+
throw new Error("Dead-code worker returned an invalid status.");
|
|
4697
|
+
};
|
|
4698
|
+
const buildDeadCodeWorkerError = (workerError) => {
|
|
4699
|
+
const error = new Error(workerError.message);
|
|
4700
|
+
if (workerError.name !== void 0) error.name = workerError.name;
|
|
4701
|
+
if (workerError.stack !== void 0) error.stack = workerError.stack;
|
|
4702
|
+
return error;
|
|
4703
|
+
};
|
|
4704
|
+
const createDeadCodeWorker = (input) => {
|
|
4705
|
+
const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
|
|
4706
|
+
eval: true,
|
|
4707
|
+
workerData: input
|
|
4708
|
+
});
|
|
4709
|
+
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));
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4730
|
+
worker.once("error", (error) => {
|
|
4731
|
+
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
|
+
}),
|
|
4738
|
+
terminate: () => {
|
|
4739
|
+
didSettle = true;
|
|
4740
|
+
worker.removeAllListeners();
|
|
4741
|
+
return worker.terminate();
|
|
4742
|
+
}
|
|
4743
|
+
};
|
|
4744
|
+
};
|
|
4745
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
4746
|
+
let didSettle = false;
|
|
4747
|
+
const timeoutHandle = setTimeout(() => {
|
|
4748
|
+
if (didSettle) return;
|
|
4749
|
+
didSettle = true;
|
|
4750
|
+
handle.terminate?.();
|
|
4751
|
+
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
4752
|
+
}, timeoutMs);
|
|
4753
|
+
timeoutHandle.unref?.();
|
|
4754
|
+
handle.result.then((value) => {
|
|
4755
|
+
if (didSettle) return;
|
|
4756
|
+
didSettle = true;
|
|
4757
|
+
clearTimeout(timeoutHandle);
|
|
4758
|
+
handle.terminate?.();
|
|
4759
|
+
resolve(value);
|
|
4760
|
+
}, (error) => {
|
|
4761
|
+
if (didSettle) return;
|
|
4762
|
+
didSettle = true;
|
|
4763
|
+
clearTimeout(timeoutHandle);
|
|
4764
|
+
handle.terminate?.();
|
|
4765
|
+
reject(error);
|
|
4766
|
+
});
|
|
4767
|
+
});
|
|
4514
4768
|
const checkDeadCode = async (options) => {
|
|
4515
4769
|
const { rootDirectory, userConfig } = options;
|
|
4516
4770
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4517
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4518
4771
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4519
|
-
const result = await
|
|
4520
|
-
|
|
4772
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4773
|
+
rootDirectory,
|
|
4521
4774
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4522
|
-
|
|
4523
|
-
|
|
4775
|
+
ignorePatterns,
|
|
4776
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4777
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4524
4778
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4525
4779
|
const diagnostics = [];
|
|
4526
4780
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -5144,7 +5398,15 @@ const buildCapabilities = (project) => {
|
|
|
5144
5398
|
capabilities.add(project.framework);
|
|
5145
5399
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5146
5400
|
const reactMajor = project.reactMajorVersion;
|
|
5147
|
-
if (reactMajor !== null)
|
|
5401
|
+
if (reactMajor !== null) {
|
|
5402
|
+
for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
|
|
5403
|
+
if (reactMajor >= 19) {
|
|
5404
|
+
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5405
|
+
major: 19,
|
|
5406
|
+
minor: 2
|
|
5407
|
+
})) capabilities.add("react:19.2");
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5148
5410
|
if (project.tailwindVersion !== null) {
|
|
5149
5411
|
capabilities.add("tailwind");
|
|
5150
5412
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5155,6 +5417,10 @@ const buildCapabilities = (project) => {
|
|
|
5155
5417
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5156
5418
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5157
5419
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5420
|
+
if (project.hasPreact) {
|
|
5421
|
+
capabilities.add("preact");
|
|
5422
|
+
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5423
|
+
}
|
|
5158
5424
|
return capabilities;
|
|
5159
5425
|
};
|
|
5160
5426
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -6484,17 +6750,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6484
6750
|
didFail: false,
|
|
6485
6751
|
reason: null
|
|
6486
6752
|
});
|
|
6487
|
-
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6488
|
-
const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6489
|
-
rootDirectory: scanDirectory,
|
|
6490
|
-
userConfig: resolvedConfig.config
|
|
6491
|
-
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6492
|
-
yield* Ref.set(deadCodeFailure, {
|
|
6493
|
-
didFail: true,
|
|
6494
|
-
reason: error.message
|
|
6495
|
-
});
|
|
6496
|
-
return Stream.empty;
|
|
6497
|
-
})))))) : Effect.succeed([]));
|
|
6498
6753
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6499
6754
|
const scanStartTime = Date.now();
|
|
6500
6755
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6524,11 +6779,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6524
6779
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6525
6780
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6526
6781
|
yield* afterLint(lintFailureState.didFail);
|
|
6527
|
-
if (lintFailureState.didFail)
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6782
|
+
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
6783
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6784
|
+
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6785
|
+
rootDirectory: scanDirectory,
|
|
6786
|
+
userConfig: resolvedConfig.config
|
|
6787
|
+
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6788
|
+
yield* Ref.set(deadCodeFailure, {
|
|
6789
|
+
didFail: true,
|
|
6790
|
+
reason: error.message
|
|
6791
|
+
});
|
|
6792
|
+
return Stream.empty;
|
|
6793
|
+
}))))))));
|
|
6532
6794
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6533
6795
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6534
6796
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
@@ -7023,7 +7285,7 @@ var cli_logger_exports = /* @__PURE__ */ __exportAll({ cliLogger: () => cliLogge
|
|
|
7023
7285
|
/**
|
|
7024
7286
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
7025
7287
|
* the imperative CLI helper files (`select-projects`, `run-explain`,
|
|
7026
|
-
* `install-
|
|
7288
|
+
* `install-react-doctor`, the legacy paths in `cli/commands/inspect.ts`)
|
|
7027
7289
|
* that aren't yet Effect-typed. Every call drains into a single
|
|
7028
7290
|
* `Console.*` Effect via `Effect.runSync`, so the underlying logging
|
|
7029
7291
|
* pipeline is identical to the canonical `yield* Console.log(...)`
|
|
@@ -7056,4 +7318,4 @@ const cliLogger = {
|
|
|
7056
7318
|
//#endregion
|
|
7057
7319
|
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 };
|
|
7058
7320
|
|
|
7059
|
-
//# sourceMappingURL=cli-logger-
|
|
7321
|
+
//# sourceMappingURL=cli-logger-BRBUS1pE.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-BRBUS1pE.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";
|
|
@@ -6320,11 +6320,11 @@ const colorizeByScore = (text, score) => {
|
|
|
6320
6320
|
return highlighter.error(text);
|
|
6321
6321
|
};
|
|
6322
6322
|
//#endregion
|
|
6323
|
+
//#region src/cli/utils/constants.ts
|
|
6324
|
+
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6325
|
+
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6326
|
+
//#endregion
|
|
6323
6327
|
//#region src/cli/utils/render-score-header.ts
|
|
6324
|
-
const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
|
|
6325
|
-
const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
|
|
6326
|
-
const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
|
|
6327
|
-
const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
|
|
6328
6328
|
const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
|
|
6329
6329
|
const RAINBOW_GRADIENT_WIDTH = 80;
|
|
6330
6330
|
const RAINBOW_OKLCH_LIGHTNESS = .638;
|
|
@@ -6433,8 +6433,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
|
|
|
6433
6433
|
};
|
|
6434
6434
|
const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
|
|
6435
6435
|
const isPerfectScore = score === 100;
|
|
6436
|
-
for (let frame = 0; frame <=
|
|
6437
|
-
const progress = easeOutCubic(frame /
|
|
6436
|
+
for (let frame = 0; frame <= 40; frame += 1) {
|
|
6437
|
+
const progress = easeOutCubic(frame / 40);
|
|
6438
6438
|
const animatedScore = Math.round(score * progress);
|
|
6439
6439
|
if (isPerfectScore) {
|
|
6440
6440
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
|
|
@@ -6444,16 +6444,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6444
6444
|
frame,
|
|
6445
6445
|
projectName
|
|
6446
6446
|
})}`);
|
|
6447
|
-
if (frame <
|
|
6447
|
+
if (frame < 40) yield* sleep(50);
|
|
6448
6448
|
continue;
|
|
6449
6449
|
}
|
|
6450
6450
|
const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
|
|
6451
6451
|
const animatedBarLine = buildScoreBar(animatedScore, score);
|
|
6452
6452
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
|
|
6453
|
-
if (frame <
|
|
6453
|
+
if (frame < 40) yield* sleep(50);
|
|
6454
6454
|
}
|
|
6455
6455
|
if (!isPerfectScore) return;
|
|
6456
|
-
for (let frame = 0; frame <
|
|
6456
|
+
for (let frame = 0; frame < 16; frame += 1) {
|
|
6457
6457
|
yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
|
|
6458
6458
|
score,
|
|
6459
6459
|
displayScore: score,
|
|
@@ -6461,9 +6461,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6461
6461
|
frame,
|
|
6462
6462
|
projectName
|
|
6463
6463
|
})}`);
|
|
6464
|
-
yield* sleep(
|
|
6464
|
+
yield* sleep(50);
|
|
6465
6465
|
}
|
|
6466
|
-
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label,
|
|
6466
|
+
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
|
|
6467
6467
|
});
|
|
6468
6468
|
const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
|
|
6469
6469
|
const isPerfectScore = scoreResult.score === 100;
|
|
@@ -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.10";
|
|
6670
6670
|
//#endregion
|
|
6671
6671
|
//#region src/inspect.ts
|
|
6672
6672
|
const silentConsole = makeNoopConsole();
|
|
@@ -6854,10 +6854,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
|
|
|
6854
6854
|
return buildResult();
|
|
6855
6855
|
});
|
|
6856
6856
|
//#endregion
|
|
6857
|
-
//#region src/cli/utils/constants.ts
|
|
6858
|
-
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6859
|
-
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6860
|
-
//#endregion
|
|
6861
6857
|
//#region src/cli/utils/get-staged-files.ts
|
|
6862
6858
|
const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
|
|
6863
6859
|
const getStagedSourceFiles = async (directory) => {
|
|
@@ -7416,7 +7412,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
7416
7412
|
return true;
|
|
7417
7413
|
};
|
|
7418
7414
|
const shouldPromptInstallSetup = (options) => {
|
|
7419
|
-
if (!options.hasScoredScan) return false;
|
|
7415
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7420
7416
|
if (options.isJsonMode) return false;
|
|
7421
7417
|
if (options.isScoreOnly) return false;
|
|
7422
7418
|
if (options.isStaged) return false;
|
|
@@ -7426,13 +7422,13 @@ const shouldPromptInstallSetup = (options) => {
|
|
|
7426
7422
|
return !hasDoctorScript(options.projectRoot);
|
|
7427
7423
|
};
|
|
7428
7424
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
7429
|
-
if (options.
|
|
7425
|
+
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
7430
7426
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
7431
|
-
for (const scanDirectory of options.
|
|
7427
|
+
for (const scanDirectory of options.scanDirectories) {
|
|
7432
7428
|
const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
|
|
7433
7429
|
packageDirectories.add(packageDirectory);
|
|
7434
7430
|
}
|
|
7435
|
-
if (packageDirectories.size !== 1) return
|
|
7431
|
+
if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
|
|
7436
7432
|
return [...packageDirectories][0] ?? null;
|
|
7437
7433
|
};
|
|
7438
7434
|
const defaultWait = (milliseconds) => new Promise((resolve) => {
|
|
@@ -7467,7 +7463,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7467
7463
|
return;
|
|
7468
7464
|
}
|
|
7469
7465
|
try {
|
|
7470
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7466
|
+
const { cliLogger } = await import("./cli-logger-BRBUS1pE.js").then((n) => n.n);
|
|
7471
7467
|
cliLogger.warn(message);
|
|
7472
7468
|
} catch {}
|
|
7473
7469
|
};
|
|
@@ -7483,7 +7479,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7483
7479
|
writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
|
|
7484
7480
|
return;
|
|
7485
7481
|
}
|
|
7486
|
-
const install = options.install ?? (await Promise.resolve().then(() =>
|
|
7482
|
+
const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
|
|
7487
7483
|
const previousExitCode = process.exitCode;
|
|
7488
7484
|
let setupExitCode;
|
|
7489
7485
|
try {
|
|
@@ -7502,7 +7498,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7502
7498
|
}
|
|
7503
7499
|
};
|
|
7504
7500
|
const shouldShowAgentInstallHint = (options) => {
|
|
7505
|
-
if (!options.hasScoredScan) return false;
|
|
7501
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7506
7502
|
if (options.isJsonMode) return false;
|
|
7507
7503
|
if (options.isScoreOnly) return false;
|
|
7508
7504
|
if (options.isStaged) return false;
|
|
@@ -7567,7 +7563,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
|
|
|
7567
7563
|
const { scanScope } = await prompts({
|
|
7568
7564
|
type: "select",
|
|
7569
7565
|
name: "scanScope",
|
|
7570
|
-
message: "
|
|
7566
|
+
message: "Choose what to scan",
|
|
7571
7567
|
choices: [{
|
|
7572
7568
|
title: "Full codebase",
|
|
7573
7569
|
value: "full"
|
|
@@ -7709,7 +7705,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
7709
7705
|
}
|
|
7710
7706
|
if (packages.length === 0) return [rootDirectory];
|
|
7711
7707
|
if (packages.length === 1) {
|
|
7712
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7708
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
|
|
7713
7709
|
return [packages[0].directory];
|
|
7714
7710
|
}
|
|
7715
7711
|
if (projectFlag) return resolveProjectFlag(projectFlag, packages);
|
|
@@ -7733,13 +7729,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
|
|
|
7733
7729
|
return resolvedDirectories;
|
|
7734
7730
|
};
|
|
7735
7731
|
const printDiscoveredProjects = (packages) => {
|
|
7736
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7732
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
|
|
7737
7733
|
};
|
|
7738
7734
|
const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
7739
7735
|
const { selectedDirectories } = await prompts({
|
|
7740
7736
|
type: "multiselect",
|
|
7741
7737
|
name: "selectedDirectories",
|
|
7742
|
-
message: "Select projects
|
|
7738
|
+
message: "Select projects",
|
|
7743
7739
|
choices: workspacePackages.map((workspacePackage) => ({
|
|
7744
7740
|
title: workspacePackage.name,
|
|
7745
7741
|
description: path.relative(rootDirectory, workspacePackage.directory),
|
|
@@ -7996,13 +7992,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
7996
7992
|
});
|
|
7997
7993
|
const setupProjectRoot = resolveInstallSetupProjectRoot({
|
|
7998
7994
|
scanRoot: resolvedDirectory,
|
|
7999
|
-
|
|
7995
|
+
scanDirectories: projectDirectories
|
|
8000
7996
|
});
|
|
8001
7997
|
if (setupProjectRoot !== null) {
|
|
8002
|
-
const
|
|
7998
|
+
const hasCompletedScan = completedScans.length > 0;
|
|
8003
7999
|
await promptInstallSetup({
|
|
8004
8000
|
projectRoot: setupProjectRoot,
|
|
8005
|
-
|
|
8001
|
+
hasCompletedScan,
|
|
8006
8002
|
issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
|
|
8007
8003
|
isJsonMode,
|
|
8008
8004
|
isScoreOnly,
|
|
@@ -8011,7 +8007,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
8011
8007
|
});
|
|
8012
8008
|
if (shouldShowAgentInstallHint({
|
|
8013
8009
|
projectRoot: setupProjectRoot,
|
|
8014
|
-
|
|
8010
|
+
hasCompletedScan,
|
|
8015
8011
|
isJsonMode,
|
|
8016
8012
|
isScoreOnly,
|
|
8017
8013
|
isStaged: Boolean(flags.staged)
|
|
@@ -8643,8 +8639,12 @@ const installReactDoctorGitHook = (options) => {
|
|
|
8643
8639
|
return installDirectGitHook(options);
|
|
8644
8640
|
};
|
|
8645
8641
|
//#endregion
|
|
8646
|
-
//#region src/cli/utils/install-
|
|
8647
|
-
var
|
|
8642
|
+
//#region src/cli/utils/install-react-doctor.ts
|
|
8643
|
+
var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
|
|
8644
|
+
const SETUP_OPTION_GIT_HOOK = "git-hook";
|
|
8645
|
+
const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
|
|
8646
|
+
const SETUP_OPTION_WORKFLOW = "workflow";
|
|
8647
|
+
const SETUP_OPTION_SKIP = "skip";
|
|
8648
8648
|
const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
|
|
8649
8649
|
"ghooks",
|
|
8650
8650
|
"git-hooks-js",
|
|
@@ -8825,7 +8825,30 @@ const getSkillSourceDirectory = () => {
|
|
|
8825
8825
|
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
8826
8826
|
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
8827
8827
|
};
|
|
8828
|
-
const
|
|
8828
|
+
const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
|
|
8829
|
+
const buildWorkflowContent = () => [
|
|
8830
|
+
"name: React Doctor",
|
|
8831
|
+
"",
|
|
8832
|
+
"on:",
|
|
8833
|
+
" pull_request:",
|
|
8834
|
+
" branches: [main]",
|
|
8835
|
+
"",
|
|
8836
|
+
"permissions:",
|
|
8837
|
+
" contents: read",
|
|
8838
|
+
" pull-requests: write",
|
|
8839
|
+
"",
|
|
8840
|
+
"jobs:",
|
|
8841
|
+
" react-doctor:",
|
|
8842
|
+
" runs-on: ubuntu-latest",
|
|
8843
|
+
" steps:",
|
|
8844
|
+
" - uses: actions/checkout@v4",
|
|
8845
|
+
" - uses: millionco/react-doctor@main",
|
|
8846
|
+
" with:",
|
|
8847
|
+
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8848
|
+
" diff: main",
|
|
8849
|
+
""
|
|
8850
|
+
].join("\n");
|
|
8851
|
+
const runInstallReactDoctor = async (options = {}) => {
|
|
8829
8852
|
const requestedProjectRoot = options.projectRoot ?? process.cwd();
|
|
8830
8853
|
const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
|
|
8831
8854
|
const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
|
|
@@ -8846,7 +8869,8 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8846
8869
|
const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
|
|
8847
8870
|
const gitHookPath = gitHookTarget?.hookPath;
|
|
8848
8871
|
const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
|
|
8849
|
-
const
|
|
8872
|
+
const prompt = options.prompt ?? prompts;
|
|
8873
|
+
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
8850
8874
|
type: "multiselect",
|
|
8851
8875
|
name: "agents",
|
|
8852
8876
|
message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
|
|
@@ -8859,13 +8883,48 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8859
8883
|
min: 1
|
|
8860
8884
|
}, promptOptions)).agents ?? [];
|
|
8861
8885
|
if (selectedAgents.length === 0) return;
|
|
8862
|
-
const
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8886
|
+
const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
|
|
8887
|
+
const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
|
|
8888
|
+
const hasExistingWorkflows = existsSync(workflowsDirectory);
|
|
8889
|
+
const canInstallWorkflow = !existsSync(workflowTargetPath);
|
|
8890
|
+
const setupActionChoices = [
|
|
8891
|
+
...gitHookPath === null || gitHookPath === void 0 ? [] : [{
|
|
8892
|
+
title: "Pre-commit hook",
|
|
8893
|
+
description: "Check staged changes before each commit",
|
|
8894
|
+
value: SETUP_OPTION_GIT_HOOK,
|
|
8895
|
+
selected: true
|
|
8896
|
+
}],
|
|
8897
|
+
...canInstallNativeAgentHooks(selectedAgents) ? [{
|
|
8898
|
+
title: "Agent hooks",
|
|
8899
|
+
description: "Ask Claude Code or Cursor to scan after code edits",
|
|
8900
|
+
value: SETUP_OPTION_AGENT_HOOKS,
|
|
8901
|
+
selected: Boolean(options.agentHooks)
|
|
8902
|
+
}] : [],
|
|
8903
|
+
...canInstallWorkflow ? [{
|
|
8904
|
+
title: "GitHub Actions workflow",
|
|
8905
|
+
description: "Scan pull requests in CI",
|
|
8906
|
+
value: SETUP_OPTION_WORKFLOW,
|
|
8907
|
+
selected: hasExistingWorkflows
|
|
8908
|
+
}] : []
|
|
8909
|
+
];
|
|
8910
|
+
const setupChoices = setupActionChoices.length === 0 ? [] : [{
|
|
8911
|
+
title: "Skip optional setup",
|
|
8912
|
+
description: "Install only the agent skill and package setup",
|
|
8913
|
+
value: SETUP_OPTION_SKIP,
|
|
8914
|
+
selected: false
|
|
8915
|
+
}, ...setupActionChoices];
|
|
8916
|
+
const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
|
|
8917
|
+
type: "multiselect",
|
|
8918
|
+
name: "setupOptions",
|
|
8919
|
+
message: "Select additional React Doctor setup:",
|
|
8920
|
+
choices: setupChoices,
|
|
8921
|
+
instructions: false
|
|
8922
|
+
}, promptOptions)).setupOptions ?? [];
|
|
8923
|
+
const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
|
|
8924
|
+
const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
|
|
8925
|
+
const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
|
|
8926
|
+
const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
|
|
8927
|
+
const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
|
|
8869
8928
|
if (options.dryRun) {
|
|
8870
8929
|
cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
|
|
8871
8930
|
for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
|
|
@@ -8874,6 +8933,7 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8874
8933
|
cliLogger.dim(" Dev dependency: react-doctor");
|
|
8875
8934
|
if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
|
|
8876
8935
|
if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
|
|
8936
|
+
if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
|
|
8877
8937
|
return;
|
|
8878
8938
|
}
|
|
8879
8939
|
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
@@ -8921,47 +8981,15 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8921
8981
|
throw error;
|
|
8922
8982
|
}
|
|
8923
8983
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
}, promptOptions);
|
|
8934
|
-
if (shouldInstallWorkflow) {
|
|
8935
|
-
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
8936
|
-
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
8937
|
-
try {
|
|
8938
|
-
writeFileSync(workflowTargetPath, [
|
|
8939
|
-
"name: React Doctor",
|
|
8940
|
-
"",
|
|
8941
|
-
"on:",
|
|
8942
|
-
" pull_request:",
|
|
8943
|
-
" branches: [main]",
|
|
8944
|
-
"",
|
|
8945
|
-
"permissions:",
|
|
8946
|
-
" contents: read",
|
|
8947
|
-
" pull-requests: write",
|
|
8948
|
-
"",
|
|
8949
|
-
"jobs:",
|
|
8950
|
-
" react-doctor:",
|
|
8951
|
-
" runs-on: ubuntu-latest",
|
|
8952
|
-
" steps:",
|
|
8953
|
-
" - uses: actions/checkout@v4",
|
|
8954
|
-
" - uses: millionco/react-doctor@main",
|
|
8955
|
-
" with:",
|
|
8956
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8957
|
-
" diff: main",
|
|
8958
|
-
""
|
|
8959
|
-
].join("\n"));
|
|
8960
|
-
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
8961
|
-
} catch (error) {
|
|
8962
|
-
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
8963
|
-
throw error;
|
|
8964
|
-
}
|
|
8984
|
+
if (shouldInstallWorkflow) {
|
|
8985
|
+
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
8986
|
+
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
8987
|
+
try {
|
|
8988
|
+
writeFileSync(workflowTargetPath, buildWorkflowContent());
|
|
8989
|
+
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
8990
|
+
} catch (error) {
|
|
8991
|
+
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
8992
|
+
throw error;
|
|
8965
8993
|
}
|
|
8966
8994
|
}
|
|
8967
8995
|
};
|
|
@@ -8971,7 +8999,7 @@ const installAction = async (options, command) => {
|
|
|
8971
8999
|
Effect.runSync(printBrandedHeader);
|
|
8972
9000
|
try {
|
|
8973
9001
|
const parentOptions = command?.parent?.opts?.();
|
|
8974
|
-
await
|
|
9002
|
+
await runInstallReactDoctor({
|
|
8975
9003
|
yes: options.yes ?? parentOptions?.yes,
|
|
8976
9004
|
dryRun: options.dryRun,
|
|
8977
9005
|
agentHooks: options.agentHooks,
|
package/dist/index.d.ts
CHANGED
|
@@ -232,7 +232,9 @@ interface ReactDoctorConfig {
|
|
|
232
232
|
* `categories` field, but keyed by React Doctor's display
|
|
233
233
|
* categories (`"Server"`, `"React Native"`, `"Architecture"`,
|
|
234
234
|
* `"Bundle Size"`, `"State & Effects"`, `"Security"`,
|
|
235
|
-
* `"Accessibility"`, `"Performance"`, `"Correctness"`,
|
|
235
|
+
* `"Accessibility"`, `"Performance"`, `"Correctness"`,
|
|
236
|
+
* `"Next.js"`, `"Preact"`, `"TanStack Query"`,
|
|
237
|
+
* `"TanStack Start"`, …).
|
|
236
238
|
*
|
|
237
239
|
* ```json
|
|
238
240
|
* { "categories": { "React Native": "warn", "Server": "off" } }
|
|
@@ -296,7 +298,7 @@ interface Diagnostic {
|
|
|
296
298
|
}
|
|
297
299
|
//#endregion
|
|
298
300
|
//#region src/types/project-info.d.ts
|
|
299
|
-
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "unknown";
|
|
301
|
+
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "preact" | "unknown";
|
|
300
302
|
interface ProjectInfo {
|
|
301
303
|
rootDirectory: string;
|
|
302
304
|
projectName: string;
|
|
@@ -307,6 +309,16 @@ interface ProjectInfo {
|
|
|
307
309
|
hasTypeScript: boolean;
|
|
308
310
|
hasReactCompiler: boolean;
|
|
309
311
|
hasTanStackQuery: boolean;
|
|
312
|
+
/**
|
|
313
|
+
* `true` when `preact` is declared anywhere in the project's
|
|
314
|
+
* dependency manifest. Drives the `preact` capability in
|
|
315
|
+
* `buildCapabilities`, which gates every `preact-*` rule. Modeled
|
|
316
|
+
* on `hasTanStackQuery` rather than the `framework` field because
|
|
317
|
+
* the dominant Preact setup today is Preact-on-Vite — those
|
|
318
|
+
* projects classify as `framework: "vite"` for build-tool reasons
|
|
319
|
+
* but still need Preact-specific rules to fire.
|
|
320
|
+
*/
|
|
321
|
+
hasPreact: boolean;
|
|
310
322
|
/**
|
|
311
323
|
* `true` when the project (or any of its workspace packages) declares
|
|
312
324
|
* React Native or Expo as a dependency. Enables the `react-native`
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ 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";
|
|
24
25
|
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
25
26
|
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
26
27
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
@@ -2298,11 +2299,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
2298
2299
|
gatsby: "Gatsby",
|
|
2299
2300
|
expo: "Expo",
|
|
2300
2301
|
"react-native": "React Native",
|
|
2302
|
+
preact: "Preact",
|
|
2301
2303
|
unknown: "React"
|
|
2302
2304
|
};
|
|
2303
2305
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
2304
2306
|
const detectFramework = (dependencies) => {
|
|
2305
2307
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
2308
|
+
if (dependencies.preact && !dependencies.react) return "preact";
|
|
2306
2309
|
return "unknown";
|
|
2307
2310
|
};
|
|
2308
2311
|
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -2766,6 +2769,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
|
|
|
2766
2769
|
}
|
|
2767
2770
|
return false;
|
|
2768
2771
|
};
|
|
2772
|
+
const hasPreact = (packageJson) => {
|
|
2773
|
+
return "preact" in {
|
|
2774
|
+
...packageJson.peerDependencies,
|
|
2775
|
+
...packageJson.dependencies,
|
|
2776
|
+
...packageJson.devDependencies
|
|
2777
|
+
};
|
|
2778
|
+
};
|
|
2769
2779
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2770
2780
|
"@tanstack/react-query",
|
|
2771
2781
|
"@tanstack/query-core",
|
|
@@ -2803,7 +2813,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2803
2813
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2804
2814
|
"react",
|
|
2805
2815
|
"react-native",
|
|
2806
|
-
"next"
|
|
2816
|
+
"next",
|
|
2817
|
+
"preact"
|
|
2807
2818
|
]);
|
|
2808
2819
|
const hasReactDependency = (packageJson) => {
|
|
2809
2820
|
const allDependencies = {
|
|
@@ -2965,12 +2976,47 @@ const discoverProject = (directory) => {
|
|
|
2965
2976
|
hasTypeScript,
|
|
2966
2977
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2967
2978
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
2979
|
+
hasPreact: hasPreact(packageJson),
|
|
2968
2980
|
hasReactNativeWorkspace,
|
|
2969
2981
|
sourceFileCount
|
|
2970
2982
|
};
|
|
2971
2983
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2972
2984
|
return projectInfo;
|
|
2973
2985
|
};
|
|
2986
|
+
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
2987
|
+
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
2988
|
+
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
2989
|
+
const parseReactMajorMinor = (reactVersion) => {
|
|
2990
|
+
if (typeof reactVersion !== "string") return null;
|
|
2991
|
+
const trimmed = reactVersion.trim();
|
|
2992
|
+
if (trimmed.length === 0) return null;
|
|
2993
|
+
const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
|
|
2994
|
+
if (lowerBoundsOnly.length === 0) return null;
|
|
2995
|
+
const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
|
|
2996
|
+
if (majorMinorMatch) {
|
|
2997
|
+
const major = Number.parseInt(majorMinorMatch[1], 10);
|
|
2998
|
+
const minor = Number.parseInt(majorMinorMatch[2], 10);
|
|
2999
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3000
|
+
if (!Number.isFinite(minor) || minor < 0) return null;
|
|
3001
|
+
return {
|
|
3002
|
+
major,
|
|
3003
|
+
minor
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
|
|
3007
|
+
if (!majorOnlyMatch) return null;
|
|
3008
|
+
const major = Number.parseInt(majorOnlyMatch[1], 10);
|
|
3009
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3010
|
+
return {
|
|
3011
|
+
major,
|
|
3012
|
+
minor: 0
|
|
3013
|
+
};
|
|
3014
|
+
};
|
|
3015
|
+
const isReactAtLeast = (detected, required) => {
|
|
3016
|
+
if (detected === null) return true;
|
|
3017
|
+
if (detected.major !== required.major) return detected.major > required.major;
|
|
3018
|
+
return detected.minor >= required.minor;
|
|
3019
|
+
};
|
|
2974
3020
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2975
3021
|
if (typeof tailwindVersion !== "string") return null;
|
|
2976
3022
|
const trimmed = tailwindVersion.trim();
|
|
@@ -3001,6 +3047,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
3001
3047
|
return detected.minor >= required.minor;
|
|
3002
3048
|
};
|
|
3003
3049
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3050
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
3004
3051
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
3005
3052
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
3006
3053
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
@@ -4522,6 +4569,49 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4522
4569
|
return patterns;
|
|
4523
4570
|
};
|
|
4524
4571
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4572
|
+
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4573
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
4574
|
+
|
|
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
|
+
});
|
|
4594
|
+
|
|
4595
|
+
const serializeError = (error) =>
|
|
4596
|
+
error instanceof Error
|
|
4597
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4598
|
+
: { message: String(error) };
|
|
4599
|
+
|
|
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
|
+
})();
|
|
4614
|
+
`;
|
|
4525
4615
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4526
4616
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4527
4617
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4542,16 +4632,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4542
4632
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4543
4633
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4544
4634
|
};
|
|
4635
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4636
|
+
const parseArray = (value, label) => {
|
|
4637
|
+
if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4638
|
+
return value;
|
|
4639
|
+
};
|
|
4640
|
+
const parseString = (value, label) => {
|
|
4641
|
+
if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4642
|
+
return value;
|
|
4643
|
+
};
|
|
4644
|
+
const parseNumber = (value, label) => {
|
|
4645
|
+
if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4646
|
+
return value;
|
|
4647
|
+
};
|
|
4648
|
+
const parseBoolean = (value, label) => {
|
|
4649
|
+
if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4650
|
+
return value;
|
|
4651
|
+
};
|
|
4652
|
+
const parseStringArray = (value, label) => {
|
|
4653
|
+
return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
|
|
4654
|
+
};
|
|
4655
|
+
const parseUnusedFiles = (value) => {
|
|
4656
|
+
const values = parseArray(value, "unusedFiles");
|
|
4657
|
+
const unusedFiles = [];
|
|
4658
|
+
for (const [index, entry] of values.entries()) {
|
|
4659
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
4660
|
+
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
4661
|
+
}
|
|
4662
|
+
return unusedFiles;
|
|
4663
|
+
};
|
|
4664
|
+
const parseUnusedExports = (value) => {
|
|
4665
|
+
const values = parseArray(value, "unusedExports");
|
|
4666
|
+
const unusedExports = [];
|
|
4667
|
+
for (const [index, entry] of values.entries()) {
|
|
4668
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
4669
|
+
unusedExports.push({
|
|
4670
|
+
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
4671
|
+
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
4672
|
+
line: parseNumber(entry.line, `unusedExports[${index}].line`),
|
|
4673
|
+
column: parseNumber(entry.column, `unusedExports[${index}].column`),
|
|
4674
|
+
isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4677
|
+
return unusedExports;
|
|
4678
|
+
};
|
|
4679
|
+
const parseUnusedDependencies = (value) => {
|
|
4680
|
+
const values = parseArray(value, "unusedDependencies");
|
|
4681
|
+
const unusedDependencies = [];
|
|
4682
|
+
for (const [index, entry] of values.entries()) {
|
|
4683
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
4684
|
+
unusedDependencies.push({
|
|
4685
|
+
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
4686
|
+
isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
return unusedDependencies;
|
|
4690
|
+
};
|
|
4691
|
+
const parseCircularDependencies = (value) => {
|
|
4692
|
+
const values = parseArray(value, "circularDependencies");
|
|
4693
|
+
const circularDependencies = [];
|
|
4694
|
+
for (const [index, entry] of values.entries()) {
|
|
4695
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
4696
|
+
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
4697
|
+
}
|
|
4698
|
+
return circularDependencies;
|
|
4699
|
+
};
|
|
4700
|
+
const parseDeadCodeWorkerResult = (value) => {
|
|
4701
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
4702
|
+
return {
|
|
4703
|
+
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
4704
|
+
unusedExports: parseUnusedExports(value.unusedExports),
|
|
4705
|
+
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
4706
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
4707
|
+
};
|
|
4708
|
+
};
|
|
4709
|
+
const parseDeadCodeWorkerError = (value) => {
|
|
4710
|
+
if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
4711
|
+
return {
|
|
4712
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
4713
|
+
message: value.message,
|
|
4714
|
+
...typeof value.stack === "string" ? { stack: value.stack } : {}
|
|
4715
|
+
};
|
|
4716
|
+
};
|
|
4717
|
+
const parseDeadCodeWorkerMessage = (value) => {
|
|
4718
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
4719
|
+
if (value.ok === true) return {
|
|
4720
|
+
ok: true,
|
|
4721
|
+
result: value.result
|
|
4722
|
+
};
|
|
4723
|
+
if (value.ok === false) return {
|
|
4724
|
+
ok: false,
|
|
4725
|
+
error: parseDeadCodeWorkerError(value.error)
|
|
4726
|
+
};
|
|
4727
|
+
throw new Error("Dead-code worker returned an invalid status.");
|
|
4728
|
+
};
|
|
4729
|
+
const buildDeadCodeWorkerError = (workerError) => {
|
|
4730
|
+
const error = new Error(workerError.message);
|
|
4731
|
+
if (workerError.name !== void 0) error.name = workerError.name;
|
|
4732
|
+
if (workerError.stack !== void 0) error.stack = workerError.stack;
|
|
4733
|
+
return error;
|
|
4734
|
+
};
|
|
4735
|
+
const createDeadCodeWorker = (input) => {
|
|
4736
|
+
const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
|
|
4737
|
+
eval: true,
|
|
4738
|
+
workerData: input
|
|
4739
|
+
});
|
|
4740
|
+
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));
|
|
4759
|
+
}
|
|
4760
|
+
});
|
|
4761
|
+
worker.once("error", (error) => {
|
|
4762
|
+
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
|
+
}),
|
|
4769
|
+
terminate: () => {
|
|
4770
|
+
didSettle = true;
|
|
4771
|
+
worker.removeAllListeners();
|
|
4772
|
+
return worker.terminate();
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4775
|
+
};
|
|
4776
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
4777
|
+
let didSettle = false;
|
|
4778
|
+
const timeoutHandle = setTimeout(() => {
|
|
4779
|
+
if (didSettle) return;
|
|
4780
|
+
didSettle = true;
|
|
4781
|
+
handle.terminate?.();
|
|
4782
|
+
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
4783
|
+
}, timeoutMs);
|
|
4784
|
+
timeoutHandle.unref?.();
|
|
4785
|
+
handle.result.then((value) => {
|
|
4786
|
+
if (didSettle) return;
|
|
4787
|
+
didSettle = true;
|
|
4788
|
+
clearTimeout(timeoutHandle);
|
|
4789
|
+
handle.terminate?.();
|
|
4790
|
+
resolve(value);
|
|
4791
|
+
}, (error) => {
|
|
4792
|
+
if (didSettle) return;
|
|
4793
|
+
didSettle = true;
|
|
4794
|
+
clearTimeout(timeoutHandle);
|
|
4795
|
+
handle.terminate?.();
|
|
4796
|
+
reject(error);
|
|
4797
|
+
});
|
|
4798
|
+
});
|
|
4545
4799
|
const checkDeadCode = async (options) => {
|
|
4546
4800
|
const { rootDirectory, userConfig } = options;
|
|
4547
4801
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4548
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4549
4802
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4550
|
-
const result = await
|
|
4551
|
-
|
|
4803
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4804
|
+
rootDirectory,
|
|
4552
4805
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4553
|
-
|
|
4554
|
-
|
|
4806
|
+
ignorePatterns,
|
|
4807
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4808
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4555
4809
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4556
4810
|
const diagnostics = [];
|
|
4557
4811
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -5175,7 +5429,15 @@ const buildCapabilities = (project) => {
|
|
|
5175
5429
|
capabilities.add(project.framework);
|
|
5176
5430
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5177
5431
|
const reactMajor = project.reactMajorVersion;
|
|
5178
|
-
if (reactMajor !== null)
|
|
5432
|
+
if (reactMajor !== null) {
|
|
5433
|
+
for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
|
|
5434
|
+
if (reactMajor >= 19) {
|
|
5435
|
+
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5436
|
+
major: 19,
|
|
5437
|
+
minor: 2
|
|
5438
|
+
})) capabilities.add("react:19.2");
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5179
5441
|
if (project.tailwindVersion !== null) {
|
|
5180
5442
|
capabilities.add("tailwind");
|
|
5181
5443
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5186,6 +5448,10 @@ const buildCapabilities = (project) => {
|
|
|
5186
5448
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5187
5449
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5188
5450
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5451
|
+
if (project.hasPreact) {
|
|
5452
|
+
capabilities.add("preact");
|
|
5453
|
+
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5454
|
+
}
|
|
5189
5455
|
return capabilities;
|
|
5190
5456
|
};
|
|
5191
5457
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -6515,17 +6781,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6515
6781
|
didFail: false,
|
|
6516
6782
|
reason: null
|
|
6517
6783
|
});
|
|
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
6784
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6530
6785
|
const scanStartTime = Date.now();
|
|
6531
6786
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6555,11 +6810,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6555
6810
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6556
6811
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6557
6812
|
yield* afterLint(lintFailureState.didFail);
|
|
6558
|
-
if (lintFailureState.didFail)
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6813
|
+
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
6814
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6815
|
+
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6816
|
+
rootDirectory: scanDirectory,
|
|
6817
|
+
userConfig: resolvedConfig.config
|
|
6818
|
+
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6819
|
+
yield* Ref.set(deadCodeFailure, {
|
|
6820
|
+
didFail: true,
|
|
6821
|
+
reason: error.message
|
|
6822
|
+
});
|
|
6823
|
+
return Stream.empty;
|
|
6824
|
+
}))))))));
|
|
6563
6825
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6564
6826
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6565
6827
|
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.10",
|
|
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.10"
|
|
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.10",
|
|
68
|
+
"@react-doctor/core": "0.2.10"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^20.19.0 || >=22.12.0"
|