react-doctor 0.6.3-dev.1571119 → 0.6.3-dev.20d81f6

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]="8532f6fd-f5ce-59ae-940d-7bab36e5937d")}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]="d7bda084-a223-5aa7-8605-578888bd1b33")}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";
@@ -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,6 +36129,8 @@ 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 CORE_PACKAGE_VERSION = "0.6.3";
36133
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36131
36134
  const SUPPLY_CHAIN_PLUGIN = "socket";
36132
36135
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36133
36136
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -38812,6 +38815,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
38812
38815
  if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
38813
38816
  return true;
38814
38817
  } }) {};
38818
+ /**
38819
+ * Whether the whole-project dead-code result cache
38820
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
38821
+ * whose inputs (source tree, manifests, configs, analyzer version) are
38822
+ * unchanged replays the stored diagnostics instead of re-running the
38823
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
38824
+ *
38825
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38826
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
38827
+ *
38828
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
38829
+ */
38830
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
38831
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38832
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
38833
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38834
+ if (CACHE_DISABLED_VALUES$1.has(noDeadCodeCache)) return false;
38835
+ return true;
38836
+ } }) {};
38815
38837
  const DIAGNOSTIC_SURFACES = [
38816
38838
  "cli",
38817
38839
  "prComment",
@@ -40544,6 +40566,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40544
40566
  return [...seen].filter((pattern) => pattern.length > 0);
40545
40567
  };
40546
40568
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40569
+ const atomicWriteJson = (filePath, value) => {
40570
+ try {
40571
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
40572
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
40573
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
40574
+ NFS.renameSync(temporaryPath, filePath);
40575
+ } catch {
40576
+ return;
40577
+ }
40578
+ };
40579
+ const failOpenReadJson = (filePath, fallback) => {
40580
+ try {
40581
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
40582
+ } catch {
40583
+ return fallback;
40584
+ }
40585
+ };
40586
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40587
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40588
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40589
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40590
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40591
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
40592
+ const fingerprints = [];
40593
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40594
+ if (!isFingerprintedFile(name)) continue;
40595
+ try {
40596
+ const fileStat = NFS.statSync(absolutePath);
40597
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
40598
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
40599
+ } catch {}
40600
+ }
40601
+ return fingerprints.sort();
40602
+ };
40603
+ const bundledRequire$2 = createRequire(import.meta.url);
40604
+ const resolveDeslopVersion = () => {
40605
+ try {
40606
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
40607
+ return isRecord$1(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
40608
+ } catch {
40609
+ return "unknown";
40610
+ }
40611
+ };
40612
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40613
+ schemaVersion: 1,
40614
+ coreVersion: input.coreVersion,
40615
+ deslopVersion: resolveDeslopVersion(),
40616
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40617
+ entryPatterns: input.entryPatterns,
40618
+ ignorePatterns: input.ignorePatterns,
40619
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
40620
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
40621
+ })).digest("hex");
40622
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
40623
+ const decodeCachedDiagnostics = (raw) => {
40624
+ try {
40625
+ for (const entry of raw) validateDiagnostic$1(entry);
40626
+ return raw;
40627
+ } catch {
40628
+ return null;
40629
+ }
40630
+ };
40631
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
40632
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
40633
+ if (persisted === null || !isRecord$1(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
40634
+ return decodeCachedDiagnostics(persisted.diagnostics);
40635
+ };
40636
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
40637
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40638
+ version: 1,
40639
+ key: cacheKey,
40640
+ diagnostics
40641
+ });
40642
+ };
40547
40643
  const readSystemFacts = () => ({
40548
40644
  availableCores: os.availableParallelism(),
40549
40645
  totalMemoryBytes: os.totalmem(),
@@ -40612,6 +40708,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40612
40708
  releaseSlot();
40613
40709
  }
40614
40710
  };
40711
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
40712
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
40713
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
40714
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
40715
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
40716
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
40717
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
40718
+ };
40719
+ const resolveUserCacheScope = () => {
40720
+ try {
40721
+ return String(process.getuid?.() ?? os.userInfo().username);
40722
+ } catch {
40723
+ return "shared";
40724
+ }
40725
+ };
40615
40726
  /**
40616
40727
  * Resolves a path to its canonical, symlink-free form, falling back to
40617
40728
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40907,13 +41018,28 @@ const checkDeadCode = async (options) => {
40907
41018
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40908
41019
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40909
41020
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
41021
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
41022
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
41023
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
41024
+ rootDirectory,
41025
+ entryPatterns,
41026
+ ignorePatterns,
41027
+ tsConfigPath,
41028
+ deslopJsModuleSpecifier,
41029
+ coreVersion: CORE_PACKAGE_VERSION
41030
+ }) : null;
41031
+ if (cacheKey !== null) {
41032
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
41033
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
41034
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41035
+ }
40910
41036
  const spawnAndRun = () => {
40911
41037
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40912
41038
  rootDirectory,
40913
41039
  entryPatterns,
40914
- tsConfigPath: resolveTsConfigPath(rootDirectory),
41040
+ tsConfigPath,
40915
41041
  ignorePatterns,
40916
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
41042
+ deslopJsModuleSpecifier,
40917
41043
  parseConcurrency: options.parseConcurrency
40918
41044
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40919
41045
  };
@@ -40974,6 +41100,7 @@ const checkDeadCode = async (options) => {
40974
41100
  category: DEAD_CODE_CATEGORY
40975
41101
  });
40976
41102
  }
41103
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
40977
41104
  return diagnostics;
40978
41105
  };
40979
41106
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -41076,12 +41203,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
41076
41203
  */
41077
41204
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
41078
41205
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
41206
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
41079
41207
  return yield* tryPromise({
41080
41208
  try: (signal) => checkDeadCode({
41081
41209
  rootDirectory: input.rootDirectory,
41082
41210
  parseConcurrency: input.parseConcurrency,
41083
41211
  workerTimeoutMs: input.workerTimeoutMs,
41084
- abortSignal: signal
41212
+ abortSignal: signal,
41213
+ cacheEnabled,
41214
+ onCacheOutcome: input.onCacheOutcome
41085
41215
  }),
41086
41216
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
41087
41217
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -42007,23 +42137,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42007
42137
  }
42008
42138
  };
42009
42139
  };
42010
- const atomicWriteJson = (filePath, value) => {
42011
- try {
42012
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
42013
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
42014
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
42015
- NFS.renameSync(temporaryPath, filePath);
42016
- } catch {
42017
- return;
42018
- }
42019
- };
42020
- const failOpenReadJson = (filePath, fallback) => {
42021
- try {
42022
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
42023
- } catch {
42024
- return fallback;
42025
- }
42026
- };
42027
42140
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
42028
42141
  const decodeFileDiagnostics = (raw) => {
42029
42142
  if (!Array.isArray(raw)) return null;
@@ -42081,7 +42194,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42081
42194
  }
42082
42195
  };
42083
42196
  };
42084
- const bundledRequire$2 = createRequire(import.meta.url);
42197
+ const bundledRequire$3 = createRequire(import.meta.url);
42085
42198
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
42086
42199
  const resolveChildNodeVersion = (nodeBinaryPath) => {
42087
42200
  if (nodeBinaryPath === process.execPath) return process.version;
@@ -42107,7 +42220,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
42107
42220
  const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
42108
42221
  const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
42109
42222
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
42110
- const packageJson = bundledRequire$2(specifier);
42223
+ const packageJson = bundledRequire$3(specifier);
42111
42224
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
42112
42225
  versions.push(`${specifier}=${version}`);
42113
42226
  } catch {
@@ -43151,21 +43264,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
43151
43264
  }
43152
43265
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
43153
43266
  };
43154
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
43155
- const resolveReactDoctorCacheDir = (projectDirectory) => {
43156
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
43157
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
43158
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
43159
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
43160
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
43161
- };
43162
- const resolveUserCacheScope = () => {
43163
- try {
43164
- return String(process.getuid?.() ?? os.userInfo().username);
43165
- } catch {
43166
- return "shared";
43167
- }
43168
- };
43169
43267
  /**
43170
43268
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
43171
43269
  * the runner: once for the primary scan, once for the
@@ -44292,7 +44390,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44292
44390
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
44293
44391
  rootDirectory: scanDirectory,
44294
44392
  parseConcurrency: deadCodeParseConcurrency,
44295
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
44393
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
44394
+ onCacheOutcome: (didHitCache) => {
44395
+ deadCodeCacheHit = didHitCache;
44396
+ }
44296
44397
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44297
44398
  yield* set(deadCodeFailure, {
44298
44399
  didFail: true,
@@ -44320,6 +44421,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44320
44421
  let lastReportedTotalFileCount = 0;
44321
44422
  let lintCacheHitFileCount = null;
44322
44423
  let lintCacheTotalFileCount = null;
44424
+ let deadCodeCacheHit = null;
44323
44425
  const baseLintStream = linterService.run({
44324
44426
  rootDirectory: scanDirectory,
44325
44427
  project,
@@ -44449,6 +44551,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44449
44551
  securityScanFailed,
44450
44552
  lintCacheHitFileCount,
44451
44553
  lintCacheTotalFileCount,
44554
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
44452
44555
  suppressedRuleCounts: transform.summarizeSuppressions()
44453
44556
  };
44454
44557
  }).pipe(withSpan("runInspect", { attributes: {
@@ -45321,7 +45424,7 @@ const makeNoopConsole = () => ({
45321
45424
  });
45322
45425
  //#endregion
45323
45426
  //#region src/cli/utils/version.ts
45324
- const VERSION = "0.6.3-dev.1571119";
45427
+ const VERSION = "0.6.3-dev.20d81f6";
45325
45428
  //#endregion
45326
45429
  //#region src/cli/utils/json-mode.ts
45327
45430
  let context = null;
@@ -45690,13 +45793,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45690
45793
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45691
45794
  * standard `SENTRY_RELEASE` override.
45692
45795
  */
45693
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.1571119`;
45796
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.20d81f6`;
45694
45797
  /**
45695
45798
  * Deployment environment shown in Sentry's environment filter. Defaults to
45696
45799
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45697
45800
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45698
45801
  */
45699
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.1571119") ? "development" : "production");
45802
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.20d81f6") ? "development" : "production");
45700
45803
  /**
45701
45804
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45702
45805
  * (set to `0` to disable tracing) and falls back to
@@ -57924,7 +58027,8 @@ const buildOutcomeAttributes = (input) => {
57924
58027
  }),
57925
58028
  ...withNamespace("deadCode", {
57926
58029
  failed: input.didDeadCodeFail ?? null,
57927
- overlapped: input.deadCodeOverlapped ?? null
58030
+ overlapped: input.deadCodeOverlapped ?? null,
58031
+ cacheHit: result.deadCodeCacheHit ?? null
57928
58032
  }),
57929
58033
  ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
57930
58034
  ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
@@ -59247,13 +59351,89 @@ const stringifyStableJson = (value) => {
59247
59351
  };
59248
59352
  const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
59249
59353
  const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
59250
- const isWorktreeClean = (projectDirectory) => {
59251
- const status = runGit(projectDirectory, [
59354
+ const parseWorktreeStatusRecords = (statusOutput) => {
59355
+ const entries = [];
59356
+ const tokens = statusOutput.split("\0");
59357
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
59358
+ let record = tokens[tokenIndex];
59359
+ if (record.length === 0) continue;
59360
+ if (record.length < 3 || record[2] !== " ") {
59361
+ if (tokenIndex !== 0) return null;
59362
+ record = ` ${record}`;
59363
+ }
59364
+ if (record.length < 4 || record[2] !== " ") return null;
59365
+ const statusCode = record.slice(0, 2);
59366
+ const recordPath = record.slice(3);
59367
+ if (statusCode.includes("R") || statusCode.includes("C")) {
59368
+ tokenIndex += 1;
59369
+ const originPath = tokens[tokenIndex];
59370
+ if (originPath === void 0 || originPath.length === 0) return null;
59371
+ entries.push({
59372
+ statusCode,
59373
+ path: recordPath,
59374
+ originPath
59375
+ });
59376
+ continue;
59377
+ }
59378
+ entries.push({
59379
+ statusCode,
59380
+ path: recordPath
59381
+ });
59382
+ }
59383
+ return entries;
59384
+ };
59385
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59386
+ let stats;
59387
+ try {
59388
+ stats = fs.lstatSync(absolutePath);
59389
+ } catch {
59390
+ return statusCode.includes("D") ? "deleted" : null;
59391
+ }
59392
+ if (stats.isSymbolicLink()) try {
59393
+ return `link:${fs.readlinkSync(absolutePath)}`;
59394
+ } catch {
59395
+ return null;
59396
+ }
59397
+ if (!stats.isFile()) return null;
59398
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59399
+ return hashFileContents(absolutePath);
59400
+ };
59401
+ const buildWorktreeFingerprint = (projectDirectory) => {
59402
+ const statusOutput = runGit(projectDirectory, [
59252
59403
  "status",
59253
59404
  "--porcelain=v1",
59254
- "--untracked-files=normal"
59405
+ "-z",
59406
+ "--untracked-files=all"
59255
59407
  ]);
59256
- return status !== null && status.length === 0;
59408
+ if (statusOutput === null) return null;
59409
+ if (statusOutput.length === 0) return [];
59410
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
59411
+ if (statusEntries === null) return null;
59412
+ if (statusEntries.length > 300) return null;
59413
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
59414
+ if (repositoryRoot === null) return null;
59415
+ const dirtyEntries = [];
59416
+ for (const statusEntry of statusEntries) {
59417
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
59418
+ if (contentFingerprint === null) return null;
59419
+ dirtyEntries.push({
59420
+ ...statusEntry,
59421
+ contentFingerprint
59422
+ });
59423
+ }
59424
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
59425
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
59426
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
59427
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
59428
+ });
59429
+ };
59430
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
59431
+ const resolveDotenvFingerprint = (projectDirectory) => {
59432
+ try {
59433
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${fileFingerprint(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
59434
+ } catch {
59435
+ return [];
59436
+ }
59257
59437
  };
59258
59438
  const hasHiddenTrackedFileState = (projectDirectory) => {
59259
59439
  const output = runGit(projectDirectory, ["ls-files", "-v"]);
@@ -59336,8 +59516,9 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
59336
59516
  };
59337
59517
  const buildScanResultCacheKey = (input) => {
59338
59518
  if (isScanResultCacheDisabled()) return null;
59339
- if (!isWorktreeClean(input.projectDirectory)) return null;
59340
59519
  if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59520
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
59521
+ if (worktreeFingerprint === null) return null;
59341
59522
  const headSha = readHeadSha(input.projectDirectory);
59342
59523
  if (headSha === null) return null;
59343
59524
  if (stringifyStableJson(input.userConfig) === null) return null;
@@ -59345,6 +59526,8 @@ const buildScanResultCacheKey = (input) => {
59345
59526
  schemaVersion: 3,
59346
59527
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
59347
59528
  headSha,
59529
+ worktreeFingerprint,
59530
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
59348
59531
  reactDoctorVersion: input.version,
59349
59532
  nodeVersion: process.version,
59350
59533
  toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
@@ -59737,7 +59920,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59737
59920
  scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59738
59921
  baselineDegraded,
59739
59922
  lintCacheHitFileCount: output.lintCacheHitFileCount,
59740
- lintCacheTotalFileCount: output.lintCacheTotalFileCount
59923
+ lintCacheTotalFileCount: output.lintCacheTotalFileCount,
59924
+ deadCodeCacheHit: output.deadCodeCacheHit
59741
59925
  });
59742
59926
  recordOnboardingCompletion(options);
59743
59927
  return result;
@@ -59772,6 +59956,7 @@ const renderAndRecordScan = async (input) => {
59772
59956
  scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
59773
59957
  lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
59774
59958
  lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
59959
+ deadCodeCacheHit: input.deadCodeCacheHit ?? null,
59775
59960
  baselineDelta: input.payload.baselineDelta
59776
59961
  }), input.options.silent));
59777
59962
  const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
@@ -59810,7 +59995,7 @@ const renderAndRecordScan = async (input) => {
59810
59995
  return result;
59811
59996
  };
59812
59997
  const finalizeAndRender = (input) => gen(function* () {
59813
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
59998
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, deadCodeCacheHit, baselineDelta } = input;
59814
59999
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
59815
60000
  didLintFail,
59816
60001
  lintFailureReason,
@@ -59834,6 +60019,7 @@ const finalizeAndRender = (input) => gen(function* () {
59834
60019
  lintCacheHitFileCount,
59835
60020
  lintCacheTotalFileCount
59836
60021
  } : {},
60022
+ ...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
59837
60023
  ...baselineDelta ? { baselineDelta } : {}
59838
60024
  });
59839
60025
  if (options.suppressRendering) return buildResult();
@@ -63680,4 +63866,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63680
63866
  export {};
63681
63867
 
63682
63868
  //# sourceMappingURL=cli.js.map
63683
- //# debugId=8532f6fd-f5ce-59ae-940d-7bab36e5937d
63869
+ //# debugId=d7bda084-a223-5aa7-8605-578888bd1b33
package/dist/index.d.ts CHANGED
@@ -9743,6 +9743,14 @@ interface InspectResult {
9743
9743
  */
9744
9744
  lintCacheHitFileCount?: number | null;
9745
9745
  lintCacheTotalFileCount?: number | null;
9746
+ /**
9747
+ * Dead-code result cache outcome: `true` when the pass replayed a cached
9748
+ * result (the analysis never ran), `false` on a fresh analysis. Absent when
9749
+ * the pass never consulted the cache — dead-code skipped, the cache
9750
+ * disabled, or a whole-repo cache replay where no analysis ran. The CLI
9751
+ * projects it onto the Sentry wide event as `deadCode.cacheHit`.
9752
+ */
9753
+ deadCodeCacheHit?: boolean | null;
9746
9754
  /**
9747
9755
  * Present only for a baseline run (`InspectOptions.baseline` set). The
9748
9756
  * `diagnostics` above are then the *introduced* findings only; this
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8565c6ea-39f3-51e6-895a-ae9f7c026d0e")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e96374bd-b4c9-5452-8570-b81c11a2959a")}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";
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
20
  import crypto, { createHash } from "node:crypto";
21
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
21
22
  import { gzipSync } from "node:zlib";
22
23
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
23
24
  /**
@@ -32927,6 +32928,8 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32927
32928
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32928
32929
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32929
32930
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32931
+ const CORE_PACKAGE_VERSION = "0.6.3";
32932
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32930
32933
  const SUPPLY_CHAIN_PLUGIN = "socket";
32931
32934
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32932
32935
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -35613,6 +35616,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35613
35616
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35614
35617
  return true;
35615
35618
  } }) {};
35619
+ /**
35620
+ * Whether the whole-project dead-code result cache
35621
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35622
+ * whose inputs (source tree, manifests, configs, analyzer version) are
35623
+ * unchanged replays the stored diagnostics instead of re-running the
35624
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
35625
+ *
35626
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35627
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
35628
+ *
35629
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
35630
+ */
35631
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
35632
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35633
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
35634
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35635
+ if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
35636
+ return true;
35637
+ } }) {};
35616
35638
  const DIAGNOSTIC_SURFACES = [
35617
35639
  "cli",
35618
35640
  "prComment",
@@ -37301,6 +37323,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37301
37323
  return [...seen].filter((pattern) => pattern.length > 0);
37302
37324
  };
37303
37325
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37326
+ const atomicWriteJson = (filePath, value) => {
37327
+ try {
37328
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
37329
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
37330
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
37331
+ NFS.renameSync(temporaryPath, filePath);
37332
+ } catch {
37333
+ return;
37334
+ }
37335
+ };
37336
+ const failOpenReadJson = (filePath, fallback) => {
37337
+ try {
37338
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
37339
+ } catch {
37340
+ return fallback;
37341
+ }
37342
+ };
37343
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37344
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37345
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37346
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37347
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37348
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
37349
+ const fingerprints = [];
37350
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37351
+ if (!isFingerprintedFile(name)) continue;
37352
+ try {
37353
+ const fileStat = NFS.statSync(absolutePath);
37354
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37355
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37356
+ } catch {}
37357
+ }
37358
+ return fingerprints.sort();
37359
+ };
37360
+ const bundledRequire$2 = createRequire(import.meta.url);
37361
+ const resolveDeslopVersion = () => {
37362
+ try {
37363
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
37364
+ return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
37365
+ } catch {
37366
+ return "unknown";
37367
+ }
37368
+ };
37369
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37370
+ schemaVersion: 1,
37371
+ coreVersion: input.coreVersion,
37372
+ deslopVersion: resolveDeslopVersion(),
37373
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37374
+ entryPatterns: input.entryPatterns,
37375
+ ignorePatterns: input.ignorePatterns,
37376
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37377
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
37378
+ })).digest("hex");
37379
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
37380
+ const decodeCachedDiagnostics = (raw) => {
37381
+ try {
37382
+ for (const entry of raw) validateDiagnostic$1(entry);
37383
+ return raw;
37384
+ } catch {
37385
+ return null;
37386
+ }
37387
+ };
37388
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37389
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37390
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37391
+ return decodeCachedDiagnostics(persisted.diagnostics);
37392
+ };
37393
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37394
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37395
+ version: 1,
37396
+ key: cacheKey,
37397
+ diagnostics
37398
+ });
37399
+ };
37304
37400
  const readSystemFacts = () => ({
37305
37401
  availableCores: os.availableParallelism(),
37306
37402
  totalMemoryBytes: os.totalmem(),
@@ -37369,6 +37465,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37369
37465
  releaseSlot();
37370
37466
  }
37371
37467
  };
37468
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
37469
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
37470
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
37471
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
37472
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
37473
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
37474
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
37475
+ };
37476
+ const resolveUserCacheScope = () => {
37477
+ try {
37478
+ return String(process.getuid?.() ?? os.userInfo().username);
37479
+ } catch {
37480
+ return "shared";
37481
+ }
37482
+ };
37372
37483
  /**
37373
37484
  * Resolves a path to its canonical, symlink-free form, falling back to
37374
37485
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37664,13 +37775,28 @@ const checkDeadCode = async (options) => {
37664
37775
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37665
37776
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37666
37777
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37778
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
37779
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37780
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37781
+ rootDirectory,
37782
+ entryPatterns,
37783
+ ignorePatterns,
37784
+ tsConfigPath,
37785
+ deslopJsModuleSpecifier,
37786
+ coreVersion: CORE_PACKAGE_VERSION
37787
+ }) : null;
37788
+ if (cacheKey !== null) {
37789
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37790
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
37791
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37792
+ }
37667
37793
  const spawnAndRun = () => {
37668
37794
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37669
37795
  rootDirectory,
37670
37796
  entryPatterns,
37671
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37797
+ tsConfigPath,
37672
37798
  ignorePatterns,
37673
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37799
+ deslopJsModuleSpecifier,
37674
37800
  parseConcurrency: options.parseConcurrency
37675
37801
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37676
37802
  };
@@ -37731,6 +37857,7 @@ const checkDeadCode = async (options) => {
37731
37857
  category: DEAD_CODE_CATEGORY
37732
37858
  });
37733
37859
  }
37860
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37734
37861
  return diagnostics;
37735
37862
  };
37736
37863
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37833,12 +37960,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
37833
37960
  */
37834
37961
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37835
37962
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37963
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37836
37964
  return yield* tryPromise({
37837
37965
  try: (signal) => checkDeadCode({
37838
37966
  rootDirectory: input.rootDirectory,
37839
37967
  parseConcurrency: input.parseConcurrency,
37840
37968
  workerTimeoutMs: input.workerTimeoutMs,
37841
- abortSignal: signal
37969
+ abortSignal: signal,
37970
+ cacheEnabled,
37971
+ onCacheOutcome: input.onCacheOutcome
37842
37972
  }),
37843
37973
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37844
37974
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38764,23 +38894,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38764
38894
  }
38765
38895
  };
38766
38896
  };
38767
- const atomicWriteJson = (filePath, value) => {
38768
- try {
38769
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38770
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38771
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38772
- NFS.renameSync(temporaryPath, filePath);
38773
- } catch {
38774
- return;
38775
- }
38776
- };
38777
- const failOpenReadJson = (filePath, fallback) => {
38778
- try {
38779
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38780
- } catch {
38781
- return fallback;
38782
- }
38783
- };
38784
38897
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38785
38898
  const decodeFileDiagnostics = (raw) => {
38786
38899
  if (!Array.isArray(raw)) return null;
@@ -39908,21 +40021,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39908
40021
  }
39909
40022
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39910
40023
  };
39911
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39912
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39913
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39914
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39915
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39916
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39917
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39918
- };
39919
- const resolveUserCacheScope = () => {
39920
- try {
39921
- return String(process.getuid?.() ?? os.userInfo().username);
39922
- } catch {
39923
- return "shared";
39924
- }
39925
- };
39926
40024
  /**
39927
40025
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39928
40026
  * the runner: once for the primary scan, once for the
@@ -41049,7 +41147,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41049
41147
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
41050
41148
  rootDirectory: scanDirectory,
41051
41149
  parseConcurrency: deadCodeParseConcurrency,
41052
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41150
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41151
+ onCacheOutcome: (didHitCache) => {
41152
+ deadCodeCacheHit = didHitCache;
41153
+ }
41053
41154
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41054
41155
  yield* set(deadCodeFailure, {
41055
41156
  didFail: true,
@@ -41077,6 +41178,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41077
41178
  let lastReportedTotalFileCount = 0;
41078
41179
  let lintCacheHitFileCount = null;
41079
41180
  let lintCacheTotalFileCount = null;
41181
+ let deadCodeCacheHit = null;
41080
41182
  const baseLintStream = linterService.run({
41081
41183
  rootDirectory: scanDirectory,
41082
41184
  project,
@@ -41206,6 +41308,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41206
41308
  securityScanFailed,
41207
41309
  lintCacheHitFileCount,
41208
41310
  lintCacheTotalFileCount,
41311
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41209
41312
  suppressedRuleCounts: transform.summarizeSuppressions()
41210
41313
  };
41211
41314
  }).pipe(withSpan("runInspect", { attributes: {
@@ -41846,4 +41949,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41846
41949
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41847
41950
 
41848
41951
  //# sourceMappingURL=index.js.map
41849
- //# debugId=8565c6ea-39f3-51e6-895a-ae9f7c026d0e
41952
+ //# debugId=e96374bd-b4c9-5452-8570-b81c11a2959a
package/dist/lsp.js CHANGED
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
15
15
  import { createJiti } from "jiti";
16
16
  import * as Crypto from "node:crypto";
17
17
  import crypto, { createHash } from "node:crypto";
18
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
18
19
  import { gzipSync } from "node:zlib";
19
20
  import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
20
21
  import { TextDocument } from "vscode-languageserver-textdocument";
@@ -32986,6 +32987,8 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32986
32987
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32987
32988
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32988
32989
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32990
+ const CORE_PACKAGE_VERSION = "0.6.3";
32991
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32989
32992
  const SUPPLY_CHAIN_PLUGIN = "socket";
32990
32993
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32991
32994
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -35646,6 +35649,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35646
35649
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35647
35650
  return true;
35648
35651
  } }) {};
35652
+ /**
35653
+ * Whether the whole-project dead-code result cache
35654
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35655
+ * whose inputs (source tree, manifests, configs, analyzer version) are
35656
+ * unchanged replays the stored diagnostics instead of re-running the
35657
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
35658
+ *
35659
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35660
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
35661
+ *
35662
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
35663
+ */
35664
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
35665
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35666
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
35667
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35668
+ if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
35669
+ return true;
35670
+ } }) {};
35649
35671
  const DIAGNOSTIC_SURFACES = [
35650
35672
  "cli",
35651
35673
  "prComment",
@@ -37286,6 +37308,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37286
37308
  return [...seen].filter((pattern) => pattern.length > 0);
37287
37309
  };
37288
37310
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37311
+ const atomicWriteJson = (filePath, value) => {
37312
+ try {
37313
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
37314
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
37315
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
37316
+ NFS.renameSync(temporaryPath, filePath);
37317
+ } catch {
37318
+ return;
37319
+ }
37320
+ };
37321
+ const failOpenReadJson = (filePath, fallback) => {
37322
+ try {
37323
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
37324
+ } catch {
37325
+ return fallback;
37326
+ }
37327
+ };
37328
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37329
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37330
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37331
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37332
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37333
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
37334
+ const fingerprints = [];
37335
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37336
+ if (!isFingerprintedFile(name)) continue;
37337
+ try {
37338
+ const fileStat = NFS.statSync(absolutePath);
37339
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37340
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37341
+ } catch {}
37342
+ }
37343
+ return fingerprints.sort();
37344
+ };
37345
+ const bundledRequire$2 = createRequire(import.meta.url);
37346
+ const resolveDeslopVersion = () => {
37347
+ try {
37348
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
37349
+ return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
37350
+ } catch {
37351
+ return "unknown";
37352
+ }
37353
+ };
37354
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37355
+ schemaVersion: 1,
37356
+ coreVersion: input.coreVersion,
37357
+ deslopVersion: resolveDeslopVersion(),
37358
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37359
+ entryPatterns: input.entryPatterns,
37360
+ ignorePatterns: input.ignorePatterns,
37361
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37362
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
37363
+ })).digest("hex");
37364
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
37365
+ const decodeCachedDiagnostics = (raw) => {
37366
+ try {
37367
+ for (const entry of raw) validateDiagnostic$1(entry);
37368
+ return raw;
37369
+ } catch {
37370
+ return null;
37371
+ }
37372
+ };
37373
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37374
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37375
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37376
+ return decodeCachedDiagnostics(persisted.diagnostics);
37377
+ };
37378
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37379
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37380
+ version: 1,
37381
+ key: cacheKey,
37382
+ diagnostics
37383
+ });
37384
+ };
37289
37385
  const readSystemFacts = () => ({
37290
37386
  availableCores: os.availableParallelism(),
37291
37387
  totalMemoryBytes: os.totalmem(),
@@ -37354,6 +37450,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37354
37450
  releaseSlot();
37355
37451
  }
37356
37452
  };
37453
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
37454
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
37455
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
37456
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
37457
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
37458
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
37459
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
37460
+ };
37461
+ const resolveUserCacheScope = () => {
37462
+ try {
37463
+ return String(process.getuid?.() ?? os.userInfo().username);
37464
+ } catch {
37465
+ return "shared";
37466
+ }
37467
+ };
37357
37468
  /**
37358
37469
  * Resolves a path to its canonical, symlink-free form, falling back to
37359
37470
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37649,13 +37760,28 @@ const checkDeadCode = async (options) => {
37649
37760
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37650
37761
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37651
37762
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37763
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
37764
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37765
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37766
+ rootDirectory,
37767
+ entryPatterns,
37768
+ ignorePatterns,
37769
+ tsConfigPath,
37770
+ deslopJsModuleSpecifier,
37771
+ coreVersion: CORE_PACKAGE_VERSION
37772
+ }) : null;
37773
+ if (cacheKey !== null) {
37774
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37775
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
37776
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37777
+ }
37652
37778
  const spawnAndRun = () => {
37653
37779
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37654
37780
  rootDirectory,
37655
37781
  entryPatterns,
37656
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37782
+ tsConfigPath,
37657
37783
  ignorePatterns,
37658
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37784
+ deslopJsModuleSpecifier,
37659
37785
  parseConcurrency: options.parseConcurrency
37660
37786
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37661
37787
  };
@@ -37716,6 +37842,7 @@ const checkDeadCode = async (options) => {
37716
37842
  category: DEAD_CODE_CATEGORY
37717
37843
  });
37718
37844
  }
37845
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37719
37846
  return diagnostics;
37720
37847
  };
37721
37848
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37818,12 +37945,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
37818
37945
  */
37819
37946
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37820
37947
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37948
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37821
37949
  return yield* tryPromise({
37822
37950
  try: (signal) => checkDeadCode({
37823
37951
  rootDirectory: input.rootDirectory,
37824
37952
  parseConcurrency: input.parseConcurrency,
37825
37953
  workerTimeoutMs: input.workerTimeoutMs,
37826
- abortSignal: signal
37954
+ abortSignal: signal,
37955
+ cacheEnabled,
37956
+ onCacheOutcome: input.onCacheOutcome
37827
37957
  }),
37828
37958
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37829
37959
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38749,23 +38879,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38749
38879
  }
38750
38880
  };
38751
38881
  };
38752
- const atomicWriteJson = (filePath, value) => {
38753
- try {
38754
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38755
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38756
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38757
- NFS.renameSync(temporaryPath, filePath);
38758
- } catch {
38759
- return;
38760
- }
38761
- };
38762
- const failOpenReadJson = (filePath, fallback) => {
38763
- try {
38764
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38765
- } catch {
38766
- return fallback;
38767
- }
38768
- };
38769
38882
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38770
38883
  const decodeFileDiagnostics = (raw) => {
38771
38884
  if (!Array.isArray(raw)) return null;
@@ -39893,21 +40006,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39893
40006
  }
39894
40007
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39895
40008
  };
39896
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39897
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39898
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39899
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39900
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39901
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39902
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39903
- };
39904
- const resolveUserCacheScope = () => {
39905
- try {
39906
- return String(process.getuid?.() ?? os.userInfo().username);
39907
- } catch {
39908
- return "shared";
39909
- }
39910
- };
39911
40009
  /**
39912
40010
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39913
40011
  * the runner: once for the primary scan, once for the
@@ -41034,7 +41132,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41034
41132
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
41035
41133
  rootDirectory: scanDirectory,
41036
41134
  parseConcurrency: deadCodeParseConcurrency,
41037
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41135
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41136
+ onCacheOutcome: (didHitCache) => {
41137
+ deadCodeCacheHit = didHitCache;
41138
+ }
41038
41139
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41039
41140
  yield* set(deadCodeFailure, {
41040
41141
  didFail: true,
@@ -41062,6 +41163,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41062
41163
  let lastReportedTotalFileCount = 0;
41063
41164
  let lintCacheHitFileCount = null;
41064
41165
  let lintCacheTotalFileCount = null;
41166
+ let deadCodeCacheHit = null;
41065
41167
  const baseLintStream = linterService.run({
41066
41168
  rootDirectory: scanDirectory,
41067
41169
  project,
@@ -41191,6 +41293,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41191
41293
  securityScanFailed,
41192
41294
  lintCacheHitFileCount,
41193
41295
  lintCacheTotalFileCount,
41296
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41194
41297
  suppressedRuleCounts: transform.summarizeSuppressions()
41195
41298
  };
41196
41299
  }).pipe(withSpan("runInspect", { attributes: {
@@ -43570,5 +43673,5 @@ const startLanguageServer = () => {
43570
43673
  };
43571
43674
  //#endregion
43572
43675
  export { startLanguageServer };
43573
- !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]="94a57ec3-d5a9-52ea-a743-e91d996065b8")}catch(e){}}();
43574
- //# debugId=94a57ec3-d5a9-52ea-a743-e91d996065b8
43676
+ !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]="440ae812-d58d-5654-b3ad-1087b4aa2060")}catch(e){}}();
43677
+ //# debugId=440ae812-d58d-5654-b3ad-1087b4aa2060
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.6.3-dev.1571119",
3
+ "version": "0.6.3-dev.20d81f6",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -66,7 +66,7 @@
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
68
  "deslop-js": "0.6.3",
69
- "oxlint-plugin-react-doctor": "0.6.3-dev.1571119"
69
+ "oxlint-plugin-react-doctor": "0.6.3-dev.20d81f6"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,8 +74,8 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/core": "0.6.3",
78
77
  "@react-doctor/api": "0.6.3",
78
+ "@react-doctor/core": "0.6.3",
79
79
  "@react-doctor/language-server": "0.6.3"
80
80
  },
81
81
  "engines": {