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/lsp.js CHANGED
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
9
  import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
- 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";
11
+ 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";
12
12
  import { parseJSON5 } from "confbox";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
@@ -32987,6 +32987,7 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32987
32987
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32988
32988
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32989
32989
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32990
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32990
32991
  const CORE_PACKAGE_VERSION = "0.6.3";
32991
32992
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32992
32993
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
@@ -32994,6 +32995,7 @@ const SUPPLY_CHAIN_PLUGIN = "socket";
32994
32995
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32995
32996
  const SUPPLY_CHAIN_CATEGORY = "Security";
32996
32997
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32998
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32997
32999
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32998
33000
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32999
33001
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -35651,6 +35653,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35651
35653
  return true;
35652
35654
  } }) {};
35653
35655
  /**
35656
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
35657
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
35658
+ * diagnostics for every file whose dependency fingerprint (the probe set the
35659
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
35660
+ * rest. Only reachable when the per-file lint cache itself is on (the
35661
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
35662
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
35663
+ *
35664
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35665
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
35666
+ * per-file cache but run the always-fresh sidecar over every hit
35667
+ * (the pre-cache behavior).
35668
+ *
35669
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
35670
+ */
35671
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
35672
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35673
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
35674
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35675
+ if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
35676
+ return true;
35677
+ } }) {};
35678
+ /**
35654
35679
  * Whether the whole-project dead-code result cache
35655
35680
  * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35656
35681
  * whose inputs (source tree, manifests, configs, analyzer version) are
@@ -37326,22 +37351,37 @@ const failOpenReadJson = (filePath, fallback) => {
37326
37351
  return fallback;
37327
37352
  }
37328
37353
  };
37354
+ const hashFileContents = (filePath) => {
37355
+ try {
37356
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
37357
+ } catch {
37358
+ return null;
37359
+ }
37360
+ };
37329
37361
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37330
37362
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37331
37363
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37332
37364
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37333
37365
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37334
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
37335
- const fingerprints = [];
37366
+ /**
37367
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
37368
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
37369
+ * verified against the tree it started from.
37370
+ */
37371
+ const collectAnalyzedFileStats = (rootDirectory) => {
37372
+ const statByRelativePath = /* @__PURE__ */ new Map();
37336
37373
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37337
37374
  if (!isFingerprintedFile(name)) continue;
37338
37375
  try {
37339
37376
  const fileStat = NFS.statSync(absolutePath);
37340
37377
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37341
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37378
+ statByRelativePath.set(relativePath, {
37379
+ mtimeMs: fileStat.mtimeMs,
37380
+ size: fileStat.size
37381
+ });
37342
37382
  } catch {}
37343
37383
  }
37344
- return fingerprints.sort();
37384
+ return statByRelativePath;
37345
37385
  };
37346
37386
  const bundledRequire$2 = createRequire(import.meta.url);
37347
37387
  const resolveDeslopVersion = () => {
@@ -37353,34 +37393,85 @@ const resolveDeslopVersion = () => {
37353
37393
  }
37354
37394
  };
37355
37395
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37356
- schemaVersion: 1,
37396
+ schemaVersion: 2,
37357
37397
  coreVersion: input.coreVersion,
37358
37398
  deslopVersion: resolveDeslopVersion(),
37359
37399
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37360
37400
  entryPatterns: input.entryPatterns,
37361
37401
  ignorePatterns: input.ignorePatterns,
37362
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37363
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
37402
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
37364
37403
  })).digest("hex");
37365
- const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
37404
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37366
37405
  const decodeCachedDiagnostics = (raw) => {
37367
37406
  try {
37368
- for (const entry of raw) validateDiagnostic$1(entry);
37407
+ for (const entry of raw) validateDiagnostic$2(entry);
37369
37408
  return raw;
37370
37409
  } catch {
37371
37410
  return null;
37372
37411
  }
37373
37412
  };
37374
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37375
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37376
- if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37377
- return decodeCachedDiagnostics(persisted.diagnostics);
37413
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
37414
+ const lookupDeadCodeResultCache = (input) => {
37415
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
37416
+ const persisted = failOpenReadJson(cacheFilePath, null);
37417
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
37418
+ const storedFileEntries = Object.entries(persisted.files);
37419
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
37420
+ const repairedFiles = {};
37421
+ let repairedCount = 0;
37422
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
37423
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
37424
+ const currentStat = input.currentFileStats.get(relativePath);
37425
+ if (currentStat === void 0) return null;
37426
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
37427
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
37428
+ repairedFiles[relativePath] = storedIdentity;
37429
+ continue;
37430
+ }
37431
+ if (currentStat.size !== storedSize) return null;
37432
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
37433
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
37434
+ repairedFiles[relativePath] = [
37435
+ currentStat.mtimeMs,
37436
+ currentStat.size,
37437
+ storedContentHash
37438
+ ];
37439
+ repairedCount += 1;
37440
+ }
37441
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
37442
+ if (diagnostics === null) return null;
37443
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
37444
+ version: 2,
37445
+ key: input.cacheKey,
37446
+ files: repairedFiles,
37447
+ diagnostics: persisted.diagnostics
37448
+ });
37449
+ return diagnostics;
37378
37450
  };
37379
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37380
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37381
- version: 1,
37382
- key: cacheKey,
37383
- diagnostics
37451
+ const storeDeadCodeResultCache = (input) => {
37452
+ const persistedFiles = {};
37453
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
37454
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
37455
+ const contentHash = hashFileContents(absolutePath);
37456
+ if (contentHash === null) return;
37457
+ let currentStat;
37458
+ try {
37459
+ currentStat = NFS.statSync(absolutePath);
37460
+ } catch {
37461
+ return;
37462
+ }
37463
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
37464
+ persistedFiles[relativePath] = [
37465
+ snapshotStat.mtimeMs,
37466
+ snapshotStat.size,
37467
+ contentHash
37468
+ ];
37469
+ }
37470
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37471
+ version: 2,
37472
+ key: input.cacheKey,
37473
+ files: persistedFiles,
37474
+ diagnostics: input.diagnostics
37384
37475
  });
37385
37476
  };
37386
37477
  const readSystemFacts = () => ({
@@ -37790,16 +37881,22 @@ const checkDeadCode = async (options) => {
37790
37881
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37791
37882
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
37792
37883
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37793
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37884
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
37885
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
37794
37886
  rootDirectory,
37795
37887
  entryPatterns,
37796
37888
  ignorePatterns,
37797
37889
  tsConfigPath,
37798
37890
  deslopJsModuleSpecifier,
37799
37891
  coreVersion: CORE_PACKAGE_VERSION
37800
- }) : null;
37801
- if (cacheKey !== null) {
37802
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37892
+ });
37893
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
37894
+ const cachedDiagnostics = lookupDeadCodeResultCache({
37895
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37896
+ cacheKey,
37897
+ rootDirectory,
37898
+ currentFileStats: fileStatsSnapshot
37899
+ });
37803
37900
  options.onCacheOutcome?.(cachedDiagnostics !== null);
37804
37901
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37805
37902
  }
@@ -37873,7 +37970,13 @@ const checkDeadCode = async (options) => {
37873
37970
  category: DEAD_CODE_CATEGORY
37874
37971
  });
37875
37972
  }
37876
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37973
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
37974
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37975
+ cacheKey,
37976
+ rootDirectory,
37977
+ snapshotFileStats: fileStatsSnapshot,
37978
+ diagnostics
37979
+ });
37877
37980
  return diagnostics;
37878
37981
  };
37879
37982
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -38855,7 +38958,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38855
38958
  }
38856
38959
  return enabled;
38857
38960
  };
38858
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38961
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
38859
38962
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38860
38963
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38861
38964
  const jsPlugins = [];
@@ -38867,6 +38970,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38867
38970
  if (!rule) continue;
38868
38971
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38869
38972
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38973
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
38870
38974
  if (rule.scan !== void 0) continue;
38871
38975
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38872
38976
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38911,23 +39015,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38911
39015
  }
38912
39016
  };
38913
39017
  };
38914
- const validateDiagnostic = decodeUnknownSync(Diagnostic);
39018
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
38915
39019
  const decodeFileDiagnostics = (raw) => {
38916
39020
  if (!Array.isArray(raw)) return null;
38917
39021
  try {
38918
- for (const entry of raw) validateDiagnostic(entry);
39022
+ for (const entry of raw) validateDiagnostic$1(entry);
38919
39023
  return raw;
38920
39024
  } catch {
38921
39025
  return null;
38922
39026
  }
38923
39027
  };
38924
- const emptyCache = () => ({
39028
+ const emptyCache$1 = () => ({
38925
39029
  version: 1,
38926
39030
  rulesets: {}
38927
39031
  });
38928
- const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39032
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
38929
39033
  const entries = /* @__PURE__ */ new Map();
38930
- const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39034
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
38931
39035
  if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38932
39036
  const bucket = persisted.rulesets[rulesetHash];
38933
39037
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
@@ -38939,7 +39043,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38939
39043
  };
38940
39044
  const createFileLintCache = (cacheDirectory, rulesetHash) => {
38941
39045
  const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38942
- const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
39046
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
38943
39047
  return {
38944
39048
  lookup: (fileKey) => entries.get(fileKey) ?? null,
38945
39049
  store: (fileKey, diagnostics) => {
@@ -38947,7 +39051,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38947
39051
  entries.set(fileKey, diagnostics);
38948
39052
  },
38949
39053
  persist: () => {
38950
- const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39054
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
38951
39055
  const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38952
39056
  const existingBucket = rulesets[rulesetHash];
38953
39057
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
@@ -38968,6 +39072,135 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38968
39072
  }
38969
39073
  };
38970
39074
  };
39075
+ const ABSENT_CONTENT_ANSWER = "absent";
39076
+ /**
39077
+ * Answers dependency probes for the sidecar lint cache — used both when
39078
+ * STORING an entry (turn the collector's probe paths into answers) and when
39079
+ * VERIFYING one (re-answer each stored probe against the current tree).
39080
+ * Store and verify must agree exactly, so both go through this one resolver.
39081
+ *
39082
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
39083
+ * (missing, a directory, permission failure). Source files reuse the
39084
+ * hashes the cache-partition loop already computed
39085
+ * (`contentHashByRelativePath`); everything else (package.json,
39086
+ * tsconfig, ignored files) is hashed on demand.
39087
+ * - `exists` — the existence class a module-resolution candidate probe
39088
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
39089
+ * `statSync` follows symlinks, matching the resolvers).
39090
+ *
39091
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
39092
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
39093
+ * tsconfig chain) repeat across many files' probe sets.
39094
+ */
39095
+ const createSidecarProbeAnswerResolver = (input) => {
39096
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
39097
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
39098
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
39099
+ const contentAnswer = (relativePath) => {
39100
+ const memoized = contentAnswerByPath.get(relativePath);
39101
+ if (memoized !== void 0) return memoized;
39102
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
39103
+ contentAnswerByPath.set(relativePath, answer);
39104
+ return answer;
39105
+ };
39106
+ const existsAnswer = (relativePath) => {
39107
+ const memoized = existsAnswerByPath.get(relativePath);
39108
+ if (memoized !== void 0) return memoized;
39109
+ let answer;
39110
+ try {
39111
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
39112
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
39113
+ } catch {
39114
+ answer = "none";
39115
+ }
39116
+ existsAnswerByPath.set(relativePath, answer);
39117
+ return answer;
39118
+ };
39119
+ return {
39120
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
39121
+ toRelativePath
39122
+ };
39123
+ };
39124
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
39125
+ const isProbeKind = (value) => value === "content" || value === "exists";
39126
+ const decodeSidecarFileEntry = (raw) => {
39127
+ if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
39128
+ const probes = [];
39129
+ for (const rawProbe of raw.probes) {
39130
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
39131
+ const [kind, probePath, answer] = rawProbe;
39132
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
39133
+ probes.push({
39134
+ kind,
39135
+ path: probePath,
39136
+ answer
39137
+ });
39138
+ }
39139
+ try {
39140
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
39141
+ } catch {
39142
+ return null;
39143
+ }
39144
+ return {
39145
+ probes,
39146
+ diagnostics: raw.diagnostics
39147
+ };
39148
+ };
39149
+ const encodeSidecarFileEntry = (entry) => ({
39150
+ probes: entry.probes.map((probe) => [
39151
+ probe.kind,
39152
+ probe.path,
39153
+ probe.answer
39154
+ ]),
39155
+ diagnostics: entry.diagnostics
39156
+ });
39157
+ const emptyCache = () => ({
39158
+ version: 1,
39159
+ rulesets: {}
39160
+ });
39161
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39162
+ const entries = /* @__PURE__ */ new Map();
39163
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39164
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39165
+ const bucket = persisted.rulesets[rulesetHash];
39166
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39167
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
39168
+ const decoded = decodeSidecarFileEntry(rawEntry);
39169
+ if (decoded !== null) entries.set(fileKey, decoded);
39170
+ }
39171
+ return entries;
39172
+ };
39173
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39174
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
39175
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
39176
+ return {
39177
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
39178
+ store: (fileKey, entry) => {
39179
+ entries.delete(fileKey);
39180
+ entries.set(fileKey, entry);
39181
+ },
39182
+ persist: () => {
39183
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39184
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39185
+ const existingBucket = rulesets[rulesetHash];
39186
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39187
+ const ourFiles = {};
39188
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
39189
+ const cappedEntries = Object.entries({
39190
+ ...existingFiles,
39191
+ ...ourFiles
39192
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
39193
+ rulesets[rulesetHash] = {
39194
+ updatedAtMs: Date.now(),
39195
+ files: Object.fromEntries(cappedEntries)
39196
+ };
39197
+ atomicWriteJson(cacheFilePath, {
39198
+ version: 1,
39199
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39200
+ });
39201
+ }
39202
+ };
39203
+ };
38971
39204
  const bundledRequire = createRequire(import.meta.url);
38972
39205
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38973
39206
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -39967,13 +40200,6 @@ const validateRuleRegistration = () => {
39967
40200
  ].filter((entry) => entry !== null).join("; ");
39968
40201
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39969
40202
  };
39970
- const hashFileContents = (filePath) => {
39971
- try {
39972
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39973
- } catch {
39974
- return null;
39975
- }
39976
- };
39977
40203
  /**
39978
40204
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39979
40205
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -40055,6 +40281,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
40055
40281
  NFS.closeSync(fileHandle);
40056
40282
  }
40057
40283
  };
40284
+ /**
40285
+ * Attributes diagnostics back to the file that produced them by the
40286
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
40287
+ * attributed — the path forms don't align, so the caller must skip its cache
40288
+ * store rather than risk caching a wrong empty result for a file that
40289
+ * actually had diagnostics.
40290
+ */
40291
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
40292
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
40293
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
40294
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
40295
+ for (const diagnostic of diagnostics) {
40296
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
40297
+ if (file === void 0) return null;
40298
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
40299
+ fileDiagnostics.push(diagnostic);
40300
+ diagnosticsByFile.set(file, fileDiagnostics);
40301
+ }
40302
+ return diagnosticsByFile;
40303
+ };
40304
+ /**
40305
+ * Collects each file's cross-file dependency probes (via the plugin's
40306
+ * collectors) with their current answers. Fail-open per file: an unreadable /
40307
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
40308
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
40309
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
40310
+ * unawaited before a lint pass and awaits it at store time, hiding the
40311
+ * in-process parse + resolution cost under the subprocess wait. The loop
40312
+ * yields on the cooperative time budget so the child's stdout handling (and
40313
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
40314
+ * the module-level probe recorder never sees interleaved use. Never rejects.
40315
+ */
40316
+ const collectSidecarProbesForFiles = async (input) => {
40317
+ const buildProbes = (file) => {
40318
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
40319
+ let trace;
40320
+ try {
40321
+ trace = collectCrossFileDependencyProbes({
40322
+ absoluteFilePath,
40323
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
40324
+ ruleIds: input.boundedSidecarRuleIds
40325
+ });
40326
+ } catch {
40327
+ return null;
40328
+ }
40329
+ if (trace === null) return null;
40330
+ const probes = [];
40331
+ for (const contentPath of trace.contentPaths) {
40332
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
40333
+ probes.push({
40334
+ kind: "content",
40335
+ path: relativePath,
40336
+ answer: input.probeAnswers.answerFor("content", relativePath)
40337
+ });
40338
+ }
40339
+ for (const existencePath of trace.existencePaths) {
40340
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
40341
+ probes.push({
40342
+ kind: "exists",
40343
+ path: relativePath,
40344
+ answer: input.probeAnswers.answerFor("exists", relativePath)
40345
+ });
40346
+ }
40347
+ return probes;
40348
+ };
40349
+ const probesByFile = /* @__PURE__ */ new Map();
40350
+ let collectSliceStartedAt = performance.now();
40351
+ for (const file of input.files) {
40352
+ probesByFile.set(file, buildProbes(file));
40353
+ if (performance.now() - collectSliceStartedAt >= 12) {
40354
+ await yieldToEventLoop();
40355
+ collectSliceStartedAt = performance.now();
40356
+ }
40357
+ }
40358
+ return probesByFile;
40359
+ };
40360
+ /**
40361
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
40362
+ * the next scan can replay instead of re-linting.
40363
+ */
40364
+ const storeSidecarEntries = (input) => {
40365
+ let didStoreAnyEntry = false;
40366
+ for (const pass of input.storablePasses) {
40367
+ if (!pass.isTrusted) continue;
40368
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
40369
+ if (diagnosticsByFile === null) continue;
40370
+ for (const file of pass.files) {
40371
+ const cacheKey = input.cacheKeyByFile.get(file);
40372
+ if (cacheKey === void 0) continue;
40373
+ const probes = pass.probesByFile.get(file);
40374
+ if (probes === void 0 || probes === null) continue;
40375
+ input.sidecarCache.store(cacheKey, {
40376
+ probes,
40377
+ diagnostics: diagnosticsByFile.get(file) ?? []
40378
+ });
40379
+ didStoreAnyEntry = true;
40380
+ }
40381
+ }
40382
+ if (didStoreAnyEntry) input.sidecarCache.persist();
40383
+ };
40058
40384
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
40059
40385
  /**
40060
40386
  * Detects an oxlint config-load crash caused by the optional
@@ -40096,7 +40422,7 @@ const reactHooksJsPluginDropNote = (error) => {
40096
40422
  * 6. always restore disable directives + clean up the temp dir
40097
40423
  */
40098
40424
  const runOxlint = async (options) => {
40099
- 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;
40425
+ 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;
40100
40426
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40101
40427
  const severityControls = buildRuleSeverityControls(userConfig);
40102
40428
  validateRuleRegistration();
@@ -40114,7 +40440,8 @@ const runOxlint = async (options) => {
40114
40440
  severityControls,
40115
40441
  userPlugins,
40116
40442
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
40117
- ruleSelection: overrides.ruleSelection
40443
+ ruleSelection: overrides.ruleSelection,
40444
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
40118
40445
  });
40119
40446
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
40120
40447
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -40217,15 +40544,18 @@ const runOxlint = async (options) => {
40217
40544
  });
40218
40545
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
40219
40546
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40547
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
40220
40548
  const missFiles = [];
40221
40549
  const replayedDiagnostics = [];
40222
40550
  let hashSliceStartedAt = performance.now();
40223
40551
  for (const candidateFile of candidateFiles) {
40224
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40552
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
40553
+ const contentHash = hashFileContents(absoluteCandidatePath);
40225
40554
  if (contentHash === null) {
40226
40555
  missFiles.push(candidateFile);
40227
40556
  continue;
40228
40557
  }
40558
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
40229
40559
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
40230
40560
  cacheKeyByFile.set(candidateFile, cacheKey);
40231
40561
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -40237,43 +40567,111 @@ const runOxlint = async (options) => {
40237
40567
  }
40238
40568
  }
40239
40569
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40570
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
40571
+ extendsPaths: [],
40572
+ ruleSelection: "sidecar"
40573
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
40574
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40575
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
40576
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40577
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
40578
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
40579
+ config: buildConfig({
40580
+ extendsPaths: [],
40581
+ ruleSelection: "sidecar",
40582
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
40583
+ }),
40584
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
40585
+ ignorePatterns: combinedPatterns,
40586
+ tsconfigContent
40587
+ })) : null;
40588
+ const probeAnswers = createSidecarProbeAnswerResolver({
40589
+ rootDirectory,
40590
+ contentHashByRelativePath
40591
+ });
40592
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40593
+ files: missFiles,
40594
+ rootDirectory,
40595
+ boundedSidecarRuleIds,
40596
+ probeAnswers
40597
+ });
40240
40598
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
40241
40599
  extendsPaths: [],
40242
40600
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
40243
40601
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
40244
40602
  const missFileSet = new Set(missFiles);
40245
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
40603
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
40604
+ const sidecarReplayedDiagnostics = [];
40605
+ const sidecarLintFiles = [];
40606
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
40607
+ else {
40608
+ let verifySliceStartedAt = performance.now();
40609
+ for (const hitFile of hitFiles) {
40610
+ const cacheKey = cacheKeyByFile.get(hitFile);
40611
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
40612
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
40613
+ else sidecarLintFiles.push(hitFile);
40614
+ if (performance.now() - verifySliceStartedAt >= 12) {
40615
+ await yieldToEventLoop();
40616
+ verifySliceStartedAt = performance.now();
40617
+ }
40618
+ }
40619
+ }
40620
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40621
+ files: sidecarLintFiles,
40622
+ rootDirectory,
40623
+ boundedSidecarRuleIds,
40624
+ probeAnswers
40625
+ });
40626
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
40627
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
40628
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
40246
40629
  extendsPaths: [],
40247
- ruleSelection: "sidecar"
40248
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
40630
+ ruleSelection: "sidecar",
40631
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
40632
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
40633
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
40634
+ extendsPaths: [],
40635
+ ruleSelection: "sidecar",
40636
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
40637
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
40638
+ diagnostics: [],
40639
+ didDropReactHooksJsPlugin: false,
40640
+ hadPartialFailure: false
40641
+ };
40249
40642
  const freshCacheableDiagnostics = [];
40250
40643
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
40251
40644
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
40252
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
40253
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
40254
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
40255
- let isAttributionSound = true;
40256
- for (const diagnostic of freshCacheableDiagnostics) {
40257
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
40258
- if (missFile === void 0) {
40259
- isAttributionSound = false;
40260
- break;
40261
- }
40262
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
40263
- fileDiagnostics.push(diagnostic);
40264
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
40265
- }
40266
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
40645
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
40646
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
40647
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
40267
40648
  for (const missFile of missFiles) {
40268
40649
  const cacheKey = cacheKeyByFile.get(missFile);
40269
40650
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
40270
40651
  }
40271
40652
  cache.persist();
40272
40653
  }
40654
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
40655
+ sidecarCache,
40656
+ cacheKeyByFile,
40657
+ storablePasses: [{
40658
+ files: sidecarLintFiles,
40659
+ diagnostics: boundedSidecarResult.diagnostics,
40660
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
40661
+ probesByFile: await sidecarProbesTask
40662
+ }, {
40663
+ files: missFiles,
40664
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
40665
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
40666
+ probesByFile: await missProbesTask
40667
+ }]
40668
+ });
40273
40669
  return dedupeDiagnostics([
40274
40670
  ...replayedDiagnostics,
40275
40671
  ...fullResult.diagnostics,
40276
- ...sidecarResult.diagnostics
40672
+ ...sidecarReplayedDiagnostics,
40673
+ ...boundedSidecarResult.diagnostics,
40674
+ ...unboundedSidecarResult.diagnostics
40277
40675
  ]);
40278
40676
  }
40279
40677
  const baseArgs = makeBaseArgs(configPath);
@@ -40371,6 +40769,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40371
40769
  const concurrency = yield* OxlintConcurrency;
40372
40770
  const lintBatchOrdering = yield* LintBatchOrdering;
40373
40771
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
40772
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
40374
40773
  const collectedFailures = [];
40375
40774
  const diagnostics = yield* tryPromise({
40376
40775
  try: (signal) => runOxlint({
@@ -40389,7 +40788,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40389
40788
  },
40390
40789
  onFileProgress: input.onFileProgress,
40391
40790
  perFileLintCacheEnabled,
40791
+ sidecarLintCacheEnabled,
40392
40792
  onCacheStats: input.onCacheStats,
40793
+ onSidecarStats: input.onSidecarStats,
40393
40794
  spawnTimeoutMs,
40394
40795
  outputMaxBytes,
40395
40796
  concurrency,
@@ -40811,6 +41212,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
40811
41212
  }));
40812
41213
  } catch {}
40813
41214
  };
41215
+ const pruneExpiredSocketCache = (cacheDirectory) => {
41216
+ try {
41217
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
41218
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
41219
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
41220
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
41221
+ try {
41222
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
41223
+ } catch {
41224
+ continue;
41225
+ }
41226
+ }
41227
+ } catch {}
41228
+ };
40814
41229
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40815
41230
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40816
41231
  if (cacheFile !== null) {
@@ -40938,6 +41353,7 @@ const checkSupplyChain = (input) => gen(function* () {
40938
41353
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
40939
41354
  if (dependencies.length === 0) return [];
40940
41355
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41356
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
40941
41357
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
40942
41358
  const diagnostics = [];
40943
41359
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -41199,6 +41615,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41199
41615
  let lastReportedTotalFileCount = 0;
41200
41616
  let lintCacheHitFileCount = null;
41201
41617
  let lintCacheTotalFileCount = null;
41618
+ let lintSidecarReplayedFileCount = null;
41619
+ let lintSidecarTotalFileCount = null;
41202
41620
  let deadCodeCacheHit = null;
41203
41621
  let deadCodeSummaryCacheHits = null;
41204
41622
  let deadCodeSummaryCacheMisses = null;
@@ -41221,6 +41639,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41221
41639
  lintCacheHitFileCount = cacheHitFileCount;
41222
41640
  lintCacheTotalFileCount = totalConsideredFileCount;
41223
41641
  },
41642
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
41643
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
41644
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
41645
+ },
41224
41646
  deadlineEpochMs: input.deadlineEpochMs
41225
41647
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41226
41648
  yield* set(lintFailure, {
@@ -41331,6 +41753,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41331
41753
  securityScanFailed,
41332
41754
  lintCacheHitFileCount,
41333
41755
  lintCacheTotalFileCount,
41756
+ lintSidecarReplayedFileCount,
41757
+ lintSidecarTotalFileCount,
41334
41758
  deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41335
41759
  deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
41336
41760
  deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
@@ -41646,11 +42070,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
41646
42070
  const parts = [`v=${version}`];
41647
42071
  let directory = projectDirectory;
41648
42072
  for (;;) {
41649
- for (const filename of CONFIG_FINGERPRINT_FILENAMES) try {
41650
- const stat = fs.statSync(path.join(directory, filename));
41651
- parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`);
41652
- } catch {
41653
- continue;
42073
+ for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
42074
+ const contentHash = hashFileContents(path.join(directory, filename));
42075
+ if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
41654
42076
  }
41655
42077
  const parent = path.dirname(directory);
41656
42078
  if (parent === directory) break;
@@ -43713,5 +44135,5 @@ const startLanguageServer = () => {
43713
44135
  };
43714
44136
  //#endregion
43715
44137
  export { startLanguageServer };
43716
- !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]="c5ee1532-72b6-5d6f-83ae-7bb4dccf2226")}catch(e){}}();
43717
- //# debugId=c5ee1532-72b6-5d6f-83ae-7bb4dccf2226
44138
+ !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]="35802bcf-f0e9-56fb-8cd9-6aee4dd99007")}catch(e){}}();
44139
+ //# debugId=35802bcf-f0e9-56fb-8cd9-6aee4dd99007