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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b55ef8b2-8e9d-512b-81de-a01f6ce3e60e")}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]="5c52dece-7b5e-5102-9381-7d05fa45734f")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -32929,13 +32929,14 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32929
32929
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32930
32930
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32931
32931
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32932
- const CORE_PACKAGE_VERSION = "0.6.3";
32932
+ const CORE_PACKAGE_VERSION = "0.7.0";
32933
32933
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32934
32934
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32935
32935
  const SUPPLY_CHAIN_PLUGIN = "socket";
32936
32936
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32937
32937
  const SUPPLY_CHAIN_CATEGORY = "Security";
32938
32938
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32939
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32939
32940
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32940
32941
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32941
32942
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -37365,22 +37366,37 @@ const failOpenReadJson = (filePath, fallback) => {
37365
37366
  return fallback;
37366
37367
  }
37367
37368
  };
37369
+ const hashFileContents = (filePath) => {
37370
+ try {
37371
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
37372
+ } catch {
37373
+ return null;
37374
+ }
37375
+ };
37368
37376
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37369
37377
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37370
37378
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37371
37379
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37372
37380
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37373
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
37374
- const fingerprints = [];
37381
+ /**
37382
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
37383
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
37384
+ * verified against the tree it started from.
37385
+ */
37386
+ const collectAnalyzedFileStats = (rootDirectory) => {
37387
+ const statByRelativePath = /* @__PURE__ */ new Map();
37375
37388
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37376
37389
  if (!isFingerprintedFile(name)) continue;
37377
37390
  try {
37378
37391
  const fileStat = NFS.statSync(absolutePath);
37379
37392
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37380
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37393
+ statByRelativePath.set(relativePath, {
37394
+ mtimeMs: fileStat.mtimeMs,
37395
+ size: fileStat.size
37396
+ });
37381
37397
  } catch {}
37382
37398
  }
37383
- return fingerprints.sort();
37399
+ return statByRelativePath;
37384
37400
  };
37385
37401
  const bundledRequire$2 = createRequire(import.meta.url);
37386
37402
  const resolveDeslopVersion = () => {
@@ -37392,14 +37408,13 @@ const resolveDeslopVersion = () => {
37392
37408
  }
37393
37409
  };
37394
37410
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37395
- schemaVersion: 1,
37411
+ schemaVersion: 2,
37396
37412
  coreVersion: input.coreVersion,
37397
37413
  deslopVersion: resolveDeslopVersion(),
37398
37414
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37399
37415
  entryPatterns: input.entryPatterns,
37400
37416
  ignorePatterns: input.ignorePatterns,
37401
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37402
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
37417
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
37403
37418
  })).digest("hex");
37404
37419
  const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37405
37420
  const decodeCachedDiagnostics = (raw) => {
@@ -37410,16 +37425,68 @@ const decodeCachedDiagnostics = (raw) => {
37410
37425
  return null;
37411
37426
  }
37412
37427
  };
37413
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37414
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37415
- if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37416
- return decodeCachedDiagnostics(persisted.diagnostics);
37428
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
37429
+ const lookupDeadCodeResultCache = (input) => {
37430
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
37431
+ const persisted = failOpenReadJson(cacheFilePath, null);
37432
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
37433
+ const storedFileEntries = Object.entries(persisted.files);
37434
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
37435
+ const repairedFiles = {};
37436
+ let repairedCount = 0;
37437
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
37438
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
37439
+ const currentStat = input.currentFileStats.get(relativePath);
37440
+ if (currentStat === void 0) return null;
37441
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
37442
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
37443
+ repairedFiles[relativePath] = storedIdentity;
37444
+ continue;
37445
+ }
37446
+ if (currentStat.size !== storedSize) return null;
37447
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
37448
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
37449
+ repairedFiles[relativePath] = [
37450
+ currentStat.mtimeMs,
37451
+ currentStat.size,
37452
+ storedContentHash
37453
+ ];
37454
+ repairedCount += 1;
37455
+ }
37456
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
37457
+ if (diagnostics === null) return null;
37458
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
37459
+ version: 2,
37460
+ key: input.cacheKey,
37461
+ files: repairedFiles,
37462
+ diagnostics: persisted.diagnostics
37463
+ });
37464
+ return diagnostics;
37417
37465
  };
37418
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37419
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37420
- version: 1,
37421
- key: cacheKey,
37422
- diagnostics
37466
+ const storeDeadCodeResultCache = (input) => {
37467
+ const persistedFiles = {};
37468
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
37469
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
37470
+ const contentHash = hashFileContents(absolutePath);
37471
+ if (contentHash === null) return;
37472
+ let currentStat;
37473
+ try {
37474
+ currentStat = NFS.statSync(absolutePath);
37475
+ } catch {
37476
+ return;
37477
+ }
37478
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
37479
+ persistedFiles[relativePath] = [
37480
+ snapshotStat.mtimeMs,
37481
+ snapshotStat.size,
37482
+ contentHash
37483
+ ];
37484
+ }
37485
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37486
+ version: 2,
37487
+ key: input.cacheKey,
37488
+ files: persistedFiles,
37489
+ diagnostics: input.diagnostics
37423
37490
  });
37424
37491
  };
37425
37492
  const readSystemFacts = () => ({
@@ -37829,16 +37896,22 @@ const checkDeadCode = async (options) => {
37829
37896
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37830
37897
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
37831
37898
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37832
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37899
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
37900
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
37833
37901
  rootDirectory,
37834
37902
  entryPatterns,
37835
37903
  ignorePatterns,
37836
37904
  tsConfigPath,
37837
37905
  deslopJsModuleSpecifier,
37838
37906
  coreVersion: CORE_PACKAGE_VERSION
37839
- }) : null;
37840
- if (cacheKey !== null) {
37841
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37907
+ });
37908
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
37909
+ const cachedDiagnostics = lookupDeadCodeResultCache({
37910
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37911
+ cacheKey,
37912
+ rootDirectory,
37913
+ currentFileStats: fileStatsSnapshot
37914
+ });
37842
37915
  options.onCacheOutcome?.(cachedDiagnostics !== null);
37843
37916
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37844
37917
  }
@@ -37912,7 +37985,13 @@ const checkDeadCode = async (options) => {
37912
37985
  category: DEAD_CODE_CATEGORY
37913
37986
  });
37914
37987
  }
37915
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37988
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
37989
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37990
+ cacheKey,
37991
+ rootDirectory,
37992
+ snapshotFileStats: fileStatsSnapshot,
37993
+ diagnostics
37994
+ });
37916
37995
  return diagnostics;
37917
37996
  };
37918
37997
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -39008,13 +39087,6 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
39008
39087
  }
39009
39088
  };
39010
39089
  };
39011
- const hashFileContents = (filePath) => {
39012
- try {
39013
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39014
- } catch {
39015
- return null;
39016
- }
39017
- };
39018
39090
  const ABSENT_CONTENT_ANSWER = "absent";
39019
39091
  /**
39020
39092
  * Answers dependency probes for the sidecar lint cache — used both when
@@ -41155,6 +41227,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
41155
41227
  }));
41156
41228
  } catch {}
41157
41229
  };
41230
+ const pruneExpiredSocketCache = (cacheDirectory) => {
41231
+ try {
41232
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
41233
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
41234
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
41235
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
41236
+ try {
41237
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
41238
+ } catch {
41239
+ continue;
41240
+ }
41241
+ }
41242
+ } catch {}
41243
+ };
41158
41244
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
41159
41245
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
41160
41246
  if (cacheFile !== null) {
@@ -41282,6 +41368,7 @@ const checkSupplyChain = (input) => gen(function* () {
41282
41368
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41283
41369
  if (dependencies.length === 0) return [];
41284
41370
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41371
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
41285
41372
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
41286
41373
  const diagnostics = [];
41287
41374
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -42326,4 +42413,4 @@ const toJsonReport = (result, options) => buildJsonReport({
42326
42413
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42327
42414
 
42328
42415
  //# sourceMappingURL=index.js.map
42329
- //# debugId=b55ef8b2-8e9d-512b-81de-a01f6ce3e60e
42416
+ //# debugId=5c52dece-7b5e-5102-9381-7d05fa45734f
package/dist/lsp.js CHANGED
@@ -32988,13 +32988,14 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32988
32988
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32989
32989
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32990
32990
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32991
- const CORE_PACKAGE_VERSION = "0.6.3";
32991
+ const CORE_PACKAGE_VERSION = "0.7.0";
32992
32992
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32993
32993
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32994
32994
  const SUPPLY_CHAIN_PLUGIN = "socket";
32995
32995
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32996
32996
  const SUPPLY_CHAIN_CATEGORY = "Security";
32997
32997
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32998
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32998
32999
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32999
33000
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33000
33001
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -37350,22 +37351,37 @@ const failOpenReadJson = (filePath, fallback) => {
37350
37351
  return fallback;
37351
37352
  }
37352
37353
  };
37354
+ const hashFileContents = (filePath) => {
37355
+ try {
37356
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
37357
+ } catch {
37358
+ return null;
37359
+ }
37360
+ };
37353
37361
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37354
37362
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37355
37363
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37356
37364
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37357
37365
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37358
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
37359
- const fingerprints = [];
37366
+ /**
37367
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
37368
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
37369
+ * verified against the tree it started from.
37370
+ */
37371
+ const collectAnalyzedFileStats = (rootDirectory) => {
37372
+ const statByRelativePath = /* @__PURE__ */ new Map();
37360
37373
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37361
37374
  if (!isFingerprintedFile(name)) continue;
37362
37375
  try {
37363
37376
  const fileStat = NFS.statSync(absolutePath);
37364
37377
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37365
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37378
+ statByRelativePath.set(relativePath, {
37379
+ mtimeMs: fileStat.mtimeMs,
37380
+ size: fileStat.size
37381
+ });
37366
37382
  } catch {}
37367
37383
  }
37368
- return fingerprints.sort();
37384
+ return statByRelativePath;
37369
37385
  };
37370
37386
  const bundledRequire$2 = createRequire(import.meta.url);
37371
37387
  const resolveDeslopVersion = () => {
@@ -37377,14 +37393,13 @@ const resolveDeslopVersion = () => {
37377
37393
  }
37378
37394
  };
37379
37395
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37380
- schemaVersion: 1,
37396
+ schemaVersion: 2,
37381
37397
  coreVersion: input.coreVersion,
37382
37398
  deslopVersion: resolveDeslopVersion(),
37383
37399
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37384
37400
  entryPatterns: input.entryPatterns,
37385
37401
  ignorePatterns: input.ignorePatterns,
37386
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37387
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
37402
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
37388
37403
  })).digest("hex");
37389
37404
  const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37390
37405
  const decodeCachedDiagnostics = (raw) => {
@@ -37395,16 +37410,68 @@ const decodeCachedDiagnostics = (raw) => {
37395
37410
  return null;
37396
37411
  }
37397
37412
  };
37398
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37399
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37400
- if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37401
- return decodeCachedDiagnostics(persisted.diagnostics);
37413
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
37414
+ const lookupDeadCodeResultCache = (input) => {
37415
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
37416
+ const persisted = failOpenReadJson(cacheFilePath, null);
37417
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
37418
+ const storedFileEntries = Object.entries(persisted.files);
37419
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
37420
+ const repairedFiles = {};
37421
+ let repairedCount = 0;
37422
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
37423
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
37424
+ const currentStat = input.currentFileStats.get(relativePath);
37425
+ if (currentStat === void 0) return null;
37426
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
37427
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
37428
+ repairedFiles[relativePath] = storedIdentity;
37429
+ continue;
37430
+ }
37431
+ if (currentStat.size !== storedSize) return null;
37432
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
37433
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
37434
+ repairedFiles[relativePath] = [
37435
+ currentStat.mtimeMs,
37436
+ currentStat.size,
37437
+ storedContentHash
37438
+ ];
37439
+ repairedCount += 1;
37440
+ }
37441
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
37442
+ if (diagnostics === null) return null;
37443
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
37444
+ version: 2,
37445
+ key: input.cacheKey,
37446
+ files: repairedFiles,
37447
+ diagnostics: persisted.diagnostics
37448
+ });
37449
+ return diagnostics;
37402
37450
  };
37403
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37404
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37405
- version: 1,
37406
- key: cacheKey,
37407
- diagnostics
37451
+ const storeDeadCodeResultCache = (input) => {
37452
+ const persistedFiles = {};
37453
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
37454
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
37455
+ const contentHash = hashFileContents(absolutePath);
37456
+ if (contentHash === null) return;
37457
+ let currentStat;
37458
+ try {
37459
+ currentStat = NFS.statSync(absolutePath);
37460
+ } catch {
37461
+ return;
37462
+ }
37463
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
37464
+ persistedFiles[relativePath] = [
37465
+ snapshotStat.mtimeMs,
37466
+ snapshotStat.size,
37467
+ contentHash
37468
+ ];
37469
+ }
37470
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37471
+ version: 2,
37472
+ key: input.cacheKey,
37473
+ files: persistedFiles,
37474
+ diagnostics: input.diagnostics
37408
37475
  });
37409
37476
  };
37410
37477
  const readSystemFacts = () => ({
@@ -37814,16 +37881,22 @@ const checkDeadCode = async (options) => {
37814
37881
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37815
37882
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
37816
37883
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37817
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37884
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
37885
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
37818
37886
  rootDirectory,
37819
37887
  entryPatterns,
37820
37888
  ignorePatterns,
37821
37889
  tsConfigPath,
37822
37890
  deslopJsModuleSpecifier,
37823
37891
  coreVersion: CORE_PACKAGE_VERSION
37824
- }) : null;
37825
- if (cacheKey !== null) {
37826
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37892
+ });
37893
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
37894
+ const cachedDiagnostics = lookupDeadCodeResultCache({
37895
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37896
+ cacheKey,
37897
+ rootDirectory,
37898
+ currentFileStats: fileStatsSnapshot
37899
+ });
37827
37900
  options.onCacheOutcome?.(cachedDiagnostics !== null);
37828
37901
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37829
37902
  }
@@ -37897,7 +37970,13 @@ const checkDeadCode = async (options) => {
37897
37970
  category: DEAD_CODE_CATEGORY
37898
37971
  });
37899
37972
  }
37900
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37973
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
37974
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
37975
+ cacheKey,
37976
+ rootDirectory,
37977
+ snapshotFileStats: fileStatsSnapshot,
37978
+ diagnostics
37979
+ });
37901
37980
  return diagnostics;
37902
37981
  };
37903
37982
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -38993,13 +39072,6 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38993
39072
  }
38994
39073
  };
38995
39074
  };
38996
- const hashFileContents = (filePath) => {
38997
- try {
38998
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
38999
- } catch {
39000
- return null;
39001
- }
39002
- };
39003
39075
  const ABSENT_CONTENT_ANSWER = "absent";
39004
39076
  /**
39005
39077
  * Answers dependency probes for the sidecar lint cache — used both when
@@ -41140,6 +41212,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
41140
41212
  }));
41141
41213
  } catch {}
41142
41214
  };
41215
+ const pruneExpiredSocketCache = (cacheDirectory) => {
41216
+ try {
41217
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
41218
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
41219
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
41220
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
41221
+ try {
41222
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
41223
+ } catch {
41224
+ continue;
41225
+ }
41226
+ }
41227
+ } catch {}
41228
+ };
41143
41229
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
41144
41230
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
41145
41231
  if (cacheFile !== null) {
@@ -41267,6 +41353,7 @@ const checkSupplyChain = (input) => gen(function* () {
41267
41353
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41268
41354
  if (dependencies.length === 0) return [];
41269
41355
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41356
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
41270
41357
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
41271
41358
  const diagnostics = [];
41272
41359
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -41983,11 +42070,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
41983
42070
  const parts = [`v=${version}`];
41984
42071
  let directory = projectDirectory;
41985
42072
  for (;;) {
41986
- for (const filename of CONFIG_FINGERPRINT_FILENAMES) try {
41987
- const stat = fs.statSync(path.join(directory, filename));
41988
- parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`);
41989
- } catch {
41990
- continue;
42073
+ for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
42074
+ const contentHash = hashFileContents(path.join(directory, filename));
42075
+ if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
41991
42076
  }
41992
42077
  const parent = path.dirname(directory);
41993
42078
  if (parent === directory) break;
@@ -42000,7 +42085,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42000
42085
  /** Display name used in client-facing messages and progress titles. */
42001
42086
  const SERVER_DISPLAY_NAME = "React Doctor";
42002
42087
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42003
- const SERVER_VERSION = "0.6.3";
42088
+ const SERVER_VERSION = "0.7.0";
42004
42089
  /** `Diagnostic.source` shown next to every published diagnostic. */
42005
42090
  const DIAGNOSTIC_SOURCE = "react-doctor";
42006
42091
  /**
@@ -44050,5 +44135,5 @@ const startLanguageServer = () => {
44050
44135
  };
44051
44136
  //#endregion
44052
44137
  export { startLanguageServer };
44053
- !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]="17a3f533-8248-511a-859b-584197cefba6")}catch(e){}}();
44054
- //# debugId=17a3f533-8248-511a-859b-584197cefba6
44138
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1b40bd68-7ad4-55e0-a6bd-91b6cdcd0ba8")}catch(e){}}();
44139
+ //# debugId=1b40bd68-7ad4-55e0-a6bd-91b6cdcd0ba8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.6.3-dev.ea9a775",
3
+ "version": "0.7.0-dev.574cba6",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "oxlint-plugin-react-doctor": "0.6.3-dev.ea9a775",
69
- "deslop-js": "0.6.3"
68
+ "deslop-js": "0.7.0",
69
+ "oxlint-plugin-react-doctor": "0.7.0-dev.574cba6"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,9 +74,9 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/api": "0.6.3",
78
- "@react-doctor/core": "0.6.3",
79
- "@react-doctor/language-server": "0.6.3"
77
+ "@react-doctor/api": "0.7.0",
78
+ "@react-doctor/core": "0.7.0",
79
+ "@react-doctor/language-server": "0.7.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"