react-doctor 0.6.3 → 0.7.0-dev.6b21b70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1049 -289
- package/dist/index.d.ts +66 -1
- package/dist/index.js +672 -82
- package/dist/lsp.js +671 -86
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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]="
|
|
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]="07deeb70-f47f-5572-9930-e743f970f84f")}catch(e){}}();
|
|
3
3
|
import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as NFS from "node:fs";
|
|
@@ -9,7 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import * as NodeChildProcess from "node:child_process";
|
|
10
10
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import * as ts from "typescript";
|
|
12
|
-
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
12
|
+
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
13
13
|
import * as OS from "node:os";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash } from "node:crypto";
|
|
21
|
+
import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
|
|
21
22
|
import { gzipSync } from "node:zlib";
|
|
22
23
|
//#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
|
|
23
24
|
/**
|
|
@@ -19373,6 +19374,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
19373
19374
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
19374
19375
|
*/
|
|
19375
19376
|
baselineDegraded: optional(Boolean$1),
|
|
19377
|
+
/**
|
|
19378
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
19379
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
19380
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
19381
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
19382
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
19383
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
19384
|
+
* and on reports from older CLI versions.
|
|
19385
|
+
*/
|
|
19386
|
+
reactDetected: optional(Boolean$1),
|
|
19376
19387
|
diff: NullOr(JsonReportDiffInfo),
|
|
19377
19388
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
19378
19389
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -19398,6 +19409,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
19398
19409
|
ok: Boolean$1,
|
|
19399
19410
|
directory: String$1,
|
|
19400
19411
|
mode: JsonReportMode,
|
|
19412
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
19413
|
+
reactDetected: optional(Boolean$1),
|
|
19401
19414
|
diff: NullOr(JsonReportDiffInfo),
|
|
19402
19415
|
baseline: JsonReportBaseline,
|
|
19403
19416
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -32927,10 +32940,15 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
32927
32940
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
32928
32941
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32929
32942
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32943
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
32944
|
+
const CORE_PACKAGE_VERSION = "0.7.0";
|
|
32945
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32946
|
+
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32930
32947
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
32931
32948
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
32932
32949
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
32933
32950
|
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
32951
|
+
const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
|
|
32934
32952
|
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
32935
32953
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
32936
32954
|
const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
@@ -35613,6 +35631,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
35613
35631
|
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35614
35632
|
return true;
|
|
35615
35633
|
} }) {};
|
|
35634
|
+
/**
|
|
35635
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
35636
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
35637
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
35638
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
35639
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
35640
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
35641
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
35642
|
+
*
|
|
35643
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35644
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
35645
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
35646
|
+
* (the pre-cache behavior).
|
|
35647
|
+
*
|
|
35648
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
35649
|
+
*/
|
|
35650
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
35651
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35652
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
35653
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35654
|
+
if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
|
|
35655
|
+
return true;
|
|
35656
|
+
} }) {};
|
|
35657
|
+
/**
|
|
35658
|
+
* Whether the whole-project dead-code result cache
|
|
35659
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
35660
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
35661
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
35662
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
35663
|
+
*
|
|
35664
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35665
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
35666
|
+
*
|
|
35667
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
35668
|
+
*/
|
|
35669
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
35670
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35671
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
35672
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35673
|
+
if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
|
|
35674
|
+
return true;
|
|
35675
|
+
} }) {};
|
|
35616
35676
|
const DIAGNOSTIC_SURFACES = [
|
|
35617
35677
|
"cli",
|
|
35618
35678
|
"prComment",
|
|
@@ -36962,10 +37022,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
36962
37022
|
}
|
|
36963
37023
|
}
|
|
36964
37024
|
}
|
|
37025
|
+
/**
|
|
37026
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
37027
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
37028
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
37029
|
+
* React-runtime rule family was gated off for the project.
|
|
37030
|
+
*/
|
|
37031
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
36965
37032
|
const buildCapabilities = (project) => {
|
|
36966
37033
|
const capabilities = /* @__PURE__ */ new Set();
|
|
36967
37034
|
capabilities.add(project.framework);
|
|
36968
|
-
if (project
|
|
37035
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
36969
37036
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
36970
37037
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
36971
37038
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -37301,6 +37368,146 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
37301
37368
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
37302
37369
|
};
|
|
37303
37370
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
37371
|
+
const atomicWriteJson = (filePath, value) => {
|
|
37372
|
+
try {
|
|
37373
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
37374
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
37375
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
37376
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
37377
|
+
} catch {
|
|
37378
|
+
return;
|
|
37379
|
+
}
|
|
37380
|
+
};
|
|
37381
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
37382
|
+
try {
|
|
37383
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
37384
|
+
} catch {
|
|
37385
|
+
return fallback;
|
|
37386
|
+
}
|
|
37387
|
+
};
|
|
37388
|
+
const hashFileContents = (filePath) => {
|
|
37389
|
+
try {
|
|
37390
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
37391
|
+
} catch {
|
|
37392
|
+
return null;
|
|
37393
|
+
}
|
|
37394
|
+
};
|
|
37395
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
37396
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
37397
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
37398
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
37399
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
37400
|
+
/**
|
|
37401
|
+
* Stat snapshot of every file the analysis reads, keyed by root-relative
|
|
37402
|
+
* `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
|
|
37403
|
+
* verified against the tree it started from.
|
|
37404
|
+
*/
|
|
37405
|
+
const collectAnalyzedFileStats = (rootDirectory) => {
|
|
37406
|
+
const statByRelativePath = /* @__PURE__ */ new Map();
|
|
37407
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
37408
|
+
if (!isFingerprintedFile(name)) continue;
|
|
37409
|
+
try {
|
|
37410
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
37411
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
37412
|
+
statByRelativePath.set(relativePath, {
|
|
37413
|
+
mtimeMs: fileStat.mtimeMs,
|
|
37414
|
+
size: fileStat.size
|
|
37415
|
+
});
|
|
37416
|
+
} catch {}
|
|
37417
|
+
}
|
|
37418
|
+
return statByRelativePath;
|
|
37419
|
+
};
|
|
37420
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
37421
|
+
const resolveDeslopVersion = () => {
|
|
37422
|
+
try {
|
|
37423
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
37424
|
+
return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
37425
|
+
} catch {
|
|
37426
|
+
return "unknown";
|
|
37427
|
+
}
|
|
37428
|
+
};
|
|
37429
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
37430
|
+
schemaVersion: 2,
|
|
37431
|
+
coreVersion: input.coreVersion,
|
|
37432
|
+
deslopVersion: resolveDeslopVersion(),
|
|
37433
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
37434
|
+
entryPatterns: input.entryPatterns,
|
|
37435
|
+
ignorePatterns: input.ignorePatterns,
|
|
37436
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
|
|
37437
|
+
})).digest("hex");
|
|
37438
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
37439
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
37440
|
+
try {
|
|
37441
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
37442
|
+
return raw;
|
|
37443
|
+
} catch {
|
|
37444
|
+
return null;
|
|
37445
|
+
}
|
|
37446
|
+
};
|
|
37447
|
+
const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
|
|
37448
|
+
const lookupDeadCodeResultCache = (input) => {
|
|
37449
|
+
const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
|
|
37450
|
+
const persisted = failOpenReadJson(cacheFilePath, null);
|
|
37451
|
+
if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
|
|
37452
|
+
const storedFileEntries = Object.entries(persisted.files);
|
|
37453
|
+
if (storedFileEntries.length !== input.currentFileStats.size) return null;
|
|
37454
|
+
const repairedFiles = {};
|
|
37455
|
+
let repairedCount = 0;
|
|
37456
|
+
for (const [relativePath, storedIdentity] of storedFileEntries) {
|
|
37457
|
+
if (!isPersistedFileIdentity(storedIdentity)) return null;
|
|
37458
|
+
const currentStat = input.currentFileStats.get(relativePath);
|
|
37459
|
+
if (currentStat === void 0) return null;
|
|
37460
|
+
const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
|
|
37461
|
+
if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
|
|
37462
|
+
repairedFiles[relativePath] = storedIdentity;
|
|
37463
|
+
continue;
|
|
37464
|
+
}
|
|
37465
|
+
if (currentStat.size !== storedSize) return null;
|
|
37466
|
+
const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
|
|
37467
|
+
if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
|
|
37468
|
+
repairedFiles[relativePath] = [
|
|
37469
|
+
currentStat.mtimeMs,
|
|
37470
|
+
currentStat.size,
|
|
37471
|
+
storedContentHash
|
|
37472
|
+
];
|
|
37473
|
+
repairedCount += 1;
|
|
37474
|
+
}
|
|
37475
|
+
const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
|
|
37476
|
+
if (diagnostics === null) return null;
|
|
37477
|
+
if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
|
|
37478
|
+
version: 2,
|
|
37479
|
+
key: input.cacheKey,
|
|
37480
|
+
files: repairedFiles,
|
|
37481
|
+
diagnostics: persisted.diagnostics
|
|
37482
|
+
});
|
|
37483
|
+
return diagnostics;
|
|
37484
|
+
};
|
|
37485
|
+
const storeDeadCodeResultCache = (input) => {
|
|
37486
|
+
const persistedFiles = {};
|
|
37487
|
+
for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
|
|
37488
|
+
const absolutePath = Path.join(input.rootDirectory, relativePath);
|
|
37489
|
+
const contentHash = hashFileContents(absolutePath);
|
|
37490
|
+
if (contentHash === null) return;
|
|
37491
|
+
let currentStat;
|
|
37492
|
+
try {
|
|
37493
|
+
currentStat = NFS.statSync(absolutePath);
|
|
37494
|
+
} catch {
|
|
37495
|
+
return;
|
|
37496
|
+
}
|
|
37497
|
+
if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
|
|
37498
|
+
persistedFiles[relativePath] = [
|
|
37499
|
+
snapshotStat.mtimeMs,
|
|
37500
|
+
snapshotStat.size,
|
|
37501
|
+
contentHash
|
|
37502
|
+
];
|
|
37503
|
+
}
|
|
37504
|
+
atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
37505
|
+
version: 2,
|
|
37506
|
+
key: input.cacheKey,
|
|
37507
|
+
files: persistedFiles,
|
|
37508
|
+
diagnostics: input.diagnostics
|
|
37509
|
+
});
|
|
37510
|
+
};
|
|
37304
37511
|
const readSystemFacts = () => ({
|
|
37305
37512
|
availableCores: os.availableParallelism(),
|
|
37306
37513
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -37369,6 +37576,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
37369
37576
|
releaseSlot();
|
|
37370
37577
|
}
|
|
37371
37578
|
};
|
|
37579
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
37580
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
37581
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
37582
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
37583
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
37584
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
37585
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
37586
|
+
};
|
|
37587
|
+
const resolveUserCacheScope = () => {
|
|
37588
|
+
try {
|
|
37589
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
37590
|
+
} catch {
|
|
37591
|
+
return "shared";
|
|
37592
|
+
}
|
|
37593
|
+
};
|
|
37372
37594
|
/**
|
|
37373
37595
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
37374
37596
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -37422,6 +37644,14 @@ process.stdin.on("end", () => {
|
|
|
37422
37644
|
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
37423
37645
|
files: cycle.files,
|
|
37424
37646
|
})),
|
|
37647
|
+
...(result.incrementalCacheStats
|
|
37648
|
+
? {
|
|
37649
|
+
summaryCacheStats: {
|
|
37650
|
+
hits: result.incrementalCacheStats.summaryHits,
|
|
37651
|
+
misses: result.incrementalCacheStats.summaryMisses,
|
|
37652
|
+
},
|
|
37653
|
+
}
|
|
37654
|
+
: {}),
|
|
37425
37655
|
});
|
|
37426
37656
|
|
|
37427
37657
|
const serializeError = (error) =>
|
|
@@ -37445,6 +37675,9 @@ process.stdin.on("end", () => {
|
|
|
37445
37675
|
...(workerInput.ignorePatterns.length > 0
|
|
37446
37676
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
37447
37677
|
: {}),
|
|
37678
|
+
...(workerInput.incrementalCachePath
|
|
37679
|
+
? { incrementalCachePath: workerInput.incrementalCachePath }
|
|
37680
|
+
: {}),
|
|
37448
37681
|
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37449
37682
|
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37450
37683
|
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
@@ -37455,12 +37688,18 @@ process.stdin.on("end", () => {
|
|
|
37455
37688
|
// are the single most expensive pass — duplicate-block detection alone was
|
|
37456
37689
|
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37457
37690
|
// speedup on a large repo.
|
|
37458
|
-
//
|
|
37691
|
+
// - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
|
|
37692
|
+
// simplifiable functions, identity wrappers, …) — ~120 ms of discarded
|
|
37693
|
+
// output per scan, and skipping them lets the incremental cache drop the
|
|
37694
|
+
// DRY-pattern summary fields (the largest slice of the cache file).
|
|
37695
|
+
// All are provably safe: the consumed graph findings are computed by their own
|
|
37459
37696
|
// detectors, independent of these passes (confirmed byte-identical on
|
|
37460
|
-
// excalidraw + mui-material + sentry
|
|
37461
|
-
//
|
|
37697
|
+
// excalidraw + mui-material + sentry; re-verified on sentry after the
|
|
37698
|
+
// reportRedundancy flip). tsConfigPath stays — the module resolver needs it
|
|
37699
|
+
// for path-alias resolution in the import graph.
|
|
37462
37700
|
semantic: { enabled: false },
|
|
37463
37701
|
reportCodeQuality: false,
|
|
37702
|
+
reportRedundancy: false,
|
|
37464
37703
|
};
|
|
37465
37704
|
const result = await analyze(defineConfig(config));
|
|
37466
37705
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -37544,13 +37783,23 @@ const parseCircularDependencies = (value) => {
|
|
|
37544
37783
|
}
|
|
37545
37784
|
return circularDependencies;
|
|
37546
37785
|
};
|
|
37786
|
+
const parseSummaryCacheStats = (value) => {
|
|
37787
|
+
if (!isRecord(value)) return void 0;
|
|
37788
|
+
if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
|
|
37789
|
+
return {
|
|
37790
|
+
hits: value.hits,
|
|
37791
|
+
misses: value.misses
|
|
37792
|
+
};
|
|
37793
|
+
};
|
|
37547
37794
|
const parseDeadCodeWorkerResult = (value) => {
|
|
37548
37795
|
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
37796
|
+
const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
|
|
37549
37797
|
return {
|
|
37550
37798
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
37551
37799
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
37552
37800
|
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
37553
|
-
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
37801
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies),
|
|
37802
|
+
...summaryCacheStats ? { summaryCacheStats } : {}
|
|
37554
37803
|
};
|
|
37555
37804
|
};
|
|
37556
37805
|
const parseDeadCodeWorkerError = (value) => {
|
|
@@ -37664,17 +37913,41 @@ const checkDeadCode = async (options) => {
|
|
|
37664
37913
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37665
37914
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37666
37915
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37916
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
37917
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
37918
|
+
const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
|
|
37919
|
+
const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
|
|
37920
|
+
rootDirectory,
|
|
37921
|
+
entryPatterns,
|
|
37922
|
+
ignorePatterns,
|
|
37923
|
+
tsConfigPath,
|
|
37924
|
+
deslopJsModuleSpecifier,
|
|
37925
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
37926
|
+
});
|
|
37927
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) {
|
|
37928
|
+
const cachedDiagnostics = lookupDeadCodeResultCache({
|
|
37929
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
37930
|
+
cacheKey,
|
|
37931
|
+
rootDirectory,
|
|
37932
|
+
currentFileStats: fileStatsSnapshot
|
|
37933
|
+
});
|
|
37934
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
37935
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
37936
|
+
}
|
|
37937
|
+
const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
|
|
37667
37938
|
const spawnAndRun = () => {
|
|
37668
37939
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37669
37940
|
rootDirectory,
|
|
37670
37941
|
entryPatterns,
|
|
37671
|
-
tsConfigPath
|
|
37942
|
+
tsConfigPath,
|
|
37672
37943
|
ignorePatterns,
|
|
37673
|
-
deslopJsModuleSpecifier
|
|
37674
|
-
parseConcurrency: options.parseConcurrency
|
|
37944
|
+
deslopJsModuleSpecifier,
|
|
37945
|
+
parseConcurrency: options.parseConcurrency,
|
|
37946
|
+
incrementalCachePath
|
|
37675
37947
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
37676
37948
|
};
|
|
37677
37949
|
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
37950
|
+
if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
|
|
37678
37951
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37679
37952
|
const diagnostics = [];
|
|
37680
37953
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37731,6 +38004,13 @@ const checkDeadCode = async (options) => {
|
|
|
37731
38004
|
category: DEAD_CODE_CATEGORY
|
|
37732
38005
|
});
|
|
37733
38006
|
}
|
|
38007
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
|
|
38008
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
38009
|
+
cacheKey,
|
|
38010
|
+
rootDirectory,
|
|
38011
|
+
snapshotFileStats: fileStatsSnapshot,
|
|
38012
|
+
diagnostics
|
|
38013
|
+
});
|
|
37734
38014
|
return diagnostics;
|
|
37735
38015
|
};
|
|
37736
38016
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -37833,12 +38113,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37833
38113
|
*/
|
|
37834
38114
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37835
38115
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
38116
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
37836
38117
|
return yield* tryPromise({
|
|
37837
38118
|
try: (signal) => checkDeadCode({
|
|
37838
38119
|
rootDirectory: input.rootDirectory,
|
|
37839
38120
|
parseConcurrency: input.parseConcurrency,
|
|
37840
38121
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
37841
|
-
abortSignal: signal
|
|
38122
|
+
abortSignal: signal,
|
|
38123
|
+
cacheEnabled,
|
|
38124
|
+
onCacheOutcome: input.onCacheOutcome,
|
|
38125
|
+
onSummaryCacheStats: input.onSummaryCacheStats
|
|
37842
38126
|
}),
|
|
37843
38127
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37844
38128
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38708,7 +38992,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38708
38992
|
}
|
|
38709
38993
|
return enabled;
|
|
38710
38994
|
};
|
|
38711
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38995
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
38712
38996
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38713
38997
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38714
38998
|
const jsPlugins = [];
|
|
@@ -38720,6 +39004,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38720
39004
|
if (!rule) continue;
|
|
38721
39005
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38722
39006
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
39007
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
38723
39008
|
if (rule.scan !== void 0) continue;
|
|
38724
39009
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38725
39010
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38764,33 +39049,145 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38764
39049
|
}
|
|
38765
39050
|
};
|
|
38766
39051
|
};
|
|
38767
|
-
const
|
|
39052
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
39053
|
+
const decodeFileDiagnostics = (raw) => {
|
|
39054
|
+
if (!Array.isArray(raw)) return null;
|
|
38768
39055
|
try {
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38772
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
39056
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
39057
|
+
return raw;
|
|
38773
39058
|
} catch {
|
|
38774
|
-
return;
|
|
39059
|
+
return null;
|
|
38775
39060
|
}
|
|
38776
39061
|
};
|
|
38777
|
-
const
|
|
38778
|
-
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
39062
|
+
const emptyCache$1 = () => ({
|
|
39063
|
+
version: 1,
|
|
39064
|
+
rulesets: {}
|
|
39065
|
+
});
|
|
39066
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
39067
|
+
const entries = /* @__PURE__ */ new Map();
|
|
39068
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
39069
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
39070
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
39071
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
39072
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
39073
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
39074
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38782
39075
|
}
|
|
39076
|
+
return entries;
|
|
39077
|
+
};
|
|
39078
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
39079
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
39080
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
39081
|
+
return {
|
|
39082
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
39083
|
+
store: (fileKey, diagnostics) => {
|
|
39084
|
+
entries.delete(fileKey);
|
|
39085
|
+
entries.set(fileKey, diagnostics);
|
|
39086
|
+
},
|
|
39087
|
+
persist: () => {
|
|
39088
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
39089
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
39090
|
+
const existingBucket = rulesets[rulesetHash];
|
|
39091
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
39092
|
+
const ourFiles = {};
|
|
39093
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
39094
|
+
const cappedEntries = Object.entries({
|
|
39095
|
+
...existingFiles,
|
|
39096
|
+
...ourFiles
|
|
39097
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
39098
|
+
rulesets[rulesetHash] = {
|
|
39099
|
+
updatedAtMs: Date.now(),
|
|
39100
|
+
files: Object.fromEntries(cappedEntries)
|
|
39101
|
+
};
|
|
39102
|
+
atomicWriteJson(cacheFilePath, {
|
|
39103
|
+
version: 1,
|
|
39104
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
39105
|
+
});
|
|
39106
|
+
}
|
|
39107
|
+
};
|
|
39108
|
+
};
|
|
39109
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
39110
|
+
/**
|
|
39111
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
39112
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
39113
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
39114
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
39115
|
+
*
|
|
39116
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
39117
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
39118
|
+
* hashes the cache-partition loop already computed
|
|
39119
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
39120
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
39121
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
39122
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
39123
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
39124
|
+
*
|
|
39125
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
39126
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
39127
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
39128
|
+
*/
|
|
39129
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
39130
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
39131
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
39132
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
39133
|
+
const contentAnswer = (relativePath) => {
|
|
39134
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
39135
|
+
if (memoized !== void 0) return memoized;
|
|
39136
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
39137
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
39138
|
+
return answer;
|
|
39139
|
+
};
|
|
39140
|
+
const existsAnswer = (relativePath) => {
|
|
39141
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
39142
|
+
if (memoized !== void 0) return memoized;
|
|
39143
|
+
let answer;
|
|
39144
|
+
try {
|
|
39145
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
39146
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
39147
|
+
} catch {
|
|
39148
|
+
answer = "none";
|
|
39149
|
+
}
|
|
39150
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
39151
|
+
return answer;
|
|
39152
|
+
};
|
|
39153
|
+
return {
|
|
39154
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
39155
|
+
toRelativePath
|
|
39156
|
+
};
|
|
38783
39157
|
};
|
|
38784
39158
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38785
|
-
const
|
|
38786
|
-
|
|
39159
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
39160
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
39161
|
+
if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
39162
|
+
const probes = [];
|
|
39163
|
+
for (const rawProbe of raw.probes) {
|
|
39164
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
39165
|
+
const [kind, probePath, answer] = rawProbe;
|
|
39166
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
39167
|
+
probes.push({
|
|
39168
|
+
kind,
|
|
39169
|
+
path: probePath,
|
|
39170
|
+
answer
|
|
39171
|
+
});
|
|
39172
|
+
}
|
|
38787
39173
|
try {
|
|
38788
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
38789
|
-
return raw;
|
|
39174
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
38790
39175
|
} catch {
|
|
38791
39176
|
return null;
|
|
38792
39177
|
}
|
|
39178
|
+
return {
|
|
39179
|
+
probes,
|
|
39180
|
+
diagnostics: raw.diagnostics
|
|
39181
|
+
};
|
|
38793
39182
|
};
|
|
39183
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
39184
|
+
probes: entry.probes.map((probe) => [
|
|
39185
|
+
probe.kind,
|
|
39186
|
+
probe.path,
|
|
39187
|
+
probe.answer
|
|
39188
|
+
]),
|
|
39189
|
+
diagnostics: entry.diagnostics
|
|
39190
|
+
});
|
|
38794
39191
|
const emptyCache = () => ({
|
|
38795
39192
|
version: 1,
|
|
38796
39193
|
rulesets: {}
|
|
@@ -38801,20 +39198,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
38801
39198
|
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38802
39199
|
const bucket = persisted.rulesets[rulesetHash];
|
|
38803
39200
|
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38804
|
-
for (const [fileKey,
|
|
38805
|
-
const decoded =
|
|
39201
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
39202
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
38806
39203
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38807
39204
|
}
|
|
38808
39205
|
return entries;
|
|
38809
39206
|
};
|
|
38810
|
-
const
|
|
38811
|
-
const cacheFilePath = Path.join(cacheDirectory,
|
|
39207
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
39208
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
38812
39209
|
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38813
39210
|
return {
|
|
38814
39211
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38815
|
-
store: (fileKey,
|
|
39212
|
+
store: (fileKey, entry) => {
|
|
38816
39213
|
entries.delete(fileKey);
|
|
38817
|
-
entries.set(fileKey,
|
|
39214
|
+
entries.set(fileKey, entry);
|
|
38818
39215
|
},
|
|
38819
39216
|
persist: () => {
|
|
38820
39217
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
@@ -38822,7 +39219,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
38822
39219
|
const existingBucket = rulesets[rulesetHash];
|
|
38823
39220
|
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38824
39221
|
const ourFiles = {};
|
|
38825
|
-
for (const [fileKey,
|
|
39222
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
38826
39223
|
const cappedEntries = Object.entries({
|
|
38827
39224
|
...existingFiles,
|
|
38828
39225
|
...ourFiles
|
|
@@ -39837,13 +40234,6 @@ const validateRuleRegistration = () => {
|
|
|
39837
40234
|
].filter((entry) => entry !== null).join("; ");
|
|
39838
40235
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39839
40236
|
};
|
|
39840
|
-
const hashFileContents = (filePath) => {
|
|
39841
|
-
try {
|
|
39842
|
-
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
39843
|
-
} catch {
|
|
39844
|
-
return null;
|
|
39845
|
-
}
|
|
39846
|
-
};
|
|
39847
40237
|
/**
|
|
39848
40238
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
39849
40239
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -39908,21 +40298,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
39908
40298
|
}
|
|
39909
40299
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
39910
40300
|
};
|
|
39911
|
-
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
39912
|
-
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
39913
|
-
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
39914
|
-
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
39915
|
-
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
39916
|
-
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
39917
|
-
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
39918
|
-
};
|
|
39919
|
-
const resolveUserCacheScope = () => {
|
|
39920
|
-
try {
|
|
39921
|
-
return String(process.getuid?.() ?? os.userInfo().username);
|
|
39922
|
-
} catch {
|
|
39923
|
-
return "shared";
|
|
39924
|
-
}
|
|
39925
|
-
};
|
|
39926
40301
|
/**
|
|
39927
40302
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39928
40303
|
* the runner: once for the primary scan, once for the
|
|
@@ -39940,6 +40315,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39940
40315
|
NFS.closeSync(fileHandle);
|
|
39941
40316
|
}
|
|
39942
40317
|
};
|
|
40318
|
+
/**
|
|
40319
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
40320
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
40321
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
40322
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
40323
|
+
* actually had diagnostics.
|
|
40324
|
+
*/
|
|
40325
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
40326
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
40327
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
40328
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
40329
|
+
for (const diagnostic of diagnostics) {
|
|
40330
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
40331
|
+
if (file === void 0) return null;
|
|
40332
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
40333
|
+
fileDiagnostics.push(diagnostic);
|
|
40334
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
40335
|
+
}
|
|
40336
|
+
return diagnosticsByFile;
|
|
40337
|
+
};
|
|
40338
|
+
/**
|
|
40339
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
40340
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
40341
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
40342
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
40343
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
40344
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
40345
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
40346
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
40347
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
40348
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
40349
|
+
*/
|
|
40350
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
40351
|
+
const buildProbes = (file) => {
|
|
40352
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
40353
|
+
let trace;
|
|
40354
|
+
try {
|
|
40355
|
+
trace = collectCrossFileDependencyProbes({
|
|
40356
|
+
absoluteFilePath,
|
|
40357
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
40358
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
40359
|
+
});
|
|
40360
|
+
} catch {
|
|
40361
|
+
return null;
|
|
40362
|
+
}
|
|
40363
|
+
if (trace === null) return null;
|
|
40364
|
+
const probes = [];
|
|
40365
|
+
for (const contentPath of trace.contentPaths) {
|
|
40366
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
40367
|
+
probes.push({
|
|
40368
|
+
kind: "content",
|
|
40369
|
+
path: relativePath,
|
|
40370
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
40371
|
+
});
|
|
40372
|
+
}
|
|
40373
|
+
for (const existencePath of trace.existencePaths) {
|
|
40374
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
40375
|
+
probes.push({
|
|
40376
|
+
kind: "exists",
|
|
40377
|
+
path: relativePath,
|
|
40378
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
40379
|
+
});
|
|
40380
|
+
}
|
|
40381
|
+
return probes;
|
|
40382
|
+
};
|
|
40383
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
40384
|
+
let collectSliceStartedAt = performance.now();
|
|
40385
|
+
for (const file of input.files) {
|
|
40386
|
+
probesByFile.set(file, buildProbes(file));
|
|
40387
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
40388
|
+
await yieldToEventLoop();
|
|
40389
|
+
collectSliceStartedAt = performance.now();
|
|
40390
|
+
}
|
|
40391
|
+
}
|
|
40392
|
+
return probesByFile;
|
|
40393
|
+
};
|
|
40394
|
+
/**
|
|
40395
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
40396
|
+
* the next scan can replay instead of re-linting.
|
|
40397
|
+
*/
|
|
40398
|
+
const storeSidecarEntries = (input) => {
|
|
40399
|
+
let didStoreAnyEntry = false;
|
|
40400
|
+
for (const pass of input.storablePasses) {
|
|
40401
|
+
if (!pass.isTrusted) continue;
|
|
40402
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
40403
|
+
if (diagnosticsByFile === null) continue;
|
|
40404
|
+
for (const file of pass.files) {
|
|
40405
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
40406
|
+
if (cacheKey === void 0) continue;
|
|
40407
|
+
const probes = pass.probesByFile.get(file);
|
|
40408
|
+
if (probes === void 0 || probes === null) continue;
|
|
40409
|
+
input.sidecarCache.store(cacheKey, {
|
|
40410
|
+
probes,
|
|
40411
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
40412
|
+
});
|
|
40413
|
+
didStoreAnyEntry = true;
|
|
40414
|
+
}
|
|
40415
|
+
}
|
|
40416
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
40417
|
+
};
|
|
39943
40418
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39944
40419
|
/**
|
|
39945
40420
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -39981,7 +40456,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39981
40456
|
* 6. always restore disable directives + clean up the temp dir
|
|
39982
40457
|
*/
|
|
39983
40458
|
const runOxlint = async (options) => {
|
|
39984
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
|
|
40459
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
|
|
39985
40460
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39986
40461
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39987
40462
|
validateRuleRegistration();
|
|
@@ -39999,7 +40474,8 @@ const runOxlint = async (options) => {
|
|
|
39999
40474
|
severityControls,
|
|
40000
40475
|
userPlugins,
|
|
40001
40476
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
40002
|
-
ruleSelection: overrides.ruleSelection
|
|
40477
|
+
ruleSelection: overrides.ruleSelection,
|
|
40478
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
40003
40479
|
});
|
|
40004
40480
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
40005
40481
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -40102,15 +40578,18 @@ const runOxlint = async (options) => {
|
|
|
40102
40578
|
});
|
|
40103
40579
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
40104
40580
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
40581
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
40105
40582
|
const missFiles = [];
|
|
40106
40583
|
const replayedDiagnostics = [];
|
|
40107
40584
|
let hashSliceStartedAt = performance.now();
|
|
40108
40585
|
for (const candidateFile of candidateFiles) {
|
|
40109
|
-
const
|
|
40586
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
40587
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
40110
40588
|
if (contentHash === null) {
|
|
40111
40589
|
missFiles.push(candidateFile);
|
|
40112
40590
|
continue;
|
|
40113
40591
|
}
|
|
40592
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
40114
40593
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
40115
40594
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
40116
40595
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -40122,43 +40601,111 @@ const runOxlint = async (options) => {
|
|
|
40122
40601
|
}
|
|
40123
40602
|
}
|
|
40124
40603
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
40604
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
40605
|
+
extendsPaths: [],
|
|
40606
|
+
ruleSelection: "sidecar"
|
|
40607
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
40608
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40609
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
40610
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40611
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
40612
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
40613
|
+
config: buildConfig({
|
|
40614
|
+
extendsPaths: [],
|
|
40615
|
+
ruleSelection: "sidecar",
|
|
40616
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
40617
|
+
}),
|
|
40618
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
40619
|
+
ignorePatterns: combinedPatterns,
|
|
40620
|
+
tsconfigContent
|
|
40621
|
+
})) : null;
|
|
40622
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
40623
|
+
rootDirectory,
|
|
40624
|
+
contentHashByRelativePath
|
|
40625
|
+
});
|
|
40626
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40627
|
+
files: missFiles,
|
|
40628
|
+
rootDirectory,
|
|
40629
|
+
boundedSidecarRuleIds,
|
|
40630
|
+
probeAnswers
|
|
40631
|
+
});
|
|
40125
40632
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
40126
40633
|
extendsPaths: [],
|
|
40127
40634
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
40128
40635
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
40129
40636
|
const missFileSet = new Set(missFiles);
|
|
40130
|
-
const
|
|
40637
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
40638
|
+
const sidecarReplayedDiagnostics = [];
|
|
40639
|
+
const sidecarLintFiles = [];
|
|
40640
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
40641
|
+
else {
|
|
40642
|
+
let verifySliceStartedAt = performance.now();
|
|
40643
|
+
for (const hitFile of hitFiles) {
|
|
40644
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
40645
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
40646
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
40647
|
+
else sidecarLintFiles.push(hitFile);
|
|
40648
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
40649
|
+
await yieldToEventLoop();
|
|
40650
|
+
verifySliceStartedAt = performance.now();
|
|
40651
|
+
}
|
|
40652
|
+
}
|
|
40653
|
+
}
|
|
40654
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40655
|
+
files: sidecarLintFiles,
|
|
40656
|
+
rootDirectory,
|
|
40657
|
+
boundedSidecarRuleIds,
|
|
40658
|
+
probeAnswers
|
|
40659
|
+
});
|
|
40660
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
40661
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
40662
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
40131
40663
|
extendsPaths: [],
|
|
40132
|
-
ruleSelection: "sidecar"
|
|
40133
|
-
|
|
40664
|
+
ruleSelection: "sidecar",
|
|
40665
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
40666
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
40667
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
40668
|
+
extendsPaths: [],
|
|
40669
|
+
ruleSelection: "sidecar",
|
|
40670
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
40671
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
40672
|
+
diagnostics: [],
|
|
40673
|
+
didDropReactHooksJsPlugin: false,
|
|
40674
|
+
hadPartialFailure: false
|
|
40675
|
+
};
|
|
40134
40676
|
const freshCacheableDiagnostics = [];
|
|
40135
40677
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
40136
40678
|
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
40137
|
-
|
|
40138
|
-
|
|
40139
|
-
|
|
40140
|
-
let isAttributionSound = true;
|
|
40141
|
-
for (const diagnostic of freshCacheableDiagnostics) {
|
|
40142
|
-
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
40143
|
-
if (missFile === void 0) {
|
|
40144
|
-
isAttributionSound = false;
|
|
40145
|
-
break;
|
|
40146
|
-
}
|
|
40147
|
-
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
40148
|
-
fileDiagnostics.push(diagnostic);
|
|
40149
|
-
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
40150
|
-
}
|
|
40151
|
-
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
|
|
40679
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
40680
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
40681
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
40152
40682
|
for (const missFile of missFiles) {
|
|
40153
40683
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
40154
40684
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
40155
40685
|
}
|
|
40156
40686
|
cache.persist();
|
|
40157
40687
|
}
|
|
40688
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
40689
|
+
sidecarCache,
|
|
40690
|
+
cacheKeyByFile,
|
|
40691
|
+
storablePasses: [{
|
|
40692
|
+
files: sidecarLintFiles,
|
|
40693
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
40694
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
40695
|
+
probesByFile: await sidecarProbesTask
|
|
40696
|
+
}, {
|
|
40697
|
+
files: missFiles,
|
|
40698
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
40699
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
40700
|
+
probesByFile: await missProbesTask
|
|
40701
|
+
}]
|
|
40702
|
+
});
|
|
40158
40703
|
return dedupeDiagnostics([
|
|
40159
40704
|
...replayedDiagnostics,
|
|
40160
40705
|
...fullResult.diagnostics,
|
|
40161
|
-
...
|
|
40706
|
+
...sidecarReplayedDiagnostics,
|
|
40707
|
+
...boundedSidecarResult.diagnostics,
|
|
40708
|
+
...unboundedSidecarResult.diagnostics
|
|
40162
40709
|
]);
|
|
40163
40710
|
}
|
|
40164
40711
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -40256,6 +40803,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40256
40803
|
const concurrency = yield* OxlintConcurrency;
|
|
40257
40804
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
40258
40805
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
40806
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
40259
40807
|
const collectedFailures = [];
|
|
40260
40808
|
const diagnostics = yield* tryPromise({
|
|
40261
40809
|
try: (signal) => runOxlint({
|
|
@@ -40274,7 +40822,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40274
40822
|
},
|
|
40275
40823
|
onFileProgress: input.onFileProgress,
|
|
40276
40824
|
perFileLintCacheEnabled,
|
|
40825
|
+
sidecarLintCacheEnabled,
|
|
40277
40826
|
onCacheStats: input.onCacheStats,
|
|
40827
|
+
onSidecarStats: input.onSidecarStats,
|
|
40278
40828
|
spawnTimeoutMs,
|
|
40279
40829
|
outputMaxBytes,
|
|
40280
40830
|
concurrency,
|
|
@@ -40696,6 +41246,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
|
|
|
40696
41246
|
}));
|
|
40697
41247
|
} catch {}
|
|
40698
41248
|
};
|
|
41249
|
+
const pruneExpiredSocketCache = (cacheDirectory) => {
|
|
41250
|
+
try {
|
|
41251
|
+
const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
|
|
41252
|
+
const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
|
|
41253
|
+
for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
|
|
41254
|
+
const entryPath = Path.join(supplyChainCacheDirectory, entryName);
|
|
41255
|
+
try {
|
|
41256
|
+
if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
|
|
41257
|
+
} catch {
|
|
41258
|
+
continue;
|
|
41259
|
+
}
|
|
41260
|
+
}
|
|
41261
|
+
} catch {}
|
|
41262
|
+
};
|
|
40699
41263
|
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
40700
41264
|
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
40701
41265
|
if (cacheFile !== null) {
|
|
@@ -40823,6 +41387,7 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
40823
41387
|
const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
40824
41388
|
if (dependencies.length === 0) return [];
|
|
40825
41389
|
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
41390
|
+
if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
|
|
40826
41391
|
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
40827
41392
|
const diagnostics = [];
|
|
40828
41393
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
@@ -41049,7 +41614,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41049
41614
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
41050
41615
|
rootDirectory: scanDirectory,
|
|
41051
41616
|
parseConcurrency: deadCodeParseConcurrency,
|
|
41052
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
41617
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
41618
|
+
onCacheOutcome: (didHitCache) => {
|
|
41619
|
+
deadCodeCacheHit = didHitCache;
|
|
41620
|
+
},
|
|
41621
|
+
onSummaryCacheStats: (stats) => {
|
|
41622
|
+
deadCodeSummaryCacheHits = stats.hits;
|
|
41623
|
+
deadCodeSummaryCacheMisses = stats.misses;
|
|
41624
|
+
}
|
|
41053
41625
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41054
41626
|
yield* set(deadCodeFailure, {
|
|
41055
41627
|
didFail: true,
|
|
@@ -41077,6 +41649,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41077
41649
|
let lastReportedTotalFileCount = 0;
|
|
41078
41650
|
let lintCacheHitFileCount = null;
|
|
41079
41651
|
let lintCacheTotalFileCount = null;
|
|
41652
|
+
let lintSidecarReplayedFileCount = null;
|
|
41653
|
+
let lintSidecarTotalFileCount = null;
|
|
41654
|
+
let deadCodeCacheHit = null;
|
|
41655
|
+
let deadCodeSummaryCacheHits = null;
|
|
41656
|
+
let deadCodeSummaryCacheMisses = null;
|
|
41080
41657
|
const baseLintStream = linterService.run({
|
|
41081
41658
|
rootDirectory: scanDirectory,
|
|
41082
41659
|
project,
|
|
@@ -41096,6 +41673,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41096
41673
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
41097
41674
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
41098
41675
|
},
|
|
41676
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
41677
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
41678
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
41679
|
+
},
|
|
41099
41680
|
deadlineEpochMs: input.deadlineEpochMs
|
|
41100
41681
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41101
41682
|
yield* set(lintFailure, {
|
|
@@ -41206,6 +41787,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41206
41787
|
securityScanFailed,
|
|
41207
41788
|
lintCacheHitFileCount,
|
|
41208
41789
|
lintCacheTotalFileCount,
|
|
41790
|
+
lintSidecarReplayedFileCount,
|
|
41791
|
+
lintSidecarTotalFileCount,
|
|
41792
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
41793
|
+
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
41794
|
+
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
41209
41795
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
41210
41796
|
};
|
|
41211
41797
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -41521,6 +42107,7 @@ const buildJsonReport = (input) => {
|
|
|
41521
42107
|
const worstScoredProject = findWorstScoredProject(projects);
|
|
41522
42108
|
const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
|
|
41523
42109
|
const shared = {
|
|
42110
|
+
...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
|
|
41524
42111
|
version: input.version,
|
|
41525
42112
|
ok: true,
|
|
41526
42113
|
directory: input.directory,
|
|
@@ -41750,6 +42337,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
|
41750
42337
|
skippedChecks,
|
|
41751
42338
|
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
41752
42339
|
project: output.project,
|
|
42340
|
+
reactDetected: hasReactRuntime(output.project),
|
|
41753
42341
|
elapsedMilliseconds
|
|
41754
42342
|
};
|
|
41755
42343
|
};
|
|
@@ -41805,10 +42393,12 @@ const diagnoseProjectBatch = async (input) => {
|
|
|
41805
42393
|
const startTime = globalThis.performance.now();
|
|
41806
42394
|
const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
|
|
41807
42395
|
const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
|
|
42396
|
+
const succeededProjects = projectResults.filter((projectResult) => projectResult.ok);
|
|
41808
42397
|
return {
|
|
41809
42398
|
projects: projectResults,
|
|
41810
|
-
diagnostics:
|
|
42399
|
+
diagnostics: succeededProjects.flatMap((projectResult) => projectResult.diagnostics),
|
|
41811
42400
|
score: findWorstScore(projectResults),
|
|
42401
|
+
...succeededProjects.length > 0 ? { reactDetected: succeededProjects.some((projectResult) => projectResult.reactDetected) } : {},
|
|
41812
42402
|
elapsedMilliseconds: globalThis.performance.now() - startTime
|
|
41813
42403
|
};
|
|
41814
42404
|
};
|
|
@@ -41843,7 +42433,7 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
41843
42433
|
totalElapsedMilliseconds: result.elapsedMilliseconds
|
|
41844
42434
|
});
|
|
41845
42435
|
//#endregion
|
|
41846
|
-
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
42436
|
+
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
41847
42437
|
|
|
41848
42438
|
//# sourceMappingURL=index.js.map
|
|
41849
|
-
//# debugId=
|
|
42439
|
+
//# debugId=07deeb70-f47f-5572-9930-e743f970f84f
|