react-doctor 0.2.14-dev.daef23c → 0.2.14-dev.e9e71bb
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 +48 -2
- package/dist/cli.js +2516 -402
- package/dist/index.d.ts +47 -9
- package/dist/index.js +631 -98
- package/dist/skills/doctor-explain/SKILL.md +75 -0
- package/dist/skills/react-doctor/SKILL.md +4 -0
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fce73b02-d297-5132-af08-817f37e1467c")}catch(e){}}();
|
|
1
3
|
import { createRequire } from "node:module";
|
|
2
4
|
import * as Schema from "effect/Schema";
|
|
3
5
|
import * as fs$1 from "node:fs";
|
|
@@ -14,7 +16,10 @@ import * as Redacted from "effect/Redacted";
|
|
|
14
16
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
15
17
|
import * as Otlp from "effect/unstable/observability/Otlp";
|
|
16
18
|
import * as Context from "effect/Context";
|
|
19
|
+
import os from "node:os";
|
|
17
20
|
import * as Console from "effect/Console";
|
|
21
|
+
import { parseJSON5 } from "confbox";
|
|
22
|
+
import { createJiti } from "jiti";
|
|
18
23
|
import * as Fiber from "effect/Fiber";
|
|
19
24
|
import * as Filter from "effect/Filter";
|
|
20
25
|
import * as Option from "effect/Option";
|
|
@@ -26,7 +31,6 @@ import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
|
26
31
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
27
32
|
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
28
33
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
29
|
-
import os from "node:os";
|
|
30
34
|
import * as ts from "typescript";
|
|
31
35
|
import { gzipSync } from "node:zlib";
|
|
32
36
|
//#region \0rolldown/runtime.js
|
|
@@ -2874,29 +2878,34 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2874
2878
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2875
2879
|
};
|
|
2876
2880
|
};
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2881
|
+
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
2882
|
+
const rootValue = select(rootPackageJson);
|
|
2883
|
+
if (rootValue !== null) return rootValue;
|
|
2879
2884
|
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2880
|
-
if (patterns.length === 0) return
|
|
2885
|
+
if (patterns.length === 0) return null;
|
|
2881
2886
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2882
2887
|
for (const pattern of patterns) {
|
|
2883
|
-
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2888
|
+
const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
|
|
2884
2889
|
for (const workspaceDirectory of directories) {
|
|
2885
2890
|
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2886
2891
|
visitedDirectories.add(workspaceDirectory);
|
|
2887
|
-
|
|
2892
|
+
const value = select(readPackageJson(path.join(workspaceDirectory, "package.json")));
|
|
2893
|
+
if (value !== null) return value;
|
|
2888
2894
|
}
|
|
2889
2895
|
}
|
|
2890
|
-
return
|
|
2896
|
+
return null;
|
|
2891
2897
|
};
|
|
2898
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
|
|
2892
2899
|
const NAMES = new Set([
|
|
2893
2900
|
"react-native",
|
|
2894
2901
|
"react-native-tvos",
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2902
|
+
...new Set([
|
|
2903
|
+
"expo",
|
|
2904
|
+
"expo-router",
|
|
2905
|
+
"@expo/cli",
|
|
2906
|
+
"@expo/metro-config",
|
|
2907
|
+
"@expo/metro-runtime"
|
|
2908
|
+
]),
|
|
2900
2909
|
"react-native-windows",
|
|
2901
2910
|
"react-native-macos"
|
|
2902
2911
|
]);
|
|
@@ -2920,6 +2929,11 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2920
2929
|
return false;
|
|
2921
2930
|
};
|
|
2922
2931
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2932
|
+
const getExpoDependencySpec = (packageJson) => {
|
|
2933
|
+
const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
|
|
2934
|
+
return typeof spec === "string" ? spec : null;
|
|
2935
|
+
};
|
|
2936
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
|
|
2923
2937
|
const getPreactVersion = (packageJson) => {
|
|
2924
2938
|
return {
|
|
2925
2939
|
...packageJson.peerDependencies,
|
|
@@ -3159,6 +3173,19 @@ const discoverProject = (directory) => {
|
|
|
3159
3173
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
3160
3174
|
const sourceFileCount = countSourceFiles(directory);
|
|
3161
3175
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
3176
|
+
let expoVersion = hasReactNativeWorkspace ? findExpoVersion(directory, packageJson) : null;
|
|
3177
|
+
if (expoVersion !== null && isCatalogReference(expoVersion)) {
|
|
3178
|
+
const catalogName = extractCatalogName(expoVersion);
|
|
3179
|
+
let resolvedExpoVersion = resolveCatalogVersion(packageJson, "expo", directory, catalogName);
|
|
3180
|
+
if (!resolvedExpoVersion) {
|
|
3181
|
+
const monorepoRoot = findMonorepoRoot(directory);
|
|
3182
|
+
if (monorepoRoot) {
|
|
3183
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
3184
|
+
if (isFile(monorepoPackageJsonPath)) resolvedExpoVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "expo", monorepoRoot, catalogName);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
expoVersion = resolvedExpoVersion ?? expoVersion;
|
|
3188
|
+
}
|
|
3162
3189
|
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
3163
3190
|
const preactVersion = getPreactVersion(packageJson);
|
|
3164
3191
|
const projectInfo = {
|
|
@@ -3176,6 +3203,7 @@ const discoverProject = (directory) => {
|
|
|
3176
3203
|
preactVersion,
|
|
3177
3204
|
preactMajorVersion: parseReactMajor(preactVersion),
|
|
3178
3205
|
hasReactNativeWorkspace,
|
|
3206
|
+
expoVersion,
|
|
3179
3207
|
hasReanimated,
|
|
3180
3208
|
sourceFileCount
|
|
3181
3209
|
};
|
|
@@ -3265,7 +3293,14 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
|
3265
3293
|
"tsconfig.json",
|
|
3266
3294
|
"tsconfig.base.json",
|
|
3267
3295
|
"package.json",
|
|
3268
|
-
"
|
|
3296
|
+
"doctor.config.ts",
|
|
3297
|
+
"doctor.config.mts",
|
|
3298
|
+
"doctor.config.cts",
|
|
3299
|
+
"doctor.config.js",
|
|
3300
|
+
"doctor.config.mjs",
|
|
3301
|
+
"doctor.config.cjs",
|
|
3302
|
+
"doctor.config.json",
|
|
3303
|
+
"doctor.config.jsonc",
|
|
3269
3304
|
"oxlint.json",
|
|
3270
3305
|
".oxlintrc.json"
|
|
3271
3306
|
];
|
|
@@ -4247,6 +4282,17 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
4247
4282
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
4248
4283
|
}).pipe(Effect.orDie));
|
|
4249
4284
|
/**
|
|
4285
|
+
* Resolves a requested lint worker count to a clamped integer within
|
|
4286
|
+
* `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
|
|
4287
|
+
* machine's CPU cores; out-of-range or non-finite requests degrade to
|
|
4288
|
+
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
4289
|
+
*/
|
|
4290
|
+
const resolveScanConcurrency = (requested) => {
|
|
4291
|
+
const desired = requested === "auto" ? os.availableParallelism() : requested;
|
|
4292
|
+
if (!Number.isFinite(desired) || desired < 1) return 1;
|
|
4293
|
+
return Math.max(1, Math.min(Math.floor(desired), 16));
|
|
4294
|
+
};
|
|
4295
|
+
/**
|
|
4250
4296
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
4251
4297
|
* startup so the eval harness can raise the budget under sandbox
|
|
4252
4298
|
* microVMs without recompiling react-doctor. Tests override via
|
|
@@ -4266,6 +4312,30 @@ var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintS
|
|
|
4266
4312
|
* tests that exercise the cap behavior.
|
|
4267
4313
|
*/
|
|
4268
4314
|
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
4315
|
+
/**
|
|
4316
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
4317
|
+
* to `1` (serial — the historical behavior) so resource usage is opt-in.
|
|
4318
|
+
* The CLI's `--experimental-parallel` flag overrides this via `Layer.succeed`; the
|
|
4319
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic /
|
|
4320
|
+
* CI callers that never touch the flag:
|
|
4321
|
+
*
|
|
4322
|
+
* - unset / `0` / `false` / `off` → `1` (serial)
|
|
4323
|
+
* - `auto` / `true` / `on` → available CPU cores (clamped)
|
|
4324
|
+
* - a positive integer → that many workers (clamped)
|
|
4325
|
+
*
|
|
4326
|
+
* The resolved value is always within
|
|
4327
|
+
* `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
|
|
4328
|
+
*/
|
|
4329
|
+
var OxlintConcurrency = class extends Context.Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
4330
|
+
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
4331
|
+
if (raw === void 0) return 1;
|
|
4332
|
+
const normalized = raw.trim().toLowerCase();
|
|
4333
|
+
if (normalized === "" || normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
4334
|
+
if (normalized === "auto" || normalized === "true" || normalized === "on") return resolveScanConcurrency("auto");
|
|
4335
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
4336
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return 1;
|
|
4337
|
+
return resolveScanConcurrency(parsed);
|
|
4338
|
+
} }) {};
|
|
4269
4339
|
const DIAGNOSTIC_SURFACES = [
|
|
4270
4340
|
"cli",
|
|
4271
4341
|
"prComment",
|
|
@@ -4422,69 +4492,109 @@ const validateConfigTypes = (config) => {
|
|
|
4422
4492
|
const warn = (message) => {
|
|
4423
4493
|
Effect.runSync(Console.warn(message));
|
|
4424
4494
|
};
|
|
4425
|
-
const
|
|
4495
|
+
const CONFIG_BASENAME = "doctor.config";
|
|
4496
|
+
const CONFIG_EXTENSIONS = [
|
|
4497
|
+
"ts",
|
|
4498
|
+
"mts",
|
|
4499
|
+
"cts",
|
|
4500
|
+
"js",
|
|
4501
|
+
"mjs",
|
|
4502
|
+
"cjs",
|
|
4503
|
+
"json",
|
|
4504
|
+
"jsonc"
|
|
4505
|
+
];
|
|
4506
|
+
const DATA_CONFIG_EXTENSIONS = new Set(["json", "jsonc"]);
|
|
4507
|
+
const PACKAGE_JSON_FILENAME = "package.json";
|
|
4426
4508
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
4427
|
-
const
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
const packageJsonPath = path.join(directory, "package.json");
|
|
4441
|
-
if (isFile(packageJsonPath)) try {
|
|
4442
|
-
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
4443
|
-
const packageJson = JSON.parse(fileContent);
|
|
4509
|
+
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
4510
|
+
const jiti = createJiti(import.meta.url);
|
|
4511
|
+
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
4512
|
+
const loadModuleConfig = async (filePath) => {
|
|
4513
|
+
const imported = await jiti.import(filePath);
|
|
4514
|
+
return imported?.default ?? imported;
|
|
4515
|
+
};
|
|
4516
|
+
const readDataConfig = (filePath) => parseJSON5(fs.readFileSync(filePath, "utf-8"));
|
|
4517
|
+
const readEmbeddedPackageJsonConfig = (directory) => {
|
|
4518
|
+
const packageJsonPath = path.join(directory, PACKAGE_JSON_FILENAME);
|
|
4519
|
+
if (!isFile(packageJsonPath)) return null;
|
|
4520
|
+
try {
|
|
4521
|
+
const packageJson = parseJSON5(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
4444
4522
|
if (isPlainObject(packageJson)) {
|
|
4445
4523
|
const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
|
|
4446
|
-
if (isPlainObject(embeddedConfig)) return
|
|
4447
|
-
|
|
4448
|
-
|
|
4524
|
+
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
4525
|
+
}
|
|
4526
|
+
} catch {}
|
|
4527
|
+
return null;
|
|
4528
|
+
};
|
|
4529
|
+
const loadPackageJsonConfig = (directory) => {
|
|
4530
|
+
const embeddedConfig = readEmbeddedPackageJsonConfig(directory);
|
|
4531
|
+
if (!embeddedConfig) return null;
|
|
4532
|
+
return {
|
|
4533
|
+
config: validateConfigTypes(embeddedConfig),
|
|
4534
|
+
sourceDirectory: directory,
|
|
4535
|
+
configFilePath: path.join(directory, PACKAGE_JSON_FILENAME),
|
|
4536
|
+
format: "package-json"
|
|
4537
|
+
};
|
|
4538
|
+
};
|
|
4539
|
+
const loadConfigFromDirectory = async (directory) => {
|
|
4540
|
+
let sawBrokenConfigFile = false;
|
|
4541
|
+
for (const extension of CONFIG_EXTENSIONS) {
|
|
4542
|
+
const filePath = path.join(directory, `${CONFIG_BASENAME}.${extension}`);
|
|
4543
|
+
if (!isFile(filePath)) continue;
|
|
4544
|
+
const isDataFile = DATA_CONFIG_EXTENSIONS.has(extension);
|
|
4545
|
+
try {
|
|
4546
|
+
const parsed = isDataFile ? readDataConfig(filePath) : await loadModuleConfig(filePath);
|
|
4547
|
+
if (isPlainObject(parsed)) return {
|
|
4548
|
+
status: "found",
|
|
4549
|
+
loaded: {
|
|
4550
|
+
config: validateConfigTypes(parsed),
|
|
4551
|
+
sourceDirectory: directory,
|
|
4552
|
+
configFilePath: filePath,
|
|
4553
|
+
format: isDataFile ? "json" : "module"
|
|
4554
|
+
}
|
|
4449
4555
|
};
|
|
4556
|
+
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
4557
|
+
sawBrokenConfigFile = true;
|
|
4558
|
+
} catch (error) {
|
|
4559
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
|
|
4560
|
+
sawBrokenConfigFile = true;
|
|
4450
4561
|
}
|
|
4451
|
-
} catch {
|
|
4452
|
-
return null;
|
|
4453
4562
|
}
|
|
4454
|
-
|
|
4563
|
+
const packageJsonConfig = loadPackageJsonConfig(directory);
|
|
4564
|
+
if (packageJsonConfig) return {
|
|
4565
|
+
status: "found",
|
|
4566
|
+
loaded: packageJsonConfig
|
|
4567
|
+
};
|
|
4568
|
+
if (isFile(path.join(directory, LEGACY_CONFIG_FILENAME))) warn(`${LEGACY_CONFIG_FILENAME} is no longer read — rename it to ${CONFIG_BASENAME}.json (or author a ${CONFIG_BASENAME}.ts).`);
|
|
4569
|
+
return {
|
|
4570
|
+
status: sawBrokenConfigFile ? "invalid" : "absent",
|
|
4571
|
+
loaded: null
|
|
4572
|
+
};
|
|
4455
4573
|
};
|
|
4456
4574
|
const cachedConfigs = /* @__PURE__ */ new Map();
|
|
4457
4575
|
const clearConfigCache = () => {
|
|
4458
4576
|
cachedConfigs.clear();
|
|
4459
4577
|
};
|
|
4460
|
-
const
|
|
4461
|
-
const
|
|
4462
|
-
if (
|
|
4463
|
-
|
|
4464
|
-
if (localConfig) {
|
|
4465
|
-
cachedConfigs.set(rootDirectory, localConfig);
|
|
4466
|
-
return localConfig;
|
|
4467
|
-
}
|
|
4468
|
-
if (isProjectBoundary(rootDirectory)) {
|
|
4469
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4470
|
-
return null;
|
|
4471
|
-
}
|
|
4578
|
+
const loadConfigWalkingUp = async (rootDirectory) => {
|
|
4579
|
+
const localResult = await loadConfigFromDirectory(rootDirectory);
|
|
4580
|
+
if (localResult.status === "found") return localResult.loaded;
|
|
4581
|
+
if (localResult.status === "invalid" || isProjectBoundary(rootDirectory)) return null;
|
|
4472
4582
|
let ancestorDirectory = path.dirname(rootDirectory);
|
|
4473
4583
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
4474
|
-
const
|
|
4475
|
-
if (
|
|
4476
|
-
|
|
4477
|
-
return ancestorConfig;
|
|
4478
|
-
}
|
|
4479
|
-
if (isProjectBoundary(ancestorDirectory)) {
|
|
4480
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4481
|
-
return null;
|
|
4482
|
-
}
|
|
4584
|
+
const ancestorResult = await loadConfigFromDirectory(ancestorDirectory);
|
|
4585
|
+
if (ancestorResult.status === "found") return ancestorResult.loaded;
|
|
4586
|
+
if (isProjectBoundary(ancestorDirectory)) return null;
|
|
4483
4587
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
4484
4588
|
}
|
|
4485
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4486
4589
|
return null;
|
|
4487
4590
|
};
|
|
4591
|
+
const loadConfigWithSource = (rootDirectory) => {
|
|
4592
|
+
const cached = cachedConfigs.get(rootDirectory);
|
|
4593
|
+
if (cached !== void 0) return cached;
|
|
4594
|
+
const loadPromise = loadConfigWalkingUp(rootDirectory);
|
|
4595
|
+
cachedConfigs.set(rootDirectory, loadPromise);
|
|
4596
|
+
return loadPromise;
|
|
4597
|
+
};
|
|
4488
4598
|
const resolveConfigRootDir = (config, configSourceDirectory) => {
|
|
4489
4599
|
if (!config || !configSourceDirectory) return null;
|
|
4490
4600
|
const rawRootDir = config.rootDir;
|
|
@@ -4512,8 +4622,7 @@ const resolveDiagnoseTarget = (directory, options = {}) => {
|
|
|
4512
4622
|
* (`inspect()`, `diagnose()`, and the CLI's `inspectAction`):
|
|
4513
4623
|
*
|
|
4514
4624
|
* 1. Resolve the requested directory to absolute.
|
|
4515
|
-
* 2. Load `
|
|
4516
|
-
* if present.
|
|
4625
|
+
* 2. Load `doctor.config.*` / `package.json#reactDoctor` if present.
|
|
4517
4626
|
* 3. Honor `config.rootDir` to redirect the scan to a nested
|
|
4518
4627
|
* project root, if configured.
|
|
4519
4628
|
* 4. Walk into a nested React subproject when the requested
|
|
@@ -4531,9 +4640,9 @@ const resolveDiagnoseTarget = (directory, options = {}) => {
|
|
|
4531
4640
|
* via its own cache). Routing through `resolveScanTarget` keeps every
|
|
4532
4641
|
* shell in agreement on what "the scan directory" means.
|
|
4533
4642
|
*/
|
|
4534
|
-
const resolveScanTarget = (requestedDirectory, options = {}) => {
|
|
4643
|
+
const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
4535
4644
|
const absoluteRequested = path.resolve(requestedDirectory);
|
|
4536
|
-
const loadedConfig = loadConfigWithSource(absoluteRequested);
|
|
4645
|
+
const loadedConfig = await loadConfigWithSource(absoluteRequested);
|
|
4537
4646
|
const userConfig = loadedConfig?.config ?? null;
|
|
4538
4647
|
const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
|
|
4539
4648
|
const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
|
|
@@ -4548,6 +4657,359 @@ const resolveScanTarget = (requestedDirectory, options = {}) => {
|
|
|
4548
4657
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
4549
4658
|
};
|
|
4550
4659
|
};
|
|
4660
|
+
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
4661
|
+
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
4662
|
+
const packageJson = readPackageJson(path.join(rootDirectory, "package.json"));
|
|
4663
|
+
return {
|
|
4664
|
+
rootDirectory,
|
|
4665
|
+
packageJson,
|
|
4666
|
+
directDependencyNames: getDirectDependencyNames(packageJson),
|
|
4667
|
+
expoSdkMajor: getLowestDependencyMajor(expoVersion)
|
|
4668
|
+
};
|
|
4669
|
+
};
|
|
4670
|
+
const buildExpoDiagnostic = (input) => ({
|
|
4671
|
+
filePath: input.filePath ?? "package.json",
|
|
4672
|
+
plugin: "react-doctor",
|
|
4673
|
+
rule: input.rule,
|
|
4674
|
+
severity: input.severity ?? "warning",
|
|
4675
|
+
message: input.message,
|
|
4676
|
+
help: input.help,
|
|
4677
|
+
line: input.line ?? 0,
|
|
4678
|
+
column: input.column ?? 0,
|
|
4679
|
+
category: input.category ?? "Correctness"
|
|
4680
|
+
});
|
|
4681
|
+
const CRITICAL_OVERRIDE_NAMES = new Set([
|
|
4682
|
+
"@expo/cli",
|
|
4683
|
+
"@expo/config",
|
|
4684
|
+
"@expo/metro-config",
|
|
4685
|
+
"@expo/metro-runtime",
|
|
4686
|
+
"@expo/metro",
|
|
4687
|
+
"metro"
|
|
4688
|
+
]);
|
|
4689
|
+
const isCriticalOverrideName = (packageName) => CRITICAL_OVERRIDE_NAMES.has(packageName) || packageName.startsWith("metro-");
|
|
4690
|
+
const collectOverrideNames = (packageJson) => new Set([
|
|
4691
|
+
...Object.keys(packageJson.overrides ?? {}),
|
|
4692
|
+
...Object.keys(packageJson.resolutions ?? {}),
|
|
4693
|
+
...Object.keys(packageJson.pnpm?.overrides ?? {})
|
|
4694
|
+
]);
|
|
4695
|
+
const checkExpoDependencyOverrides = (context) => {
|
|
4696
|
+
const overriddenCriticalNames = [...collectOverrideNames(context.packageJson)].filter(isCriticalOverrideName).sort();
|
|
4697
|
+
if (overriddenCriticalNames.length === 0) return [];
|
|
4698
|
+
const quotedNames = overriddenCriticalNames.map((name) => `"${name}"`).join(", ");
|
|
4699
|
+
return [buildExpoDiagnostic({
|
|
4700
|
+
rule: "expo-no-conflicting-dependency-override",
|
|
4701
|
+
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`,
|
|
4702
|
+
help: `Remove the override/resolution for ${quotedNames} and reinstall so the Expo-pinned versions are used`
|
|
4703
|
+
})];
|
|
4704
|
+
};
|
|
4705
|
+
const isPathGitIgnored = (rootDirectory, absolutePath) => {
|
|
4706
|
+
const result = spawnSync("git", [
|
|
4707
|
+
"check-ignore",
|
|
4708
|
+
"-q",
|
|
4709
|
+
absolutePath
|
|
4710
|
+
], {
|
|
4711
|
+
cwd: rootDirectory,
|
|
4712
|
+
stdio: [
|
|
4713
|
+
"ignore",
|
|
4714
|
+
"ignore",
|
|
4715
|
+
"ignore"
|
|
4716
|
+
]
|
|
4717
|
+
});
|
|
4718
|
+
if (result.error) return null;
|
|
4719
|
+
if (result.status === 0) return true;
|
|
4720
|
+
if (result.status === 1) return false;
|
|
4721
|
+
return null;
|
|
4722
|
+
};
|
|
4723
|
+
const LOCAL_ENV_FILE_NAMES = [
|
|
4724
|
+
".env.local",
|
|
4725
|
+
".env.development.local",
|
|
4726
|
+
".env.production.local",
|
|
4727
|
+
".env.test.local"
|
|
4728
|
+
];
|
|
4729
|
+
const checkExpoEnvLocalFiles = (context) => {
|
|
4730
|
+
const { rootDirectory } = context;
|
|
4731
|
+
const committedEnvFiles = LOCAL_ENV_FILE_NAMES.filter((fileName) => {
|
|
4732
|
+
const filePath = path.join(rootDirectory, fileName);
|
|
4733
|
+
if (!isFile(filePath)) return false;
|
|
4734
|
+
return isPathGitIgnored(rootDirectory, filePath) === false;
|
|
4735
|
+
});
|
|
4736
|
+
if (committedEnvFiles.length === 0) return [];
|
|
4737
|
+
return [buildExpoDiagnostic({
|
|
4738
|
+
rule: "expo-env-local-not-gitignored",
|
|
4739
|
+
category: "Security",
|
|
4740
|
+
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`,
|
|
4741
|
+
help: `Add \`.env*.local\` to your .gitignore. If already committed, untrack with \`git rm --cached ${committedEnvFiles.join(" ")}\``
|
|
4742
|
+
})];
|
|
4743
|
+
};
|
|
4744
|
+
const isExpoSdkAtLeast = (expoSdkMajor, minMajor) => expoSdkMajor !== null && expoSdkMajor >= minMajor;
|
|
4745
|
+
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";
|
|
4746
|
+
const FIREBASE_HELP = "Use the Firebase JS SDK or React Native Firebase directly. See https://expo.fyi/firebase-migration-guide";
|
|
4747
|
+
const unimodulesEntry = (packageName) => ({
|
|
4748
|
+
packageName,
|
|
4749
|
+
rule: "expo-no-unimodules-packages",
|
|
4750
|
+
message: `"${packageName}" is a legacy unimodules package that is incompatible with Expo SDK 44+ and will break native builds`,
|
|
4751
|
+
help: UNIMODULES_HELP
|
|
4752
|
+
});
|
|
4753
|
+
const FLAGGED_DEPENDENCIES = [
|
|
4754
|
+
unimodulesEntry("@unimodules/core"),
|
|
4755
|
+
unimodulesEntry("@unimodules/react-native-adapter"),
|
|
4756
|
+
unimodulesEntry("react-native-unimodules"),
|
|
4757
|
+
{
|
|
4758
|
+
packageName: "expo-cli",
|
|
4759
|
+
rule: "expo-no-cli-dependencies",
|
|
4760
|
+
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`",
|
|
4761
|
+
help: "Remove `expo-cli` from your dependencies and use the bundled CLI via `npx expo`"
|
|
4762
|
+
},
|
|
4763
|
+
{
|
|
4764
|
+
packageName: "eas-cli",
|
|
4765
|
+
rule: "expo-no-cli-dependencies",
|
|
4766
|
+
message: "`eas-cli` is a project dependency — pinning it in package.json drifts from the latest EAS CLI and bloats installs",
|
|
4767
|
+
help: "Remove `eas-cli` from your dependencies and run it on demand with `npx eas-cli` (or install it globally)"
|
|
4768
|
+
},
|
|
4769
|
+
{
|
|
4770
|
+
packageName: "expo-modules-autolinking",
|
|
4771
|
+
rule: "expo-no-redundant-dependency",
|
|
4772
|
+
message: "\"expo-modules-autolinking\" should not be a direct dependency — Expo installs it transitively as needed",
|
|
4773
|
+
help: "Remove `expo-modules-autolinking` from your package.json"
|
|
4774
|
+
},
|
|
4775
|
+
{
|
|
4776
|
+
packageName: "expo-dev-launcher",
|
|
4777
|
+
rule: "expo-no-redundant-dependency",
|
|
4778
|
+
message: "\"expo-dev-launcher\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4779
|
+
help: "Remove `expo-dev-launcher` and depend on `expo-dev-client` instead"
|
|
4780
|
+
},
|
|
4781
|
+
{
|
|
4782
|
+
packageName: "expo-dev-menu",
|
|
4783
|
+
rule: "expo-no-redundant-dependency",
|
|
4784
|
+
message: "\"expo-dev-menu\" should not be a direct dependency — it is pulled in by `expo-dev-client`",
|
|
4785
|
+
help: "Remove `expo-dev-menu` and depend on `expo-dev-client` instead"
|
|
4786
|
+
},
|
|
4787
|
+
{
|
|
4788
|
+
packageName: "expo-modules-core",
|
|
4789
|
+
rule: "expo-no-redundant-dependency",
|
|
4790
|
+
message: "\"expo-modules-core\" should not be a direct dependency — use the API re-exported from the `expo` package",
|
|
4791
|
+
help: "Remove `expo-modules-core` from your package.json and import from `expo` instead"
|
|
4792
|
+
},
|
|
4793
|
+
{
|
|
4794
|
+
packageName: "@expo/metro-config",
|
|
4795
|
+
rule: "expo-no-redundant-dependency",
|
|
4796
|
+
message: "\"@expo/metro-config\" should not be a direct dependency — use the `expo/metro-config` sub-export of the `expo` package",
|
|
4797
|
+
help: "Remove `@expo/metro-config` and import `expo/metro-config` in your metro.config.js"
|
|
4798
|
+
},
|
|
4799
|
+
{
|
|
4800
|
+
packageName: "@types/react-native",
|
|
4801
|
+
rule: "expo-no-redundant-dependency",
|
|
4802
|
+
message: "\"@types/react-native\" should not be installed — React Native ships its own types since SDK 48",
|
|
4803
|
+
help: "Remove `@types/react-native` from your package.json",
|
|
4804
|
+
minSdkMajor: 48
|
|
4805
|
+
},
|
|
4806
|
+
{
|
|
4807
|
+
packageName: "@expo/config-plugins",
|
|
4808
|
+
rule: "expo-no-redundant-dependency",
|
|
4809
|
+
message: "\"@expo/config-plugins\" should not be a direct dependency — use the `expo/config-plugins` sub-export of the `expo` package",
|
|
4810
|
+
help: "Remove `@expo/config-plugins`; config-plugin authors should import from `expo/config-plugins`. See https://github.com/expo/expo/pull/18855",
|
|
4811
|
+
minSdkMajor: 48
|
|
4812
|
+
},
|
|
4813
|
+
{
|
|
4814
|
+
packageName: "@expo/prebuild-config",
|
|
4815
|
+
rule: "expo-no-redundant-dependency",
|
|
4816
|
+
message: "\"@expo/prebuild-config\" should not be a direct dependency — Expo installs it transitively",
|
|
4817
|
+
help: "Remove `@expo/prebuild-config` from your package.json",
|
|
4818
|
+
minSdkMajor: 53
|
|
4819
|
+
},
|
|
4820
|
+
{
|
|
4821
|
+
packageName: "expo-permissions",
|
|
4822
|
+
rule: "expo-no-redundant-dependency",
|
|
4823
|
+
message: "\"expo-permissions\" was deprecated in SDK 41 and may no longer compile — permissions moved onto each module (e.g. `MediaLibrary.requestPermissionsAsync()`)",
|
|
4824
|
+
help: "Remove `expo-permissions` and request permissions from the relevant module instead",
|
|
4825
|
+
minSdkMajor: 50
|
|
4826
|
+
},
|
|
4827
|
+
{
|
|
4828
|
+
packageName: "expo-app-loading",
|
|
4829
|
+
rule: "expo-no-redundant-dependency",
|
|
4830
|
+
message: "\"expo-app-loading\" was removed in SDK 49",
|
|
4831
|
+
help: "Remove `expo-app-loading` and use `expo-splash-screen` instead. See https://docs.expo.dev/versions/latest/sdk/splash-screen/",
|
|
4832
|
+
minSdkMajor: 49
|
|
4833
|
+
},
|
|
4834
|
+
{
|
|
4835
|
+
packageName: "expo-firebase-analytics",
|
|
4836
|
+
rule: "expo-no-redundant-dependency",
|
|
4837
|
+
message: "\"expo-firebase-analytics\" was removed in SDK 48",
|
|
4838
|
+
help: FIREBASE_HELP,
|
|
4839
|
+
minSdkMajor: 48
|
|
4840
|
+
},
|
|
4841
|
+
{
|
|
4842
|
+
packageName: "expo-firebase-recaptcha",
|
|
4843
|
+
rule: "expo-no-redundant-dependency",
|
|
4844
|
+
message: "\"expo-firebase-recaptcha\" was removed in SDK 48",
|
|
4845
|
+
help: FIREBASE_HELP,
|
|
4846
|
+
minSdkMajor: 48
|
|
4847
|
+
},
|
|
4848
|
+
{
|
|
4849
|
+
packageName: "expo-firebase-core",
|
|
4850
|
+
rule: "expo-no-redundant-dependency",
|
|
4851
|
+
message: "\"expo-firebase-core\" was removed in SDK 48",
|
|
4852
|
+
help: FIREBASE_HELP,
|
|
4853
|
+
minSdkMajor: 48
|
|
4854
|
+
}
|
|
4855
|
+
];
|
|
4856
|
+
const checkExpoFlaggedDependencies = (context) => FLAGGED_DEPENDENCIES.filter((flaggedDependency) => {
|
|
4857
|
+
if (!context.directDependencyNames.has(flaggedDependency.packageName)) return false;
|
|
4858
|
+
if (flaggedDependency.minSdkMajor === void 0) return true;
|
|
4859
|
+
return isExpoSdkAtLeast(context.expoSdkMajor, flaggedDependency.minSdkMajor);
|
|
4860
|
+
}).map((flaggedDependency) => buildExpoDiagnostic({
|
|
4861
|
+
rule: flaggedDependency.rule,
|
|
4862
|
+
message: flaggedDependency.message,
|
|
4863
|
+
help: flaggedDependency.help
|
|
4864
|
+
}));
|
|
4865
|
+
const findLocalModuleNativeFiles = (rootDirectory) => {
|
|
4866
|
+
const modulesDirectory = path.join(rootDirectory, "modules");
|
|
4867
|
+
if (!isDirectory(modulesDirectory)) return [];
|
|
4868
|
+
const nativeFilePaths = [];
|
|
4869
|
+
for (const moduleEntry of readDirectoryEntries(modulesDirectory)) {
|
|
4870
|
+
if (!moduleEntry.isDirectory()) continue;
|
|
4871
|
+
const moduleDirectory = path.join(modulesDirectory, moduleEntry.name);
|
|
4872
|
+
const gradlePath = path.join(moduleDirectory, "android", "build.gradle");
|
|
4873
|
+
if (isFile(gradlePath)) nativeFilePaths.push(gradlePath);
|
|
4874
|
+
const iosDirectory = path.join(moduleDirectory, "ios");
|
|
4875
|
+
if (isDirectory(iosDirectory)) {
|
|
4876
|
+
for (const iosEntry of readDirectoryEntries(iosDirectory)) if (iosEntry.isFile() && iosEntry.name.endsWith(".podspec")) nativeFilePaths.push(path.join(iosDirectory, iosEntry.name));
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
return nativeFilePaths;
|
|
4880
|
+
};
|
|
4881
|
+
const checkExpoGitignore = (context) => {
|
|
4882
|
+
const { rootDirectory } = context;
|
|
4883
|
+
const diagnostics = [];
|
|
4884
|
+
const expoStateDirectory = path.join(rootDirectory, ".expo");
|
|
4885
|
+
if (isDirectory(expoStateDirectory) && isPathGitIgnored(rootDirectory, expoStateDirectory) === false) diagnostics.push(buildExpoDiagnostic({
|
|
4886
|
+
rule: "expo-gitignore",
|
|
4887
|
+
message: "The `.expo` directory is not ignored by Git — it holds machine-specific device history and dev-server settings that should not be committed",
|
|
4888
|
+
help: "Add `.expo/` to your .gitignore"
|
|
4889
|
+
}));
|
|
4890
|
+
if (findLocalModuleNativeFiles(rootDirectory).find((nativeFilePath) => isPathGitIgnored(rootDirectory, nativeFilePath) === true) !== void 0) diagnostics.push(buildExpoDiagnostic({
|
|
4891
|
+
rule: "expo-gitignore",
|
|
4892
|
+
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",
|
|
4893
|
+
help: "Use anchored patterns like `/ios` and `/android` in .gitignore so only the top-level native directories are excluded, not those inside `modules/`"
|
|
4894
|
+
}));
|
|
4895
|
+
return diagnostics;
|
|
4896
|
+
};
|
|
4897
|
+
const LOCKFILE_NAMES = [
|
|
4898
|
+
"pnpm-lock.yaml",
|
|
4899
|
+
"yarn.lock",
|
|
4900
|
+
"package-lock.json",
|
|
4901
|
+
"bun.lockb",
|
|
4902
|
+
"bun.lock"
|
|
4903
|
+
];
|
|
4904
|
+
const checkExpoLockfile = (context) => {
|
|
4905
|
+
const workspaceRoot = isMonorepoRoot(context.rootDirectory) ? context.rootDirectory : findMonorepoRoot(context.rootDirectory) ?? context.rootDirectory;
|
|
4906
|
+
const presentLockfiles = LOCKFILE_NAMES.filter((lockfileName) => isFile(path.join(workspaceRoot, lockfileName)));
|
|
4907
|
+
if (presentLockfiles.length === 0) return [buildExpoDiagnostic({
|
|
4908
|
+
rule: "expo-lockfile",
|
|
4909
|
+
message: "No lock file detected at the project root — installs are not reproducible, and EAS Build cannot infer your package manager",
|
|
4910
|
+
help: "Install dependencies with your package manager to generate a lock file, then commit it"
|
|
4911
|
+
})];
|
|
4912
|
+
if (presentLockfiles.length > 1) return [buildExpoDiagnostic({
|
|
4913
|
+
rule: "expo-lockfile",
|
|
4914
|
+
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`,
|
|
4915
|
+
help: "Delete the lock files for the package managers you are not using and keep only one"
|
|
4916
|
+
})];
|
|
4917
|
+
return [];
|
|
4918
|
+
};
|
|
4919
|
+
const METRO_CONFIG_FILE_NAMES = [
|
|
4920
|
+
"metro.config.js",
|
|
4921
|
+
"metro.config.cjs",
|
|
4922
|
+
"metro.config.mjs",
|
|
4923
|
+
"metro.config.ts"
|
|
4924
|
+
];
|
|
4925
|
+
const EXPO_METRO_CONFIG_EXTEND_SIGNALS = [
|
|
4926
|
+
"expo/metro-config",
|
|
4927
|
+
"@sentry/react-native/metro",
|
|
4928
|
+
"getSentryExpoConfig"
|
|
4929
|
+
];
|
|
4930
|
+
const checkExpoMetroConfig = (context) => {
|
|
4931
|
+
const metroConfigPath = METRO_CONFIG_FILE_NAMES.map((fileName) => path.join(context.rootDirectory, fileName)).find((candidatePath) => isFile(candidatePath));
|
|
4932
|
+
if (metroConfigPath === void 0) return [];
|
|
4933
|
+
let contents;
|
|
4934
|
+
try {
|
|
4935
|
+
contents = fs.readFileSync(metroConfigPath, "utf-8");
|
|
4936
|
+
} catch {
|
|
4937
|
+
return [];
|
|
4938
|
+
}
|
|
4939
|
+
if (EXPO_METRO_CONFIG_EXTEND_SIGNALS.some((signal) => contents.includes(signal))) return [];
|
|
4940
|
+
return [buildExpoDiagnostic({
|
|
4941
|
+
rule: "expo-metro-config",
|
|
4942
|
+
filePath: path.basename(metroConfigPath),
|
|
4943
|
+
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",
|
|
4944
|
+
help: "Update your metro config to extend `expo/metro-config`. See https://docs.expo.dev/guides/customizing-metro/"
|
|
4945
|
+
})];
|
|
4946
|
+
};
|
|
4947
|
+
const CONFLICTING_SCRIPT_NAMES = ["expo", "react-native"];
|
|
4948
|
+
const checkExpoPackageJsonConflicts = (context) => {
|
|
4949
|
+
const { packageJson } = context;
|
|
4950
|
+
const diagnostics = [];
|
|
4951
|
+
const conflictingScriptNames = CONFLICTING_SCRIPT_NAMES.filter((scriptName) => Boolean(packageJson.scripts?.[scriptName]));
|
|
4952
|
+
if (conflictingScriptNames.length > 0) {
|
|
4953
|
+
const quotedNames = conflictingScriptNames.map((name) => `"${name}"`).join(", ");
|
|
4954
|
+
const shadowsExpoCli = conflictingScriptNames.includes("expo");
|
|
4955
|
+
diagnostics.push(buildExpoDiagnostic({
|
|
4956
|
+
rule: "expo-package-json-conflict",
|
|
4957
|
+
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" : ""}`,
|
|
4958
|
+
help: "Rename these scripts so they don't collide with the `expo` / `react-native` binaries"
|
|
4959
|
+
}));
|
|
4960
|
+
}
|
|
4961
|
+
const packageName = packageJson.name;
|
|
4962
|
+
if (typeof packageName === "string" && context.directDependencyNames.has(packageName)) diagnostics.push(buildExpoDiagnostic({
|
|
4963
|
+
rule: "expo-package-json-conflict",
|
|
4964
|
+
message: `package.json "name" is "${packageName}", which collides with a dependency of the same name`,
|
|
4965
|
+
help: "Rename your package so it no longer matches one of its dependencies"
|
|
4966
|
+
}));
|
|
4967
|
+
return diagnostics;
|
|
4968
|
+
};
|
|
4969
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR = 56;
|
|
4970
|
+
const EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE = 57;
|
|
4971
|
+
const checkExpoRouterReactNavigation = (context) => {
|
|
4972
|
+
const { expoSdkMajor } = context;
|
|
4973
|
+
if (!isExpoSdkAtLeast(expoSdkMajor, EXPO_ROUTER_REACT_NAVIGATION_MIN_SDK_MAJOR)) return [];
|
|
4974
|
+
if (expoSdkMajor !== null && expoSdkMajor >= EXPO_ROUTER_REACT_NAVIGATION_MAX_SDK_MAJOR_EXCLUSIVE) return [];
|
|
4975
|
+
if (!context.directDependencyNames.has("expo-router")) return [];
|
|
4976
|
+
const reactNavigationNames = [...context.directDependencyNames].filter((packageName) => packageName.startsWith("@react-navigation/")).sort();
|
|
4977
|
+
if (reactNavigationNames.length === 0) return [];
|
|
4978
|
+
return [buildExpoDiagnostic({
|
|
4979
|
+
rule: "expo-router-no-react-navigation",
|
|
4980
|
+
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"}`,
|
|
4981
|
+
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/"
|
|
4982
|
+
})];
|
|
4983
|
+
};
|
|
4984
|
+
const VECTOR_ICONS_MIN_SDK_MAJOR = 56;
|
|
4985
|
+
const SCOPED_VECTOR_ICONS_NAMESPACE = "@react-native-vector-icons/";
|
|
4986
|
+
const CONFLICTING_VECTOR_ICONS_PACKAGES = ["@expo/vector-icons", "react-native-vector-icons"];
|
|
4987
|
+
const checkExpoVectorIcons = (context) => {
|
|
4988
|
+
if (!isExpoSdkAtLeast(context.expoSdkMajor, VECTOR_ICONS_MIN_SDK_MAJOR)) return [];
|
|
4989
|
+
const hasScopedPackage = [...context.directDependencyNames].some((packageName) => packageName.startsWith(SCOPED_VECTOR_ICONS_NAMESPACE));
|
|
4990
|
+
const hasConflictingPackage = CONFLICTING_VECTOR_ICONS_PACKAGES.some((packageName) => context.directDependencyNames.has(packageName));
|
|
4991
|
+
if (!hasScopedPackage || !hasConflictingPackage) return [];
|
|
4992
|
+
return [buildExpoDiagnostic({
|
|
4993
|
+
rule: "expo-vector-icons-conflict",
|
|
4994
|
+
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",
|
|
4995
|
+
help: "Migrate to the scoped packages by running `npx @react-native-vector-icons/codemod`, then remove the conflicting package"
|
|
4996
|
+
})];
|
|
4997
|
+
};
|
|
4998
|
+
const checkExpoProject = (rootDirectory, project) => {
|
|
4999
|
+
if (project.expoVersion === null) return [];
|
|
5000
|
+
const context = buildExpoCheckContext(rootDirectory, project.expoVersion);
|
|
5001
|
+
return [
|
|
5002
|
+
...checkExpoFlaggedDependencies(context),
|
|
5003
|
+
...checkExpoDependencyOverrides(context),
|
|
5004
|
+
...checkExpoRouterReactNavigation(context),
|
|
5005
|
+
...checkExpoVectorIcons(context),
|
|
5006
|
+
...checkExpoPackageJsonConflicts(context),
|
|
5007
|
+
...checkExpoLockfile(context),
|
|
5008
|
+
...checkExpoGitignore(context),
|
|
5009
|
+
...checkExpoEnvLocalFiles(context),
|
|
5010
|
+
...checkExpoMetroConfig(context)
|
|
5011
|
+
];
|
|
5012
|
+
};
|
|
4551
5013
|
const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
4552
5014
|
const PNPM_LOCKFILE = "pnpm-lock.yaml";
|
|
4553
5015
|
const PACKAGE_JSON_FILE = "package.json";
|
|
@@ -5230,8 +5692,8 @@ var Config = class Config extends Context.Service()("react-doctor/Config") {
|
|
|
5230
5692
|
const cache = yield* Cache.make({
|
|
5231
5693
|
capacity: 16,
|
|
5232
5694
|
timeToLive: CONFIG_CACHE_TTL_MS,
|
|
5233
|
-
lookup: (directory) => Effect.
|
|
5234
|
-
const loaded = loadConfigWithSource(directory);
|
|
5695
|
+
lookup: (directory) => Effect.promise(async () => {
|
|
5696
|
+
const loaded = await loadConfigWithSource(directory);
|
|
5235
5697
|
const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
|
|
5236
5698
|
return {
|
|
5237
5699
|
config: loaded?.config ?? null,
|
|
@@ -5828,6 +6290,7 @@ const buildCapabilities = (project) => {
|
|
|
5828
6290
|
const capabilities = /* @__PURE__ */ new Set();
|
|
5829
6291
|
capabilities.add(project.framework);
|
|
5830
6292
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
6293
|
+
if (project.expoVersion !== null) capabilities.add("expo");
|
|
5831
6294
|
const reactMajor = project.reactMajorVersion;
|
|
5832
6295
|
if (reactMajor !== null) {
|
|
5833
6296
|
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
@@ -6105,6 +6568,44 @@ const dedupeDiagnostics = (diagnostics) => {
|
|
|
6105
6568
|
}
|
|
6106
6569
|
return uniqueDiagnostics;
|
|
6107
6570
|
};
|
|
6571
|
+
/**
|
|
6572
|
+
* Runs `task` over `items` with at most `concurrency` tasks in flight at
|
|
6573
|
+
* once, returning results in input order. A pool of workers each pulls the
|
|
6574
|
+
* next not-yet-started index until the list drains — so a worker that
|
|
6575
|
+
* finishes a fast task immediately picks up the next one (greedy load
|
|
6576
|
+
* balancing), which matters when tasks have uneven durations (oxlint
|
|
6577
|
+
* batches do).
|
|
6578
|
+
*
|
|
6579
|
+
* Failure semantics mirror a bounded `Promise.all`: on the first rejection
|
|
6580
|
+
* no further tasks are started, the already-in-flight tasks are awaited to
|
|
6581
|
+
* settle (so no subprocess is orphaned mid-write), and the returned promise
|
|
6582
|
+
* rejects with that first error. This keeps the caller's fail-fast retry
|
|
6583
|
+
* path (e.g. oxlint's retry-without-extends) from spawning a second wave on
|
|
6584
|
+
* top of a still-running first one.
|
|
6585
|
+
*/
|
|
6586
|
+
const mapWithConcurrency = async (items, concurrency, task) => {
|
|
6587
|
+
const results = new Array(items.length);
|
|
6588
|
+
if (items.length === 0) return results;
|
|
6589
|
+
const workerCount = Math.min(Math.max(1, Math.floor(concurrency) || 1), items.length);
|
|
6590
|
+
let nextIndex = 0;
|
|
6591
|
+
const errors = [];
|
|
6592
|
+
const runWorker = async () => {
|
|
6593
|
+
while (errors.length === 0) {
|
|
6594
|
+
const index = nextIndex;
|
|
6595
|
+
nextIndex += 1;
|
|
6596
|
+
if (index >= items.length) return;
|
|
6597
|
+
try {
|
|
6598
|
+
results[index] = await task(items[index], index);
|
|
6599
|
+
} catch (error) {
|
|
6600
|
+
errors.push(error);
|
|
6601
|
+
return;
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
};
|
|
6605
|
+
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
|
6606
|
+
if (errors.length > 0) throw errors[0];
|
|
6607
|
+
return results;
|
|
6608
|
+
};
|
|
6108
6609
|
const getPublicEnvPrefix = (framework) => {
|
|
6109
6610
|
switch (framework) {
|
|
6110
6611
|
case "nextjs": return "NEXT_PUBLIC_*";
|
|
@@ -6787,6 +7288,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
6787
7288
|
*/
|
|
6788
7289
|
const spawnLintBatches = async (input) => {
|
|
6789
7290
|
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
7291
|
+
const concurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
6790
7292
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
6791
7293
|
const allDiagnostics = [];
|
|
6792
7294
|
const droppedFiles = [];
|
|
@@ -6806,23 +7308,31 @@ const spawnLintBatches = async (input) => {
|
|
|
6806
7308
|
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
6807
7309
|
}
|
|
6808
7310
|
};
|
|
7311
|
+
let startedFileCount = 0;
|
|
6809
7312
|
let scannedFileCount = 0;
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
7313
|
+
let displayedFileCount = 0;
|
|
7314
|
+
const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
|
|
7315
|
+
const ceiling = Math.min(startedFileCount, totalFileCount - 1);
|
|
7316
|
+
if (displayedFileCount < ceiling) {
|
|
7317
|
+
displayedFileCount += 1;
|
|
7318
|
+
onFileProgress(displayedFileCount, totalFileCount);
|
|
7319
|
+
}
|
|
7320
|
+
}, 50) : null;
|
|
7321
|
+
progressTimer?.unref?.();
|
|
7322
|
+
try {
|
|
7323
|
+
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
7324
|
+
startedFileCount += batch.length;
|
|
6819
7325
|
const batchDiagnostics = await spawnLintBatch(batch);
|
|
6820
|
-
allDiagnostics.push(...batchDiagnostics);
|
|
6821
7326
|
scannedFileCount += batch.length;
|
|
6822
|
-
onFileProgress
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
7327
|
+
if (onFileProgress) {
|
|
7328
|
+
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
7329
|
+
onFileProgress(displayedFileCount, totalFileCount);
|
|
7330
|
+
}
|
|
7331
|
+
return batchDiagnostics;
|
|
7332
|
+
});
|
|
7333
|
+
for (const batchDiagnostics of batchResults) allDiagnostics.push(...batchDiagnostics);
|
|
7334
|
+
} finally {
|
|
7335
|
+
if (progressTimer !== null) clearInterval(progressTimer);
|
|
6826
7336
|
}
|
|
6827
7337
|
if (droppedFiles.length > 0 && onPartialFailure) {
|
|
6828
7338
|
const previewFiles = droppedFiles.slice(0, 3).join(", ");
|
|
@@ -6949,7 +7459,8 @@ const runOxlint = async (options) => {
|
|
|
6949
7459
|
onPartialFailure,
|
|
6950
7460
|
onFileProgress: options.onFileProgress,
|
|
6951
7461
|
spawnTimeoutMs,
|
|
6952
|
-
outputMaxBytes
|
|
7462
|
+
outputMaxBytes,
|
|
7463
|
+
concurrency: options.concurrency
|
|
6953
7464
|
});
|
|
6954
7465
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6955
7466
|
try {
|
|
@@ -7017,6 +7528,7 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
7017
7528
|
const partialFailures = yield* LintPartialFailures;
|
|
7018
7529
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
7019
7530
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
7531
|
+
const concurrency = yield* OxlintConcurrency;
|
|
7020
7532
|
const collectedFailures = [];
|
|
7021
7533
|
const diagnostics = yield* Effect.tryPromise({
|
|
7022
7534
|
try: () => runOxlint({
|
|
@@ -7035,7 +7547,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
7035
7547
|
},
|
|
7036
7548
|
onFileProgress: input.onFileProgress,
|
|
7037
7549
|
spawnTimeoutMs,
|
|
7038
|
-
outputMaxBytes
|
|
7550
|
+
outputMaxBytes,
|
|
7551
|
+
concurrency
|
|
7039
7552
|
}),
|
|
7040
7553
|
catch: ensureReactDoctorError
|
|
7041
7554
|
});
|
|
@@ -7359,7 +7872,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7359
7872
|
const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
|
|
7360
7873
|
yield* beforeLint(project, lintIncludePaths ?? void 0);
|
|
7361
7874
|
const isDiffMode = input.includePaths.length > 0;
|
|
7362
|
-
const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ??
|
|
7875
|
+
const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
|
|
7363
7876
|
const transform = buildDiagnosticPipeline({
|
|
7364
7877
|
rootDirectory: scanDirectory,
|
|
7365
7878
|
userConfig: resolvedConfig.config,
|
|
@@ -7368,7 +7881,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7368
7881
|
showWarnings
|
|
7369
7882
|
});
|
|
7370
7883
|
const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
|
|
7371
|
-
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7884
|
+
const environmentDiagnostics = isDiffMode ? [] : [
|
|
7885
|
+
...checkReducedMotion(scanDirectory),
|
|
7886
|
+
...checkPnpmHardening(scanDirectory),
|
|
7887
|
+
...checkExpoProject(scanDirectory, project)
|
|
7888
|
+
];
|
|
7372
7889
|
const envCollected = yield* Stream.runCollect(applyPerElementPipeline(Stream.fromIterable(environmentDiagnostics)));
|
|
7373
7890
|
const lintFailure = yield* Ref.make({
|
|
7374
7891
|
didFail: false,
|
|
@@ -7380,6 +7897,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7380
7897
|
didFail: false,
|
|
7381
7898
|
reason: null
|
|
7382
7899
|
});
|
|
7900
|
+
const scanConcurrency = yield* OxlintConcurrency;
|
|
7901
|
+
const workerCountSuffix = scanConcurrency > 1 ? ` · ${scanConcurrency} workers` : "";
|
|
7383
7902
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
7384
7903
|
const scanStartTime = Date.now();
|
|
7385
7904
|
let lastReportedTotalFileCount = 0;
|
|
@@ -7396,7 +7915,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7396
7915
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
7397
7916
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
7398
7917
|
lastReportedTotalFileCount = totalFileCount;
|
|
7399
|
-
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
7918
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
7400
7919
|
}
|
|
7401
7920
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
7402
7921
|
yield* Ref.set(lintFailure, {
|
|
@@ -7428,7 +7947,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7428
7947
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
7429
7948
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
7430
7949
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
7431
|
-
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
|
|
7950
|
+
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
7432
7951
|
yield* reporterService.finalize;
|
|
7433
7952
|
const finalDiagnostics = [
|
|
7434
7953
|
...envCollected,
|
|
@@ -7480,7 +7999,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7480
7999
|
"inspect.isCi": input.isCi,
|
|
7481
8000
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
7482
8001
|
} }));
|
|
7483
|
-
Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
|
|
7484
8002
|
const parseNodeVersion = (versionString) => {
|
|
7485
8003
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
7486
8004
|
return {
|
|
@@ -7779,6 +8297,26 @@ const buildJsonReport = (input) => {
|
|
|
7779
8297
|
};
|
|
7780
8298
|
};
|
|
7781
8299
|
/**
|
|
8300
|
+
* Single source of truth for the skipped-check accounting shared by the
|
|
8301
|
+
* CLI renderer (`react-doctor/src/inspect.ts → finalizeAndRender`) and the
|
|
8302
|
+
* programmatic shell (`@react-doctor/api → diagnose()`). Both surface a
|
|
8303
|
+
* failed lint / dead-code pass instead of a false "all clear", so the
|
|
8304
|
+
* branch logic lives here once.
|
|
8305
|
+
*/
|
|
8306
|
+
const buildSkippedChecks = (input) => {
|
|
8307
|
+
const skippedChecks = [];
|
|
8308
|
+
if (input.didLintFail) skippedChecks.push("lint");
|
|
8309
|
+
if (input.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
8310
|
+
const skippedCheckReasons = {};
|
|
8311
|
+
if (input.didLintFail && input.lintFailureReason !== null) skippedCheckReasons.lint = input.lintFailureReason;
|
|
8312
|
+
else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
|
|
8313
|
+
if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
|
|
8314
|
+
return {
|
|
8315
|
+
skippedChecks,
|
|
8316
|
+
skippedCheckReasons
|
|
8317
|
+
};
|
|
8318
|
+
};
|
|
8319
|
+
/**
|
|
7782
8320
|
* Programmatic façade over `Git.diffSelection`. Async because the
|
|
7783
8321
|
* Git service runs through Effect's `ChildProcess` (true subprocess
|
|
7784
8322
|
* spawn, not `spawnSync`).
|
|
@@ -7884,7 +8422,7 @@ import_picocolors.default.red, import_picocolors.default.yellow, import_picocolo
|
|
|
7884
8422
|
const clearAutoSuppressionCaches = () => {};
|
|
7885
8423
|
//#endregion
|
|
7886
8424
|
//#region ../api/dist/index.js
|
|
7887
|
-
const
|
|
8425
|
+
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);
|
|
7888
8426
|
const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
7889
8427
|
const effectiveConfig = configOverride ?? scanTarget.userConfig;
|
|
7890
8428
|
const includePaths = options.includePaths ?? [];
|
|
@@ -7893,7 +8431,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7893
8431
|
includePaths,
|
|
7894
8432
|
customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
|
|
7895
8433
|
respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
|
|
7896
|
-
warnings: options.warnings ?? effectiveConfig?.warnings ??
|
|
8434
|
+
warnings: options.warnings ?? effectiveConfig?.warnings ?? true,
|
|
7897
8435
|
adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
|
|
7898
8436
|
ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
|
|
7899
8437
|
runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
|
|
@@ -7903,13 +8441,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7903
8441
|
};
|
|
7904
8442
|
const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
7905
8443
|
if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
|
|
7906
|
-
const skippedChecks =
|
|
7907
|
-
if (output.didLintFail) skippedChecks.push("lint");
|
|
7908
|
-
if (output.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
7909
|
-
const skippedCheckReasons = {};
|
|
7910
|
-
if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
|
|
7911
|
-
else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
|
|
7912
|
-
if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
|
|
8444
|
+
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks(output);
|
|
7913
8445
|
return {
|
|
7914
8446
|
diagnostics: [...output.diagnostics],
|
|
7915
8447
|
score: output.score,
|
|
@@ -7921,8 +8453,8 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
|
7921
8453
|
};
|
|
7922
8454
|
const diagnose = async (directory, options = {}) => {
|
|
7923
8455
|
const startTime = globalThis.performance.now();
|
|
7924
|
-
const program = buildInspectProgram(resolveScanTarget(directory), options);
|
|
7925
|
-
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(
|
|
8456
|
+
const program = buildInspectProgram(await resolveScanTarget(directory), options);
|
|
8457
|
+
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(buildDiagnoseLayer()), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
|
|
7926
8458
|
};
|
|
7927
8459
|
//#endregion
|
|
7928
8460
|
//#region src/index.ts
|
|
@@ -7955,4 +8487,5 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
7955
8487
|
//#endregion
|
|
7956
8488
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
7957
8489
|
|
|
7958
|
-
//# sourceMappingURL=index.js.map
|
|
8490
|
+
//# sourceMappingURL=index.js.map
|
|
8491
|
+
//# debugId=fce73b02-d297-5132-af08-817f37e1467c
|