react-doctor 0.6.3-dev.ea9a775 → 0.7.0-dev.574cba6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/cli.js +462 -314
  2. package/dist/index.js +118 -31
  3. package/dist/lsp.js +122 -37
  4. package/package.json +6 -6
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]="d9c72b21-3e94-5923-b692-077f137a0078")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a88b7ed4-e1d2-587a-a826-36b16313d032")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -36130,13 +36130,14 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36130
36130
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36131
36131
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
36132
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36133
- const CORE_PACKAGE_VERSION = "0.6.3";
36133
+ const CORE_PACKAGE_VERSION = "0.7.0";
36134
36134
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36135
36135
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36136
36136
  const SUPPLY_CHAIN_PLUGIN = "socket";
36137
36137
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36138
36138
  const SUPPLY_CHAIN_CATEGORY = "Security";
36139
36139
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36140
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36140
36141
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36141
36142
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36142
36143
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -40608,22 +40609,37 @@ const failOpenReadJson = (filePath, fallback) => {
40608
40609
  return fallback;
40609
40610
  }
40610
40611
  };
40612
+ const hashFileContents = (filePath) => {
40613
+ try {
40614
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
40615
+ } catch {
40616
+ return null;
40617
+ }
40618
+ };
40611
40619
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40612
40620
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40613
40621
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40614
40622
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40615
40623
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40616
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
40617
- const fingerprints = [];
40624
+ /**
40625
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
40626
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
40627
+ * verified against the tree it started from.
40628
+ */
40629
+ const collectAnalyzedFileStats = (rootDirectory) => {
40630
+ const statByRelativePath = /* @__PURE__ */ new Map();
40618
40631
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40619
40632
  if (!isFingerprintedFile(name)) continue;
40620
40633
  try {
40621
40634
  const fileStat = NFS.statSync(absolutePath);
40622
40635
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
40623
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
40636
+ statByRelativePath.set(relativePath, {
40637
+ mtimeMs: fileStat.mtimeMs,
40638
+ size: fileStat.size
40639
+ });
40624
40640
  } catch {}
40625
40641
  }
40626
- return fingerprints.sort();
40642
+ return statByRelativePath;
40627
40643
  };
40628
40644
  const bundledRequire$2 = createRequire(import.meta.url);
40629
40645
  const resolveDeslopVersion = () => {
@@ -40635,14 +40651,13 @@ const resolveDeslopVersion = () => {
40635
40651
  }
40636
40652
  };
40637
40653
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40638
- schemaVersion: 1,
40654
+ schemaVersion: 2,
40639
40655
  coreVersion: input.coreVersion,
40640
40656
  deslopVersion: resolveDeslopVersion(),
40641
40657
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40642
40658
  entryPatterns: input.entryPatterns,
40643
40659
  ignorePatterns: input.ignorePatterns,
40644
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
40645
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
40660
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
40646
40661
  })).digest("hex");
40647
40662
  const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
40648
40663
  const decodeCachedDiagnostics = (raw) => {
@@ -40653,16 +40668,68 @@ const decodeCachedDiagnostics = (raw) => {
40653
40668
  return null;
40654
40669
  }
40655
40670
  };
40656
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
40657
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
40658
- if (persisted === null || !isRecord$1(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
40659
- return decodeCachedDiagnostics(persisted.diagnostics);
40671
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
40672
+ const lookupDeadCodeResultCache = (input) => {
40673
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
40674
+ const persisted = failOpenReadJson(cacheFilePath, null);
40675
+ if (persisted === null || !isRecord$1(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord$1(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
40676
+ const storedFileEntries = Object.entries(persisted.files);
40677
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
40678
+ const repairedFiles = {};
40679
+ let repairedCount = 0;
40680
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
40681
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
40682
+ const currentStat = input.currentFileStats.get(relativePath);
40683
+ if (currentStat === void 0) return null;
40684
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
40685
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
40686
+ repairedFiles[relativePath] = storedIdentity;
40687
+ continue;
40688
+ }
40689
+ if (currentStat.size !== storedSize) return null;
40690
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
40691
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
40692
+ repairedFiles[relativePath] = [
40693
+ currentStat.mtimeMs,
40694
+ currentStat.size,
40695
+ storedContentHash
40696
+ ];
40697
+ repairedCount += 1;
40698
+ }
40699
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
40700
+ if (diagnostics === null) return null;
40701
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
40702
+ version: 2,
40703
+ key: input.cacheKey,
40704
+ files: repairedFiles,
40705
+ diagnostics: persisted.diagnostics
40706
+ });
40707
+ return diagnostics;
40660
40708
  };
40661
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
40662
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40663
- version: 1,
40664
- key: cacheKey,
40665
- diagnostics
40709
+ const storeDeadCodeResultCache = (input) => {
40710
+ const persistedFiles = {};
40711
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
40712
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
40713
+ const contentHash = hashFileContents(absolutePath);
40714
+ if (contentHash === null) return;
40715
+ let currentStat;
40716
+ try {
40717
+ currentStat = NFS.statSync(absolutePath);
40718
+ } catch {
40719
+ return;
40720
+ }
40721
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
40722
+ persistedFiles[relativePath] = [
40723
+ snapshotStat.mtimeMs,
40724
+ snapshotStat.size,
40725
+ contentHash
40726
+ ];
40727
+ }
40728
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40729
+ version: 2,
40730
+ key: input.cacheKey,
40731
+ files: persistedFiles,
40732
+ diagnostics: input.diagnostics
40666
40733
  });
40667
40734
  };
40668
40735
  const readSystemFacts = () => ({
@@ -41072,16 +41139,22 @@ const checkDeadCode = async (options) => {
41072
41139
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
41073
41140
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
41074
41141
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
41075
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
41142
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
41143
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
41076
41144
  rootDirectory,
41077
41145
  entryPatterns,
41078
41146
  ignorePatterns,
41079
41147
  tsConfigPath,
41080
41148
  deslopJsModuleSpecifier,
41081
41149
  coreVersion: CORE_PACKAGE_VERSION
41082
- }) : null;
41083
- if (cacheKey !== null) {
41084
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
41150
+ });
41151
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
41152
+ const cachedDiagnostics = lookupDeadCodeResultCache({
41153
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41154
+ cacheKey,
41155
+ rootDirectory,
41156
+ currentFileStats: fileStatsSnapshot
41157
+ });
41085
41158
  options.onCacheOutcome?.(cachedDiagnostics !== null);
41086
41159
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41087
41160
  }
@@ -41155,7 +41228,13 @@ const checkDeadCode = async (options) => {
41155
41228
  category: DEAD_CODE_CATEGORY
41156
41229
  });
41157
41230
  }
41158
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
41231
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
41232
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41233
+ cacheKey,
41234
+ rootDirectory,
41235
+ snapshotFileStats: fileStatsSnapshot,
41236
+ diagnostics
41237
+ });
41159
41238
  return diagnostics;
41160
41239
  };
41161
41240
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -42251,13 +42330,6 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42251
42330
  }
42252
42331
  };
42253
42332
  };
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
42333
  const ABSENT_CONTENT_ANSWER = "absent";
42262
42334
  /**
42263
42335
  * Answers dependency probes for the sidecar lint cache — used both when
@@ -44398,6 +44470,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
44398
44470
  }));
44399
44471
  } catch {}
44400
44472
  };
44473
+ const pruneExpiredSocketCache = (cacheDirectory) => {
44474
+ try {
44475
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
44476
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
44477
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
44478
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
44479
+ try {
44480
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
44481
+ } catch {
44482
+ continue;
44483
+ }
44484
+ }
44485
+ } catch {}
44486
+ };
44401
44487
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
44402
44488
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
44403
44489
  if (cacheFile !== null) {
@@ -44525,6 +44611,7 @@ const checkSupplyChain = (input) => gen(function* () {
44525
44611
  const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
44526
44612
  if (dependencies.length === 0) return [];
44527
44613
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44614
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
44528
44615
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
44529
44616
  const diagnostics = [];
44530
44617
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -45410,11 +45497,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
45410
45497
  const parts = [`v=${version}`];
45411
45498
  let directory = projectDirectory;
45412
45499
  for (;;) {
45413
- for (const filename of CONFIG_FINGERPRINT_FILENAMES) try {
45414
- const stat = fs.statSync(path.join(directory, filename));
45415
- parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`);
45416
- } catch {
45417
- continue;
45500
+ for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
45501
+ const contentHash = hashFileContents(path.join(directory, filename));
45502
+ if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
45418
45503
  }
45419
45504
  const parent = path.dirname(directory);
45420
45505
  if (parent === directory) break;
@@ -45710,6 +45795,7 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
45710
45795
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
45711
45796
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
45712
45797
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
45798
+ const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
45713
45799
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
45714
45800
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
45715
45801
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
@@ -45801,7 +45887,7 @@ const makeNoopConsole = () => ({
45801
45887
  });
45802
45888
  //#endregion
45803
45889
  //#region src/cli/utils/version.ts
45804
- const VERSION = "0.6.3-dev.ea9a775";
45890
+ const VERSION = "0.7.0-dev.574cba6";
45805
45891
  //#endregion
45806
45892
  //#region src/cli/utils/json-mode.ts
45807
45893
  let context = null;
@@ -46170,13 +46256,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46170
46256
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46171
46257
  * standard `SENTRY_RELEASE` override.
46172
46258
  */
46173
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.ea9a775`;
46259
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.0-dev.574cba6`;
46174
46260
  /**
46175
46261
  * Deployment environment shown in Sentry's environment filter. Defaults to
46176
46262
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46177
46263
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46178
46264
  */
46179
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.ea9a775") ? "development" : "production");
46265
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.0-dev.574cba6") ? "development" : "production");
46180
46266
  /**
46181
46267
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46182
46268
  * (set to `0` to disable tracing) and falls back to
@@ -58254,6 +58340,276 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
58254
58340
  })).toSorted((left, right) => right.fileCount - left.fileCount);
58255
58341
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
58256
58342
  //#endregion
58343
+ //#region src/cli/utils/scan-result-cache.ts
58344
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
58345
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
58346
+ "oxlint/package.json",
58347
+ "oxlint-plugin-react-doctor/package.json",
58348
+ "deslop-js/package.json",
58349
+ "eslint-plugin-react-hooks/package.json"
58350
+ ];
58351
+ const bundledRequire = createRequire(import.meta.url);
58352
+ const normalizeForStableJson = (value) => {
58353
+ if (value === null) return null;
58354
+ if (value === void 0) return void 0;
58355
+ const valueType = typeof value;
58356
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
58357
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
58358
+ if (Array.isArray(value)) return value.map((item) => {
58359
+ const normalized = normalizeForStableJson(item);
58360
+ return normalized === void 0 ? null : normalized;
58361
+ });
58362
+ const prototype = Object.getPrototypeOf(value);
58363
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
58364
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
58365
+ const normalizedRecord = {};
58366
+ for (const key of Object.keys(value).sort()) {
58367
+ const normalized = normalizeForStableJson(value[key]);
58368
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
58369
+ }
58370
+ return normalizedRecord;
58371
+ };
58372
+ const stringifyStableJson = (value) => {
58373
+ try {
58374
+ return JSON.stringify(normalizeForStableJson(value));
58375
+ } catch {
58376
+ return null;
58377
+ }
58378
+ };
58379
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
58380
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
58381
+ const parseWorktreeStatusRecords = (statusOutput) => {
58382
+ const entries = [];
58383
+ const tokens = statusOutput.split("\0");
58384
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
58385
+ let record = tokens[tokenIndex];
58386
+ if (record.length === 0) continue;
58387
+ if (record.length < 3 || record[2] !== " ") {
58388
+ if (tokenIndex !== 0) return null;
58389
+ record = ` ${record}`;
58390
+ }
58391
+ if (record.length < 4 || record[2] !== " ") return null;
58392
+ const statusCode = record.slice(0, 2);
58393
+ const recordPath = record.slice(3);
58394
+ if (statusCode.includes("R") || statusCode.includes("C")) {
58395
+ tokenIndex += 1;
58396
+ const originPath = tokens[tokenIndex];
58397
+ if (originPath === void 0 || originPath.length === 0) return null;
58398
+ entries.push({
58399
+ statusCode,
58400
+ path: recordPath,
58401
+ originPath
58402
+ });
58403
+ continue;
58404
+ }
58405
+ entries.push({
58406
+ statusCode,
58407
+ path: recordPath
58408
+ });
58409
+ }
58410
+ return entries;
58411
+ };
58412
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
58413
+ let stats;
58414
+ try {
58415
+ stats = fs.lstatSync(absolutePath);
58416
+ } catch {
58417
+ return statusCode.includes("D") ? "deleted" : null;
58418
+ }
58419
+ if (stats.isSymbolicLink()) try {
58420
+ return `link:${fs.readlinkSync(absolutePath)}`;
58421
+ } catch {
58422
+ return null;
58423
+ }
58424
+ if (!stats.isFile()) return null;
58425
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
58426
+ return hashFileContents(absolutePath);
58427
+ };
58428
+ const buildWorktreeFingerprint = (projectDirectory) => {
58429
+ const statusOutput = runGit(projectDirectory, [
58430
+ "status",
58431
+ "--porcelain=v1",
58432
+ "-z",
58433
+ "--untracked-files=all"
58434
+ ]);
58435
+ if (statusOutput === null) return null;
58436
+ if (statusOutput.length === 0) return [];
58437
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
58438
+ if (statusEntries === null) return null;
58439
+ if (statusEntries.length > 300) return null;
58440
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
58441
+ if (repositoryRoot === null) return null;
58442
+ const dirtyEntries = [];
58443
+ for (const statusEntry of statusEntries) {
58444
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
58445
+ if (contentFingerprint === null) return null;
58446
+ dirtyEntries.push({
58447
+ ...statusEntry,
58448
+ contentFingerprint
58449
+ });
58450
+ }
58451
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
58452
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
58453
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
58454
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
58455
+ });
58456
+ };
58457
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
58458
+ const resolveDotenvFingerprint = (projectDirectory) => {
58459
+ try {
58460
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
58461
+ } catch {
58462
+ return [];
58463
+ }
58464
+ };
58465
+ const hasHiddenTrackedFileState = (projectDirectory) => {
58466
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
58467
+ if (output === null) return true;
58468
+ return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58469
+ };
58470
+ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58471
+ const readPersistedCache = (cacheFilePath) => {
58472
+ try {
58473
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58474
+ if (!isRecord(parsed) || parsed.version !== 3) return {
58475
+ version: 3,
58476
+ entries: []
58477
+ };
58478
+ if (!Array.isArray(parsed.entries)) return {
58479
+ version: 3,
58480
+ entries: []
58481
+ };
58482
+ const entries = [];
58483
+ for (const entry of parsed.entries) {
58484
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
58485
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
58486
+ entries.push(entry);
58487
+ }
58488
+ return {
58489
+ version: 3,
58490
+ entries
58491
+ };
58492
+ } catch {
58493
+ return {
58494
+ version: 3,
58495
+ entries: []
58496
+ };
58497
+ }
58498
+ };
58499
+ const writePersistedCache = (cacheFilePath, cache) => {
58500
+ try {
58501
+ fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
58502
+ const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
58503
+ fs.writeFileSync(tempPath, JSON.stringify(cache));
58504
+ fs.renameSync(tempPath, cacheFilePath);
58505
+ } catch {
58506
+ return;
58507
+ }
58508
+ };
58509
+ /**
58510
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
58511
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
58512
+ * sidecar, and dead-code caches (their `Context.Reference` defaults read the
58513
+ * same variable). Exported for the wide event's `cache.temperature`
58514
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
58515
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
58516
+ * not consulted — they leave the other subsystems live.
58517
+ */
58518
+ const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
58519
+ const resolveProjectIdentity = (projectDirectory) => {
58520
+ try {
58521
+ return fs.realpathSync.native(projectDirectory);
58522
+ } catch {
58523
+ return path.resolve(projectDirectory);
58524
+ }
58525
+ };
58526
+ const fileFingerprint = (filePath) => {
58527
+ try {
58528
+ const stat = fs.statSync(filePath);
58529
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
58530
+ } catch {
58531
+ return null;
58532
+ }
58533
+ };
58534
+ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58535
+ const fingerprints = [];
58536
+ if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
58537
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
58538
+ const packageJson = bundledRequire(specifier);
58539
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
58540
+ fingerprints.push(`${specifier}=${version}`);
58541
+ } catch {
58542
+ fingerprints.push(`${specifier}=missing`);
58543
+ }
58544
+ return fingerprints;
58545
+ };
58546
+ const buildScanResultCacheKey = (input) => {
58547
+ if (isCacheGloballyDisabled()) return null;
58548
+ if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58549
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58550
+ if (worktreeFingerprint === null) return null;
58551
+ const headSha = readHeadSha(input.projectDirectory);
58552
+ if (headSha === null) return null;
58553
+ if (stringifyStableJson(input.userConfig) === null) return null;
58554
+ const cacheKeyJson = stringifyStableJson({
58555
+ schemaVersion: 3,
58556
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
58557
+ headSha,
58558
+ worktreeFingerprint,
58559
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
58560
+ reactDoctorVersion: input.version,
58561
+ nodeVersion: process.version,
58562
+ toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
58563
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
58564
+ hasConfigOverride: input.hasConfigOverride,
58565
+ configSourceDirectory: input.configSourceDirectory,
58566
+ userConfig: input.userConfig,
58567
+ engineOptions: {
58568
+ lint: input.options.lint,
58569
+ deadCode: input.options.deadCode,
58570
+ includePaths: [...input.options.includePaths].sort(),
58571
+ customRulesOnly: input.options.customRulesOnly,
58572
+ respectInlineDisables: input.options.respectInlineDisables,
58573
+ warnings: input.options.warnings,
58574
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
58575
+ ignoredTags: [...input.options.ignoredTags].sort(),
58576
+ concurrency: input.options.concurrency,
58577
+ lintBatchOrdering: resolveLintBatchOrdering(),
58578
+ baselineRef: input.options.baseline?.ref,
58579
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
58580
+ noScore: input.options.noScore,
58581
+ isCi: input.options.isCi,
58582
+ suppressRendering: input.options.suppressRendering,
58583
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
58584
+ }
58585
+ });
58586
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
58587
+ };
58588
+ const createScanResultCache = (projectDirectory) => {
58589
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
58590
+ const persistedCache = readPersistedCache(cacheFilePath);
58591
+ const entries = /* @__PURE__ */ new Map();
58592
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58593
+ const persist = () => {
58594
+ writePersistedCache(cacheFilePath, {
58595
+ version: 3,
58596
+ entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58597
+ });
58598
+ };
58599
+ return {
58600
+ lookup: (key) => entries.get(key)?.payload ?? null,
58601
+ store: (key, payload) => {
58602
+ entries.set(key, {
58603
+ key,
58604
+ createdAtMs: Date.now(),
58605
+ payload
58606
+ });
58607
+ persist();
58608
+ }
58609
+ };
58610
+ };
58611
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
58612
+ //#endregion
58257
58613
  //#region src/cli/utils/should-block-ci.ts
58258
58614
  const shouldBlockCi = (diagnostics, blockingLevel) => {
58259
58615
  if (blockingLevel === "none") return false;
@@ -58295,6 +58651,63 @@ const readEnvBoolean = (name) => {
58295
58651
  if (value === void 0) return null;
58296
58652
  return value.toLowerCase() === "true" || value === "1";
58297
58653
  };
58654
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
58655
+ const resolveDeadCodeReuseRatio = (result) => {
58656
+ if (result.deadCodeCacheHit === true) return 1;
58657
+ const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
58658
+ if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
58659
+ return result.deadCodeCacheHit === false ? 0 : null;
58660
+ };
58661
+ /**
58662
+ * One queryable cache temperature per scan, derived from the whole stack:
58663
+ *
58664
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
58665
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
58666
+ * branch; no scan work ran).
58667
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
58668
+ * replays, a dead-code whole-result hit, or dead-code
58669
+ * summary-cache hits.
58670
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
58671
+ * off-switch is on. Granular per-cache opt-outs
58672
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
58673
+ * since the other subsystems stay live.
58674
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
58675
+ *
58676
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
58677
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
58678
+ * replay ratio, dead-code reuse), skipping subsystems that never consulted a
58679
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
58680
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
58681
+ * p50/p90-able summary. Emitted only on the success path.
58682
+ */
58683
+ const buildCacheAttributes = (input) => {
58684
+ if (input.result === void 0) return {};
58685
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
58686
+ wholeRepoHit: true,
58687
+ temperature: "turbo",
58688
+ warmth: 1
58689
+ });
58690
+ const result = input.result;
58691
+ const subsystemReuseRatios = [
58692
+ ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58693
+ ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
58694
+ resolveDeadCodeReuseRatio(result)
58695
+ ];
58696
+ let knownSubsystemCount = 0;
58697
+ let reuseRatioSum = 0;
58698
+ for (const reuseRatio of subsystemReuseRatios) {
58699
+ if (reuseRatio === null) continue;
58700
+ knownSubsystemCount += 1;
58701
+ reuseRatioSum += reuseRatio;
58702
+ }
58703
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
58704
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
58705
+ return withNamespace("cache", {
58706
+ wholeRepoHit: input.wholeRepoCacheHit ?? null,
58707
+ temperature,
58708
+ warmth
58709
+ });
58710
+ };
58298
58711
  const resolveVersionPin = (versionInput) => {
58299
58712
  if (versionInput === void 0 || versionInput.trim() === "") return null;
58300
58713
  if (versionInput === "latest") return "latest";
@@ -58400,10 +58813,10 @@ const buildOutcomeAttributes = (input) => {
58400
58813
  deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
58401
58814
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
58402
58815
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
58403
- cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
58816
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58404
58817
  sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
58405
58818
  sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
58406
- sidecarReplayRatio: result.lintSidecarTotalFileCount != null && result.lintSidecarTotalFileCount > 0 ? (result.lintSidecarReplayedFileCount ?? 0) / result.lintSidecarTotalFileCount : null
58819
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
58407
58820
  }),
58408
58821
  ...withNamespace("deadCode", {
58409
58822
  failed: input.didDeadCodeFail ?? null,
@@ -58470,7 +58883,7 @@ const buildScanAttributes = (input) => {
58470
58883
  * Projects a scan into the namespaced attribute set for its root span — the
58471
58884
  * canonical per-scan "wide event". Every attribute carries a dotted namespace
58472
58885
  * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
58473
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
58886
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
58474
58887
  * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
58475
58888
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
58476
58889
  * in the Spans dataset. Pure and exported so the projection (outcome
@@ -58484,7 +58897,8 @@ const buildScanAttributes = (input) => {
58484
58897
  const buildRunEventAttributes = (input) => toSpanAttributes({
58485
58898
  ...buildScanAttributes(input),
58486
58899
  ...buildActionAttributes(),
58487
- ...buildOutcomeAttributes(input)
58900
+ ...buildOutcomeAttributes(input),
58901
+ ...buildCacheAttributes(input)
58488
58902
  });
58489
58903
  /**
58490
58904
  * Stamps the wide-event attributes onto the run's root span. A guarded no-op
@@ -59694,275 +60108,6 @@ const resolveCliCategories = (categoryFlag) => {
59694
60108
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
59695
60109
  };
59696
60110
  //#endregion
59697
- //#region src/cli/utils/scan-result-cache.ts
59698
- const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59699
- const TOOLCHAIN_PACKAGE_SPECIFIERS = [
59700
- "oxlint",
59701
- "oxlint/package.json",
59702
- "oxlint-plugin-react-doctor",
59703
- "deslop-js/package.json",
59704
- "eslint-plugin-react-hooks/package.json"
59705
- ];
59706
- const bundledRequire = createRequire(import.meta.url);
59707
- const normalizeForStableJson = (value) => {
59708
- if (value === null) return null;
59709
- if (value === void 0) return void 0;
59710
- const valueType = typeof value;
59711
- if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
59712
- if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
59713
- if (Array.isArray(value)) return value.map((item) => {
59714
- const normalized = normalizeForStableJson(item);
59715
- return normalized === void 0 ? null : normalized;
59716
- });
59717
- const prototype = Object.getPrototypeOf(value);
59718
- if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
59719
- if (!isRecord(value)) throw new Error("Unsupported cache key object");
59720
- const normalizedRecord = {};
59721
- for (const key of Object.keys(value).sort()) {
59722
- const normalized = normalizeForStableJson(value[key]);
59723
- if (normalized !== void 0) normalizedRecord[key] = normalized;
59724
- }
59725
- return normalizedRecord;
59726
- };
59727
- const stringifyStableJson = (value) => {
59728
- try {
59729
- return JSON.stringify(normalizeForStableJson(value));
59730
- } catch {
59731
- return null;
59732
- }
59733
- };
59734
- const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
59735
- const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
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, [
59785
- "status",
59786
- "--porcelain=v1",
59787
- "-z",
59788
- "--untracked-files=all"
59789
- ]);
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
- }
59819
- };
59820
- const hasHiddenTrackedFileState = (projectDirectory) => {
59821
- const output = runGit(projectDirectory, ["ls-files", "-v"]);
59822
- if (output === null) return true;
59823
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
59824
- };
59825
- const resolveCacheFilePath = (projectDirectory) => {
59826
- const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
59827
- if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
59828
- const projectHash = hashString(projectDirectory).slice(0, 16);
59829
- return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
59830
- };
59831
- const readPersistedCache = (cacheFilePath) => {
59832
- try {
59833
- const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
59834
- if (!isRecord(parsed) || parsed.version !== 3) return {
59835
- version: 3,
59836
- entries: []
59837
- };
59838
- if (!Array.isArray(parsed.entries)) return {
59839
- version: 3,
59840
- entries: []
59841
- };
59842
- const entries = [];
59843
- for (const entry of parsed.entries) {
59844
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
59845
- if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
59846
- entries.push(entry);
59847
- }
59848
- return {
59849
- version: 3,
59850
- entries
59851
- };
59852
- } catch {
59853
- return {
59854
- version: 3,
59855
- entries: []
59856
- };
59857
- }
59858
- };
59859
- const writePersistedCache = (cacheFilePath, cache) => {
59860
- try {
59861
- fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
59862
- const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
59863
- fs.writeFileSync(tempPath, JSON.stringify(cache));
59864
- fs.renameSync(tempPath, cacheFilePath);
59865
- } catch {
59866
- return;
59867
- }
59868
- };
59869
- const isScanResultCacheDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
59870
- const resolveProjectIdentity = (projectDirectory) => {
59871
- try {
59872
- return fs.realpathSync.native(projectDirectory);
59873
- } catch {
59874
- return path.resolve(projectDirectory);
59875
- }
59876
- };
59877
- const fileFingerprint = (filePath) => {
59878
- try {
59879
- const stat = fs.statSync(filePath);
59880
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
59881
- } catch {
59882
- return null;
59883
- }
59884
- };
59885
- const resolveToolchainFingerprint = (nodeBinaryPath) => {
59886
- const fingerprints = [];
59887
- if (nodeBinaryPath !== null) {
59888
- const fingerprint = fileFingerprint(nodeBinaryPath);
59889
- if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
59890
- }
59891
- for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
59892
- const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
59893
- if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
59894
- } catch {
59895
- continue;
59896
- }
59897
- return fingerprints;
59898
- };
59899
- const buildScanResultCacheKey = (input) => {
59900
- if (isScanResultCacheDisabled()) return null;
59901
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59902
- const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
59903
- if (worktreeFingerprint === null) return null;
59904
- const headSha = readHeadSha(input.projectDirectory);
59905
- if (headSha === null) return null;
59906
- if (stringifyStableJson(input.userConfig) === null) return null;
59907
- const cacheKeyJson = stringifyStableJson({
59908
- schemaVersion: 3,
59909
- projectIdentity: resolveProjectIdentity(input.projectDirectory),
59910
- headSha,
59911
- worktreeFingerprint,
59912
- dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
59913
- reactDoctorVersion: input.version,
59914
- nodeVersion: process.version,
59915
- toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
59916
- configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
59917
- hasConfigOverride: input.hasConfigOverride,
59918
- configSourceDirectory: input.configSourceDirectory,
59919
- userConfig: input.userConfig,
59920
- engineOptions: {
59921
- lint: input.options.lint,
59922
- deadCode: input.options.deadCode,
59923
- includePaths: [...input.options.includePaths].sort(),
59924
- customRulesOnly: input.options.customRulesOnly,
59925
- respectInlineDisables: input.options.respectInlineDisables,
59926
- warnings: input.options.warnings,
59927
- adoptExistingLintConfig: input.options.adoptExistingLintConfig,
59928
- ignoredTags: [...input.options.ignoredTags].sort(),
59929
- concurrency: input.options.concurrency,
59930
- lintBatchOrdering: resolveLintBatchOrdering(),
59931
- baselineRef: input.options.baseline?.ref,
59932
- changedLineRanges: input.options.changedLineRanges ?? void 0,
59933
- noScore: input.options.noScore,
59934
- isCi: input.options.isCi,
59935
- suppressRendering: input.options.suppressRendering,
59936
- supplyChainManifestChanged: input.options.supplyChainManifestChanged
59937
- }
59938
- });
59939
- return cacheKeyJson === null ? null : hashString(cacheKeyJson);
59940
- };
59941
- const createScanResultCache = (projectDirectory) => {
59942
- const cacheFilePath = resolveCacheFilePath(projectDirectory);
59943
- const persistedCache = readPersistedCache(cacheFilePath);
59944
- const entries = /* @__PURE__ */ new Map();
59945
- for (const entry of persistedCache.entries) entries.set(entry.key, entry);
59946
- const persist = () => {
59947
- writePersistedCache(cacheFilePath, {
59948
- version: 3,
59949
- entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
59950
- });
59951
- };
59952
- return {
59953
- lookup: (key) => entries.get(key)?.payload ?? null,
59954
- store: (key, payload) => {
59955
- entries.set(key, {
59956
- key,
59957
- createdAtMs: Date.now(),
59958
- payload
59959
- });
59960
- persist();
59961
- }
59962
- };
59963
- };
59964
- const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
59965
- //#endregion
59966
60111
  //#region src/inspect.ts
59967
60112
  const silentConsole = makeNoopConsole();
59968
60113
  const runConsole = (effect) => {
@@ -60139,7 +60284,7 @@ const runBaselineComparison = async (params) => {
60139
60284
  suppressScanSummary: true,
60140
60285
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
60141
60286
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
60142
- }, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
60287
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
60143
60288
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
60144
60289
  const delta = computeDiagnosticDelta({
60145
60290
  headDiagnostics: params.headDiagnostics,
@@ -60192,7 +60337,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60192
60337
  startTime,
60193
60338
  rootSentrySpan,
60194
60339
  scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
60195
- baselineDegraded
60340
+ baselineDegraded,
60341
+ wholeRepoCacheHit: true
60196
60342
  });
60197
60343
  recordOnboardingCompletion(options);
60198
60344
  return result;
@@ -60301,6 +60447,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60301
60447
  rootSentrySpan,
60302
60448
  scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
60303
60449
  baselineDegraded,
60450
+ wholeRepoCacheHit: false,
60304
60451
  lintCacheHitFileCount: output.lintCacheHitFileCount,
60305
60452
  lintCacheTotalFileCount: output.lintCacheTotalFileCount,
60306
60453
  lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
@@ -60371,6 +60518,7 @@ const renderAndRecordScan = async (input) => {
60371
60518
  result,
60372
60519
  mode: input.scanMode,
60373
60520
  gateExempt: input.baselineDegraded,
60521
+ wholeRepoCacheHit: input.wholeRepoCacheHit,
60374
60522
  didLintFail: input.payload.didLintFail,
60375
60523
  lintFailureReasonKind: input.payload.lintFailureReasonKind,
60376
60524
  lintPartialFailureCount: input.payload.lintPartialFailures.length,
@@ -64264,4 +64412,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64264
64412
  export {};
64265
64413
 
64266
64414
  //# sourceMappingURL=cli.js.map
64267
- //# debugId=d9c72b21-3e94-5923-b692-077f137a0078
64415
+ //# debugId=a88b7ed4-e1d2-587a-a826-36b16313d032