react-doctor 0.2.8 → 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-C35LXalM.js → cli-logger-BRBUS1pE.js} +303 -31
- package/dist/cli.js +224 -90
- package/dist/index.d.ts +64 -7
- package/dist/index.js +321 -58
- package/package.json +5 -5
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";
|
|
@@ -2067,6 +2068,14 @@ var PackageJsonNotFoundError = class extends Error {
|
|
|
2067
2068
|
this.directory = directory;
|
|
2068
2069
|
}
|
|
2069
2070
|
};
|
|
2071
|
+
var NotADirectoryError = class extends Error {
|
|
2072
|
+
name = "NotADirectoryError";
|
|
2073
|
+
resolvedPath;
|
|
2074
|
+
constructor(resolvedPath, options) {
|
|
2075
|
+
super(`Resolved scan target "${resolvedPath}" is not a directory. Ensure the path exists and points to a project directory, not a file.`, options);
|
|
2076
|
+
this.resolvedPath = resolvedPath;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2070
2079
|
var AmbiguousProjectError = class extends Error {
|
|
2071
2080
|
name = "AmbiguousProjectError";
|
|
2072
2081
|
directory;
|
|
@@ -2077,7 +2086,7 @@ var AmbiguousProjectError = class extends Error {
|
|
|
2077
2086
|
this.candidates = candidates;
|
|
2078
2087
|
}
|
|
2079
2088
|
};
|
|
2080
|
-
const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof AmbiguousProjectError;
|
|
2089
|
+
const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
|
|
2081
2090
|
const isFile = (filePath) => {
|
|
2082
2091
|
try {
|
|
2083
2092
|
return fs.statSync(filePath).isFile();
|
|
@@ -2290,11 +2299,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
2290
2299
|
gatsby: "Gatsby",
|
|
2291
2300
|
expo: "Expo",
|
|
2292
2301
|
"react-native": "React Native",
|
|
2302
|
+
preact: "Preact",
|
|
2293
2303
|
unknown: "React"
|
|
2294
2304
|
};
|
|
2295
2305
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
2296
2306
|
const detectFramework = (dependencies) => {
|
|
2297
2307
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
2308
|
+
if (dependencies.preact && !dependencies.react) return "preact";
|
|
2298
2309
|
return "unknown";
|
|
2299
2310
|
};
|
|
2300
2311
|
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -2758,6 +2769,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
|
|
|
2758
2769
|
}
|
|
2759
2770
|
return false;
|
|
2760
2771
|
};
|
|
2772
|
+
const hasPreact = (packageJson) => {
|
|
2773
|
+
return "preact" in {
|
|
2774
|
+
...packageJson.peerDependencies,
|
|
2775
|
+
...packageJson.dependencies,
|
|
2776
|
+
...packageJson.devDependencies
|
|
2777
|
+
};
|
|
2778
|
+
};
|
|
2761
2779
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2762
2780
|
"@tanstack/react-query",
|
|
2763
2781
|
"@tanstack/query-core",
|
|
@@ -2795,7 +2813,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2795
2813
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2796
2814
|
"react",
|
|
2797
2815
|
"react-native",
|
|
2798
|
-
"next"
|
|
2816
|
+
"next",
|
|
2817
|
+
"preact"
|
|
2799
2818
|
]);
|
|
2800
2819
|
const hasReactDependency = (packageJson) => {
|
|
2801
2820
|
const allDependencies = {
|
|
@@ -2957,12 +2976,47 @@ const discoverProject = (directory) => {
|
|
|
2957
2976
|
hasTypeScript,
|
|
2958
2977
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2959
2978
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
2979
|
+
hasPreact: hasPreact(packageJson),
|
|
2960
2980
|
hasReactNativeWorkspace,
|
|
2961
2981
|
sourceFileCount
|
|
2962
2982
|
};
|
|
2963
2983
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2964
2984
|
return projectInfo;
|
|
2965
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
|
+
};
|
|
2966
3020
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2967
3021
|
if (typeof tailwindVersion !== "string") return null;
|
|
2968
3022
|
const trimmed = tailwindVersion.trim();
|
|
@@ -2993,6 +3047,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
2993
3047
|
return detected.minor >= required.minor;
|
|
2994
3048
|
};
|
|
2995
3049
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3050
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
2996
3051
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
2997
3052
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
2998
3053
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
@@ -4160,8 +4215,10 @@ const resolveScanTarget = (requestedDirectory) => {
|
|
|
4160
4215
|
const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
|
|
4161
4216
|
const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
|
|
4162
4217
|
const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
|
|
4218
|
+
const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
|
|
4219
|
+
if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
|
|
4163
4220
|
return {
|
|
4164
|
-
resolvedDirectory
|
|
4221
|
+
resolvedDirectory,
|
|
4165
4222
|
requestedDirectory: absoluteRequested,
|
|
4166
4223
|
userConfig,
|
|
4167
4224
|
configSourceDirectory,
|
|
@@ -4512,6 +4569,49 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4512
4569
|
return patterns;
|
|
4513
4570
|
};
|
|
4514
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
|
+
`;
|
|
4515
4615
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4516
4616
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4517
4617
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4532,16 +4632,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4532
4632
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4533
4633
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4534
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
|
+
});
|
|
4535
4799
|
const checkDeadCode = async (options) => {
|
|
4536
4800
|
const { rootDirectory, userConfig } = options;
|
|
4537
4801
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4538
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4539
4802
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4540
|
-
const result = await
|
|
4541
|
-
|
|
4803
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4804
|
+
rootDirectory,
|
|
4542
4805
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4543
|
-
|
|
4544
|
-
|
|
4806
|
+
ignorePatterns,
|
|
4807
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4808
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4545
4809
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4546
4810
|
const diagnostics = [];
|
|
4547
4811
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -4645,7 +4909,7 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
|
|
|
4645
4909
|
* pattern in react-doctor-evals' test layers.
|
|
4646
4910
|
*/
|
|
4647
4911
|
static layerInMemory = (tree) => {
|
|
4648
|
-
const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath :
|
|
4912
|
+
const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : `${rootDirectory}/${filePath}`;
|
|
4649
4913
|
return Layer.succeed(Files, Files.of({
|
|
4650
4914
|
readLines: (input) => Effect.sync(() => {
|
|
4651
4915
|
const absolute = resolveAbsolute(input.filePath, input.rootDirectory);
|
|
@@ -4653,17 +4917,17 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
|
|
|
4653
4917
|
return content === void 0 ? null : content.split("\n");
|
|
4654
4918
|
}),
|
|
4655
4919
|
listSourceFiles: (rootDirectory) => Effect.sync(() => {
|
|
4656
|
-
const prefix = rootDirectory.endsWith(
|
|
4920
|
+
const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
|
|
4657
4921
|
const files = [];
|
|
4658
4922
|
for (const absolute of tree.keys()) {
|
|
4659
4923
|
if (!absolute.startsWith(prefix)) continue;
|
|
4660
|
-
files.push(absolute.slice(prefix.length)
|
|
4924
|
+
files.push(absolute.slice(prefix.length));
|
|
4661
4925
|
}
|
|
4662
4926
|
return files;
|
|
4663
4927
|
}),
|
|
4664
4928
|
isFile: (filePath) => Effect.sync(() => tree.has(filePath)),
|
|
4665
4929
|
isDirectory: (filePath) => Effect.sync(() => {
|
|
4666
|
-
const prefix = filePath.endsWith(
|
|
4930
|
+
const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
|
|
4667
4931
|
for (const absolute of tree.keys()) if (absolute.startsWith(prefix)) return true;
|
|
4668
4932
|
return false;
|
|
4669
4933
|
})
|
|
@@ -5165,7 +5429,15 @@ const buildCapabilities = (project) => {
|
|
|
5165
5429
|
capabilities.add(project.framework);
|
|
5166
5430
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5167
5431
|
const reactMajor = project.reactMajorVersion;
|
|
5168
|
-
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
|
+
}
|
|
5169
5441
|
if (project.tailwindVersion !== null) {
|
|
5170
5442
|
capabilities.add("tailwind");
|
|
5171
5443
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5176,6 +5448,10 @@ const buildCapabilities = (project) => {
|
|
|
5176
5448
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5177
5449
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5178
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
|
+
}
|
|
5179
5455
|
return capabilities;
|
|
5180
5456
|
};
|
|
5181
5457
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -5800,7 +6076,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
5800
6076
|
const primaryLabel = diagnostic.labels[0];
|
|
5801
6077
|
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
|
|
5802
6078
|
return {
|
|
5803
|
-
filePath: diagnostic.filename,
|
|
6079
|
+
filePath: diagnostic.filename.replaceAll("\\", "/"),
|
|
5804
6080
|
plugin,
|
|
5805
6081
|
rule,
|
|
5806
6082
|
severity: diagnostic.severity,
|
|
@@ -6505,17 +6781,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6505
6781
|
didFail: false,
|
|
6506
6782
|
reason: null
|
|
6507
6783
|
});
|
|
6508
|
-
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6509
|
-
const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6510
|
-
rootDirectory: scanDirectory,
|
|
6511
|
-
userConfig: resolvedConfig.config
|
|
6512
|
-
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6513
|
-
yield* Ref.set(deadCodeFailure, {
|
|
6514
|
-
didFail: true,
|
|
6515
|
-
reason: error.message
|
|
6516
|
-
});
|
|
6517
|
-
return Stream.empty;
|
|
6518
|
-
})))))) : Effect.succeed([]));
|
|
6519
6784
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6520
6785
|
const scanStartTime = Date.now();
|
|
6521
6786
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6545,11 +6810,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6545
6810
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6546
6811
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6547
6812
|
yield* afterLint(lintFailureState.didFail);
|
|
6548
|
-
if (lintFailureState.didFail)
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
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
|
+
}))))))));
|
|
6553
6825
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6554
6826
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6555
6827
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
@@ -6602,22 +6874,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6602
6874
|
"inspect.isCi": input.isCi,
|
|
6603
6875
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
6604
6876
|
} }));
|
|
6605
|
-
|
|
6606
|
-
* Default layer stack for the production CLI / programmatic API:
|
|
6607
|
-
* real Node-side services for Project / Config / Files / Git / Linter /
|
|
6608
|
-
* DeadCode; HTTP for Score; noop Progress (the CLI overrides with
|
|
6609
|
-
* `Progress.layerOra(...)` for terminal feedback); the silent Reporter
|
|
6610
|
-
* (the orchestrator already returns the diagnostic array via
|
|
6611
|
-
* `Stream.runCollect`).
|
|
6612
|
-
*
|
|
6613
|
-
* Callers tweak by replacing individual layers: `--no-score` swaps
|
|
6614
|
-
* `Score.layerHttp` for `Score.layerOf(null)`; `--no-lint` swaps
|
|
6615
|
-
* `Linter.layerOxlint` for `Linter.layerOf([])`; `--no-dead-code`
|
|
6616
|
-
* swaps `DeadCode.layerNode` for `DeadCode.layerOf([])`; a caller
|
|
6617
|
-
* with a pre-loaded config swaps `Config.layerNode` for
|
|
6618
|
-
* `Config.layerOf(resolved)`.
|
|
6619
|
-
*/
|
|
6620
|
-
const layerInspectLive = Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
6877
|
+
Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
6621
6878
|
const parseNodeVersion = (versionString) => {
|
|
6622
6879
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
6623
6880
|
return {
|
|
@@ -7021,22 +7278,23 @@ import_picocolors.default.red, import_picocolors.default.yellow, import_picocolo
|
|
|
7021
7278
|
const clearAutoSuppressionCaches = () => {};
|
|
7022
7279
|
//#endregion
|
|
7023
7280
|
//#region ../api/dist/index.js
|
|
7024
|
-
const
|
|
7025
|
-
|
|
7026
|
-
const
|
|
7281
|
+
const DEFAULT_LAYER = Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
7282
|
+
const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
7283
|
+
const effectiveConfig = configOverride ?? scanTarget.userConfig;
|
|
7027
7284
|
const includePaths = options.includePaths ?? [];
|
|
7028
|
-
|
|
7285
|
+
return runInspect({
|
|
7029
7286
|
directory: scanTarget.resolvedDirectory,
|
|
7030
7287
|
includePaths,
|
|
7031
|
-
customRulesOnly:
|
|
7032
|
-
respectInlineDisables: options.respectInlineDisables ??
|
|
7033
|
-
adoptExistingLintConfig:
|
|
7034
|
-
ignoredTags: new Set(
|
|
7035
|
-
runDeadCode: options.deadCode ??
|
|
7288
|
+
customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
|
|
7289
|
+
respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
|
|
7290
|
+
adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
|
|
7291
|
+
ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
|
|
7292
|
+
runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
|
|
7036
7293
|
isCi: false,
|
|
7037
7294
|
resolveLocalGithubViewerPermission: true
|
|
7038
7295
|
});
|
|
7039
|
-
|
|
7296
|
+
};
|
|
7297
|
+
const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
7040
7298
|
if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
|
|
7041
7299
|
const skippedChecks = [];
|
|
7042
7300
|
const skippedCheckReasons = {};
|
|
@@ -7050,9 +7308,14 @@ const diagnose = async (directory, options = {}) => {
|
|
|
7050
7308
|
skippedChecks,
|
|
7051
7309
|
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
7052
7310
|
project: output.project,
|
|
7053
|
-
elapsedMilliseconds
|
|
7311
|
+
elapsedMilliseconds
|
|
7054
7312
|
};
|
|
7055
7313
|
};
|
|
7314
|
+
const diagnose = async (directory, options = {}) => {
|
|
7315
|
+
const startTime = globalThis.performance.now();
|
|
7316
|
+
const program = buildInspectProgram(resolveScanTarget(directory), options);
|
|
7317
|
+
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(DEFAULT_LAYER), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
|
|
7318
|
+
};
|
|
7056
7319
|
//#endregion
|
|
7057
7320
|
//#region src/index.ts
|
|
7058
7321
|
const clearCaches = () => {
|
|
@@ -7081,6 +7344,6 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
7081
7344
|
totalElapsedMilliseconds: result.elapsedMilliseconds
|
|
7082
7345
|
});
|
|
7083
7346
|
//#endregion
|
|
7084
|
-
export { AmbiguousProjectError, NoReactDependencyError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
7347
|
+
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
7085
7348
|
|
|
7086
7349
|
//# sourceMappingURL=index.js.map
|
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,21 +58,21 @@
|
|
|
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"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"dev": "vp pack --watch",
|
|
75
|
-
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && NODE_ENV=production vp pack",
|
|
75
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && cross-env NODE_ENV=production vp pack",
|
|
76
76
|
"typecheck": "tsc --noEmit",
|
|
77
77
|
"test": "vp test run"
|
|
78
78
|
}
|