react-doctor 0.6.3-dev.8325343 → 0.6.3-dev.bdf8074

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]="8565c6ea-39f3-51e6-895a-ae9f7c026d0e")}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]="b55ef8b2-8e9d-512b-81de-a01f6ce3e60e")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import * as NodeChildProcess from "node:child_process";
10
10
  import { execFileSync, spawn, spawnSync } from "node:child_process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
20
  import crypto, { createHash } from "node:crypto";
21
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
21
22
  import { gzipSync } from "node:zlib";
22
23
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
23
24
  /**
@@ -32927,6 +32928,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32927
32928
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32928
32929
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32929
32930
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32931
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32932
+ const CORE_PACKAGE_VERSION = "0.6.3";
32933
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32934
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32930
32935
  const SUPPLY_CHAIN_PLUGIN = "socket";
32931
32936
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32932
32937
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -35613,6 +35618,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35613
35618
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35614
35619
  return true;
35615
35620
  } }) {};
35621
+ /**
35622
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
35623
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
35624
+ * diagnostics for every file whose dependency fingerprint (the probe set the
35625
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
35626
+ * rest. Only reachable when the per-file lint cache itself is on (the
35627
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
35628
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
35629
+ *
35630
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35631
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
35632
+ * per-file cache but run the always-fresh sidecar over every hit
35633
+ * (the pre-cache behavior).
35634
+ *
35635
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
35636
+ */
35637
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
35638
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35639
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
35640
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35641
+ if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
35642
+ return true;
35643
+ } }) {};
35644
+ /**
35645
+ * Whether the whole-project dead-code result cache
35646
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35647
+ * whose inputs (source tree, manifests, configs, analyzer version) are
35648
+ * unchanged replays the stored diagnostics instead of re-running the
35649
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
35650
+ *
35651
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35652
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
35653
+ *
35654
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
35655
+ */
35656
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
35657
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35658
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
35659
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35660
+ if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
35661
+ return true;
35662
+ } }) {};
35616
35663
  const DIAGNOSTIC_SURFACES = [
35617
35664
  "cli",
35618
35665
  "prComment",
@@ -37301,6 +37348,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37301
37348
  return [...seen].filter((pattern) => pattern.length > 0);
37302
37349
  };
37303
37350
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37351
+ const atomicWriteJson = (filePath, value) => {
37352
+ try {
37353
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
37354
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
37355
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
37356
+ NFS.renameSync(temporaryPath, filePath);
37357
+ } catch {
37358
+ return;
37359
+ }
37360
+ };
37361
+ const failOpenReadJson = (filePath, fallback) => {
37362
+ try {
37363
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
37364
+ } catch {
37365
+ return fallback;
37366
+ }
37367
+ };
37368
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37369
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37370
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37371
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37372
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37373
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
37374
+ const fingerprints = [];
37375
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37376
+ if (!isFingerprintedFile(name)) continue;
37377
+ try {
37378
+ const fileStat = NFS.statSync(absolutePath);
37379
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37380
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37381
+ } catch {}
37382
+ }
37383
+ return fingerprints.sort();
37384
+ };
37385
+ const bundledRequire$2 = createRequire(import.meta.url);
37386
+ const resolveDeslopVersion = () => {
37387
+ try {
37388
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
37389
+ return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
37390
+ } catch {
37391
+ return "unknown";
37392
+ }
37393
+ };
37394
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37395
+ schemaVersion: 1,
37396
+ coreVersion: input.coreVersion,
37397
+ deslopVersion: resolveDeslopVersion(),
37398
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37399
+ entryPatterns: input.entryPatterns,
37400
+ ignorePatterns: input.ignorePatterns,
37401
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37402
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
37403
+ })).digest("hex");
37404
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37405
+ const decodeCachedDiagnostics = (raw) => {
37406
+ try {
37407
+ for (const entry of raw) validateDiagnostic$2(entry);
37408
+ return raw;
37409
+ } catch {
37410
+ return null;
37411
+ }
37412
+ };
37413
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37414
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37415
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37416
+ return decodeCachedDiagnostics(persisted.diagnostics);
37417
+ };
37418
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37419
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37420
+ version: 1,
37421
+ key: cacheKey,
37422
+ diagnostics
37423
+ });
37424
+ };
37304
37425
  const readSystemFacts = () => ({
37305
37426
  availableCores: os.availableParallelism(),
37306
37427
  totalMemoryBytes: os.totalmem(),
@@ -37369,6 +37490,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37369
37490
  releaseSlot();
37370
37491
  }
37371
37492
  };
37493
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
37494
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
37495
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
37496
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
37497
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
37498
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
37499
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
37500
+ };
37501
+ const resolveUserCacheScope = () => {
37502
+ try {
37503
+ return String(process.getuid?.() ?? os.userInfo().username);
37504
+ } catch {
37505
+ return "shared";
37506
+ }
37507
+ };
37372
37508
  /**
37373
37509
  * Resolves a path to its canonical, symlink-free form, falling back to
37374
37510
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37422,6 +37558,14 @@ process.stdin.on("end", () => {
37422
37558
  circularDependencies: result.circularDependencies.map((cycle) => ({
37423
37559
  files: cycle.files,
37424
37560
  })),
37561
+ ...(result.incrementalCacheStats
37562
+ ? {
37563
+ summaryCacheStats: {
37564
+ hits: result.incrementalCacheStats.summaryHits,
37565
+ misses: result.incrementalCacheStats.summaryMisses,
37566
+ },
37567
+ }
37568
+ : {}),
37425
37569
  });
37426
37570
 
37427
37571
  const serializeError = (error) =>
@@ -37445,6 +37589,9 @@ process.stdin.on("end", () => {
37445
37589
  ...(workerInput.ignorePatterns.length > 0
37446
37590
  ? { ignorePatterns: workerInput.ignorePatterns }
37447
37591
  : {}),
37592
+ ...(workerInput.incrementalCachePath
37593
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
37594
+ : {}),
37448
37595
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37449
37596
  // unusedDependencies, circularDependencies). Everything else deslop can compute
37450
37597
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -37455,12 +37602,18 @@ process.stdin.on("end", () => {
37455
37602
  // are the single most expensive pass — duplicate-block detection alone was
37456
37603
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37457
37604
  // speedup on a large repo.
37458
- // Both are provably safe: the consumed graph findings are computed by their own
37605
+ // - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
37606
+ // simplifiable functions, identity wrappers, …) — ~120 ms of discarded
37607
+ // output per scan, and skipping them lets the incremental cache drop the
37608
+ // DRY-pattern summary fields (the largest slice of the cache file).
37609
+ // All are provably safe: the consumed graph findings are computed by their own
37459
37610
  // detectors, independent of these passes (confirmed byte-identical on
37460
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37461
- // needs it for path-alias resolution in the import graph.
37611
+ // excalidraw + mui-material + sentry; re-verified on sentry after the
37612
+ // reportRedundancy flip). tsConfigPath stays — the module resolver needs it
37613
+ // for path-alias resolution in the import graph.
37462
37614
  semantic: { enabled: false },
37463
37615
  reportCodeQuality: false,
37616
+ reportRedundancy: false,
37464
37617
  };
37465
37618
  const result = await analyze(defineConfig(config));
37466
37619
  emit({ ok: true, result: normalizeResult(result) });
@@ -37544,13 +37697,23 @@ const parseCircularDependencies = (value) => {
37544
37697
  }
37545
37698
  return circularDependencies;
37546
37699
  };
37700
+ const parseSummaryCacheStats = (value) => {
37701
+ if (!isRecord(value)) return void 0;
37702
+ if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
37703
+ return {
37704
+ hits: value.hits,
37705
+ misses: value.misses
37706
+ };
37707
+ };
37547
37708
  const parseDeadCodeWorkerResult = (value) => {
37548
37709
  if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
37710
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
37549
37711
  return {
37550
37712
  unusedFiles: parseUnusedFiles(value.unusedFiles),
37551
37713
  unusedExports: parseUnusedExports(value.unusedExports),
37552
37714
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
37553
- circularDependencies: parseCircularDependencies(value.circularDependencies)
37715
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
37716
+ ...summaryCacheStats ? { summaryCacheStats } : {}
37554
37717
  };
37555
37718
  };
37556
37719
  const parseDeadCodeWorkerError = (value) => {
@@ -37664,17 +37827,35 @@ const checkDeadCode = async (options) => {
37664
37827
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37665
37828
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37666
37829
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37830
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
37831
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37832
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37833
+ rootDirectory,
37834
+ entryPatterns,
37835
+ ignorePatterns,
37836
+ tsConfigPath,
37837
+ deslopJsModuleSpecifier,
37838
+ coreVersion: CORE_PACKAGE_VERSION
37839
+ }) : null;
37840
+ if (cacheKey !== null) {
37841
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37842
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
37843
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37844
+ }
37845
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
37667
37846
  const spawnAndRun = () => {
37668
37847
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37669
37848
  rootDirectory,
37670
37849
  entryPatterns,
37671
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37850
+ tsConfigPath,
37672
37851
  ignorePatterns,
37673
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37674
- parseConcurrency: options.parseConcurrency
37852
+ deslopJsModuleSpecifier,
37853
+ parseConcurrency: options.parseConcurrency,
37854
+ incrementalCachePath
37675
37855
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37676
37856
  };
37677
37857
  const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37858
+ if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
37678
37859
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37679
37860
  const diagnostics = [];
37680
37861
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37731,6 +37912,7 @@ const checkDeadCode = async (options) => {
37731
37912
  category: DEAD_CODE_CATEGORY
37732
37913
  });
37733
37914
  }
37915
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37734
37916
  return diagnostics;
37735
37917
  };
37736
37918
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37833,12 +38015,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
37833
38015
  */
37834
38016
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37835
38017
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
38018
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37836
38019
  return yield* tryPromise({
37837
38020
  try: (signal) => checkDeadCode({
37838
38021
  rootDirectory: input.rootDirectory,
37839
38022
  parseConcurrency: input.parseConcurrency,
37840
38023
  workerTimeoutMs: input.workerTimeoutMs,
37841
- abortSignal: signal
38024
+ abortSignal: signal,
38025
+ cacheEnabled,
38026
+ onCacheOutcome: input.onCacheOutcome,
38027
+ onSummaryCacheStats: input.onSummaryCacheStats
37842
38028
  }),
37843
38029
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37844
38030
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38708,7 +38894,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38708
38894
  }
38709
38895
  return enabled;
38710
38896
  };
38711
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38897
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
38712
38898
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38713
38899
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38714
38900
  const jsPlugins = [];
@@ -38720,6 +38906,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38720
38906
  if (!rule) continue;
38721
38907
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38722
38908
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38909
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
38723
38910
  if (rule.scan !== void 0) continue;
38724
38911
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38725
38912
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38764,33 +38951,152 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38764
38951
  }
38765
38952
  };
38766
38953
  };
38767
- const atomicWriteJson = (filePath, value) => {
38954
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
38955
+ const decodeFileDiagnostics = (raw) => {
38956
+ if (!Array.isArray(raw)) return null;
38768
38957
  try {
38769
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38770
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38771
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38772
- NFS.renameSync(temporaryPath, filePath);
38958
+ for (const entry of raw) validateDiagnostic$1(entry);
38959
+ return raw;
38773
38960
  } catch {
38774
- return;
38961
+ return null;
38775
38962
  }
38776
38963
  };
38777
- const failOpenReadJson = (filePath, fallback) => {
38964
+ const emptyCache$1 = () => ({
38965
+ version: 1,
38966
+ rulesets: {}
38967
+ });
38968
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
38969
+ const entries = /* @__PURE__ */ new Map();
38970
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
38971
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38972
+ const bucket = persisted.rulesets[rulesetHash];
38973
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38974
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38975
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38976
+ if (decoded !== null) entries.set(fileKey, decoded);
38977
+ }
38978
+ return entries;
38979
+ };
38980
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38981
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38982
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
38983
+ return {
38984
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38985
+ store: (fileKey, diagnostics) => {
38986
+ entries.delete(fileKey);
38987
+ entries.set(fileKey, diagnostics);
38988
+ },
38989
+ persist: () => {
38990
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
38991
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38992
+ const existingBucket = rulesets[rulesetHash];
38993
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38994
+ const ourFiles = {};
38995
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38996
+ const cappedEntries = Object.entries({
38997
+ ...existingFiles,
38998
+ ...ourFiles
38999
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
39000
+ rulesets[rulesetHash] = {
39001
+ updatedAtMs: Date.now(),
39002
+ files: Object.fromEntries(cappedEntries)
39003
+ };
39004
+ atomicWriteJson(cacheFilePath, {
39005
+ version: 1,
39006
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39007
+ });
39008
+ }
39009
+ };
39010
+ };
39011
+ const hashFileContents = (filePath) => {
38778
39012
  try {
38779
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
39013
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
38780
39014
  } catch {
38781
- return fallback;
39015
+ return null;
38782
39016
  }
38783
39017
  };
39018
+ const ABSENT_CONTENT_ANSWER = "absent";
39019
+ /**
39020
+ * Answers dependency probes for the sidecar lint cache — used both when
39021
+ * STORING an entry (turn the collector's probe paths into answers) and when
39022
+ * VERIFYING one (re-answer each stored probe against the current tree).
39023
+ * Store and verify must agree exactly, so both go through this one resolver.
39024
+ *
39025
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
39026
+ * (missing, a directory, permission failure). Source files reuse the
39027
+ * hashes the cache-partition loop already computed
39028
+ * (`contentHashByRelativePath`); everything else (package.json,
39029
+ * tsconfig, ignored files) is hashed on demand.
39030
+ * - `exists` — the existence class a module-resolution candidate probe
39031
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
39032
+ * `statSync` follows symlinks, matching the resolvers).
39033
+ *
39034
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
39035
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
39036
+ * tsconfig chain) repeat across many files' probe sets.
39037
+ */
39038
+ const createSidecarProbeAnswerResolver = (input) => {
39039
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
39040
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
39041
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
39042
+ const contentAnswer = (relativePath) => {
39043
+ const memoized = contentAnswerByPath.get(relativePath);
39044
+ if (memoized !== void 0) return memoized;
39045
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
39046
+ contentAnswerByPath.set(relativePath, answer);
39047
+ return answer;
39048
+ };
39049
+ const existsAnswer = (relativePath) => {
39050
+ const memoized = existsAnswerByPath.get(relativePath);
39051
+ if (memoized !== void 0) return memoized;
39052
+ let answer;
39053
+ try {
39054
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
39055
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
39056
+ } catch {
39057
+ answer = "none";
39058
+ }
39059
+ existsAnswerByPath.set(relativePath, answer);
39060
+ return answer;
39061
+ };
39062
+ return {
39063
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
39064
+ toRelativePath
39065
+ };
39066
+ };
38784
39067
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38785
- const decodeFileDiagnostics = (raw) => {
38786
- if (!Array.isArray(raw)) return null;
39068
+ const isProbeKind = (value) => value === "content" || value === "exists";
39069
+ const decodeSidecarFileEntry = (raw) => {
39070
+ if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
39071
+ const probes = [];
39072
+ for (const rawProbe of raw.probes) {
39073
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
39074
+ const [kind, probePath, answer] = rawProbe;
39075
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
39076
+ probes.push({
39077
+ kind,
39078
+ path: probePath,
39079
+ answer
39080
+ });
39081
+ }
38787
39082
  try {
38788
- for (const entry of raw) validateDiagnostic(entry);
38789
- return raw;
39083
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
38790
39084
  } catch {
38791
39085
  return null;
38792
39086
  }
39087
+ return {
39088
+ probes,
39089
+ diagnostics: raw.diagnostics
39090
+ };
38793
39091
  };
39092
+ const encodeSidecarFileEntry = (entry) => ({
39093
+ probes: entry.probes.map((probe) => [
39094
+ probe.kind,
39095
+ probe.path,
39096
+ probe.answer
39097
+ ]),
39098
+ diagnostics: entry.diagnostics
39099
+ });
38794
39100
  const emptyCache = () => ({
38795
39101
  version: 1,
38796
39102
  rulesets: {}
@@ -38801,20 +39107,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38801
39107
  if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38802
39108
  const bucket = persisted.rulesets[rulesetHash];
38803
39109
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38804
- for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38805
- const decoded = decodeFileDiagnostics(rawDiagnostics);
39110
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
39111
+ const decoded = decodeSidecarFileEntry(rawEntry);
38806
39112
  if (decoded !== null) entries.set(fileKey, decoded);
38807
39113
  }
38808
39114
  return entries;
38809
39115
  };
38810
- const createFileLintCache = (cacheDirectory, rulesetHash) => {
38811
- const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
39116
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39117
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
38812
39118
  const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38813
39119
  return {
38814
39120
  lookup: (fileKey) => entries.get(fileKey) ?? null,
38815
- store: (fileKey, diagnostics) => {
39121
+ store: (fileKey, entry) => {
38816
39122
  entries.delete(fileKey);
38817
- entries.set(fileKey, diagnostics);
39123
+ entries.set(fileKey, entry);
38818
39124
  },
38819
39125
  persist: () => {
38820
39126
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
@@ -38822,7 +39128,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38822
39128
  const existingBucket = rulesets[rulesetHash];
38823
39129
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38824
39130
  const ourFiles = {};
38825
- for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
39131
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
38826
39132
  const cappedEntries = Object.entries({
38827
39133
  ...existingFiles,
38828
39134
  ...ourFiles
@@ -39837,13 +40143,6 @@ const validateRuleRegistration = () => {
39837
40143
  ].filter((entry) => entry !== null).join("; ");
39838
40144
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39839
40145
  };
39840
- const hashFileContents = (filePath) => {
39841
- try {
39842
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39843
- } catch {
39844
- return null;
39845
- }
39846
- };
39847
40146
  /**
39848
40147
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39849
40148
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -39908,21 +40207,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39908
40207
  }
39909
40208
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39910
40209
  };
39911
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39912
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39913
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39914
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39915
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39916
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39917
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39918
- };
39919
- const resolveUserCacheScope = () => {
39920
- try {
39921
- return String(process.getuid?.() ?? os.userInfo().username);
39922
- } catch {
39923
- return "shared";
39924
- }
39925
- };
39926
40210
  /**
39927
40211
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39928
40212
  * the runner: once for the primary scan, once for the
@@ -39940,6 +40224,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39940
40224
  NFS.closeSync(fileHandle);
39941
40225
  }
39942
40226
  };
40227
+ /**
40228
+ * Attributes diagnostics back to the file that produced them by the
40229
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
40230
+ * attributed — the path forms don't align, so the caller must skip its cache
40231
+ * store rather than risk caching a wrong empty result for a file that
40232
+ * actually had diagnostics.
40233
+ */
40234
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
40235
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
40236
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
40237
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
40238
+ for (const diagnostic of diagnostics) {
40239
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
40240
+ if (file === void 0) return null;
40241
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
40242
+ fileDiagnostics.push(diagnostic);
40243
+ diagnosticsByFile.set(file, fileDiagnostics);
40244
+ }
40245
+ return diagnosticsByFile;
40246
+ };
40247
+ /**
40248
+ * Collects each file's cross-file dependency probes (via the plugin's
40249
+ * collectors) with their current answers. Fail-open per file: an unreadable /
40250
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
40251
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
40252
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
40253
+ * unawaited before a lint pass and awaits it at store time, hiding the
40254
+ * in-process parse + resolution cost under the subprocess wait. The loop
40255
+ * yields on the cooperative time budget so the child's stdout handling (and
40256
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
40257
+ * the module-level probe recorder never sees interleaved use. Never rejects.
40258
+ */
40259
+ const collectSidecarProbesForFiles = async (input) => {
40260
+ const buildProbes = (file) => {
40261
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
40262
+ let trace;
40263
+ try {
40264
+ trace = collectCrossFileDependencyProbes({
40265
+ absoluteFilePath,
40266
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
40267
+ ruleIds: input.boundedSidecarRuleIds
40268
+ });
40269
+ } catch {
40270
+ return null;
40271
+ }
40272
+ if (trace === null) return null;
40273
+ const probes = [];
40274
+ for (const contentPath of trace.contentPaths) {
40275
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
40276
+ probes.push({
40277
+ kind: "content",
40278
+ path: relativePath,
40279
+ answer: input.probeAnswers.answerFor("content", relativePath)
40280
+ });
40281
+ }
40282
+ for (const existencePath of trace.existencePaths) {
40283
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
40284
+ probes.push({
40285
+ kind: "exists",
40286
+ path: relativePath,
40287
+ answer: input.probeAnswers.answerFor("exists", relativePath)
40288
+ });
40289
+ }
40290
+ return probes;
40291
+ };
40292
+ const probesByFile = /* @__PURE__ */ new Map();
40293
+ let collectSliceStartedAt = performance.now();
40294
+ for (const file of input.files) {
40295
+ probesByFile.set(file, buildProbes(file));
40296
+ if (performance.now() - collectSliceStartedAt >= 12) {
40297
+ await yieldToEventLoop();
40298
+ collectSliceStartedAt = performance.now();
40299
+ }
40300
+ }
40301
+ return probesByFile;
40302
+ };
40303
+ /**
40304
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
40305
+ * the next scan can replay instead of re-linting.
40306
+ */
40307
+ const storeSidecarEntries = (input) => {
40308
+ let didStoreAnyEntry = false;
40309
+ for (const pass of input.storablePasses) {
40310
+ if (!pass.isTrusted) continue;
40311
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
40312
+ if (diagnosticsByFile === null) continue;
40313
+ for (const file of pass.files) {
40314
+ const cacheKey = input.cacheKeyByFile.get(file);
40315
+ if (cacheKey === void 0) continue;
40316
+ const probes = pass.probesByFile.get(file);
40317
+ if (probes === void 0 || probes === null) continue;
40318
+ input.sidecarCache.store(cacheKey, {
40319
+ probes,
40320
+ diagnostics: diagnosticsByFile.get(file) ?? []
40321
+ });
40322
+ didStoreAnyEntry = true;
40323
+ }
40324
+ }
40325
+ if (didStoreAnyEntry) input.sidecarCache.persist();
40326
+ };
39943
40327
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39944
40328
  /**
39945
40329
  * Detects an oxlint config-load crash caused by the optional
@@ -39981,7 +40365,7 @@ const reactHooksJsPluginDropNote = (error) => {
39981
40365
  * 6. always restore disable directives + clean up the temp dir
39982
40366
  */
39983
40367
  const runOxlint = async (options) => {
39984
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
40368
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
39985
40369
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39986
40370
  const severityControls = buildRuleSeverityControls(userConfig);
39987
40371
  validateRuleRegistration();
@@ -39999,7 +40383,8 @@ const runOxlint = async (options) => {
39999
40383
  severityControls,
40000
40384
  userPlugins,
40001
40385
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
40002
- ruleSelection: overrides.ruleSelection
40386
+ ruleSelection: overrides.ruleSelection,
40387
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
40003
40388
  });
40004
40389
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
40005
40390
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -40102,15 +40487,18 @@ const runOxlint = async (options) => {
40102
40487
  });
40103
40488
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
40104
40489
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40490
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
40105
40491
  const missFiles = [];
40106
40492
  const replayedDiagnostics = [];
40107
40493
  let hashSliceStartedAt = performance.now();
40108
40494
  for (const candidateFile of candidateFiles) {
40109
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40495
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
40496
+ const contentHash = hashFileContents(absoluteCandidatePath);
40110
40497
  if (contentHash === null) {
40111
40498
  missFiles.push(candidateFile);
40112
40499
  continue;
40113
40500
  }
40501
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
40114
40502
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
40115
40503
  cacheKeyByFile.set(candidateFile, cacheKey);
40116
40504
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -40122,43 +40510,111 @@ const runOxlint = async (options) => {
40122
40510
  }
40123
40511
  }
40124
40512
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40513
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
40514
+ extendsPaths: [],
40515
+ ruleSelection: "sidecar"
40516
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
40517
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40518
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
40519
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40520
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
40521
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
40522
+ config: buildConfig({
40523
+ extendsPaths: [],
40524
+ ruleSelection: "sidecar",
40525
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
40526
+ }),
40527
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
40528
+ ignorePatterns: combinedPatterns,
40529
+ tsconfigContent
40530
+ })) : null;
40531
+ const probeAnswers = createSidecarProbeAnswerResolver({
40532
+ rootDirectory,
40533
+ contentHashByRelativePath
40534
+ });
40535
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40536
+ files: missFiles,
40537
+ rootDirectory,
40538
+ boundedSidecarRuleIds,
40539
+ probeAnswers
40540
+ });
40125
40541
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
40126
40542
  extendsPaths: [],
40127
40543
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
40128
40544
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
40129
40545
  const missFileSet = new Set(missFiles);
40130
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
40546
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
40547
+ const sidecarReplayedDiagnostics = [];
40548
+ const sidecarLintFiles = [];
40549
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
40550
+ else {
40551
+ let verifySliceStartedAt = performance.now();
40552
+ for (const hitFile of hitFiles) {
40553
+ const cacheKey = cacheKeyByFile.get(hitFile);
40554
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
40555
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
40556
+ else sidecarLintFiles.push(hitFile);
40557
+ if (performance.now() - verifySliceStartedAt >= 12) {
40558
+ await yieldToEventLoop();
40559
+ verifySliceStartedAt = performance.now();
40560
+ }
40561
+ }
40562
+ }
40563
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40564
+ files: sidecarLintFiles,
40565
+ rootDirectory,
40566
+ boundedSidecarRuleIds,
40567
+ probeAnswers
40568
+ });
40569
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
40570
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
40571
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
40131
40572
  extendsPaths: [],
40132
- ruleSelection: "sidecar"
40133
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
40573
+ ruleSelection: "sidecar",
40574
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
40575
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
40576
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
40577
+ extendsPaths: [],
40578
+ ruleSelection: "sidecar",
40579
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
40580
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
40581
+ diagnostics: [],
40582
+ didDropReactHooksJsPlugin: false,
40583
+ hadPartialFailure: false
40584
+ };
40134
40585
  const freshCacheableDiagnostics = [];
40135
40586
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
40136
40587
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
40137
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
40138
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
40139
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
40140
- let isAttributionSound = true;
40141
- for (const diagnostic of freshCacheableDiagnostics) {
40142
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
40143
- if (missFile === void 0) {
40144
- isAttributionSound = false;
40145
- break;
40146
- }
40147
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
40148
- fileDiagnostics.push(diagnostic);
40149
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
40150
- }
40151
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
40588
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
40589
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
40590
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
40152
40591
  for (const missFile of missFiles) {
40153
40592
  const cacheKey = cacheKeyByFile.get(missFile);
40154
40593
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
40155
40594
  }
40156
40595
  cache.persist();
40157
40596
  }
40597
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
40598
+ sidecarCache,
40599
+ cacheKeyByFile,
40600
+ storablePasses: [{
40601
+ files: sidecarLintFiles,
40602
+ diagnostics: boundedSidecarResult.diagnostics,
40603
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
40604
+ probesByFile: await sidecarProbesTask
40605
+ }, {
40606
+ files: missFiles,
40607
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
40608
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
40609
+ probesByFile: await missProbesTask
40610
+ }]
40611
+ });
40158
40612
  return dedupeDiagnostics([
40159
40613
  ...replayedDiagnostics,
40160
40614
  ...fullResult.diagnostics,
40161
- ...sidecarResult.diagnostics
40615
+ ...sidecarReplayedDiagnostics,
40616
+ ...boundedSidecarResult.diagnostics,
40617
+ ...unboundedSidecarResult.diagnostics
40162
40618
  ]);
40163
40619
  }
40164
40620
  const baseArgs = makeBaseArgs(configPath);
@@ -40256,6 +40712,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40256
40712
  const concurrency = yield* OxlintConcurrency;
40257
40713
  const lintBatchOrdering = yield* LintBatchOrdering;
40258
40714
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
40715
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
40259
40716
  const collectedFailures = [];
40260
40717
  const diagnostics = yield* tryPromise({
40261
40718
  try: (signal) => runOxlint({
@@ -40274,7 +40731,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40274
40731
  },
40275
40732
  onFileProgress: input.onFileProgress,
40276
40733
  perFileLintCacheEnabled,
40734
+ sidecarLintCacheEnabled,
40277
40735
  onCacheStats: input.onCacheStats,
40736
+ onSidecarStats: input.onSidecarStats,
40278
40737
  spawnTimeoutMs,
40279
40738
  outputMaxBytes,
40280
40739
  concurrency,
@@ -41049,7 +41508,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41049
41508
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
41050
41509
  rootDirectory: scanDirectory,
41051
41510
  parseConcurrency: deadCodeParseConcurrency,
41052
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41511
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41512
+ onCacheOutcome: (didHitCache) => {
41513
+ deadCodeCacheHit = didHitCache;
41514
+ },
41515
+ onSummaryCacheStats: (stats) => {
41516
+ deadCodeSummaryCacheHits = stats.hits;
41517
+ deadCodeSummaryCacheMisses = stats.misses;
41518
+ }
41053
41519
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41054
41520
  yield* set(deadCodeFailure, {
41055
41521
  didFail: true,
@@ -41077,6 +41543,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41077
41543
  let lastReportedTotalFileCount = 0;
41078
41544
  let lintCacheHitFileCount = null;
41079
41545
  let lintCacheTotalFileCount = null;
41546
+ let lintSidecarReplayedFileCount = null;
41547
+ let lintSidecarTotalFileCount = null;
41548
+ let deadCodeCacheHit = null;
41549
+ let deadCodeSummaryCacheHits = null;
41550
+ let deadCodeSummaryCacheMisses = null;
41080
41551
  const baseLintStream = linterService.run({
41081
41552
  rootDirectory: scanDirectory,
41082
41553
  project,
@@ -41096,6 +41567,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41096
41567
  lintCacheHitFileCount = cacheHitFileCount;
41097
41568
  lintCacheTotalFileCount = totalConsideredFileCount;
41098
41569
  },
41570
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
41571
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
41572
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
41573
+ },
41099
41574
  deadlineEpochMs: input.deadlineEpochMs
41100
41575
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41101
41576
  yield* set(lintFailure, {
@@ -41206,6 +41681,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41206
41681
  securityScanFailed,
41207
41682
  lintCacheHitFileCount,
41208
41683
  lintCacheTotalFileCount,
41684
+ lintSidecarReplayedFileCount,
41685
+ lintSidecarTotalFileCount,
41686
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41687
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
41688
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
41209
41689
  suppressedRuleCounts: transform.summarizeSuppressions()
41210
41690
  };
41211
41691
  }).pipe(withSpan("runInspect", { attributes: {
@@ -41846,4 +42326,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41846
42326
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41847
42327
 
41848
42328
  //# sourceMappingURL=index.js.map
41849
- //# debugId=8565c6ea-39f3-51e6-895a-ae9f7c026d0e
42329
+ //# debugId=b55ef8b2-8e9d-512b-81de-a01f6ce3e60e