react-doctor 0.2.14-dev.3ceb748 → 0.2.14-dev.4bc8a73
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 +2 -0
- package/dist/cli.js +14284 -5747
- package/dist/index.d.ts +85 -10
- package/dist/index.js +766 -168
- package/package.json +5 -2
- package/dist/cli-logger-BgVL1vBI.js +0 -7722
- package/dist/rolldown-runtime-uZX_iqCz.js +0 -35
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ var Diagnostic = class extends Schema.Class("Diagnostic")({
|
|
|
59
59
|
plugin: Schema.String,
|
|
60
60
|
rule: Schema.String,
|
|
61
61
|
severity: Severity,
|
|
62
|
+
title: Schema.optional(Schema.String),
|
|
62
63
|
message: Schema.String,
|
|
63
64
|
help: Schema.String,
|
|
64
65
|
url: Schema.optional(Schema.String),
|
|
@@ -2094,6 +2095,8 @@ const isFile = (filePath) => {
|
|
|
2094
2095
|
}
|
|
2095
2096
|
};
|
|
2096
2097
|
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
2098
|
+
const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.js$/i;
|
|
2099
|
+
const MINIFIED_SNIFF_BYTES = 65536;
|
|
2097
2100
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
2098
2101
|
const IGNORED_DIRECTORIES = new Set([
|
|
2099
2102
|
".git",
|
|
@@ -2109,6 +2112,34 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
2109
2112
|
"out",
|
|
2110
2113
|
"storybook-static"
|
|
2111
2114
|
]);
|
|
2115
|
+
const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
|
|
2116
|
+
const isMinifiedSource = (absolutePath) => {
|
|
2117
|
+
let fileDescriptor;
|
|
2118
|
+
try {
|
|
2119
|
+
fileDescriptor = fs.openSync(absolutePath, "r");
|
|
2120
|
+
const buffer = Buffer.alloc(MINIFIED_SNIFF_BYTES);
|
|
2121
|
+
const bytesRead = fs.readSync(fileDescriptor, buffer, 0, MINIFIED_SNIFF_BYTES, 0);
|
|
2122
|
+
const prefix = buffer.toString("utf8", 0, bytesRead);
|
|
2123
|
+
const lines = prefix.split("\n");
|
|
2124
|
+
const longestLineLength = lines.reduce((longest, line) => Math.max(longest, line.length), 0);
|
|
2125
|
+
const averageLineLength = prefix.length / lines.length;
|
|
2126
|
+
return longestLineLength > 1e3 && averageLineLength > 500;
|
|
2127
|
+
} catch {
|
|
2128
|
+
return false;
|
|
2129
|
+
} finally {
|
|
2130
|
+
if (fileDescriptor !== void 0) fs.closeSync(fileDescriptor);
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
const isLargeMinifiedFile = (absolutePath) => {
|
|
2134
|
+
let sizeBytes;
|
|
2135
|
+
try {
|
|
2136
|
+
sizeBytes = fs.statSync(absolutePath).size;
|
|
2137
|
+
} catch {
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
if (sizeBytes < 2e4) return false;
|
|
2141
|
+
return isMinifiedSource(absolutePath);
|
|
2142
|
+
};
|
|
2112
2143
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
2113
2144
|
"EACCES",
|
|
2114
2145
|
"EPERM",
|
|
@@ -2139,7 +2170,7 @@ const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
2139
2170
|
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
|
|
2140
2171
|
continue;
|
|
2141
2172
|
}
|
|
2142
|
-
if (entry.isFile() &&
|
|
2173
|
+
if (entry.isFile() && isLintableSourceFile(entry.name) && !isLargeMinifiedFile(path.join(currentDirectory, entry.name))) count++;
|
|
2143
2174
|
}
|
|
2144
2175
|
}
|
|
2145
2176
|
return count;
|
|
@@ -2157,7 +2188,7 @@ const countSourceFilesViaGit = (rootDirectory) => {
|
|
|
2157
2188
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
2158
2189
|
});
|
|
2159
2190
|
if (result.error || result.status !== 0) return null;
|
|
2160
|
-
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 &&
|
|
2191
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !isLargeMinifiedFile(path.resolve(rootDirectory, filePath))).length;
|
|
2161
2192
|
};
|
|
2162
2193
|
const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
|
|
2163
2194
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
@@ -2843,29 +2874,34 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2843
2874
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2844
2875
|
};
|
|
2845
2876
|
};
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2877
|
+
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
2878
|
+
const rootValue = select(rootPackageJson);
|
|
2879
|
+
if (rootValue !== null) return rootValue;
|
|
2848
2880
|
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2849
|
-
if (patterns.length === 0) return
|
|
2881
|
+
if (patterns.length === 0) return null;
|
|
2850
2882
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2851
2883
|
for (const pattern of patterns) {
|
|
2852
|
-
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2884
|
+
const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
|
|
2853
2885
|
for (const workspaceDirectory of directories) {
|
|
2854
2886
|
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2855
2887
|
visitedDirectories.add(workspaceDirectory);
|
|
2856
|
-
|
|
2888
|
+
const value = select(readPackageJson(path.join(workspaceDirectory, "package.json")));
|
|
2889
|
+
if (value !== null) return value;
|
|
2857
2890
|
}
|
|
2858
2891
|
}
|
|
2859
|
-
return
|
|
2892
|
+
return null;
|
|
2860
2893
|
};
|
|
2894
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
|
|
2861
2895
|
const NAMES = new Set([
|
|
2862
2896
|
"react-native",
|
|
2863
2897
|
"react-native-tvos",
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2898
|
+
...new Set([
|
|
2899
|
+
"expo",
|
|
2900
|
+
"expo-router",
|
|
2901
|
+
"@expo/cli",
|
|
2902
|
+
"@expo/metro-config",
|
|
2903
|
+
"@expo/metro-runtime"
|
|
2904
|
+
]),
|
|
2869
2905
|
"react-native-windows",
|
|
2870
2906
|
"react-native-macos"
|
|
2871
2907
|
]);
|
|
@@ -2889,6 +2925,11 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2889
2925
|
return false;
|
|
2890
2926
|
};
|
|
2891
2927
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2928
|
+
const getExpoDependencySpec = (packageJson) => {
|
|
2929
|
+
const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
|
|
2930
|
+
return typeof spec === "string" ? spec : null;
|
|
2931
|
+
};
|
|
2932
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
|
|
2892
2933
|
const getPreactVersion = (packageJson) => {
|
|
2893
2934
|
return {
|
|
2894
2935
|
...packageJson.peerDependencies,
|
|
@@ -3128,6 +3169,19 @@ const discoverProject = (directory) => {
|
|
|
3128
3169
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
3129
3170
|
const sourceFileCount = countSourceFiles(directory);
|
|
3130
3171
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
3172
|
+
let expoVersion = hasReactNativeWorkspace ? findExpoVersion(directory, packageJson) : null;
|
|
3173
|
+
if (expoVersion !== null && isCatalogReference(expoVersion)) {
|
|
3174
|
+
const catalogName = extractCatalogName(expoVersion);
|
|
3175
|
+
let resolvedExpoVersion = resolveCatalogVersion(packageJson, "expo", directory, catalogName);
|
|
3176
|
+
if (!resolvedExpoVersion) {
|
|
3177
|
+
const monorepoRoot = findMonorepoRoot(directory);
|
|
3178
|
+
if (monorepoRoot) {
|
|
3179
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
3180
|
+
if (isFile(monorepoPackageJsonPath)) resolvedExpoVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "expo", monorepoRoot, catalogName);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
expoVersion = resolvedExpoVersion ?? expoVersion;
|
|
3184
|
+
}
|
|
3131
3185
|
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
3132
3186
|
const preactVersion = getPreactVersion(packageJson);
|
|
3133
3187
|
const projectInfo = {
|
|
@@ -3145,6 +3199,7 @@ const discoverProject = (directory) => {
|
|
|
3145
3199
|
preactVersion,
|
|
3146
3200
|
preactMajorVersion: parseReactMajor(preactVersion),
|
|
3147
3201
|
hasReactNativeWorkspace,
|
|
3202
|
+
expoVersion,
|
|
3148
3203
|
hasReanimated,
|
|
3149
3204
|
sourceFileCount
|
|
3150
3205
|
};
|
|
@@ -3240,7 +3295,18 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
|
3240
3295
|
];
|
|
3241
3296
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
3242
3297
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
3298
|
+
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
3243
3299
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
3300
|
+
const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
3301
|
+
"Security",
|
|
3302
|
+
"Bugs",
|
|
3303
|
+
"Performance",
|
|
3304
|
+
"Accessibility",
|
|
3305
|
+
"Maintainability"
|
|
3306
|
+
];
|
|
3307
|
+
const APP_ONLY_RULE_KEYS = new Set(["react-hooks-js/static-components", "react-doctor/no-render-prop-children"]);
|
|
3308
|
+
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
3309
|
+
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
3244
3310
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
3245
3311
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
3246
3312
|
var InvalidGlobPatternError = class extends Error {
|
|
@@ -3360,10 +3426,11 @@ const restampSeverity = (diagnostic, override) => {
|
|
|
3360
3426
|
*/
|
|
3361
3427
|
const buildRuleSeverityControls = (config) => {
|
|
3362
3428
|
if (!config) return void 0;
|
|
3363
|
-
if (config.rules === void 0 && config.categories === void 0
|
|
3429
|
+
if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
|
|
3364
3430
|
return {
|
|
3365
3431
|
...config.rules !== void 0 ? { rules: config.rules } : {},
|
|
3366
|
-
...config.categories !== void 0 ? { categories: config.categories } : {}
|
|
3432
|
+
...config.categories !== void 0 ? { categories: config.categories } : {},
|
|
3433
|
+
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
3367
3434
|
};
|
|
3368
3435
|
};
|
|
3369
3436
|
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
@@ -3727,6 +3794,69 @@ const resolveRuleSeverityOverride = (input, controls) => {
|
|
|
3727
3794
|
}
|
|
3728
3795
|
return input.category !== void 0 ? controls.categories?.[input.category] : void 0;
|
|
3729
3796
|
};
|
|
3797
|
+
const cachedRoleByPackageDirectory = /* @__PURE__ */ new Map();
|
|
3798
|
+
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
3799
|
+
const findNearestPackageDirectory = (filename) => {
|
|
3800
|
+
if (!filename) return null;
|
|
3801
|
+
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
3802
|
+
if (fromCache !== void 0) return fromCache;
|
|
3803
|
+
let currentDirectory = path.dirname(filename);
|
|
3804
|
+
while (true) {
|
|
3805
|
+
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
3806
|
+
let hasPackageJson = false;
|
|
3807
|
+
try {
|
|
3808
|
+
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
3809
|
+
} catch {
|
|
3810
|
+
hasPackageJson = false;
|
|
3811
|
+
}
|
|
3812
|
+
if (hasPackageJson) {
|
|
3813
|
+
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
3814
|
+
return currentDirectory;
|
|
3815
|
+
}
|
|
3816
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
3817
|
+
if (parentDirectory === currentDirectory) {
|
|
3818
|
+
cachedPackageDirectoryByFilename.set(filename, null);
|
|
3819
|
+
return null;
|
|
3820
|
+
}
|
|
3821
|
+
currentDirectory = parentDirectory;
|
|
3822
|
+
}
|
|
3823
|
+
};
|
|
3824
|
+
const readManifest = (packageJsonPath) => {
|
|
3825
|
+
try {
|
|
3826
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
3827
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
3828
|
+
return null;
|
|
3829
|
+
} catch {
|
|
3830
|
+
return null;
|
|
3831
|
+
}
|
|
3832
|
+
};
|
|
3833
|
+
const hasPublishContract = (manifest) => typeof manifest.name === "string" && manifest.name.length > 0 && manifest.exports !== void 0 && manifest.exports !== null && manifest.private !== true;
|
|
3834
|
+
const classifyByDirectoryCohort = (packageDirectory) => {
|
|
3835
|
+
let current = packageDirectory;
|
|
3836
|
+
while (true) {
|
|
3837
|
+
if (path.basename(current) === "apps") return "app";
|
|
3838
|
+
const parent = path.dirname(current);
|
|
3839
|
+
if (parent === current) return null;
|
|
3840
|
+
current = parent;
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
const clearPackageRoleCache = () => {
|
|
3844
|
+
cachedRoleByPackageDirectory.clear();
|
|
3845
|
+
cachedPackageDirectoryByFilename.clear();
|
|
3846
|
+
};
|
|
3847
|
+
const classifyPackageRole = (filename) => {
|
|
3848
|
+
if (!filename) return "unknown";
|
|
3849
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
3850
|
+
if (!packageDirectory) return "unknown";
|
|
3851
|
+
const cached = cachedRoleByPackageDirectory.get(packageDirectory);
|
|
3852
|
+
if (cached !== void 0) return cached;
|
|
3853
|
+
const manifest = readManifest(path.join(packageDirectory, "package.json"));
|
|
3854
|
+
let result;
|
|
3855
|
+
if (manifest && hasPublishContract(manifest)) result = "library";
|
|
3856
|
+
else result = classifyByDirectoryCohort(path.dirname(packageDirectory)) ?? "unknown";
|
|
3857
|
+
cachedRoleByPackageDirectory.set(packageDirectory, result);
|
|
3858
|
+
return result;
|
|
3859
|
+
};
|
|
3730
3860
|
/**
|
|
3731
3861
|
* Resolves the absolute path to read for a diagnostic's `filePath`,
|
|
3732
3862
|
* accounting for the various shapes oxlint emits:
|
|
@@ -3862,10 +3992,13 @@ const collectStringSet = (values) => {
|
|
|
3862
3992
|
* wins over `test-noise`)
|
|
3863
3993
|
* 2. severity overrides (top-level `rules` / `categories`, with
|
|
3864
3994
|
* `"off"` dropping)
|
|
3865
|
-
* 3.
|
|
3866
|
-
*
|
|
3995
|
+
* 3. warning suppression (only when `showWarnings` is false: drops every
|
|
3996
|
+
* `"warning"`-severity diagnostic unless a severity override opts a
|
|
3997
|
+
* specific rule / category back in)
|
|
3998
|
+
* 4. ignore filters (rules / file patterns / per-file overrides)
|
|
3999
|
+
* 5. `rn-no-raw-text` suppression via configured `textComponents` and
|
|
3867
4000
|
* `rawTextWrapperComponents` (config-driven JSX enclosure checks)
|
|
3868
|
-
*
|
|
4001
|
+
* 6. inline suppressions (`// react-doctor-disable-next-line ...`)
|
|
3869
4002
|
*
|
|
3870
4003
|
* Returns `null` when the diagnostic is dropped, the (possibly
|
|
3871
4004
|
* severity-restamped) diagnostic otherwise.
|
|
@@ -3875,7 +4008,7 @@ const collectStringSet = (values) => {
|
|
|
3875
4008
|
* `mergeAndFilterDiagnostics` wrapper apply this closure per element.
|
|
3876
4009
|
*/
|
|
3877
4010
|
const buildDiagnosticPipeline = (input) => {
|
|
3878
|
-
const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables } = input;
|
|
4011
|
+
const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables, showWarnings } = input;
|
|
3879
4012
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
3880
4013
|
const ignoredRules = new Set(Array.isArray(userConfig?.ignore?.rules) ? userConfig.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
3881
4014
|
const ignoredFilePatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -3886,6 +4019,15 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3886
4019
|
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
3887
4020
|
const fileLinesCache = /* @__PURE__ */ new Map();
|
|
3888
4021
|
const testFileCache = /* @__PURE__ */ new Map();
|
|
4022
|
+
const libraryFileCache = /* @__PURE__ */ new Map();
|
|
4023
|
+
const isLibraryFile = (filePath) => {
|
|
4024
|
+
let cached = libraryFileCache.get(filePath);
|
|
4025
|
+
if (cached === void 0) {
|
|
4026
|
+
cached = classifyPackageRole(resolveCandidateReadPath(rootDirectory, filePath)) === "library";
|
|
4027
|
+
libraryFileCache.set(filePath, cached);
|
|
4028
|
+
}
|
|
4029
|
+
return cached;
|
|
4030
|
+
};
|
|
3889
4031
|
const getFileLines = (filePath) => {
|
|
3890
4032
|
const cached = fileLinesCache.get(filePath);
|
|
3891
4033
|
if (cached !== void 0) return cached;
|
|
@@ -3912,6 +4054,10 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3912
4054
|
for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
|
|
3913
4055
|
return false;
|
|
3914
4056
|
};
|
|
4057
|
+
const isAppOnlyRule = (ruleIdentifier) => {
|
|
4058
|
+
for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
|
|
4059
|
+
return false;
|
|
4060
|
+
};
|
|
3915
4061
|
const isRnRawTextSuppressedByConfig = (diagnostic) => {
|
|
3916
4062
|
if (diagnostic.rule !== "rn-no-raw-text") return false;
|
|
3917
4063
|
if (diagnostic.line <= 0) return false;
|
|
@@ -3925,15 +4071,22 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3925
4071
|
return { apply: (diagnostic) => {
|
|
3926
4072
|
if (shouldAutoSuppress(diagnostic)) return null;
|
|
3927
4073
|
let current = diagnostic;
|
|
4074
|
+
let explicitSeverityOverride;
|
|
4075
|
+
let explicitRuleOverride;
|
|
3928
4076
|
if (severityControls) {
|
|
3929
4077
|
const { ruleKey, category } = getDiagnosticRuleIdentity(current);
|
|
3930
|
-
|
|
4078
|
+
explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
|
|
4079
|
+
explicitSeverityOverride = resolveRuleSeverityOverride({
|
|
3931
4080
|
ruleKey,
|
|
3932
4081
|
category
|
|
3933
4082
|
}, severityControls);
|
|
3934
|
-
if (
|
|
3935
|
-
if (
|
|
4083
|
+
if (explicitSeverityOverride === "off") return null;
|
|
4084
|
+
if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
|
|
3936
4085
|
}
|
|
4086
|
+
if (explicitRuleOverride === void 0) {
|
|
4087
|
+
if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
|
|
4088
|
+
}
|
|
4089
|
+
if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
|
|
3937
4090
|
if (userConfig) {
|
|
3938
4091
|
if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
|
|
3939
4092
|
if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
|
|
@@ -4165,10 +4318,18 @@ const VALID_RULE_SEVERITIES = [
|
|
|
4165
4318
|
"warn",
|
|
4166
4319
|
"off"
|
|
4167
4320
|
];
|
|
4321
|
+
const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
|
|
4322
|
+
const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
|
|
4323
|
+
const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
|
|
4324
|
+
if (isDiagnosticCategoryBucket(category)) return true;
|
|
4325
|
+
warnConfigIssue(`config field "${fieldName}" lists "${category}", which is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
|
|
4326
|
+
return false;
|
|
4327
|
+
});
|
|
4168
4328
|
const BOOLEAN_FIELD_NAMES = [
|
|
4169
4329
|
"lint",
|
|
4170
4330
|
"deadCode",
|
|
4171
4331
|
"verbose",
|
|
4332
|
+
"warnings",
|
|
4172
4333
|
"customRulesOnly",
|
|
4173
4334
|
"share",
|
|
4174
4335
|
"noScore",
|
|
@@ -4217,13 +4378,15 @@ const validateSurfaceControls = (surface, rawControls) => {
|
|
|
4217
4378
|
warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
|
|
4218
4379
|
return;
|
|
4219
4380
|
}
|
|
4220
|
-
const
|
|
4381
|
+
const validatedSurfaceControls = {};
|
|
4221
4382
|
for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
|
|
4222
4383
|
if (rawControls[fieldName] === void 0) continue;
|
|
4223
|
-
const
|
|
4224
|
-
|
|
4384
|
+
const qualifiedName = `surfaces.${surface}.${fieldName}`;
|
|
4385
|
+
const result = validateStringArrayField(qualifiedName, rawControls[fieldName]);
|
|
4386
|
+
if (result === void 0) continue;
|
|
4387
|
+
validatedSurfaceControls[fieldName] = fieldName === "includeCategories" || fieldName === "excludeCategories" ? filterKnownCategories(qualifiedName, result) : result;
|
|
4225
4388
|
}
|
|
4226
|
-
return
|
|
4389
|
+
return validatedSurfaceControls;
|
|
4227
4390
|
};
|
|
4228
4391
|
const validateSurfacesField = (rawSurfaces) => {
|
|
4229
4392
|
if (!isPlainObject$1(rawSurfaces)) {
|
|
@@ -4241,7 +4404,7 @@ const validateSurfacesField = (rawSurfaces) => {
|
|
|
4241
4404
|
}
|
|
4242
4405
|
return validated;
|
|
4243
4406
|
};
|
|
4244
|
-
const validateSeverityMap = (fieldName, rawMap) => {
|
|
4407
|
+
const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
|
|
4245
4408
|
if (!isPlainObject$1(rawMap)) {
|
|
4246
4409
|
warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
|
|
4247
4410
|
return;
|
|
@@ -4252,6 +4415,10 @@ const validateSeverityMap = (fieldName, rawMap) => {
|
|
|
4252
4415
|
warnConfigIssue(`config field "${fieldName}" has an empty key; ignoring the entry.`);
|
|
4253
4416
|
continue;
|
|
4254
4417
|
}
|
|
4418
|
+
if (keysAreCategories && !isDiagnosticCategoryBucket(key)) {
|
|
4419
|
+
warnConfigIssue(`config field "${fieldName}.${key}" is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4255
4422
|
if (!isRuleSeverity(value)) {
|
|
4256
4423
|
warnConfigIssue(`config field "${fieldName}.${key}" must be one of: ${VALID_RULE_SEVERITIES.join(", ")} (got ${formatType(value)}); ignoring the entry.`);
|
|
4257
4424
|
continue;
|
|
@@ -4272,7 +4439,7 @@ const validateConfigTypes = (config) => {
|
|
|
4272
4439
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
4273
4440
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
4274
4441
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
4275
|
-
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value));
|
|
4442
|
+
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
4276
4443
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
4277
4444
|
return validated;
|
|
4278
4445
|
};
|
|
@@ -4356,11 +4523,12 @@ const resolveConfigRootDir = (config, configSourceDirectory) => {
|
|
|
4356
4523
|
}
|
|
4357
4524
|
return resolvedRootDir;
|
|
4358
4525
|
};
|
|
4359
|
-
const resolveDiagnoseTarget = (directory) => {
|
|
4526
|
+
const resolveDiagnoseTarget = (directory, options = {}) => {
|
|
4360
4527
|
if (isFile(path.join(directory, "package.json"))) return directory;
|
|
4361
4528
|
const reactSubprojects = discoverReactSubprojects(directory);
|
|
4362
4529
|
if (reactSubprojects.length === 0) return null;
|
|
4363
4530
|
if (reactSubprojects.length === 1) return reactSubprojects[0].directory;
|
|
4531
|
+
if (options.allowAmbiguous === true) return null;
|
|
4364
4532
|
throw new AmbiguousProjectError(directory, reactSubprojects.map((subproject) => path.relative(directory, subproject.directory)).toSorted());
|
|
4365
4533
|
};
|
|
4366
4534
|
/**
|
|
@@ -4374,7 +4542,8 @@ const resolveDiagnoseTarget = (directory) => {
|
|
|
4374
4542
|
* project root, if configured.
|
|
4375
4543
|
* 4. Walk into a nested React subproject when the requested
|
|
4376
4544
|
* directory has no `package.json` of its own (raises
|
|
4377
|
-
* `AmbiguousProjectError` when multiple candidates exist
|
|
4545
|
+
* `AmbiguousProjectError` when multiple candidates exist unless
|
|
4546
|
+
* the caller opts into keeping the wrapper directory).
|
|
4378
4547
|
*
|
|
4379
4548
|
* Throws `ProjectNotFoundError` when neither the requested directory
|
|
4380
4549
|
* nor any discoverable nested project has a `package.json`.
|
|
@@ -4386,14 +4555,14 @@ const resolveDiagnoseTarget = (directory) => {
|
|
|
4386
4555
|
* via its own cache). Routing through `resolveScanTarget` keeps every
|
|
4387
4556
|
* shell in agreement on what "the scan directory" means.
|
|
4388
4557
|
*/
|
|
4389
|
-
const resolveScanTarget = (requestedDirectory) => {
|
|
4558
|
+
const resolveScanTarget = (requestedDirectory, options = {}) => {
|
|
4390
4559
|
const absoluteRequested = path.resolve(requestedDirectory);
|
|
4391
4560
|
const loadedConfig = loadConfigWithSource(absoluteRequested);
|
|
4392
4561
|
const userConfig = loadedConfig?.config ?? null;
|
|
4393
4562
|
const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
|
|
4394
4563
|
const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
|
|
4395
4564
|
const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
|
|
4396
|
-
const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
|
|
4565
|
+
const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect, options) ?? directoryAfterRedirect;
|
|
4397
4566
|
if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
|
|
4398
4567
|
return {
|
|
4399
4568
|
resolvedDirectory,
|
|
@@ -4403,6 +4572,359 @@ const resolveScanTarget = (requestedDirectory) => {
|
|
|
4403
4572
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
4404
4573
|
};
|
|
4405
4574
|
};
|
|
4575
|
+
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
4576
|
+
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
4577
|
+
const packageJson = readPackageJson(path.join(rootDirectory, "package.json"));
|
|
4578
|
+
return {
|
|
4579
|
+
rootDirectory,
|
|
4580
|
+
packageJson,
|
|
4581
|
+
directDependencyNames: getDirectDependencyNames(packageJson),
|
|
4582
|
+
expoSdkMajor: getLowestDependencyMajor(expoVersion)
|
|
4583
|
+
};
|
|
4584
|
+
};
|
|
4585
|
+
const buildExpoDiagnostic = (input) => ({
|
|
4586
|
+
filePath: input.filePath ?? "package.json",
|
|
4587
|
+
plugin: "react-doctor",
|
|
4588
|
+
rule: input.rule,
|
|
4589
|
+
severity: input.severity ?? "warning",
|
|
4590
|
+
message: input.message,
|
|
4591
|
+
help: input.help,
|
|
4592
|
+
line: input.line ?? 0,
|
|
4593
|
+
column: input.column ?? 0,
|
|
4594
|
+
category: input.category ?? "Correctness"
|
|
4595
|
+
});
|
|
4596
|
+
const CRITICAL_OVERRIDE_NAMES = new Set([
|
|
4597
|
+
"@expo/cli",
|
|
4598
|
+
"@expo/config",
|
|
4599
|
+
"@expo/metro-config",
|
|
4600
|
+
"@expo/metro-runtime",
|
|
4601
|
+
"@expo/metro",
|
|
4602
|
+
"metro"
|
|
4603
|
+
]);
|
|
4604
|
+
const isCriticalOverrideName = (packageName) => CRITICAL_OVERRIDE_NAMES.has(packageName) || packageName.startsWith("metro-");
|
|
4605
|
+
const collectOverrideNames = (packageJson) => new Set([
|
|
4606
|
+
...Object.keys(packageJson.overrides ?? {}),
|
|
4607
|
+
...Object.keys(packageJson.resolutions ?? {}),
|
|
4608
|
+
...Object.keys(packageJson.pnpm?.overrides ?? {})
|
|
4609
|
+
]);
|
|
4610
|
+
const checkExpoDependencyOverrides = (context) => {
|
|
4611
|
+
const overriddenCriticalNames = [...collectOverrideNames(context.packageJson)].filter(isCriticalOverrideName).sort();
|
|
4612
|
+
if (overriddenCriticalNames.length === 0) return [];
|
|
4613
|
+
const quotedNames = overriddenCriticalNames.map((name) => `"${name}"`).join(", ");
|
|
4614
|
+
return [buildExpoDiagnostic({
|
|
4615
|
+
rule: "expo-no-conflicting-dependency-override",
|
|
4616
|
+
message: `package.json pins SDK-critical ${overriddenCriticalNames.length === 1 ? "package" : "packages"} via overrides/resolutions (${quotedNames}) — these versions are tied to the Expo SDK release and overriding them is unsupported and may break Metro or native builds`,
|
|
4617
|
+
help: `Remove the override/resolution for ${quotedNames} and reinstall so the Expo-pinned versions are used`
|
|
4618
|
+
})];
|
|
4619
|
+
};
|
|
4620
|
+
const isPathGitIgnored = (rootDirectory, absolutePath) => {
|
|
4621
|
+
const result = spawnSync("git", [
|
|
4622
|
+
"check-ignore",
|
|
4623
|
+
"-q",
|
|
4624
|
+
absolutePath
|
|
4625
|
+
], {
|
|
4626
|
+
cwd: rootDirectory,
|
|
4627
|
+
stdio: [
|
|
4628
|
+
"ignore",
|
|
4629
|
+
"ignore",
|
|
4630
|
+
"ignore"
|
|
4631
|
+
]
|
|
4632
|
+
});
|
|
4633
|
+
if (result.error) return null;
|
|
4634
|
+
if (result.status === 0) return true;
|
|
4635
|
+
if (result.status === 1) return false;
|
|
4636
|
+
return null;
|
|
4637
|
+
};
|
|
4638
|
+
const LOCAL_ENV_FILE_NAMES = [
|
|
4639
|
+
".env.local",
|
|
4640
|
+
".env.development.local",
|
|
4641
|
+
".env.production.local",
|
|
4642
|
+
".env.test.local"
|
|
4643
|
+
];
|
|
4644
|
+
const checkExpoEnvLocalFiles = (context) => {
|
|
4645
|
+
const { rootDirectory } = context;
|
|
4646
|
+
const committedEnvFiles = LOCAL_ENV_FILE_NAMES.filter((fileName) => {
|
|
4647
|
+
const filePath = path.join(rootDirectory, fileName);
|
|
4648
|
+
if (!isFile(filePath)) return false;
|
|
4649
|
+
return isPathGitIgnored(rootDirectory, filePath) === false;
|
|
4650
|
+
});
|
|
4651
|
+
if (committedEnvFiles.length === 0) return [];
|
|
4652
|
+
return [buildExpoDiagnostic({
|
|
4653
|
+
rule: "expo-env-local-not-gitignored",
|
|
4654
|
+
category: "Security",
|
|
4655
|
+
message: `Local environment ${committedEnvFiles.length === 1 ? "file" : "files"} (${committedEnvFiles.join(", ")}) ${committedEnvFiles.length === 1 ? "is" : "are"} not ignored by Git — committing \`.env*.local\` risks leaking secrets and overriding committed defaults for everyone who clones the project`,
|
|
4656
|
+
help: `Add \`.env*.local\` to your .gitignore. If already committed, untrack with \`git rm --cached ${committedEnvFiles.join(" ")}\``
|
|
4657
|
+
})];
|
|
4658
|
+
};
|
|
4659
|
+
const isExpoSdkAtLeast = (expoSdkMajor, minMajor) => expoSdkMajor !== null && expoSdkMajor >= minMajor;
|
|
4660
|
+
const UNIMODULES_HELP = "Remove every `@unimodules/*` and `react-native-unimodules` package — their functionality now lives in `expo-modules-core`. See https://expo.fyi/r/sdk-44-remove-unimodules";
|
|
4661
|
+
const FIREBASE_HELP = "Use the Firebase JS SDK or React Native Firebase directly. See https://expo.fyi/firebase-migration-guide";
|
|
4662
|
+
const unimodulesEntry = (packageName) => ({
|
|
4663
|
+
packageName,
|
|
4664
|
+
rule: "expo-no-unimodules-packages",
|
|
4665
|
+
message: `"${packageName}" is a legacy unimodules package that is incompatible with Expo SDK 44+ and will break native builds`,
|
|
4666
|
+
help: UNIMODULES_HELP
|
|
4667
|
+
});
|
|
4668
|
+
const FLAGGED_DEPENDENCIES = [
|
|
4669
|
+
unimodulesEntry("@unimodules/core"),
|
|
4670
|
+
unimodulesEntry("@unimodules/react-native-adapter"),
|
|
4671
|
+
unimodulesEntry("react-native-unimodules"),
|
|
4672
|
+
{
|
|
4673
|
+
packageName: "expo-cli",
|
|
4674
|
+
rule: "expo-no-cli-dependencies",
|
|
4675
|
+
message: "`expo-cli` (the legacy global CLI) is a project dependency — the CLI now ships inside the `expo` package, and keeping `expo-cli` causes failures such as `unknown option --fix` when running `npx expo install --fix`",
|
|
4676
|
+
help: "Remove `expo-cli` from your dependencies and use the bundled CLI via `npx expo`"
|
|
4677
|
+
},
|
|
4678
|
+
{
|
|
4679
|
+
packageName: "eas-cli",
|
|
4680
|
+
rule: "expo-no-cli-dependencies",
|
|
4681
|
+
message: "`eas-cli` is a project dependency — pinning it in package.json drifts from the latest EAS CLI and bloats installs",
|
|
4682
|
+
help: "Remove `eas-cli` from your dependencies and run it on demand with `npx eas-cli` (or install it globally)"
|
|
4683
|
+
},
|
|
4684
|
+
{
|
|
4685
|
+
packageName: "expo-modules-autolinking",
|
|
4686
|
+
rule: "expo-no-redundant-dependency",
|
|
4687
|
+
message: "\"expo-modules-autolinking\" should not be a direct dependency — Expo installs it transitively as needed",
|
|
4688
|
+
help: "Remove `expo-modules-autolinking` from your package.json"
|
|
4689
|
+
},
|
|
4690
|
+
{
|
|
4691
|
+
packageName: "expo-dev-launcher",
|
|
4692
|
+
rule: "expo-no-redundant-dependency",
|
|
4693
|
+
message: "\"expo-dev-launcher\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4694
|
+
help: "Remove `expo-dev-launcher` and depend on `expo-dev-client` instead"
|
|
4695
|
+
},
|
|
4696
|
+
{
|
|
4697
|
+
packageName: "expo-dev-menu",
|
|
4698
|
+
rule: "expo-no-redundant-dependency",
|
|
4699
|
+
message: "\"expo-dev-menu\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4700
|
+
help: "Remove `expo-dev-menu` and depend on `expo-dev-client` instead"
|
|
4701
|
+
},
|
|
4702
|
+
{
|
|
4703
|
+
packageName: "expo-modules-core",
|
|
4704
|
+
rule: "expo-no-redundant-dependency",
|
|
4705
|
+
message: "\"expo-modules-core\" should not be a direct dependency — use the API re-exported from the `expo` package",
|
|
4706
|
+
help: "Remove `expo-modules-core` from your package.json and import from `expo` instead"
|
|
4707
|
+
},
|
|
4708
|
+
{
|
|
4709
|
+
packageName: "@expo/metro-config",
|
|
4710
|
+
rule: "expo-no-redundant-dependency",
|
|
4711
|
+
message: "\"@expo/metro-config\" should not be a direct dependency — use the `expo/metro-config` sub-export of the `expo` package",
|
|
4712
|
+
help: "Remove `@expo/metro-config` and import `expo/metro-config` in your metro.config.js"
|
|
4713
|
+
},
|
|
4714
|
+
{
|
|
4715
|
+
packageName: "@types/react-native",
|
|
4716
|
+
rule: "expo-no-redundant-dependency",
|
|
4717
|
+
message: "\"@types/react-native\" should not be installed — React Native ships its own types since SDK 48",
|
|
4718
|
+
help: "Remove `@types/react-native` from your package.json",
|
|
4719
|
+
minSdkMajor: 48
|
|
4720
|
+
},
|
|
4721
|
+
{
|
|
4722
|
+
packageName: "@expo/config-plugins",
|
|
4723
|
+
rule: "expo-no-redundant-dependency",
|
|
4724
|
+
message: "\"@expo/config-plugins\" should not be a direct dependency — use the `expo/config-plugins` sub-export of the `expo` package",
|
|
4725
|
+
help: "Remove `@expo/config-plugins`; config-plugin authors should import from `expo/config-plugins`. See https://github.com/expo/expo/pull/18855",
|
|
4726
|
+
minSdkMajor: 48
|
|
4727
|
+
},
|
|
4728
|
+
{
|
|
4729
|
+
packageName: "@expo/prebuild-config",
|
|
4730
|
+
rule: "expo-no-redundant-dependency",
|
|
4731
|
+
message: "\"@expo/prebuild-config\" should not be a direct dependency — Expo installs it transitively",
|
|
4732
|
+
help: "Remove `@expo/prebuild-config` from your package.json",
|
|
4733
|
+
minSdkMajor: 53
|
|
4734
|
+
},
|
|
4735
|
+
{
|
|
4736
|
+
packageName: "expo-permissions",
|
|
4737
|
+
rule: "expo-no-redundant-dependency",
|
|
4738
|
+
message: "\"expo-permissions\" was deprecated in SDK 41 and may no longer compile — permissions moved onto each module (e.g. `MediaLibrary.requestPermissionsAsync()`)",
|
|
4739
|
+
help: "Remove `expo-permissions` and request permissions from the relevant module instead",
|
|
4740
|
+
minSdkMajor: 50
|
|
4741
|
+
},
|
|
4742
|
+
{
|
|
4743
|
+
packageName: "expo-app-loading",
|
|
4744
|
+
rule: "expo-no-redundant-dependency",
|
|
4745
|
+
message: "\"expo-app-loading\" was removed in SDK 49",
|
|
4746
|
+
help: "Remove `expo-app-loading` and use `expo-splash-screen` instead. See https://docs.expo.dev/versions/latest/sdk/splash-screen/",
|
|
4747
|
+
minSdkMajor: 49
|
|
4748
|
+
},
|
|
4749
|
+
{
|
|
4750
|
+
packageName: "expo-firebase-analytics",
|
|
4751
|
+
rule: "expo-no-redundant-dependency",
|
|
4752
|
+
message: "\"expo-firebase-analytics\" was removed in SDK 48",
|
|
4753
|
+
help: FIREBASE_HELP,
|
|
4754
|
+
minSdkMajor: 48
|
|
4755
|
+
},
|
|
4756
|
+
{
|
|
4757
|
+
packageName: "expo-firebase-recaptcha",
|
|
4758
|
+
rule: "expo-no-redundant-dependency",
|
|
4759
|
+
message: "\"expo-firebase-recaptcha\" was removed in SDK 48",
|
|
4760
|
+
help: FIREBASE_HELP,
|
|
4761
|
+
minSdkMajor: 48
|
|
4762
|
+
},
|
|
4763
|
+
{
|
|
4764
|
+
packageName: "expo-firebase-core",
|
|
4765
|
+
rule: "expo-no-redundant-dependency",
|
|
4766
|
+
message: "\"expo-firebase-core\" was removed in SDK 48",
|
|
4767
|
+
help: FIREBASE_HELP,
|
|
4768
|
+
minSdkMajor: 48
|
|
4769
|
+
}
|
|
4770
|
+
];
|
|
4771
|
+
const checkExpoFlaggedDependencies = (context) => FLAGGED_DEPENDENCIES.filter((flaggedDependency) => {
|
|
4772
|
+
if (!context.directDependencyNames.has(flaggedDependency.packageName)) return false;
|
|
4773
|
+
if (flaggedDependency.minSdkMajor === void 0) return true;
|
|
4774
|
+
return isExpoSdkAtLeast(context.expoSdkMajor, flaggedDependency.minSdkMajor);
|
|
4775
|
+
}).map((flaggedDependency) => buildExpoDiagnostic({
|
|
4776
|
+
rule: flaggedDependency.rule,
|
|
4777
|
+
message: flaggedDependency.message,
|
|
4778
|
+
help: flaggedDependency.help
|
|
4779
|
+
}));
|
|
4780
|
+
const findLocalModuleNativeFiles = (rootDirectory) => {
|
|
4781
|
+
const modulesDirectory = path.join(rootDirectory, "modules");
|
|
4782
|
+
if (!isDirectory(modulesDirectory)) return [];
|
|
4783
|
+
const nativeFilePaths = [];
|
|
4784
|
+
for (const moduleEntry of readDirectoryEntries(modulesDirectory)) {
|
|
4785
|
+
if (!moduleEntry.isDirectory()) continue;
|
|
4786
|
+
const moduleDirectory = path.join(modulesDirectory, moduleEntry.name);
|
|
4787
|
+
const gradlePath = path.join(moduleDirectory, "android", "build.gradle");
|
|
4788
|
+
if (isFile(gradlePath)) nativeFilePaths.push(gradlePath);
|
|
4789
|
+
const iosDirectory = path.join(moduleDirectory, "ios");
|
|
4790
|
+
if (isDirectory(iosDirectory)) {
|
|
4791
|
+
for (const iosEntry of readDirectoryEntries(iosDirectory)) if (iosEntry.isFile() && iosEntry.name.endsWith(".podspec")) nativeFilePaths.push(path.join(iosDirectory, iosEntry.name));
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
return nativeFilePaths;
|
|
4795
|
+
};
|
|
4796
|
+
const checkExpoGitignore = (context) => {
|
|
4797
|
+
const { rootDirectory } = context;
|
|
4798
|
+
const diagnostics = [];
|
|
4799
|
+
const expoStateDirectory = path.join(rootDirectory, ".expo");
|
|
4800
|
+
if (isDirectory(expoStateDirectory) && isPathGitIgnored(rootDirectory, expoStateDirectory) === false) diagnostics.push(buildExpoDiagnostic({
|
|
4801
|
+
rule: "expo-gitignore",
|
|
4802
|
+
message: "The `.expo` directory is not ignored by Git — it holds machine-specific device history and dev-server settings that should not be committed",
|
|
4803
|
+
help: "Add `.expo/` to your .gitignore"
|
|
4804
|
+
}));
|
|
4805
|
+
if (findLocalModuleNativeFiles(rootDirectory).find((nativeFilePath) => isPathGitIgnored(rootDirectory, nativeFilePath) === true) !== void 0) diagnostics.push(buildExpoDiagnostic({
|
|
4806
|
+
rule: "expo-gitignore",
|
|
4807
|
+
message: "The native `ios`/`android` directories of a local Expo module under `modules/` are gitignored — usually caused by an overly broad `ios`/`android` ignore rule",
|
|
4808
|
+
help: "Use anchored patterns like `/ios` and `/android` in .gitignore so only the top-level native directories are excluded, not those inside `modules/`"
|
|
4809
|
+
}));
|
|
4810
|
+
return diagnostics;
|
|
4811
|
+
};
|
|
4812
|
+
const LOCKFILE_NAMES = [
|
|
4813
|
+
"pnpm-lock.yaml",
|
|
4814
|
+
"yarn.lock",
|
|
4815
|
+
"package-lock.json",
|
|
4816
|
+
"bun.lockb",
|
|
4817
|
+
"bun.lock"
|
|
4818
|
+
];
|
|
4819
|
+
const checkExpoLockfile = (context) => {
|
|
4820
|
+
const workspaceRoot = isMonorepoRoot(context.rootDirectory) ? context.rootDirectory : findMonorepoRoot(context.rootDirectory) ?? context.rootDirectory;
|
|
4821
|
+
const presentLockfiles = LOCKFILE_NAMES.filter((lockfileName) => isFile(path.join(workspaceRoot, lockfileName)));
|
|
4822
|
+
if (presentLockfiles.length === 0) return [buildExpoDiagnostic({
|
|
4823
|
+
rule: "expo-lockfile",
|
|
4824
|
+
message: "No lock file detected at the project root — installs are not reproducible, and EAS Build cannot infer your package manager",
|
|
4825
|
+
help: "Install dependencies with your package manager to generate a lock file, then commit it"
|
|
4826
|
+
})];
|
|
4827
|
+
if (presentLockfiles.length > 1) return [buildExpoDiagnostic({
|
|
4828
|
+
rule: "expo-lockfile",
|
|
4829
|
+
message: `Multiple lock files detected (${presentLockfiles.join(", ")}) — CI environments such as EAS Build infer the package manager from the lock file, so this is ambiguous`,
|
|
4830
|
+
help: "Delete the lock files for the package managers you are not using and keep only one"
|
|
4831
|
+
})];
|
|
4832
|
+
return [];
|
|
4833
|
+
};
|
|
4834
|
+
const METRO_CONFIG_FILE_NAMES = [
|
|
4835
|
+
"metro.config.js",
|
|
4836
|
+
"metro.config.cjs",
|
|
4837
|
+
"metro.config.mjs",
|
|
4838
|
+
"metro.config.ts"
|
|
4839
|
+
];
|
|
4840
|
+
const EXPO_METRO_CONFIG_EXTEND_SIGNALS = [
|
|
4841
|
+
"expo/metro-config",
|
|
4842
|
+
"@sentry/react-native/metro",
|
|
4843
|
+
"getSentryExpoConfig"
|
|
4844
|
+
];
|
|
4845
|
+
const checkExpoMetroConfig = (context) => {
|
|
4846
|
+
const metroConfigPath = METRO_CONFIG_FILE_NAMES.map((fileName) => path.join(context.rootDirectory, fileName)).find((candidatePath) => isFile(candidatePath));
|
|
4847
|
+
if (metroConfigPath === void 0) return [];
|
|
4848
|
+
let contents;
|
|
4849
|
+
try {
|
|
4850
|
+
contents = fs.readFileSync(metroConfigPath, "utf-8");
|
|
4851
|
+
} catch {
|
|
4852
|
+
return [];
|
|
4853
|
+
}
|
|
4854
|
+
if (EXPO_METRO_CONFIG_EXTEND_SIGNALS.some((signal) => contents.includes(signal))) return [];
|
|
4855
|
+
return [buildExpoDiagnostic({
|
|
4856
|
+
rule: "expo-metro-config",
|
|
4857
|
+
filePath: path.basename(metroConfigPath),
|
|
4858
|
+
message: "Your metro.config does not extend `expo/metro-config` — a custom Metro config that doesn't extend Expo's leads to unexpected, hard-to-debug bundling issues",
|
|
4859
|
+
help: "Update your metro config to extend `expo/metro-config`. See https://docs.expo.dev/guides/customizing-metro/"
|
|
4860
|
+
})];
|
|
4861
|
+
};
|
|
4862
|
+
const CONFLICTING_SCRIPT_NAMES = ["expo", "react-native"];
|
|
4863
|
+
const checkExpoPackageJsonConflicts = (context) => {
|
|
4864
|
+
const { packageJson } = context;
|
|
4865
|
+
const diagnostics = [];
|
|
4866
|
+
const conflictingScriptNames = CONFLICTING_SCRIPT_NAMES.filter((scriptName) => Boolean(packageJson.scripts?.[scriptName]));
|
|
4867
|
+
if (conflictingScriptNames.length > 0) {
|
|
4868
|
+
const quotedNames = conflictingScriptNames.map((name) => `"${name}"`).join(", ");
|
|
4869
|
+
const shadowsExpoCli = conflictingScriptNames.includes("expo");
|
|
4870
|
+
diagnostics.push(buildExpoDiagnostic({
|
|
4871
|
+
rule: "expo-package-json-conflict",
|
|
4872
|
+
message: `package.json defines ${quotedNames} ${conflictingScriptNames.length === 1 ? "as a script that conflicts" : "as scripts that conflict"} with binaries in node_modules/.bin${shadowsExpoCli ? " — a `expo` script shadows the Expo CLI and will likely cause build failures" : ""}`,
|
|
4873
|
+
help: "Rename these scripts so they don't collide with the `expo` / `react-native` binaries"
|
|
4874
|
+
}));
|
|
4875
|
+
}
|
|
4876
|
+
const packageName = packageJson.name;
|
|
4877
|
+
if (typeof packageName === "string" && context.directDependencyNames.has(packageName)) diagnostics.push(buildExpoDiagnostic({
|
|
4878
|
+
rule: "expo-package-json-conflict",
|
|
4879
|
+
message: `package.json "name" is "${packageName}", which collides with a dependency of the same name`,
|
|
4880
|
+
help: "Rename your package so it no longer matches one of its dependencies"
|
|
4881
|
+
}));
|
|
4882
|
+
return diagnostics;
|
|
4883
|
+
};
|
|
4884
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR = 56;
|
|
4885
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE = 57;
|
|
4886
|
+
const checkExpoRouterReactNavigation = (context) => {
|
|
4887
|
+
const { expoSdkMajor } = context;
|
|
4888
|
+
if (!isExpoSdkAtLeast(expoSdkMajor, EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR)) return [];
|
|
4889
|
+
if (expoSdkMajor !== null && expoSdkMajor >= EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE) return [];
|
|
4890
|
+
if (!context.directDependencyNames.has("expo-router")) return [];
|
|
4891
|
+
const reactNavigationNames = [...context.directDependencyNames].filter((packageName) => packageName.startsWith("@react-navigation/")).sort();
|
|
4892
|
+
if (reactNavigationNames.length === 0) return [];
|
|
4893
|
+
return [buildExpoDiagnostic({
|
|
4894
|
+
rule: "expo-router-no-react-navigation",
|
|
4895
|
+
message: `As of SDK 56, expo-router is no longer compatible with react-navigation, but ${reactNavigationNames.map((name) => `"${name}"`).join(", ")} ${reactNavigationNames.length === 1 ? "is" : "are"} installed as direct ${reactNavigationNames.length === 1 ? "dependency" : "dependencies"}`,
|
|
4896
|
+
help: "Remove these `@react-navigation/*` packages and replace direct imports with their expo-router equivalents. See https://docs.expo.dev/router/migrate/sdk-55-to-56/"
|
|
4897
|
+
})];
|
|
4898
|
+
};
|
|
4899
|
+
const VECTOR_ICONS_MIN_SDK_MAJOR = 56;
|
|
4900
|
+
const SCOPED_VECTOR_ICONS_NAMESPACE = "@react-native-vector-icons/";
|
|
4901
|
+
const CONFLICTING_VECTOR_ICONS_PACKAGES = ["@expo/vector-icons", "react-native-vector-icons"];
|
|
4902
|
+
const checkExpoVectorIcons = (context) => {
|
|
4903
|
+
if (!isExpoSdkAtLeast(context.expoSdkMajor, VECTOR_ICONS_MIN_SDK_MAJOR)) return [];
|
|
4904
|
+
const hasScopedPackage = [...context.directDependencyNames].some((packageName) => packageName.startsWith(SCOPED_VECTOR_ICONS_NAMESPACE));
|
|
4905
|
+
const hasConflictingPackage = CONFLICTING_VECTOR_ICONS_PACKAGES.some((packageName) => context.directDependencyNames.has(packageName));
|
|
4906
|
+
if (!hasScopedPackage || !hasConflictingPackage) return [];
|
|
4907
|
+
return [buildExpoDiagnostic({
|
|
4908
|
+
rule: "expo-vector-icons-conflict",
|
|
4909
|
+
message: "This project installs both the scoped `@react-native-vector-icons/*` packages and `@expo/vector-icons` (or the deprecated `react-native-vector-icons`) — mixing them causes icon-rendering conflicts",
|
|
4910
|
+
help: "Migrate to the scoped packages by running `npx @react-native-vector-icons/codemod`, then remove the conflicting package"
|
|
4911
|
+
})];
|
|
4912
|
+
};
|
|
4913
|
+
const checkExpoProject = (rootDirectory, project) => {
|
|
4914
|
+
if (project.expoVersion === null) return [];
|
|
4915
|
+
const context = buildExpoCheckContext(rootDirectory, project.expoVersion);
|
|
4916
|
+
return [
|
|
4917
|
+
...checkExpoFlaggedDependencies(context),
|
|
4918
|
+
...checkExpoDependencyOverrides(context),
|
|
4919
|
+
...checkExpoRouterReactNavigation(context),
|
|
4920
|
+
...checkExpoVectorIcons(context),
|
|
4921
|
+
...checkExpoPackageJsonConflicts(context),
|
|
4922
|
+
...checkExpoLockfile(context),
|
|
4923
|
+
...checkExpoGitignore(context),
|
|
4924
|
+
...checkExpoEnvLocalFiles(context),
|
|
4925
|
+
...checkExpoMetroConfig(context)
|
|
4926
|
+
];
|
|
4927
|
+
};
|
|
4406
4928
|
const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
4407
4929
|
const PNPM_LOCKFILE = "pnpm-lock.yaml";
|
|
4408
4930
|
const PACKAGE_JSON_FILE = "package.json";
|
|
@@ -4572,99 +5094,6 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
4572
5094
|
return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
4573
5095
|
};
|
|
4574
5096
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
4575
|
-
const toStringSet = (values) => {
|
|
4576
|
-
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
|
|
4577
|
-
return new Set(values.filter((value) => typeof value === "string" && value.length > 0));
|
|
4578
|
-
};
|
|
4579
|
-
const buildResolvedControls = (surface, userControls) => {
|
|
4580
|
-
const excludeTags = new Set(DEFAULT_SURFACE_EXCLUDED_TAGS[surface]);
|
|
4581
|
-
const includeTags = toStringSet(userControls?.includeTags);
|
|
4582
|
-
for (const tag of includeTags) excludeTags.delete(tag);
|
|
4583
|
-
for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
|
|
4584
|
-
return {
|
|
4585
|
-
includeTags,
|
|
4586
|
-
excludeTags,
|
|
4587
|
-
includeCategories: toStringSet(userControls?.includeCategories),
|
|
4588
|
-
excludeCategories: toStringSet(userControls?.excludeCategories),
|
|
4589
|
-
includeRuleKeys: toStringSet(userControls?.includeRules),
|
|
4590
|
-
excludeRuleKeys: toStringSet(userControls?.excludeRules)
|
|
4591
|
-
};
|
|
4592
|
-
};
|
|
4593
|
-
const intersects = (values, candidates) => values.some((value) => candidates.has(value));
|
|
4594
|
-
const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
4595
|
-
const resolved = buildResolvedControls(surface, config?.surfaces?.[surface]);
|
|
4596
|
-
const { ruleKey, category, tags } = getDiagnosticRuleIdentity(diagnostic);
|
|
4597
|
-
if (resolved.includeRuleKeys.has(ruleKey)) return true;
|
|
4598
|
-
if (resolved.includeCategories.has(category)) return true;
|
|
4599
|
-
if (intersects(tags, resolved.includeTags)) return true;
|
|
4600
|
-
if (resolved.excludeRuleKeys.has(ruleKey)) return false;
|
|
4601
|
-
if (resolved.excludeCategories.has(category)) return false;
|
|
4602
|
-
if (intersects(tags, resolved.excludeTags)) return false;
|
|
4603
|
-
return true;
|
|
4604
|
-
};
|
|
4605
|
-
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
4606
|
-
const listSourceFilesViaGit = (rootDirectory) => {
|
|
4607
|
-
const result = spawnSync("git", [
|
|
4608
|
-
"ls-files",
|
|
4609
|
-
"-z",
|
|
4610
|
-
"--cached",
|
|
4611
|
-
"--others",
|
|
4612
|
-
"--exclude-standard"
|
|
4613
|
-
], {
|
|
4614
|
-
cwd: rootDirectory,
|
|
4615
|
-
encoding: "utf-8",
|
|
4616
|
-
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
4617
|
-
});
|
|
4618
|
-
if (result.error || result.status !== 0) return null;
|
|
4619
|
-
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
4620
|
-
};
|
|
4621
|
-
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
4622
|
-
const filePaths = [];
|
|
4623
|
-
const stack = [rootDirectory];
|
|
4624
|
-
while (stack.length > 0) {
|
|
4625
|
-
const currentDirectory = stack.pop();
|
|
4626
|
-
const entries = readDirectoryEntries(currentDirectory);
|
|
4627
|
-
for (const entry of entries) {
|
|
4628
|
-
const absolutePath = path.join(currentDirectory, entry.name);
|
|
4629
|
-
if (entry.isDirectory()) {
|
|
4630
|
-
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
4631
|
-
continue;
|
|
4632
|
-
}
|
|
4633
|
-
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
4634
|
-
}
|
|
4635
|
-
}
|
|
4636
|
-
return filePaths;
|
|
4637
|
-
};
|
|
4638
|
-
const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
|
|
4639
|
-
const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
4640
|
-
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
4641
|
-
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
4642
|
-
return listSourceFiles(rootDirectory).filter((filePath) => {
|
|
4643
|
-
if (!JSX_FILE_PATTERN.test(filePath)) return false;
|
|
4644
|
-
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
4645
|
-
});
|
|
4646
|
-
};
|
|
4647
|
-
var Config = class Config extends Context.Service()("react-doctor/Config") {
|
|
4648
|
-
static layerNode = Layer.effect(Config, Effect.gen(function* () {
|
|
4649
|
-
const cache = yield* Cache.make({
|
|
4650
|
-
capacity: 16,
|
|
4651
|
-
timeToLive: CONFIG_CACHE_TTL_MS,
|
|
4652
|
-
lookup: (directory) => Effect.sync(() => {
|
|
4653
|
-
const loaded = loadConfigWithSource(directory);
|
|
4654
|
-
const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
|
|
4655
|
-
return {
|
|
4656
|
-
config: loaded?.config ?? null,
|
|
4657
|
-
resolvedDirectory: redirected ?? directory,
|
|
4658
|
-
configSourceDirectory: loaded?.sourceDirectory ?? null
|
|
4659
|
-
};
|
|
4660
|
-
})
|
|
4661
|
-
});
|
|
4662
|
-
return Config.of({ resolve: Effect.fn("Config.resolve")(function* (directory) {
|
|
4663
|
-
return yield* Cache.get(cache, directory);
|
|
4664
|
-
}) });
|
|
4665
|
-
}));
|
|
4666
|
-
static layerOf = (resolved) => Layer.succeed(Config, Config.of({ resolve: () => Effect.succeed(resolved) }));
|
|
4667
|
-
};
|
|
4668
5097
|
const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
|
|
4669
5098
|
const FALSY_VALUES = new Set([
|
|
4670
5099
|
"false",
|
|
@@ -4746,6 +5175,30 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4746
5175
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
4747
5176
|
return patterns;
|
|
4748
5177
|
};
|
|
5178
|
+
/**
|
|
5179
|
+
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
5180
|
+
* the input when it cannot be realpath'd (broken symlink, permission
|
|
5181
|
+
* error) so a best-effort normalization never throws.
|
|
5182
|
+
*
|
|
5183
|
+
* deslop's dead-code module graph is collected with `fast-glob` (which
|
|
5184
|
+
* keeps the scan root's symlinks intact) while imports are resolved
|
|
5185
|
+
* through `oxc-resolver` (which returns realpath'd targets). When the
|
|
5186
|
+
* project root sits behind a symlink — e.g. macOS iCloud-synced
|
|
5187
|
+
* `~/Documents` / `~/Desktop`, or a symlinked checkout — those two path
|
|
5188
|
+
* spaces diverge: every resolved import misses the graph and the files
|
|
5189
|
+
* they point at (commonly every `@/…` alias target) are mis-reported as
|
|
5190
|
+
* unreachable. Canonicalizing the root before the scan keeps both path
|
|
5191
|
+
* spaces in agreement.
|
|
5192
|
+
*/
|
|
5193
|
+
const toCanonicalPath = (filePath) => {
|
|
5194
|
+
try {
|
|
5195
|
+
return fs.realpathSync(filePath);
|
|
5196
|
+
} catch {
|
|
5197
|
+
return filePath;
|
|
5198
|
+
}
|
|
5199
|
+
};
|
|
5200
|
+
const DEAD_CODE_PLUGIN = "deslop";
|
|
5201
|
+
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
4749
5202
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4750
5203
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4751
5204
|
const inputChunks = [];
|
|
@@ -4921,7 +5374,11 @@ const buildDeadCodeWorkerError = (workerError) => {
|
|
|
4921
5374
|
return error;
|
|
4922
5375
|
};
|
|
4923
5376
|
const createDeadCodeWorker = (input) => {
|
|
4924
|
-
const child = spawn(process.execPath, [
|
|
5377
|
+
const child = spawn(process.execPath, [
|
|
5378
|
+
`--max-old-space-size=${DEAD_CODE_WORKER_MAX_OLD_SPACE_MB}`,
|
|
5379
|
+
"-e",
|
|
5380
|
+
DEAD_CODE_WORKER_SCRIPT
|
|
5381
|
+
], {
|
|
4925
5382
|
stdio: [
|
|
4926
5383
|
"pipe",
|
|
4927
5384
|
"pipe",
|
|
@@ -4996,7 +5453,8 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
4996
5453
|
});
|
|
4997
5454
|
});
|
|
4998
5455
|
const checkDeadCode = async (options) => {
|
|
4999
|
-
const {
|
|
5456
|
+
const { userConfig } = options;
|
|
5457
|
+
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
5000
5458
|
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
5001
5459
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
5002
5460
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
@@ -5009,59 +5467,162 @@ const checkDeadCode = async (options) => {
|
|
|
5009
5467
|
const diagnostics = [];
|
|
5010
5468
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
5011
5469
|
filePath: toRelative(unusedFile.path),
|
|
5012
|
-
plugin:
|
|
5470
|
+
plugin: DEAD_CODE_PLUGIN,
|
|
5013
5471
|
rule: "unused-file",
|
|
5014
5472
|
severity: "warning",
|
|
5015
5473
|
message: "Unused file — not reachable from any entry point",
|
|
5016
5474
|
help: "Delete the file if it is truly unreachable, or import it from an entry point.",
|
|
5017
5475
|
line: 0,
|
|
5018
5476
|
column: 0,
|
|
5019
|
-
category:
|
|
5477
|
+
category: DEAD_CODE_CATEGORY
|
|
5020
5478
|
});
|
|
5021
5479
|
for (const unusedExport of result.unusedExports) {
|
|
5022
5480
|
const label = unusedExport.isTypeOnly ? "type export" : "export";
|
|
5023
5481
|
diagnostics.push({
|
|
5024
5482
|
filePath: toRelative(unusedExport.path),
|
|
5025
|
-
plugin:
|
|
5483
|
+
plugin: DEAD_CODE_PLUGIN,
|
|
5026
5484
|
rule: unusedExport.isTypeOnly ? "unused-type" : "unused-export",
|
|
5027
5485
|
severity: "warning",
|
|
5028
5486
|
message: `Unused ${label}: \`${unusedExport.name}\``,
|
|
5029
5487
|
help: "Drop the `export` keyword (or remove the declaration) if no other module uses this symbol.",
|
|
5030
5488
|
line: unusedExport.line,
|
|
5031
5489
|
column: unusedExport.column,
|
|
5032
|
-
category:
|
|
5490
|
+
category: DEAD_CODE_CATEGORY
|
|
5033
5491
|
});
|
|
5034
5492
|
}
|
|
5035
5493
|
for (const unusedDependency of result.unusedDependencies) {
|
|
5036
5494
|
const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
|
|
5037
5495
|
diagnostics.push({
|
|
5038
5496
|
filePath: "package.json",
|
|
5039
|
-
plugin:
|
|
5497
|
+
plugin: DEAD_CODE_PLUGIN,
|
|
5040
5498
|
rule: unusedDependency.isDevDependency ? "unused-dev-dependency" : "unused-dependency",
|
|
5041
5499
|
severity: "warning",
|
|
5042
5500
|
message: `Unused ${label}: \`${unusedDependency.name}\``,
|
|
5043
5501
|
help: "Remove the dependency from package.json if it is genuinely unused.",
|
|
5044
5502
|
line: 0,
|
|
5045
5503
|
column: 0,
|
|
5046
|
-
category:
|
|
5504
|
+
category: DEAD_CODE_CATEGORY
|
|
5047
5505
|
});
|
|
5048
5506
|
}
|
|
5049
5507
|
for (const cycle of result.circularDependencies) {
|
|
5050
5508
|
if (cycle.files.length === 0) continue;
|
|
5051
5509
|
diagnostics.push({
|
|
5052
5510
|
filePath: toRelative(cycle.files[0]),
|
|
5053
|
-
plugin:
|
|
5511
|
+
plugin: DEAD_CODE_PLUGIN,
|
|
5054
5512
|
rule: "circular-dependency",
|
|
5055
5513
|
severity: "warning",
|
|
5056
5514
|
message: `Circular import cycle: ${cycle.files.map(toRelative).join(" → ")}`,
|
|
5057
5515
|
help: "Break the cycle by extracting the shared code into a third module that both files import.",
|
|
5058
5516
|
line: 0,
|
|
5059
5517
|
column: 0,
|
|
5060
|
-
category:
|
|
5518
|
+
category: DEAD_CODE_CATEGORY
|
|
5061
5519
|
});
|
|
5062
5520
|
}
|
|
5063
5521
|
return diagnostics;
|
|
5064
5522
|
};
|
|
5523
|
+
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
5524
|
+
const isSurfacingOverride = (override) => override === "warn" || override === "error";
|
|
5525
|
+
const deadCodeMaySurfaceWhenWarningsHidden = (userConfig) => {
|
|
5526
|
+
const severityControls = buildRuleSeverityControls(userConfig);
|
|
5527
|
+
if (!severityControls) return false;
|
|
5528
|
+
if (isSurfacingOverride(severityControls.categories?.["Maintainability"])) return true;
|
|
5529
|
+
for (const [ruleKey, override] of Object.entries(severityControls.rules ?? {})) if (ruleKey.startsWith(DEAD_CODE_RULE_KEY_PREFIX) && isSurfacingOverride(override)) return true;
|
|
5530
|
+
return false;
|
|
5531
|
+
};
|
|
5532
|
+
const toStringSet = (values) => {
|
|
5533
|
+
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
|
|
5534
|
+
return new Set(values.filter((value) => typeof value === "string" && value.length > 0));
|
|
5535
|
+
};
|
|
5536
|
+
const buildResolvedControls = (surface, userControls) => {
|
|
5537
|
+
const excludeTags = new Set(DEFAULT_SURFACE_EXCLUDED_TAGS[surface]);
|
|
5538
|
+
const includeTags = toStringSet(userControls?.includeTags);
|
|
5539
|
+
for (const tag of includeTags) excludeTags.delete(tag);
|
|
5540
|
+
for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
|
|
5541
|
+
return {
|
|
5542
|
+
includeTags,
|
|
5543
|
+
excludeTags,
|
|
5544
|
+
includeCategories: toStringSet(userControls?.includeCategories),
|
|
5545
|
+
excludeCategories: toStringSet(userControls?.excludeCategories),
|
|
5546
|
+
includeRuleKeys: toStringSet(userControls?.includeRules),
|
|
5547
|
+
excludeRuleKeys: toStringSet(userControls?.excludeRules)
|
|
5548
|
+
};
|
|
5549
|
+
};
|
|
5550
|
+
const intersects = (values, candidates) => values.some((value) => candidates.has(value));
|
|
5551
|
+
const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
5552
|
+
const resolved = buildResolvedControls(surface, config?.surfaces?.[surface]);
|
|
5553
|
+
const { ruleKey, category, tags } = getDiagnosticRuleIdentity(diagnostic);
|
|
5554
|
+
if (resolved.includeRuleKeys.has(ruleKey)) return true;
|
|
5555
|
+
if (resolved.includeCategories.has(category)) return true;
|
|
5556
|
+
if (intersects(tags, resolved.includeTags)) return true;
|
|
5557
|
+
if (resolved.excludeRuleKeys.has(ruleKey)) return false;
|
|
5558
|
+
if (resolved.excludeCategories.has(category)) return false;
|
|
5559
|
+
if (intersects(tags, resolved.excludeTags)) return false;
|
|
5560
|
+
return true;
|
|
5561
|
+
};
|
|
5562
|
+
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
5563
|
+
const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(path.resolve(rootDirectory, relativePath)));
|
|
5564
|
+
const listSourceFilesViaGit = (rootDirectory) => {
|
|
5565
|
+
const result = spawnSync("git", [
|
|
5566
|
+
"ls-files",
|
|
5567
|
+
"-z",
|
|
5568
|
+
"--cached",
|
|
5569
|
+
"--others",
|
|
5570
|
+
"--exclude-standard"
|
|
5571
|
+
], {
|
|
5572
|
+
cwd: rootDirectory,
|
|
5573
|
+
encoding: "utf-8",
|
|
5574
|
+
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
5575
|
+
});
|
|
5576
|
+
if (result.error || result.status !== 0) return null;
|
|
5577
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
|
|
5578
|
+
};
|
|
5579
|
+
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
5580
|
+
const filePaths = [];
|
|
5581
|
+
const stack = [rootDirectory];
|
|
5582
|
+
while (stack.length > 0) {
|
|
5583
|
+
const currentDirectory = stack.pop();
|
|
5584
|
+
const entries = readDirectoryEntries(currentDirectory);
|
|
5585
|
+
for (const entry of entries) {
|
|
5586
|
+
const absolutePath = path.join(currentDirectory, entry.name);
|
|
5587
|
+
if (entry.isDirectory()) {
|
|
5588
|
+
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5591
|
+
if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
return filePaths;
|
|
5595
|
+
};
|
|
5596
|
+
const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
5597
|
+
const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
5598
|
+
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
5599
|
+
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
5600
|
+
return listSourceFiles(rootDirectory).filter((filePath) => {
|
|
5601
|
+
if (!JSX_FILE_PATTERN.test(filePath)) return false;
|
|
5602
|
+
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
5603
|
+
});
|
|
5604
|
+
};
|
|
5605
|
+
var Config = class Config extends Context.Service()("react-doctor/Config") {
|
|
5606
|
+
static layerNode = Layer.effect(Config, Effect.gen(function* () {
|
|
5607
|
+
const cache = yield* Cache.make({
|
|
5608
|
+
capacity: 16,
|
|
5609
|
+
timeToLive: CONFIG_CACHE_TTL_MS,
|
|
5610
|
+
lookup: (directory) => Effect.sync(() => {
|
|
5611
|
+
const loaded = loadConfigWithSource(directory);
|
|
5612
|
+
const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
|
|
5613
|
+
return {
|
|
5614
|
+
config: loaded?.config ?? null,
|
|
5615
|
+
resolvedDirectory: redirected ?? directory,
|
|
5616
|
+
configSourceDirectory: loaded?.sourceDirectory ?? null
|
|
5617
|
+
};
|
|
5618
|
+
})
|
|
5619
|
+
});
|
|
5620
|
+
return Config.of({ resolve: Effect.fn("Config.resolve")(function* (directory) {
|
|
5621
|
+
return yield* Cache.get(cache, directory);
|
|
5622
|
+
}) });
|
|
5623
|
+
}));
|
|
5624
|
+
static layerOf = (resolved) => Layer.succeed(Config, Config.of({ resolve: () => Effect.succeed(resolved) }));
|
|
5625
|
+
};
|
|
5065
5626
|
/**
|
|
5066
5627
|
* `DeadCode` runs whole-project reachability analysis and streams
|
|
5067
5628
|
* diagnostics. Reachability is a whole-project property — the
|
|
@@ -5567,12 +6128,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
|
|
|
5567
6128
|
return null;
|
|
5568
6129
|
}
|
|
5569
6130
|
if (grepResult === null) return null;
|
|
5570
|
-
return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 &&
|
|
6131
|
+
return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
|
|
5571
6132
|
};
|
|
5572
6133
|
const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
|
|
5573
6134
|
const matches = [];
|
|
5574
6135
|
const checkFile = (relativePath) => {
|
|
5575
|
-
if (!
|
|
6136
|
+
if (!isLintableSourceFile(relativePath)) return;
|
|
5576
6137
|
const absolutePath = path.join(rootDirectory, relativePath);
|
|
5577
6138
|
let content;
|
|
5578
6139
|
try {
|
|
@@ -5644,6 +6205,7 @@ const buildCapabilities = (project) => {
|
|
|
5644
6205
|
const capabilities = /* @__PURE__ */ new Set();
|
|
5645
6206
|
capabilities.add(project.framework);
|
|
5646
6207
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
6208
|
+
if (project.expoVersion !== null) capabilities.add("expo");
|
|
5647
6209
|
const reactMajor = project.reactMajorVersion;
|
|
5648
6210
|
if (reactMajor !== null) {
|
|
5649
6211
|
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
@@ -5815,10 +6377,14 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
|
|
|
5815
6377
|
if (!fs.existsSync(rootDirectory)) return rootDirectory;
|
|
5816
6378
|
return fs.realpathSync(rootDirectory);
|
|
5817
6379
|
};
|
|
6380
|
+
const resolveCompilerCleanupBucketSeverity = (ruleKey, severityControls) => {
|
|
6381
|
+
if (!COMPILER_CLEANUP_RULE_KEYS.has(ruleKey)) return void 0;
|
|
6382
|
+
return severityControls?.buckets?.[COMPILER_CLEANUP_BUCKET];
|
|
6383
|
+
};
|
|
5818
6384
|
const applyRuleSeverityControls = (rules, severityControls) => {
|
|
5819
6385
|
const enabledRules = {};
|
|
5820
6386
|
for (const [ruleKey, defaultSeverity] of Object.entries(rules)) {
|
|
5821
|
-
const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? defaultSeverity;
|
|
6387
|
+
const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? resolveCompilerCleanupBucketSeverity(ruleKey, severityControls) ?? defaultSeverity;
|
|
5822
6388
|
if (severity === "off") continue;
|
|
5823
6389
|
enabledRules[ruleKey] = severity;
|
|
5824
6390
|
}
|
|
@@ -5860,7 +6426,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
5860
6426
|
category: rule.category
|
|
5861
6427
|
}, severityControls);
|
|
5862
6428
|
if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
|
|
5863
|
-
const severity = explicitSeverity ?? rule.severity;
|
|
6429
|
+
const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
|
|
5864
6430
|
if (severity === "off") continue;
|
|
5865
6431
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
5866
6432
|
}
|
|
@@ -6032,7 +6598,7 @@ const KNOWN_SECRET_RULES = [
|
|
|
6032
6598
|
}
|
|
6033
6599
|
];
|
|
6034
6600
|
const CANDIDATE_TOKEN_PATTERN = /[A-Za-z0-9_][A-Za-z0-9_-]*/g;
|
|
6035
|
-
const
|
|
6601
|
+
const HEX_DIGEST_PATTERN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
6036
6602
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6037
6603
|
const HAS_LETTER_PATTERN = /[A-Za-z]/;
|
|
6038
6604
|
const HAS_DIGIT_PATTERN = /[0-9]/;
|
|
@@ -6049,7 +6615,7 @@ const shannonEntropyBits = (value) => {
|
|
|
6049
6615
|
const looksLikeHighEntropySecret = (token) => {
|
|
6050
6616
|
if (token.length < 32) return false;
|
|
6051
6617
|
if (!HAS_LETTER_PATTERN.test(token) || !HAS_DIGIT_PATTERN.test(token)) return false;
|
|
6052
|
-
if (
|
|
6618
|
+
if (HEX_DIGEST_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
|
|
6053
6619
|
return shannonEntropyBits(token) >= 3;
|
|
6054
6620
|
};
|
|
6055
6621
|
const redactHighEntropyTokens = (text) => text.replace(CANDIDATE_TOKEN_PATTERN, (token) => looksLikeHighEntropySecret(token) ? REDACTED_PLACEHOLDER : token);
|
|
@@ -6376,25 +6942,26 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
|
|
|
6376
6942
|
return bindingResolution !== null && !bindingResolution.isReactUseBinding;
|
|
6377
6943
|
};
|
|
6378
6944
|
const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
|
|
6379
|
-
const
|
|
6945
|
+
const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
|
|
6946
|
+
const REACT_COMPILER_MESSAGE = "This component misses React Compiler's automatic memoization & re-renders more than it should. Rewrite the flagged code so the compiler can optimize it.";
|
|
6380
6947
|
const PLUGIN_CATEGORY_MAP = {
|
|
6381
|
-
react: "
|
|
6382
|
-
"react-hooks": "
|
|
6383
|
-
"react-hooks-js": "
|
|
6384
|
-
"react-doctor": "
|
|
6948
|
+
react: "Bugs",
|
|
6949
|
+
"react-hooks": "Bugs",
|
|
6950
|
+
"react-hooks-js": "Performance",
|
|
6951
|
+
"react-doctor": "Bugs",
|
|
6385
6952
|
"jsx-a11y": "Accessibility",
|
|
6386
|
-
effect: "
|
|
6387
|
-
eslint: "
|
|
6388
|
-
oxc: "
|
|
6389
|
-
typescript: "
|
|
6390
|
-
unicorn: "
|
|
6391
|
-
import: "
|
|
6392
|
-
promise: "
|
|
6393
|
-
n: "
|
|
6394
|
-
node: "
|
|
6395
|
-
vitest: "
|
|
6396
|
-
jest: "
|
|
6397
|
-
nextjs: "
|
|
6953
|
+
effect: "Bugs",
|
|
6954
|
+
eslint: "Bugs",
|
|
6955
|
+
oxc: "Bugs",
|
|
6956
|
+
typescript: "Bugs",
|
|
6957
|
+
unicorn: "Bugs",
|
|
6958
|
+
import: "Performance",
|
|
6959
|
+
promise: "Bugs",
|
|
6960
|
+
n: "Bugs",
|
|
6961
|
+
node: "Bugs",
|
|
6962
|
+
vitest: "Bugs",
|
|
6963
|
+
jest: "Bugs",
|
|
6964
|
+
nextjs: "Bugs"
|
|
6398
6965
|
};
|
|
6399
6966
|
const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
|
|
6400
6967
|
const getRuleRecommendation = (ruleName, project) => {
|
|
@@ -6402,6 +6969,8 @@ const getRuleRecommendation = (ruleName, project) => {
|
|
|
6402
6969
|
return reactDoctorPlugin.rules[ruleName]?.recommendation;
|
|
6403
6970
|
};
|
|
6404
6971
|
const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
|
|
6972
|
+
const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
|
|
6973
|
+
const resolveDiagnosticTitle = (plugin, rule) => plugin === "react-hooks-js" ? REACT_COMPILER_TITLE : getRuleTitle(rule);
|
|
6405
6974
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
6406
6975
|
const cleaned = resolveCleanedDiagnostic(typeof message === "string" ? message : "", typeof help === "string" ? help : "", plugin, rule, project);
|
|
6407
6976
|
return {
|
|
@@ -6430,7 +6999,7 @@ const parseRuleCode = (code) => {
|
|
|
6430
6999
|
rule: match[2]
|
|
6431
7000
|
};
|
|
6432
7001
|
};
|
|
6433
|
-
const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "
|
|
7002
|
+
const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
|
|
6434
7003
|
const isOxlintOutput = (value) => {
|
|
6435
7004
|
if (typeof value !== "object" || value === null) return false;
|
|
6436
7005
|
const candidate = value;
|
|
@@ -6454,7 +7023,16 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
6454
7023
|
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
|
|
6455
7024
|
}
|
|
6456
7025
|
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
|
|
6457
|
-
|
|
7026
|
+
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
7027
|
+
const isMinifiedDiagnosticFile = (filename) => {
|
|
7028
|
+
const absolutePath = path.isAbsolute(filename) ? filename : path.resolve(rootDirectory || ".", filename);
|
|
7029
|
+
const cached = minifiedFileCache.get(absolutePath);
|
|
7030
|
+
if (cached !== void 0) return cached;
|
|
7031
|
+
const minified = isMinifiedSource(absolutePath);
|
|
7032
|
+
minifiedFileCache.set(absolutePath, minified);
|
|
7033
|
+
return minified;
|
|
7034
|
+
};
|
|
7035
|
+
return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
|
|
6458
7036
|
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
6459
7037
|
const primaryLabel = diagnostic.labels[0];
|
|
6460
7038
|
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
|
|
@@ -6463,6 +7041,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
6463
7041
|
plugin,
|
|
6464
7042
|
rule,
|
|
6465
7043
|
severity: diagnostic.severity,
|
|
7044
|
+
title: resolveDiagnosticTitle(plugin, rule),
|
|
6466
7045
|
message: cleaned.message,
|
|
6467
7046
|
help: cleaned.help,
|
|
6468
7047
|
url: diagnostic.url,
|
|
@@ -6880,7 +7459,8 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
|
|
|
6880
7459
|
static layerNoop = Layer.succeed(Progress, Progress.of({ start: () => Effect.succeed({
|
|
6881
7460
|
update: () => Effect.void,
|
|
6882
7461
|
succeed: () => Effect.void,
|
|
6883
|
-
fail: () => Effect.void
|
|
7462
|
+
fail: () => Effect.void,
|
|
7463
|
+
stop: () => Effect.void
|
|
6884
7464
|
}) }));
|
|
6885
7465
|
static layerCapture = Layer.effect(Progress, Effect.map(ProgressCapture, (events) => Progress.of({ start: (text) => Effect.gen(function* () {
|
|
6886
7466
|
yield* Ref.update(events, (existing) => [...existing, {
|
|
@@ -6899,6 +7479,10 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
|
|
|
6899
7479
|
fail: (displayText) => Ref.update(events, (existing) => [...existing, {
|
|
6900
7480
|
_tag: "Failed",
|
|
6901
7481
|
text: displayText
|
|
7482
|
+
}]),
|
|
7483
|
+
stop: () => Ref.update(events, (existing) => [...existing, {
|
|
7484
|
+
_tag: "Stopped",
|
|
7485
|
+
text
|
|
6902
7486
|
}])
|
|
6903
7487
|
};
|
|
6904
7488
|
}) }))).pipe(Layer.provideMerge(ProgressCapture.layer));
|
|
@@ -7148,18 +7732,25 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7148
7732
|
repo
|
|
7149
7733
|
}).pipe(Effect.orElseSucceed(() => null)) : Effect.succeed(null));
|
|
7150
7734
|
const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
|
|
7735
|
+
const scannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => path.resolve(scanDirectory, relativePath)) : [];
|
|
7151
7736
|
const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
|
|
7152
7737
|
const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
|
|
7153
7738
|
yield* beforeLint(project, lintIncludePaths ?? void 0);
|
|
7154
7739
|
const isDiffMode = input.includePaths.length > 0;
|
|
7740
|
+
const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? false;
|
|
7155
7741
|
const transform = buildDiagnosticPipeline({
|
|
7156
7742
|
rootDirectory: scanDirectory,
|
|
7157
7743
|
userConfig: resolvedConfig.config,
|
|
7158
7744
|
readFileLinesSync: fileReader(filesService, scanDirectory),
|
|
7159
|
-
respectInlineDisables: input.respectInlineDisables
|
|
7745
|
+
respectInlineDisables: input.respectInlineDisables,
|
|
7746
|
+
showWarnings
|
|
7160
7747
|
});
|
|
7161
7748
|
const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
|
|
7162
|
-
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7749
|
+
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7750
|
+
...checkReducedMotion(scanDirectory),
|
|
7751
|
+
...checkPnpmHardening(scanDirectory),
|
|
7752
|
+
...checkExpoProject(scanDirectory, project)
|
|
7753
|
+
];
|
|
7163
7754
|
const envCollected = yield* Stream.runCollect(applyPerElementPipeline(Stream.fromIterable(environmentDiagnostics)));
|
|
7164
7755
|
const lintFailure = yield* Ref.make({
|
|
7165
7756
|
didFail: false,
|
|
@@ -7202,7 +7793,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7202
7793
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
7203
7794
|
yield* afterLint(lintFailureState.didFail);
|
|
7204
7795
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
7205
|
-
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
7796
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
7206
7797
|
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
7207
7798
|
rootDirectory: scanDirectory,
|
|
7208
7799
|
userConfig: resolvedConfig.config
|
|
@@ -7214,9 +7805,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7214
7805
|
return Stream.empty;
|
|
7215
7806
|
}))))))));
|
|
7216
7807
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
7217
|
-
const
|
|
7808
|
+
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
7809
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
|
|
7218
7810
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
7219
7811
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
7812
|
+
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
7220
7813
|
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
|
|
7221
7814
|
yield* reporterService.finalize;
|
|
7222
7815
|
const finalDiagnostics = [
|
|
@@ -7257,7 +7850,10 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7257
7850
|
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
7258
7851
|
lintPartialFailures,
|
|
7259
7852
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
7260
|
-
deadCodeFailureReason: deadCodeFailureState.reason
|
|
7853
|
+
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
7854
|
+
scannedFileCount: totalFileCount,
|
|
7855
|
+
scannedFilePaths,
|
|
7856
|
+
scanElapsedMilliseconds
|
|
7261
7857
|
};
|
|
7262
7858
|
}).pipe(Effect.withSpan("runInspect", { attributes: {
|
|
7263
7859
|
"inspect.directory": input.directory,
|
|
@@ -7383,7 +7979,7 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
|
|
|
7383
7979
|
static layerNode = Layer.effect(StagedFiles, Effect.gen(function* () {
|
|
7384
7980
|
const git = yield* Git;
|
|
7385
7981
|
return StagedFiles.of({
|
|
7386
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(
|
|
7982
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(isLintableSourceFile))),
|
|
7387
7983
|
materialize: ({ directory, stagedFiles, tempDirectory }) => Effect.gen(function* () {
|
|
7388
7984
|
const materializedFiles = [];
|
|
7389
7985
|
const resolvedTempDirectory = path.resolve(tempDirectory);
|
|
@@ -7592,7 +8188,7 @@ const getDiffInfo = (directory, explicitBaseBranch) => Effect.runPromise(Effect.
|
|
|
7592
8188
|
GitBaseBranchInvalid: (reason) => Effect.die(new Error(reason.detail)),
|
|
7593
8189
|
GitBaseBranchMissing: (reason) => Effect.die(new Error(reason.message))
|
|
7594
8190
|
})));
|
|
7595
|
-
const filterSourceFiles = (filePaths) => filePaths.filter(
|
|
8191
|
+
const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
|
|
7596
8192
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7597
8193
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
7598
8194
|
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
@@ -7679,6 +8275,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7679
8275
|
includePaths,
|
|
7680
8276
|
customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
|
|
7681
8277
|
respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
|
|
8278
|
+
warnings: options.warnings ?? effectiveConfig?.warnings ?? false,
|
|
7682
8279
|
adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
|
|
7683
8280
|
ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
|
|
7684
8281
|
runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
|
|
@@ -7716,6 +8313,7 @@ const clearCaches = () => {
|
|
|
7716
8313
|
clearConfigCache();
|
|
7717
8314
|
clearPackageJsonCache();
|
|
7718
8315
|
clearIgnorePatternsCache();
|
|
8316
|
+
clearPackageRoleCache();
|
|
7719
8317
|
clearAutoSuppressionCaches();
|
|
7720
8318
|
};
|
|
7721
8319
|
const toJsonReport = (result, options) => buildJsonReport({
|