react-doctor 0.6.3-dev.e257a5e → 0.6.3-dev.ea9a775

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="82500030-a7a7-5da8-b18e-cbdb42102dc3")}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]="d9c72b21-3e94-5923-b692-077f137a0078")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -18,6 +18,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
20
  import crypto, { createHash, randomUUID } from "node:crypto";
21
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
21
22
  import { gzipSync } from "node:zlib";
22
23
  import * as Sentry from "@sentry/node";
23
24
  import { performance as performance$1 } from "node:perf_hooks";
@@ -36128,7 +36129,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36128
36129
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36129
36130
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36130
36131
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36133
+ const CORE_PACKAGE_VERSION = "0.6.3";
36131
36134
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36135
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36132
36136
  const SUPPLY_CHAIN_PLUGIN = "socket";
36133
36137
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36134
36138
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -38814,6 +38818,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
38814
38818
  return true;
38815
38819
  } }) {};
38816
38820
  /**
38821
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
38822
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
38823
+ * diagnostics for every file whose dependency fingerprint (the probe set the
38824
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
38825
+ * rest. Only reachable when the per-file lint cache itself is on (the
38826
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
38827
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
38828
+ *
38829
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38830
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
38831
+ * per-file cache but run the always-fresh sidecar over every hit
38832
+ * (the pre-cache behavior).
38833
+ *
38834
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
38835
+ */
38836
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
38837
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38838
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
38839
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38840
+ if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
38841
+ return true;
38842
+ } }) {};
38843
+ /**
38817
38844
  * Whether the whole-project dead-code result cache
38818
38845
  * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
38819
38846
  * whose inputs (source tree, manifests, configs, analyzer version) are
@@ -40581,38 +40608,9 @@ const failOpenReadJson = (filePath, fallback) => {
40581
40608
  return fallback;
40582
40609
  }
40583
40610
  };
40584
- const ANALYZED_FILE_EXTENSIONS = new Set([
40585
- ".ts",
40586
- ".tsx",
40587
- ".js",
40588
- ".jsx",
40589
- ".mts",
40590
- ".mjs",
40591
- ".cts",
40592
- ".cjs",
40593
- ".mdx",
40594
- ".astro",
40595
- ".graphql",
40596
- ".gql",
40597
- ".css",
40598
- ".scss",
40599
- ".vue",
40600
- ".svelte"
40601
- ]);
40602
- const ANALYZED_MANIFEST_NAMES = new Set([
40603
- "package.json",
40604
- "knip.json",
40605
- "app.json",
40606
- "pnpm-workspace.yaml",
40607
- "lerna.json",
40608
- "pnpm-lock.yaml",
40609
- "package-lock.json",
40610
- "yarn.lock",
40611
- "bun.lock",
40612
- "bun.lockb",
40613
- "deno.lock",
40614
- ".gitignore"
40615
- ]);
40611
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40612
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40613
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40616
40614
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40617
40615
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40618
40616
  const collectAnalyzedFileFingerprints = (rootDirectory) => {
@@ -40638,6 +40636,7 @@ const resolveDeslopVersion = () => {
40638
40636
  };
40639
40637
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40640
40638
  schemaVersion: 1,
40639
+ coreVersion: input.coreVersion,
40641
40640
  deslopVersion: resolveDeslopVersion(),
40642
40641
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40643
40642
  entryPatterns: input.entryPatterns,
@@ -40645,10 +40644,10 @@ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON
40645
40644
  tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
40646
40645
  files: collectAnalyzedFileFingerprints(input.rootDirectory)
40647
40646
  })).digest("hex");
40648
- const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
40647
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
40649
40648
  const decodeCachedDiagnostics = (raw) => {
40650
40649
  try {
40651
- for (const entry of raw) validateDiagnostic$1(entry);
40650
+ for (const entry of raw) validateDiagnostic$2(entry);
40652
40651
  return raw;
40653
40652
  } catch {
40654
40653
  return null;
@@ -40802,6 +40801,14 @@ process.stdin.on("end", () => {
40802
40801
  circularDependencies: result.circularDependencies.map((cycle) => ({
40803
40802
  files: cycle.files,
40804
40803
  })),
40804
+ ...(result.incrementalCacheStats
40805
+ ? {
40806
+ summaryCacheStats: {
40807
+ hits: result.incrementalCacheStats.summaryHits,
40808
+ misses: result.incrementalCacheStats.summaryMisses,
40809
+ },
40810
+ }
40811
+ : {}),
40805
40812
  });
40806
40813
 
40807
40814
  const serializeError = (error) =>
@@ -40825,6 +40832,9 @@ process.stdin.on("end", () => {
40825
40832
  ...(workerInput.ignorePatterns.length > 0
40826
40833
  ? { ignorePatterns: workerInput.ignorePatterns }
40827
40834
  : {}),
40835
+ ...(workerInput.incrementalCachePath
40836
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
40837
+ : {}),
40828
40838
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
40829
40839
  // unusedDependencies, circularDependencies). Everything else deslop can compute
40830
40840
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -40835,12 +40845,18 @@ process.stdin.on("end", () => {
40835
40845
  // are the single most expensive pass — duplicate-block detection alone was
40836
40846
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
40837
40847
  // speedup on a large repo.
40838
- // Both are provably safe: the consumed graph findings are computed by their own
40848
+ // - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
40849
+ // simplifiable functions, identity wrappers, …) — ~120 ms of discarded
40850
+ // output per scan, and skipping them lets the incremental cache drop the
40851
+ // DRY-pattern summary fields (the largest slice of the cache file).
40852
+ // All are provably safe: the consumed graph findings are computed by their own
40839
40853
  // detectors, independent of these passes (confirmed byte-identical on
40840
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
40841
- // needs it for path-alias resolution in the import graph.
40854
+ // excalidraw + mui-material + sentry; re-verified on sentry after the
40855
+ // reportRedundancy flip). tsConfigPath stays — the module resolver needs it
40856
+ // for path-alias resolution in the import graph.
40842
40857
  semantic: { enabled: false },
40843
40858
  reportCodeQuality: false,
40859
+ reportRedundancy: false,
40844
40860
  };
40845
40861
  const result = await analyze(defineConfig(config));
40846
40862
  emit({ ok: true, result: normalizeResult(result) });
@@ -40924,13 +40940,23 @@ const parseCircularDependencies = (value) => {
40924
40940
  }
40925
40941
  return circularDependencies;
40926
40942
  };
40943
+ const parseSummaryCacheStats = (value) => {
40944
+ if (!isRecord$1(value)) return void 0;
40945
+ if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
40946
+ return {
40947
+ hits: value.hits,
40948
+ misses: value.misses
40949
+ };
40950
+ };
40927
40951
  const parseDeadCodeWorkerResult = (value) => {
40928
40952
  if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
40953
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
40929
40954
  return {
40930
40955
  unusedFiles: parseUnusedFiles(value.unusedFiles),
40931
40956
  unusedExports: parseUnusedExports(value.unusedExports),
40932
40957
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
40933
- circularDependencies: parseCircularDependencies(value.circularDependencies)
40958
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
40959
+ ...summaryCacheStats ? { summaryCacheStats } : {}
40934
40960
  };
40935
40961
  };
40936
40962
  const parseDeadCodeWorkerError = (value) => {
@@ -41051,13 +41077,15 @@ const checkDeadCode = async (options) => {
41051
41077
  entryPatterns,
41052
41078
  ignorePatterns,
41053
41079
  tsConfigPath,
41054
- deslopJsModuleSpecifier
41080
+ deslopJsModuleSpecifier,
41081
+ coreVersion: CORE_PACKAGE_VERSION
41055
41082
  }) : null;
41056
41083
  if (cacheKey !== null) {
41057
41084
  const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
41058
41085
  options.onCacheOutcome?.(cachedDiagnostics !== null);
41059
41086
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41060
41087
  }
41088
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
41061
41089
  const spawnAndRun = () => {
41062
41090
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
41063
41091
  rootDirectory,
@@ -41065,10 +41093,12 @@ const checkDeadCode = async (options) => {
41065
41093
  tsConfigPath,
41066
41094
  ignorePatterns,
41067
41095
  deslopJsModuleSpecifier,
41068
- parseConcurrency: options.parseConcurrency
41096
+ parseConcurrency: options.parseConcurrency,
41097
+ incrementalCachePath
41069
41098
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
41070
41099
  };
41071
41100
  const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
41101
+ if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
41072
41102
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
41073
41103
  const diagnostics = [];
41074
41104
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -41236,7 +41266,8 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
41236
41266
  workerTimeoutMs: input.workerTimeoutMs,
41237
41267
  abortSignal: signal,
41238
41268
  cacheEnabled,
41239
- onCacheOutcome: input.onCacheOutcome
41269
+ onCacheOutcome: input.onCacheOutcome,
41270
+ onSummaryCacheStats: input.onSummaryCacheStats
41240
41271
  }),
41241
41272
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
41242
41273
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -42106,7 +42137,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
42106
42137
  }
42107
42138
  return enabled;
42108
42139
  };
42109
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
42140
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
42110
42141
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
42111
42142
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42112
42143
  const jsPlugins = [];
@@ -42118,6 +42149,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42118
42149
  if (!rule) continue;
42119
42150
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42120
42151
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42152
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
42121
42153
  if (rule.scan !== void 0) continue;
42122
42154
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42123
42155
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -42162,23 +42194,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42162
42194
  }
42163
42195
  };
42164
42196
  };
42165
- const validateDiagnostic = decodeUnknownSync(Diagnostic);
42197
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
42166
42198
  const decodeFileDiagnostics = (raw) => {
42167
42199
  if (!Array.isArray(raw)) return null;
42168
42200
  try {
42169
- for (const entry of raw) validateDiagnostic(entry);
42201
+ for (const entry of raw) validateDiagnostic$1(entry);
42170
42202
  return raw;
42171
42203
  } catch {
42172
42204
  return null;
42173
42205
  }
42174
42206
  };
42175
- const emptyCache = () => ({
42207
+ const emptyCache$1 = () => ({
42176
42208
  version: 1,
42177
42209
  rulesets: {}
42178
42210
  });
42179
- const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42211
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
42180
42212
  const entries = /* @__PURE__ */ new Map();
42181
- const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42213
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
42182
42214
  if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42183
42215
  const bucket = persisted.rulesets[rulesetHash];
42184
42216
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
@@ -42190,7 +42222,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42190
42222
  };
42191
42223
  const createFileLintCache = (cacheDirectory, rulesetHash) => {
42192
42224
  const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42193
- const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42225
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
42194
42226
  return {
42195
42227
  lookup: (fileKey) => entries.get(fileKey) ?? null,
42196
42228
  store: (fileKey, diagnostics) => {
@@ -42198,7 +42230,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42198
42230
  entries.set(fileKey, diagnostics);
42199
42231
  },
42200
42232
  persist: () => {
42201
- const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42233
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
42202
42234
  const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42203
42235
  const existingBucket = rulesets[rulesetHash];
42204
42236
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
@@ -42219,6 +42251,142 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42219
42251
  }
42220
42252
  };
42221
42253
  };
42254
+ const hashFileContents = (filePath) => {
42255
+ try {
42256
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
42257
+ } catch {
42258
+ return null;
42259
+ }
42260
+ };
42261
+ const ABSENT_CONTENT_ANSWER = "absent";
42262
+ /**
42263
+ * Answers dependency probes for the sidecar lint cache — used both when
42264
+ * STORING an entry (turn the collector's probe paths into answers) and when
42265
+ * VERIFYING one (re-answer each stored probe against the current tree).
42266
+ * Store and verify must agree exactly, so both go through this one resolver.
42267
+ *
42268
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
42269
+ * (missing, a directory, permission failure). Source files reuse the
42270
+ * hashes the cache-partition loop already computed
42271
+ * (`contentHashByRelativePath`); everything else (package.json,
42272
+ * tsconfig, ignored files) is hashed on demand.
42273
+ * - `exists` — the existence class a module-resolution candidate probe
42274
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
42275
+ * `statSync` follows symlinks, matching the resolvers).
42276
+ *
42277
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
42278
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
42279
+ * tsconfig chain) repeat across many files' probe sets.
42280
+ */
42281
+ const createSidecarProbeAnswerResolver = (input) => {
42282
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
42283
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
42284
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
42285
+ const contentAnswer = (relativePath) => {
42286
+ const memoized = contentAnswerByPath.get(relativePath);
42287
+ if (memoized !== void 0) return memoized;
42288
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
42289
+ contentAnswerByPath.set(relativePath, answer);
42290
+ return answer;
42291
+ };
42292
+ const existsAnswer = (relativePath) => {
42293
+ const memoized = existsAnswerByPath.get(relativePath);
42294
+ if (memoized !== void 0) return memoized;
42295
+ let answer;
42296
+ try {
42297
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
42298
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
42299
+ } catch {
42300
+ answer = "none";
42301
+ }
42302
+ existsAnswerByPath.set(relativePath, answer);
42303
+ return answer;
42304
+ };
42305
+ return {
42306
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
42307
+ toRelativePath
42308
+ };
42309
+ };
42310
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
42311
+ const isProbeKind = (value) => value === "content" || value === "exists";
42312
+ const decodeSidecarFileEntry = (raw) => {
42313
+ if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
42314
+ const probes = [];
42315
+ for (const rawProbe of raw.probes) {
42316
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
42317
+ const [kind, probePath, answer] = rawProbe;
42318
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
42319
+ probes.push({
42320
+ kind,
42321
+ path: probePath,
42322
+ answer
42323
+ });
42324
+ }
42325
+ try {
42326
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
42327
+ } catch {
42328
+ return null;
42329
+ }
42330
+ return {
42331
+ probes,
42332
+ diagnostics: raw.diagnostics
42333
+ };
42334
+ };
42335
+ const encodeSidecarFileEntry = (entry) => ({
42336
+ probes: entry.probes.map((probe) => [
42337
+ probe.kind,
42338
+ probe.path,
42339
+ probe.answer
42340
+ ]),
42341
+ diagnostics: entry.diagnostics
42342
+ });
42343
+ const emptyCache = () => ({
42344
+ version: 1,
42345
+ rulesets: {}
42346
+ });
42347
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42348
+ const entries = /* @__PURE__ */ new Map();
42349
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42350
+ if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42351
+ const bucket = persisted.rulesets[rulesetHash];
42352
+ if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42353
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
42354
+ const decoded = decodeSidecarFileEntry(rawEntry);
42355
+ if (decoded !== null) entries.set(fileKey, decoded);
42356
+ }
42357
+ return entries;
42358
+ };
42359
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42360
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
42361
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42362
+ return {
42363
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
42364
+ store: (fileKey, entry) => {
42365
+ entries.delete(fileKey);
42366
+ entries.set(fileKey, entry);
42367
+ },
42368
+ persist: () => {
42369
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42370
+ const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42371
+ const existingBucket = rulesets[rulesetHash];
42372
+ const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42373
+ const ourFiles = {};
42374
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
42375
+ const cappedEntries = Object.entries({
42376
+ ...existingFiles,
42377
+ ...ourFiles
42378
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
42379
+ rulesets[rulesetHash] = {
42380
+ updatedAtMs: Date.now(),
42381
+ files: Object.fromEntries(cappedEntries)
42382
+ };
42383
+ atomicWriteJson(cacheFilePath, {
42384
+ version: 1,
42385
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42386
+ });
42387
+ }
42388
+ };
42389
+ };
42222
42390
  const bundledRequire$3 = createRequire(import.meta.url);
42223
42391
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
42224
42392
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -43218,13 +43386,6 @@ const validateRuleRegistration = () => {
43218
43386
  ].filter((entry) => entry !== null).join("; ");
43219
43387
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
43220
43388
  };
43221
- const hashFileContents = (filePath) => {
43222
- try {
43223
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
43224
- } catch {
43225
- return null;
43226
- }
43227
- };
43228
43389
  /**
43229
43390
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
43230
43391
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -43306,6 +43467,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
43306
43467
  NFS.closeSync(fileHandle);
43307
43468
  }
43308
43469
  };
43470
+ /**
43471
+ * Attributes diagnostics back to the file that produced them by the
43472
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
43473
+ * attributed — the path forms don't align, so the caller must skip its cache
43474
+ * store rather than risk caching a wrong empty result for a file that
43475
+ * actually had diagnostics.
43476
+ */
43477
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
43478
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
43479
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
43480
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
43481
+ for (const diagnostic of diagnostics) {
43482
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
43483
+ if (file === void 0) return null;
43484
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
43485
+ fileDiagnostics.push(diagnostic);
43486
+ diagnosticsByFile.set(file, fileDiagnostics);
43487
+ }
43488
+ return diagnosticsByFile;
43489
+ };
43490
+ /**
43491
+ * Collects each file's cross-file dependency probes (via the plugin's
43492
+ * collectors) with their current answers. Fail-open per file: an unreadable /
43493
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
43494
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
43495
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
43496
+ * unawaited before a lint pass and awaits it at store time, hiding the
43497
+ * in-process parse + resolution cost under the subprocess wait. The loop
43498
+ * yields on the cooperative time budget so the child's stdout handling (and
43499
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
43500
+ * the module-level probe recorder never sees interleaved use. Never rejects.
43501
+ */
43502
+ const collectSidecarProbesForFiles = async (input) => {
43503
+ const buildProbes = (file) => {
43504
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
43505
+ let trace;
43506
+ try {
43507
+ trace = collectCrossFileDependencyProbes({
43508
+ absoluteFilePath,
43509
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
43510
+ ruleIds: input.boundedSidecarRuleIds
43511
+ });
43512
+ } catch {
43513
+ return null;
43514
+ }
43515
+ if (trace === null) return null;
43516
+ const probes = [];
43517
+ for (const contentPath of trace.contentPaths) {
43518
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
43519
+ probes.push({
43520
+ kind: "content",
43521
+ path: relativePath,
43522
+ answer: input.probeAnswers.answerFor("content", relativePath)
43523
+ });
43524
+ }
43525
+ for (const existencePath of trace.existencePaths) {
43526
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
43527
+ probes.push({
43528
+ kind: "exists",
43529
+ path: relativePath,
43530
+ answer: input.probeAnswers.answerFor("exists", relativePath)
43531
+ });
43532
+ }
43533
+ return probes;
43534
+ };
43535
+ const probesByFile = /* @__PURE__ */ new Map();
43536
+ let collectSliceStartedAt = performance.now();
43537
+ for (const file of input.files) {
43538
+ probesByFile.set(file, buildProbes(file));
43539
+ if (performance.now() - collectSliceStartedAt >= 12) {
43540
+ await yieldToEventLoop();
43541
+ collectSliceStartedAt = performance.now();
43542
+ }
43543
+ }
43544
+ return probesByFile;
43545
+ };
43546
+ /**
43547
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
43548
+ * the next scan can replay instead of re-linting.
43549
+ */
43550
+ const storeSidecarEntries = (input) => {
43551
+ let didStoreAnyEntry = false;
43552
+ for (const pass of input.storablePasses) {
43553
+ if (!pass.isTrusted) continue;
43554
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
43555
+ if (diagnosticsByFile === null) continue;
43556
+ for (const file of pass.files) {
43557
+ const cacheKey = input.cacheKeyByFile.get(file);
43558
+ if (cacheKey === void 0) continue;
43559
+ const probes = pass.probesByFile.get(file);
43560
+ if (probes === void 0 || probes === null) continue;
43561
+ input.sidecarCache.store(cacheKey, {
43562
+ probes,
43563
+ diagnostics: diagnosticsByFile.get(file) ?? []
43564
+ });
43565
+ didStoreAnyEntry = true;
43566
+ }
43567
+ }
43568
+ if (didStoreAnyEntry) input.sidecarCache.persist();
43569
+ };
43309
43570
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
43310
43571
  /**
43311
43572
  * Detects an oxlint config-load crash caused by the optional
@@ -43347,7 +43608,7 @@ const reactHooksJsPluginDropNote = (error) => {
43347
43608
  * 6. always restore disable directives + clean up the temp dir
43348
43609
  */
43349
43610
  const runOxlint = async (options) => {
43350
- 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;
43611
+ 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;
43351
43612
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43352
43613
  const severityControls = buildRuleSeverityControls(userConfig);
43353
43614
  validateRuleRegistration();
@@ -43365,7 +43626,8 @@ const runOxlint = async (options) => {
43365
43626
  severityControls,
43366
43627
  userPlugins,
43367
43628
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
43368
- ruleSelection: overrides.ruleSelection
43629
+ ruleSelection: overrides.ruleSelection,
43630
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
43369
43631
  });
43370
43632
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
43371
43633
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -43468,15 +43730,18 @@ const runOxlint = async (options) => {
43468
43730
  });
43469
43731
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
43470
43732
  const cacheKeyByFile = /* @__PURE__ */ new Map();
43733
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
43471
43734
  const missFiles = [];
43472
43735
  const replayedDiagnostics = [];
43473
43736
  let hashSliceStartedAt = performance.now();
43474
43737
  for (const candidateFile of candidateFiles) {
43475
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
43738
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
43739
+ const contentHash = hashFileContents(absoluteCandidatePath);
43476
43740
  if (contentHash === null) {
43477
43741
  missFiles.push(candidateFile);
43478
43742
  continue;
43479
43743
  }
43744
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
43480
43745
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
43481
43746
  cacheKeyByFile.set(candidateFile, cacheKey);
43482
43747
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -43488,43 +43753,111 @@ const runOxlint = async (options) => {
43488
43753
  }
43489
43754
  }
43490
43755
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
43756
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
43757
+ extendsPaths: [],
43758
+ ruleSelection: "sidecar"
43759
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
43760
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43761
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
43762
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43763
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
43764
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
43765
+ config: buildConfig({
43766
+ extendsPaths: [],
43767
+ ruleSelection: "sidecar",
43768
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
43769
+ }),
43770
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
43771
+ ignorePatterns: combinedPatterns,
43772
+ tsconfigContent
43773
+ })) : null;
43774
+ const probeAnswers = createSidecarProbeAnswerResolver({
43775
+ rootDirectory,
43776
+ contentHashByRelativePath
43777
+ });
43778
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43779
+ files: missFiles,
43780
+ rootDirectory,
43781
+ boundedSidecarRuleIds,
43782
+ probeAnswers
43783
+ });
43491
43784
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
43492
43785
  extendsPaths: [],
43493
43786
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
43494
43787
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
43495
43788
  const missFileSet = new Set(missFiles);
43496
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
43789
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
43790
+ const sidecarReplayedDiagnostics = [];
43791
+ const sidecarLintFiles = [];
43792
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
43793
+ else {
43794
+ let verifySliceStartedAt = performance.now();
43795
+ for (const hitFile of hitFiles) {
43796
+ const cacheKey = cacheKeyByFile.get(hitFile);
43797
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
43798
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
43799
+ else sidecarLintFiles.push(hitFile);
43800
+ if (performance.now() - verifySliceStartedAt >= 12) {
43801
+ await yieldToEventLoop();
43802
+ verifySliceStartedAt = performance.now();
43803
+ }
43804
+ }
43805
+ }
43806
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43807
+ files: sidecarLintFiles,
43808
+ rootDirectory,
43809
+ boundedSidecarRuleIds,
43810
+ probeAnswers
43811
+ });
43812
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
43813
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
43814
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
43497
43815
  extendsPaths: [],
43498
- ruleSelection: "sidecar"
43499
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
43816
+ ruleSelection: "sidecar",
43817
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
43818
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
43819
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
43820
+ extendsPaths: [],
43821
+ ruleSelection: "sidecar",
43822
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
43823
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
43824
+ diagnostics: [],
43825
+ didDropReactHooksJsPlugin: false,
43826
+ hadPartialFailure: false
43827
+ };
43500
43828
  const freshCacheableDiagnostics = [];
43501
43829
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
43502
43830
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
43503
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
43504
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
43505
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
43506
- let isAttributionSound = true;
43507
- for (const diagnostic of freshCacheableDiagnostics) {
43508
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
43509
- if (missFile === void 0) {
43510
- isAttributionSound = false;
43511
- break;
43512
- }
43513
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
43514
- fileDiagnostics.push(diagnostic);
43515
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
43516
- }
43517
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
43831
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
43832
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
43833
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
43518
43834
  for (const missFile of missFiles) {
43519
43835
  const cacheKey = cacheKeyByFile.get(missFile);
43520
43836
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
43521
43837
  }
43522
43838
  cache.persist();
43523
43839
  }
43840
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
43841
+ sidecarCache,
43842
+ cacheKeyByFile,
43843
+ storablePasses: [{
43844
+ files: sidecarLintFiles,
43845
+ diagnostics: boundedSidecarResult.diagnostics,
43846
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
43847
+ probesByFile: await sidecarProbesTask
43848
+ }, {
43849
+ files: missFiles,
43850
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
43851
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
43852
+ probesByFile: await missProbesTask
43853
+ }]
43854
+ });
43524
43855
  return dedupeDiagnostics([
43525
43856
  ...replayedDiagnostics,
43526
43857
  ...fullResult.diagnostics,
43527
- ...sidecarResult.diagnostics
43858
+ ...sidecarReplayedDiagnostics,
43859
+ ...boundedSidecarResult.diagnostics,
43860
+ ...unboundedSidecarResult.diagnostics
43528
43861
  ]);
43529
43862
  }
43530
43863
  const baseArgs = makeBaseArgs(configPath);
@@ -43622,6 +43955,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43622
43955
  const concurrency = yield* OxlintConcurrency;
43623
43956
  const lintBatchOrdering = yield* LintBatchOrdering;
43624
43957
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
43958
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
43625
43959
  const collectedFailures = [];
43626
43960
  const diagnostics = yield* tryPromise({
43627
43961
  try: (signal) => runOxlint({
@@ -43640,7 +43974,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43640
43974
  },
43641
43975
  onFileProgress: input.onFileProgress,
43642
43976
  perFileLintCacheEnabled,
43977
+ sidecarLintCacheEnabled,
43643
43978
  onCacheStats: input.onCacheStats,
43979
+ onSidecarStats: input.onSidecarStats,
43644
43980
  spawnTimeoutMs,
43645
43981
  outputMaxBytes,
43646
43982
  concurrency,
@@ -44418,6 +44754,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44418
44754
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
44419
44755
  onCacheOutcome: (didHitCache) => {
44420
44756
  deadCodeCacheHit = didHitCache;
44757
+ },
44758
+ onSummaryCacheStats: (stats) => {
44759
+ deadCodeSummaryCacheHits = stats.hits;
44760
+ deadCodeSummaryCacheMisses = stats.misses;
44421
44761
  }
44422
44762
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44423
44763
  yield* set(deadCodeFailure, {
@@ -44446,7 +44786,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44446
44786
  let lastReportedTotalFileCount = 0;
44447
44787
  let lintCacheHitFileCount = null;
44448
44788
  let lintCacheTotalFileCount = null;
44789
+ let lintSidecarReplayedFileCount = null;
44790
+ let lintSidecarTotalFileCount = null;
44449
44791
  let deadCodeCacheHit = null;
44792
+ let deadCodeSummaryCacheHits = null;
44793
+ let deadCodeSummaryCacheMisses = null;
44450
44794
  const baseLintStream = linterService.run({
44451
44795
  rootDirectory: scanDirectory,
44452
44796
  project,
@@ -44466,6 +44810,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44466
44810
  lintCacheHitFileCount = cacheHitFileCount;
44467
44811
  lintCacheTotalFileCount = totalConsideredFileCount;
44468
44812
  },
44813
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
44814
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
44815
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
44816
+ },
44469
44817
  deadlineEpochMs: input.deadlineEpochMs
44470
44818
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44471
44819
  yield* set(lintFailure, {
@@ -44576,7 +44924,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44576
44924
  securityScanFailed,
44577
44925
  lintCacheHitFileCount,
44578
44926
  lintCacheTotalFileCount,
44927
+ lintSidecarReplayedFileCount,
44928
+ lintSidecarTotalFileCount,
44579
44929
  deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
44930
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
44931
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
44580
44932
  suppressedRuleCounts: transform.summarizeSuppressions()
44581
44933
  };
44582
44934
  }).pipe(withSpan("runInspect", { attributes: {
@@ -45449,7 +45801,7 @@ const makeNoopConsole = () => ({
45449
45801
  });
45450
45802
  //#endregion
45451
45803
  //#region src/cli/utils/version.ts
45452
- const VERSION = "0.6.3-dev.e257a5e";
45804
+ const VERSION = "0.6.3-dev.ea9a775";
45453
45805
  //#endregion
45454
45806
  //#region src/cli/utils/json-mode.ts
45455
45807
  let context = null;
@@ -45818,13 +46170,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45818
46170
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45819
46171
  * standard `SENTRY_RELEASE` override.
45820
46172
  */
45821
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.e257a5e`;
46173
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.ea9a775`;
45822
46174
  /**
45823
46175
  * Deployment environment shown in Sentry's environment filter. Defaults to
45824
46176
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45825
46177
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45826
46178
  */
45827
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.e257a5e") ? "development" : "production");
46179
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.ea9a775") ? "development" : "production");
45828
46180
  /**
45829
46181
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45830
46182
  * (set to `0` to disable tracing) and falls back to
@@ -58048,12 +58400,17 @@ const buildOutcomeAttributes = (input) => {
58048
58400
  deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
58049
58401
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
58050
58402
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
58051
- cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
58403
+ cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
58404
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
58405
+ sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
58406
+ sidecarReplayRatio: result.lintSidecarTotalFileCount != null && result.lintSidecarTotalFileCount > 0 ? (result.lintSidecarReplayedFileCount ?? 0) / result.lintSidecarTotalFileCount : null
58052
58407
  }),
58053
58408
  ...withNamespace("deadCode", {
58054
58409
  failed: input.didDeadCodeFail ?? null,
58055
58410
  overlapped: input.deadCodeOverlapped ?? null,
58056
- cacheHit: result.deadCodeCacheHit ?? null
58411
+ cacheHit: result.deadCodeCacheHit ?? null,
58412
+ summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
58413
+ summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
58057
58414
  }),
58058
58415
  ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
58059
58416
  ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
@@ -59376,13 +59733,89 @@ const stringifyStableJson = (value) => {
59376
59733
  };
59377
59734
  const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
59378
59735
  const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
59379
- const isWorktreeClean = (projectDirectory) => {
59380
- const status = runGit(projectDirectory, [
59736
+ const parseWorktreeStatusRecords = (statusOutput) => {
59737
+ const entries = [];
59738
+ const tokens = statusOutput.split("\0");
59739
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
59740
+ let record = tokens[tokenIndex];
59741
+ if (record.length === 0) continue;
59742
+ if (record.length < 3 || record[2] !== " ") {
59743
+ if (tokenIndex !== 0) return null;
59744
+ record = ` ${record}`;
59745
+ }
59746
+ if (record.length < 4 || record[2] !== " ") return null;
59747
+ const statusCode = record.slice(0, 2);
59748
+ const recordPath = record.slice(3);
59749
+ if (statusCode.includes("R") || statusCode.includes("C")) {
59750
+ tokenIndex += 1;
59751
+ const originPath = tokens[tokenIndex];
59752
+ if (originPath === void 0 || originPath.length === 0) return null;
59753
+ entries.push({
59754
+ statusCode,
59755
+ path: recordPath,
59756
+ originPath
59757
+ });
59758
+ continue;
59759
+ }
59760
+ entries.push({
59761
+ statusCode,
59762
+ path: recordPath
59763
+ });
59764
+ }
59765
+ return entries;
59766
+ };
59767
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59768
+ let stats;
59769
+ try {
59770
+ stats = fs.lstatSync(absolutePath);
59771
+ } catch {
59772
+ return statusCode.includes("D") ? "deleted" : null;
59773
+ }
59774
+ if (stats.isSymbolicLink()) try {
59775
+ return `link:${fs.readlinkSync(absolutePath)}`;
59776
+ } catch {
59777
+ return null;
59778
+ }
59779
+ if (!stats.isFile()) return null;
59780
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59781
+ return hashFileContents(absolutePath);
59782
+ };
59783
+ const buildWorktreeFingerprint = (projectDirectory) => {
59784
+ const statusOutput = runGit(projectDirectory, [
59381
59785
  "status",
59382
59786
  "--porcelain=v1",
59383
- "--untracked-files=normal"
59787
+ "-z",
59788
+ "--untracked-files=all"
59384
59789
  ]);
59385
- return status !== null && status.length === 0;
59790
+ if (statusOutput === null) return null;
59791
+ if (statusOutput.length === 0) return [];
59792
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
59793
+ if (statusEntries === null) return null;
59794
+ if (statusEntries.length > 300) return null;
59795
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
59796
+ if (repositoryRoot === null) return null;
59797
+ const dirtyEntries = [];
59798
+ for (const statusEntry of statusEntries) {
59799
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
59800
+ if (contentFingerprint === null) return null;
59801
+ dirtyEntries.push({
59802
+ ...statusEntry,
59803
+ contentFingerprint
59804
+ });
59805
+ }
59806
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
59807
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
59808
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
59809
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
59810
+ });
59811
+ };
59812
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
59813
+ const resolveDotenvFingerprint = (projectDirectory) => {
59814
+ try {
59815
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${fileFingerprint(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
59816
+ } catch {
59817
+ return [];
59818
+ }
59386
59819
  };
59387
59820
  const hasHiddenTrackedFileState = (projectDirectory) => {
59388
59821
  const output = runGit(projectDirectory, ["ls-files", "-v"]);
@@ -59465,8 +59898,9 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
59465
59898
  };
59466
59899
  const buildScanResultCacheKey = (input) => {
59467
59900
  if (isScanResultCacheDisabled()) return null;
59468
- if (!isWorktreeClean(input.projectDirectory)) return null;
59469
59901
  if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59902
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
59903
+ if (worktreeFingerprint === null) return null;
59470
59904
  const headSha = readHeadSha(input.projectDirectory);
59471
59905
  if (headSha === null) return null;
59472
59906
  if (stringifyStableJson(input.userConfig) === null) return null;
@@ -59474,6 +59908,8 @@ const buildScanResultCacheKey = (input) => {
59474
59908
  schemaVersion: 3,
59475
59909
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
59476
59910
  headSha,
59911
+ worktreeFingerprint,
59912
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
59477
59913
  reactDoctorVersion: input.version,
59478
59914
  nodeVersion: process.version,
59479
59915
  toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
@@ -59867,7 +60303,11 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59867
60303
  baselineDegraded,
59868
60304
  lintCacheHitFileCount: output.lintCacheHitFileCount,
59869
60305
  lintCacheTotalFileCount: output.lintCacheTotalFileCount,
59870
- deadCodeCacheHit: output.deadCodeCacheHit
60306
+ lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
60307
+ lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
60308
+ deadCodeCacheHit: output.deadCodeCacheHit,
60309
+ deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
60310
+ deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
59871
60311
  });
59872
60312
  recordOnboardingCompletion(options);
59873
60313
  return result;
@@ -59902,7 +60342,11 @@ const renderAndRecordScan = async (input) => {
59902
60342
  scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
59903
60343
  lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
59904
60344
  lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
60345
+ lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
60346
+ lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
59905
60347
  deadCodeCacheHit: input.deadCodeCacheHit ?? null,
60348
+ deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
60349
+ deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
59906
60350
  baselineDelta: input.payload.baselineDelta
59907
60351
  }), input.options.silent));
59908
60352
  const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
@@ -59941,7 +60385,7 @@ const renderAndRecordScan = async (input) => {
59941
60385
  return result;
59942
60386
  };
59943
60387
  const finalizeAndRender = (input) => gen(function* () {
59944
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, deadCodeCacheHit, baselineDelta } = input;
60388
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
59945
60389
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
59946
60390
  didLintFail,
59947
60391
  lintFailureReason,
@@ -59965,7 +60409,15 @@ const finalizeAndRender = (input) => gen(function* () {
59965
60409
  lintCacheHitFileCount,
59966
60410
  lintCacheTotalFileCount
59967
60411
  } : {},
60412
+ ...lintSidecarTotalFileCount !== null ? {
60413
+ lintSidecarReplayedFileCount,
60414
+ lintSidecarTotalFileCount
60415
+ } : {},
59968
60416
  ...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
60417
+ ...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
60418
+ deadCodeSummaryCacheHits,
60419
+ deadCodeSummaryCacheMisses
60420
+ } : {},
59969
60421
  ...baselineDelta ? { baselineDelta } : {}
59970
60422
  });
59971
60423
  if (options.suppressRendering) return buildResult();
@@ -63812,4 +64264,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63812
64264
  export {};
63813
64265
 
63814
64266
  //# sourceMappingURL=cli.js.map
63815
- //# debugId=82500030-a7a7-5da8-b18e-cbdb42102dc3
64267
+ //# debugId=d9c72b21-3e94-5923-b692-077f137a0078