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/cli.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]="e920d892-391e-5c2c-85c7-689bd4fe9775")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
|
|
|
9
9
|
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import process$1 from "node: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, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
|
|
21
22
|
import { gzipSync } from "node:zlib";
|
|
22
23
|
import * as Sentry from "@sentry/node";
|
|
23
24
|
import { performance as performance$1 } from "node:perf_hooks";
|
|
@@ -22474,6 +22475,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
22474
22475
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
22475
22476
|
*/
|
|
22476
22477
|
baselineDegraded: optional(Boolean$1),
|
|
22478
|
+
/**
|
|
22479
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
22480
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
22481
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
22482
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
22483
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
22484
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
22485
|
+
* and on reports from older CLI versions.
|
|
22486
|
+
*/
|
|
22487
|
+
reactDetected: optional(Boolean$1),
|
|
22477
22488
|
diff: NullOr(JsonReportDiffInfo),
|
|
22478
22489
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
22479
22490
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -22499,6 +22510,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
22499
22510
|
ok: Boolean$1,
|
|
22500
22511
|
directory: String$1,
|
|
22501
22512
|
mode: JsonReportMode,
|
|
22513
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
22514
|
+
reactDetected: optional(Boolean$1),
|
|
22502
22515
|
diff: NullOr(JsonReportDiffInfo),
|
|
22503
22516
|
baseline: JsonReportBaseline,
|
|
22504
22517
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -36128,10 +36141,15 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
36128
36141
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36129
36142
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36130
36143
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36144
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
36145
|
+
const CORE_PACKAGE_VERSION = "0.7.0";
|
|
36146
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
36147
|
+
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
36131
36148
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36132
36149
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36133
36150
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36134
36151
|
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36152
|
+
const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
|
|
36135
36153
|
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36136
36154
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36137
36155
|
const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
@@ -38812,6 +38830,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
38812
38830
|
if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
|
|
38813
38831
|
return true;
|
|
38814
38832
|
} }) {};
|
|
38833
|
+
/**
|
|
38834
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
38835
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
38836
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
38837
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
38838
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
38839
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
38840
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
38841
|
+
*
|
|
38842
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
38843
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
38844
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
38845
|
+
* (the pre-cache behavior).
|
|
38846
|
+
*
|
|
38847
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
38848
|
+
*/
|
|
38849
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
38850
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38851
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
38852
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38853
|
+
if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
|
|
38854
|
+
return true;
|
|
38855
|
+
} }) {};
|
|
38856
|
+
/**
|
|
38857
|
+
* Whether the whole-project dead-code result cache
|
|
38858
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
38859
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
38860
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
38861
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
38862
|
+
*
|
|
38863
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
38864
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
38865
|
+
*
|
|
38866
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
38867
|
+
*/
|
|
38868
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
38869
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38870
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
38871
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38872
|
+
if (CACHE_DISABLED_VALUES$1.has(noDeadCodeCache)) return false;
|
|
38873
|
+
return true;
|
|
38874
|
+
} }) {};
|
|
38815
38875
|
const DIAGNOSTIC_SURFACES = [
|
|
38816
38876
|
"cli",
|
|
38817
38877
|
"prComment",
|
|
@@ -40189,10 +40249,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
40189
40249
|
}
|
|
40190
40250
|
}
|
|
40191
40251
|
}
|
|
40252
|
+
/**
|
|
40253
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
40254
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
40255
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
40256
|
+
* React-runtime rule family was gated off for the project.
|
|
40257
|
+
*/
|
|
40258
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
40192
40259
|
const buildCapabilities = (project) => {
|
|
40193
40260
|
const capabilities = /* @__PURE__ */ new Set();
|
|
40194
40261
|
capabilities.add(project.framework);
|
|
40195
|
-
if (project
|
|
40262
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
40196
40263
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
40197
40264
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
40198
40265
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -40544,6 +40611,146 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40544
40611
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40545
40612
|
};
|
|
40546
40613
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
40614
|
+
const atomicWriteJson = (filePath, value) => {
|
|
40615
|
+
try {
|
|
40616
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
40617
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
40618
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
40619
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
40620
|
+
} catch {
|
|
40621
|
+
return;
|
|
40622
|
+
}
|
|
40623
|
+
};
|
|
40624
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
40625
|
+
try {
|
|
40626
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
40627
|
+
} catch {
|
|
40628
|
+
return fallback;
|
|
40629
|
+
}
|
|
40630
|
+
};
|
|
40631
|
+
const hashFileContents = (filePath) => {
|
|
40632
|
+
try {
|
|
40633
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
40634
|
+
} catch {
|
|
40635
|
+
return null;
|
|
40636
|
+
}
|
|
40637
|
+
};
|
|
40638
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
40639
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
40640
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
40641
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
40642
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
40643
|
+
/**
|
|
40644
|
+
* Stat snapshot of every file the analysis reads, keyed by root-relative
|
|
40645
|
+
* `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
|
|
40646
|
+
* verified against the tree it started from.
|
|
40647
|
+
*/
|
|
40648
|
+
const collectAnalyzedFileStats = (rootDirectory) => {
|
|
40649
|
+
const statByRelativePath = /* @__PURE__ */ new Map();
|
|
40650
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
40651
|
+
if (!isFingerprintedFile(name)) continue;
|
|
40652
|
+
try {
|
|
40653
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
40654
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
40655
|
+
statByRelativePath.set(relativePath, {
|
|
40656
|
+
mtimeMs: fileStat.mtimeMs,
|
|
40657
|
+
size: fileStat.size
|
|
40658
|
+
});
|
|
40659
|
+
} catch {}
|
|
40660
|
+
}
|
|
40661
|
+
return statByRelativePath;
|
|
40662
|
+
};
|
|
40663
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
40664
|
+
const resolveDeslopVersion = () => {
|
|
40665
|
+
try {
|
|
40666
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
40667
|
+
return isRecord$1(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
40668
|
+
} catch {
|
|
40669
|
+
return "unknown";
|
|
40670
|
+
}
|
|
40671
|
+
};
|
|
40672
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
40673
|
+
schemaVersion: 2,
|
|
40674
|
+
coreVersion: input.coreVersion,
|
|
40675
|
+
deslopVersion: resolveDeslopVersion(),
|
|
40676
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
40677
|
+
entryPatterns: input.entryPatterns,
|
|
40678
|
+
ignorePatterns: input.ignorePatterns,
|
|
40679
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
|
|
40680
|
+
})).digest("hex");
|
|
40681
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
40682
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
40683
|
+
try {
|
|
40684
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
40685
|
+
return raw;
|
|
40686
|
+
} catch {
|
|
40687
|
+
return null;
|
|
40688
|
+
}
|
|
40689
|
+
};
|
|
40690
|
+
const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
|
|
40691
|
+
const lookupDeadCodeResultCache = (input) => {
|
|
40692
|
+
const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
|
|
40693
|
+
const persisted = failOpenReadJson(cacheFilePath, null);
|
|
40694
|
+
if (persisted === null || !isRecord$1(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord$1(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
|
|
40695
|
+
const storedFileEntries = Object.entries(persisted.files);
|
|
40696
|
+
if (storedFileEntries.length !== input.currentFileStats.size) return null;
|
|
40697
|
+
const repairedFiles = {};
|
|
40698
|
+
let repairedCount = 0;
|
|
40699
|
+
for (const [relativePath, storedIdentity] of storedFileEntries) {
|
|
40700
|
+
if (!isPersistedFileIdentity(storedIdentity)) return null;
|
|
40701
|
+
const currentStat = input.currentFileStats.get(relativePath);
|
|
40702
|
+
if (currentStat === void 0) return null;
|
|
40703
|
+
const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
|
|
40704
|
+
if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
|
|
40705
|
+
repairedFiles[relativePath] = storedIdentity;
|
|
40706
|
+
continue;
|
|
40707
|
+
}
|
|
40708
|
+
if (currentStat.size !== storedSize) return null;
|
|
40709
|
+
const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
|
|
40710
|
+
if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
|
|
40711
|
+
repairedFiles[relativePath] = [
|
|
40712
|
+
currentStat.mtimeMs,
|
|
40713
|
+
currentStat.size,
|
|
40714
|
+
storedContentHash
|
|
40715
|
+
];
|
|
40716
|
+
repairedCount += 1;
|
|
40717
|
+
}
|
|
40718
|
+
const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
|
|
40719
|
+
if (diagnostics === null) return null;
|
|
40720
|
+
if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
|
|
40721
|
+
version: 2,
|
|
40722
|
+
key: input.cacheKey,
|
|
40723
|
+
files: repairedFiles,
|
|
40724
|
+
diagnostics: persisted.diagnostics
|
|
40725
|
+
});
|
|
40726
|
+
return diagnostics;
|
|
40727
|
+
};
|
|
40728
|
+
const storeDeadCodeResultCache = (input) => {
|
|
40729
|
+
const persistedFiles = {};
|
|
40730
|
+
for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
|
|
40731
|
+
const absolutePath = Path.join(input.rootDirectory, relativePath);
|
|
40732
|
+
const contentHash = hashFileContents(absolutePath);
|
|
40733
|
+
if (contentHash === null) return;
|
|
40734
|
+
let currentStat;
|
|
40735
|
+
try {
|
|
40736
|
+
currentStat = NFS.statSync(absolutePath);
|
|
40737
|
+
} catch {
|
|
40738
|
+
return;
|
|
40739
|
+
}
|
|
40740
|
+
if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
|
|
40741
|
+
persistedFiles[relativePath] = [
|
|
40742
|
+
snapshotStat.mtimeMs,
|
|
40743
|
+
snapshotStat.size,
|
|
40744
|
+
contentHash
|
|
40745
|
+
];
|
|
40746
|
+
}
|
|
40747
|
+
atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
40748
|
+
version: 2,
|
|
40749
|
+
key: input.cacheKey,
|
|
40750
|
+
files: persistedFiles,
|
|
40751
|
+
diagnostics: input.diagnostics
|
|
40752
|
+
});
|
|
40753
|
+
};
|
|
40547
40754
|
const readSystemFacts = () => ({
|
|
40548
40755
|
availableCores: os.availableParallelism(),
|
|
40549
40756
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -40612,6 +40819,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
40612
40819
|
releaseSlot();
|
|
40613
40820
|
}
|
|
40614
40821
|
};
|
|
40822
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
40823
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
40824
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
40825
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
40826
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
40827
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
40828
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
40829
|
+
};
|
|
40830
|
+
const resolveUserCacheScope = () => {
|
|
40831
|
+
try {
|
|
40832
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
40833
|
+
} catch {
|
|
40834
|
+
return "shared";
|
|
40835
|
+
}
|
|
40836
|
+
};
|
|
40615
40837
|
/**
|
|
40616
40838
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40617
40839
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40665,6 +40887,14 @@ process.stdin.on("end", () => {
|
|
|
40665
40887
|
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
40666
40888
|
files: cycle.files,
|
|
40667
40889
|
})),
|
|
40890
|
+
...(result.incrementalCacheStats
|
|
40891
|
+
? {
|
|
40892
|
+
summaryCacheStats: {
|
|
40893
|
+
hits: result.incrementalCacheStats.summaryHits,
|
|
40894
|
+
misses: result.incrementalCacheStats.summaryMisses,
|
|
40895
|
+
},
|
|
40896
|
+
}
|
|
40897
|
+
: {}),
|
|
40668
40898
|
});
|
|
40669
40899
|
|
|
40670
40900
|
const serializeError = (error) =>
|
|
@@ -40688,6 +40918,9 @@ process.stdin.on("end", () => {
|
|
|
40688
40918
|
...(workerInput.ignorePatterns.length > 0
|
|
40689
40919
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
40690
40920
|
: {}),
|
|
40921
|
+
...(workerInput.incrementalCachePath
|
|
40922
|
+
? { incrementalCachePath: workerInput.incrementalCachePath }
|
|
40923
|
+
: {}),
|
|
40691
40924
|
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
40692
40925
|
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
40693
40926
|
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
@@ -40698,12 +40931,18 @@ process.stdin.on("end", () => {
|
|
|
40698
40931
|
// are the single most expensive pass — duplicate-block detection alone was
|
|
40699
40932
|
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
40700
40933
|
// speedup on a large repo.
|
|
40701
|
-
//
|
|
40934
|
+
// - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
|
|
40935
|
+
// simplifiable functions, identity wrappers, …) — ~120 ms of discarded
|
|
40936
|
+
// output per scan, and skipping them lets the incremental cache drop the
|
|
40937
|
+
// DRY-pattern summary fields (the largest slice of the cache file).
|
|
40938
|
+
// All are provably safe: the consumed graph findings are computed by their own
|
|
40702
40939
|
// detectors, independent of these passes (confirmed byte-identical on
|
|
40703
|
-
// excalidraw + mui-material + sentry
|
|
40704
|
-
//
|
|
40940
|
+
// excalidraw + mui-material + sentry; re-verified on sentry after the
|
|
40941
|
+
// reportRedundancy flip). tsConfigPath stays — the module resolver needs it
|
|
40942
|
+
// for path-alias resolution in the import graph.
|
|
40705
40943
|
semantic: { enabled: false },
|
|
40706
40944
|
reportCodeQuality: false,
|
|
40945
|
+
reportRedundancy: false,
|
|
40707
40946
|
};
|
|
40708
40947
|
const result = await analyze(defineConfig(config));
|
|
40709
40948
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -40787,13 +41026,23 @@ const parseCircularDependencies = (value) => {
|
|
|
40787
41026
|
}
|
|
40788
41027
|
return circularDependencies;
|
|
40789
41028
|
};
|
|
41029
|
+
const parseSummaryCacheStats = (value) => {
|
|
41030
|
+
if (!isRecord$1(value)) return void 0;
|
|
41031
|
+
if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
|
|
41032
|
+
return {
|
|
41033
|
+
hits: value.hits,
|
|
41034
|
+
misses: value.misses
|
|
41035
|
+
};
|
|
41036
|
+
};
|
|
40790
41037
|
const parseDeadCodeWorkerResult = (value) => {
|
|
40791
41038
|
if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
41039
|
+
const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
|
|
40792
41040
|
return {
|
|
40793
41041
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
40794
41042
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
40795
41043
|
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
40796
|
-
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
41044
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies),
|
|
41045
|
+
...summaryCacheStats ? { summaryCacheStats } : {}
|
|
40797
41046
|
};
|
|
40798
41047
|
};
|
|
40799
41048
|
const parseDeadCodeWorkerError = (value) => {
|
|
@@ -40907,17 +41156,41 @@ const checkDeadCode = async (options) => {
|
|
|
40907
41156
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40908
41157
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40909
41158
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
41159
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
41160
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
41161
|
+
const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
|
|
41162
|
+
const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
|
|
41163
|
+
rootDirectory,
|
|
41164
|
+
entryPatterns,
|
|
41165
|
+
ignorePatterns,
|
|
41166
|
+
tsConfigPath,
|
|
41167
|
+
deslopJsModuleSpecifier,
|
|
41168
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
41169
|
+
});
|
|
41170
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) {
|
|
41171
|
+
const cachedDiagnostics = lookupDeadCodeResultCache({
|
|
41172
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
41173
|
+
cacheKey,
|
|
41174
|
+
rootDirectory,
|
|
41175
|
+
currentFileStats: fileStatsSnapshot
|
|
41176
|
+
});
|
|
41177
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
41178
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
41179
|
+
}
|
|
41180
|
+
const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
|
|
40910
41181
|
const spawnAndRun = () => {
|
|
40911
41182
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40912
41183
|
rootDirectory,
|
|
40913
41184
|
entryPatterns,
|
|
40914
|
-
tsConfigPath
|
|
41185
|
+
tsConfigPath,
|
|
40915
41186
|
ignorePatterns,
|
|
40916
|
-
deslopJsModuleSpecifier
|
|
40917
|
-
parseConcurrency: options.parseConcurrency
|
|
41187
|
+
deslopJsModuleSpecifier,
|
|
41188
|
+
parseConcurrency: options.parseConcurrency,
|
|
41189
|
+
incrementalCachePath
|
|
40918
41190
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40919
41191
|
};
|
|
40920
41192
|
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
41193
|
+
if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
|
|
40921
41194
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40922
41195
|
const diagnostics = [];
|
|
40923
41196
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40974,6 +41247,13 @@ const checkDeadCode = async (options) => {
|
|
|
40974
41247
|
category: DEAD_CODE_CATEGORY
|
|
40975
41248
|
});
|
|
40976
41249
|
}
|
|
41250
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
|
|
41251
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
41252
|
+
cacheKey,
|
|
41253
|
+
rootDirectory,
|
|
41254
|
+
snapshotFileStats: fileStatsSnapshot,
|
|
41255
|
+
diagnostics
|
|
41256
|
+
});
|
|
40977
41257
|
return diagnostics;
|
|
40978
41258
|
};
|
|
40979
41259
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -41076,12 +41356,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
41076
41356
|
*/
|
|
41077
41357
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
41078
41358
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
41359
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
41079
41360
|
return yield* tryPromise({
|
|
41080
41361
|
try: (signal) => checkDeadCode({
|
|
41081
41362
|
rootDirectory: input.rootDirectory,
|
|
41082
41363
|
parseConcurrency: input.parseConcurrency,
|
|
41083
41364
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
41084
|
-
abortSignal: signal
|
|
41365
|
+
abortSignal: signal,
|
|
41366
|
+
cacheEnabled,
|
|
41367
|
+
onCacheOutcome: input.onCacheOutcome,
|
|
41368
|
+
onSummaryCacheStats: input.onSummaryCacheStats
|
|
41085
41369
|
}),
|
|
41086
41370
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
41087
41371
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -41951,7 +42235,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41951
42235
|
}
|
|
41952
42236
|
return enabled;
|
|
41953
42237
|
};
|
|
41954
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
42238
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
41955
42239
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41956
42240
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41957
42241
|
const jsPlugins = [];
|
|
@@ -41963,6 +42247,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41963
42247
|
if (!rule) continue;
|
|
41964
42248
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41965
42249
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
42250
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
41966
42251
|
if (rule.scan !== void 0) continue;
|
|
41967
42252
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41968
42253
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -42007,33 +42292,145 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
42007
42292
|
}
|
|
42008
42293
|
};
|
|
42009
42294
|
};
|
|
42010
|
-
const
|
|
42295
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
42296
|
+
const decodeFileDiagnostics = (raw) => {
|
|
42297
|
+
if (!Array.isArray(raw)) return null;
|
|
42011
42298
|
try {
|
|
42012
|
-
|
|
42013
|
-
|
|
42014
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
42015
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
42299
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
42300
|
+
return raw;
|
|
42016
42301
|
} catch {
|
|
42017
|
-
return;
|
|
42302
|
+
return null;
|
|
42018
42303
|
}
|
|
42019
42304
|
};
|
|
42020
|
-
const
|
|
42021
|
-
|
|
42022
|
-
|
|
42023
|
-
|
|
42024
|
-
|
|
42305
|
+
const emptyCache$1 = () => ({
|
|
42306
|
+
version: 1,
|
|
42307
|
+
rulesets: {}
|
|
42308
|
+
});
|
|
42309
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
42310
|
+
const entries = /* @__PURE__ */ new Map();
|
|
42311
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
42312
|
+
if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
|
|
42313
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
42314
|
+
if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
|
|
42315
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
42316
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
42317
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
42025
42318
|
}
|
|
42319
|
+
return entries;
|
|
42320
|
+
};
|
|
42321
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
42322
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
42323
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
42324
|
+
return {
|
|
42325
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
42326
|
+
store: (fileKey, diagnostics) => {
|
|
42327
|
+
entries.delete(fileKey);
|
|
42328
|
+
entries.set(fileKey, diagnostics);
|
|
42329
|
+
},
|
|
42330
|
+
persist: () => {
|
|
42331
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
42332
|
+
const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
42333
|
+
const existingBucket = rulesets[rulesetHash];
|
|
42334
|
+
const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
|
|
42335
|
+
const ourFiles = {};
|
|
42336
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
42337
|
+
const cappedEntries = Object.entries({
|
|
42338
|
+
...existingFiles,
|
|
42339
|
+
...ourFiles
|
|
42340
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
42341
|
+
rulesets[rulesetHash] = {
|
|
42342
|
+
updatedAtMs: Date.now(),
|
|
42343
|
+
files: Object.fromEntries(cappedEntries)
|
|
42344
|
+
};
|
|
42345
|
+
atomicWriteJson(cacheFilePath, {
|
|
42346
|
+
version: 1,
|
|
42347
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
42348
|
+
});
|
|
42349
|
+
}
|
|
42350
|
+
};
|
|
42351
|
+
};
|
|
42352
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
42353
|
+
/**
|
|
42354
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
42355
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
42356
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
42357
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
42358
|
+
*
|
|
42359
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
42360
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
42361
|
+
* hashes the cache-partition loop already computed
|
|
42362
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
42363
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
42364
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
42365
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
42366
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
42367
|
+
*
|
|
42368
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
42369
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
42370
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
42371
|
+
*/
|
|
42372
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
42373
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
42374
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
42375
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
42376
|
+
const contentAnswer = (relativePath) => {
|
|
42377
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
42378
|
+
if (memoized !== void 0) return memoized;
|
|
42379
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
42380
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
42381
|
+
return answer;
|
|
42382
|
+
};
|
|
42383
|
+
const existsAnswer = (relativePath) => {
|
|
42384
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
42385
|
+
if (memoized !== void 0) return memoized;
|
|
42386
|
+
let answer;
|
|
42387
|
+
try {
|
|
42388
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
42389
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
42390
|
+
} catch {
|
|
42391
|
+
answer = "none";
|
|
42392
|
+
}
|
|
42393
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
42394
|
+
return answer;
|
|
42395
|
+
};
|
|
42396
|
+
return {
|
|
42397
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
42398
|
+
toRelativePath
|
|
42399
|
+
};
|
|
42026
42400
|
};
|
|
42027
42401
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
42028
|
-
const
|
|
42029
|
-
|
|
42402
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
42403
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
42404
|
+
if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
42405
|
+
const probes = [];
|
|
42406
|
+
for (const rawProbe of raw.probes) {
|
|
42407
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
42408
|
+
const [kind, probePath, answer] = rawProbe;
|
|
42409
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
42410
|
+
probes.push({
|
|
42411
|
+
kind,
|
|
42412
|
+
path: probePath,
|
|
42413
|
+
answer
|
|
42414
|
+
});
|
|
42415
|
+
}
|
|
42030
42416
|
try {
|
|
42031
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
42032
|
-
return raw;
|
|
42417
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
42033
42418
|
} catch {
|
|
42034
42419
|
return null;
|
|
42035
42420
|
}
|
|
42421
|
+
return {
|
|
42422
|
+
probes,
|
|
42423
|
+
diagnostics: raw.diagnostics
|
|
42424
|
+
};
|
|
42036
42425
|
};
|
|
42426
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
42427
|
+
probes: entry.probes.map((probe) => [
|
|
42428
|
+
probe.kind,
|
|
42429
|
+
probe.path,
|
|
42430
|
+
probe.answer
|
|
42431
|
+
]),
|
|
42432
|
+
diagnostics: entry.diagnostics
|
|
42433
|
+
});
|
|
42037
42434
|
const emptyCache = () => ({
|
|
42038
42435
|
version: 1,
|
|
42039
42436
|
rulesets: {}
|
|
@@ -42044,20 +42441,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
42044
42441
|
if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
|
|
42045
42442
|
const bucket = persisted.rulesets[rulesetHash];
|
|
42046
42443
|
if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
|
|
42047
|
-
for (const [fileKey,
|
|
42048
|
-
const decoded =
|
|
42444
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
42445
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
42049
42446
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
42050
42447
|
}
|
|
42051
42448
|
return entries;
|
|
42052
42449
|
};
|
|
42053
|
-
const
|
|
42054
|
-
const cacheFilePath = Path.join(cacheDirectory,
|
|
42450
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
42451
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
42055
42452
|
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
42056
42453
|
return {
|
|
42057
42454
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
42058
|
-
store: (fileKey,
|
|
42455
|
+
store: (fileKey, entry) => {
|
|
42059
42456
|
entries.delete(fileKey);
|
|
42060
|
-
entries.set(fileKey,
|
|
42457
|
+
entries.set(fileKey, entry);
|
|
42061
42458
|
},
|
|
42062
42459
|
persist: () => {
|
|
42063
42460
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
@@ -42065,7 +42462,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42065
42462
|
const existingBucket = rulesets[rulesetHash];
|
|
42066
42463
|
const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
|
|
42067
42464
|
const ourFiles = {};
|
|
42068
|
-
for (const [fileKey,
|
|
42465
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
42069
42466
|
const cappedEntries = Object.entries({
|
|
42070
42467
|
...existingFiles,
|
|
42071
42468
|
...ourFiles
|
|
@@ -42081,7 +42478,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42081
42478
|
}
|
|
42082
42479
|
};
|
|
42083
42480
|
};
|
|
42084
|
-
const bundledRequire$
|
|
42481
|
+
const bundledRequire$3 = createRequire(import.meta.url);
|
|
42085
42482
|
const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
|
|
42086
42483
|
const resolveChildNodeVersion = (nodeBinaryPath) => {
|
|
42087
42484
|
if (nodeBinaryPath === process.execPath) return process.version;
|
|
@@ -42107,7 +42504,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
|
|
|
42107
42504
|
const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
|
|
42108
42505
|
const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
|
|
42109
42506
|
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
|
|
42110
|
-
const packageJson = bundledRequire$
|
|
42507
|
+
const packageJson = bundledRequire$3(specifier);
|
|
42111
42508
|
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
42112
42509
|
versions.push(`${specifier}=${version}`);
|
|
42113
42510
|
} catch {
|
|
@@ -43080,13 +43477,6 @@ const validateRuleRegistration = () => {
|
|
|
43080
43477
|
].filter((entry) => entry !== null).join("; ");
|
|
43081
43478
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
43082
43479
|
};
|
|
43083
|
-
const hashFileContents = (filePath) => {
|
|
43084
|
-
try {
|
|
43085
|
-
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
43086
|
-
} catch {
|
|
43087
|
-
return null;
|
|
43088
|
-
}
|
|
43089
|
-
};
|
|
43090
43480
|
/**
|
|
43091
43481
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
43092
43482
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -43151,21 +43541,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
43151
43541
|
}
|
|
43152
43542
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
43153
43543
|
};
|
|
43154
|
-
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
43155
|
-
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
43156
|
-
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
43157
|
-
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
43158
|
-
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
43159
|
-
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
43160
|
-
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
43161
|
-
};
|
|
43162
|
-
const resolveUserCacheScope = () => {
|
|
43163
|
-
try {
|
|
43164
|
-
return String(process.getuid?.() ?? os.userInfo().username);
|
|
43165
|
-
} catch {
|
|
43166
|
-
return "shared";
|
|
43167
|
-
}
|
|
43168
|
-
};
|
|
43169
43544
|
/**
|
|
43170
43545
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
43171
43546
|
* the runner: once for the primary scan, once for the
|
|
@@ -43183,6 +43558,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
43183
43558
|
NFS.closeSync(fileHandle);
|
|
43184
43559
|
}
|
|
43185
43560
|
};
|
|
43561
|
+
/**
|
|
43562
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
43563
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
43564
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
43565
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
43566
|
+
* actually had diagnostics.
|
|
43567
|
+
*/
|
|
43568
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
43569
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43570
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
43571
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43572
|
+
for (const diagnostic of diagnostics) {
|
|
43573
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
43574
|
+
if (file === void 0) return null;
|
|
43575
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
43576
|
+
fileDiagnostics.push(diagnostic);
|
|
43577
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
43578
|
+
}
|
|
43579
|
+
return diagnosticsByFile;
|
|
43580
|
+
};
|
|
43581
|
+
/**
|
|
43582
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
43583
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
43584
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
43585
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
43586
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
43587
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
43588
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
43589
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
43590
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
43591
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
43592
|
+
*/
|
|
43593
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
43594
|
+
const buildProbes = (file) => {
|
|
43595
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
43596
|
+
let trace;
|
|
43597
|
+
try {
|
|
43598
|
+
trace = collectCrossFileDependencyProbes({
|
|
43599
|
+
absoluteFilePath,
|
|
43600
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
43601
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
43602
|
+
});
|
|
43603
|
+
} catch {
|
|
43604
|
+
return null;
|
|
43605
|
+
}
|
|
43606
|
+
if (trace === null) return null;
|
|
43607
|
+
const probes = [];
|
|
43608
|
+
for (const contentPath of trace.contentPaths) {
|
|
43609
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
43610
|
+
probes.push({
|
|
43611
|
+
kind: "content",
|
|
43612
|
+
path: relativePath,
|
|
43613
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
43614
|
+
});
|
|
43615
|
+
}
|
|
43616
|
+
for (const existencePath of trace.existencePaths) {
|
|
43617
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
43618
|
+
probes.push({
|
|
43619
|
+
kind: "exists",
|
|
43620
|
+
path: relativePath,
|
|
43621
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
43622
|
+
});
|
|
43623
|
+
}
|
|
43624
|
+
return probes;
|
|
43625
|
+
};
|
|
43626
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
43627
|
+
let collectSliceStartedAt = performance.now();
|
|
43628
|
+
for (const file of input.files) {
|
|
43629
|
+
probesByFile.set(file, buildProbes(file));
|
|
43630
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
43631
|
+
await yieldToEventLoop();
|
|
43632
|
+
collectSliceStartedAt = performance.now();
|
|
43633
|
+
}
|
|
43634
|
+
}
|
|
43635
|
+
return probesByFile;
|
|
43636
|
+
};
|
|
43637
|
+
/**
|
|
43638
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
43639
|
+
* the next scan can replay instead of re-linting.
|
|
43640
|
+
*/
|
|
43641
|
+
const storeSidecarEntries = (input) => {
|
|
43642
|
+
let didStoreAnyEntry = false;
|
|
43643
|
+
for (const pass of input.storablePasses) {
|
|
43644
|
+
if (!pass.isTrusted) continue;
|
|
43645
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
43646
|
+
if (diagnosticsByFile === null) continue;
|
|
43647
|
+
for (const file of pass.files) {
|
|
43648
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
43649
|
+
if (cacheKey === void 0) continue;
|
|
43650
|
+
const probes = pass.probesByFile.get(file);
|
|
43651
|
+
if (probes === void 0 || probes === null) continue;
|
|
43652
|
+
input.sidecarCache.store(cacheKey, {
|
|
43653
|
+
probes,
|
|
43654
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
43655
|
+
});
|
|
43656
|
+
didStoreAnyEntry = true;
|
|
43657
|
+
}
|
|
43658
|
+
}
|
|
43659
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
43660
|
+
};
|
|
43186
43661
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
43187
43662
|
/**
|
|
43188
43663
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -43224,7 +43699,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
43224
43699
|
* 6. always restore disable directives + clean up the temp dir
|
|
43225
43700
|
*/
|
|
43226
43701
|
const runOxlint = async (options) => {
|
|
43227
|
-
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;
|
|
43702
|
+
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;
|
|
43228
43703
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
43229
43704
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
43230
43705
|
validateRuleRegistration();
|
|
@@ -43242,7 +43717,8 @@ const runOxlint = async (options) => {
|
|
|
43242
43717
|
severityControls,
|
|
43243
43718
|
userPlugins,
|
|
43244
43719
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
43245
|
-
ruleSelection: overrides.ruleSelection
|
|
43720
|
+
ruleSelection: overrides.ruleSelection,
|
|
43721
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
43246
43722
|
});
|
|
43247
43723
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
43248
43724
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -43345,15 +43821,18 @@ const runOxlint = async (options) => {
|
|
|
43345
43821
|
});
|
|
43346
43822
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43347
43823
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43824
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
43348
43825
|
const missFiles = [];
|
|
43349
43826
|
const replayedDiagnostics = [];
|
|
43350
43827
|
let hashSliceStartedAt = performance.now();
|
|
43351
43828
|
for (const candidateFile of candidateFiles) {
|
|
43352
|
-
const
|
|
43829
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
43830
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
43353
43831
|
if (contentHash === null) {
|
|
43354
43832
|
missFiles.push(candidateFile);
|
|
43355
43833
|
continue;
|
|
43356
43834
|
}
|
|
43835
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
43357
43836
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
43358
43837
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43359
43838
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -43365,43 +43844,111 @@ const runOxlint = async (options) => {
|
|
|
43365
43844
|
}
|
|
43366
43845
|
}
|
|
43367
43846
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43847
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
43848
|
+
extendsPaths: [],
|
|
43849
|
+
ruleSelection: "sidecar"
|
|
43850
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
43851
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
43852
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
43853
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
43854
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
43855
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
43856
|
+
config: buildConfig({
|
|
43857
|
+
extendsPaths: [],
|
|
43858
|
+
ruleSelection: "sidecar",
|
|
43859
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
43860
|
+
}),
|
|
43861
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
43862
|
+
ignorePatterns: combinedPatterns,
|
|
43863
|
+
tsconfigContent
|
|
43864
|
+
})) : null;
|
|
43865
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
43866
|
+
rootDirectory,
|
|
43867
|
+
contentHashByRelativePath
|
|
43868
|
+
});
|
|
43869
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
43870
|
+
files: missFiles,
|
|
43871
|
+
rootDirectory,
|
|
43872
|
+
boundedSidecarRuleIds,
|
|
43873
|
+
probeAnswers
|
|
43874
|
+
});
|
|
43368
43875
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43369
43876
|
extendsPaths: [],
|
|
43370
43877
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43371
43878
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
43372
43879
|
const missFileSet = new Set(missFiles);
|
|
43373
|
-
const
|
|
43880
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
43881
|
+
const sidecarReplayedDiagnostics = [];
|
|
43882
|
+
const sidecarLintFiles = [];
|
|
43883
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
43884
|
+
else {
|
|
43885
|
+
let verifySliceStartedAt = performance.now();
|
|
43886
|
+
for (const hitFile of hitFiles) {
|
|
43887
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
43888
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
43889
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
43890
|
+
else sidecarLintFiles.push(hitFile);
|
|
43891
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
43892
|
+
await yieldToEventLoop();
|
|
43893
|
+
verifySliceStartedAt = performance.now();
|
|
43894
|
+
}
|
|
43895
|
+
}
|
|
43896
|
+
}
|
|
43897
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
43898
|
+
files: sidecarLintFiles,
|
|
43899
|
+
rootDirectory,
|
|
43900
|
+
boundedSidecarRuleIds,
|
|
43901
|
+
probeAnswers
|
|
43902
|
+
});
|
|
43903
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
43904
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
43905
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43374
43906
|
extendsPaths: [],
|
|
43375
|
-
ruleSelection: "sidecar"
|
|
43376
|
-
|
|
43907
|
+
ruleSelection: "sidecar",
|
|
43908
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
43909
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
43910
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
43911
|
+
extendsPaths: [],
|
|
43912
|
+
ruleSelection: "sidecar",
|
|
43913
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
43914
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
43915
|
+
diagnostics: [],
|
|
43916
|
+
didDropReactHooksJsPlugin: false,
|
|
43917
|
+
hadPartialFailure: false
|
|
43918
|
+
};
|
|
43377
43919
|
const freshCacheableDiagnostics = [];
|
|
43378
43920
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
43379
43921
|
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43380
|
-
|
|
43381
|
-
|
|
43382
|
-
|
|
43383
|
-
let isAttributionSound = true;
|
|
43384
|
-
for (const diagnostic of freshCacheableDiagnostics) {
|
|
43385
|
-
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43386
|
-
if (missFile === void 0) {
|
|
43387
|
-
isAttributionSound = false;
|
|
43388
|
-
break;
|
|
43389
|
-
}
|
|
43390
|
-
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43391
|
-
fileDiagnostics.push(diagnostic);
|
|
43392
|
-
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43393
|
-
}
|
|
43394
|
-
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
|
|
43922
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
43923
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
43924
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
43395
43925
|
for (const missFile of missFiles) {
|
|
43396
43926
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43397
43927
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43398
43928
|
}
|
|
43399
43929
|
cache.persist();
|
|
43400
43930
|
}
|
|
43931
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
43932
|
+
sidecarCache,
|
|
43933
|
+
cacheKeyByFile,
|
|
43934
|
+
storablePasses: [{
|
|
43935
|
+
files: sidecarLintFiles,
|
|
43936
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
43937
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
43938
|
+
probesByFile: await sidecarProbesTask
|
|
43939
|
+
}, {
|
|
43940
|
+
files: missFiles,
|
|
43941
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
43942
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
43943
|
+
probesByFile: await missProbesTask
|
|
43944
|
+
}]
|
|
43945
|
+
});
|
|
43401
43946
|
return dedupeDiagnostics([
|
|
43402
43947
|
...replayedDiagnostics,
|
|
43403
43948
|
...fullResult.diagnostics,
|
|
43404
|
-
...
|
|
43949
|
+
...sidecarReplayedDiagnostics,
|
|
43950
|
+
...boundedSidecarResult.diagnostics,
|
|
43951
|
+
...unboundedSidecarResult.diagnostics
|
|
43405
43952
|
]);
|
|
43406
43953
|
}
|
|
43407
43954
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -43499,6 +44046,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
43499
44046
|
const concurrency = yield* OxlintConcurrency;
|
|
43500
44047
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43501
44048
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
44049
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
43502
44050
|
const collectedFailures = [];
|
|
43503
44051
|
const diagnostics = yield* tryPromise({
|
|
43504
44052
|
try: (signal) => runOxlint({
|
|
@@ -43517,7 +44065,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
43517
44065
|
},
|
|
43518
44066
|
onFileProgress: input.onFileProgress,
|
|
43519
44067
|
perFileLintCacheEnabled,
|
|
44068
|
+
sidecarLintCacheEnabled,
|
|
43520
44069
|
onCacheStats: input.onCacheStats,
|
|
44070
|
+
onSidecarStats: input.onSidecarStats,
|
|
43521
44071
|
spawnTimeoutMs,
|
|
43522
44072
|
outputMaxBytes,
|
|
43523
44073
|
concurrency,
|
|
@@ -43939,6 +44489,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
|
|
|
43939
44489
|
}));
|
|
43940
44490
|
} catch {}
|
|
43941
44491
|
};
|
|
44492
|
+
const pruneExpiredSocketCache = (cacheDirectory) => {
|
|
44493
|
+
try {
|
|
44494
|
+
const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
|
|
44495
|
+
const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
|
|
44496
|
+
for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
|
|
44497
|
+
const entryPath = Path.join(supplyChainCacheDirectory, entryName);
|
|
44498
|
+
try {
|
|
44499
|
+
if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
|
|
44500
|
+
} catch {
|
|
44501
|
+
continue;
|
|
44502
|
+
}
|
|
44503
|
+
}
|
|
44504
|
+
} catch {}
|
|
44505
|
+
};
|
|
43942
44506
|
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
43943
44507
|
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
43944
44508
|
if (cacheFile !== null) {
|
|
@@ -44066,6 +44630,7 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
44066
44630
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
44067
44631
|
if (dependencies.length === 0) return [];
|
|
44068
44632
|
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
44633
|
+
if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
|
|
44069
44634
|
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
44070
44635
|
const diagnostics = [];
|
|
44071
44636
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
@@ -44292,7 +44857,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44292
44857
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
44293
44858
|
rootDirectory: scanDirectory,
|
|
44294
44859
|
parseConcurrency: deadCodeParseConcurrency,
|
|
44295
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
44860
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
44861
|
+
onCacheOutcome: (didHitCache) => {
|
|
44862
|
+
deadCodeCacheHit = didHitCache;
|
|
44863
|
+
},
|
|
44864
|
+
onSummaryCacheStats: (stats) => {
|
|
44865
|
+
deadCodeSummaryCacheHits = stats.hits;
|
|
44866
|
+
deadCodeSummaryCacheMisses = stats.misses;
|
|
44867
|
+
}
|
|
44296
44868
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
44297
44869
|
yield* set(deadCodeFailure, {
|
|
44298
44870
|
didFail: true,
|
|
@@ -44320,6 +44892,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44320
44892
|
let lastReportedTotalFileCount = 0;
|
|
44321
44893
|
let lintCacheHitFileCount = null;
|
|
44322
44894
|
let lintCacheTotalFileCount = null;
|
|
44895
|
+
let lintSidecarReplayedFileCount = null;
|
|
44896
|
+
let lintSidecarTotalFileCount = null;
|
|
44897
|
+
let deadCodeCacheHit = null;
|
|
44898
|
+
let deadCodeSummaryCacheHits = null;
|
|
44899
|
+
let deadCodeSummaryCacheMisses = null;
|
|
44323
44900
|
const baseLintStream = linterService.run({
|
|
44324
44901
|
rootDirectory: scanDirectory,
|
|
44325
44902
|
project,
|
|
@@ -44339,6 +44916,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44339
44916
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
44340
44917
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
44341
44918
|
},
|
|
44919
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
44920
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
44921
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
44922
|
+
},
|
|
44342
44923
|
deadlineEpochMs: input.deadlineEpochMs
|
|
44343
44924
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
44344
44925
|
yield* set(lintFailure, {
|
|
@@ -44449,6 +45030,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44449
45030
|
securityScanFailed,
|
|
44450
45031
|
lintCacheHitFileCount,
|
|
44451
45032
|
lintCacheTotalFileCount,
|
|
45033
|
+
lintSidecarReplayedFileCount,
|
|
45034
|
+
lintSidecarTotalFileCount,
|
|
45035
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
45036
|
+
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
45037
|
+
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
44452
45038
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
44453
45039
|
};
|
|
44454
45040
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -44788,6 +45374,7 @@ const buildJsonReport = (input) => {
|
|
|
44788
45374
|
const worstScoredProject = findWorstScoredProject(projects);
|
|
44789
45375
|
const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
|
|
44790
45376
|
const shared = {
|
|
45377
|
+
...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
|
|
44791
45378
|
version: input.version,
|
|
44792
45379
|
ok: true,
|
|
44793
45380
|
directory: input.directory,
|
|
@@ -44930,11 +45517,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
44930
45517
|
const parts = [`v=${version}`];
|
|
44931
45518
|
let directory = projectDirectory;
|
|
44932
45519
|
for (;;) {
|
|
44933
|
-
for (const filename of CONFIG_FINGERPRINT_FILENAMES)
|
|
44934
|
-
const
|
|
44935
|
-
parts.push(`${directory}/${filename}=${
|
|
44936
|
-
} catch {
|
|
44937
|
-
continue;
|
|
45520
|
+
for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
|
|
45521
|
+
const contentHash = hashFileContents(path.join(directory, filename));
|
|
45522
|
+
if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
|
|
44938
45523
|
}
|
|
44939
45524
|
const parent = path.dirname(directory);
|
|
44940
45525
|
if (parent === directory) break;
|
|
@@ -45230,6 +45815,8 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
|
|
|
45230
45815
|
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
45231
45816
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
45232
45817
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
45818
|
+
const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
|
|
45819
|
+
const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
45233
45820
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
45234
45821
|
const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
|
|
45235
45822
|
const WELCOME_EXPLANATION_HOLD_MS = 1e3;
|
|
@@ -45251,6 +45838,7 @@ const METRIC = {
|
|
|
45251
45838
|
scanScore: "scan.score",
|
|
45252
45839
|
scanClean: "scan.clean",
|
|
45253
45840
|
scanCheckSkipped: "scan.check_skipped",
|
|
45841
|
+
scanNoReactDetected: "scan.no_react_detected",
|
|
45254
45842
|
baselineDegraded: "baseline.degraded",
|
|
45255
45843
|
ruleFired: "rule.fired",
|
|
45256
45844
|
ruleDisabled: "rule.disabled",
|
|
@@ -45320,7 +45908,7 @@ const makeNoopConsole = () => ({
|
|
|
45320
45908
|
});
|
|
45321
45909
|
//#endregion
|
|
45322
45910
|
//#region src/cli/utils/version.ts
|
|
45323
|
-
const VERSION = "0.
|
|
45911
|
+
const VERSION = "0.7.0-dev.6b21b70";
|
|
45324
45912
|
//#endregion
|
|
45325
45913
|
//#region src/cli/utils/json-mode.ts
|
|
45326
45914
|
let context = null;
|
|
@@ -45689,13 +46277,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45689
46277
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45690
46278
|
* standard `SENTRY_RELEASE` override.
|
|
45691
46279
|
*/
|
|
45692
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.
|
|
46280
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.0-dev.6b21b70`;
|
|
45693
46281
|
/**
|
|
45694
46282
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45695
46283
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45696
46284
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45697
46285
|
*/
|
|
45698
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.
|
|
46286
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.0-dev.6b21b70") ? "development" : "production");
|
|
45699
46287
|
/**
|
|
45700
46288
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45701
46289
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -45896,7 +46484,8 @@ const runGit = (projectRoot, args) => {
|
|
|
45896
46484
|
"ignore",
|
|
45897
46485
|
"pipe",
|
|
45898
46486
|
"ignore"
|
|
45899
|
-
]
|
|
46487
|
+
],
|
|
46488
|
+
maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
|
|
45900
46489
|
}).trim();
|
|
45901
46490
|
} catch {
|
|
45902
46491
|
return null;
|
|
@@ -57772,6 +58361,276 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
|
|
|
57772
58361
|
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
57773
58362
|
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
57774
58363
|
//#endregion
|
|
58364
|
+
//#region src/cli/utils/scan-result-cache.ts
|
|
58365
|
+
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
58366
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
58367
|
+
"oxlint/package.json",
|
|
58368
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
58369
|
+
"deslop-js/package.json",
|
|
58370
|
+
"eslint-plugin-react-hooks/package.json"
|
|
58371
|
+
];
|
|
58372
|
+
const bundledRequire = createRequire(import.meta.url);
|
|
58373
|
+
const normalizeForStableJson = (value) => {
|
|
58374
|
+
if (value === null) return null;
|
|
58375
|
+
if (value === void 0) return void 0;
|
|
58376
|
+
const valueType = typeof value;
|
|
58377
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
|
|
58378
|
+
if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
|
|
58379
|
+
if (Array.isArray(value)) return value.map((item) => {
|
|
58380
|
+
const normalized = normalizeForStableJson(item);
|
|
58381
|
+
return normalized === void 0 ? null : normalized;
|
|
58382
|
+
});
|
|
58383
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58384
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
|
|
58385
|
+
if (!isRecord(value)) throw new Error("Unsupported cache key object");
|
|
58386
|
+
const normalizedRecord = {};
|
|
58387
|
+
for (const key of Object.keys(value).sort()) {
|
|
58388
|
+
const normalized = normalizeForStableJson(value[key]);
|
|
58389
|
+
if (normalized !== void 0) normalizedRecord[key] = normalized;
|
|
58390
|
+
}
|
|
58391
|
+
return normalizedRecord;
|
|
58392
|
+
};
|
|
58393
|
+
const stringifyStableJson = (value) => {
|
|
58394
|
+
try {
|
|
58395
|
+
return JSON.stringify(normalizeForStableJson(value));
|
|
58396
|
+
} catch {
|
|
58397
|
+
return null;
|
|
58398
|
+
}
|
|
58399
|
+
};
|
|
58400
|
+
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
58401
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
58402
|
+
const parseWorktreeStatusRecords = (statusOutput) => {
|
|
58403
|
+
const entries = [];
|
|
58404
|
+
const tokens = statusOutput.split("\0");
|
|
58405
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
58406
|
+
let record = tokens[tokenIndex];
|
|
58407
|
+
if (record.length === 0) continue;
|
|
58408
|
+
if (record.length < 3 || record[2] !== " ") {
|
|
58409
|
+
if (tokenIndex !== 0) return null;
|
|
58410
|
+
record = ` ${record}`;
|
|
58411
|
+
}
|
|
58412
|
+
if (record.length < 4 || record[2] !== " ") return null;
|
|
58413
|
+
const statusCode = record.slice(0, 2);
|
|
58414
|
+
const recordPath = record.slice(3);
|
|
58415
|
+
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
58416
|
+
tokenIndex += 1;
|
|
58417
|
+
const originPath = tokens[tokenIndex];
|
|
58418
|
+
if (originPath === void 0 || originPath.length === 0) return null;
|
|
58419
|
+
entries.push({
|
|
58420
|
+
statusCode,
|
|
58421
|
+
path: recordPath,
|
|
58422
|
+
originPath
|
|
58423
|
+
});
|
|
58424
|
+
continue;
|
|
58425
|
+
}
|
|
58426
|
+
entries.push({
|
|
58427
|
+
statusCode,
|
|
58428
|
+
path: recordPath
|
|
58429
|
+
});
|
|
58430
|
+
}
|
|
58431
|
+
return entries;
|
|
58432
|
+
};
|
|
58433
|
+
const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
|
|
58434
|
+
let stats;
|
|
58435
|
+
try {
|
|
58436
|
+
stats = fs.lstatSync(absolutePath);
|
|
58437
|
+
} catch {
|
|
58438
|
+
return statusCode.includes("D") ? "deleted" : null;
|
|
58439
|
+
}
|
|
58440
|
+
if (stats.isSymbolicLink()) try {
|
|
58441
|
+
return `link:${fs.readlinkSync(absolutePath)}`;
|
|
58442
|
+
} catch {
|
|
58443
|
+
return null;
|
|
58444
|
+
}
|
|
58445
|
+
if (!stats.isFile()) return null;
|
|
58446
|
+
if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
|
|
58447
|
+
return hashFileContents(absolutePath);
|
|
58448
|
+
};
|
|
58449
|
+
const buildWorktreeFingerprint = (projectDirectory) => {
|
|
58450
|
+
const statusOutput = runGit(projectDirectory, [
|
|
58451
|
+
"status",
|
|
58452
|
+
"--porcelain=v1",
|
|
58453
|
+
"-z",
|
|
58454
|
+
"--untracked-files=all"
|
|
58455
|
+
]);
|
|
58456
|
+
if (statusOutput === null) return null;
|
|
58457
|
+
if (statusOutput.length === 0) return [];
|
|
58458
|
+
const statusEntries = parseWorktreeStatusRecords(statusOutput);
|
|
58459
|
+
if (statusEntries === null) return null;
|
|
58460
|
+
if (statusEntries.length > 300) return null;
|
|
58461
|
+
const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
|
|
58462
|
+
if (repositoryRoot === null) return null;
|
|
58463
|
+
const dirtyEntries = [];
|
|
58464
|
+
for (const statusEntry of statusEntries) {
|
|
58465
|
+
const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
|
|
58466
|
+
if (contentFingerprint === null) return null;
|
|
58467
|
+
dirtyEntries.push({
|
|
58468
|
+
...statusEntry,
|
|
58469
|
+
contentFingerprint
|
|
58470
|
+
});
|
|
58471
|
+
}
|
|
58472
|
+
return dirtyEntries.sort((firstEntry, secondEntry) => {
|
|
58473
|
+
const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
|
|
58474
|
+
const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
|
|
58475
|
+
return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
|
|
58476
|
+
});
|
|
58477
|
+
};
|
|
58478
|
+
const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
|
|
58479
|
+
const resolveDotenvFingerprint = (projectDirectory) => {
|
|
58480
|
+
try {
|
|
58481
|
+
return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
|
|
58482
|
+
} catch {
|
|
58483
|
+
return [];
|
|
58484
|
+
}
|
|
58485
|
+
};
|
|
58486
|
+
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
58487
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
58488
|
+
if (output === null) return true;
|
|
58489
|
+
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
58490
|
+
};
|
|
58491
|
+
const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
|
|
58492
|
+
const readPersistedCache = (cacheFilePath) => {
|
|
58493
|
+
try {
|
|
58494
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
58495
|
+
if (!isRecord(parsed) || parsed.version !== 3) return {
|
|
58496
|
+
version: 3,
|
|
58497
|
+
entries: []
|
|
58498
|
+
};
|
|
58499
|
+
if (!Array.isArray(parsed.entries)) return {
|
|
58500
|
+
version: 3,
|
|
58501
|
+
entries: []
|
|
58502
|
+
};
|
|
58503
|
+
const entries = [];
|
|
58504
|
+
for (const entry of parsed.entries) {
|
|
58505
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
58506
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
58507
|
+
entries.push(entry);
|
|
58508
|
+
}
|
|
58509
|
+
return {
|
|
58510
|
+
version: 3,
|
|
58511
|
+
entries
|
|
58512
|
+
};
|
|
58513
|
+
} catch {
|
|
58514
|
+
return {
|
|
58515
|
+
version: 3,
|
|
58516
|
+
entries: []
|
|
58517
|
+
};
|
|
58518
|
+
}
|
|
58519
|
+
};
|
|
58520
|
+
const writePersistedCache = (cacheFilePath, cache) => {
|
|
58521
|
+
try {
|
|
58522
|
+
fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
|
|
58523
|
+
const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
58524
|
+
fs.writeFileSync(tempPath, JSON.stringify(cache));
|
|
58525
|
+
fs.renameSync(tempPath, cacheFilePath);
|
|
58526
|
+
} catch {
|
|
58527
|
+
return;
|
|
58528
|
+
}
|
|
58529
|
+
};
|
|
58530
|
+
/**
|
|
58531
|
+
* The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
|
|
58532
|
+
* cache subsystem: this whole-repo scan cache plus core's per-file lint,
|
|
58533
|
+
* sidecar, and dead-code caches (their `Context.Reference` defaults read the
|
|
58534
|
+
* same variable). Exported for the wide event's `cache.temperature`
|
|
58535
|
+
* derivation, which reports `"disabled"` instead of `"cold"` when the switch
|
|
58536
|
+
* is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
|
|
58537
|
+
* not consulted — they leave the other subsystems live.
|
|
58538
|
+
*/
|
|
58539
|
+
const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
|
|
58540
|
+
const resolveProjectIdentity = (projectDirectory) => {
|
|
58541
|
+
try {
|
|
58542
|
+
return fs.realpathSync.native(projectDirectory);
|
|
58543
|
+
} catch {
|
|
58544
|
+
return path.resolve(projectDirectory);
|
|
58545
|
+
}
|
|
58546
|
+
};
|
|
58547
|
+
const fileFingerprint = (filePath) => {
|
|
58548
|
+
try {
|
|
58549
|
+
const stat = fs.statSync(filePath);
|
|
58550
|
+
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
58551
|
+
} catch {
|
|
58552
|
+
return null;
|
|
58553
|
+
}
|
|
58554
|
+
};
|
|
58555
|
+
const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
58556
|
+
const fingerprints = [];
|
|
58557
|
+
if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
|
|
58558
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
58559
|
+
const packageJson = bundledRequire(specifier);
|
|
58560
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
58561
|
+
fingerprints.push(`${specifier}=${version}`);
|
|
58562
|
+
} catch {
|
|
58563
|
+
fingerprints.push(`${specifier}=missing`);
|
|
58564
|
+
}
|
|
58565
|
+
return fingerprints;
|
|
58566
|
+
};
|
|
58567
|
+
const buildScanResultCacheKey = (input) => {
|
|
58568
|
+
if (isCacheGloballyDisabled()) return null;
|
|
58569
|
+
if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
|
|
58570
|
+
const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
|
|
58571
|
+
if (worktreeFingerprint === null) return null;
|
|
58572
|
+
const headSha = readHeadSha(input.projectDirectory);
|
|
58573
|
+
if (headSha === null) return null;
|
|
58574
|
+
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
58575
|
+
const cacheKeyJson = stringifyStableJson({
|
|
58576
|
+
schemaVersion: 3,
|
|
58577
|
+
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
58578
|
+
headSha,
|
|
58579
|
+
worktreeFingerprint,
|
|
58580
|
+
dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
|
|
58581
|
+
reactDoctorVersion: input.version,
|
|
58582
|
+
nodeVersion: process.version,
|
|
58583
|
+
toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
|
|
58584
|
+
configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
|
|
58585
|
+
hasConfigOverride: input.hasConfigOverride,
|
|
58586
|
+
configSourceDirectory: input.configSourceDirectory,
|
|
58587
|
+
userConfig: input.userConfig,
|
|
58588
|
+
engineOptions: {
|
|
58589
|
+
lint: input.options.lint,
|
|
58590
|
+
deadCode: input.options.deadCode,
|
|
58591
|
+
includePaths: [...input.options.includePaths].sort(),
|
|
58592
|
+
customRulesOnly: input.options.customRulesOnly,
|
|
58593
|
+
respectInlineDisables: input.options.respectInlineDisables,
|
|
58594
|
+
warnings: input.options.warnings,
|
|
58595
|
+
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
58596
|
+
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
58597
|
+
concurrency: input.options.concurrency,
|
|
58598
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
58599
|
+
baselineRef: input.options.baseline?.ref,
|
|
58600
|
+
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
58601
|
+
noScore: input.options.noScore,
|
|
58602
|
+
isCi: input.options.isCi,
|
|
58603
|
+
suppressRendering: input.options.suppressRendering,
|
|
58604
|
+
supplyChainManifestChanged: input.options.supplyChainManifestChanged
|
|
58605
|
+
}
|
|
58606
|
+
});
|
|
58607
|
+
return cacheKeyJson === null ? null : hashString(cacheKeyJson);
|
|
58608
|
+
};
|
|
58609
|
+
const createScanResultCache = (projectDirectory) => {
|
|
58610
|
+
const cacheFilePath = resolveCacheFilePath(projectDirectory);
|
|
58611
|
+
const persistedCache = readPersistedCache(cacheFilePath);
|
|
58612
|
+
const entries = /* @__PURE__ */ new Map();
|
|
58613
|
+
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
58614
|
+
const persist = () => {
|
|
58615
|
+
writePersistedCache(cacheFilePath, {
|
|
58616
|
+
version: 3,
|
|
58617
|
+
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
58618
|
+
});
|
|
58619
|
+
};
|
|
58620
|
+
return {
|
|
58621
|
+
lookup: (key) => entries.get(key)?.payload ?? null,
|
|
58622
|
+
store: (key, payload) => {
|
|
58623
|
+
entries.set(key, {
|
|
58624
|
+
key,
|
|
58625
|
+
createdAtMs: Date.now(),
|
|
58626
|
+
payload
|
|
58627
|
+
});
|
|
58628
|
+
persist();
|
|
58629
|
+
}
|
|
58630
|
+
};
|
|
58631
|
+
};
|
|
58632
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
|
|
58633
|
+
//#endregion
|
|
57775
58634
|
//#region src/cli/utils/should-block-ci.ts
|
|
57776
58635
|
const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
57777
58636
|
if (blockingLevel === "none") return false;
|
|
@@ -57813,6 +58672,63 @@ const readEnvBoolean = (name) => {
|
|
|
57813
58672
|
if (value === void 0) return null;
|
|
57814
58673
|
return value.toLowerCase() === "true" || value === "1";
|
|
57815
58674
|
};
|
|
58675
|
+
const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
|
|
58676
|
+
const resolveDeadCodeReuseRatio = (result) => {
|
|
58677
|
+
if (result.deadCodeCacheHit === true) return 1;
|
|
58678
|
+
const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
|
|
58679
|
+
if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
|
|
58680
|
+
return result.deadCodeCacheHit === false ? 0 : null;
|
|
58681
|
+
};
|
|
58682
|
+
/**
|
|
58683
|
+
* One queryable cache temperature per scan, derived from the whole stack:
|
|
58684
|
+
*
|
|
58685
|
+
* - `"turbo"` — whole-repo scan-result replay (the explicit
|
|
58686
|
+
* `wholeRepoCacheHit` flag from the CLI's cachedPayload
|
|
58687
|
+
* branch; no scan work ran).
|
|
58688
|
+
* - `"warm"` — any incremental reuse: per-file lint hits, sidecar
|
|
58689
|
+
* replays, a dead-code whole-result hit, or dead-code
|
|
58690
|
+
* summary-cache hits.
|
|
58691
|
+
* - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
|
|
58692
|
+
* off-switch is on. Granular per-cache opt-outs
|
|
58693
|
+
* (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
|
|
58694
|
+
* since the other subsystems stay live.
|
|
58695
|
+
* - `"cold"` — caches on, zero reuse (first scan / everything changed).
|
|
58696
|
+
*
|
|
58697
|
+
* `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
|
|
58698
|
+
* the subsystem reuse fractions known this run (lint hit ratio, sidecar
|
|
58699
|
+
* replay ratio, dead-code reuse), skipping subsystems that never consulted a
|
|
58700
|
+
* cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
|
|
58701
|
+
* unweighted — the per-subsystem dims stay the precise signal; warmth is the
|
|
58702
|
+
* p50/p90-able summary. Emitted only on the success path.
|
|
58703
|
+
*/
|
|
58704
|
+
const buildCacheAttributes = (input) => {
|
|
58705
|
+
if (input.result === void 0) return {};
|
|
58706
|
+
if (input.wholeRepoCacheHit) return withNamespace("cache", {
|
|
58707
|
+
wholeRepoHit: true,
|
|
58708
|
+
temperature: "turbo",
|
|
58709
|
+
warmth: 1
|
|
58710
|
+
});
|
|
58711
|
+
const result = input.result;
|
|
58712
|
+
const subsystemReuseRatios = [
|
|
58713
|
+
ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
|
|
58714
|
+
ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
|
|
58715
|
+
resolveDeadCodeReuseRatio(result)
|
|
58716
|
+
];
|
|
58717
|
+
let knownSubsystemCount = 0;
|
|
58718
|
+
let reuseRatioSum = 0;
|
|
58719
|
+
for (const reuseRatio of subsystemReuseRatios) {
|
|
58720
|
+
if (reuseRatio === null) continue;
|
|
58721
|
+
knownSubsystemCount += 1;
|
|
58722
|
+
reuseRatioSum += reuseRatio;
|
|
58723
|
+
}
|
|
58724
|
+
const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
|
|
58725
|
+
const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
|
|
58726
|
+
return withNamespace("cache", {
|
|
58727
|
+
wholeRepoHit: input.wholeRepoCacheHit ?? null,
|
|
58728
|
+
temperature,
|
|
58729
|
+
warmth
|
|
58730
|
+
});
|
|
58731
|
+
};
|
|
57816
58732
|
const resolveVersionPin = (versionInput) => {
|
|
57817
58733
|
if (versionInput === void 0 || versionInput.trim() === "") return null;
|
|
57818
58734
|
if (versionInput === "latest") return "latest";
|
|
@@ -57918,11 +58834,17 @@ const buildOutcomeAttributes = (input) => {
|
|
|
57918
58834
|
deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
|
|
57919
58835
|
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
57920
58836
|
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
57921
|
-
cacheHitRatio:
|
|
58837
|
+
cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
|
|
58838
|
+
sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
|
|
58839
|
+
sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
|
|
58840
|
+
sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
|
|
57922
58841
|
}),
|
|
57923
58842
|
...withNamespace("deadCode", {
|
|
57924
58843
|
failed: input.didDeadCodeFail ?? null,
|
|
57925
|
-
overlapped: input.deadCodeOverlapped ?? null
|
|
58844
|
+
overlapped: input.deadCodeOverlapped ?? null,
|
|
58845
|
+
cacheHit: result.deadCodeCacheHit ?? null,
|
|
58846
|
+
summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
|
|
58847
|
+
summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
|
|
57926
58848
|
}),
|
|
57927
58849
|
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
|
|
57928
58850
|
...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
|
|
@@ -57982,7 +58904,7 @@ const buildScanAttributes = (input) => {
|
|
|
57982
58904
|
* Projects a scan into the namespaced attribute set for its root span — the
|
|
57983
58905
|
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
57984
58906
|
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
57985
|
-
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
58907
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
|
|
57986
58908
|
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
57987
58909
|
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
57988
58910
|
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
@@ -57996,7 +58918,8 @@ const buildScanAttributes = (input) => {
|
|
|
57996
58918
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
57997
58919
|
...buildScanAttributes(input),
|
|
57998
58920
|
...buildActionAttributes(),
|
|
57999
|
-
...buildOutcomeAttributes(input)
|
|
58921
|
+
...buildOutcomeAttributes(input),
|
|
58922
|
+
...buildCacheAttributes(input)
|
|
58000
58923
|
});
|
|
58001
58924
|
/**
|
|
58002
58925
|
* Stamps the wide-event attributes onto the run's root span. A guarded no-op
|
|
@@ -59206,196 +60129,6 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
59206
60129
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
59207
60130
|
};
|
|
59208
60131
|
//#endregion
|
|
59209
|
-
//#region src/cli/utils/scan-result-cache.ts
|
|
59210
|
-
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
59211
|
-
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
59212
|
-
"oxlint",
|
|
59213
|
-
"oxlint/package.json",
|
|
59214
|
-
"oxlint-plugin-react-doctor",
|
|
59215
|
-
"deslop-js/package.json",
|
|
59216
|
-
"eslint-plugin-react-hooks/package.json"
|
|
59217
|
-
];
|
|
59218
|
-
const bundledRequire = createRequire(import.meta.url);
|
|
59219
|
-
const normalizeForStableJson = (value) => {
|
|
59220
|
-
if (value === null) return null;
|
|
59221
|
-
if (value === void 0) return void 0;
|
|
59222
|
-
const valueType = typeof value;
|
|
59223
|
-
if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
|
|
59224
|
-
if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
|
|
59225
|
-
if (Array.isArray(value)) return value.map((item) => {
|
|
59226
|
-
const normalized = normalizeForStableJson(item);
|
|
59227
|
-
return normalized === void 0 ? null : normalized;
|
|
59228
|
-
});
|
|
59229
|
-
const prototype = Object.getPrototypeOf(value);
|
|
59230
|
-
if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
|
|
59231
|
-
if (!isRecord(value)) throw new Error("Unsupported cache key object");
|
|
59232
|
-
const normalizedRecord = {};
|
|
59233
|
-
for (const key of Object.keys(value).sort()) {
|
|
59234
|
-
const normalized = normalizeForStableJson(value[key]);
|
|
59235
|
-
if (normalized !== void 0) normalizedRecord[key] = normalized;
|
|
59236
|
-
}
|
|
59237
|
-
return normalizedRecord;
|
|
59238
|
-
};
|
|
59239
|
-
const stringifyStableJson = (value) => {
|
|
59240
|
-
try {
|
|
59241
|
-
return JSON.stringify(normalizeForStableJson(value));
|
|
59242
|
-
} catch {
|
|
59243
|
-
return null;
|
|
59244
|
-
}
|
|
59245
|
-
};
|
|
59246
|
-
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
59247
|
-
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
59248
|
-
const isWorktreeClean = (projectDirectory) => {
|
|
59249
|
-
const status = runGit(projectDirectory, [
|
|
59250
|
-
"status",
|
|
59251
|
-
"--porcelain=v1",
|
|
59252
|
-
"--untracked-files=normal"
|
|
59253
|
-
]);
|
|
59254
|
-
return status !== null && status.length === 0;
|
|
59255
|
-
};
|
|
59256
|
-
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
59257
|
-
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
59258
|
-
if (output === null) return true;
|
|
59259
|
-
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
59260
|
-
};
|
|
59261
|
-
const resolveCacheFilePath = (projectDirectory) => {
|
|
59262
|
-
const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
|
|
59263
|
-
if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
|
|
59264
|
-
const projectHash = hashString(projectDirectory).slice(0, 16);
|
|
59265
|
-
return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
|
|
59266
|
-
};
|
|
59267
|
-
const readPersistedCache = (cacheFilePath) => {
|
|
59268
|
-
try {
|
|
59269
|
-
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
59270
|
-
if (!isRecord(parsed) || parsed.version !== 3) return {
|
|
59271
|
-
version: 3,
|
|
59272
|
-
entries: []
|
|
59273
|
-
};
|
|
59274
|
-
if (!Array.isArray(parsed.entries)) return {
|
|
59275
|
-
version: 3,
|
|
59276
|
-
entries: []
|
|
59277
|
-
};
|
|
59278
|
-
const entries = [];
|
|
59279
|
-
for (const entry of parsed.entries) {
|
|
59280
|
-
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
59281
|
-
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
59282
|
-
entries.push(entry);
|
|
59283
|
-
}
|
|
59284
|
-
return {
|
|
59285
|
-
version: 3,
|
|
59286
|
-
entries
|
|
59287
|
-
};
|
|
59288
|
-
} catch {
|
|
59289
|
-
return {
|
|
59290
|
-
version: 3,
|
|
59291
|
-
entries: []
|
|
59292
|
-
};
|
|
59293
|
-
}
|
|
59294
|
-
};
|
|
59295
|
-
const writePersistedCache = (cacheFilePath, cache) => {
|
|
59296
|
-
try {
|
|
59297
|
-
fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
|
|
59298
|
-
const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
59299
|
-
fs.writeFileSync(tempPath, JSON.stringify(cache));
|
|
59300
|
-
fs.renameSync(tempPath, cacheFilePath);
|
|
59301
|
-
} catch {
|
|
59302
|
-
return;
|
|
59303
|
-
}
|
|
59304
|
-
};
|
|
59305
|
-
const isScanResultCacheDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
|
|
59306
|
-
const resolveProjectIdentity = (projectDirectory) => {
|
|
59307
|
-
try {
|
|
59308
|
-
return fs.realpathSync.native(projectDirectory);
|
|
59309
|
-
} catch {
|
|
59310
|
-
return path.resolve(projectDirectory);
|
|
59311
|
-
}
|
|
59312
|
-
};
|
|
59313
|
-
const fileFingerprint = (filePath) => {
|
|
59314
|
-
try {
|
|
59315
|
-
const stat = fs.statSync(filePath);
|
|
59316
|
-
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
59317
|
-
} catch {
|
|
59318
|
-
return null;
|
|
59319
|
-
}
|
|
59320
|
-
};
|
|
59321
|
-
const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
59322
|
-
const fingerprints = [];
|
|
59323
|
-
if (nodeBinaryPath !== null) {
|
|
59324
|
-
const fingerprint = fileFingerprint(nodeBinaryPath);
|
|
59325
|
-
if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
|
|
59326
|
-
}
|
|
59327
|
-
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
59328
|
-
const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
|
|
59329
|
-
if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
|
|
59330
|
-
} catch {
|
|
59331
|
-
continue;
|
|
59332
|
-
}
|
|
59333
|
-
return fingerprints;
|
|
59334
|
-
};
|
|
59335
|
-
const buildScanResultCacheKey = (input) => {
|
|
59336
|
-
if (isScanResultCacheDisabled()) return null;
|
|
59337
|
-
if (!isWorktreeClean(input.projectDirectory)) return null;
|
|
59338
|
-
if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
|
|
59339
|
-
const headSha = readHeadSha(input.projectDirectory);
|
|
59340
|
-
if (headSha === null) return null;
|
|
59341
|
-
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
59342
|
-
const cacheKeyJson = stringifyStableJson({
|
|
59343
|
-
schemaVersion: 3,
|
|
59344
|
-
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
59345
|
-
headSha,
|
|
59346
|
-
reactDoctorVersion: input.version,
|
|
59347
|
-
nodeVersion: process.version,
|
|
59348
|
-
toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
|
|
59349
|
-
configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
|
|
59350
|
-
hasConfigOverride: input.hasConfigOverride,
|
|
59351
|
-
configSourceDirectory: input.configSourceDirectory,
|
|
59352
|
-
userConfig: input.userConfig,
|
|
59353
|
-
engineOptions: {
|
|
59354
|
-
lint: input.options.lint,
|
|
59355
|
-
deadCode: input.options.deadCode,
|
|
59356
|
-
includePaths: [...input.options.includePaths].sort(),
|
|
59357
|
-
customRulesOnly: input.options.customRulesOnly,
|
|
59358
|
-
respectInlineDisables: input.options.respectInlineDisables,
|
|
59359
|
-
warnings: input.options.warnings,
|
|
59360
|
-
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
59361
|
-
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
59362
|
-
concurrency: input.options.concurrency,
|
|
59363
|
-
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
59364
|
-
baselineRef: input.options.baseline?.ref,
|
|
59365
|
-
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
59366
|
-
noScore: input.options.noScore,
|
|
59367
|
-
isCi: input.options.isCi,
|
|
59368
|
-
suppressRendering: input.options.suppressRendering,
|
|
59369
|
-
supplyChainManifestChanged: input.options.supplyChainManifestChanged
|
|
59370
|
-
}
|
|
59371
|
-
});
|
|
59372
|
-
return cacheKeyJson === null ? null : hashString(cacheKeyJson);
|
|
59373
|
-
};
|
|
59374
|
-
const createScanResultCache = (projectDirectory) => {
|
|
59375
|
-
const cacheFilePath = resolveCacheFilePath(projectDirectory);
|
|
59376
|
-
const persistedCache = readPersistedCache(cacheFilePath);
|
|
59377
|
-
const entries = /* @__PURE__ */ new Map();
|
|
59378
|
-
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
59379
|
-
const persist = () => {
|
|
59380
|
-
writePersistedCache(cacheFilePath, {
|
|
59381
|
-
version: 3,
|
|
59382
|
-
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
59383
|
-
});
|
|
59384
|
-
};
|
|
59385
|
-
return {
|
|
59386
|
-
lookup: (key) => entries.get(key)?.payload ?? null,
|
|
59387
|
-
store: (key, payload) => {
|
|
59388
|
-
entries.set(key, {
|
|
59389
|
-
key,
|
|
59390
|
-
createdAtMs: Date.now(),
|
|
59391
|
-
payload
|
|
59392
|
-
});
|
|
59393
|
-
persist();
|
|
59394
|
-
}
|
|
59395
|
-
};
|
|
59396
|
-
};
|
|
59397
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
|
|
59398
|
-
//#endregion
|
|
59399
60132
|
//#region src/inspect.ts
|
|
59400
60133
|
const silentConsole = makeNoopConsole();
|
|
59401
60134
|
const runConsole = (effect) => {
|
|
@@ -59572,7 +60305,7 @@ const runBaselineComparison = async (params) => {
|
|
|
59572
60305
|
suppressScanSummary: true,
|
|
59573
60306
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged,
|
|
59574
60307
|
deadlineEpochMs: params.deadlineEpochMs ?? void 0
|
|
59575
|
-
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
60308
|
+
}, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
|
|
59576
60309
|
if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
|
|
59577
60310
|
const delta = computeDiagnosticDelta({
|
|
59578
60311
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -59625,7 +60358,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59625
60358
|
startTime,
|
|
59626
60359
|
rootSentrySpan,
|
|
59627
60360
|
scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
59628
|
-
baselineDegraded
|
|
60361
|
+
baselineDegraded,
|
|
60362
|
+
wholeRepoCacheHit: true
|
|
59629
60363
|
});
|
|
59630
60364
|
recordOnboardingCompletion(options);
|
|
59631
60365
|
return result;
|
|
@@ -59734,8 +60468,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59734
60468
|
rootSentrySpan,
|
|
59735
60469
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
59736
60470
|
baselineDegraded,
|
|
60471
|
+
wholeRepoCacheHit: false,
|
|
59737
60472
|
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
59738
|
-
lintCacheTotalFileCount: output.lintCacheTotalFileCount
|
|
60473
|
+
lintCacheTotalFileCount: output.lintCacheTotalFileCount,
|
|
60474
|
+
lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
|
|
60475
|
+
lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
|
|
60476
|
+
deadCodeCacheHit: output.deadCodeCacheHit,
|
|
60477
|
+
deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
|
|
60478
|
+
deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
|
|
59739
60479
|
});
|
|
59740
60480
|
recordOnboardingCompletion(options);
|
|
59741
60481
|
return result;
|
|
@@ -59770,6 +60510,11 @@ const renderAndRecordScan = async (input) => {
|
|
|
59770
60510
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
59771
60511
|
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
59772
60512
|
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
60513
|
+
lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
|
|
60514
|
+
lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
|
|
60515
|
+
deadCodeCacheHit: input.deadCodeCacheHit ?? null,
|
|
60516
|
+
deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
|
|
60517
|
+
deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
|
|
59773
60518
|
baselineDelta: input.payload.baselineDelta
|
|
59774
60519
|
}), input.options.silent));
|
|
59775
60520
|
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
@@ -59794,6 +60539,7 @@ const renderAndRecordScan = async (input) => {
|
|
|
59794
60539
|
result,
|
|
59795
60540
|
mode: input.scanMode,
|
|
59796
60541
|
gateExempt: input.baselineDegraded,
|
|
60542
|
+
wholeRepoCacheHit: input.wholeRepoCacheHit,
|
|
59797
60543
|
didLintFail: input.payload.didLintFail,
|
|
59798
60544
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
59799
60545
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
@@ -59808,7 +60554,7 @@ const renderAndRecordScan = async (input) => {
|
|
|
59808
60554
|
return result;
|
|
59809
60555
|
};
|
|
59810
60556
|
const finalizeAndRender = (input) => gen(function* () {
|
|
59811
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
|
|
60557
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
|
|
59812
60558
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
59813
60559
|
didLintFail,
|
|
59814
60560
|
lintFailureReason,
|
|
@@ -59832,6 +60578,15 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
59832
60578
|
lintCacheHitFileCount,
|
|
59833
60579
|
lintCacheTotalFileCount
|
|
59834
60580
|
} : {},
|
|
60581
|
+
...lintSidecarTotalFileCount !== null ? {
|
|
60582
|
+
lintSidecarReplayedFileCount,
|
|
60583
|
+
lintSidecarTotalFileCount
|
|
60584
|
+
} : {},
|
|
60585
|
+
...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
|
|
60586
|
+
...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
|
|
60587
|
+
deadCodeSummaryCacheHits,
|
|
60588
|
+
deadCodeSummaryCacheMisses
|
|
60589
|
+
} : {},
|
|
59835
60590
|
...baselineDelta ? { baselineDelta } : {}
|
|
59836
60591
|
});
|
|
59837
60592
|
if (options.suppressRendering) return buildResult();
|
|
@@ -62241,6 +62996,11 @@ const finalizeScans = (input) => {
|
|
|
62241
62996
|
const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
|
|
62242
62997
|
const baselineDegraded = input.baselineIntended && !baselineComputed;
|
|
62243
62998
|
const mode = baselineDegraded ? "diff" : input.mode;
|
|
62999
|
+
const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
|
|
63000
|
+
if (input.completedScans.length > 0 && !isReactDetected) {
|
|
63001
|
+
recordCount(METRIC.scanNoReactDetected, 1);
|
|
63002
|
+
cliLogger.warn(`No React project detected at ${input.resolvedDirectory} — React rules were gated off; this is not the same as a clean scan.`);
|
|
63003
|
+
}
|
|
62244
63004
|
const jsonCompletedScans = filterCompletedScansByCategories(input.completedScans, input.categoryFilters);
|
|
62245
63005
|
if (input.isJsonMode) {
|
|
62246
63006
|
const baseline = baselineComputed && baselineDeltas.length > 0 ? {
|
|
@@ -63678,4 +64438,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
63678
64438
|
export {};
|
|
63679
64439
|
|
|
63680
64440
|
//# sourceMappingURL=cli.js.map
|
|
63681
|
-
//# debugId=
|
|
64441
|
+
//# debugId=e920d892-391e-5c2c-85c7-689bd4fe9775
|