react-doctor 0.2.14-dev.09fe1ff → 0.2.14-dev.24425b1
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 +8 -0
- package/dist/cli.js +863 -99
- package/dist/index.d.ts +39 -1
- package/dist/index.js +653 -70
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as Redacted from "effect/Redacted";
|
|
|
14
14
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
15
15
|
import * as Otlp from "effect/unstable/observability/Otlp";
|
|
16
16
|
import * as Context from "effect/Context";
|
|
17
|
+
import os from "node:os";
|
|
17
18
|
import * as Console from "effect/Console";
|
|
18
19
|
import * as Fiber from "effect/Fiber";
|
|
19
20
|
import * as Filter from "effect/Filter";
|
|
@@ -26,7 +27,6 @@ import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
|
26
27
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
27
28
|
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
28
29
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
29
|
-
import os from "node:os";
|
|
30
30
|
import * as ts from "typescript";
|
|
31
31
|
import { gzipSync } from "node:zlib";
|
|
32
32
|
//#region \0rolldown/runtime.js
|
|
@@ -2874,29 +2874,34 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2874
2874
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2875
2875
|
};
|
|
2876
2876
|
};
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2877
|
+
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
2878
|
+
const rootValue = select(rootPackageJson);
|
|
2879
|
+
if (rootValue !== null) return rootValue;
|
|
2879
2880
|
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2880
|
-
if (patterns.length === 0) return
|
|
2881
|
+
if (patterns.length === 0) return null;
|
|
2881
2882
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2882
2883
|
for (const pattern of patterns) {
|
|
2883
|
-
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2884
|
+
const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
|
|
2884
2885
|
for (const workspaceDirectory of directories) {
|
|
2885
2886
|
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2886
2887
|
visitedDirectories.add(workspaceDirectory);
|
|
2887
|
-
|
|
2888
|
+
const value = select(readPackageJson(path.join(workspaceDirectory, "package.json")));
|
|
2889
|
+
if (value !== null) return value;
|
|
2888
2890
|
}
|
|
2889
2891
|
}
|
|
2890
|
-
return
|
|
2892
|
+
return null;
|
|
2891
2893
|
};
|
|
2894
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
|
|
2892
2895
|
const NAMES = new Set([
|
|
2893
2896
|
"react-native",
|
|
2894
2897
|
"react-native-tvos",
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2898
|
+
...new Set([
|
|
2899
|
+
"expo",
|
|
2900
|
+
"expo-router",
|
|
2901
|
+
"@expo/cli",
|
|
2902
|
+
"@expo/metro-config",
|
|
2903
|
+
"@expo/metro-runtime"
|
|
2904
|
+
]),
|
|
2900
2905
|
"react-native-windows",
|
|
2901
2906
|
"react-native-macos"
|
|
2902
2907
|
]);
|
|
@@ -2920,6 +2925,11 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2920
2925
|
return false;
|
|
2921
2926
|
};
|
|
2922
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);
|
|
2923
2933
|
const getPreactVersion = (packageJson) => {
|
|
2924
2934
|
return {
|
|
2925
2935
|
...packageJson.peerDependencies,
|
|
@@ -3159,6 +3169,19 @@ const discoverProject = (directory) => {
|
|
|
3159
3169
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
3160
3170
|
const sourceFileCount = countSourceFiles(directory);
|
|
3161
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
|
+
}
|
|
3162
3185
|
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
3163
3186
|
const preactVersion = getPreactVersion(packageJson);
|
|
3164
3187
|
const projectInfo = {
|
|
@@ -3176,6 +3199,7 @@ const discoverProject = (directory) => {
|
|
|
3176
3199
|
preactVersion,
|
|
3177
3200
|
preactMajorVersion: parseReactMajor(preactVersion),
|
|
3178
3201
|
hasReactNativeWorkspace,
|
|
3202
|
+
expoVersion,
|
|
3179
3203
|
hasReanimated,
|
|
3180
3204
|
sourceFileCount
|
|
3181
3205
|
};
|
|
@@ -3280,6 +3304,9 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
|
3280
3304
|
"Accessibility",
|
|
3281
3305
|
"Maintainability"
|
|
3282
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"]);
|
|
3283
3310
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
3284
3311
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
3285
3312
|
var InvalidGlobPatternError = class extends Error {
|
|
@@ -3399,10 +3426,11 @@ const restampSeverity = (diagnostic, override) => {
|
|
|
3399
3426
|
*/
|
|
3400
3427
|
const buildRuleSeverityControls = (config) => {
|
|
3401
3428
|
if (!config) return void 0;
|
|
3402
|
-
if (config.rules === void 0 && config.categories === void 0
|
|
3429
|
+
if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
|
|
3403
3430
|
return {
|
|
3404
3431
|
...config.rules !== void 0 ? { rules: config.rules } : {},
|
|
3405
|
-
...config.categories !== void 0 ? { categories: config.categories } : {}
|
|
3432
|
+
...config.categories !== void 0 ? { categories: config.categories } : {},
|
|
3433
|
+
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
3406
3434
|
};
|
|
3407
3435
|
};
|
|
3408
3436
|
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
@@ -3766,6 +3794,69 @@ const resolveRuleSeverityOverride = (input, controls) => {
|
|
|
3766
3794
|
}
|
|
3767
3795
|
return input.category !== void 0 ? controls.categories?.[input.category] : void 0;
|
|
3768
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
|
+
};
|
|
3769
3860
|
/**
|
|
3770
3861
|
* Resolves the absolute path to read for a diagnostic's `filePath`,
|
|
3771
3862
|
* accounting for the various shapes oxlint emits:
|
|
@@ -3928,6 +4019,15 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3928
4019
|
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
3929
4020
|
const fileLinesCache = /* @__PURE__ */ new Map();
|
|
3930
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
|
+
};
|
|
3931
4031
|
const getFileLines = (filePath) => {
|
|
3932
4032
|
const cached = fileLinesCache.get(filePath);
|
|
3933
4033
|
if (cached !== void 0) return cached;
|
|
@@ -3954,6 +4054,10 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3954
4054
|
for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
|
|
3955
4055
|
return false;
|
|
3956
4056
|
};
|
|
4057
|
+
const isAppOnlyRule = (ruleIdentifier) => {
|
|
4058
|
+
for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
|
|
4059
|
+
return false;
|
|
4060
|
+
};
|
|
3957
4061
|
const isRnRawTextSuppressedByConfig = (diagnostic) => {
|
|
3958
4062
|
if (diagnostic.rule !== "rn-no-raw-text") return false;
|
|
3959
4063
|
if (diagnostic.line <= 0) return false;
|
|
@@ -3968,8 +4072,10 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3968
4072
|
if (shouldAutoSuppress(diagnostic)) return null;
|
|
3969
4073
|
let current = diagnostic;
|
|
3970
4074
|
let explicitSeverityOverride;
|
|
4075
|
+
let explicitRuleOverride;
|
|
3971
4076
|
if (severityControls) {
|
|
3972
4077
|
const { ruleKey, category } = getDiagnosticRuleIdentity(current);
|
|
4078
|
+
explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
|
|
3973
4079
|
explicitSeverityOverride = resolveRuleSeverityOverride({
|
|
3974
4080
|
ruleKey,
|
|
3975
4081
|
category
|
|
@@ -3977,6 +4083,9 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
3977
4083
|
if (explicitSeverityOverride === "off") return null;
|
|
3978
4084
|
if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
|
|
3979
4085
|
}
|
|
4086
|
+
if (explicitRuleOverride === void 0) {
|
|
4087
|
+
if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
|
|
4088
|
+
}
|
|
3980
4089
|
if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
|
|
3981
4090
|
if (userConfig) {
|
|
3982
4091
|
if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
|
|
@@ -4162,6 +4271,17 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
4162
4271
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
4163
4272
|
}).pipe(Effect.orDie));
|
|
4164
4273
|
/**
|
|
4274
|
+
* Resolves a requested lint worker count to a clamped integer within
|
|
4275
|
+
* `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
|
|
4276
|
+
* machine's CPU cores; out-of-range or non-finite requests degrade to
|
|
4277
|
+
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
4278
|
+
*/
|
|
4279
|
+
const resolveScanConcurrency = (requested) => {
|
|
4280
|
+
const desired = requested === "auto" ? os.availableParallelism() : requested;
|
|
4281
|
+
if (!Number.isFinite(desired) || desired < 1) return 1;
|
|
4282
|
+
return Math.max(1, Math.min(Math.floor(desired), 16));
|
|
4283
|
+
};
|
|
4284
|
+
/**
|
|
4165
4285
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
4166
4286
|
* startup so the eval harness can raise the budget under sandbox
|
|
4167
4287
|
* microVMs without recompiling react-doctor. Tests override via
|
|
@@ -4181,6 +4301,30 @@ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintS
|
|
|
4181
4301
|
* tests that exercise the cap behavior.
|
|
4182
4302
|
*/
|
|
4183
4303
|
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
4304
|
+
/**
|
|
4305
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
4306
|
+
* to `1` (serial — the historical behavior) so resource usage is opt-in.
|
|
4307
|
+
* The CLI's `--experimental-parallel` flag overrides this via `Layer.succeed`; the
|
|
4308
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic /
|
|
4309
|
+
* CI callers that never touch the flag:
|
|
4310
|
+
*
|
|
4311
|
+
* - unset / `0` / `false` / `off` → `1` (serial)
|
|
4312
|
+
* - `auto` / `true` / `on` → available CPU cores (clamped)
|
|
4313
|
+
* - a positive integer → that many workers (clamped)
|
|
4314
|
+
*
|
|
4315
|
+
* The resolved value is always within
|
|
4316
|
+
* `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
|
|
4317
|
+
*/
|
|
4318
|
+
var OxlintConcurrency = class extends Context.Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
4319
|
+
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
4320
|
+
if (raw === void 0) return 1;
|
|
4321
|
+
const normalized = raw.trim().toLowerCase();
|
|
4322
|
+
if (normalized === "" || normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
4323
|
+
if (normalized === "auto" || normalized === "true" || normalized === "on") return resolveScanConcurrency("auto");
|
|
4324
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
4325
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return 1;
|
|
4326
|
+
return resolveScanConcurrency(parsed);
|
|
4327
|
+
} }) {};
|
|
4184
4328
|
const DIAGNOSTIC_SURFACES = [
|
|
4185
4329
|
"cli",
|
|
4186
4330
|
"prComment",
|
|
@@ -4341,16 +4485,23 @@ const CONFIG_FILENAME = "react-doctor.config.json";
|
|
|
4341
4485
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
4342
4486
|
const loadConfigFromDirectory = (directory) => {
|
|
4343
4487
|
const configFilePath = path.join(directory, CONFIG_FILENAME);
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4488
|
+
let sawBrokenConfigFile = false;
|
|
4489
|
+
if (isFile(configFilePath)) {
|
|
4490
|
+
try {
|
|
4491
|
+
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
4492
|
+
const parsed = JSON.parse(fileContent);
|
|
4493
|
+
if (isPlainObject(parsed)) return {
|
|
4494
|
+
status: "found",
|
|
4495
|
+
loaded: {
|
|
4496
|
+
config: validateConfigTypes(parsed),
|
|
4497
|
+
sourceDirectory: directory
|
|
4498
|
+
}
|
|
4499
|
+
};
|
|
4500
|
+
warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4503
|
+
}
|
|
4504
|
+
sawBrokenConfigFile = true;
|
|
4354
4505
|
}
|
|
4355
4506
|
const packageJsonPath = path.join(directory, "package.json");
|
|
4356
4507
|
if (isFile(packageJsonPath)) try {
|
|
@@ -4359,14 +4510,18 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
4359
4510
|
if (isPlainObject(packageJson)) {
|
|
4360
4511
|
const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
|
|
4361
4512
|
if (isPlainObject(embeddedConfig)) return {
|
|
4362
|
-
|
|
4363
|
-
|
|
4513
|
+
status: "found",
|
|
4514
|
+
loaded: {
|
|
4515
|
+
config: validateConfigTypes(embeddedConfig),
|
|
4516
|
+
sourceDirectory: directory
|
|
4517
|
+
}
|
|
4364
4518
|
};
|
|
4365
4519
|
}
|
|
4366
|
-
} catch {
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4520
|
+
} catch {}
|
|
4521
|
+
return {
|
|
4522
|
+
status: sawBrokenConfigFile ? "invalid" : "absent",
|
|
4523
|
+
loaded: null
|
|
4524
|
+
};
|
|
4370
4525
|
};
|
|
4371
4526
|
const cachedConfigs = /* @__PURE__ */ new Map();
|
|
4372
4527
|
const clearConfigCache = () => {
|
|
@@ -4375,21 +4530,21 @@ const clearConfigCache = () => {
|
|
|
4375
4530
|
const loadConfigWithSource = (rootDirectory) => {
|
|
4376
4531
|
const cached = cachedConfigs.get(rootDirectory);
|
|
4377
4532
|
if (cached !== void 0) return cached;
|
|
4378
|
-
const
|
|
4379
|
-
if (
|
|
4380
|
-
cachedConfigs.set(rootDirectory,
|
|
4381
|
-
return
|
|
4533
|
+
const localResult = loadConfigFromDirectory(rootDirectory);
|
|
4534
|
+
if (localResult.status === "found") {
|
|
4535
|
+
cachedConfigs.set(rootDirectory, localResult.loaded);
|
|
4536
|
+
return localResult.loaded;
|
|
4382
4537
|
}
|
|
4383
|
-
if (isProjectBoundary(rootDirectory)) {
|
|
4538
|
+
if (localResult.status === "invalid" || isProjectBoundary(rootDirectory)) {
|
|
4384
4539
|
cachedConfigs.set(rootDirectory, null);
|
|
4385
4540
|
return null;
|
|
4386
4541
|
}
|
|
4387
4542
|
let ancestorDirectory = path.dirname(rootDirectory);
|
|
4388
4543
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
4389
|
-
const
|
|
4390
|
-
if (
|
|
4391
|
-
cachedConfigs.set(rootDirectory,
|
|
4392
|
-
return
|
|
4544
|
+
const ancestorResult = loadConfigFromDirectory(ancestorDirectory);
|
|
4545
|
+
if (ancestorResult.status === "found") {
|
|
4546
|
+
cachedConfigs.set(rootDirectory, ancestorResult.loaded);
|
|
4547
|
+
return ancestorResult.loaded;
|
|
4393
4548
|
}
|
|
4394
4549
|
if (isProjectBoundary(ancestorDirectory)) {
|
|
4395
4550
|
cachedConfigs.set(rootDirectory, null);
|
|
@@ -4463,6 +4618,359 @@ const resolveScanTarget = (requestedDirectory, options = {}) => {
|
|
|
4463
4618
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
4464
4619
|
};
|
|
4465
4620
|
};
|
|
4621
|
+
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
4622
|
+
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
4623
|
+
const packageJson = readPackageJson(path.join(rootDirectory, "package.json"));
|
|
4624
|
+
return {
|
|
4625
|
+
rootDirectory,
|
|
4626
|
+
packageJson,
|
|
4627
|
+
directDependencyNames: getDirectDependencyNames(packageJson),
|
|
4628
|
+
expoSdkMajor: getLowestDependencyMajor(expoVersion)
|
|
4629
|
+
};
|
|
4630
|
+
};
|
|
4631
|
+
const buildExpoDiagnostic = (input) => ({
|
|
4632
|
+
filePath: input.filePath ?? "package.json",
|
|
4633
|
+
plugin: "react-doctor",
|
|
4634
|
+
rule: input.rule,
|
|
4635
|
+
severity: input.severity ?? "warning",
|
|
4636
|
+
message: input.message,
|
|
4637
|
+
help: input.help,
|
|
4638
|
+
line: input.line ?? 0,
|
|
4639
|
+
column: input.column ?? 0,
|
|
4640
|
+
category: input.category ?? "Correctness"
|
|
4641
|
+
});
|
|
4642
|
+
const CRITICAL_OVERRIDE_NAMES = new Set([
|
|
4643
|
+
"@expo/cli",
|
|
4644
|
+
"@expo/config",
|
|
4645
|
+
"@expo/metro-config",
|
|
4646
|
+
"@expo/metro-runtime",
|
|
4647
|
+
"@expo/metro",
|
|
4648
|
+
"metro"
|
|
4649
|
+
]);
|
|
4650
|
+
const isCriticalOverrideName = (packageName) => CRITICAL_OVERRIDE_NAMES.has(packageName) || packageName.startsWith("metro-");
|
|
4651
|
+
const collectOverrideNames = (packageJson) => new Set([
|
|
4652
|
+
...Object.keys(packageJson.overrides ?? {}),
|
|
4653
|
+
...Object.keys(packageJson.resolutions ?? {}),
|
|
4654
|
+
...Object.keys(packageJson.pnpm?.overrides ?? {})
|
|
4655
|
+
]);
|
|
4656
|
+
const checkExpoDependencyOverrides = (context) => {
|
|
4657
|
+
const overriddenCriticalNames = [...collectOverrideNames(context.packageJson)].filter(isCriticalOverrideName).sort();
|
|
4658
|
+
if (overriddenCriticalNames.length === 0) return [];
|
|
4659
|
+
const quotedNames = overriddenCriticalNames.map((name) => `"${name}"`).join(", ");
|
|
4660
|
+
return [buildExpoDiagnostic({
|
|
4661
|
+
rule: "expo-no-conflicting-dependency-override",
|
|
4662
|
+
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`,
|
|
4663
|
+
help: `Remove the override/resolution for ${quotedNames} and reinstall so the Expo-pinned versions are used`
|
|
4664
|
+
})];
|
|
4665
|
+
};
|
|
4666
|
+
const isPathGitIgnored = (rootDirectory, absolutePath) => {
|
|
4667
|
+
const result = spawnSync("git", [
|
|
4668
|
+
"check-ignore",
|
|
4669
|
+
"-q",
|
|
4670
|
+
absolutePath
|
|
4671
|
+
], {
|
|
4672
|
+
cwd: rootDirectory,
|
|
4673
|
+
stdio: [
|
|
4674
|
+
"ignore",
|
|
4675
|
+
"ignore",
|
|
4676
|
+
"ignore"
|
|
4677
|
+
]
|
|
4678
|
+
});
|
|
4679
|
+
if (result.error) return null;
|
|
4680
|
+
if (result.status === 0) return true;
|
|
4681
|
+
if (result.status === 1) return false;
|
|
4682
|
+
return null;
|
|
4683
|
+
};
|
|
4684
|
+
const LOCAL_ENV_FILE_NAMES = [
|
|
4685
|
+
".env.local",
|
|
4686
|
+
".env.development.local",
|
|
4687
|
+
".env.production.local",
|
|
4688
|
+
".env.test.local"
|
|
4689
|
+
];
|
|
4690
|
+
const checkExpoEnvLocalFiles = (context) => {
|
|
4691
|
+
const { rootDirectory } = context;
|
|
4692
|
+
const committedEnvFiles = LOCAL_ENV_FILE_NAMES.filter((fileName) => {
|
|
4693
|
+
const filePath = path.join(rootDirectory, fileName);
|
|
4694
|
+
if (!isFile(filePath)) return false;
|
|
4695
|
+
return isPathGitIgnored(rootDirectory, filePath) === false;
|
|
4696
|
+
});
|
|
4697
|
+
if (committedEnvFiles.length === 0) return [];
|
|
4698
|
+
return [buildExpoDiagnostic({
|
|
4699
|
+
rule: "expo-env-local-not-gitignored",
|
|
4700
|
+
category: "Security",
|
|
4701
|
+
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`,
|
|
4702
|
+
help: `Add \`.env*.local\` to your .gitignore. If already committed, untrack with \`git rm --cached ${committedEnvFiles.join(" ")}\``
|
|
4703
|
+
})];
|
|
4704
|
+
};
|
|
4705
|
+
const isExpoSdkAtLeast = (expoSdkMajor, minMajor) => expoSdkMajor !== null && expoSdkMajor >= minMajor;
|
|
4706
|
+
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";
|
|
4707
|
+
const FIREBASE_HELP = "Use the Firebase JS SDK or React Native Firebase directly. See https://expo.fyi/firebase-migration-guide";
|
|
4708
|
+
const unimodulesEntry = (packageName) => ({
|
|
4709
|
+
packageName,
|
|
4710
|
+
rule: "expo-no-unimodules-packages",
|
|
4711
|
+
message: `"${packageName}" is a legacy unimodules package that is incompatible with Expo SDK 44+ and will break native builds`,
|
|
4712
|
+
help: UNIMODULES_HELP
|
|
4713
|
+
});
|
|
4714
|
+
const FLAGGED_DEPENDENCIES = [
|
|
4715
|
+
unimodulesEntry("@unimodules/core"),
|
|
4716
|
+
unimodulesEntry("@unimodules/react-native-adapter"),
|
|
4717
|
+
unimodulesEntry("react-native-unimodules"),
|
|
4718
|
+
{
|
|
4719
|
+
packageName: "expo-cli",
|
|
4720
|
+
rule: "expo-no-cli-dependencies",
|
|
4721
|
+
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`",
|
|
4722
|
+
help: "Remove `expo-cli` from your dependencies and use the bundled CLI via `npx expo`"
|
|
4723
|
+
},
|
|
4724
|
+
{
|
|
4725
|
+
packageName: "eas-cli",
|
|
4726
|
+
rule: "expo-no-cli-dependencies",
|
|
4727
|
+
message: "`eas-cli` is a project dependency — pinning it in package.json drifts from the latest EAS CLI and bloats installs",
|
|
4728
|
+
help: "Remove `eas-cli` from your dependencies and run it on demand with `npx eas-cli` (or install it globally)"
|
|
4729
|
+
},
|
|
4730
|
+
{
|
|
4731
|
+
packageName: "expo-modules-autolinking",
|
|
4732
|
+
rule: "expo-no-redundant-dependency",
|
|
4733
|
+
message: "\"expo-modules-autolinking\" should not be a direct dependency — Expo installs it transitively as needed",
|
|
4734
|
+
help: "Remove `expo-modules-autolinking` from your package.json"
|
|
4735
|
+
},
|
|
4736
|
+
{
|
|
4737
|
+
packageName: "expo-dev-launcher",
|
|
4738
|
+
rule: "expo-no-redundant-dependency",
|
|
4739
|
+
message: "\"expo-dev-launcher\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4740
|
+
help: "Remove `expo-dev-launcher` and depend on `expo-dev-client` instead"
|
|
4741
|
+
},
|
|
4742
|
+
{
|
|
4743
|
+
packageName: "expo-dev-menu",
|
|
4744
|
+
rule: "expo-no-redundant-dependency",
|
|
4745
|
+
message: "\"expo-dev-menu\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4746
|
+
help: "Remove `expo-dev-menu` and depend on `expo-dev-client` instead"
|
|
4747
|
+
},
|
|
4748
|
+
{
|
|
4749
|
+
packageName: "expo-modules-core",
|
|
4750
|
+
rule: "expo-no-redundant-dependency",
|
|
4751
|
+
message: "\"expo-modules-core\" should not be a direct dependency — use the API re-exported from the `expo` package",
|
|
4752
|
+
help: "Remove `expo-modules-core` from your package.json and import from `expo` instead"
|
|
4753
|
+
},
|
|
4754
|
+
{
|
|
4755
|
+
packageName: "@expo/metro-config",
|
|
4756
|
+
rule: "expo-no-redundant-dependency",
|
|
4757
|
+
message: "\"@expo/metro-config\" should not be a direct dependency — use the `expo/metro-config` sub-export of the `expo` package",
|
|
4758
|
+
help: "Remove `@expo/metro-config` and import `expo/metro-config` in your metro.config.js"
|
|
4759
|
+
},
|
|
4760
|
+
{
|
|
4761
|
+
packageName: "@types/react-native",
|
|
4762
|
+
rule: "expo-no-redundant-dependency",
|
|
4763
|
+
message: "\"@types/react-native\" should not be installed — React Native ships its own types since SDK 48",
|
|
4764
|
+
help: "Remove `@types/react-native` from your package.json",
|
|
4765
|
+
minSdkMajor: 48
|
|
4766
|
+
},
|
|
4767
|
+
{
|
|
4768
|
+
packageName: "@expo/config-plugins",
|
|
4769
|
+
rule: "expo-no-redundant-dependency",
|
|
4770
|
+
message: "\"@expo/config-plugins\" should not be a direct dependency — use the `expo/config-plugins` sub-export of the `expo` package",
|
|
4771
|
+
help: "Remove `@expo/config-plugins`; config-plugin authors should import from `expo/config-plugins`. See https://github.com/expo/expo/pull/18855",
|
|
4772
|
+
minSdkMajor: 48
|
|
4773
|
+
},
|
|
4774
|
+
{
|
|
4775
|
+
packageName: "@expo/prebuild-config",
|
|
4776
|
+
rule: "expo-no-redundant-dependency",
|
|
4777
|
+
message: "\"@expo/prebuild-config\" should not be a direct dependency — Expo installs it transitively",
|
|
4778
|
+
help: "Remove `@expo/prebuild-config` from your package.json",
|
|
4779
|
+
minSdkMajor: 53
|
|
4780
|
+
},
|
|
4781
|
+
{
|
|
4782
|
+
packageName: "expo-permissions",
|
|
4783
|
+
rule: "expo-no-redundant-dependency",
|
|
4784
|
+
message: "\"expo-permissions\" was deprecated in SDK 41 and may no longer compile — permissions moved onto each module (e.g. `MediaLibrary.requestPermissionsAsync()`)",
|
|
4785
|
+
help: "Remove `expo-permissions` and request permissions from the relevant module instead",
|
|
4786
|
+
minSdkMajor: 50
|
|
4787
|
+
},
|
|
4788
|
+
{
|
|
4789
|
+
packageName: "expo-app-loading",
|
|
4790
|
+
rule: "expo-no-redundant-dependency",
|
|
4791
|
+
message: "\"expo-app-loading\" was removed in SDK 49",
|
|
4792
|
+
help: "Remove `expo-app-loading` and use `expo-splash-screen` instead. See https://docs.expo.dev/versions/latest/sdk/splash-screen/",
|
|
4793
|
+
minSdkMajor: 49
|
|
4794
|
+
},
|
|
4795
|
+
{
|
|
4796
|
+
packageName: "expo-firebase-analytics",
|
|
4797
|
+
rule: "expo-no-redundant-dependency",
|
|
4798
|
+
message: "\"expo-firebase-analytics\" was removed in SDK 48",
|
|
4799
|
+
help: FIREBASE_HELP,
|
|
4800
|
+
minSdkMajor: 48
|
|
4801
|
+
},
|
|
4802
|
+
{
|
|
4803
|
+
packageName: "expo-firebase-recaptcha",
|
|
4804
|
+
rule: "expo-no-redundant-dependency",
|
|
4805
|
+
message: "\"expo-firebase-recaptcha\" was removed in SDK 48",
|
|
4806
|
+
help: FIREBASE_HELP,
|
|
4807
|
+
minSdkMajor: 48
|
|
4808
|
+
},
|
|
4809
|
+
{
|
|
4810
|
+
packageName: "expo-firebase-core",
|
|
4811
|
+
rule: "expo-no-redundant-dependency",
|
|
4812
|
+
message: "\"expo-firebase-core\" was removed in SDK 48",
|
|
4813
|
+
help: FIREBASE_HELP,
|
|
4814
|
+
minSdkMajor: 48
|
|
4815
|
+
}
|
|
4816
|
+
];
|
|
4817
|
+
const checkExpoFlaggedDependencies = (context) => FLAGGED_DEPENDENCIES.filter((flaggedDependency) => {
|
|
4818
|
+
if (!context.directDependencyNames.has(flaggedDependency.packageName)) return false;
|
|
4819
|
+
if (flaggedDependency.minSdkMajor === void 0) return true;
|
|
4820
|
+
return isExpoSdkAtLeast(context.expoSdkMajor, flaggedDependency.minSdkMajor);
|
|
4821
|
+
}).map((flaggedDependency) => buildExpoDiagnostic({
|
|
4822
|
+
rule: flaggedDependency.rule,
|
|
4823
|
+
message: flaggedDependency.message,
|
|
4824
|
+
help: flaggedDependency.help
|
|
4825
|
+
}));
|
|
4826
|
+
const findLocalModuleNativeFiles = (rootDirectory) => {
|
|
4827
|
+
const modulesDirectory = path.join(rootDirectory, "modules");
|
|
4828
|
+
if (!isDirectory(modulesDirectory)) return [];
|
|
4829
|
+
const nativeFilePaths = [];
|
|
4830
|
+
for (const moduleEntry of readDirectoryEntries(modulesDirectory)) {
|
|
4831
|
+
if (!moduleEntry.isDirectory()) continue;
|
|
4832
|
+
const moduleDirectory = path.join(modulesDirectory, moduleEntry.name);
|
|
4833
|
+
const gradlePath = path.join(moduleDirectory, "android", "build.gradle");
|
|
4834
|
+
if (isFile(gradlePath)) nativeFilePaths.push(gradlePath);
|
|
4835
|
+
const iosDirectory = path.join(moduleDirectory, "ios");
|
|
4836
|
+
if (isDirectory(iosDirectory)) {
|
|
4837
|
+
for (const iosEntry of readDirectoryEntries(iosDirectory)) if (iosEntry.isFile() && iosEntry.name.endsWith(".podspec")) nativeFilePaths.push(path.join(iosDirectory, iosEntry.name));
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
return nativeFilePaths;
|
|
4841
|
+
};
|
|
4842
|
+
const checkExpoGitignore = (context) => {
|
|
4843
|
+
const { rootDirectory } = context;
|
|
4844
|
+
const diagnostics = [];
|
|
4845
|
+
const expoStateDirectory = path.join(rootDirectory, ".expo");
|
|
4846
|
+
if (isDirectory(expoStateDirectory) && isPathGitIgnored(rootDirectory, expoStateDirectory) === false) diagnostics.push(buildExpoDiagnostic({
|
|
4847
|
+
rule: "expo-gitignore",
|
|
4848
|
+
message: "The `.expo` directory is not ignored by Git — it holds machine-specific device history and dev-server settings that should not be committed",
|
|
4849
|
+
help: "Add `.expo/` to your .gitignore"
|
|
4850
|
+
}));
|
|
4851
|
+
if (findLocalModuleNativeFiles(rootDirectory).find((nativeFilePath) => isPathGitIgnored(rootDirectory, nativeFilePath) === true) !== void 0) diagnostics.push(buildExpoDiagnostic({
|
|
4852
|
+
rule: "expo-gitignore",
|
|
4853
|
+
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",
|
|
4854
|
+
help: "Use anchored patterns like `/ios` and `/android` in .gitignore so only the top-level native directories are excluded, not those inside `modules/`"
|
|
4855
|
+
}));
|
|
4856
|
+
return diagnostics;
|
|
4857
|
+
};
|
|
4858
|
+
const LOCKFILE_NAMES = [
|
|
4859
|
+
"pnpm-lock.yaml",
|
|
4860
|
+
"yarn.lock",
|
|
4861
|
+
"package-lock.json",
|
|
4862
|
+
"bun.lockb",
|
|
4863
|
+
"bun.lock"
|
|
4864
|
+
];
|
|
4865
|
+
const checkExpoLockfile = (context) => {
|
|
4866
|
+
const workspaceRoot = isMonorepoRoot(context.rootDirectory) ? context.rootDirectory : findMonorepoRoot(context.rootDirectory) ?? context.rootDirectory;
|
|
4867
|
+
const presentLockfiles = LOCKFILE_NAMES.filter((lockfileName) => isFile(path.join(workspaceRoot, lockfileName)));
|
|
4868
|
+
if (presentLockfiles.length === 0) return [buildExpoDiagnostic({
|
|
4869
|
+
rule: "expo-lockfile",
|
|
4870
|
+
message: "No lock file detected at the project root — installs are not reproducible, and EAS Build cannot infer your package manager",
|
|
4871
|
+
help: "Install dependencies with your package manager to generate a lock file, then commit it"
|
|
4872
|
+
})];
|
|
4873
|
+
if (presentLockfiles.length > 1) return [buildExpoDiagnostic({
|
|
4874
|
+
rule: "expo-lockfile",
|
|
4875
|
+
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`,
|
|
4876
|
+
help: "Delete the lock files for the package managers you are not using and keep only one"
|
|
4877
|
+
})];
|
|
4878
|
+
return [];
|
|
4879
|
+
};
|
|
4880
|
+
const METRO_CONFIG_FILE_NAMES = [
|
|
4881
|
+
"metro.config.js",
|
|
4882
|
+
"metro.config.cjs",
|
|
4883
|
+
"metro.config.mjs",
|
|
4884
|
+
"metro.config.ts"
|
|
4885
|
+
];
|
|
4886
|
+
const EXPO_METRO_CONFIG_EXTEND_SIGNALS = [
|
|
4887
|
+
"expo/metro-config",
|
|
4888
|
+
"@sentry/react-native/metro",
|
|
4889
|
+
"getSentryExpoConfig"
|
|
4890
|
+
];
|
|
4891
|
+
const checkExpoMetroConfig = (context) => {
|
|
4892
|
+
const metroConfigPath = METRO_CONFIG_FILE_NAMES.map((fileName) => path.join(context.rootDirectory, fileName)).find((candidatePath) => isFile(candidatePath));
|
|
4893
|
+
if (metroConfigPath === void 0) return [];
|
|
4894
|
+
let contents;
|
|
4895
|
+
try {
|
|
4896
|
+
contents = fs.readFileSync(metroConfigPath, "utf-8");
|
|
4897
|
+
} catch {
|
|
4898
|
+
return [];
|
|
4899
|
+
}
|
|
4900
|
+
if (EXPO_METRO_CONFIG_EXTEND_SIGNALS.some((signal) => contents.includes(signal))) return [];
|
|
4901
|
+
return [buildExpoDiagnostic({
|
|
4902
|
+
rule: "expo-metro-config",
|
|
4903
|
+
filePath: path.basename(metroConfigPath),
|
|
4904
|
+
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",
|
|
4905
|
+
help: "Update your metro config to extend `expo/metro-config`. See https://docs.expo.dev/guides/customizing-metro/"
|
|
4906
|
+
})];
|
|
4907
|
+
};
|
|
4908
|
+
const CONFLICTING_SCRIPT_NAMES = ["expo", "react-native"];
|
|
4909
|
+
const checkExpoPackageJsonConflicts = (context) => {
|
|
4910
|
+
const { packageJson } = context;
|
|
4911
|
+
const diagnostics = [];
|
|
4912
|
+
const conflictingScriptNames = CONFLICTING_SCRIPT_NAMES.filter((scriptName) => Boolean(packageJson.scripts?.[scriptName]));
|
|
4913
|
+
if (conflictingScriptNames.length > 0) {
|
|
4914
|
+
const quotedNames = conflictingScriptNames.map((name) => `"${name}"`).join(", ");
|
|
4915
|
+
const shadowsExpoCli = conflictingScriptNames.includes("expo");
|
|
4916
|
+
diagnostics.push(buildExpoDiagnostic({
|
|
4917
|
+
rule: "expo-package-json-conflict",
|
|
4918
|
+
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" : ""}`,
|
|
4919
|
+
help: "Rename these scripts so they don't collide with the `expo` / `react-native` binaries"
|
|
4920
|
+
}));
|
|
4921
|
+
}
|
|
4922
|
+
const packageName = packageJson.name;
|
|
4923
|
+
if (typeof packageName === "string" && context.directDependencyNames.has(packageName)) diagnostics.push(buildExpoDiagnostic({
|
|
4924
|
+
rule: "expo-package-json-conflict",
|
|
4925
|
+
message: `package.json "name" is "${packageName}", which collides with a dependency of the same name`,
|
|
4926
|
+
help: "Rename your package so it no longer matches one of its dependencies"
|
|
4927
|
+
}));
|
|
4928
|
+
return diagnostics;
|
|
4929
|
+
};
|
|
4930
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR = 56;
|
|
4931
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE = 57;
|
|
4932
|
+
const checkExpoRouterReactNavigation = (context) => {
|
|
4933
|
+
const { expoSdkMajor } = context;
|
|
4934
|
+
if (!isExpoSdkAtLeast(expoSdkMajor, EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR)) return [];
|
|
4935
|
+
if (expoSdkMajor !== null && expoSdkMajor >= EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE) return [];
|
|
4936
|
+
if (!context.directDependencyNames.has("expo-router")) return [];
|
|
4937
|
+
const reactNavigationNames = [...context.directDependencyNames].filter((packageName) => packageName.startsWith("@react-navigation/")).sort();
|
|
4938
|
+
if (reactNavigationNames.length === 0) return [];
|
|
4939
|
+
return [buildExpoDiagnostic({
|
|
4940
|
+
rule: "expo-router-no-react-navigation",
|
|
4941
|
+
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"}`,
|
|
4942
|
+
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/"
|
|
4943
|
+
})];
|
|
4944
|
+
};
|
|
4945
|
+
const VECTOR_ICONS_MIN_SDK_MAJOR = 56;
|
|
4946
|
+
const SCOPED_VECTOR_ICONS_NAMESPACE = "@react-native-vector-icons/";
|
|
4947
|
+
const CONFLICTING_VECTOR_ICONS_PACKAGES = ["@expo/vector-icons", "react-native-vector-icons"];
|
|
4948
|
+
const checkExpoVectorIcons = (context) => {
|
|
4949
|
+
if (!isExpoSdkAtLeast(context.expoSdkMajor, VECTOR_ICONS_MIN_SDK_MAJOR)) return [];
|
|
4950
|
+
const hasScopedPackage = [...context.directDependencyNames].some((packageName) => packageName.startsWith(SCOPED_VECTOR_ICONS_NAMESPACE));
|
|
4951
|
+
const hasConflictingPackage = CONFLICTING_VECTOR_ICONS_PACKAGES.some((packageName) => context.directDependencyNames.has(packageName));
|
|
4952
|
+
if (!hasScopedPackage || !hasConflictingPackage) return [];
|
|
4953
|
+
return [buildExpoDiagnostic({
|
|
4954
|
+
rule: "expo-vector-icons-conflict",
|
|
4955
|
+
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",
|
|
4956
|
+
help: "Migrate to the scoped packages by running `npx @react-native-vector-icons/codemod`, then remove the conflicting package"
|
|
4957
|
+
})];
|
|
4958
|
+
};
|
|
4959
|
+
const checkExpoProject = (rootDirectory, project) => {
|
|
4960
|
+
if (project.expoVersion === null) return [];
|
|
4961
|
+
const context = buildExpoCheckContext(rootDirectory, project.expoVersion);
|
|
4962
|
+
return [
|
|
4963
|
+
...checkExpoFlaggedDependencies(context),
|
|
4964
|
+
...checkExpoDependencyOverrides(context),
|
|
4965
|
+
...checkExpoRouterReactNavigation(context),
|
|
4966
|
+
...checkExpoVectorIcons(context),
|
|
4967
|
+
...checkExpoPackageJsonConflicts(context),
|
|
4968
|
+
...checkExpoLockfile(context),
|
|
4969
|
+
...checkExpoGitignore(context),
|
|
4970
|
+
...checkExpoEnvLocalFiles(context),
|
|
4971
|
+
...checkExpoMetroConfig(context)
|
|
4972
|
+
];
|
|
4973
|
+
};
|
|
4466
4974
|
const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
4467
4975
|
const PNPM_LOCKFILE = "pnpm-lock.yaml";
|
|
4468
4976
|
const PACKAGE_JSON_FILE = "package.json";
|
|
@@ -5743,6 +6251,7 @@ const buildCapabilities = (project) => {
|
|
|
5743
6251
|
const capabilities = /* @__PURE__ */ new Set();
|
|
5744
6252
|
capabilities.add(project.framework);
|
|
5745
6253
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
6254
|
+
if (project.expoVersion !== null) capabilities.add("expo");
|
|
5746
6255
|
const reactMajor = project.reactMajorVersion;
|
|
5747
6256
|
if (reactMajor !== null) {
|
|
5748
6257
|
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
@@ -5914,10 +6423,14 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
|
|
|
5914
6423
|
if (!fs.existsSync(rootDirectory)) return rootDirectory;
|
|
5915
6424
|
return fs.realpathSync(rootDirectory);
|
|
5916
6425
|
};
|
|
6426
|
+
const resolveCompilerCleanupBucketSeverity = (ruleKey, severityControls) => {
|
|
6427
|
+
if (!COMPILER_CLEANUP_RULE_KEYS.has(ruleKey)) return void 0;
|
|
6428
|
+
return severityControls?.buckets?.[COMPILER_CLEANUP_BUCKET];
|
|
6429
|
+
};
|
|
5917
6430
|
const applyRuleSeverityControls = (rules, severityControls) => {
|
|
5918
6431
|
const enabledRules = {};
|
|
5919
6432
|
for (const [ruleKey, defaultSeverity] of Object.entries(rules)) {
|
|
5920
|
-
const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? defaultSeverity;
|
|
6433
|
+
const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? resolveCompilerCleanupBucketSeverity(ruleKey, severityControls) ?? defaultSeverity;
|
|
5921
6434
|
if (severity === "off") continue;
|
|
5922
6435
|
enabledRules[ruleKey] = severity;
|
|
5923
6436
|
}
|
|
@@ -5959,7 +6472,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
5959
6472
|
category: rule.category
|
|
5960
6473
|
}, severityControls);
|
|
5961
6474
|
if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
|
|
5962
|
-
const severity = explicitSeverity ?? rule.severity;
|
|
6475
|
+
const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
|
|
5963
6476
|
if (severity === "off") continue;
|
|
5964
6477
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
5965
6478
|
}
|
|
@@ -6016,6 +6529,44 @@ const dedupeDiagnostics = (diagnostics) => {
|
|
|
6016
6529
|
}
|
|
6017
6530
|
return uniqueDiagnostics;
|
|
6018
6531
|
};
|
|
6532
|
+
/**
|
|
6533
|
+
* Runs `task` over `items` with at most `concurrency` tasks in flight at
|
|
6534
|
+
* once, returning results in input order. A pool of workers each pulls the
|
|
6535
|
+
* next not-yet-started index until the list drains — so a worker that
|
|
6536
|
+
* finishes a fast task immediately picks up the next one (greedy load
|
|
6537
|
+
* balancing), which matters when tasks have uneven durations (oxlint
|
|
6538
|
+
* batches do).
|
|
6539
|
+
*
|
|
6540
|
+
* Failure semantics mirror a bounded `Promise.all`: on the first rejection
|
|
6541
|
+
* no further tasks are started, the already-in-flight tasks are awaited to
|
|
6542
|
+
* settle (so no subprocess is orphaned mid-write), and the returned promise
|
|
6543
|
+
* rejects with that first error. This keeps the caller's fail-fast retry
|
|
6544
|
+
* path (e.g. oxlint's retry-without-extends) from spawning a second wave on
|
|
6545
|
+
* top of a still-running first one.
|
|
6546
|
+
*/
|
|
6547
|
+
const mapWithConcurrency = async (items, concurrency, task) => {
|
|
6548
|
+
const results = new Array(items.length);
|
|
6549
|
+
if (items.length === 0) return results;
|
|
6550
|
+
const workerCount = Math.min(Math.max(1, Math.floor(concurrency) || 1), items.length);
|
|
6551
|
+
let nextIndex = 0;
|
|
6552
|
+
const errors = [];
|
|
6553
|
+
const runWorker = async () => {
|
|
6554
|
+
while (errors.length === 0) {
|
|
6555
|
+
const index = nextIndex;
|
|
6556
|
+
nextIndex += 1;
|
|
6557
|
+
if (index >= items.length) return;
|
|
6558
|
+
try {
|
|
6559
|
+
results[index] = await task(items[index], index);
|
|
6560
|
+
} catch (error) {
|
|
6561
|
+
errors.push(error);
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
};
|
|
6566
|
+
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
|
6567
|
+
if (errors.length > 0) throw errors[0];
|
|
6568
|
+
return results;
|
|
6569
|
+
};
|
|
6019
6570
|
const getPublicEnvPrefix = (framework) => {
|
|
6020
6571
|
switch (framework) {
|
|
6021
6572
|
case "nextjs": return "NEXT_PUBLIC_*";
|
|
@@ -6698,6 +7249,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
6698
7249
|
*/
|
|
6699
7250
|
const spawnLintBatches = async (input) => {
|
|
6700
7251
|
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
7252
|
+
const concurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
6701
7253
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
6702
7254
|
const allDiagnostics = [];
|
|
6703
7255
|
const droppedFiles = [];
|
|
@@ -6717,23 +7269,31 @@ const spawnLintBatches = async (input) => {
|
|
|
6717
7269
|
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
6718
7270
|
}
|
|
6719
7271
|
};
|
|
7272
|
+
let startedFileCount = 0;
|
|
6720
7273
|
let scannedFileCount = 0;
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
const
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
7274
|
+
let displayedFileCount = 0;
|
|
7275
|
+
const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
|
|
7276
|
+
const ceiling = Math.min(startedFileCount, totalFileCount - 1);
|
|
7277
|
+
if (displayedFileCount < ceiling) {
|
|
7278
|
+
displayedFileCount += 1;
|
|
7279
|
+
onFileProgress(displayedFileCount, totalFileCount);
|
|
7280
|
+
}
|
|
7281
|
+
}, 50) : null;
|
|
7282
|
+
progressTimer?.unref?.();
|
|
7283
|
+
try {
|
|
7284
|
+
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
7285
|
+
startedFileCount += batch.length;
|
|
6730
7286
|
const batchDiagnostics = await spawnLintBatch(batch);
|
|
6731
|
-
allDiagnostics.push(...batchDiagnostics);
|
|
6732
7287
|
scannedFileCount += batch.length;
|
|
6733
|
-
onFileProgress
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
7288
|
+
if (onFileProgress) {
|
|
7289
|
+
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
7290
|
+
onFileProgress(displayedFileCount, totalFileCount);
|
|
7291
|
+
}
|
|
7292
|
+
return batchDiagnostics;
|
|
7293
|
+
});
|
|
7294
|
+
for (const batchDiagnostics of batchResults) allDiagnostics.push(...batchDiagnostics);
|
|
7295
|
+
} finally {
|
|
7296
|
+
if (progressTimer !== null) clearInterval(progressTimer);
|
|
6737
7297
|
}
|
|
6738
7298
|
if (droppedFiles.length > 0 && onPartialFailure) {
|
|
6739
7299
|
const previewFiles = droppedFiles.slice(0, 3).join(", ");
|
|
@@ -6860,7 +7420,8 @@ const runOxlint = async (options) => {
|
|
|
6860
7420
|
onPartialFailure,
|
|
6861
7421
|
onFileProgress: options.onFileProgress,
|
|
6862
7422
|
spawnTimeoutMs,
|
|
6863
|
-
outputMaxBytes
|
|
7423
|
+
outputMaxBytes,
|
|
7424
|
+
concurrency: options.concurrency
|
|
6864
7425
|
});
|
|
6865
7426
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6866
7427
|
try {
|
|
@@ -6928,6 +7489,7 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6928
7489
|
const partialFailures = yield* LintPartialFailures;
|
|
6929
7490
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
6930
7491
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
7492
|
+
const concurrency = yield* OxlintConcurrency;
|
|
6931
7493
|
const collectedFailures = [];
|
|
6932
7494
|
const diagnostics = yield* Effect.tryPromise({
|
|
6933
7495
|
try: () => runOxlint({
|
|
@@ -6946,7 +7508,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6946
7508
|
},
|
|
6947
7509
|
onFileProgress: input.onFileProgress,
|
|
6948
7510
|
spawnTimeoutMs,
|
|
6949
|
-
outputMaxBytes
|
|
7511
|
+
outputMaxBytes,
|
|
7512
|
+
concurrency
|
|
6950
7513
|
}),
|
|
6951
7514
|
catch: ensureReactDoctorError
|
|
6952
7515
|
});
|
|
@@ -7279,7 +7842,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7279
7842
|
showWarnings
|
|
7280
7843
|
});
|
|
7281
7844
|
const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
|
|
7282
|
-
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7845
|
+
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7846
|
+
...checkReducedMotion(scanDirectory),
|
|
7847
|
+
...checkPnpmHardening(scanDirectory),
|
|
7848
|
+
...checkExpoProject(scanDirectory, project)
|
|
7849
|
+
];
|
|
7283
7850
|
const envCollected = yield* Stream.runCollect(applyPerElementPipeline(Stream.fromIterable(environmentDiagnostics)));
|
|
7284
7851
|
const lintFailure = yield* Ref.make({
|
|
7285
7852
|
didFail: false,
|
|
@@ -7291,6 +7858,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7291
7858
|
didFail: false,
|
|
7292
7859
|
reason: null
|
|
7293
7860
|
});
|
|
7861
|
+
const scanConcurrency = yield* OxlintConcurrency;
|
|
7862
|
+
const workerCountSuffix = scanConcurrency > 1 ? ` · ${scanConcurrency} workers` : "";
|
|
7294
7863
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
7295
7864
|
const scanStartTime = Date.now();
|
|
7296
7865
|
let lastReportedTotalFileCount = 0;
|
|
@@ -7307,7 +7876,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7307
7876
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
7308
7877
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
7309
7878
|
lastReportedTotalFileCount = totalFileCount;
|
|
7310
|
-
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
7879
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
7311
7880
|
}
|
|
7312
7881
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
7313
7882
|
yield* Ref.set(lintFailure, {
|
|
@@ -7339,7 +7908,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7339
7908
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
7340
7909
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
7341
7910
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
7342
|
-
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
|
|
7911
|
+
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
7343
7912
|
yield* reporterService.finalize;
|
|
7344
7913
|
const finalDiagnostics = [
|
|
7345
7914
|
...envCollected,
|
|
@@ -7391,7 +7960,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7391
7960
|
"inspect.isCi": input.isCi,
|
|
7392
7961
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
7393
7962
|
} }));
|
|
7394
|
-
Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
7395
7963
|
const parseNodeVersion = (versionString) => {
|
|
7396
7964
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
7397
7965
|
return {
|
|
@@ -7690,6 +8258,26 @@ const buildJsonReport = (input) => {
|
|
|
7690
8258
|
};
|
|
7691
8259
|
};
|
|
7692
8260
|
/**
|
|
8261
|
+
* Single source of truth for the skipped-check accounting shared by the
|
|
8262
|
+
* CLI renderer (`react-doctor/src/inspect.ts → finalizeAndRender`) and the
|
|
8263
|
+
* programmatic shell (`@react-doctor/api → diagnose()`). Both surface a
|
|
8264
|
+
* failed lint / dead-code pass instead of a false "all clear", so the
|
|
8265
|
+
* branch logic lives here once.
|
|
8266
|
+
*/
|
|
8267
|
+
const buildSkippedChecks = (input) => {
|
|
8268
|
+
const skippedChecks = [];
|
|
8269
|
+
if (input.didLintFail) skippedChecks.push("lint");
|
|
8270
|
+
if (input.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
8271
|
+
const skippedCheckReasons = {};
|
|
8272
|
+
if (input.didLintFail && input.lintFailureReason !== null) skippedCheckReasons.lint = input.lintFailureReason;
|
|
8273
|
+
else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
|
|
8274
|
+
if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
|
|
8275
|
+
return {
|
|
8276
|
+
skippedChecks,
|
|
8277
|
+
skippedCheckReasons
|
|
8278
|
+
};
|
|
8279
|
+
};
|
|
8280
|
+
/**
|
|
7693
8281
|
* Programmatic façade over `Git.diffSelection`. Async because the
|
|
7694
8282
|
* Git service runs through Effect's `ChildProcess` (true subprocess
|
|
7695
8283
|
* spawn, not `spawnSync`).
|
|
@@ -7795,7 +8383,7 @@ import_picocolors.default.red, import_picocolors.default.yellow, import_picocolo
|
|
|
7795
8383
|
const clearAutoSuppressionCaches = () => {};
|
|
7796
8384
|
//#endregion
|
|
7797
8385
|
//#region ../api/dist/index.js
|
|
7798
|
-
const
|
|
8386
|
+
const buildDiagnoseLayer = (configLayer = Config.layerNode) => Layer.mergeAll(Project.layerNode, configLayer, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
7799
8387
|
const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
7800
8388
|
const effectiveConfig = configOverride ?? scanTarget.userConfig;
|
|
7801
8389
|
const includePaths = options.includePaths ?? [];
|
|
@@ -7814,13 +8402,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7814
8402
|
};
|
|
7815
8403
|
const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
7816
8404
|
if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
|
|
7817
|
-
const skippedChecks =
|
|
7818
|
-
if (output.didLintFail) skippedChecks.push("lint");
|
|
7819
|
-
if (output.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
7820
|
-
const skippedCheckReasons = {};
|
|
7821
|
-
if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
|
|
7822
|
-
else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
|
|
7823
|
-
if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
|
|
8405
|
+
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks(output);
|
|
7824
8406
|
return {
|
|
7825
8407
|
diagnostics: [...output.diagnostics],
|
|
7826
8408
|
score: output.score,
|
|
@@ -7833,7 +8415,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
|
7833
8415
|
const diagnose = async (directory, options = {}) => {
|
|
7834
8416
|
const startTime = globalThis.performance.now();
|
|
7835
8417
|
const program = buildInspectProgram(resolveScanTarget(directory), options);
|
|
7836
|
-
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(
|
|
8418
|
+
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(buildDiagnoseLayer()), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
|
|
7837
8419
|
};
|
|
7838
8420
|
//#endregion
|
|
7839
8421
|
//#region src/index.ts
|
|
@@ -7842,6 +8424,7 @@ const clearCaches = () => {
|
|
|
7842
8424
|
clearConfigCache();
|
|
7843
8425
|
clearPackageJsonCache();
|
|
7844
8426
|
clearIgnorePatternsCache();
|
|
8427
|
+
clearPackageRoleCache();
|
|
7845
8428
|
clearAutoSuppressionCaches();
|
|
7846
8429
|
};
|
|
7847
8430
|
const toJsonReport = (result, options) => buildJsonReport({
|