react-doctor 0.6.3-dev.ce49250 → 0.6.3-dev.ced746f

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f615f76d-a949-5fb6-ace8-145f3e137af4")}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]="b81c51d5-0ead-51a4-a9f9-ae499fd19a52")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import * as NodeChildProcess from "node:child_process";
10
10
  import { execFileSync, spawn, spawnSync } from "node:child_process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -32928,6 +32928,7 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32928
32928
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32929
32929
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32930
32930
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32931
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32931
32932
  const CORE_PACKAGE_VERSION = "0.6.3";
32932
32933
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32933
32934
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
@@ -32935,6 +32936,7 @@ const SUPPLY_CHAIN_PLUGIN = "socket";
32935
32936
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32936
32937
  const SUPPLY_CHAIN_CATEGORY = "Security";
32937
32938
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32939
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32938
32940
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32939
32941
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32940
32942
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -35618,6 +35620,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35618
35620
  return true;
35619
35621
  } }) {};
35620
35622
  /**
35623
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
35624
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
35625
+ * diagnostics for every file whose dependency fingerprint (the probe set the
35626
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
35627
+ * rest. Only reachable when the per-file lint cache itself is on (the
35628
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
35629
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
35630
+ *
35631
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35632
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
35633
+ * per-file cache but run the always-fresh sidecar over every hit
35634
+ * (the pre-cache behavior).
35635
+ *
35636
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
35637
+ */
35638
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
35639
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35640
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
35641
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35642
+ if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
35643
+ return true;
35644
+ } }) {};
35645
+ /**
35621
35646
  * Whether the whole-project dead-code result cache
35622
35647
  * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35623
35648
  * whose inputs (source tree, manifests, configs, analyzer version) are
@@ -37341,22 +37366,37 @@ const failOpenReadJson = (filePath, fallback) => {
37341
37366
  return fallback;
37342
37367
  }
37343
37368
  };
37369
+ const hashFileContents = (filePath) => {
37370
+ try {
37371
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
37372
+ } catch {
37373
+ return null;
37374
+ }
37375
+ };
37344
37376
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37345
37377
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37346
37378
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37347
37379
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37348
37380
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37349
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
37350
- const fingerprints = [];
37381
+ /**
37382
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
37383
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
37384
+ * verified against the tree it started from.
37385
+ */
37386
+ const collectAnalyzedFileStats = (rootDirectory) => {
37387
+ const statByRelativePath = /* @__PURE__ */ new Map();
37351
37388
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37352
37389
  if (!isFingerprintedFile(name)) continue;
37353
37390
  try {
37354
37391
  const fileStat = NFS.statSync(absolutePath);
37355
37392
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37356
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37393
+ statByRelativePath.set(relativePath, {
37394
+ mtimeMs: fileStat.mtimeMs,
37395
+ size: fileStat.size
37396
+ });
37357
37397
  } catch {}
37358
37398
  }
37359
- return fingerprints.sort();
37399
+ return statByRelativePath;
37360
37400
  };
37361
37401
  const bundledRequire$2 = createRequire(import.meta.url);
37362
37402
  const resolveDeslopVersion = () => {
@@ -37368,34 +37408,85 @@ const resolveDeslopVersion = () => {
37368
37408
  }
37369
37409
  };
37370
37410
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37371
- schemaVersion: 1,
37411
+ schemaVersion: 2,
37372
37412
  coreVersion: input.coreVersion,
37373
37413
  deslopVersion: resolveDeslopVersion(),
37374
37414
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37375
37415
  entryPatterns: input.entryPatterns,
37376
37416
  ignorePatterns: input.ignorePatterns,
37377
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37378
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
37417
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
37379
37418
  })).digest("hex");
37380
- const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
37419
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37381
37420
  const decodeCachedDiagnostics = (raw) => {
37382
37421
  try {
37383
- for (const entry of raw) validateDiagnostic$1(entry);
37422
+ for (const entry of raw) validateDiagnostic$2(entry);
37384
37423
  return raw;
37385
37424
  } catch {
37386
37425
  return null;
37387
37426
  }
37388
37427
  };
37389
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37390
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37391
- if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37392
- return decodeCachedDiagnostics(persisted.diagnostics);
37428
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
37429
+ const lookupDeadCodeResultCache = (input) => {
37430
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
37431
+ const persisted = failOpenReadJson(cacheFilePath, null);
37432
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
37433
+ const storedFileEntries = Object.entries(persisted.files);
37434
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
37435
+ const repairedFiles = {};
37436
+ let repairedCount = 0;
37437
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
37438
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
37439
+ const currentStat = input.currentFileStats.get(relativePath);
37440
+ if (currentStat === void 0) return null;
37441
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
37442
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
37443
+ repairedFiles[relativePath] = storedIdentity;
37444
+ continue;
37445
+ }
37446
+ if (currentStat.size !== storedSize) return null;
37447
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
37448
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
37449
+ repairedFiles[relativePath] = [
37450
+ currentStat.mtimeMs,
37451
+ currentStat.size,
37452
+ storedContentHash
37453
+ ];
37454
+ repairedCount += 1;
37455
+ }
37456
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
37457
+ if (diagnostics === null) return null;
37458
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
37459
+ version: 2,
37460
+ key: input.cacheKey,
37461
+ files: repairedFiles,
37462
+ diagnostics: persisted.diagnostics
37463
+ });
37464
+ return diagnostics;
37393
37465
  };
37394
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37395
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37396
- version: 1,
37397
- key: cacheKey,
37398
- diagnostics
37466
+ const storeDeadCodeResultCache = (input) => {
37467
+ const persistedFiles = {};
37468
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
37469
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
37470
+ const contentHash = hashFileContents(absolutePath);
37471
+ if (contentHash === null) return;
37472
+ let currentStat;
37473
+ try {
37474
+ currentStat = NFS.statSync(absolutePath);
37475
+ } catch {
37476
+ return;
37477
+ }
37478
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
37479
+ persistedFiles[relativePath] = [
37480
+ snapshotStat.mtimeMs,
37481
+ snapshotStat.size,
37482
+ contentHash
37483
+ ];
37484
+ }
37485
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37486
+ version: 2,
37487
+ key: input.cacheKey,
37488
+ files: persistedFiles,
37489
+ diagnostics: input.diagnostics
37399
37490
  });
37400
37491
  };
37401
37492
  const readSystemFacts = () => ({
@@ -37805,16 +37896,22 @@ const checkDeadCode = async (options) => {
37805
37896
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37806
37897
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
37807
37898
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37808
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37899
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
37900
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
37809
37901
  rootDirectory,
37810
37902
  entryPatterns,
37811
37903
  ignorePatterns,
37812
37904
  tsConfigPath,
37813
37905
  deslopJsModuleSpecifier,
37814
37906
  coreVersion: CORE_PACKAGE_VERSION
37815
- }) : null;
37816
- if (cacheKey !== null) {
37817
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37907
+ });
37908
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
37909
+ const cachedDiagnostics = lookupDeadCodeResultCache({
37910
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37911
+ cacheKey,
37912
+ rootDirectory,
37913
+ currentFileStats: fileStatsSnapshot
37914
+ });
37818
37915
  options.onCacheOutcome?.(cachedDiagnostics !== null);
37819
37916
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37820
37917
  }
@@ -37888,7 +37985,13 @@ const checkDeadCode = async (options) => {
37888
37985
  category: DEAD_CODE_CATEGORY
37889
37986
  });
37890
37987
  }
37891
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37988
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
37989
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37990
+ cacheKey,
37991
+ rootDirectory,
37992
+ snapshotFileStats: fileStatsSnapshot,
37993
+ diagnostics
37994
+ });
37892
37995
  return diagnostics;
37893
37996
  };
37894
37997
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -38870,7 +38973,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38870
38973
  }
38871
38974
  return enabled;
38872
38975
  };
38873
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38976
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
38874
38977
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38875
38978
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38876
38979
  const jsPlugins = [];
@@ -38882,6 +38985,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38882
38985
  if (!rule) continue;
38883
38986
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38884
38987
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38988
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
38885
38989
  if (rule.scan !== void 0) continue;
38886
38990
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38887
38991
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38926,23 +39030,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38926
39030
  }
38927
39031
  };
38928
39032
  };
38929
- const validateDiagnostic = decodeUnknownSync(Diagnostic);
39033
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
38930
39034
  const decodeFileDiagnostics = (raw) => {
38931
39035
  if (!Array.isArray(raw)) return null;
38932
39036
  try {
38933
- for (const entry of raw) validateDiagnostic(entry);
39037
+ for (const entry of raw) validateDiagnostic$1(entry);
38934
39038
  return raw;
38935
39039
  } catch {
38936
39040
  return null;
38937
39041
  }
38938
39042
  };
38939
- const emptyCache = () => ({
39043
+ const emptyCache$1 = () => ({
38940
39044
  version: 1,
38941
39045
  rulesets: {}
38942
39046
  });
38943
- const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39047
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
38944
39048
  const entries = /* @__PURE__ */ new Map();
38945
- const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39049
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
38946
39050
  if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38947
39051
  const bucket = persisted.rulesets[rulesetHash];
38948
39052
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
@@ -38954,7 +39058,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38954
39058
  };
38955
39059
  const createFileLintCache = (cacheDirectory, rulesetHash) => {
38956
39060
  const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38957
- const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
39061
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
38958
39062
  return {
38959
39063
  lookup: (fileKey) => entries.get(fileKey) ?? null,
38960
39064
  store: (fileKey, diagnostics) => {
@@ -38962,7 +39066,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38962
39066
  entries.set(fileKey, diagnostics);
38963
39067
  },
38964
39068
  persist: () => {
38965
- const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39069
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
38966
39070
  const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38967
39071
  const existingBucket = rulesets[rulesetHash];
38968
39072
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
@@ -38983,6 +39087,135 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38983
39087
  }
38984
39088
  };
38985
39089
  };
39090
+ const ABSENT_CONTENT_ANSWER = "absent";
39091
+ /**
39092
+ * Answers dependency probes for the sidecar lint cache — used both when
39093
+ * STORING an entry (turn the collector's probe paths into answers) and when
39094
+ * VERIFYING one (re-answer each stored probe against the current tree).
39095
+ * Store and verify must agree exactly, so both go through this one resolver.
39096
+ *
39097
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
39098
+ * (missing, a directory, permission failure). Source files reuse the
39099
+ * hashes the cache-partition loop already computed
39100
+ * (`contentHashByRelativePath`); everything else (package.json,
39101
+ * tsconfig, ignored files) is hashed on demand.
39102
+ * - `exists` — the existence class a module-resolution candidate probe
39103
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
39104
+ * `statSync` follows symlinks, matching the resolvers).
39105
+ *
39106
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
39107
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
39108
+ * tsconfig chain) repeat across many files' probe sets.
39109
+ */
39110
+ const createSidecarProbeAnswerResolver = (input) => {
39111
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
39112
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
39113
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
39114
+ const contentAnswer = (relativePath) => {
39115
+ const memoized = contentAnswerByPath.get(relativePath);
39116
+ if (memoized !== void 0) return memoized;
39117
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
39118
+ contentAnswerByPath.set(relativePath, answer);
39119
+ return answer;
39120
+ };
39121
+ const existsAnswer = (relativePath) => {
39122
+ const memoized = existsAnswerByPath.get(relativePath);
39123
+ if (memoized !== void 0) return memoized;
39124
+ let answer;
39125
+ try {
39126
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
39127
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
39128
+ } catch {
39129
+ answer = "none";
39130
+ }
39131
+ existsAnswerByPath.set(relativePath, answer);
39132
+ return answer;
39133
+ };
39134
+ return {
39135
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
39136
+ toRelativePath
39137
+ };
39138
+ };
39139
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
39140
+ const isProbeKind = (value) => value === "content" || value === "exists";
39141
+ const decodeSidecarFileEntry = (raw) => {
39142
+ if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
39143
+ const probes = [];
39144
+ for (const rawProbe of raw.probes) {
39145
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
39146
+ const [kind, probePath, answer] = rawProbe;
39147
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
39148
+ probes.push({
39149
+ kind,
39150
+ path: probePath,
39151
+ answer
39152
+ });
39153
+ }
39154
+ try {
39155
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
39156
+ } catch {
39157
+ return null;
39158
+ }
39159
+ return {
39160
+ probes,
39161
+ diagnostics: raw.diagnostics
39162
+ };
39163
+ };
39164
+ const encodeSidecarFileEntry = (entry) => ({
39165
+ probes: entry.probes.map((probe) => [
39166
+ probe.kind,
39167
+ probe.path,
39168
+ probe.answer
39169
+ ]),
39170
+ diagnostics: entry.diagnostics
39171
+ });
39172
+ const emptyCache = () => ({
39173
+ version: 1,
39174
+ rulesets: {}
39175
+ });
39176
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39177
+ const entries = /* @__PURE__ */ new Map();
39178
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39179
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39180
+ const bucket = persisted.rulesets[rulesetHash];
39181
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39182
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
39183
+ const decoded = decodeSidecarFileEntry(rawEntry);
39184
+ if (decoded !== null) entries.set(fileKey, decoded);
39185
+ }
39186
+ return entries;
39187
+ };
39188
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39189
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
39190
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
39191
+ return {
39192
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
39193
+ store: (fileKey, entry) => {
39194
+ entries.delete(fileKey);
39195
+ entries.set(fileKey, entry);
39196
+ },
39197
+ persist: () => {
39198
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39199
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39200
+ const existingBucket = rulesets[rulesetHash];
39201
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39202
+ const ourFiles = {};
39203
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
39204
+ const cappedEntries = Object.entries({
39205
+ ...existingFiles,
39206
+ ...ourFiles
39207
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
39208
+ rulesets[rulesetHash] = {
39209
+ updatedAtMs: Date.now(),
39210
+ files: Object.fromEntries(cappedEntries)
39211
+ };
39212
+ atomicWriteJson(cacheFilePath, {
39213
+ version: 1,
39214
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39215
+ });
39216
+ }
39217
+ };
39218
+ };
38986
39219
  const bundledRequire = createRequire(import.meta.url);
38987
39220
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38988
39221
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -39982,13 +40215,6 @@ const validateRuleRegistration = () => {
39982
40215
  ].filter((entry) => entry !== null).join("; ");
39983
40216
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39984
40217
  };
39985
- const hashFileContents = (filePath) => {
39986
- try {
39987
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39988
- } catch {
39989
- return null;
39990
- }
39991
- };
39992
40218
  /**
39993
40219
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39994
40220
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -40070,6 +40296,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
40070
40296
  NFS.closeSync(fileHandle);
40071
40297
  }
40072
40298
  };
40299
+ /**
40300
+ * Attributes diagnostics back to the file that produced them by the
40301
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
40302
+ * attributed — the path forms don't align, so the caller must skip its cache
40303
+ * store rather than risk caching a wrong empty result for a file that
40304
+ * actually had diagnostics.
40305
+ */
40306
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
40307
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
40308
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
40309
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
40310
+ for (const diagnostic of diagnostics) {
40311
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
40312
+ if (file === void 0) return null;
40313
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
40314
+ fileDiagnostics.push(diagnostic);
40315
+ diagnosticsByFile.set(file, fileDiagnostics);
40316
+ }
40317
+ return diagnosticsByFile;
40318
+ };
40319
+ /**
40320
+ * Collects each file's cross-file dependency probes (via the plugin's
40321
+ * collectors) with their current answers. Fail-open per file: an unreadable /
40322
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
40323
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
40324
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
40325
+ * unawaited before a lint pass and awaits it at store time, hiding the
40326
+ * in-process parse + resolution cost under the subprocess wait. The loop
40327
+ * yields on the cooperative time budget so the child's stdout handling (and
40328
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
40329
+ * the module-level probe recorder never sees interleaved use. Never rejects.
40330
+ */
40331
+ const collectSidecarProbesForFiles = async (input) => {
40332
+ const buildProbes = (file) => {
40333
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
40334
+ let trace;
40335
+ try {
40336
+ trace = collectCrossFileDependencyProbes({
40337
+ absoluteFilePath,
40338
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
40339
+ ruleIds: input.boundedSidecarRuleIds
40340
+ });
40341
+ } catch {
40342
+ return null;
40343
+ }
40344
+ if (trace === null) return null;
40345
+ const probes = [];
40346
+ for (const contentPath of trace.contentPaths) {
40347
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
40348
+ probes.push({
40349
+ kind: "content",
40350
+ path: relativePath,
40351
+ answer: input.probeAnswers.answerFor("content", relativePath)
40352
+ });
40353
+ }
40354
+ for (const existencePath of trace.existencePaths) {
40355
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
40356
+ probes.push({
40357
+ kind: "exists",
40358
+ path: relativePath,
40359
+ answer: input.probeAnswers.answerFor("exists", relativePath)
40360
+ });
40361
+ }
40362
+ return probes;
40363
+ };
40364
+ const probesByFile = /* @__PURE__ */ new Map();
40365
+ let collectSliceStartedAt = performance.now();
40366
+ for (const file of input.files) {
40367
+ probesByFile.set(file, buildProbes(file));
40368
+ if (performance.now() - collectSliceStartedAt >= 12) {
40369
+ await yieldToEventLoop();
40370
+ collectSliceStartedAt = performance.now();
40371
+ }
40372
+ }
40373
+ return probesByFile;
40374
+ };
40375
+ /**
40376
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
40377
+ * the next scan can replay instead of re-linting.
40378
+ */
40379
+ const storeSidecarEntries = (input) => {
40380
+ let didStoreAnyEntry = false;
40381
+ for (const pass of input.storablePasses) {
40382
+ if (!pass.isTrusted) continue;
40383
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
40384
+ if (diagnosticsByFile === null) continue;
40385
+ for (const file of pass.files) {
40386
+ const cacheKey = input.cacheKeyByFile.get(file);
40387
+ if (cacheKey === void 0) continue;
40388
+ const probes = pass.probesByFile.get(file);
40389
+ if (probes === void 0 || probes === null) continue;
40390
+ input.sidecarCache.store(cacheKey, {
40391
+ probes,
40392
+ diagnostics: diagnosticsByFile.get(file) ?? []
40393
+ });
40394
+ didStoreAnyEntry = true;
40395
+ }
40396
+ }
40397
+ if (didStoreAnyEntry) input.sidecarCache.persist();
40398
+ };
40073
40399
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
40074
40400
  /**
40075
40401
  * Detects an oxlint config-load crash caused by the optional
@@ -40111,7 +40437,7 @@ const reactHooksJsPluginDropNote = (error) => {
40111
40437
  * 6. always restore disable directives + clean up the temp dir
40112
40438
  */
40113
40439
  const runOxlint = async (options) => {
40114
- 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;
40440
+ 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;
40115
40441
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40116
40442
  const severityControls = buildRuleSeverityControls(userConfig);
40117
40443
  validateRuleRegistration();
@@ -40129,7 +40455,8 @@ const runOxlint = async (options) => {
40129
40455
  severityControls,
40130
40456
  userPlugins,
40131
40457
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
40132
- ruleSelection: overrides.ruleSelection
40458
+ ruleSelection: overrides.ruleSelection,
40459
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
40133
40460
  });
40134
40461
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
40135
40462
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -40232,15 +40559,18 @@ const runOxlint = async (options) => {
40232
40559
  });
40233
40560
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
40234
40561
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40562
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
40235
40563
  const missFiles = [];
40236
40564
  const replayedDiagnostics = [];
40237
40565
  let hashSliceStartedAt = performance.now();
40238
40566
  for (const candidateFile of candidateFiles) {
40239
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40567
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
40568
+ const contentHash = hashFileContents(absoluteCandidatePath);
40240
40569
  if (contentHash === null) {
40241
40570
  missFiles.push(candidateFile);
40242
40571
  continue;
40243
40572
  }
40573
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
40244
40574
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
40245
40575
  cacheKeyByFile.set(candidateFile, cacheKey);
40246
40576
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -40252,43 +40582,111 @@ const runOxlint = async (options) => {
40252
40582
  }
40253
40583
  }
40254
40584
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40585
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
40586
+ extendsPaths: [],
40587
+ ruleSelection: "sidecar"
40588
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
40589
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40590
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
40591
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40592
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
40593
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
40594
+ config: buildConfig({
40595
+ extendsPaths: [],
40596
+ ruleSelection: "sidecar",
40597
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
40598
+ }),
40599
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
40600
+ ignorePatterns: combinedPatterns,
40601
+ tsconfigContent
40602
+ })) : null;
40603
+ const probeAnswers = createSidecarProbeAnswerResolver({
40604
+ rootDirectory,
40605
+ contentHashByRelativePath
40606
+ });
40607
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40608
+ files: missFiles,
40609
+ rootDirectory,
40610
+ boundedSidecarRuleIds,
40611
+ probeAnswers
40612
+ });
40255
40613
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
40256
40614
  extendsPaths: [],
40257
40615
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
40258
40616
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
40259
40617
  const missFileSet = new Set(missFiles);
40260
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
40618
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
40619
+ const sidecarReplayedDiagnostics = [];
40620
+ const sidecarLintFiles = [];
40621
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
40622
+ else {
40623
+ let verifySliceStartedAt = performance.now();
40624
+ for (const hitFile of hitFiles) {
40625
+ const cacheKey = cacheKeyByFile.get(hitFile);
40626
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
40627
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
40628
+ else sidecarLintFiles.push(hitFile);
40629
+ if (performance.now() - verifySliceStartedAt >= 12) {
40630
+ await yieldToEventLoop();
40631
+ verifySliceStartedAt = performance.now();
40632
+ }
40633
+ }
40634
+ }
40635
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40636
+ files: sidecarLintFiles,
40637
+ rootDirectory,
40638
+ boundedSidecarRuleIds,
40639
+ probeAnswers
40640
+ });
40641
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
40642
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
40643
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
40261
40644
  extendsPaths: [],
40262
- ruleSelection: "sidecar"
40263
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
40645
+ ruleSelection: "sidecar",
40646
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
40647
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
40648
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
40649
+ extendsPaths: [],
40650
+ ruleSelection: "sidecar",
40651
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
40652
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
40653
+ diagnostics: [],
40654
+ didDropReactHooksJsPlugin: false,
40655
+ hadPartialFailure: false
40656
+ };
40264
40657
  const freshCacheableDiagnostics = [];
40265
40658
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
40266
40659
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
40267
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
40268
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
40269
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
40270
- let isAttributionSound = true;
40271
- for (const diagnostic of freshCacheableDiagnostics) {
40272
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
40273
- if (missFile === void 0) {
40274
- isAttributionSound = false;
40275
- break;
40276
- }
40277
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
40278
- fileDiagnostics.push(diagnostic);
40279
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
40280
- }
40281
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
40660
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
40661
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
40662
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
40282
40663
  for (const missFile of missFiles) {
40283
40664
  const cacheKey = cacheKeyByFile.get(missFile);
40284
40665
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
40285
40666
  }
40286
40667
  cache.persist();
40287
40668
  }
40669
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
40670
+ sidecarCache,
40671
+ cacheKeyByFile,
40672
+ storablePasses: [{
40673
+ files: sidecarLintFiles,
40674
+ diagnostics: boundedSidecarResult.diagnostics,
40675
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
40676
+ probesByFile: await sidecarProbesTask
40677
+ }, {
40678
+ files: missFiles,
40679
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
40680
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
40681
+ probesByFile: await missProbesTask
40682
+ }]
40683
+ });
40288
40684
  return dedupeDiagnostics([
40289
40685
  ...replayedDiagnostics,
40290
40686
  ...fullResult.diagnostics,
40291
- ...sidecarResult.diagnostics
40687
+ ...sidecarReplayedDiagnostics,
40688
+ ...boundedSidecarResult.diagnostics,
40689
+ ...unboundedSidecarResult.diagnostics
40292
40690
  ]);
40293
40691
  }
40294
40692
  const baseArgs = makeBaseArgs(configPath);
@@ -40386,6 +40784,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40386
40784
  const concurrency = yield* OxlintConcurrency;
40387
40785
  const lintBatchOrdering = yield* LintBatchOrdering;
40388
40786
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
40787
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
40389
40788
  const collectedFailures = [];
40390
40789
  const diagnostics = yield* tryPromise({
40391
40790
  try: (signal) => runOxlint({
@@ -40404,7 +40803,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40404
40803
  },
40405
40804
  onFileProgress: input.onFileProgress,
40406
40805
  perFileLintCacheEnabled,
40806
+ sidecarLintCacheEnabled,
40407
40807
  onCacheStats: input.onCacheStats,
40808
+ onSidecarStats: input.onSidecarStats,
40408
40809
  spawnTimeoutMs,
40409
40810
  outputMaxBytes,
40410
40811
  concurrency,
@@ -40826,6 +41227,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
40826
41227
  }));
40827
41228
  } catch {}
40828
41229
  };
41230
+ const pruneExpiredSocketCache = (cacheDirectory) => {
41231
+ try {
41232
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
41233
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
41234
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
41235
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
41236
+ try {
41237
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
41238
+ } catch {
41239
+ continue;
41240
+ }
41241
+ }
41242
+ } catch {}
41243
+ };
40829
41244
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40830
41245
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40831
41246
  if (cacheFile !== null) {
@@ -40953,6 +41368,7 @@ const checkSupplyChain = (input) => gen(function* () {
40953
41368
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
40954
41369
  if (dependencies.length === 0) return [];
40955
41370
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41371
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
40956
41372
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
40957
41373
  const diagnostics = [];
40958
41374
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -41214,6 +41630,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41214
41630
  let lastReportedTotalFileCount = 0;
41215
41631
  let lintCacheHitFileCount = null;
41216
41632
  let lintCacheTotalFileCount = null;
41633
+ let lintSidecarReplayedFileCount = null;
41634
+ let lintSidecarTotalFileCount = null;
41217
41635
  let deadCodeCacheHit = null;
41218
41636
  let deadCodeSummaryCacheHits = null;
41219
41637
  let deadCodeSummaryCacheMisses = null;
@@ -41236,6 +41654,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41236
41654
  lintCacheHitFileCount = cacheHitFileCount;
41237
41655
  lintCacheTotalFileCount = totalConsideredFileCount;
41238
41656
  },
41657
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
41658
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
41659
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
41660
+ },
41239
41661
  deadlineEpochMs: input.deadlineEpochMs
41240
41662
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41241
41663
  yield* set(lintFailure, {
@@ -41346,6 +41768,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41346
41768
  securityScanFailed,
41347
41769
  lintCacheHitFileCount,
41348
41770
  lintCacheTotalFileCount,
41771
+ lintSidecarReplayedFileCount,
41772
+ lintSidecarTotalFileCount,
41349
41773
  deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41350
41774
  deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
41351
41775
  deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
@@ -41989,4 +42413,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41989
42413
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41990
42414
 
41991
42415
  //# sourceMappingURL=index.js.map
41992
- //# debugId=f615f76d-a949-5fb6-ace8-145f3e137af4
42416
+ //# debugId=b81c51d5-0ead-51a4-a9f9-ae499fd19a52