react-doctor 0.6.3 → 0.7.0-dev.574cba6

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 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]="328318eb-6af2-5007-b816-51c953a96047")}catch(e){}}();
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]="a88b7ed4-e1d2-587a-a826-36b16313d032")}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";
@@ -36128,10 +36129,15 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36128
36129
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36129
36130
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36130
36131
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36133
+ const CORE_PACKAGE_VERSION = "0.7.0";
36134
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36135
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36131
36136
  const SUPPLY_CHAIN_PLUGIN = "socket";
36132
36137
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36133
36138
  const SUPPLY_CHAIN_CATEGORY = "Security";
36134
36139
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36140
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36135
36141
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36136
36142
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36137
36143
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -38812,6 +38818,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
38812
38818
  if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
38813
38819
  return true;
38814
38820
  } }) {};
38821
+ /**
38822
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
38823
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
38824
+ * diagnostics for every file whose dependency fingerprint (the probe set the
38825
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
38826
+ * rest. Only reachable when the per-file lint cache itself is on (the
38827
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
38828
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
38829
+ *
38830
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38831
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
38832
+ * per-file cache but run the always-fresh sidecar over every hit
38833
+ * (the pre-cache behavior).
38834
+ *
38835
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
38836
+ */
38837
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
38838
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38839
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
38840
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38841
+ if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
38842
+ return true;
38843
+ } }) {};
38844
+ /**
38845
+ * Whether the whole-project dead-code result cache
38846
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
38847
+ * whose inputs (source tree, manifests, configs, analyzer version) are
38848
+ * unchanged replays the stored diagnostics instead of re-running the
38849
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
38850
+ *
38851
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38852
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
38853
+ *
38854
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
38855
+ */
38856
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
38857
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38858
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
38859
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38860
+ if (CACHE_DISABLED_VALUES$1.has(noDeadCodeCache)) return false;
38861
+ return true;
38862
+ } }) {};
38815
38863
  const DIAGNOSTIC_SURFACES = [
38816
38864
  "cli",
38817
38865
  "prComment",
@@ -40544,6 +40592,146 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40544
40592
  return [...seen].filter((pattern) => pattern.length > 0);
40545
40593
  };
40546
40594
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40595
+ const atomicWriteJson = (filePath, value) => {
40596
+ try {
40597
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
40598
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
40599
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
40600
+ NFS.renameSync(temporaryPath, filePath);
40601
+ } catch {
40602
+ return;
40603
+ }
40604
+ };
40605
+ const failOpenReadJson = (filePath, fallback) => {
40606
+ try {
40607
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
40608
+ } catch {
40609
+ return fallback;
40610
+ }
40611
+ };
40612
+ const hashFileContents = (filePath) => {
40613
+ try {
40614
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
40615
+ } catch {
40616
+ return null;
40617
+ }
40618
+ };
40619
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40620
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40621
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40622
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40623
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40624
+ /**
40625
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
40626
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
40627
+ * verified against the tree it started from.
40628
+ */
40629
+ const collectAnalyzedFileStats = (rootDirectory) => {
40630
+ const statByRelativePath = /* @__PURE__ */ new Map();
40631
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40632
+ if (!isFingerprintedFile(name)) continue;
40633
+ try {
40634
+ const fileStat = NFS.statSync(absolutePath);
40635
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
40636
+ statByRelativePath.set(relativePath, {
40637
+ mtimeMs: fileStat.mtimeMs,
40638
+ size: fileStat.size
40639
+ });
40640
+ } catch {}
40641
+ }
40642
+ return statByRelativePath;
40643
+ };
40644
+ const bundledRequire$2 = createRequire(import.meta.url);
40645
+ const resolveDeslopVersion = () => {
40646
+ try {
40647
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
40648
+ return isRecord$1(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
40649
+ } catch {
40650
+ return "unknown";
40651
+ }
40652
+ };
40653
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40654
+ schemaVersion: 2,
40655
+ coreVersion: input.coreVersion,
40656
+ deslopVersion: resolveDeslopVersion(),
40657
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40658
+ entryPatterns: input.entryPatterns,
40659
+ ignorePatterns: input.ignorePatterns,
40660
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
40661
+ })).digest("hex");
40662
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
40663
+ const decodeCachedDiagnostics = (raw) => {
40664
+ try {
40665
+ for (const entry of raw) validateDiagnostic$2(entry);
40666
+ return raw;
40667
+ } catch {
40668
+ return null;
40669
+ }
40670
+ };
40671
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
40672
+ const lookupDeadCodeResultCache = (input) => {
40673
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
40674
+ const persisted = failOpenReadJson(cacheFilePath, null);
40675
+ if (persisted === null || !isRecord$1(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord$1(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
40676
+ const storedFileEntries = Object.entries(persisted.files);
40677
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
40678
+ const repairedFiles = {};
40679
+ let repairedCount = 0;
40680
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
40681
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
40682
+ const currentStat = input.currentFileStats.get(relativePath);
40683
+ if (currentStat === void 0) return null;
40684
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
40685
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
40686
+ repairedFiles[relativePath] = storedIdentity;
40687
+ continue;
40688
+ }
40689
+ if (currentStat.size !== storedSize) return null;
40690
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
40691
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
40692
+ repairedFiles[relativePath] = [
40693
+ currentStat.mtimeMs,
40694
+ currentStat.size,
40695
+ storedContentHash
40696
+ ];
40697
+ repairedCount += 1;
40698
+ }
40699
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
40700
+ if (diagnostics === null) return null;
40701
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
40702
+ version: 2,
40703
+ key: input.cacheKey,
40704
+ files: repairedFiles,
40705
+ diagnostics: persisted.diagnostics
40706
+ });
40707
+ return diagnostics;
40708
+ };
40709
+ const storeDeadCodeResultCache = (input) => {
40710
+ const persistedFiles = {};
40711
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
40712
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
40713
+ const contentHash = hashFileContents(absolutePath);
40714
+ if (contentHash === null) return;
40715
+ let currentStat;
40716
+ try {
40717
+ currentStat = NFS.statSync(absolutePath);
40718
+ } catch {
40719
+ return;
40720
+ }
40721
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
40722
+ persistedFiles[relativePath] = [
40723
+ snapshotStat.mtimeMs,
40724
+ snapshotStat.size,
40725
+ contentHash
40726
+ ];
40727
+ }
40728
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40729
+ version: 2,
40730
+ key: input.cacheKey,
40731
+ files: persistedFiles,
40732
+ diagnostics: input.diagnostics
40733
+ });
40734
+ };
40547
40735
  const readSystemFacts = () => ({
40548
40736
  availableCores: os.availableParallelism(),
40549
40737
  totalMemoryBytes: os.totalmem(),
@@ -40612,6 +40800,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40612
40800
  releaseSlot();
40613
40801
  }
40614
40802
  };
40803
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
40804
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
40805
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
40806
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
40807
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
40808
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
40809
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
40810
+ };
40811
+ const resolveUserCacheScope = () => {
40812
+ try {
40813
+ return String(process.getuid?.() ?? os.userInfo().username);
40814
+ } catch {
40815
+ return "shared";
40816
+ }
40817
+ };
40615
40818
  /**
40616
40819
  * Resolves a path to its canonical, symlink-free form, falling back to
40617
40820
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40665,6 +40868,14 @@ process.stdin.on("end", () => {
40665
40868
  circularDependencies: result.circularDependencies.map((cycle) => ({
40666
40869
  files: cycle.files,
40667
40870
  })),
40871
+ ...(result.incrementalCacheStats
40872
+ ? {
40873
+ summaryCacheStats: {
40874
+ hits: result.incrementalCacheStats.summaryHits,
40875
+ misses: result.incrementalCacheStats.summaryMisses,
40876
+ },
40877
+ }
40878
+ : {}),
40668
40879
  });
40669
40880
 
40670
40881
  const serializeError = (error) =>
@@ -40688,6 +40899,9 @@ process.stdin.on("end", () => {
40688
40899
  ...(workerInput.ignorePatterns.length > 0
40689
40900
  ? { ignorePatterns: workerInput.ignorePatterns }
40690
40901
  : {}),
40902
+ ...(workerInput.incrementalCachePath
40903
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
40904
+ : {}),
40691
40905
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
40692
40906
  // unusedDependencies, circularDependencies). Everything else deslop can compute
40693
40907
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -40698,12 +40912,18 @@ process.stdin.on("end", () => {
40698
40912
  // are the single most expensive pass — duplicate-block detection alone was
40699
40913
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
40700
40914
  // speedup on a large repo.
40701
- // Both are provably safe: the consumed graph findings are computed by their own
40915
+ // - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
40916
+ // simplifiable functions, identity wrappers, …) — ~120 ms of discarded
40917
+ // output per scan, and skipping them lets the incremental cache drop the
40918
+ // DRY-pattern summary fields (the largest slice of the cache file).
40919
+ // All are provably safe: the consumed graph findings are computed by their own
40702
40920
  // detectors, independent of these passes (confirmed byte-identical on
40703
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
40704
- // needs it for path-alias resolution in the import graph.
40921
+ // excalidraw + mui-material + sentry; re-verified on sentry after the
40922
+ // reportRedundancy flip). tsConfigPath stays — the module resolver needs it
40923
+ // for path-alias resolution in the import graph.
40705
40924
  semantic: { enabled: false },
40706
40925
  reportCodeQuality: false,
40926
+ reportRedundancy: false,
40707
40927
  };
40708
40928
  const result = await analyze(defineConfig(config));
40709
40929
  emit({ ok: true, result: normalizeResult(result) });
@@ -40787,13 +41007,23 @@ const parseCircularDependencies = (value) => {
40787
41007
  }
40788
41008
  return circularDependencies;
40789
41009
  };
41010
+ const parseSummaryCacheStats = (value) => {
41011
+ if (!isRecord$1(value)) return void 0;
41012
+ if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
41013
+ return {
41014
+ hits: value.hits,
41015
+ misses: value.misses
41016
+ };
41017
+ };
40790
41018
  const parseDeadCodeWorkerResult = (value) => {
40791
41019
  if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
41020
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
40792
41021
  return {
40793
41022
  unusedFiles: parseUnusedFiles(value.unusedFiles),
40794
41023
  unusedExports: parseUnusedExports(value.unusedExports),
40795
41024
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
40796
- circularDependencies: parseCircularDependencies(value.circularDependencies)
41025
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
41026
+ ...summaryCacheStats ? { summaryCacheStats } : {}
40797
41027
  };
40798
41028
  };
40799
41029
  const parseDeadCodeWorkerError = (value) => {
@@ -40907,17 +41137,41 @@ const checkDeadCode = async (options) => {
40907
41137
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40908
41138
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40909
41139
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
41140
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
41141
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
41142
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
41143
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
41144
+ rootDirectory,
41145
+ entryPatterns,
41146
+ ignorePatterns,
41147
+ tsConfigPath,
41148
+ deslopJsModuleSpecifier,
41149
+ coreVersion: CORE_PACKAGE_VERSION
41150
+ });
41151
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
41152
+ const cachedDiagnostics = lookupDeadCodeResultCache({
41153
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41154
+ cacheKey,
41155
+ rootDirectory,
41156
+ currentFileStats: fileStatsSnapshot
41157
+ });
41158
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
41159
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41160
+ }
41161
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
40910
41162
  const spawnAndRun = () => {
40911
41163
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40912
41164
  rootDirectory,
40913
41165
  entryPatterns,
40914
- tsConfigPath: resolveTsConfigPath(rootDirectory),
41166
+ tsConfigPath,
40915
41167
  ignorePatterns,
40916
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40917
- parseConcurrency: options.parseConcurrency
41168
+ deslopJsModuleSpecifier,
41169
+ parseConcurrency: options.parseConcurrency,
41170
+ incrementalCachePath
40918
41171
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40919
41172
  };
40920
41173
  const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
41174
+ if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
40921
41175
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40922
41176
  const diagnostics = [];
40923
41177
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -40974,6 +41228,13 @@ const checkDeadCode = async (options) => {
40974
41228
  category: DEAD_CODE_CATEGORY
40975
41229
  });
40976
41230
  }
41231
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
41232
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41233
+ cacheKey,
41234
+ rootDirectory,
41235
+ snapshotFileStats: fileStatsSnapshot,
41236
+ diagnostics
41237
+ });
40977
41238
  return diagnostics;
40978
41239
  };
40979
41240
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -41076,12 +41337,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
41076
41337
  */
41077
41338
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
41078
41339
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
41340
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
41079
41341
  return yield* tryPromise({
41080
41342
  try: (signal) => checkDeadCode({
41081
41343
  rootDirectory: input.rootDirectory,
41082
41344
  parseConcurrency: input.parseConcurrency,
41083
41345
  workerTimeoutMs: input.workerTimeoutMs,
41084
- abortSignal: signal
41346
+ abortSignal: signal,
41347
+ cacheEnabled,
41348
+ onCacheOutcome: input.onCacheOutcome,
41349
+ onSummaryCacheStats: input.onSummaryCacheStats
41085
41350
  }),
41086
41351
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
41087
41352
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -41951,7 +42216,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41951
42216
  }
41952
42217
  return enabled;
41953
42218
  };
41954
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
42219
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
41955
42220
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41956
42221
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41957
42222
  const jsPlugins = [];
@@ -41963,6 +42228,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
41963
42228
  if (!rule) continue;
41964
42229
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
41965
42230
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42231
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
41966
42232
  if (rule.scan !== void 0) continue;
41967
42233
  if (customRulesOnly && registryEntry.originallyExternal) continue;
41968
42234
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -42007,33 +42273,145 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42007
42273
  }
42008
42274
  };
42009
42275
  };
42010
- const atomicWriteJson = (filePath, value) => {
42276
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
42277
+ const decodeFileDiagnostics = (raw) => {
42278
+ if (!Array.isArray(raw)) return null;
42011
42279
  try {
42012
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
42013
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
42014
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
42015
- NFS.renameSync(temporaryPath, filePath);
42280
+ for (const entry of raw) validateDiagnostic$1(entry);
42281
+ return raw;
42016
42282
  } catch {
42017
- return;
42283
+ return null;
42018
42284
  }
42019
42285
  };
42020
- const failOpenReadJson = (filePath, fallback) => {
42021
- try {
42022
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
42023
- } catch {
42024
- return fallback;
42286
+ const emptyCache$1 = () => ({
42287
+ version: 1,
42288
+ rulesets: {}
42289
+ });
42290
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
42291
+ const entries = /* @__PURE__ */ new Map();
42292
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
42293
+ if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42294
+ const bucket = persisted.rulesets[rulesetHash];
42295
+ if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42296
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
42297
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
42298
+ if (decoded !== null) entries.set(fileKey, decoded);
42025
42299
  }
42300
+ return entries;
42301
+ };
42302
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42303
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42304
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
42305
+ return {
42306
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
42307
+ store: (fileKey, diagnostics) => {
42308
+ entries.delete(fileKey);
42309
+ entries.set(fileKey, diagnostics);
42310
+ },
42311
+ persist: () => {
42312
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
42313
+ const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42314
+ const existingBucket = rulesets[rulesetHash];
42315
+ const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42316
+ const ourFiles = {};
42317
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
42318
+ const cappedEntries = Object.entries({
42319
+ ...existingFiles,
42320
+ ...ourFiles
42321
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
42322
+ rulesets[rulesetHash] = {
42323
+ updatedAtMs: Date.now(),
42324
+ files: Object.fromEntries(cappedEntries)
42325
+ };
42326
+ atomicWriteJson(cacheFilePath, {
42327
+ version: 1,
42328
+ 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))
42329
+ });
42330
+ }
42331
+ };
42332
+ };
42333
+ const ABSENT_CONTENT_ANSWER = "absent";
42334
+ /**
42335
+ * Answers dependency probes for the sidecar lint cache — used both when
42336
+ * STORING an entry (turn the collector's probe paths into answers) and when
42337
+ * VERIFYING one (re-answer each stored probe against the current tree).
42338
+ * Store and verify must agree exactly, so both go through this one resolver.
42339
+ *
42340
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
42341
+ * (missing, a directory, permission failure). Source files reuse the
42342
+ * hashes the cache-partition loop already computed
42343
+ * (`contentHashByRelativePath`); everything else (package.json,
42344
+ * tsconfig, ignored files) is hashed on demand.
42345
+ * - `exists` — the existence class a module-resolution candidate probe
42346
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
42347
+ * `statSync` follows symlinks, matching the resolvers).
42348
+ *
42349
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
42350
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
42351
+ * tsconfig chain) repeat across many files' probe sets.
42352
+ */
42353
+ const createSidecarProbeAnswerResolver = (input) => {
42354
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
42355
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
42356
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
42357
+ const contentAnswer = (relativePath) => {
42358
+ const memoized = contentAnswerByPath.get(relativePath);
42359
+ if (memoized !== void 0) return memoized;
42360
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
42361
+ contentAnswerByPath.set(relativePath, answer);
42362
+ return answer;
42363
+ };
42364
+ const existsAnswer = (relativePath) => {
42365
+ const memoized = existsAnswerByPath.get(relativePath);
42366
+ if (memoized !== void 0) return memoized;
42367
+ let answer;
42368
+ try {
42369
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
42370
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
42371
+ } catch {
42372
+ answer = "none";
42373
+ }
42374
+ existsAnswerByPath.set(relativePath, answer);
42375
+ return answer;
42376
+ };
42377
+ return {
42378
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
42379
+ toRelativePath
42380
+ };
42026
42381
  };
42027
42382
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
42028
- const decodeFileDiagnostics = (raw) => {
42029
- if (!Array.isArray(raw)) return null;
42383
+ const isProbeKind = (value) => value === "content" || value === "exists";
42384
+ const decodeSidecarFileEntry = (raw) => {
42385
+ if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
42386
+ const probes = [];
42387
+ for (const rawProbe of raw.probes) {
42388
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
42389
+ const [kind, probePath, answer] = rawProbe;
42390
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
42391
+ probes.push({
42392
+ kind,
42393
+ path: probePath,
42394
+ answer
42395
+ });
42396
+ }
42030
42397
  try {
42031
- for (const entry of raw) validateDiagnostic(entry);
42032
- return raw;
42398
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
42033
42399
  } catch {
42034
42400
  return null;
42035
42401
  }
42402
+ return {
42403
+ probes,
42404
+ diagnostics: raw.diagnostics
42405
+ };
42036
42406
  };
42407
+ const encodeSidecarFileEntry = (entry) => ({
42408
+ probes: entry.probes.map((probe) => [
42409
+ probe.kind,
42410
+ probe.path,
42411
+ probe.answer
42412
+ ]),
42413
+ diagnostics: entry.diagnostics
42414
+ });
42037
42415
  const emptyCache = () => ({
42038
42416
  version: 1,
42039
42417
  rulesets: {}
@@ -42044,20 +42422,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42044
42422
  if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42045
42423
  const bucket = persisted.rulesets[rulesetHash];
42046
42424
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42047
- for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
42048
- const decoded = decodeFileDiagnostics(rawDiagnostics);
42425
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
42426
+ const decoded = decodeSidecarFileEntry(rawEntry);
42049
42427
  if (decoded !== null) entries.set(fileKey, decoded);
42050
42428
  }
42051
42429
  return entries;
42052
42430
  };
42053
- const createFileLintCache = (cacheDirectory, rulesetHash) => {
42054
- const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42431
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42432
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
42055
42433
  const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42056
42434
  return {
42057
42435
  lookup: (fileKey) => entries.get(fileKey) ?? null,
42058
- store: (fileKey, diagnostics) => {
42436
+ store: (fileKey, entry) => {
42059
42437
  entries.delete(fileKey);
42060
- entries.set(fileKey, diagnostics);
42438
+ entries.set(fileKey, entry);
42061
42439
  },
42062
42440
  persist: () => {
42063
42441
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
@@ -42065,7 +42443,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42065
42443
  const existingBucket = rulesets[rulesetHash];
42066
42444
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42067
42445
  const ourFiles = {};
42068
- for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
42446
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
42069
42447
  const cappedEntries = Object.entries({
42070
42448
  ...existingFiles,
42071
42449
  ...ourFiles
@@ -42081,7 +42459,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42081
42459
  }
42082
42460
  };
42083
42461
  };
42084
- const bundledRequire$2 = createRequire(import.meta.url);
42462
+ const bundledRequire$3 = createRequire(import.meta.url);
42085
42463
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
42086
42464
  const resolveChildNodeVersion = (nodeBinaryPath) => {
42087
42465
  if (nodeBinaryPath === process.execPath) return process.version;
@@ -42107,7 +42485,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
42107
42485
  const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
42108
42486
  const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
42109
42487
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
42110
- const packageJson = bundledRequire$2(specifier);
42488
+ const packageJson = bundledRequire$3(specifier);
42111
42489
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
42112
42490
  versions.push(`${specifier}=${version}`);
42113
42491
  } catch {
@@ -43080,13 +43458,6 @@ const validateRuleRegistration = () => {
43080
43458
  ].filter((entry) => entry !== null).join("; ");
43081
43459
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
43082
43460
  };
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
43461
  /**
43091
43462
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
43092
43463
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -43151,21 +43522,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
43151
43522
  }
43152
43523
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
43153
43524
  };
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
43525
  /**
43170
43526
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
43171
43527
  * the runner: once for the primary scan, once for the
@@ -43183,6 +43539,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
43183
43539
  NFS.closeSync(fileHandle);
43184
43540
  }
43185
43541
  };
43542
+ /**
43543
+ * Attributes diagnostics back to the file that produced them by the
43544
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
43545
+ * attributed — the path forms don't align, so the caller must skip its cache
43546
+ * store rather than risk caching a wrong empty result for a file that
43547
+ * actually had diagnostics.
43548
+ */
43549
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
43550
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
43551
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
43552
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
43553
+ for (const diagnostic of diagnostics) {
43554
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
43555
+ if (file === void 0) return null;
43556
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
43557
+ fileDiagnostics.push(diagnostic);
43558
+ diagnosticsByFile.set(file, fileDiagnostics);
43559
+ }
43560
+ return diagnosticsByFile;
43561
+ };
43562
+ /**
43563
+ * Collects each file's cross-file dependency probes (via the plugin's
43564
+ * collectors) with their current answers. Fail-open per file: an unreadable /
43565
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
43566
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
43567
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
43568
+ * unawaited before a lint pass and awaits it at store time, hiding the
43569
+ * in-process parse + resolution cost under the subprocess wait. The loop
43570
+ * yields on the cooperative time budget so the child's stdout handling (and
43571
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
43572
+ * the module-level probe recorder never sees interleaved use. Never rejects.
43573
+ */
43574
+ const collectSidecarProbesForFiles = async (input) => {
43575
+ const buildProbes = (file) => {
43576
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
43577
+ let trace;
43578
+ try {
43579
+ trace = collectCrossFileDependencyProbes({
43580
+ absoluteFilePath,
43581
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
43582
+ ruleIds: input.boundedSidecarRuleIds
43583
+ });
43584
+ } catch {
43585
+ return null;
43586
+ }
43587
+ if (trace === null) return null;
43588
+ const probes = [];
43589
+ for (const contentPath of trace.contentPaths) {
43590
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
43591
+ probes.push({
43592
+ kind: "content",
43593
+ path: relativePath,
43594
+ answer: input.probeAnswers.answerFor("content", relativePath)
43595
+ });
43596
+ }
43597
+ for (const existencePath of trace.existencePaths) {
43598
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
43599
+ probes.push({
43600
+ kind: "exists",
43601
+ path: relativePath,
43602
+ answer: input.probeAnswers.answerFor("exists", relativePath)
43603
+ });
43604
+ }
43605
+ return probes;
43606
+ };
43607
+ const probesByFile = /* @__PURE__ */ new Map();
43608
+ let collectSliceStartedAt = performance.now();
43609
+ for (const file of input.files) {
43610
+ probesByFile.set(file, buildProbes(file));
43611
+ if (performance.now() - collectSliceStartedAt >= 12) {
43612
+ await yieldToEventLoop();
43613
+ collectSliceStartedAt = performance.now();
43614
+ }
43615
+ }
43616
+ return probesByFile;
43617
+ };
43618
+ /**
43619
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
43620
+ * the next scan can replay instead of re-linting.
43621
+ */
43622
+ const storeSidecarEntries = (input) => {
43623
+ let didStoreAnyEntry = false;
43624
+ for (const pass of input.storablePasses) {
43625
+ if (!pass.isTrusted) continue;
43626
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
43627
+ if (diagnosticsByFile === null) continue;
43628
+ for (const file of pass.files) {
43629
+ const cacheKey = input.cacheKeyByFile.get(file);
43630
+ if (cacheKey === void 0) continue;
43631
+ const probes = pass.probesByFile.get(file);
43632
+ if (probes === void 0 || probes === null) continue;
43633
+ input.sidecarCache.store(cacheKey, {
43634
+ probes,
43635
+ diagnostics: diagnosticsByFile.get(file) ?? []
43636
+ });
43637
+ didStoreAnyEntry = true;
43638
+ }
43639
+ }
43640
+ if (didStoreAnyEntry) input.sidecarCache.persist();
43641
+ };
43186
43642
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
43187
43643
  /**
43188
43644
  * Detects an oxlint config-load crash caused by the optional
@@ -43224,7 +43680,7 @@ const reactHooksJsPluginDropNote = (error) => {
43224
43680
  * 6. always restore disable directives + clean up the temp dir
43225
43681
  */
43226
43682
  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;
43683
+ 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
43684
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43229
43685
  const severityControls = buildRuleSeverityControls(userConfig);
43230
43686
  validateRuleRegistration();
@@ -43242,7 +43698,8 @@ const runOxlint = async (options) => {
43242
43698
  severityControls,
43243
43699
  userPlugins,
43244
43700
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
43245
- ruleSelection: overrides.ruleSelection
43701
+ ruleSelection: overrides.ruleSelection,
43702
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
43246
43703
  });
43247
43704
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
43248
43705
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -43345,15 +43802,18 @@ const runOxlint = async (options) => {
43345
43802
  });
43346
43803
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
43347
43804
  const cacheKeyByFile = /* @__PURE__ */ new Map();
43805
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
43348
43806
  const missFiles = [];
43349
43807
  const replayedDiagnostics = [];
43350
43808
  let hashSliceStartedAt = performance.now();
43351
43809
  for (const candidateFile of candidateFiles) {
43352
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
43810
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
43811
+ const contentHash = hashFileContents(absoluteCandidatePath);
43353
43812
  if (contentHash === null) {
43354
43813
  missFiles.push(candidateFile);
43355
43814
  continue;
43356
43815
  }
43816
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
43357
43817
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
43358
43818
  cacheKeyByFile.set(candidateFile, cacheKey);
43359
43819
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -43365,43 +43825,111 @@ const runOxlint = async (options) => {
43365
43825
  }
43366
43826
  }
43367
43827
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
43828
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
43829
+ extendsPaths: [],
43830
+ ruleSelection: "sidecar"
43831
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
43832
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43833
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
43834
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43835
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
43836
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
43837
+ config: buildConfig({
43838
+ extendsPaths: [],
43839
+ ruleSelection: "sidecar",
43840
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
43841
+ }),
43842
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
43843
+ ignorePatterns: combinedPatterns,
43844
+ tsconfigContent
43845
+ })) : null;
43846
+ const probeAnswers = createSidecarProbeAnswerResolver({
43847
+ rootDirectory,
43848
+ contentHashByRelativePath
43849
+ });
43850
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43851
+ files: missFiles,
43852
+ rootDirectory,
43853
+ boundedSidecarRuleIds,
43854
+ probeAnswers
43855
+ });
43368
43856
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
43369
43857
  extendsPaths: [],
43370
43858
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
43371
43859
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
43372
43860
  const missFileSet = new Set(missFiles);
43373
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
43861
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
43862
+ const sidecarReplayedDiagnostics = [];
43863
+ const sidecarLintFiles = [];
43864
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
43865
+ else {
43866
+ let verifySliceStartedAt = performance.now();
43867
+ for (const hitFile of hitFiles) {
43868
+ const cacheKey = cacheKeyByFile.get(hitFile);
43869
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
43870
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
43871
+ else sidecarLintFiles.push(hitFile);
43872
+ if (performance.now() - verifySliceStartedAt >= 12) {
43873
+ await yieldToEventLoop();
43874
+ verifySliceStartedAt = performance.now();
43875
+ }
43876
+ }
43877
+ }
43878
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43879
+ files: sidecarLintFiles,
43880
+ rootDirectory,
43881
+ boundedSidecarRuleIds,
43882
+ probeAnswers
43883
+ });
43884
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
43885
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
43886
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
43374
43887
  extendsPaths: [],
43375
- ruleSelection: "sidecar"
43376
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
43888
+ ruleSelection: "sidecar",
43889
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
43890
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
43891
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
43892
+ extendsPaths: [],
43893
+ ruleSelection: "sidecar",
43894
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
43895
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
43896
+ diagnostics: [],
43897
+ didDropReactHooksJsPlugin: false,
43898
+ hadPartialFailure: false
43899
+ };
43377
43900
  const freshCacheableDiagnostics = [];
43378
43901
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
43379
43902
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
43380
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
43381
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
43382
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
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) {
43903
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
43904
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
43905
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
43395
43906
  for (const missFile of missFiles) {
43396
43907
  const cacheKey = cacheKeyByFile.get(missFile);
43397
43908
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
43398
43909
  }
43399
43910
  cache.persist();
43400
43911
  }
43912
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
43913
+ sidecarCache,
43914
+ cacheKeyByFile,
43915
+ storablePasses: [{
43916
+ files: sidecarLintFiles,
43917
+ diagnostics: boundedSidecarResult.diagnostics,
43918
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
43919
+ probesByFile: await sidecarProbesTask
43920
+ }, {
43921
+ files: missFiles,
43922
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
43923
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
43924
+ probesByFile: await missProbesTask
43925
+ }]
43926
+ });
43401
43927
  return dedupeDiagnostics([
43402
43928
  ...replayedDiagnostics,
43403
43929
  ...fullResult.diagnostics,
43404
- ...sidecarResult.diagnostics
43930
+ ...sidecarReplayedDiagnostics,
43931
+ ...boundedSidecarResult.diagnostics,
43932
+ ...unboundedSidecarResult.diagnostics
43405
43933
  ]);
43406
43934
  }
43407
43935
  const baseArgs = makeBaseArgs(configPath);
@@ -43499,6 +44027,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43499
44027
  const concurrency = yield* OxlintConcurrency;
43500
44028
  const lintBatchOrdering = yield* LintBatchOrdering;
43501
44029
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
44030
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
43502
44031
  const collectedFailures = [];
43503
44032
  const diagnostics = yield* tryPromise({
43504
44033
  try: (signal) => runOxlint({
@@ -43517,7 +44046,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43517
44046
  },
43518
44047
  onFileProgress: input.onFileProgress,
43519
44048
  perFileLintCacheEnabled,
44049
+ sidecarLintCacheEnabled,
43520
44050
  onCacheStats: input.onCacheStats,
44051
+ onSidecarStats: input.onSidecarStats,
43521
44052
  spawnTimeoutMs,
43522
44053
  outputMaxBytes,
43523
44054
  concurrency,
@@ -43939,6 +44470,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
43939
44470
  }));
43940
44471
  } catch {}
43941
44472
  };
44473
+ const pruneExpiredSocketCache = (cacheDirectory) => {
44474
+ try {
44475
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
44476
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
44477
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
44478
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
44479
+ try {
44480
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
44481
+ } catch {
44482
+ continue;
44483
+ }
44484
+ }
44485
+ } catch {}
44486
+ };
43942
44487
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
43943
44488
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
43944
44489
  if (cacheFile !== null) {
@@ -44066,6 +44611,7 @@ const checkSupplyChain = (input) => gen(function* () {
44066
44611
  const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
44067
44612
  if (dependencies.length === 0) return [];
44068
44613
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44614
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
44069
44615
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
44070
44616
  const diagnostics = [];
44071
44617
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -44292,7 +44838,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44292
44838
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
44293
44839
  rootDirectory: scanDirectory,
44294
44840
  parseConcurrency: deadCodeParseConcurrency,
44295
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
44841
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
44842
+ onCacheOutcome: (didHitCache) => {
44843
+ deadCodeCacheHit = didHitCache;
44844
+ },
44845
+ onSummaryCacheStats: (stats) => {
44846
+ deadCodeSummaryCacheHits = stats.hits;
44847
+ deadCodeSummaryCacheMisses = stats.misses;
44848
+ }
44296
44849
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44297
44850
  yield* set(deadCodeFailure, {
44298
44851
  didFail: true,
@@ -44320,6 +44873,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44320
44873
  let lastReportedTotalFileCount = 0;
44321
44874
  let lintCacheHitFileCount = null;
44322
44875
  let lintCacheTotalFileCount = null;
44876
+ let lintSidecarReplayedFileCount = null;
44877
+ let lintSidecarTotalFileCount = null;
44878
+ let deadCodeCacheHit = null;
44879
+ let deadCodeSummaryCacheHits = null;
44880
+ let deadCodeSummaryCacheMisses = null;
44323
44881
  const baseLintStream = linterService.run({
44324
44882
  rootDirectory: scanDirectory,
44325
44883
  project,
@@ -44339,6 +44897,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44339
44897
  lintCacheHitFileCount = cacheHitFileCount;
44340
44898
  lintCacheTotalFileCount = totalConsideredFileCount;
44341
44899
  },
44900
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
44901
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
44902
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
44903
+ },
44342
44904
  deadlineEpochMs: input.deadlineEpochMs
44343
44905
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44344
44906
  yield* set(lintFailure, {
@@ -44449,6 +45011,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44449
45011
  securityScanFailed,
44450
45012
  lintCacheHitFileCount,
44451
45013
  lintCacheTotalFileCount,
45014
+ lintSidecarReplayedFileCount,
45015
+ lintSidecarTotalFileCount,
45016
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
45017
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
45018
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
44452
45019
  suppressedRuleCounts: transform.summarizeSuppressions()
44453
45020
  };
44454
45021
  }).pipe(withSpan("runInspect", { attributes: {
@@ -44930,11 +45497,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
44930
45497
  const parts = [`v=${version}`];
44931
45498
  let directory = projectDirectory;
44932
45499
  for (;;) {
44933
- for (const filename of CONFIG_FINGERPRINT_FILENAMES) try {
44934
- const stat = fs.statSync(path.join(directory, filename));
44935
- parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`);
44936
- } catch {
44937
- continue;
45500
+ for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
45501
+ const contentHash = hashFileContents(path.join(directory, filename));
45502
+ if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
44938
45503
  }
44939
45504
  const parent = path.dirname(directory);
44940
45505
  if (parent === directory) break;
@@ -45230,6 +45795,8 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
45230
45795
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
45231
45796
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
45232
45797
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
45798
+ const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
45799
+ const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
45233
45800
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
45234
45801
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
45235
45802
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
@@ -45320,7 +45887,7 @@ const makeNoopConsole = () => ({
45320
45887
  });
45321
45888
  //#endregion
45322
45889
  //#region src/cli/utils/version.ts
45323
- const VERSION = "0.6.3";
45890
+ const VERSION = "0.7.0-dev.574cba6";
45324
45891
  //#endregion
45325
45892
  //#region src/cli/utils/json-mode.ts
45326
45893
  let context = null;
@@ -45689,13 +46256,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45689
46256
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45690
46257
  * standard `SENTRY_RELEASE` override.
45691
46258
  */
45692
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3`;
46259
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.0-dev.574cba6`;
45693
46260
  /**
45694
46261
  * Deployment environment shown in Sentry's environment filter. Defaults to
45695
46262
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45696
46263
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45697
46264
  */
45698
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3") ? "development" : "production");
46265
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.0-dev.574cba6") ? "development" : "production");
45699
46266
  /**
45700
46267
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45701
46268
  * (set to `0` to disable tracing) and falls back to
@@ -45896,7 +46463,8 @@ const runGit = (projectRoot, args) => {
45896
46463
  "ignore",
45897
46464
  "pipe",
45898
46465
  "ignore"
45899
- ]
46466
+ ],
46467
+ maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
45900
46468
  }).trim();
45901
46469
  } catch {
45902
46470
  return null;
@@ -57772,6 +58340,276 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
57772
58340
  })).toSorted((left, right) => right.fileCount - left.fileCount);
57773
58341
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
57774
58342
  //#endregion
58343
+ //#region src/cli/utils/scan-result-cache.ts
58344
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
58345
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
58346
+ "oxlint/package.json",
58347
+ "oxlint-plugin-react-doctor/package.json",
58348
+ "deslop-js/package.json",
58349
+ "eslint-plugin-react-hooks/package.json"
58350
+ ];
58351
+ const bundledRequire = createRequire(import.meta.url);
58352
+ const normalizeForStableJson = (value) => {
58353
+ if (value === null) return null;
58354
+ if (value === void 0) return void 0;
58355
+ const valueType = typeof value;
58356
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
58357
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
58358
+ if (Array.isArray(value)) return value.map((item) => {
58359
+ const normalized = normalizeForStableJson(item);
58360
+ return normalized === void 0 ? null : normalized;
58361
+ });
58362
+ const prototype = Object.getPrototypeOf(value);
58363
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
58364
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
58365
+ const normalizedRecord = {};
58366
+ for (const key of Object.keys(value).sort()) {
58367
+ const normalized = normalizeForStableJson(value[key]);
58368
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
58369
+ }
58370
+ return normalizedRecord;
58371
+ };
58372
+ const stringifyStableJson = (value) => {
58373
+ try {
58374
+ return JSON.stringify(normalizeForStableJson(value));
58375
+ } catch {
58376
+ return null;
58377
+ }
58378
+ };
58379
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
58380
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
58381
+ const parseWorktreeStatusRecords = (statusOutput) => {
58382
+ const entries = [];
58383
+ const tokens = statusOutput.split("\0");
58384
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
58385
+ let record = tokens[tokenIndex];
58386
+ if (record.length === 0) continue;
58387
+ if (record.length < 3 || record[2] !== " ") {
58388
+ if (tokenIndex !== 0) return null;
58389
+ record = ` ${record}`;
58390
+ }
58391
+ if (record.length < 4 || record[2] !== " ") return null;
58392
+ const statusCode = record.slice(0, 2);
58393
+ const recordPath = record.slice(3);
58394
+ if (statusCode.includes("R") || statusCode.includes("C")) {
58395
+ tokenIndex += 1;
58396
+ const originPath = tokens[tokenIndex];
58397
+ if (originPath === void 0 || originPath.length === 0) return null;
58398
+ entries.push({
58399
+ statusCode,
58400
+ path: recordPath,
58401
+ originPath
58402
+ });
58403
+ continue;
58404
+ }
58405
+ entries.push({
58406
+ statusCode,
58407
+ path: recordPath
58408
+ });
58409
+ }
58410
+ return entries;
58411
+ };
58412
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
58413
+ let stats;
58414
+ try {
58415
+ stats = fs.lstatSync(absolutePath);
58416
+ } catch {
58417
+ return statusCode.includes("D") ? "deleted" : null;
58418
+ }
58419
+ if (stats.isSymbolicLink()) try {
58420
+ return `link:${fs.readlinkSync(absolutePath)}`;
58421
+ } catch {
58422
+ return null;
58423
+ }
58424
+ if (!stats.isFile()) return null;
58425
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
58426
+ return hashFileContents(absolutePath);
58427
+ };
58428
+ const buildWorktreeFingerprint = (projectDirectory) => {
58429
+ const statusOutput = runGit(projectDirectory, [
58430
+ "status",
58431
+ "--porcelain=v1",
58432
+ "-z",
58433
+ "--untracked-files=all"
58434
+ ]);
58435
+ if (statusOutput === null) return null;
58436
+ if (statusOutput.length === 0) return [];
58437
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
58438
+ if (statusEntries === null) return null;
58439
+ if (statusEntries.length > 300) return null;
58440
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
58441
+ if (repositoryRoot === null) return null;
58442
+ const dirtyEntries = [];
58443
+ for (const statusEntry of statusEntries) {
58444
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
58445
+ if (contentFingerprint === null) return null;
58446
+ dirtyEntries.push({
58447
+ ...statusEntry,
58448
+ contentFingerprint
58449
+ });
58450
+ }
58451
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
58452
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
58453
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
58454
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
58455
+ });
58456
+ };
58457
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
58458
+ const resolveDotenvFingerprint = (projectDirectory) => {
58459
+ try {
58460
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
58461
+ } catch {
58462
+ return [];
58463
+ }
58464
+ };
58465
+ const hasHiddenTrackedFileState = (projectDirectory) => {
58466
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
58467
+ if (output === null) return true;
58468
+ return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58469
+ };
58470
+ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58471
+ const readPersistedCache = (cacheFilePath) => {
58472
+ try {
58473
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58474
+ if (!isRecord(parsed) || parsed.version !== 3) return {
58475
+ version: 3,
58476
+ entries: []
58477
+ };
58478
+ if (!Array.isArray(parsed.entries)) return {
58479
+ version: 3,
58480
+ entries: []
58481
+ };
58482
+ const entries = [];
58483
+ for (const entry of parsed.entries) {
58484
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
58485
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
58486
+ entries.push(entry);
58487
+ }
58488
+ return {
58489
+ version: 3,
58490
+ entries
58491
+ };
58492
+ } catch {
58493
+ return {
58494
+ version: 3,
58495
+ entries: []
58496
+ };
58497
+ }
58498
+ };
58499
+ const writePersistedCache = (cacheFilePath, cache) => {
58500
+ try {
58501
+ fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
58502
+ const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
58503
+ fs.writeFileSync(tempPath, JSON.stringify(cache));
58504
+ fs.renameSync(tempPath, cacheFilePath);
58505
+ } catch {
58506
+ return;
58507
+ }
58508
+ };
58509
+ /**
58510
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
58511
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
58512
+ * sidecar, and dead-code caches (their `Context.Reference` defaults read the
58513
+ * same variable). Exported for the wide event's `cache.temperature`
58514
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
58515
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
58516
+ * not consulted — they leave the other subsystems live.
58517
+ */
58518
+ const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
58519
+ const resolveProjectIdentity = (projectDirectory) => {
58520
+ try {
58521
+ return fs.realpathSync.native(projectDirectory);
58522
+ } catch {
58523
+ return path.resolve(projectDirectory);
58524
+ }
58525
+ };
58526
+ const fileFingerprint = (filePath) => {
58527
+ try {
58528
+ const stat = fs.statSync(filePath);
58529
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
58530
+ } catch {
58531
+ return null;
58532
+ }
58533
+ };
58534
+ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58535
+ const fingerprints = [];
58536
+ if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
58537
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
58538
+ const packageJson = bundledRequire(specifier);
58539
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
58540
+ fingerprints.push(`${specifier}=${version}`);
58541
+ } catch {
58542
+ fingerprints.push(`${specifier}=missing`);
58543
+ }
58544
+ return fingerprints;
58545
+ };
58546
+ const buildScanResultCacheKey = (input) => {
58547
+ if (isCacheGloballyDisabled()) return null;
58548
+ if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58549
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58550
+ if (worktreeFingerprint === null) return null;
58551
+ const headSha = readHeadSha(input.projectDirectory);
58552
+ if (headSha === null) return null;
58553
+ if (stringifyStableJson(input.userConfig) === null) return null;
58554
+ const cacheKeyJson = stringifyStableJson({
58555
+ schemaVersion: 3,
58556
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
58557
+ headSha,
58558
+ worktreeFingerprint,
58559
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
58560
+ reactDoctorVersion: input.version,
58561
+ nodeVersion: process.version,
58562
+ toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
58563
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
58564
+ hasConfigOverride: input.hasConfigOverride,
58565
+ configSourceDirectory: input.configSourceDirectory,
58566
+ userConfig: input.userConfig,
58567
+ engineOptions: {
58568
+ lint: input.options.lint,
58569
+ deadCode: input.options.deadCode,
58570
+ includePaths: [...input.options.includePaths].sort(),
58571
+ customRulesOnly: input.options.customRulesOnly,
58572
+ respectInlineDisables: input.options.respectInlineDisables,
58573
+ warnings: input.options.warnings,
58574
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
58575
+ ignoredTags: [...input.options.ignoredTags].sort(),
58576
+ concurrency: input.options.concurrency,
58577
+ lintBatchOrdering: resolveLintBatchOrdering(),
58578
+ baselineRef: input.options.baseline?.ref,
58579
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
58580
+ noScore: input.options.noScore,
58581
+ isCi: input.options.isCi,
58582
+ suppressRendering: input.options.suppressRendering,
58583
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
58584
+ }
58585
+ });
58586
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
58587
+ };
58588
+ const createScanResultCache = (projectDirectory) => {
58589
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
58590
+ const persistedCache = readPersistedCache(cacheFilePath);
58591
+ const entries = /* @__PURE__ */ new Map();
58592
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58593
+ const persist = () => {
58594
+ writePersistedCache(cacheFilePath, {
58595
+ version: 3,
58596
+ entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58597
+ });
58598
+ };
58599
+ return {
58600
+ lookup: (key) => entries.get(key)?.payload ?? null,
58601
+ store: (key, payload) => {
58602
+ entries.set(key, {
58603
+ key,
58604
+ createdAtMs: Date.now(),
58605
+ payload
58606
+ });
58607
+ persist();
58608
+ }
58609
+ };
58610
+ };
58611
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
58612
+ //#endregion
57775
58613
  //#region src/cli/utils/should-block-ci.ts
57776
58614
  const shouldBlockCi = (diagnostics, blockingLevel) => {
57777
58615
  if (blockingLevel === "none") return false;
@@ -57813,6 +58651,63 @@ const readEnvBoolean = (name) => {
57813
58651
  if (value === void 0) return null;
57814
58652
  return value.toLowerCase() === "true" || value === "1";
57815
58653
  };
58654
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
58655
+ const resolveDeadCodeReuseRatio = (result) => {
58656
+ if (result.deadCodeCacheHit === true) return 1;
58657
+ const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
58658
+ if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
58659
+ return result.deadCodeCacheHit === false ? 0 : null;
58660
+ };
58661
+ /**
58662
+ * One queryable cache temperature per scan, derived from the whole stack:
58663
+ *
58664
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
58665
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
58666
+ * branch; no scan work ran).
58667
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
58668
+ * replays, a dead-code whole-result hit, or dead-code
58669
+ * summary-cache hits.
58670
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
58671
+ * off-switch is on. Granular per-cache opt-outs
58672
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
58673
+ * since the other subsystems stay live.
58674
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
58675
+ *
58676
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
58677
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
58678
+ * replay ratio, dead-code reuse), skipping subsystems that never consulted a
58679
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
58680
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
58681
+ * p50/p90-able summary. Emitted only on the success path.
58682
+ */
58683
+ const buildCacheAttributes = (input) => {
58684
+ if (input.result === void 0) return {};
58685
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
58686
+ wholeRepoHit: true,
58687
+ temperature: "turbo",
58688
+ warmth: 1
58689
+ });
58690
+ const result = input.result;
58691
+ const subsystemReuseRatios = [
58692
+ ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58693
+ ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
58694
+ resolveDeadCodeReuseRatio(result)
58695
+ ];
58696
+ let knownSubsystemCount = 0;
58697
+ let reuseRatioSum = 0;
58698
+ for (const reuseRatio of subsystemReuseRatios) {
58699
+ if (reuseRatio === null) continue;
58700
+ knownSubsystemCount += 1;
58701
+ reuseRatioSum += reuseRatio;
58702
+ }
58703
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
58704
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
58705
+ return withNamespace("cache", {
58706
+ wholeRepoHit: input.wholeRepoCacheHit ?? null,
58707
+ temperature,
58708
+ warmth
58709
+ });
58710
+ };
57816
58711
  const resolveVersionPin = (versionInput) => {
57817
58712
  if (versionInput === void 0 || versionInput.trim() === "") return null;
57818
58713
  if (versionInput === "latest") return "latest";
@@ -57918,11 +58813,17 @@ const buildOutcomeAttributes = (input) => {
57918
58813
  deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
57919
58814
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
57920
58815
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
57921
- cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
58816
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58817
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
58818
+ sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
58819
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
57922
58820
  }),
57923
58821
  ...withNamespace("deadCode", {
57924
58822
  failed: input.didDeadCodeFail ?? null,
57925
- overlapped: input.deadCodeOverlapped ?? null
58823
+ overlapped: input.deadCodeOverlapped ?? null,
58824
+ cacheHit: result.deadCodeCacheHit ?? null,
58825
+ summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
58826
+ summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
57926
58827
  }),
57927
58828
  ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
57928
58829
  ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
@@ -57982,7 +58883,7 @@ const buildScanAttributes = (input) => {
57982
58883
  * Projects a scan into the namespaced attribute set for its root span — the
57983
58884
  * canonical per-scan "wide event". Every attribute carries a dotted namespace
57984
58885
  * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
57985
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
58886
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
57986
58887
  * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
57987
58888
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
57988
58889
  * in the Spans dataset. Pure and exported so the projection (outcome
@@ -57996,7 +58897,8 @@ const buildScanAttributes = (input) => {
57996
58897
  const buildRunEventAttributes = (input) => toSpanAttributes({
57997
58898
  ...buildScanAttributes(input),
57998
58899
  ...buildActionAttributes(),
57999
- ...buildOutcomeAttributes(input)
58900
+ ...buildOutcomeAttributes(input),
58901
+ ...buildCacheAttributes(input)
58000
58902
  });
58001
58903
  /**
58002
58904
  * Stamps the wide-event attributes onto the run's root span. A guarded no-op
@@ -59206,196 +60108,6 @@ const resolveCliCategories = (categoryFlag) => {
59206
60108
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
59207
60109
  };
59208
60110
  //#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
60111
  //#region src/inspect.ts
59400
60112
  const silentConsole = makeNoopConsole();
59401
60113
  const runConsole = (effect) => {
@@ -59572,7 +60284,7 @@ const runBaselineComparison = async (params) => {
59572
60284
  suppressScanSummary: true,
59573
60285
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
59574
60286
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
59575
- }, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
60287
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
59576
60288
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
59577
60289
  const delta = computeDiagnosticDelta({
59578
60290
  headDiagnostics: params.headDiagnostics,
@@ -59625,7 +60337,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59625
60337
  startTime,
59626
60338
  rootSentrySpan,
59627
60339
  scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59628
- baselineDegraded
60340
+ baselineDegraded,
60341
+ wholeRepoCacheHit: true
59629
60342
  });
59630
60343
  recordOnboardingCompletion(options);
59631
60344
  return result;
@@ -59734,8 +60447,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59734
60447
  rootSentrySpan,
59735
60448
  scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59736
60449
  baselineDegraded,
60450
+ wholeRepoCacheHit: false,
59737
60451
  lintCacheHitFileCount: output.lintCacheHitFileCount,
59738
- lintCacheTotalFileCount: output.lintCacheTotalFileCount
60452
+ lintCacheTotalFileCount: output.lintCacheTotalFileCount,
60453
+ lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
60454
+ lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
60455
+ deadCodeCacheHit: output.deadCodeCacheHit,
60456
+ deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
60457
+ deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
59739
60458
  });
59740
60459
  recordOnboardingCompletion(options);
59741
60460
  return result;
@@ -59770,6 +60489,11 @@ const renderAndRecordScan = async (input) => {
59770
60489
  scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
59771
60490
  lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
59772
60491
  lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
60492
+ lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
60493
+ lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
60494
+ deadCodeCacheHit: input.deadCodeCacheHit ?? null,
60495
+ deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
60496
+ deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
59773
60497
  baselineDelta: input.payload.baselineDelta
59774
60498
  }), input.options.silent));
59775
60499
  const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
@@ -59794,6 +60518,7 @@ const renderAndRecordScan = async (input) => {
59794
60518
  result,
59795
60519
  mode: input.scanMode,
59796
60520
  gateExempt: input.baselineDegraded,
60521
+ wholeRepoCacheHit: input.wholeRepoCacheHit,
59797
60522
  didLintFail: input.payload.didLintFail,
59798
60523
  lintFailureReasonKind: input.payload.lintFailureReasonKind,
59799
60524
  lintPartialFailureCount: input.payload.lintPartialFailures.length,
@@ -59808,7 +60533,7 @@ const renderAndRecordScan = async (input) => {
59808
60533
  return result;
59809
60534
  };
59810
60535
  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;
60536
+ 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
60537
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
59813
60538
  didLintFail,
59814
60539
  lintFailureReason,
@@ -59832,6 +60557,15 @@ const finalizeAndRender = (input) => gen(function* () {
59832
60557
  lintCacheHitFileCount,
59833
60558
  lintCacheTotalFileCount
59834
60559
  } : {},
60560
+ ...lintSidecarTotalFileCount !== null ? {
60561
+ lintSidecarReplayedFileCount,
60562
+ lintSidecarTotalFileCount
60563
+ } : {},
60564
+ ...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
60565
+ ...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
60566
+ deadCodeSummaryCacheHits,
60567
+ deadCodeSummaryCacheMisses
60568
+ } : {},
59835
60569
  ...baselineDelta ? { baselineDelta } : {}
59836
60570
  });
59837
60571
  if (options.suppressRendering) return buildResult();
@@ -63678,4 +64412,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63678
64412
  export {};
63679
64413
 
63680
64414
  //# sourceMappingURL=cli.js.map
63681
- //# debugId=328318eb-6af2-5007-b816-51c953a96047
64415
+ //# debugId=a88b7ed4-e1d2-587a-a826-36b16313d032