react-doctor 0.2.9 → 0.2.11-dev.15e5fec
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/README.md +26 -0
- package/dist/{cli-logger-BliQX9s8.js → cli-logger-CSZagq1E.js} +598 -93
- package/dist/cli.js +194 -110
- package/dist/index.d.ts +26 -2
- package/dist/index.js +592 -94
- package/package.json +4 -4
|
@@ -2272,22 +2272,100 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
2272
2272
|
gatsby: "Gatsby",
|
|
2273
2273
|
expo: "Expo",
|
|
2274
2274
|
"react-native": "React Native",
|
|
2275
|
+
preact: "Preact",
|
|
2275
2276
|
unknown: "React"
|
|
2276
2277
|
};
|
|
2277
2278
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
2278
2279
|
const detectFramework = (dependencies) => {
|
|
2279
2280
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
2281
|
+
if (dependencies.preact && !dependencies.react) return "preact";
|
|
2280
2282
|
return "unknown";
|
|
2281
2283
|
};
|
|
2282
|
-
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
2283
|
-
const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
|
|
2284
|
-
const OR_SEPARATOR = /\s*\|\|\s*/;
|
|
2285
2284
|
const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
|
|
2286
2285
|
const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
|
|
2287
2286
|
const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
|
|
2288
|
-
const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
|
|
2289
|
-
const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
|
|
2290
2287
|
const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
|
|
2288
|
+
const isDigit = (value) => value !== void 0 && value >= "0" && value <= "9";
|
|
2289
|
+
const isWhitespace = (value) => value === " " || value === " " || value === "\n" || value === "\r" || value === "\f" || value === "\v";
|
|
2290
|
+
const isSeparator = (value) => isWhitespace(value) || value === "," || value === "|";
|
|
2291
|
+
const skipWhitespace = (value, start) => {
|
|
2292
|
+
let index = start;
|
|
2293
|
+
while (isWhitespace(value[index])) index += 1;
|
|
2294
|
+
return index;
|
|
2295
|
+
};
|
|
2296
|
+
const skipSeparators = (value, start) => {
|
|
2297
|
+
let index = start;
|
|
2298
|
+
while (isSeparator(value[index])) index += 1;
|
|
2299
|
+
return index;
|
|
2300
|
+
};
|
|
2301
|
+
const readDigits = (value, start) => {
|
|
2302
|
+
let index = start;
|
|
2303
|
+
while (isDigit(value[index])) index += 1;
|
|
2304
|
+
return index;
|
|
2305
|
+
};
|
|
2306
|
+
const getUpperBoundComparatorEnd = (version, start) => {
|
|
2307
|
+
if (version[start] !== "<") return null;
|
|
2308
|
+
let index = skipWhitespace(version, start + 1);
|
|
2309
|
+
if (version[index] === "=") index = skipWhitespace(version, index + 1);
|
|
2310
|
+
const majorStart = index;
|
|
2311
|
+
index = readDigits(version, index);
|
|
2312
|
+
if (index === majorStart) return null;
|
|
2313
|
+
for (let segments = 0; segments < 2 && version[index] === "."; segments += 1) {
|
|
2314
|
+
const segmentStart = index + 1;
|
|
2315
|
+
const segmentEnd = readDigits(version, segmentStart);
|
|
2316
|
+
if (segmentEnd === segmentStart) break;
|
|
2317
|
+
index = segmentEnd;
|
|
2318
|
+
}
|
|
2319
|
+
if (version[index] === "-") {
|
|
2320
|
+
index += 1;
|
|
2321
|
+
while (index < version.length && !isSeparator(version[index])) index += 1;
|
|
2322
|
+
}
|
|
2323
|
+
return index;
|
|
2324
|
+
};
|
|
2325
|
+
const stripUpperBoundComparators = (version) => {
|
|
2326
|
+
let stripped = "";
|
|
2327
|
+
let index = 0;
|
|
2328
|
+
while (index < version.length) {
|
|
2329
|
+
const comparatorEnd = getUpperBoundComparatorEnd(version, index);
|
|
2330
|
+
if (comparatorEnd === null) {
|
|
2331
|
+
stripped += version[index];
|
|
2332
|
+
index += 1;
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
stripped += " ";
|
|
2336
|
+
index = comparatorEnd;
|
|
2337
|
+
}
|
|
2338
|
+
return stripped;
|
|
2339
|
+
};
|
|
2340
|
+
const hasNonLowerBoundComparator = (branch) => {
|
|
2341
|
+
for (let index = 0; index < branch.length; index += 1) {
|
|
2342
|
+
if (index > 0 && !isSeparator(branch[index - 1])) continue;
|
|
2343
|
+
if (branch[index] === ">" && branch[index + 1] !== "=") {
|
|
2344
|
+
if (isDigit(branch[skipWhitespace(branch, index + 1)])) return true;
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (branch[index] !== "!") continue;
|
|
2348
|
+
let valueIndex = index + 1;
|
|
2349
|
+
if (branch[valueIndex] === "=") valueIndex += 1;
|
|
2350
|
+
if (branch[valueIndex] === "=") valueIndex += 1;
|
|
2351
|
+
valueIndex = skipWhitespace(branch, valueIndex);
|
|
2352
|
+
if (isDigit(branch[valueIndex])) return true;
|
|
2353
|
+
}
|
|
2354
|
+
return false;
|
|
2355
|
+
};
|
|
2356
|
+
const isMajorTerminator = (value) => value === void 0 || isSeparator(value) || value === "." || value === "*" || value === "x" || value === "X" || value === "-";
|
|
2357
|
+
const getLowerBoundMajorAt = (branch, start) => {
|
|
2358
|
+
let index = start;
|
|
2359
|
+
if (branch[index] === ">" && branch[index + 1] === "=") index = skipWhitespace(branch, index + 2);
|
|
2360
|
+
else if (branch[index] === "~" || branch[index] === "^" || branch[index] === "=" || branch[index] === "v") index = skipWhitespace(branch, index + 1);
|
|
2361
|
+
const majorStart = index;
|
|
2362
|
+
const majorEnd = readDigits(branch, majorStart);
|
|
2363
|
+
if (majorEnd === majorStart || !isMajorTerminator(branch[majorEnd])) return null;
|
|
2364
|
+
return {
|
|
2365
|
+
end: majorEnd,
|
|
2366
|
+
major: Number.parseInt(branch.slice(majorStart, majorEnd), 10)
|
|
2367
|
+
};
|
|
2368
|
+
};
|
|
2291
2369
|
const normalizeDependencyVersion = (version) => {
|
|
2292
2370
|
const trimmed = version.trim();
|
|
2293
2371
|
if (trimmed.length === 0) return null;
|
|
@@ -2297,17 +2375,29 @@ const normalizeDependencyVersion = (version) => {
|
|
|
2297
2375
|
if (WILDCARD_VERSION.test(normalizedVersion)) return null;
|
|
2298
2376
|
return normalizedVersion;
|
|
2299
2377
|
};
|
|
2300
|
-
const splitDependencyVersionBranches = (version) => version.split(
|
|
2301
|
-
const hasUpperBoundComparator = (version) =>
|
|
2378
|
+
const splitDependencyVersionBranches = (version) => version.split("||").map((branch) => branch.trim()).filter(Boolean);
|
|
2379
|
+
const hasUpperBoundComparator = (version) => {
|
|
2380
|
+
for (let index = 0; index < version.length; index += 1) if (getUpperBoundComparatorEnd(version, index) !== null) return true;
|
|
2381
|
+
return false;
|
|
2382
|
+
};
|
|
2302
2383
|
const getBranchLowestMajor = (branch) => {
|
|
2303
|
-
if (
|
|
2304
|
-
const lowerBoundComparators = branch
|
|
2384
|
+
if (hasNonLowerBoundComparator(branch)) return null;
|
|
2385
|
+
const lowerBoundComparators = stripUpperBoundComparators(branch).trim();
|
|
2305
2386
|
if (lowerBoundComparators.length === 0) return null;
|
|
2306
2387
|
let branchLowestMajor = null;
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (
|
|
2388
|
+
let index = 0;
|
|
2389
|
+
while (index < lowerBoundComparators.length) {
|
|
2390
|
+
const lowerBoundStart = skipSeparators(lowerBoundComparators, index);
|
|
2391
|
+
if (lowerBoundStart > 0 && !isSeparator(lowerBoundComparators[lowerBoundStart - 1])) {
|
|
2392
|
+
index = lowerBoundStart + 1;
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const lowerBoundMajor = getLowerBoundMajorAt(lowerBoundComparators, lowerBoundStart);
|
|
2396
|
+
if (lowerBoundMajor !== null && Number.isFinite(lowerBoundMajor.major) && lowerBoundMajor.major > 0) {
|
|
2397
|
+
const major = lowerBoundMajor.major;
|
|
2398
|
+
if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
|
|
2399
|
+
}
|
|
2400
|
+
index = lowerBoundMajor?.end ?? lowerBoundStart + 1;
|
|
2311
2401
|
}
|
|
2312
2402
|
return branchLowestMajor;
|
|
2313
2403
|
};
|
|
@@ -2476,6 +2566,7 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
|
|
|
2476
2566
|
const EMPTY_DEPENDENCY_INFO = {
|
|
2477
2567
|
reactVersion: null,
|
|
2478
2568
|
tailwindVersion: null,
|
|
2569
|
+
zodVersion: null,
|
|
2479
2570
|
framework: "unknown"
|
|
2480
2571
|
};
|
|
2481
2572
|
const pickConcreteVersion = (packageJson, packageName, sections) => {
|
|
@@ -2504,6 +2595,11 @@ const extractDependencyInfo = (packageJson) => {
|
|
|
2504
2595
|
"devDependencies",
|
|
2505
2596
|
"peerDependencies"
|
|
2506
2597
|
]),
|
|
2598
|
+
zodVersion: pickConcreteVersion(packageJson, "zod", [
|
|
2599
|
+
"dependencies",
|
|
2600
|
+
"devDependencies",
|
|
2601
|
+
"peerDependencies"
|
|
2602
|
+
]),
|
|
2507
2603
|
framework: detectFramework(allDependencies)
|
|
2508
2604
|
};
|
|
2509
2605
|
};
|
|
@@ -2648,8 +2744,22 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
|
|
|
2648
2744
|
workspaceDirectory,
|
|
2649
2745
|
workspacePackageJson
|
|
2650
2746
|
});
|
|
2747
|
+
const zodVersion = resolveWorkspaceDependencyVersion({
|
|
2748
|
+
concreteVersion: info.zodVersion,
|
|
2749
|
+
packageName: "zod",
|
|
2750
|
+
rootDirectory,
|
|
2751
|
+
rootPackageJson: packageJson,
|
|
2752
|
+
sections: [
|
|
2753
|
+
"dependencies",
|
|
2754
|
+
"devDependencies",
|
|
2755
|
+
"peerDependencies"
|
|
2756
|
+
],
|
|
2757
|
+
workspaceDirectory,
|
|
2758
|
+
workspacePackageJson
|
|
2759
|
+
});
|
|
2651
2760
|
if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
|
|
2652
2761
|
if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
|
|
2762
|
+
if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
|
|
2653
2763
|
if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
|
|
2654
2764
|
const resultReactMajor = parseReactMajor(result.reactVersion);
|
|
2655
2765
|
if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
|
|
@@ -2684,17 +2794,44 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2684
2794
|
"peerDependencies"
|
|
2685
2795
|
]
|
|
2686
2796
|
}) : null;
|
|
2797
|
+
const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
|
|
2798
|
+
packageJson: leafPackageJson,
|
|
2799
|
+
packageName: "zod",
|
|
2800
|
+
sections: [
|
|
2801
|
+
"dependencies",
|
|
2802
|
+
"devDependencies",
|
|
2803
|
+
"peerDependencies"
|
|
2804
|
+
]
|
|
2805
|
+
}) : null;
|
|
2687
2806
|
const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
|
|
2688
2807
|
const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
|
|
2808
|
+
const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
|
|
2689
2809
|
const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
|
|
2690
2810
|
const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
|
|
2811
|
+
const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
|
|
2691
2812
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
2692
2813
|
return {
|
|
2693
2814
|
reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
2694
2815
|
tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
|
|
2816
|
+
zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
|
|
2695
2817
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2696
2818
|
};
|
|
2697
2819
|
};
|
|
2820
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
|
|
2821
|
+
if (predicate(rootPackageJson)) return true;
|
|
2822
|
+
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2823
|
+
if (patterns.length === 0) return false;
|
|
2824
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2825
|
+
for (const pattern of patterns) {
|
|
2826
|
+
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2827
|
+
for (const workspaceDirectory of directories) {
|
|
2828
|
+
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2829
|
+
visitedDirectories.add(workspaceDirectory);
|
|
2830
|
+
if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return false;
|
|
2834
|
+
};
|
|
2698
2835
|
const NAMES = new Set([
|
|
2699
2836
|
"react-native",
|
|
2700
2837
|
"react-native-tvos",
|
|
@@ -2725,20 +2862,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2725
2862
|
if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
|
|
2726
2863
|
return false;
|
|
2727
2864
|
};
|
|
2728
|
-
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) =>
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
for (const workspaceDirectory of directories) {
|
|
2736
|
-
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2737
|
-
visitedDirectories.add(workspaceDirectory);
|
|
2738
|
-
if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
return false;
|
|
2865
|
+
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2866
|
+
const getPreactVersion = (packageJson) => {
|
|
2867
|
+
return {
|
|
2868
|
+
...packageJson.peerDependencies,
|
|
2869
|
+
...packageJson.dependencies,
|
|
2870
|
+
...packageJson.devDependencies
|
|
2871
|
+
}.preact ?? null;
|
|
2742
2872
|
};
|
|
2743
2873
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2744
2874
|
"@tanstack/react-query",
|
|
@@ -2753,6 +2883,20 @@ const hasTanStackQuery = (packageJson) => {
|
|
|
2753
2883
|
};
|
|
2754
2884
|
return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
2755
2885
|
};
|
|
2886
|
+
const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
|
|
2887
|
+
const isPackageJsonReanimatedAware = (packageJson) => {
|
|
2888
|
+
const allDependencies = {
|
|
2889
|
+
...packageJson.peerDependencies,
|
|
2890
|
+
...packageJson.dependencies,
|
|
2891
|
+
...packageJson.devDependencies,
|
|
2892
|
+
...packageJson.optionalDependencies
|
|
2893
|
+
};
|
|
2894
|
+
return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
|
|
2895
|
+
};
|
|
2896
|
+
const parseZodMajor = (zodVersion) => {
|
|
2897
|
+
if (typeof zodVersion !== "string") return null;
|
|
2898
|
+
return getLowestDependencyMajor(zodVersion);
|
|
2899
|
+
};
|
|
2756
2900
|
const hasUpperBoundOnlyPeerRange = (range) => {
|
|
2757
2901
|
if (typeof range !== "string") return false;
|
|
2758
2902
|
const normalizedRange = normalizeDependencyVersion(range);
|
|
@@ -2777,7 +2921,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2777
2921
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2778
2922
|
"react",
|
|
2779
2923
|
"react-native",
|
|
2780
|
-
"next"
|
|
2924
|
+
"next",
|
|
2925
|
+
"preact"
|
|
2781
2926
|
]);
|
|
2782
2927
|
const hasReactDependency = (packageJson) => {
|
|
2783
2928
|
const allDependencies = {
|
|
@@ -2838,12 +2983,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
|
|
|
2838
2983
|
const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
|
|
2839
2984
|
return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
|
|
2840
2985
|
};
|
|
2986
|
+
const NON_PROJECT_DIRECTORIES = new Set([
|
|
2987
|
+
"AppData",
|
|
2988
|
+
"Application Data",
|
|
2989
|
+
"Library"
|
|
2990
|
+
]);
|
|
2991
|
+
const MAX_SCAN_DEPTH = 6;
|
|
2841
2992
|
const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
2842
2993
|
const packages = [];
|
|
2843
|
-
const pendingDirectories = [
|
|
2994
|
+
const pendingDirectories = [{
|
|
2995
|
+
directory: rootDirectory,
|
|
2996
|
+
depth: 0
|
|
2997
|
+
}];
|
|
2844
2998
|
while (pendingDirectories.length > 0) {
|
|
2845
|
-
const
|
|
2846
|
-
if (!
|
|
2999
|
+
const current = pendingDirectories.pop();
|
|
3000
|
+
if (!current) continue;
|
|
3001
|
+
const { directory: currentDirectory, depth } = current;
|
|
2847
3002
|
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
2848
3003
|
if (isFile(packageJsonPath)) {
|
|
2849
3004
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -2855,10 +3010,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
|
2855
3010
|
});
|
|
2856
3011
|
}
|
|
2857
3012
|
}
|
|
3013
|
+
if (depth >= MAX_SCAN_DEPTH) continue;
|
|
2858
3014
|
const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
|
|
2859
3015
|
for (const entry of entries) {
|
|
2860
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
2861
|
-
pendingDirectories.push(
|
|
3016
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
|
|
3017
|
+
pendingDirectories.push({
|
|
3018
|
+
directory: path.join(currentDirectory, entry.name),
|
|
3019
|
+
depth: depth + 1
|
|
3020
|
+
});
|
|
2862
3021
|
}
|
|
2863
3022
|
}
|
|
2864
3023
|
return packages;
|
|
@@ -2876,7 +3035,7 @@ const discoverProject = (directory) => {
|
|
|
2876
3035
|
const packageJsonPath = path.join(directory, "package.json");
|
|
2877
3036
|
if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
|
|
2878
3037
|
const packageJson = readPackageJson(packageJsonPath);
|
|
2879
|
-
let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
|
|
3038
|
+
let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
|
|
2880
3039
|
const reactDeclaration = getDependencyDeclaration({
|
|
2881
3040
|
packageJson,
|
|
2882
3041
|
packageName: "react",
|
|
@@ -2895,9 +3054,19 @@ const discoverProject = (directory) => {
|
|
|
2895
3054
|
"peerDependencies"
|
|
2896
3055
|
]
|
|
2897
3056
|
});
|
|
3057
|
+
const zodDeclaration = getDependencyDeclaration({
|
|
3058
|
+
packageJson,
|
|
3059
|
+
packageName: "zod",
|
|
3060
|
+
sections: [
|
|
3061
|
+
"dependencies",
|
|
3062
|
+
"devDependencies",
|
|
3063
|
+
"peerDependencies"
|
|
3064
|
+
]
|
|
3065
|
+
});
|
|
2898
3066
|
if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
|
|
2899
3067
|
if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
|
|
2900
|
-
if (!
|
|
3068
|
+
if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
|
|
3069
|
+
if (!reactVersion || !tailwindVersion || !zodVersion) {
|
|
2901
3070
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
2902
3071
|
if (monorepoRoot) {
|
|
2903
3072
|
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
@@ -2905,6 +3074,7 @@ const discoverProject = (directory) => {
|
|
|
2905
3074
|
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
2906
3075
|
if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
|
|
2907
3076
|
if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
|
|
3077
|
+
if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
|
|
2908
3078
|
}
|
|
2909
3079
|
}
|
|
2910
3080
|
}
|
|
@@ -2912,36 +3082,81 @@ const discoverProject = (directory) => {
|
|
|
2912
3082
|
const workspaceInfo = findReactInWorkspaces(directory, packageJson);
|
|
2913
3083
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
2914
3084
|
if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
|
|
3085
|
+
if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
|
|
2915
3086
|
if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
|
|
2916
3087
|
}
|
|
2917
3088
|
if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
|
|
2918
3089
|
const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
|
|
2919
3090
|
if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
|
|
2920
3091
|
if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
|
|
3092
|
+
if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
|
|
2921
3093
|
if (framework === "unknown") framework = monorepoInfo.framework;
|
|
2922
3094
|
}
|
|
2923
3095
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
2924
3096
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
3097
|
+
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
2925
3098
|
const projectName = packageJson.name ?? path.basename(directory);
|
|
2926
3099
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
2927
3100
|
const sourceFileCount = countSourceFiles(directory);
|
|
2928
3101
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
3102
|
+
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
3103
|
+
const preactVersion = getPreactVersion(packageJson);
|
|
2929
3104
|
const projectInfo = {
|
|
2930
3105
|
rootDirectory: directory,
|
|
2931
3106
|
projectName,
|
|
2932
3107
|
reactVersion,
|
|
2933
3108
|
reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
|
|
2934
3109
|
tailwindVersion,
|
|
3110
|
+
zodVersion,
|
|
3111
|
+
zodMajorVersion: parseZodMajor(zodVersion),
|
|
2935
3112
|
framework,
|
|
2936
3113
|
hasTypeScript,
|
|
2937
3114
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2938
3115
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
3116
|
+
preactVersion,
|
|
3117
|
+
preactMajorVersion: parseReactMajor(preactVersion),
|
|
2939
3118
|
hasReactNativeWorkspace,
|
|
3119
|
+
hasReanimated,
|
|
2940
3120
|
sourceFileCount
|
|
2941
3121
|
};
|
|
2942
3122
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2943
3123
|
return projectInfo;
|
|
2944
3124
|
};
|
|
3125
|
+
const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
3126
|
+
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
3127
|
+
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
3128
|
+
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
3129
|
+
const parseReactMajorMinor = (reactVersion) => {
|
|
3130
|
+
if (typeof reactVersion !== "string") return null;
|
|
3131
|
+
const trimmed = reactVersion.trim();
|
|
3132
|
+
if (trimmed.length === 0) return null;
|
|
3133
|
+
const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
|
|
3134
|
+
if (lowerBoundsOnly.length === 0) return null;
|
|
3135
|
+
const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
|
|
3136
|
+
if (majorMinorMatch) {
|
|
3137
|
+
const major = Number.parseInt(majorMinorMatch[1], 10);
|
|
3138
|
+
const minor = Number.parseInt(majorMinorMatch[2], 10);
|
|
3139
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3140
|
+
if (!Number.isFinite(minor) || minor < 0) return null;
|
|
3141
|
+
return {
|
|
3142
|
+
major,
|
|
3143
|
+
minor
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
|
|
3147
|
+
if (!majorOnlyMatch) return null;
|
|
3148
|
+
const major = Number.parseInt(majorOnlyMatch[1], 10);
|
|
3149
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3150
|
+
return {
|
|
3151
|
+
major,
|
|
3152
|
+
minor: 0
|
|
3153
|
+
};
|
|
3154
|
+
};
|
|
3155
|
+
const isReactAtLeast = (detected, required) => {
|
|
3156
|
+
if (detected === null) return true;
|
|
3157
|
+
if (detected.major !== required.major) return detected.major > required.major;
|
|
3158
|
+
return detected.minor >= required.minor;
|
|
3159
|
+
};
|
|
2945
3160
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2946
3161
|
if (typeof tailwindVersion !== "string") return null;
|
|
2947
3162
|
const trimmed = tailwindVersion.trim();
|
|
@@ -2972,8 +3187,10 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
2972
3187
|
return detected.minor >= required.minor;
|
|
2973
3188
|
};
|
|
2974
3189
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3190
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
2975
3191
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
2976
3192
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
3193
|
+
const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
|
|
2977
3194
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
2978
3195
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
2979
3196
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
@@ -3876,17 +4093,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
3876
4093
|
headers
|
|
3877
4094
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
3878
4095
|
}).pipe(Effect.orDie));
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4096
|
+
/**
|
|
4097
|
+
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
4098
|
+
* startup so the eval harness can raise the budget under sandbox
|
|
4099
|
+
* microVMs without recompiling react-doctor. Tests override via
|
|
4100
|
+
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
4101
|
+
*/
|
|
4102
|
+
var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
3882
4103
|
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
3883
4104
|
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3884
4105
|
const parsed = Number(raw);
|
|
3885
4106
|
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3886
4107
|
return parsed;
|
|
3887
|
-
} });
|
|
3888
|
-
|
|
3889
|
-
|
|
4108
|
+
} }) {};
|
|
4109
|
+
/**
|
|
4110
|
+
* Hard cap on combined stdout+stderr bytes per oxlint batch. The
|
|
4111
|
+
* subprocess gets SIGKILL'd if it produces more; the recovery path
|
|
4112
|
+
* suggests narrowing the scan with --diff. Override via Layer in
|
|
4113
|
+
* tests that exercise the cap behavior.
|
|
4114
|
+
*/
|
|
4115
|
+
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
3890
4116
|
const DIAGNOSTIC_SURFACES = [
|
|
3891
4117
|
"cli",
|
|
3892
4118
|
"prComment",
|
|
@@ -4491,6 +4717,59 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4491
4717
|
return patterns;
|
|
4492
4718
|
};
|
|
4493
4719
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4720
|
+
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4721
|
+
const inputChunks = [];
|
|
4722
|
+
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
4723
|
+
process.stdin.on("end", () => {
|
|
4724
|
+
const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
|
|
4725
|
+
|
|
4726
|
+
const normalizeResult = (result) => ({
|
|
4727
|
+
unusedFiles: result.unusedFiles.map((unusedFile) => ({
|
|
4728
|
+
path: unusedFile.path,
|
|
4729
|
+
})),
|
|
4730
|
+
unusedExports: result.unusedExports.map((unusedExport) => ({
|
|
4731
|
+
path: unusedExport.path,
|
|
4732
|
+
name: unusedExport.name,
|
|
4733
|
+
line: unusedExport.line,
|
|
4734
|
+
column: unusedExport.column,
|
|
4735
|
+
isTypeOnly: unusedExport.isTypeOnly,
|
|
4736
|
+
})),
|
|
4737
|
+
unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
|
|
4738
|
+
name: unusedDependency.name,
|
|
4739
|
+
isDevDependency: unusedDependency.isDevDependency,
|
|
4740
|
+
})),
|
|
4741
|
+
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
4742
|
+
files: cycle.files,
|
|
4743
|
+
})),
|
|
4744
|
+
});
|
|
4745
|
+
|
|
4746
|
+
const serializeError = (error) =>
|
|
4747
|
+
error instanceof Error
|
|
4748
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4749
|
+
: { message: String(error) };
|
|
4750
|
+
|
|
4751
|
+
const emit = (message) => {
|
|
4752
|
+
process.stdout.write(JSON.stringify(message), () => process.exit(0));
|
|
4753
|
+
};
|
|
4754
|
+
|
|
4755
|
+
(async () => {
|
|
4756
|
+
try {
|
|
4757
|
+
const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
|
|
4758
|
+
const config = {
|
|
4759
|
+
rootDir: workerInput.rootDirectory,
|
|
4760
|
+
...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
|
|
4761
|
+
...(workerInput.ignorePatterns.length > 0
|
|
4762
|
+
? { ignorePatterns: workerInput.ignorePatterns }
|
|
4763
|
+
: {}),
|
|
4764
|
+
};
|
|
4765
|
+
const result = await analyze(defineConfig(config));
|
|
4766
|
+
emit({ ok: true, result: normalizeResult(result) });
|
|
4767
|
+
} catch (error) {
|
|
4768
|
+
emit({ ok: false, error: serializeError(error) });
|
|
4769
|
+
}
|
|
4770
|
+
})();
|
|
4771
|
+
});
|
|
4772
|
+
`;
|
|
4494
4773
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4495
4774
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4496
4775
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4511,16 +4790,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4511
4790
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4512
4791
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4513
4792
|
};
|
|
4793
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4794
|
+
const parseArray = (value, label) => {
|
|
4795
|
+
if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4796
|
+
return value;
|
|
4797
|
+
};
|
|
4798
|
+
const parseString = (value, label) => {
|
|
4799
|
+
if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4800
|
+
return value;
|
|
4801
|
+
};
|
|
4802
|
+
const parseNumber = (value, label) => {
|
|
4803
|
+
if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4804
|
+
return value;
|
|
4805
|
+
};
|
|
4806
|
+
const parseBoolean = (value, label) => {
|
|
4807
|
+
if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4808
|
+
return value;
|
|
4809
|
+
};
|
|
4810
|
+
const parseStringArray = (value, label) => {
|
|
4811
|
+
return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
|
|
4812
|
+
};
|
|
4813
|
+
const parseUnusedFiles = (value) => {
|
|
4814
|
+
const values = parseArray(value, "unusedFiles");
|
|
4815
|
+
const unusedFiles = [];
|
|
4816
|
+
for (const [index, entry] of values.entries()) {
|
|
4817
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
4818
|
+
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
4819
|
+
}
|
|
4820
|
+
return unusedFiles;
|
|
4821
|
+
};
|
|
4822
|
+
const parseUnusedExports = (value) => {
|
|
4823
|
+
const values = parseArray(value, "unusedExports");
|
|
4824
|
+
const unusedExports = [];
|
|
4825
|
+
for (const [index, entry] of values.entries()) {
|
|
4826
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
4827
|
+
unusedExports.push({
|
|
4828
|
+
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
4829
|
+
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
4830
|
+
line: parseNumber(entry.line, `unusedExports[${index}].line`),
|
|
4831
|
+
column: parseNumber(entry.column, `unusedExports[${index}].column`),
|
|
4832
|
+
isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
|
|
4833
|
+
});
|
|
4834
|
+
}
|
|
4835
|
+
return unusedExports;
|
|
4836
|
+
};
|
|
4837
|
+
const parseUnusedDependencies = (value) => {
|
|
4838
|
+
const values = parseArray(value, "unusedDependencies");
|
|
4839
|
+
const unusedDependencies = [];
|
|
4840
|
+
for (const [index, entry] of values.entries()) {
|
|
4841
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
4842
|
+
unusedDependencies.push({
|
|
4843
|
+
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
4844
|
+
isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
return unusedDependencies;
|
|
4848
|
+
};
|
|
4849
|
+
const parseCircularDependencies = (value) => {
|
|
4850
|
+
const values = parseArray(value, "circularDependencies");
|
|
4851
|
+
const circularDependencies = [];
|
|
4852
|
+
for (const [index, entry] of values.entries()) {
|
|
4853
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
4854
|
+
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
4855
|
+
}
|
|
4856
|
+
return circularDependencies;
|
|
4857
|
+
};
|
|
4858
|
+
const parseDeadCodeWorkerResult = (value) => {
|
|
4859
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
4860
|
+
return {
|
|
4861
|
+
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
4862
|
+
unusedExports: parseUnusedExports(value.unusedExports),
|
|
4863
|
+
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
4864
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
4865
|
+
};
|
|
4866
|
+
};
|
|
4867
|
+
const parseDeadCodeWorkerError = (value) => {
|
|
4868
|
+
if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
4869
|
+
return {
|
|
4870
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
4871
|
+
message: value.message,
|
|
4872
|
+
...typeof value.stack === "string" ? { stack: value.stack } : {}
|
|
4873
|
+
};
|
|
4874
|
+
};
|
|
4875
|
+
const parseDeadCodeWorkerMessage = (value) => {
|
|
4876
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
4877
|
+
if (value.ok === true) return {
|
|
4878
|
+
ok: true,
|
|
4879
|
+
result: value.result
|
|
4880
|
+
};
|
|
4881
|
+
if (value.ok === false) return {
|
|
4882
|
+
ok: false,
|
|
4883
|
+
error: parseDeadCodeWorkerError(value.error)
|
|
4884
|
+
};
|
|
4885
|
+
throw new Error("Dead-code worker returned an invalid status.");
|
|
4886
|
+
};
|
|
4887
|
+
const buildDeadCodeWorkerError = (workerError) => {
|
|
4888
|
+
const error = new Error(workerError.message);
|
|
4889
|
+
if (workerError.name !== void 0) error.name = workerError.name;
|
|
4890
|
+
if (workerError.stack !== void 0) error.stack = workerError.stack;
|
|
4891
|
+
return error;
|
|
4892
|
+
};
|
|
4893
|
+
const createDeadCodeWorker = (input) => {
|
|
4894
|
+
const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
|
|
4895
|
+
stdio: [
|
|
4896
|
+
"pipe",
|
|
4897
|
+
"pipe",
|
|
4898
|
+
"pipe"
|
|
4899
|
+
],
|
|
4900
|
+
windowsHide: true
|
|
4901
|
+
});
|
|
4902
|
+
const stdoutChunks = [];
|
|
4903
|
+
const stderrChunks = [];
|
|
4904
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
4905
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
4906
|
+
let didSettle = false;
|
|
4907
|
+
const result = new Promise((resolve, reject) => {
|
|
4908
|
+
const settle = (callback) => {
|
|
4909
|
+
if (didSettle) return;
|
|
4910
|
+
didSettle = true;
|
|
4911
|
+
callback();
|
|
4912
|
+
};
|
|
4913
|
+
child.once("error", (error) => {
|
|
4914
|
+
settle(() => reject(error));
|
|
4915
|
+
});
|
|
4916
|
+
child.once("close", (exitCode) => {
|
|
4917
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
4918
|
+
if (stdout.length === 0) {
|
|
4919
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
4920
|
+
settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
try {
|
|
4924
|
+
const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
|
|
4925
|
+
if (parsedMessage.ok) {
|
|
4926
|
+
settle(() => resolve(parsedMessage.result));
|
|
4927
|
+
return;
|
|
4928
|
+
}
|
|
4929
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4930
|
+
} catch (error) {
|
|
4931
|
+
settle(() => reject(error));
|
|
4932
|
+
}
|
|
4933
|
+
});
|
|
4934
|
+
});
|
|
4935
|
+
child.stdin.on("error", () => {});
|
|
4936
|
+
child.stdin.end(JSON.stringify(input));
|
|
4937
|
+
return {
|
|
4938
|
+
result,
|
|
4939
|
+
terminate: () => {
|
|
4940
|
+
didSettle = true;
|
|
4941
|
+
child.kill("SIGKILL");
|
|
4942
|
+
}
|
|
4943
|
+
};
|
|
4944
|
+
};
|
|
4945
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
4946
|
+
let didSettle = false;
|
|
4947
|
+
const timeoutHandle = setTimeout(() => {
|
|
4948
|
+
if (didSettle) return;
|
|
4949
|
+
didSettle = true;
|
|
4950
|
+
handle.terminate?.();
|
|
4951
|
+
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
4952
|
+
}, timeoutMs);
|
|
4953
|
+
timeoutHandle.unref?.();
|
|
4954
|
+
handle.result.then((value) => {
|
|
4955
|
+
if (didSettle) return;
|
|
4956
|
+
didSettle = true;
|
|
4957
|
+
clearTimeout(timeoutHandle);
|
|
4958
|
+
handle.terminate?.();
|
|
4959
|
+
resolve(value);
|
|
4960
|
+
}, (error) => {
|
|
4961
|
+
if (didSettle) return;
|
|
4962
|
+
didSettle = true;
|
|
4963
|
+
clearTimeout(timeoutHandle);
|
|
4964
|
+
handle.terminate?.();
|
|
4965
|
+
reject(error);
|
|
4966
|
+
});
|
|
4967
|
+
});
|
|
4514
4968
|
const checkDeadCode = async (options) => {
|
|
4515
4969
|
const { rootDirectory, userConfig } = options;
|
|
4516
4970
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4517
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4518
4971
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4519
|
-
const result = await
|
|
4520
|
-
|
|
4972
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4973
|
+
rootDirectory,
|
|
4521
4974
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4522
|
-
|
|
4523
|
-
|
|
4975
|
+
ignorePatterns,
|
|
4976
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4977
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4524
4978
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4525
4979
|
const diagnostics = [];
|
|
4526
4980
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -4726,8 +5180,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4726
5180
|
env: input.env,
|
|
4727
5181
|
extendEnv: true
|
|
4728
5182
|
}));
|
|
5183
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
5184
|
+
const stdoutByteCount = yield* Ref.make(0);
|
|
5185
|
+
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({
|
|
5186
|
+
args: [...input.args],
|
|
5187
|
+
directory: input.directory,
|
|
5188
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
5189
|
+
}) })) : Effect.void))));
|
|
4729
5190
|
const [stdout, stderr, status] = yield* Effect.all([
|
|
4730
|
-
Stream.mkString(Stream.decodeText(
|
|
5191
|
+
Stream.mkString(Stream.decodeText(stdoutStream)),
|
|
4731
5192
|
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
4732
5193
|
handle.exitCode
|
|
4733
5194
|
], { concurrency: 3 });
|
|
@@ -4889,7 +5350,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4889
5350
|
if (result.status !== 0) return [];
|
|
4890
5351
|
return splitNullSeparated(result.stdout);
|
|
4891
5352
|
})),
|
|
4892
|
-
showStagedContent: (directory, relativePath) =>
|
|
5353
|
+
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
5354
|
+
command: "git",
|
|
5355
|
+
args: ["show", `:${relativePath}`],
|
|
5356
|
+
directory,
|
|
5357
|
+
maxStdoutBytes: options?.maxBufferBytes
|
|
5358
|
+
}).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
|
|
4893
5359
|
grep: (input) => Effect.gen(function* () {
|
|
4894
5360
|
const args = ["grep"];
|
|
4895
5361
|
if (input.listMatchingFiles ?? true) args.push("-l");
|
|
@@ -4897,7 +5363,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4897
5363
|
if (input.extendedRegexp ?? false) args.push("-E");
|
|
4898
5364
|
args.push(input.pattern);
|
|
4899
5365
|
if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
|
|
4900
|
-
const result = yield*
|
|
5366
|
+
const result = yield* runCommand({
|
|
5367
|
+
command: "git",
|
|
5368
|
+
args,
|
|
5369
|
+
directory: input.directory,
|
|
5370
|
+
maxStdoutBytes: input.maxBufferBytes
|
|
5371
|
+
});
|
|
4901
5372
|
if (result.status === 128) return null;
|
|
4902
5373
|
return {
|
|
4903
5374
|
status: result.status,
|
|
@@ -5144,7 +5615,16 @@ const buildCapabilities = (project) => {
|
|
|
5144
5615
|
capabilities.add(project.framework);
|
|
5145
5616
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5146
5617
|
const reactMajor = project.reactMajorVersion;
|
|
5147
|
-
if (reactMajor !== null)
|
|
5618
|
+
if (reactMajor !== null) {
|
|
5619
|
+
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
5620
|
+
for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
|
|
5621
|
+
if (reactMajor >= 19) {
|
|
5622
|
+
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5623
|
+
major: 19,
|
|
5624
|
+
minor: 2
|
|
5625
|
+
})) capabilities.add("react:19.2");
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5148
5628
|
if (project.tailwindVersion !== null) {
|
|
5149
5629
|
capabilities.add("tailwind");
|
|
5150
5630
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5152,9 +5632,22 @@ const buildCapabilities = (project) => {
|
|
|
5152
5632
|
minor: 4
|
|
5153
5633
|
})) capabilities.add("tailwind:3.4");
|
|
5154
5634
|
}
|
|
5635
|
+
if (project.zodVersion !== null) {
|
|
5636
|
+
capabilities.add("zod");
|
|
5637
|
+
if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
|
|
5638
|
+
}
|
|
5155
5639
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5156
5640
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5157
5641
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5642
|
+
if (project.preactVersion !== null) {
|
|
5643
|
+
capabilities.add("preact");
|
|
5644
|
+
const preactMajor = project.preactMajorVersion;
|
|
5645
|
+
if (preactMajor !== null) {
|
|
5646
|
+
const cappedPreactMajor = Math.min(preactMajor, 20);
|
|
5647
|
+
for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
|
|
5648
|
+
}
|
|
5649
|
+
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5650
|
+
}
|
|
5158
5651
|
return capabilities;
|
|
5159
5652
|
};
|
|
5160
5653
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -5409,6 +5902,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
|
|
|
5409
5902
|
if (!publicEnvPrefix) return fallbackRecommendation;
|
|
5410
5903
|
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`;
|
|
5411
5904
|
};
|
|
5905
|
+
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";
|
|
5906
|
+
const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
5907
|
+
if (rule !== "immutability") return help;
|
|
5908
|
+
if (!project.hasReanimated) return help;
|
|
5909
|
+
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5910
|
+
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5911
|
+
};
|
|
5412
5912
|
const REACT_MODULE_SOURCE = "react";
|
|
5413
5913
|
const REQUIRE_IDENTIFIER = "require";
|
|
5414
5914
|
const USE_IDENTIFIER = "use";
|
|
@@ -5732,7 +6232,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
|
|
|
5732
6232
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
5733
6233
|
if (plugin === "react-hooks-js") return {
|
|
5734
6234
|
message: REACT_COMPILER_MESSAGE,
|
|
5735
|
-
help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
|
|
6235
|
+
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
5736
6236
|
};
|
|
5737
6237
|
return {
|
|
5738
6238
|
message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
|
|
@@ -5801,13 +6301,6 @@ const SANITIZED_ENV = (() => {
|
|
|
5801
6301
|
}
|
|
5802
6302
|
return sanitized;
|
|
5803
6303
|
})();
|
|
5804
|
-
const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
5805
|
-
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
5806
|
-
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
5807
|
-
const parsed = Number(raw);
|
|
5808
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
5809
|
-
return parsed;
|
|
5810
|
-
})();
|
|
5811
6304
|
/**
|
|
5812
6305
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
5813
6306
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -5824,7 +6317,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
|
5824
6317
|
* The first three are splittable (the caller's binary-split retry
|
|
5825
6318
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
5826
6319
|
*/
|
|
5827
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
6320
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
5828
6321
|
const child = spawn(nodeBinaryPath, args, {
|
|
5829
6322
|
cwd: rootDirectory,
|
|
5830
6323
|
env: SANITIZED_ENV
|
|
@@ -5833,9 +6326,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5833
6326
|
child.kill("SIGKILL");
|
|
5834
6327
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5835
6328
|
kind: "timeout",
|
|
5836
|
-
detail: `${
|
|
6329
|
+
detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
|
|
5837
6330
|
}) }));
|
|
5838
|
-
},
|
|
6331
|
+
}, spawnTimeoutMs);
|
|
5839
6332
|
timeoutHandle.unref?.();
|
|
5840
6333
|
const stdoutBuffers = [];
|
|
5841
6334
|
const stderrBuffers = [];
|
|
@@ -5845,7 +6338,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5845
6338
|
const killIfTooLarge = (incomingBytes, isStdout) => {
|
|
5846
6339
|
if (isStdout) stdoutByteCount += incomingBytes;
|
|
5847
6340
|
else stderrByteCount += incomingBytes;
|
|
5848
|
-
if (stdoutByteCount + stderrByteCount >
|
|
6341
|
+
if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
|
|
5849
6342
|
didKillForSize = true;
|
|
5850
6343
|
child.kill("SIGKILL");
|
|
5851
6344
|
return true;
|
|
@@ -5871,7 +6364,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5871
6364
|
if (didKillForSize) {
|
|
5872
6365
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5873
6366
|
kind: "output-too-large",
|
|
5874
|
-
detail: `exceeded ${
|
|
6367
|
+
detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
|
|
5875
6368
|
}) }));
|
|
5876
6369
|
return;
|
|
5877
6370
|
}
|
|
@@ -5912,7 +6405,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5912
6405
|
* with a slimmer config in that case.
|
|
5913
6406
|
*/
|
|
5914
6407
|
const spawnLintBatches = async (input) => {
|
|
5915
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
|
|
6408
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
5916
6409
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
5917
6410
|
const allDiagnostics = [];
|
|
5918
6411
|
const droppedFiles = [];
|
|
@@ -5920,7 +6413,7 @@ const spawnLintBatches = async (input) => {
|
|
|
5920
6413
|
const spawnLintBatch = async (batch) => {
|
|
5921
6414
|
const batchArgs = [...baseArgs, ...batch];
|
|
5922
6415
|
try {
|
|
5923
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
|
|
6416
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
5924
6417
|
} catch (error) {
|
|
5925
6418
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
5926
6419
|
if (batch.length <= 1) {
|
|
@@ -6023,13 +6516,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
6023
6516
|
* 6. always restore disable directives + clean up the temp dir
|
|
6024
6517
|
*/
|
|
6025
6518
|
const runOxlint = async (options) => {
|
|
6026
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
|
|
6519
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
6027
6520
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
6028
6521
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
6029
6522
|
validateRuleRegistration();
|
|
6030
6523
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
6031
|
-
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6032
|
-
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6033
6524
|
const pluginPath = resolvePluginPath();
|
|
6034
6525
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
6035
6526
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
@@ -6044,6 +6535,8 @@ const runOxlint = async (options) => {
|
|
|
6044
6535
|
userPlugins
|
|
6045
6536
|
});
|
|
6046
6537
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
6538
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6539
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6047
6540
|
try {
|
|
6048
6541
|
const baseArgs = [
|
|
6049
6542
|
resolveOxlintBinary(),
|
|
@@ -6070,7 +6563,9 @@ const runOxlint = async (options) => {
|
|
|
6070
6563
|
nodeBinaryPath,
|
|
6071
6564
|
project,
|
|
6072
6565
|
onPartialFailure,
|
|
6073
|
-
onFileProgress: options.onFileProgress
|
|
6566
|
+
onFileProgress: options.onFileProgress,
|
|
6567
|
+
spawnTimeoutMs,
|
|
6568
|
+
outputMaxBytes
|
|
6074
6569
|
});
|
|
6075
6570
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6076
6571
|
try {
|
|
@@ -6136,6 +6631,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6136
6631
|
*/
|
|
6137
6632
|
static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
|
|
6138
6633
|
const partialFailures = yield* LintPartialFailures;
|
|
6634
|
+
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
6635
|
+
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
6139
6636
|
const collectedFailures = [];
|
|
6140
6637
|
const diagnostics = yield* Effect.tryPromise({
|
|
6141
6638
|
try: () => runOxlint({
|
|
@@ -6152,7 +6649,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6152
6649
|
onPartialFailure: (reason) => {
|
|
6153
6650
|
collectedFailures.push(reason);
|
|
6154
6651
|
},
|
|
6155
|
-
onFileProgress: input.onFileProgress
|
|
6652
|
+
onFileProgress: input.onFileProgress,
|
|
6653
|
+
spawnTimeoutMs,
|
|
6654
|
+
outputMaxBytes
|
|
6156
6655
|
}),
|
|
6157
6656
|
catch: ensureReactDoctorError
|
|
6158
6657
|
});
|
|
@@ -6450,7 +6949,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6450
6949
|
const resolvedConfig = yield* configService.resolve(input.directory);
|
|
6451
6950
|
const scanDirectory = resolvedConfig.resolvedDirectory;
|
|
6452
6951
|
const project = yield* projectService.discover(scanDirectory);
|
|
6453
|
-
if (project
|
|
6952
|
+
if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
|
|
6454
6953
|
const [repo, sha, defaultBranch] = yield* Effect.all([
|
|
6455
6954
|
gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
6456
6955
|
gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
@@ -6478,23 +6977,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6478
6977
|
const lintFailure = yield* Ref.make({
|
|
6479
6978
|
didFail: false,
|
|
6480
6979
|
reason: null,
|
|
6481
|
-
reasonTag: null
|
|
6980
|
+
reasonTag: null,
|
|
6981
|
+
reasonKind: null
|
|
6482
6982
|
});
|
|
6483
6983
|
const deadCodeFailure = yield* Ref.make({
|
|
6484
6984
|
didFail: false,
|
|
6485
6985
|
reason: null
|
|
6486
6986
|
});
|
|
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
6987
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6499
6988
|
const scanStartTime = Date.now();
|
|
6500
6989
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6511,24 +7000,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6511
7000
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
6512
7001
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
6513
7002
|
lastReportedTotalFileCount = totalFileCount;
|
|
6514
|
-
Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
|
|
7003
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
6515
7004
|
}
|
|
6516
7005
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6517
7006
|
yield* Ref.set(lintFailure, {
|
|
6518
7007
|
didFail: true,
|
|
6519
7008
|
reason: error.message,
|
|
6520
|
-
reasonTag: error.reason._tag
|
|
7009
|
+
reasonTag: error.reason._tag,
|
|
7010
|
+
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
6521
7011
|
});
|
|
6522
7012
|
return Stream.empty;
|
|
6523
7013
|
}))));
|
|
6524
7014
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6525
7015
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6526
7016
|
yield* afterLint(lintFailureState.didFail);
|
|
6527
|
-
if (lintFailureState.didFail)
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
7017
|
+
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
7018
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
7019
|
+
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
7020
|
+
rootDirectory: scanDirectory,
|
|
7021
|
+
userConfig: resolvedConfig.config
|
|
7022
|
+
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
7023
|
+
yield* Ref.set(deadCodeFailure, {
|
|
7024
|
+
didFail: true,
|
|
7025
|
+
reason: error.message
|
|
7026
|
+
});
|
|
7027
|
+
return Stream.empty;
|
|
7028
|
+
}))))))));
|
|
6532
7029
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6533
7030
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6534
7031
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
@@ -6570,6 +7067,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6570
7067
|
didLintFail: lintFailureState.didFail,
|
|
6571
7068
|
lintFailureReason: lintFailureState.reason,
|
|
6572
7069
|
lintFailureReasonTag: lintFailureState.reasonTag,
|
|
7070
|
+
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
6573
7071
|
lintPartialFailures,
|
|
6574
7072
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
6575
7073
|
deadCodeFailureReason: deadCodeFailureState.reason
|
|
@@ -7007,6 +7505,13 @@ const highlighter = {
|
|
|
7007
7505
|
gray: import_picocolors.default.gray,
|
|
7008
7506
|
bold: import_picocolors.default.bold
|
|
7009
7507
|
};
|
|
7508
|
+
/**
|
|
7509
|
+
* Canonical URL for a rule's reviewer-tested fix recipe, served at
|
|
7510
|
+
* `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. The
|
|
7511
|
+
* `/doctor` playbook fetches it on demand so each fix follows the
|
|
7512
|
+
* canonical recipe instead of being improvised per diagnostic.
|
|
7513
|
+
*/
|
|
7514
|
+
const buildRulePromptUrl = (plugin, rule) => `${PROMPTS_RULES_BASE_URL}/${plugin}/${rule}.md`;
|
|
7010
7515
|
const groupBy = (items, keyFn) => {
|
|
7011
7516
|
const groups = /* @__PURE__ */ new Map();
|
|
7012
7517
|
for (const item of items) {
|
|
@@ -7023,7 +7528,7 @@ var cli_logger_exports = /* @__PURE__ */ __exportAll({ cliLogger: () => cliLogge
|
|
|
7023
7528
|
/**
|
|
7024
7529
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
7025
7530
|
* the imperative CLI helper files (`select-projects`, `run-explain`,
|
|
7026
|
-
* `install-
|
|
7531
|
+
* `install-react-doctor`, the legacy paths in `cli/commands/inspect.ts`)
|
|
7027
7532
|
* that aren't yet Effect-typed. Every call drains into a single
|
|
7028
7533
|
* `Console.*` Effect via `Effect.runSync`, so the underlying logging
|
|
7029
7534
|
* pipeline is identical to the canonical `yield* Console.log(...)`
|
|
@@ -7054,6 +7559,6 @@ const cliLogger = {
|
|
|
7054
7559
|
}
|
|
7055
7560
|
};
|
|
7056
7561
|
//#endregion
|
|
7057
|
-
export {
|
|
7562
|
+
export { isMonorepoRoot as A, filterDiagnosticsForSurface as C, getDiffInfo as D, formatReactDoctorError as E, restoreLegacyThrow as F, runInspect as I, toRelativePath as L, layerOtlp as M, listWorkspacePackages as N, groupBy as O, resolveScanTarget as P, discoverReactSubprojects as S, formatErrorChain 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, isReactDoctorError as j, highlighter 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, filterSourceFiles as w, buildRulePromptUrl as x, buildJsonReport as y };
|
|
7058
7563
|
|
|
7059
|
-
//# sourceMappingURL=cli-logger-
|
|
7564
|
+
//# sourceMappingURL=cli-logger-CSZagq1E.js.map
|