react-doctor 0.6.2 → 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/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";
@@ -35512,7 +35515,7 @@ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35512
35515
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35513
35516
  };
35514
35517
  const resolveLintBatchOrdering = () => {
35515
- return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35518
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
35516
35519
  };
35517
35520
  /**
35518
35521
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35609,17 +35612,19 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
35609
35612
  return "auto";
35610
35613
  } }) {};
35611
35614
  /**
35612
- * How the full-scan lint pass orders its file batches. `"arrival"` (the
35613
- * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35614
- * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35615
- * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35616
- * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35617
- * batch is a straggler (and can trip the per-batch timeout + split), measurably
35618
- * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35619
- * batches before `cost` earns the default. Tests override via
35620
- * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35621
- * — they pass user-scoped `includePaths` that skip discovery and stay in
35622
- * arrival order; only the full-scan branch reads it.
35615
+ * How the full-scan lint pass plans its file batches. `"cost"` (the default)
35616
+ * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
35617
+ * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
35618
+ * an even share of files AND bytes, so no 100-file chunk is a straggler while
35619
+ * the remainder-batch worker idles — and the heavy files are SPREAD across
35620
+ * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
35621
+ * lacked (it packed the heaviest files into one wave-1 straggler batch,
35622
+ * measurably regressing size-skewed repos, which is why it never earned the
35623
+ * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
35624
+ * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
35625
+ * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
35626
+ * never reach this — they pass user-scoped `includePaths` that skip discovery
35627
+ * and stay in arrival order; only the full-scan branch reads it.
35623
35628
  */
35624
35629
  var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35625
35630
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
@@ -35644,6 +35649,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35644
35649
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35645
35650
  return true;
35646
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
+ } }) {};
35647
35671
  const DIAGNOSTIC_SURFACES = [
35648
35672
  "cli",
35649
35673
  "prComment",
@@ -36928,6 +36952,7 @@ function* collectSecurityScanFiles(rootDirectory) {
36928
36952
  isGeneratedBundleByName: classification.isGeneratedBundleByName
36929
36953
  });
36930
36954
  }
36955
+ yield null;
36931
36956
  }
36932
36957
  for (const candidates of [
36933
36958
  priorityCandidates,
@@ -37053,9 +37078,18 @@ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
37053
37078
  const session = createSecurityScanSession(rootDirectory, options);
37054
37079
  if (session === null) return [];
37055
37080
  let sliceStartedAt = performance.now();
37056
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
37057
- await yieldToEventLoop();
37058
- sliceStartedAt = performance.now();
37081
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
37082
+ if (file === null) {
37083
+ if (performance.now() - sliceStartedAt >= 12) {
37084
+ await yieldToEventLoop();
37085
+ sliceStartedAt = performance.now();
37086
+ }
37087
+ continue;
37088
+ }
37089
+ for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
37090
+ await yieldToEventLoop();
37091
+ sliceStartedAt = performance.now();
37092
+ }
37059
37093
  }
37060
37094
  return session.diagnostics;
37061
37095
  };
@@ -37274,6 +37308,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37274
37308
  return [...seen].filter((pattern) => pattern.length > 0);
37275
37309
  };
37276
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
+ };
37277
37385
  const readSystemFacts = () => ({
37278
37386
  availableCores: os.availableParallelism(),
37279
37387
  totalMemoryBytes: os.totalmem(),
@@ -37342,6 +37450,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37342
37450
  releaseSlot();
37343
37451
  }
37344
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
+ };
37345
37468
  /**
37346
37469
  * Resolves a path to its canonical, symlink-free form, falling back to
37347
37470
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37637,13 +37760,28 @@ const checkDeadCode = async (options) => {
37637
37760
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37638
37761
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37639
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
+ }
37640
37778
  const spawnAndRun = () => {
37641
37779
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37642
37780
  rootDirectory,
37643
37781
  entryPatterns,
37644
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37782
+ tsConfigPath,
37645
37783
  ignorePatterns,
37646
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37784
+ deslopJsModuleSpecifier,
37647
37785
  parseConcurrency: options.parseConcurrency
37648
37786
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37649
37787
  };
@@ -37704,6 +37842,7 @@ const checkDeadCode = async (options) => {
37704
37842
  category: DEAD_CODE_CATEGORY
37705
37843
  });
37706
37844
  }
37845
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37707
37846
  return diagnostics;
37708
37847
  };
37709
37848
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37806,12 +37945,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
37806
37945
  */
37807
37946
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37808
37947
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37948
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37809
37949
  return yield* tryPromise({
37810
37950
  try: (signal) => checkDeadCode({
37811
37951
  rootDirectory: input.rootDirectory,
37812
37952
  parseConcurrency: input.parseConcurrency,
37813
37953
  workerTimeoutMs: input.workerTimeoutMs,
37814
- abortSignal: signal
37954
+ abortSignal: signal,
37955
+ cacheEnabled,
37956
+ onCacheOutcome: input.onCacheOutcome
37815
37957
  }),
37816
37958
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37817
37959
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38737,23 +38879,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38737
38879
  }
38738
38880
  };
38739
38881
  };
38740
- const atomicWriteJson = (filePath, value) => {
38741
- try {
38742
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38743
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38744
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38745
- NFS.renameSync(temporaryPath, filePath);
38746
- } catch {
38747
- return;
38748
- }
38749
- };
38750
- const failOpenReadJson = (filePath, fallback) => {
38751
- try {
38752
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38753
- } catch {
38754
- return fallback;
38755
- }
38756
- };
38757
38882
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38758
38883
  const decodeFileDiagnostics = (raw) => {
38759
38884
  if (!Array.isArray(raw)) return null;
@@ -39817,22 +39942,70 @@ const hashFileContents = (filePath) => {
39817
39942
  return null;
39818
39943
  }
39819
39944
  };
39820
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39821
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39822
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39823
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39824
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39825
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39826
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39827
- };
39828
- const resolveUserCacheScope = () => {
39829
- try {
39830
- return String(process.getuid?.() ?? os.userInfo().username);
39831
- } catch {
39832
- return "shared";
39945
+ /**
39946
+ * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39947
+ * `batchIncludePaths` greedily fills 100-file batches and leaves a small
39948
+ * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
39949
+ * is set by whichever 100-file chunk happened to collect the most work while
39950
+ * the remainder's worker idles), this planner keeps the SAME mandatory batch
39951
+ * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
39952
+ * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
39953
+ * each to the least-loaded batch by cumulative byte size. Every batch ends up
39954
+ * with an even share of files AND bytes, so no batch is a straggler; heavy
39955
+ * files are SPREAD across batches (the documented precondition for `cost`
39956
+ * ordering to beat `arrival`) instead of concentrated in one.
39957
+ *
39958
+ * Deliberately NOT worker-count-aware: splitting into extra batches to feed
39959
+ * idle workers was measured (619-file corpus, 10 cores) to REGRESS the lint
39960
+ * wall — each additional concurrent subprocess pays a contended cold start
39961
+ * (node boot + plugin import + native dlopen, ~180 ms uncontended and 2-3×
39962
+ * that in a full wave) that outweighs the smaller per-batch file share.
39963
+ *
39964
+ * Every batch also respects the spawn-args char budget; a file that fits no
39965
+ * existing batch opens a new one, mirroring `batchIncludePaths`. Within a
39966
+ * batch, files return in arrival order, and the assignment is fully
39967
+ * deterministic (ties broken by lowest batch index), so a plan is
39968
+ * reproducible for a fixed (files, sizes) input. Batch composition cannot
39969
+ * change diagnostics — rules that look beyond one file (`CROSS_FILE_RULE_IDS`)
39970
+ * read the OTHER files via the filesystem, never via batch-mates — so the
39971
+ * plan only moves wall-clock time.
39972
+ *
39973
+ * O(files × batches); the linear least-loaded scan is fine at real-world
39974
+ * scale (a 3,500-file monorepo is 36 batches ≈ 126k comparisons).
39975
+ */
39976
+ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39977
+ if (files.length === 0) return [];
39978
+ const batchCount = Math.ceil(files.length / 100);
39979
+ const maxFilesPerBatch = Math.ceil(files.length / batchCount);
39980
+ const baseArgsLengthChars = estimateArgsLength(baseArgs);
39981
+ const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
39982
+ const batches = Array.from({ length: batchCount }, () => ({
39983
+ fileIndexes: [],
39984
+ totalSizeBytes: 0,
39985
+ argsLengthChars: baseArgsLengthChars
39986
+ }));
39987
+ for (const fileIndex of descendingSizeIndexes) {
39988
+ const entryLengthChars = files[fileIndex].length + 1;
39989
+ let targetBatch = null;
39990
+ for (const batch of batches) {
39991
+ if (batch.fileIndexes.length >= maxFilesPerBatch) continue;
39992
+ if (batch.fileIndexes.length > 0 && batch.argsLengthChars + entryLengthChars > 24e3) continue;
39993
+ if (targetBatch === null || batch.totalSizeBytes < targetBatch.totalSizeBytes || batch.totalSizeBytes === targetBatch.totalSizeBytes && batch.fileIndexes.length < targetBatch.fileIndexes.length) targetBatch = batch;
39994
+ }
39995
+ if (targetBatch === null) {
39996
+ targetBatch = {
39997
+ fileIndexes: [],
39998
+ totalSizeBytes: 0,
39999
+ argsLengthChars: baseArgsLengthChars
40000
+ };
40001
+ batches.push(targetBatch);
40002
+ }
40003
+ targetBatch.fileIndexes.push(fileIndex);
40004
+ targetBatch.totalSizeBytes += sizeByFile.get(files[fileIndex]) ?? 0;
40005
+ targetBatch.argsLengthChars += entryLengthChars;
39833
40006
  }
40007
+ return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39834
40008
  };
39835
- const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39836
40009
  /**
39837
40010
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39838
40011
  * the runner: once for the primary scan, once for the
@@ -39891,7 +40064,7 @@ const reactHooksJsPluginDropNote = (error) => {
39891
40064
  * 6. always restore disable directives + clean up the temp dir
39892
40065
  */
39893
40066
  const runOxlint = async (options) => {
39894
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "arrival" } = options;
40067
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
39895
40068
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39896
40069
  const severityControls = buildRuleSeverityControls(userConfig);
39897
40070
  validateRuleRegistration();
@@ -39943,8 +40116,15 @@ const runOxlint = async (options) => {
39943
40116
  "json",
39944
40117
  ...sharedArgs
39945
40118
  ];
39946
- const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39947
- const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
40119
+ await yieldToEventLoop();
40120
+ const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
40121
+ const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
40122
+ const sizeByFile = sizedScanFiles !== null && lintBatchOrdering === "cost" ? new Map(sizedScanFiles.map((entry) => [entry.path, entry.sizeBytes])) : null;
40123
+ const buildFileBatches = (passBaseArgs, passFiles) => sizeByFile !== null ? planLintBatches({
40124
+ baseArgs: passBaseArgs,
40125
+ files: passFiles,
40126
+ sizeByFile
40127
+ }) : batchIncludePaths(passBaseArgs, passFiles);
39948
40128
  const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39949
40129
  if (files.length === 0) return {
39950
40130
  diagnostics: [],
@@ -39958,7 +40138,7 @@ const runOxlint = async (options) => {
39958
40138
  };
39959
40139
  const passConfigPath = Path.join(configDirectory, configFileName);
39960
40140
  const passBaseArgs = makeBaseArgs(passConfigPath);
39961
- const passFileBatches = batchIncludePaths(passBaseArgs, files);
40141
+ const passFileBatches = buildFileBatches(passBaseArgs, files);
39962
40142
  const spawnPass = () => spawnLintBatches({
39963
40143
  baseArgs: passBaseArgs,
39964
40144
  fileBatches: passFileBatches,
@@ -40007,6 +40187,7 @@ const runOxlint = async (options) => {
40007
40187
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40008
40188
  const missFiles = [];
40009
40189
  const replayedDiagnostics = [];
40190
+ let hashSliceStartedAt = performance.now();
40010
40191
  for (const candidateFile of candidateFiles) {
40011
40192
  const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40012
40193
  if (contentHash === null) {
@@ -40018,6 +40199,10 @@ const runOxlint = async (options) => {
40018
40199
  const cachedDiagnostics = cache.lookup(cacheKey);
40019
40200
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
40020
40201
  else replayedDiagnostics.push(...cachedDiagnostics);
40202
+ if (performance.now() - hashSliceStartedAt >= 12) {
40203
+ await yieldToEventLoop();
40204
+ hashSliceStartedAt = performance.now();
40205
+ }
40021
40206
  }
40022
40207
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40023
40208
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
@@ -40060,7 +40245,7 @@ const runOxlint = async (options) => {
40060
40245
  ]);
40061
40246
  }
40062
40247
  const baseArgs = makeBaseArgs(configPath);
40063
- const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
40248
+ const fileBatches = buildFileBatches(baseArgs, candidateFiles);
40064
40249
  const runBatches = () => spawnLintBatches({
40065
40250
  baseArgs,
40066
40251
  fileBatches,
@@ -40947,7 +41132,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40947
41132
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40948
41133
  rootDirectory: scanDirectory,
40949
41134
  parseConcurrency: deadCodeParseConcurrency,
40950
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41135
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41136
+ onCacheOutcome: (didHitCache) => {
41137
+ deadCodeCacheHit = didHitCache;
41138
+ }
40951
41139
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40952
41140
  yield* set(deadCodeFailure, {
40953
41141
  didFail: true,
@@ -40975,6 +41163,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40975
41163
  let lastReportedTotalFileCount = 0;
40976
41164
  let lintCacheHitFileCount = null;
40977
41165
  let lintCacheTotalFileCount = null;
41166
+ let deadCodeCacheHit = null;
40978
41167
  const baseLintStream = linterService.run({
40979
41168
  rootDirectory: scanDirectory,
40980
41169
  project,
@@ -41104,6 +41293,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41104
41293
  securityScanFailed,
41105
41294
  lintCacheHitFileCount,
41106
41295
  lintCacheTotalFileCount,
41296
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41107
41297
  suppressedRuleCounts: transform.summarizeSuppressions()
41108
41298
  };
41109
41299
  }).pipe(withSpan("runInspect", { attributes: {
@@ -41433,7 +41623,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
41433
41623
  /** Display name used in client-facing messages and progress titles. */
41434
41624
  const SERVER_DISPLAY_NAME = "React Doctor";
41435
41625
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
41436
- const SERVER_VERSION = "0.6.2";
41626
+ const SERVER_VERSION = "0.6.3";
41437
41627
  /** `Diagnostic.source` shown next to every published diagnostic. */
41438
41628
  const DIAGNOSTIC_SOURCE = "react-doctor";
41439
41629
  /**
@@ -43483,5 +43673,5 @@ const startLanguageServer = () => {
43483
43673
  };
43484
43674
  //#endregion
43485
43675
  export { startLanguageServer };
43486
- !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]="9ba45115-9e66-529f-a5b1-8a80c03af114")}catch(e){}}();
43487
- //# debugId=9ba45115-9e66-529f-a5b1-8a80c03af114
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.2",
3
+ "version": "0.6.3-dev.20d81f6",
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
- "deslop-js": "0.6.2",
69
- "oxlint-plugin-react-doctor": "0.6.2"
68
+ "deslop-js": "0.6.3",
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,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.2",
78
- "@react-doctor/core": "0.6.2",
79
- "@react-doctor/language-server": "0.6.2"
77
+ "@react-doctor/api": "0.6.3",
78
+ "@react-doctor/core": "0.6.3",
79
+ "@react-doctor/language-server": "0.6.3"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"