react-doctor 0.2.9 → 0.2.11-dev.f036b0f
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-Df45H6Lw.js} +430 -77
- package/dist/cli.js +131 -96
- package/dist/index.d.ts +23 -2
- package/dist/index.js +433 -79
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2298,11 +2298,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
2298
2298
|
gatsby: "Gatsby",
|
|
2299
2299
|
expo: "Expo",
|
|
2300
2300
|
"react-native": "React Native",
|
|
2301
|
+
preact: "Preact",
|
|
2301
2302
|
unknown: "React"
|
|
2302
2303
|
};
|
|
2303
2304
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
2304
2305
|
const detectFramework = (dependencies) => {
|
|
2305
2306
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
2307
|
+
if (dependencies.preact && !dependencies.react) return "preact";
|
|
2306
2308
|
return "unknown";
|
|
2307
2309
|
};
|
|
2308
2310
|
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -2721,6 +2723,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2721
2723
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2722
2724
|
};
|
|
2723
2725
|
};
|
|
2726
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
|
|
2727
|
+
if (predicate(rootPackageJson)) return true;
|
|
2728
|
+
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2729
|
+
if (patterns.length === 0) return false;
|
|
2730
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2731
|
+
for (const pattern of patterns) {
|
|
2732
|
+
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2733
|
+
for (const workspaceDirectory of directories) {
|
|
2734
|
+
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2735
|
+
visitedDirectories.add(workspaceDirectory);
|
|
2736
|
+
if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return false;
|
|
2740
|
+
};
|
|
2724
2741
|
const NAMES = new Set([
|
|
2725
2742
|
"react-native",
|
|
2726
2743
|
"react-native-tvos",
|
|
@@ -2751,20 +2768,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2751
2768
|
if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
|
|
2752
2769
|
return false;
|
|
2753
2770
|
};
|
|
2754
|
-
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) =>
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
for (const workspaceDirectory of directories) {
|
|
2762
|
-
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2763
|
-
visitedDirectories.add(workspaceDirectory);
|
|
2764
|
-
if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
return false;
|
|
2771
|
+
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2772
|
+
const getPreactVersion = (packageJson) => {
|
|
2773
|
+
return {
|
|
2774
|
+
...packageJson.peerDependencies,
|
|
2775
|
+
...packageJson.dependencies,
|
|
2776
|
+
...packageJson.devDependencies
|
|
2777
|
+
}.preact ?? null;
|
|
2768
2778
|
};
|
|
2769
2779
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2770
2780
|
"@tanstack/react-query",
|
|
@@ -2779,6 +2789,16 @@ const hasTanStackQuery = (packageJson) => {
|
|
|
2779
2789
|
};
|
|
2780
2790
|
return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
2781
2791
|
};
|
|
2792
|
+
const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
|
|
2793
|
+
const isPackageJsonReanimatedAware = (packageJson) => {
|
|
2794
|
+
const allDependencies = {
|
|
2795
|
+
...packageJson.peerDependencies,
|
|
2796
|
+
...packageJson.dependencies,
|
|
2797
|
+
...packageJson.devDependencies,
|
|
2798
|
+
...packageJson.optionalDependencies
|
|
2799
|
+
};
|
|
2800
|
+
return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
|
|
2801
|
+
};
|
|
2782
2802
|
const hasUpperBoundOnlyPeerRange = (range) => {
|
|
2783
2803
|
if (typeof range !== "string") return false;
|
|
2784
2804
|
const normalizedRange = normalizeDependencyVersion(range);
|
|
@@ -2803,7 +2823,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2803
2823
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2804
2824
|
"react",
|
|
2805
2825
|
"react-native",
|
|
2806
|
-
"next"
|
|
2826
|
+
"next",
|
|
2827
|
+
"preact"
|
|
2807
2828
|
]);
|
|
2808
2829
|
const hasReactDependency = (packageJson) => {
|
|
2809
2830
|
const allDependencies = {
|
|
@@ -2864,12 +2885,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
|
|
|
2864
2885
|
const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
|
|
2865
2886
|
return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
|
|
2866
2887
|
};
|
|
2888
|
+
const NON_PROJECT_DIRECTORIES = new Set([
|
|
2889
|
+
"AppData",
|
|
2890
|
+
"Application Data",
|
|
2891
|
+
"Library"
|
|
2892
|
+
]);
|
|
2893
|
+
const MAX_SCAN_DEPTH = 6;
|
|
2867
2894
|
const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
2868
2895
|
const packages = [];
|
|
2869
|
-
const pendingDirectories = [
|
|
2896
|
+
const pendingDirectories = [{
|
|
2897
|
+
directory: rootDirectory,
|
|
2898
|
+
depth: 0
|
|
2899
|
+
}];
|
|
2870
2900
|
while (pendingDirectories.length > 0) {
|
|
2871
|
-
const
|
|
2872
|
-
if (!
|
|
2901
|
+
const current = pendingDirectories.pop();
|
|
2902
|
+
if (!current) continue;
|
|
2903
|
+
const { directory: currentDirectory, depth } = current;
|
|
2873
2904
|
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
2874
2905
|
if (isFile(packageJsonPath)) {
|
|
2875
2906
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -2881,10 +2912,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
|
2881
2912
|
});
|
|
2882
2913
|
}
|
|
2883
2914
|
}
|
|
2915
|
+
if (depth >= MAX_SCAN_DEPTH) continue;
|
|
2884
2916
|
const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
|
|
2885
2917
|
for (const entry of entries) {
|
|
2886
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
2887
|
-
pendingDirectories.push(
|
|
2918
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
|
|
2919
|
+
pendingDirectories.push({
|
|
2920
|
+
directory: path.join(currentDirectory, entry.name),
|
|
2921
|
+
depth: depth + 1
|
|
2922
|
+
});
|
|
2888
2923
|
}
|
|
2889
2924
|
}
|
|
2890
2925
|
return packages;
|
|
@@ -2955,6 +2990,8 @@ const discoverProject = (directory) => {
|
|
|
2955
2990
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
2956
2991
|
const sourceFileCount = countSourceFiles(directory);
|
|
2957
2992
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
2993
|
+
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
2994
|
+
const preactVersion = getPreactVersion(packageJson);
|
|
2958
2995
|
const projectInfo = {
|
|
2959
2996
|
rootDirectory: directory,
|
|
2960
2997
|
projectName,
|
|
@@ -2965,12 +3002,50 @@ const discoverProject = (directory) => {
|
|
|
2965
3002
|
hasTypeScript,
|
|
2966
3003
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2967
3004
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
3005
|
+
preactVersion,
|
|
3006
|
+
preactMajorVersion: parseReactMajor(preactVersion),
|
|
2968
3007
|
hasReactNativeWorkspace,
|
|
3008
|
+
hasReanimated,
|
|
2969
3009
|
sourceFileCount
|
|
2970
3010
|
};
|
|
2971
3011
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2972
3012
|
return projectInfo;
|
|
2973
3013
|
};
|
|
3014
|
+
const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
3015
|
+
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
3016
|
+
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
3017
|
+
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
3018
|
+
const parseReactMajorMinor = (reactVersion) => {
|
|
3019
|
+
if (typeof reactVersion !== "string") return null;
|
|
3020
|
+
const trimmed = reactVersion.trim();
|
|
3021
|
+
if (trimmed.length === 0) return null;
|
|
3022
|
+
const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
|
|
3023
|
+
if (lowerBoundsOnly.length === 0) return null;
|
|
3024
|
+
const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
|
|
3025
|
+
if (majorMinorMatch) {
|
|
3026
|
+
const major = Number.parseInt(majorMinorMatch[1], 10);
|
|
3027
|
+
const minor = Number.parseInt(majorMinorMatch[2], 10);
|
|
3028
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3029
|
+
if (!Number.isFinite(minor) || minor < 0) return null;
|
|
3030
|
+
return {
|
|
3031
|
+
major,
|
|
3032
|
+
minor
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
|
|
3036
|
+
if (!majorOnlyMatch) return null;
|
|
3037
|
+
const major = Number.parseInt(majorOnlyMatch[1], 10);
|
|
3038
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3039
|
+
return {
|
|
3040
|
+
major,
|
|
3041
|
+
minor: 0
|
|
3042
|
+
};
|
|
3043
|
+
};
|
|
3044
|
+
const isReactAtLeast = (detected, required) => {
|
|
3045
|
+
if (detected === null) return true;
|
|
3046
|
+
if (detected.major !== required.major) return detected.major > required.major;
|
|
3047
|
+
return detected.minor >= required.minor;
|
|
3048
|
+
};
|
|
2974
3049
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2975
3050
|
if (typeof tailwindVersion !== "string") return null;
|
|
2976
3051
|
const trimmed = tailwindVersion.trim();
|
|
@@ -3001,6 +3076,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
3001
3076
|
return detected.minor >= required.minor;
|
|
3002
3077
|
};
|
|
3003
3078
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3079
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
3004
3080
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
3005
3081
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
3006
3082
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
@@ -3901,17 +3977,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
3901
3977
|
headers
|
|
3902
3978
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
3903
3979
|
}).pipe(Effect.orDie));
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3980
|
+
/**
|
|
3981
|
+
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
3982
|
+
* startup so the eval harness can raise the budget under sandbox
|
|
3983
|
+
* microVMs without recompiling react-doctor. Tests override via
|
|
3984
|
+
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
3985
|
+
*/
|
|
3986
|
+
var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
3907
3987
|
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
3908
3988
|
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3909
3989
|
const parsed = Number(raw);
|
|
3910
3990
|
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3911
3991
|
return parsed;
|
|
3912
|
-
} });
|
|
3913
|
-
|
|
3914
|
-
|
|
3992
|
+
} }) {};
|
|
3993
|
+
/**
|
|
3994
|
+
* Hard cap on combined stdout+stderr bytes per oxlint batch. The
|
|
3995
|
+
* subprocess gets SIGKILL'd if it produces more; the recovery path
|
|
3996
|
+
* suggests narrowing the scan with --diff. Override via Layer in
|
|
3997
|
+
* tests that exercise the cap behavior.
|
|
3998
|
+
*/
|
|
3999
|
+
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
3915
4000
|
const DIAGNOSTIC_SURFACES = [
|
|
3916
4001
|
"cli",
|
|
3917
4002
|
"prComment",
|
|
@@ -4522,6 +4607,59 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4522
4607
|
return patterns;
|
|
4523
4608
|
};
|
|
4524
4609
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4610
|
+
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4611
|
+
const inputChunks = [];
|
|
4612
|
+
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
4613
|
+
process.stdin.on("end", () => {
|
|
4614
|
+
const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
|
|
4615
|
+
|
|
4616
|
+
const normalizeResult = (result) => ({
|
|
4617
|
+
unusedFiles: result.unusedFiles.map((unusedFile) => ({
|
|
4618
|
+
path: unusedFile.path,
|
|
4619
|
+
})),
|
|
4620
|
+
unusedExports: result.unusedExports.map((unusedExport) => ({
|
|
4621
|
+
path: unusedExport.path,
|
|
4622
|
+
name: unusedExport.name,
|
|
4623
|
+
line: unusedExport.line,
|
|
4624
|
+
column: unusedExport.column,
|
|
4625
|
+
isTypeOnly: unusedExport.isTypeOnly,
|
|
4626
|
+
})),
|
|
4627
|
+
unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
|
|
4628
|
+
name: unusedDependency.name,
|
|
4629
|
+
isDevDependency: unusedDependency.isDevDependency,
|
|
4630
|
+
})),
|
|
4631
|
+
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
4632
|
+
files: cycle.files,
|
|
4633
|
+
})),
|
|
4634
|
+
});
|
|
4635
|
+
|
|
4636
|
+
const serializeError = (error) =>
|
|
4637
|
+
error instanceof Error
|
|
4638
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4639
|
+
: { message: String(error) };
|
|
4640
|
+
|
|
4641
|
+
const emit = (message) => {
|
|
4642
|
+
process.stdout.write(JSON.stringify(message), () => process.exit(0));
|
|
4643
|
+
};
|
|
4644
|
+
|
|
4645
|
+
(async () => {
|
|
4646
|
+
try {
|
|
4647
|
+
const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
|
|
4648
|
+
const config = {
|
|
4649
|
+
rootDir: workerInput.rootDirectory,
|
|
4650
|
+
...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
|
|
4651
|
+
...(workerInput.ignorePatterns.length > 0
|
|
4652
|
+
? { ignorePatterns: workerInput.ignorePatterns }
|
|
4653
|
+
: {}),
|
|
4654
|
+
};
|
|
4655
|
+
const result = await analyze(defineConfig(config));
|
|
4656
|
+
emit({ ok: true, result: normalizeResult(result) });
|
|
4657
|
+
} catch (error) {
|
|
4658
|
+
emit({ ok: false, error: serializeError(error) });
|
|
4659
|
+
}
|
|
4660
|
+
})();
|
|
4661
|
+
});
|
|
4662
|
+
`;
|
|
4525
4663
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4526
4664
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4527
4665
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4542,16 +4680,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4542
4680
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4543
4681
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4544
4682
|
};
|
|
4683
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4684
|
+
const parseArray = (value, label) => {
|
|
4685
|
+
if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4686
|
+
return value;
|
|
4687
|
+
};
|
|
4688
|
+
const parseString = (value, label) => {
|
|
4689
|
+
if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4690
|
+
return value;
|
|
4691
|
+
};
|
|
4692
|
+
const parseNumber = (value, label) => {
|
|
4693
|
+
if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4694
|
+
return value;
|
|
4695
|
+
};
|
|
4696
|
+
const parseBoolean = (value, label) => {
|
|
4697
|
+
if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4698
|
+
return value;
|
|
4699
|
+
};
|
|
4700
|
+
const parseStringArray = (value, label) => {
|
|
4701
|
+
return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
|
|
4702
|
+
};
|
|
4703
|
+
const parseUnusedFiles = (value) => {
|
|
4704
|
+
const values = parseArray(value, "unusedFiles");
|
|
4705
|
+
const unusedFiles = [];
|
|
4706
|
+
for (const [index, entry] of values.entries()) {
|
|
4707
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
4708
|
+
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
4709
|
+
}
|
|
4710
|
+
return unusedFiles;
|
|
4711
|
+
};
|
|
4712
|
+
const parseUnusedExports = (value) => {
|
|
4713
|
+
const values = parseArray(value, "unusedExports");
|
|
4714
|
+
const unusedExports = [];
|
|
4715
|
+
for (const [index, entry] of values.entries()) {
|
|
4716
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
4717
|
+
unusedExports.push({
|
|
4718
|
+
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
4719
|
+
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
4720
|
+
line: parseNumber(entry.line, `unusedExports[${index}].line`),
|
|
4721
|
+
column: parseNumber(entry.column, `unusedExports[${index}].column`),
|
|
4722
|
+
isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
|
|
4723
|
+
});
|
|
4724
|
+
}
|
|
4725
|
+
return unusedExports;
|
|
4726
|
+
};
|
|
4727
|
+
const parseUnusedDependencies = (value) => {
|
|
4728
|
+
const values = parseArray(value, "unusedDependencies");
|
|
4729
|
+
const unusedDependencies = [];
|
|
4730
|
+
for (const [index, entry] of values.entries()) {
|
|
4731
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
4732
|
+
unusedDependencies.push({
|
|
4733
|
+
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
4734
|
+
isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
return unusedDependencies;
|
|
4738
|
+
};
|
|
4739
|
+
const parseCircularDependencies = (value) => {
|
|
4740
|
+
const values = parseArray(value, "circularDependencies");
|
|
4741
|
+
const circularDependencies = [];
|
|
4742
|
+
for (const [index, entry] of values.entries()) {
|
|
4743
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
4744
|
+
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
4745
|
+
}
|
|
4746
|
+
return circularDependencies;
|
|
4747
|
+
};
|
|
4748
|
+
const parseDeadCodeWorkerResult = (value) => {
|
|
4749
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
4750
|
+
return {
|
|
4751
|
+
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
4752
|
+
unusedExports: parseUnusedExports(value.unusedExports),
|
|
4753
|
+
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
4754
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
4755
|
+
};
|
|
4756
|
+
};
|
|
4757
|
+
const parseDeadCodeWorkerError = (value) => {
|
|
4758
|
+
if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
4759
|
+
return {
|
|
4760
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
4761
|
+
message: value.message,
|
|
4762
|
+
...typeof value.stack === "string" ? { stack: value.stack } : {}
|
|
4763
|
+
};
|
|
4764
|
+
};
|
|
4765
|
+
const parseDeadCodeWorkerMessage = (value) => {
|
|
4766
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
4767
|
+
if (value.ok === true) return {
|
|
4768
|
+
ok: true,
|
|
4769
|
+
result: value.result
|
|
4770
|
+
};
|
|
4771
|
+
if (value.ok === false) return {
|
|
4772
|
+
ok: false,
|
|
4773
|
+
error: parseDeadCodeWorkerError(value.error)
|
|
4774
|
+
};
|
|
4775
|
+
throw new Error("Dead-code worker returned an invalid status.");
|
|
4776
|
+
};
|
|
4777
|
+
const buildDeadCodeWorkerError = (workerError) => {
|
|
4778
|
+
const error = new Error(workerError.message);
|
|
4779
|
+
if (workerError.name !== void 0) error.name = workerError.name;
|
|
4780
|
+
if (workerError.stack !== void 0) error.stack = workerError.stack;
|
|
4781
|
+
return error;
|
|
4782
|
+
};
|
|
4783
|
+
const createDeadCodeWorker = (input) => {
|
|
4784
|
+
const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
|
|
4785
|
+
stdio: [
|
|
4786
|
+
"pipe",
|
|
4787
|
+
"pipe",
|
|
4788
|
+
"pipe"
|
|
4789
|
+
],
|
|
4790
|
+
windowsHide: true
|
|
4791
|
+
});
|
|
4792
|
+
const stdoutChunks = [];
|
|
4793
|
+
const stderrChunks = [];
|
|
4794
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
4795
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
4796
|
+
let didSettle = false;
|
|
4797
|
+
const result = new Promise((resolve, reject) => {
|
|
4798
|
+
const settle = (callback) => {
|
|
4799
|
+
if (didSettle) return;
|
|
4800
|
+
didSettle = true;
|
|
4801
|
+
callback();
|
|
4802
|
+
};
|
|
4803
|
+
child.once("error", (error) => {
|
|
4804
|
+
settle(() => reject(error));
|
|
4805
|
+
});
|
|
4806
|
+
child.once("close", (exitCode) => {
|
|
4807
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
4808
|
+
if (stdout.length === 0) {
|
|
4809
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
4810
|
+
settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
try {
|
|
4814
|
+
const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
|
|
4815
|
+
if (parsedMessage.ok) {
|
|
4816
|
+
settle(() => resolve(parsedMessage.result));
|
|
4817
|
+
return;
|
|
4818
|
+
}
|
|
4819
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
settle(() => reject(error));
|
|
4822
|
+
}
|
|
4823
|
+
});
|
|
4824
|
+
});
|
|
4825
|
+
child.stdin.on("error", () => {});
|
|
4826
|
+
child.stdin.end(JSON.stringify(input));
|
|
4827
|
+
return {
|
|
4828
|
+
result,
|
|
4829
|
+
terminate: () => {
|
|
4830
|
+
didSettle = true;
|
|
4831
|
+
child.kill("SIGKILL");
|
|
4832
|
+
}
|
|
4833
|
+
};
|
|
4834
|
+
};
|
|
4835
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
4836
|
+
let didSettle = false;
|
|
4837
|
+
const timeoutHandle = setTimeout(() => {
|
|
4838
|
+
if (didSettle) return;
|
|
4839
|
+
didSettle = true;
|
|
4840
|
+
handle.terminate?.();
|
|
4841
|
+
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
4842
|
+
}, timeoutMs);
|
|
4843
|
+
timeoutHandle.unref?.();
|
|
4844
|
+
handle.result.then((value) => {
|
|
4845
|
+
if (didSettle) return;
|
|
4846
|
+
didSettle = true;
|
|
4847
|
+
clearTimeout(timeoutHandle);
|
|
4848
|
+
handle.terminate?.();
|
|
4849
|
+
resolve(value);
|
|
4850
|
+
}, (error) => {
|
|
4851
|
+
if (didSettle) return;
|
|
4852
|
+
didSettle = true;
|
|
4853
|
+
clearTimeout(timeoutHandle);
|
|
4854
|
+
handle.terminate?.();
|
|
4855
|
+
reject(error);
|
|
4856
|
+
});
|
|
4857
|
+
});
|
|
4545
4858
|
const checkDeadCode = async (options) => {
|
|
4546
4859
|
const { rootDirectory, userConfig } = options;
|
|
4547
4860
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4548
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4549
4861
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4550
|
-
const result = await
|
|
4551
|
-
|
|
4862
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4863
|
+
rootDirectory,
|
|
4552
4864
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4553
|
-
|
|
4554
|
-
|
|
4865
|
+
ignorePatterns,
|
|
4866
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4867
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4555
4868
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4556
4869
|
const diagnostics = [];
|
|
4557
4870
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -4757,8 +5070,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4757
5070
|
env: input.env,
|
|
4758
5071
|
extendEnv: true
|
|
4759
5072
|
}));
|
|
5073
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
5074
|
+
const stdoutByteCount = yield* Ref.make(0);
|
|
5075
|
+
const stdoutStream = maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(Stream.tap((chunk) => Ref.updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(Effect.flatMap((total) => total > maxStdoutBytes ? Effect.fail(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
5076
|
+
args: [...input.args],
|
|
5077
|
+
directory: input.directory,
|
|
5078
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
5079
|
+
}) })) : Effect.void))));
|
|
4760
5080
|
const [stdout, stderr, status] = yield* Effect.all([
|
|
4761
|
-
Stream.mkString(Stream.decodeText(
|
|
5081
|
+
Stream.mkString(Stream.decodeText(stdoutStream)),
|
|
4762
5082
|
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
4763
5083
|
handle.exitCode
|
|
4764
5084
|
], { concurrency: 3 });
|
|
@@ -4920,7 +5240,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4920
5240
|
if (result.status !== 0) return [];
|
|
4921
5241
|
return splitNullSeparated(result.stdout);
|
|
4922
5242
|
})),
|
|
4923
|
-
showStagedContent: (directory, relativePath) =>
|
|
5243
|
+
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
5244
|
+
command: "git",
|
|
5245
|
+
args: ["show", `:${relativePath}`],
|
|
5246
|
+
directory,
|
|
5247
|
+
maxStdoutBytes: options?.maxBufferBytes
|
|
5248
|
+
}).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
|
|
4924
5249
|
grep: (input) => Effect.gen(function* () {
|
|
4925
5250
|
const args = ["grep"];
|
|
4926
5251
|
if (input.listMatchingFiles ?? true) args.push("-l");
|
|
@@ -4928,7 +5253,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4928
5253
|
if (input.extendedRegexp ?? false) args.push("-E");
|
|
4929
5254
|
args.push(input.pattern);
|
|
4930
5255
|
if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
|
|
4931
|
-
const result = yield*
|
|
5256
|
+
const result = yield* runCommand({
|
|
5257
|
+
command: "git",
|
|
5258
|
+
args,
|
|
5259
|
+
directory: input.directory,
|
|
5260
|
+
maxStdoutBytes: input.maxBufferBytes
|
|
5261
|
+
});
|
|
4932
5262
|
if (result.status === 128) return null;
|
|
4933
5263
|
return {
|
|
4934
5264
|
status: result.status,
|
|
@@ -5175,7 +5505,16 @@ const buildCapabilities = (project) => {
|
|
|
5175
5505
|
capabilities.add(project.framework);
|
|
5176
5506
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5177
5507
|
const reactMajor = project.reactMajorVersion;
|
|
5178
|
-
if (reactMajor !== null)
|
|
5508
|
+
if (reactMajor !== null) {
|
|
5509
|
+
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
5510
|
+
for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
|
|
5511
|
+
if (reactMajor >= 19) {
|
|
5512
|
+
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5513
|
+
major: 19,
|
|
5514
|
+
minor: 2
|
|
5515
|
+
})) capabilities.add("react:19.2");
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5179
5518
|
if (project.tailwindVersion !== null) {
|
|
5180
5519
|
capabilities.add("tailwind");
|
|
5181
5520
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5186,6 +5525,15 @@ const buildCapabilities = (project) => {
|
|
|
5186
5525
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5187
5526
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5188
5527
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5528
|
+
if (project.preactVersion !== null) {
|
|
5529
|
+
capabilities.add("preact");
|
|
5530
|
+
const preactMajor = project.preactMajorVersion;
|
|
5531
|
+
if (preactMajor !== null) {
|
|
5532
|
+
const cappedPreactMajor = Math.min(preactMajor, 20);
|
|
5533
|
+
for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
|
|
5534
|
+
}
|
|
5535
|
+
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5536
|
+
}
|
|
5189
5537
|
return capabilities;
|
|
5190
5538
|
};
|
|
5191
5539
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -5440,6 +5788,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
|
|
|
5440
5788
|
if (!publicEnvPrefix) return fallbackRecommendation;
|
|
5441
5789
|
return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
|
|
5442
5790
|
};
|
|
5791
|
+
const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
|
|
5792
|
+
const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
5793
|
+
if (rule !== "immutability") return help;
|
|
5794
|
+
if (!project.hasReanimated) return help;
|
|
5795
|
+
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5796
|
+
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5797
|
+
};
|
|
5443
5798
|
const REACT_MODULE_SOURCE = "react";
|
|
5444
5799
|
const REQUIRE_IDENTIFIER = "require";
|
|
5445
5800
|
const USE_IDENTIFIER = "use";
|
|
@@ -5763,7 +6118,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
|
|
|
5763
6118
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
5764
6119
|
if (plugin === "react-hooks-js") return {
|
|
5765
6120
|
message: REACT_COMPILER_MESSAGE,
|
|
5766
|
-
help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
|
|
6121
|
+
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
5767
6122
|
};
|
|
5768
6123
|
return {
|
|
5769
6124
|
message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
|
|
@@ -5832,13 +6187,6 @@ const SANITIZED_ENV = (() => {
|
|
|
5832
6187
|
}
|
|
5833
6188
|
return sanitized;
|
|
5834
6189
|
})();
|
|
5835
|
-
const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
5836
|
-
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
5837
|
-
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
5838
|
-
const parsed = Number(raw);
|
|
5839
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
5840
|
-
return parsed;
|
|
5841
|
-
})();
|
|
5842
6190
|
/**
|
|
5843
6191
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
5844
6192
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -5855,7 +6203,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
|
5855
6203
|
* The first three are splittable (the caller's binary-split retry
|
|
5856
6204
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
5857
6205
|
*/
|
|
5858
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
6206
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
5859
6207
|
const child = spawn(nodeBinaryPath, args, {
|
|
5860
6208
|
cwd: rootDirectory,
|
|
5861
6209
|
env: SANITIZED_ENV
|
|
@@ -5864,9 +6212,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5864
6212
|
child.kill("SIGKILL");
|
|
5865
6213
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5866
6214
|
kind: "timeout",
|
|
5867
|
-
detail: `${
|
|
6215
|
+
detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
|
|
5868
6216
|
}) }));
|
|
5869
|
-
},
|
|
6217
|
+
}, spawnTimeoutMs);
|
|
5870
6218
|
timeoutHandle.unref?.();
|
|
5871
6219
|
const stdoutBuffers = [];
|
|
5872
6220
|
const stderrBuffers = [];
|
|
@@ -5876,7 +6224,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5876
6224
|
const killIfTooLarge = (incomingBytes, isStdout) => {
|
|
5877
6225
|
if (isStdout) stdoutByteCount += incomingBytes;
|
|
5878
6226
|
else stderrByteCount += incomingBytes;
|
|
5879
|
-
if (stdoutByteCount + stderrByteCount >
|
|
6227
|
+
if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
|
|
5880
6228
|
didKillForSize = true;
|
|
5881
6229
|
child.kill("SIGKILL");
|
|
5882
6230
|
return true;
|
|
@@ -5902,7 +6250,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5902
6250
|
if (didKillForSize) {
|
|
5903
6251
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5904
6252
|
kind: "output-too-large",
|
|
5905
|
-
detail: `exceeded ${
|
|
6253
|
+
detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
|
|
5906
6254
|
}) }));
|
|
5907
6255
|
return;
|
|
5908
6256
|
}
|
|
@@ -5943,7 +6291,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5943
6291
|
* with a slimmer config in that case.
|
|
5944
6292
|
*/
|
|
5945
6293
|
const spawnLintBatches = async (input) => {
|
|
5946
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
|
|
6294
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
5947
6295
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
5948
6296
|
const allDiagnostics = [];
|
|
5949
6297
|
const droppedFiles = [];
|
|
@@ -5951,7 +6299,7 @@ const spawnLintBatches = async (input) => {
|
|
|
5951
6299
|
const spawnLintBatch = async (batch) => {
|
|
5952
6300
|
const batchArgs = [...baseArgs, ...batch];
|
|
5953
6301
|
try {
|
|
5954
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
|
|
6302
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
5955
6303
|
} catch (error) {
|
|
5956
6304
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
5957
6305
|
if (batch.length <= 1) {
|
|
@@ -6054,13 +6402,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
6054
6402
|
* 6. always restore disable directives + clean up the temp dir
|
|
6055
6403
|
*/
|
|
6056
6404
|
const runOxlint = async (options) => {
|
|
6057
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
|
|
6405
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
6058
6406
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
6059
6407
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
6060
6408
|
validateRuleRegistration();
|
|
6061
6409
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
6062
|
-
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6063
|
-
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6064
6410
|
const pluginPath = resolvePluginPath();
|
|
6065
6411
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
6066
6412
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
@@ -6075,6 +6421,8 @@ const runOxlint = async (options) => {
|
|
|
6075
6421
|
userPlugins
|
|
6076
6422
|
});
|
|
6077
6423
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
6424
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6425
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6078
6426
|
try {
|
|
6079
6427
|
const baseArgs = [
|
|
6080
6428
|
resolveOxlintBinary(),
|
|
@@ -6101,7 +6449,9 @@ const runOxlint = async (options) => {
|
|
|
6101
6449
|
nodeBinaryPath,
|
|
6102
6450
|
project,
|
|
6103
6451
|
onPartialFailure,
|
|
6104
|
-
onFileProgress: options.onFileProgress
|
|
6452
|
+
onFileProgress: options.onFileProgress,
|
|
6453
|
+
spawnTimeoutMs,
|
|
6454
|
+
outputMaxBytes
|
|
6105
6455
|
});
|
|
6106
6456
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6107
6457
|
try {
|
|
@@ -6167,6 +6517,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6167
6517
|
*/
|
|
6168
6518
|
static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
|
|
6169
6519
|
const partialFailures = yield* LintPartialFailures;
|
|
6520
|
+
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
6521
|
+
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
6170
6522
|
const collectedFailures = [];
|
|
6171
6523
|
const diagnostics = yield* Effect.tryPromise({
|
|
6172
6524
|
try: () => runOxlint({
|
|
@@ -6183,7 +6535,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6183
6535
|
onPartialFailure: (reason) => {
|
|
6184
6536
|
collectedFailures.push(reason);
|
|
6185
6537
|
},
|
|
6186
|
-
onFileProgress: input.onFileProgress
|
|
6538
|
+
onFileProgress: input.onFileProgress,
|
|
6539
|
+
spawnTimeoutMs,
|
|
6540
|
+
outputMaxBytes
|
|
6187
6541
|
}),
|
|
6188
6542
|
catch: ensureReactDoctorError
|
|
6189
6543
|
});
|
|
@@ -6481,7 +6835,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6481
6835
|
const resolvedConfig = yield* configService.resolve(input.directory);
|
|
6482
6836
|
const scanDirectory = resolvedConfig.resolvedDirectory;
|
|
6483
6837
|
const project = yield* projectService.discover(scanDirectory);
|
|
6484
|
-
if (project
|
|
6838
|
+
if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
|
|
6485
6839
|
const [repo, sha, defaultBranch] = yield* Effect.all([
|
|
6486
6840
|
gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
6487
6841
|
gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
@@ -6509,23 +6863,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6509
6863
|
const lintFailure = yield* Ref.make({
|
|
6510
6864
|
didFail: false,
|
|
6511
6865
|
reason: null,
|
|
6512
|
-
reasonTag: null
|
|
6866
|
+
reasonTag: null,
|
|
6867
|
+
reasonKind: null
|
|
6513
6868
|
});
|
|
6514
6869
|
const deadCodeFailure = yield* Ref.make({
|
|
6515
6870
|
didFail: false,
|
|
6516
6871
|
reason: null
|
|
6517
6872
|
});
|
|
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
6873
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6530
6874
|
const scanStartTime = Date.now();
|
|
6531
6875
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6542,24 +6886,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6542
6886
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
6543
6887
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
6544
6888
|
lastReportedTotalFileCount = totalFileCount;
|
|
6545
|
-
Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
|
|
6889
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
6546
6890
|
}
|
|
6547
6891
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6548
6892
|
yield* Ref.set(lintFailure, {
|
|
6549
6893
|
didFail: true,
|
|
6550
6894
|
reason: error.message,
|
|
6551
|
-
reasonTag: error.reason._tag
|
|
6895
|
+
reasonTag: error.reason._tag,
|
|
6896
|
+
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
6552
6897
|
});
|
|
6553
6898
|
return Stream.empty;
|
|
6554
6899
|
}))));
|
|
6555
6900
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6556
6901
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6557
6902
|
yield* afterLint(lintFailureState.didFail);
|
|
6558
|
-
if (lintFailureState.didFail)
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6903
|
+
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
6904
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6905
|
+
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6906
|
+
rootDirectory: scanDirectory,
|
|
6907
|
+
userConfig: resolvedConfig.config
|
|
6908
|
+
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6909
|
+
yield* Ref.set(deadCodeFailure, {
|
|
6910
|
+
didFail: true,
|
|
6911
|
+
reason: error.message
|
|
6912
|
+
});
|
|
6913
|
+
return Stream.empty;
|
|
6914
|
+
}))))))));
|
|
6563
6915
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6564
6916
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6565
6917
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
@@ -6601,6 +6953,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6601
6953
|
didLintFail: lintFailureState.didFail,
|
|
6602
6954
|
lintFailureReason: lintFailureState.reason,
|
|
6603
6955
|
lintFailureReasonTag: lintFailureState.reasonTag,
|
|
6956
|
+
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
6604
6957
|
lintPartialFailures,
|
|
6605
6958
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
6606
6959
|
deadCodeFailureReason: deadCodeFailureState.reason
|
|
@@ -7035,11 +7388,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7035
7388
|
const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
7036
7389
|
if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
|
|
7037
7390
|
const skippedChecks = [];
|
|
7391
|
+
if (output.didLintFail) skippedChecks.push("lint");
|
|
7392
|
+
if (output.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
7038
7393
|
const skippedCheckReasons = {};
|
|
7039
|
-
if (output.
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
}
|
|
7394
|
+
if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
|
|
7395
|
+
else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
|
|
7396
|
+
if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
|
|
7043
7397
|
return {
|
|
7044
7398
|
diagnostics: [...output.diagnostics],
|
|
7045
7399
|
score: output.score,
|