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
|
@@ -2272,11 +2272,13 @@ 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
2284
|
const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -2695,6 +2697,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2695
2697
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2696
2698
|
};
|
|
2697
2699
|
};
|
|
2700
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
|
|
2701
|
+
if (predicate(rootPackageJson)) return true;
|
|
2702
|
+
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2703
|
+
if (patterns.length === 0) return false;
|
|
2704
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2705
|
+
for (const pattern of patterns) {
|
|
2706
|
+
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2707
|
+
for (const workspaceDirectory of directories) {
|
|
2708
|
+
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2709
|
+
visitedDirectories.add(workspaceDirectory);
|
|
2710
|
+
if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
return false;
|
|
2714
|
+
};
|
|
2698
2715
|
const NAMES = new Set([
|
|
2699
2716
|
"react-native",
|
|
2700
2717
|
"react-native-tvos",
|
|
@@ -2725,20 +2742,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2725
2742
|
if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
|
|
2726
2743
|
return false;
|
|
2727
2744
|
};
|
|
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;
|
|
2745
|
+
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2746
|
+
const getPreactVersion = (packageJson) => {
|
|
2747
|
+
return {
|
|
2748
|
+
...packageJson.peerDependencies,
|
|
2749
|
+
...packageJson.dependencies,
|
|
2750
|
+
...packageJson.devDependencies
|
|
2751
|
+
}.preact ?? null;
|
|
2742
2752
|
};
|
|
2743
2753
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2744
2754
|
"@tanstack/react-query",
|
|
@@ -2753,6 +2763,16 @@ const hasTanStackQuery = (packageJson) => {
|
|
|
2753
2763
|
};
|
|
2754
2764
|
return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
2755
2765
|
};
|
|
2766
|
+
const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
|
|
2767
|
+
const isPackageJsonReanimatedAware = (packageJson) => {
|
|
2768
|
+
const allDependencies = {
|
|
2769
|
+
...packageJson.peerDependencies,
|
|
2770
|
+
...packageJson.dependencies,
|
|
2771
|
+
...packageJson.devDependencies,
|
|
2772
|
+
...packageJson.optionalDependencies
|
|
2773
|
+
};
|
|
2774
|
+
return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
|
|
2775
|
+
};
|
|
2756
2776
|
const hasUpperBoundOnlyPeerRange = (range) => {
|
|
2757
2777
|
if (typeof range !== "string") return false;
|
|
2758
2778
|
const normalizedRange = normalizeDependencyVersion(range);
|
|
@@ -2777,7 +2797,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
2777
2797
|
const REACT_DEPENDENCY_NAMES = new Set([
|
|
2778
2798
|
"react",
|
|
2779
2799
|
"react-native",
|
|
2780
|
-
"next"
|
|
2800
|
+
"next",
|
|
2801
|
+
"preact"
|
|
2781
2802
|
]);
|
|
2782
2803
|
const hasReactDependency = (packageJson) => {
|
|
2783
2804
|
const allDependencies = {
|
|
@@ -2838,12 +2859,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
|
|
|
2838
2859
|
const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
|
|
2839
2860
|
return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
|
|
2840
2861
|
};
|
|
2862
|
+
const NON_PROJECT_DIRECTORIES = new Set([
|
|
2863
|
+
"AppData",
|
|
2864
|
+
"Application Data",
|
|
2865
|
+
"Library"
|
|
2866
|
+
]);
|
|
2867
|
+
const MAX_SCAN_DEPTH = 6;
|
|
2841
2868
|
const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
2842
2869
|
const packages = [];
|
|
2843
|
-
const pendingDirectories = [
|
|
2870
|
+
const pendingDirectories = [{
|
|
2871
|
+
directory: rootDirectory,
|
|
2872
|
+
depth: 0
|
|
2873
|
+
}];
|
|
2844
2874
|
while (pendingDirectories.length > 0) {
|
|
2845
|
-
const
|
|
2846
|
-
if (!
|
|
2875
|
+
const current = pendingDirectories.pop();
|
|
2876
|
+
if (!current) continue;
|
|
2877
|
+
const { directory: currentDirectory, depth } = current;
|
|
2847
2878
|
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
2848
2879
|
if (isFile(packageJsonPath)) {
|
|
2849
2880
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -2855,10 +2886,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
|
2855
2886
|
});
|
|
2856
2887
|
}
|
|
2857
2888
|
}
|
|
2889
|
+
if (depth >= MAX_SCAN_DEPTH) continue;
|
|
2858
2890
|
const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
|
|
2859
2891
|
for (const entry of entries) {
|
|
2860
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
2861
|
-
pendingDirectories.push(
|
|
2892
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
|
|
2893
|
+
pendingDirectories.push({
|
|
2894
|
+
directory: path.join(currentDirectory, entry.name),
|
|
2895
|
+
depth: depth + 1
|
|
2896
|
+
});
|
|
2862
2897
|
}
|
|
2863
2898
|
}
|
|
2864
2899
|
return packages;
|
|
@@ -2926,6 +2961,8 @@ const discoverProject = (directory) => {
|
|
|
2926
2961
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
2927
2962
|
const sourceFileCount = countSourceFiles(directory);
|
|
2928
2963
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
2964
|
+
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
2965
|
+
const preactVersion = getPreactVersion(packageJson);
|
|
2929
2966
|
const projectInfo = {
|
|
2930
2967
|
rootDirectory: directory,
|
|
2931
2968
|
projectName,
|
|
@@ -2936,12 +2973,50 @@ const discoverProject = (directory) => {
|
|
|
2936
2973
|
hasTypeScript,
|
|
2937
2974
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2938
2975
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
2976
|
+
preactVersion,
|
|
2977
|
+
preactMajorVersion: parseReactMajor(preactVersion),
|
|
2939
2978
|
hasReactNativeWorkspace,
|
|
2979
|
+
hasReanimated,
|
|
2940
2980
|
sourceFileCount
|
|
2941
2981
|
};
|
|
2942
2982
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2943
2983
|
return projectInfo;
|
|
2944
2984
|
};
|
|
2985
|
+
const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
2986
|
+
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
2987
|
+
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
2988
|
+
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
2989
|
+
const parseReactMajorMinor = (reactVersion) => {
|
|
2990
|
+
if (typeof reactVersion !== "string") return null;
|
|
2991
|
+
const trimmed = reactVersion.trim();
|
|
2992
|
+
if (trimmed.length === 0) return null;
|
|
2993
|
+
const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
|
|
2994
|
+
if (lowerBoundsOnly.length === 0) return null;
|
|
2995
|
+
const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
|
|
2996
|
+
if (majorMinorMatch) {
|
|
2997
|
+
const major = Number.parseInt(majorMinorMatch[1], 10);
|
|
2998
|
+
const minor = Number.parseInt(majorMinorMatch[2], 10);
|
|
2999
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3000
|
+
if (!Number.isFinite(minor) || minor < 0) return null;
|
|
3001
|
+
return {
|
|
3002
|
+
major,
|
|
3003
|
+
minor
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
|
|
3007
|
+
if (!majorOnlyMatch) return null;
|
|
3008
|
+
const major = Number.parseInt(majorOnlyMatch[1], 10);
|
|
3009
|
+
if (!Number.isFinite(major) || major <= 0) return null;
|
|
3010
|
+
return {
|
|
3011
|
+
major,
|
|
3012
|
+
minor: 0
|
|
3013
|
+
};
|
|
3014
|
+
};
|
|
3015
|
+
const isReactAtLeast = (detected, required) => {
|
|
3016
|
+
if (detected === null) return true;
|
|
3017
|
+
if (detected.major !== required.major) return detected.major > required.major;
|
|
3018
|
+
return detected.minor >= required.minor;
|
|
3019
|
+
};
|
|
2945
3020
|
const parseTailwindMajorMinor = (tailwindVersion) => {
|
|
2946
3021
|
if (typeof tailwindVersion !== "string") return null;
|
|
2947
3022
|
const trimmed = tailwindVersion.trim();
|
|
@@ -2972,6 +3047,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
2972
3047
|
return detected.minor >= required.minor;
|
|
2973
3048
|
};
|
|
2974
3049
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3050
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
2975
3051
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
2976
3052
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
2977
3053
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -3876,17 +3952,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
3876
3952
|
headers
|
|
3877
3953
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
3878
3954
|
}).pipe(Effect.orDie));
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3955
|
+
/**
|
|
3956
|
+
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
3957
|
+
* startup so the eval harness can raise the budget under sandbox
|
|
3958
|
+
* microVMs without recompiling react-doctor. Tests override via
|
|
3959
|
+
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
3960
|
+
*/
|
|
3961
|
+
var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
3882
3962
|
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
3883
3963
|
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3884
3964
|
const parsed = Number(raw);
|
|
3885
3965
|
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3886
3966
|
return parsed;
|
|
3887
|
-
} });
|
|
3888
|
-
|
|
3889
|
-
|
|
3967
|
+
} }) {};
|
|
3968
|
+
/**
|
|
3969
|
+
* Hard cap on combined stdout+stderr bytes per oxlint batch. The
|
|
3970
|
+
* subprocess gets SIGKILL'd if it produces more; the recovery path
|
|
3971
|
+
* suggests narrowing the scan with --diff. Override via Layer in
|
|
3972
|
+
* tests that exercise the cap behavior.
|
|
3973
|
+
*/
|
|
3974
|
+
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
3890
3975
|
const DIAGNOSTIC_SURFACES = [
|
|
3891
3976
|
"cli",
|
|
3892
3977
|
"prComment",
|
|
@@ -4491,6 +4576,59 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4491
4576
|
return patterns;
|
|
4492
4577
|
};
|
|
4493
4578
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4579
|
+
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4580
|
+
const inputChunks = [];
|
|
4581
|
+
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
4582
|
+
process.stdin.on("end", () => {
|
|
4583
|
+
const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
|
|
4584
|
+
|
|
4585
|
+
const normalizeResult = (result) => ({
|
|
4586
|
+
unusedFiles: result.unusedFiles.map((unusedFile) => ({
|
|
4587
|
+
path: unusedFile.path,
|
|
4588
|
+
})),
|
|
4589
|
+
unusedExports: result.unusedExports.map((unusedExport) => ({
|
|
4590
|
+
path: unusedExport.path,
|
|
4591
|
+
name: unusedExport.name,
|
|
4592
|
+
line: unusedExport.line,
|
|
4593
|
+
column: unusedExport.column,
|
|
4594
|
+
isTypeOnly: unusedExport.isTypeOnly,
|
|
4595
|
+
})),
|
|
4596
|
+
unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
|
|
4597
|
+
name: unusedDependency.name,
|
|
4598
|
+
isDevDependency: unusedDependency.isDevDependency,
|
|
4599
|
+
})),
|
|
4600
|
+
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
4601
|
+
files: cycle.files,
|
|
4602
|
+
})),
|
|
4603
|
+
});
|
|
4604
|
+
|
|
4605
|
+
const serializeError = (error) =>
|
|
4606
|
+
error instanceof Error
|
|
4607
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4608
|
+
: { message: String(error) };
|
|
4609
|
+
|
|
4610
|
+
const emit = (message) => {
|
|
4611
|
+
process.stdout.write(JSON.stringify(message), () => process.exit(0));
|
|
4612
|
+
};
|
|
4613
|
+
|
|
4614
|
+
(async () => {
|
|
4615
|
+
try {
|
|
4616
|
+
const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
|
|
4617
|
+
const config = {
|
|
4618
|
+
rootDir: workerInput.rootDirectory,
|
|
4619
|
+
...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
|
|
4620
|
+
...(workerInput.ignorePatterns.length > 0
|
|
4621
|
+
? { ignorePatterns: workerInput.ignorePatterns }
|
|
4622
|
+
: {}),
|
|
4623
|
+
};
|
|
4624
|
+
const result = await analyze(defineConfig(config));
|
|
4625
|
+
emit({ ok: true, result: normalizeResult(result) });
|
|
4626
|
+
} catch (error) {
|
|
4627
|
+
emit({ ok: false, error: serializeError(error) });
|
|
4628
|
+
}
|
|
4629
|
+
})();
|
|
4630
|
+
});
|
|
4631
|
+
`;
|
|
4494
4632
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4495
4633
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
4496
4634
|
const candidate = path.join(rootDirectory, filename);
|
|
@@ -4511,16 +4649,191 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
|
4511
4649
|
const relative = toRelativePath(filePath, rootDirectory);
|
|
4512
4650
|
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
4513
4651
|
};
|
|
4652
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4653
|
+
const parseArray = (value, label) => {
|
|
4654
|
+
if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4655
|
+
return value;
|
|
4656
|
+
};
|
|
4657
|
+
const parseString = (value, label) => {
|
|
4658
|
+
if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4659
|
+
return value;
|
|
4660
|
+
};
|
|
4661
|
+
const parseNumber = (value, label) => {
|
|
4662
|
+
if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4663
|
+
return value;
|
|
4664
|
+
};
|
|
4665
|
+
const parseBoolean = (value, label) => {
|
|
4666
|
+
if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
|
|
4667
|
+
return value;
|
|
4668
|
+
};
|
|
4669
|
+
const parseStringArray = (value, label) => {
|
|
4670
|
+
return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
|
|
4671
|
+
};
|
|
4672
|
+
const parseUnusedFiles = (value) => {
|
|
4673
|
+
const values = parseArray(value, "unusedFiles");
|
|
4674
|
+
const unusedFiles = [];
|
|
4675
|
+
for (const [index, entry] of values.entries()) {
|
|
4676
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
4677
|
+
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
4678
|
+
}
|
|
4679
|
+
return unusedFiles;
|
|
4680
|
+
};
|
|
4681
|
+
const parseUnusedExports = (value) => {
|
|
4682
|
+
const values = parseArray(value, "unusedExports");
|
|
4683
|
+
const unusedExports = [];
|
|
4684
|
+
for (const [index, entry] of values.entries()) {
|
|
4685
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
4686
|
+
unusedExports.push({
|
|
4687
|
+
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
4688
|
+
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
4689
|
+
line: parseNumber(entry.line, `unusedExports[${index}].line`),
|
|
4690
|
+
column: parseNumber(entry.column, `unusedExports[${index}].column`),
|
|
4691
|
+
isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
return unusedExports;
|
|
4695
|
+
};
|
|
4696
|
+
const parseUnusedDependencies = (value) => {
|
|
4697
|
+
const values = parseArray(value, "unusedDependencies");
|
|
4698
|
+
const unusedDependencies = [];
|
|
4699
|
+
for (const [index, entry] of values.entries()) {
|
|
4700
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
4701
|
+
unusedDependencies.push({
|
|
4702
|
+
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
4703
|
+
isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
return unusedDependencies;
|
|
4707
|
+
};
|
|
4708
|
+
const parseCircularDependencies = (value) => {
|
|
4709
|
+
const values = parseArray(value, "circularDependencies");
|
|
4710
|
+
const circularDependencies = [];
|
|
4711
|
+
for (const [index, entry] of values.entries()) {
|
|
4712
|
+
if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
4713
|
+
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
4714
|
+
}
|
|
4715
|
+
return circularDependencies;
|
|
4716
|
+
};
|
|
4717
|
+
const parseDeadCodeWorkerResult = (value) => {
|
|
4718
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
4719
|
+
return {
|
|
4720
|
+
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
4721
|
+
unusedExports: parseUnusedExports(value.unusedExports),
|
|
4722
|
+
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
4723
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
4724
|
+
};
|
|
4725
|
+
};
|
|
4726
|
+
const parseDeadCodeWorkerError = (value) => {
|
|
4727
|
+
if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
4728
|
+
return {
|
|
4729
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
4730
|
+
message: value.message,
|
|
4731
|
+
...typeof value.stack === "string" ? { stack: value.stack } : {}
|
|
4732
|
+
};
|
|
4733
|
+
};
|
|
4734
|
+
const parseDeadCodeWorkerMessage = (value) => {
|
|
4735
|
+
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
4736
|
+
if (value.ok === true) return {
|
|
4737
|
+
ok: true,
|
|
4738
|
+
result: value.result
|
|
4739
|
+
};
|
|
4740
|
+
if (value.ok === false) return {
|
|
4741
|
+
ok: false,
|
|
4742
|
+
error: parseDeadCodeWorkerError(value.error)
|
|
4743
|
+
};
|
|
4744
|
+
throw new Error("Dead-code worker returned an invalid status.");
|
|
4745
|
+
};
|
|
4746
|
+
const buildDeadCodeWorkerError = (workerError) => {
|
|
4747
|
+
const error = new Error(workerError.message);
|
|
4748
|
+
if (workerError.name !== void 0) error.name = workerError.name;
|
|
4749
|
+
if (workerError.stack !== void 0) error.stack = workerError.stack;
|
|
4750
|
+
return error;
|
|
4751
|
+
};
|
|
4752
|
+
const createDeadCodeWorker = (input) => {
|
|
4753
|
+
const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
|
|
4754
|
+
stdio: [
|
|
4755
|
+
"pipe",
|
|
4756
|
+
"pipe",
|
|
4757
|
+
"pipe"
|
|
4758
|
+
],
|
|
4759
|
+
windowsHide: true
|
|
4760
|
+
});
|
|
4761
|
+
const stdoutChunks = [];
|
|
4762
|
+
const stderrChunks = [];
|
|
4763
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
4764
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
4765
|
+
let didSettle = false;
|
|
4766
|
+
const result = new Promise((resolve, reject) => {
|
|
4767
|
+
const settle = (callback) => {
|
|
4768
|
+
if (didSettle) return;
|
|
4769
|
+
didSettle = true;
|
|
4770
|
+
callback();
|
|
4771
|
+
};
|
|
4772
|
+
child.once("error", (error) => {
|
|
4773
|
+
settle(() => reject(error));
|
|
4774
|
+
});
|
|
4775
|
+
child.once("close", (exitCode) => {
|
|
4776
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
4777
|
+
if (stdout.length === 0) {
|
|
4778
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
4779
|
+
settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
try {
|
|
4783
|
+
const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
|
|
4784
|
+
if (parsedMessage.ok) {
|
|
4785
|
+
settle(() => resolve(parsedMessage.result));
|
|
4786
|
+
return;
|
|
4787
|
+
}
|
|
4788
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4789
|
+
} catch (error) {
|
|
4790
|
+
settle(() => reject(error));
|
|
4791
|
+
}
|
|
4792
|
+
});
|
|
4793
|
+
});
|
|
4794
|
+
child.stdin.on("error", () => {});
|
|
4795
|
+
child.stdin.end(JSON.stringify(input));
|
|
4796
|
+
return {
|
|
4797
|
+
result,
|
|
4798
|
+
terminate: () => {
|
|
4799
|
+
didSettle = true;
|
|
4800
|
+
child.kill("SIGKILL");
|
|
4801
|
+
}
|
|
4802
|
+
};
|
|
4803
|
+
};
|
|
4804
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
4805
|
+
let didSettle = false;
|
|
4806
|
+
const timeoutHandle = setTimeout(() => {
|
|
4807
|
+
if (didSettle) return;
|
|
4808
|
+
didSettle = true;
|
|
4809
|
+
handle.terminate?.();
|
|
4810
|
+
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
4811
|
+
}, timeoutMs);
|
|
4812
|
+
timeoutHandle.unref?.();
|
|
4813
|
+
handle.result.then((value) => {
|
|
4814
|
+
if (didSettle) return;
|
|
4815
|
+
didSettle = true;
|
|
4816
|
+
clearTimeout(timeoutHandle);
|
|
4817
|
+
handle.terminate?.();
|
|
4818
|
+
resolve(value);
|
|
4819
|
+
}, (error) => {
|
|
4820
|
+
if (didSettle) return;
|
|
4821
|
+
didSettle = true;
|
|
4822
|
+
clearTimeout(timeoutHandle);
|
|
4823
|
+
handle.terminate?.();
|
|
4824
|
+
reject(error);
|
|
4825
|
+
});
|
|
4826
|
+
});
|
|
4514
4827
|
const checkDeadCode = async (options) => {
|
|
4515
4828
|
const { rootDirectory, userConfig } = options;
|
|
4516
4829
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
4517
|
-
const { analyze, defineConfig } = await import("deslop-js");
|
|
4518
4830
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
4519
|
-
const result = await
|
|
4520
|
-
|
|
4831
|
+
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
4832
|
+
rootDirectory,
|
|
4521
4833
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
4522
|
-
|
|
4523
|
-
|
|
4834
|
+
ignorePatterns,
|
|
4835
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
4836
|
+
}), options.workerTimeoutMs ?? 12e4));
|
|
4524
4837
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
4525
4838
|
const diagnostics = [];
|
|
4526
4839
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -4726,8 +5039,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4726
5039
|
env: input.env,
|
|
4727
5040
|
extendEnv: true
|
|
4728
5041
|
}));
|
|
5042
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
5043
|
+
const stdoutByteCount = yield* Ref.make(0);
|
|
5044
|
+
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({
|
|
5045
|
+
args: [...input.args],
|
|
5046
|
+
directory: input.directory,
|
|
5047
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
5048
|
+
}) })) : Effect.void))));
|
|
4729
5049
|
const [stdout, stderr, status] = yield* Effect.all([
|
|
4730
|
-
Stream.mkString(Stream.decodeText(
|
|
5050
|
+
Stream.mkString(Stream.decodeText(stdoutStream)),
|
|
4731
5051
|
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
4732
5052
|
handle.exitCode
|
|
4733
5053
|
], { concurrency: 3 });
|
|
@@ -4889,7 +5209,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4889
5209
|
if (result.status !== 0) return [];
|
|
4890
5210
|
return splitNullSeparated(result.stdout);
|
|
4891
5211
|
})),
|
|
4892
|
-
showStagedContent: (directory, relativePath) =>
|
|
5212
|
+
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
5213
|
+
command: "git",
|
|
5214
|
+
args: ["show", `:${relativePath}`],
|
|
5215
|
+
directory,
|
|
5216
|
+
maxStdoutBytes: options?.maxBufferBytes
|
|
5217
|
+
}).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
|
|
4893
5218
|
grep: (input) => Effect.gen(function* () {
|
|
4894
5219
|
const args = ["grep"];
|
|
4895
5220
|
if (input.listMatchingFiles ?? true) args.push("-l");
|
|
@@ -4897,7 +5222,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
4897
5222
|
if (input.extendedRegexp ?? false) args.push("-E");
|
|
4898
5223
|
args.push(input.pattern);
|
|
4899
5224
|
if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
|
|
4900
|
-
const result = yield*
|
|
5225
|
+
const result = yield* runCommand({
|
|
5226
|
+
command: "git",
|
|
5227
|
+
args,
|
|
5228
|
+
directory: input.directory,
|
|
5229
|
+
maxStdoutBytes: input.maxBufferBytes
|
|
5230
|
+
});
|
|
4901
5231
|
if (result.status === 128) return null;
|
|
4902
5232
|
return {
|
|
4903
5233
|
status: result.status,
|
|
@@ -5144,7 +5474,16 @@ const buildCapabilities = (project) => {
|
|
|
5144
5474
|
capabilities.add(project.framework);
|
|
5145
5475
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5146
5476
|
const reactMajor = project.reactMajorVersion;
|
|
5147
|
-
if (reactMajor !== null)
|
|
5477
|
+
if (reactMajor !== null) {
|
|
5478
|
+
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
5479
|
+
for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
|
|
5480
|
+
if (reactMajor >= 19) {
|
|
5481
|
+
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5482
|
+
major: 19,
|
|
5483
|
+
minor: 2
|
|
5484
|
+
})) capabilities.add("react:19.2");
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5148
5487
|
if (project.tailwindVersion !== null) {
|
|
5149
5488
|
capabilities.add("tailwind");
|
|
5150
5489
|
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
@@ -5155,6 +5494,15 @@ const buildCapabilities = (project) => {
|
|
|
5155
5494
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5156
5495
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5157
5496
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5497
|
+
if (project.preactVersion !== null) {
|
|
5498
|
+
capabilities.add("preact");
|
|
5499
|
+
const preactMajor = project.preactMajorVersion;
|
|
5500
|
+
if (preactMajor !== null) {
|
|
5501
|
+
const cappedPreactMajor = Math.min(preactMajor, 20);
|
|
5502
|
+
for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
|
|
5503
|
+
}
|
|
5504
|
+
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5505
|
+
}
|
|
5158
5506
|
return capabilities;
|
|
5159
5507
|
};
|
|
5160
5508
|
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
@@ -5409,6 +5757,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
|
|
|
5409
5757
|
if (!publicEnvPrefix) return fallbackRecommendation;
|
|
5410
5758
|
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
5759
|
};
|
|
5760
|
+
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";
|
|
5761
|
+
const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
5762
|
+
if (rule !== "immutability") return help;
|
|
5763
|
+
if (!project.hasReanimated) return help;
|
|
5764
|
+
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5765
|
+
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5766
|
+
};
|
|
5412
5767
|
const REACT_MODULE_SOURCE = "react";
|
|
5413
5768
|
const REQUIRE_IDENTIFIER = "require";
|
|
5414
5769
|
const USE_IDENTIFIER = "use";
|
|
@@ -5732,7 +6087,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
|
|
|
5732
6087
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
5733
6088
|
if (plugin === "react-hooks-js") return {
|
|
5734
6089
|
message: REACT_COMPILER_MESSAGE,
|
|
5735
|
-
help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
|
|
6090
|
+
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
5736
6091
|
};
|
|
5737
6092
|
return {
|
|
5738
6093
|
message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
|
|
@@ -5801,13 +6156,6 @@ const SANITIZED_ENV = (() => {
|
|
|
5801
6156
|
}
|
|
5802
6157
|
return sanitized;
|
|
5803
6158
|
})();
|
|
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
6159
|
/**
|
|
5812
6160
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
5813
6161
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -5824,7 +6172,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
|
5824
6172
|
* The first three are splittable (the caller's binary-split retry
|
|
5825
6173
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
5826
6174
|
*/
|
|
5827
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
6175
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
5828
6176
|
const child = spawn(nodeBinaryPath, args, {
|
|
5829
6177
|
cwd: rootDirectory,
|
|
5830
6178
|
env: SANITIZED_ENV
|
|
@@ -5833,9 +6181,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5833
6181
|
child.kill("SIGKILL");
|
|
5834
6182
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5835
6183
|
kind: "timeout",
|
|
5836
|
-
detail: `${
|
|
6184
|
+
detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
|
|
5837
6185
|
}) }));
|
|
5838
|
-
},
|
|
6186
|
+
}, spawnTimeoutMs);
|
|
5839
6187
|
timeoutHandle.unref?.();
|
|
5840
6188
|
const stdoutBuffers = [];
|
|
5841
6189
|
const stderrBuffers = [];
|
|
@@ -5845,7 +6193,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5845
6193
|
const killIfTooLarge = (incomingBytes, isStdout) => {
|
|
5846
6194
|
if (isStdout) stdoutByteCount += incomingBytes;
|
|
5847
6195
|
else stderrByteCount += incomingBytes;
|
|
5848
|
-
if (stdoutByteCount + stderrByteCount >
|
|
6196
|
+
if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
|
|
5849
6197
|
didKillForSize = true;
|
|
5850
6198
|
child.kill("SIGKILL");
|
|
5851
6199
|
return true;
|
|
@@ -5871,7 +6219,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5871
6219
|
if (didKillForSize) {
|
|
5872
6220
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
5873
6221
|
kind: "output-too-large",
|
|
5874
|
-
detail: `exceeded ${
|
|
6222
|
+
detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
|
|
5875
6223
|
}) }));
|
|
5876
6224
|
return;
|
|
5877
6225
|
}
|
|
@@ -5912,7 +6260,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
5912
6260
|
* with a slimmer config in that case.
|
|
5913
6261
|
*/
|
|
5914
6262
|
const spawnLintBatches = async (input) => {
|
|
5915
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
|
|
6263
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
5916
6264
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
5917
6265
|
const allDiagnostics = [];
|
|
5918
6266
|
const droppedFiles = [];
|
|
@@ -5920,7 +6268,7 @@ const spawnLintBatches = async (input) => {
|
|
|
5920
6268
|
const spawnLintBatch = async (batch) => {
|
|
5921
6269
|
const batchArgs = [...baseArgs, ...batch];
|
|
5922
6270
|
try {
|
|
5923
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
|
|
6271
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
5924
6272
|
} catch (error) {
|
|
5925
6273
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
5926
6274
|
if (batch.length <= 1) {
|
|
@@ -6023,13 +6371,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
6023
6371
|
* 6. always restore disable directives + clean up the temp dir
|
|
6024
6372
|
*/
|
|
6025
6373
|
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;
|
|
6374
|
+
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
6375
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
6028
6376
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
6029
6377
|
validateRuleRegistration();
|
|
6030
6378
|
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
6379
|
const pluginPath = resolvePluginPath();
|
|
6034
6380
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
6035
6381
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
@@ -6044,6 +6390,8 @@ const runOxlint = async (options) => {
|
|
|
6044
6390
|
userPlugins
|
|
6045
6391
|
});
|
|
6046
6392
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
6393
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6394
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6047
6395
|
try {
|
|
6048
6396
|
const baseArgs = [
|
|
6049
6397
|
resolveOxlintBinary(),
|
|
@@ -6070,7 +6418,9 @@ const runOxlint = async (options) => {
|
|
|
6070
6418
|
nodeBinaryPath,
|
|
6071
6419
|
project,
|
|
6072
6420
|
onPartialFailure,
|
|
6073
|
-
onFileProgress: options.onFileProgress
|
|
6421
|
+
onFileProgress: options.onFileProgress,
|
|
6422
|
+
spawnTimeoutMs,
|
|
6423
|
+
outputMaxBytes
|
|
6074
6424
|
});
|
|
6075
6425
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6076
6426
|
try {
|
|
@@ -6136,6 +6486,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6136
6486
|
*/
|
|
6137
6487
|
static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
|
|
6138
6488
|
const partialFailures = yield* LintPartialFailures;
|
|
6489
|
+
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
6490
|
+
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
6139
6491
|
const collectedFailures = [];
|
|
6140
6492
|
const diagnostics = yield* Effect.tryPromise({
|
|
6141
6493
|
try: () => runOxlint({
|
|
@@ -6152,7 +6504,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6152
6504
|
onPartialFailure: (reason) => {
|
|
6153
6505
|
collectedFailures.push(reason);
|
|
6154
6506
|
},
|
|
6155
|
-
onFileProgress: input.onFileProgress
|
|
6507
|
+
onFileProgress: input.onFileProgress,
|
|
6508
|
+
spawnTimeoutMs,
|
|
6509
|
+
outputMaxBytes
|
|
6156
6510
|
}),
|
|
6157
6511
|
catch: ensureReactDoctorError
|
|
6158
6512
|
});
|
|
@@ -6450,7 +6804,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6450
6804
|
const resolvedConfig = yield* configService.resolve(input.directory);
|
|
6451
6805
|
const scanDirectory = resolvedConfig.resolvedDirectory;
|
|
6452
6806
|
const project = yield* projectService.discover(scanDirectory);
|
|
6453
|
-
if (project
|
|
6807
|
+
if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
|
|
6454
6808
|
const [repo, sha, defaultBranch] = yield* Effect.all([
|
|
6455
6809
|
gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
6456
6810
|
gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
@@ -6478,23 +6832,13 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6478
6832
|
const lintFailure = yield* Ref.make({
|
|
6479
6833
|
didFail: false,
|
|
6480
6834
|
reason: null,
|
|
6481
|
-
reasonTag: null
|
|
6835
|
+
reasonTag: null,
|
|
6836
|
+
reasonKind: null
|
|
6482
6837
|
});
|
|
6483
6838
|
const deadCodeFailure = yield* Ref.make({
|
|
6484
6839
|
didFail: false,
|
|
6485
6840
|
reason: null
|
|
6486
6841
|
});
|
|
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
6842
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
6499
6843
|
const scanStartTime = Date.now();
|
|
6500
6844
|
let lastReportedTotalFileCount = 0;
|
|
@@ -6511,24 +6855,32 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6511
6855
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
6512
6856
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
6513
6857
|
lastReportedTotalFileCount = totalFileCount;
|
|
6514
|
-
Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
|
|
6858
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
6515
6859
|
}
|
|
6516
6860
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6517
6861
|
yield* Ref.set(lintFailure, {
|
|
6518
6862
|
didFail: true,
|
|
6519
6863
|
reason: error.message,
|
|
6520
|
-
reasonTag: error.reason._tag
|
|
6864
|
+
reasonTag: error.reason._tag,
|
|
6865
|
+
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
6521
6866
|
});
|
|
6522
6867
|
return Stream.empty;
|
|
6523
6868
|
}))));
|
|
6524
6869
|
const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
|
|
6525
6870
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
6526
6871
|
yield* afterLint(lintFailureState.didFail);
|
|
6527
|
-
if (lintFailureState.didFail)
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6872
|
+
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
6873
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
6874
|
+
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
6875
|
+
rootDirectory: scanDirectory,
|
|
6876
|
+
userConfig: resolvedConfig.config
|
|
6877
|
+
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6878
|
+
yield* Ref.set(deadCodeFailure, {
|
|
6879
|
+
didFail: true,
|
|
6880
|
+
reason: error.message
|
|
6881
|
+
});
|
|
6882
|
+
return Stream.empty;
|
|
6883
|
+
}))))))));
|
|
6532
6884
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
6533
6885
|
const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
|
|
6534
6886
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
@@ -6570,6 +6922,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6570
6922
|
didLintFail: lintFailureState.didFail,
|
|
6571
6923
|
lintFailureReason: lintFailureState.reason,
|
|
6572
6924
|
lintFailureReasonTag: lintFailureState.reasonTag,
|
|
6925
|
+
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
6573
6926
|
lintPartialFailures,
|
|
6574
6927
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
6575
6928
|
deadCodeFailureReason: deadCodeFailureState.reason
|
|
@@ -7023,7 +7376,7 @@ var cli_logger_exports = /* @__PURE__ */ __exportAll({ cliLogger: () => cliLogge
|
|
|
7023
7376
|
/**
|
|
7024
7377
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
7025
7378
|
* the imperative CLI helper files (`select-projects`, `run-explain`,
|
|
7026
|
-
* `install-
|
|
7379
|
+
* `install-react-doctor`, the legacy paths in `cli/commands/inspect.ts`)
|
|
7027
7380
|
* that aren't yet Effect-typed. Every call drains into a single
|
|
7028
7381
|
* `Console.*` Effect via `Effect.runSync`, so the underlying logging
|
|
7029
7382
|
* pipeline is identical to the canonical `yield* Console.log(...)`
|
|
@@ -7056,4 +7409,4 @@ const cliLogger = {
|
|
|
7056
7409
|
//#endregion
|
|
7057
7410
|
export { isReactDoctorError as A, filterSourceFiles as C, groupBy as D, getDiffInfo as E, runInspect as F, toRelativePath as I, listWorkspacePackages as M, resolveScanTarget as N, highlighter as O, restoreLegacyThrow as P, filterDiagnosticsForSurface as S, formatReactDoctorError as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, layerOtlp as j, isMonorepoRoot as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, formatErrorChain as w, discoverReactSubprojects as x, buildJsonReport as y };
|
|
7058
7411
|
|
|
7059
|
-
//# sourceMappingURL=cli-logger-
|
|
7412
|
+
//# sourceMappingURL=cli-logger-Df45H6Lw.js.map
|