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/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]="be2212ec-649e-55f5-a360-fc2eea17659d")}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";
@@ -35479,7 +35482,7 @@ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35479
35482
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35480
35483
  };
35481
35484
  const resolveLintBatchOrdering = () => {
35482
- return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35485
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
35483
35486
  };
35484
35487
  /**
35485
35488
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35576,17 +35579,19 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
35576
35579
  return "auto";
35577
35580
  } }) {};
35578
35581
  /**
35579
- * How the full-scan lint pass orders its file batches. `"arrival"` (the
35580
- * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35581
- * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35582
- * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35583
- * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35584
- * batch is a straggler (and can trip the per-batch timeout + split), measurably
35585
- * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35586
- * batches before `cost` earns the default. Tests override via
35587
- * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35588
- * — they pass user-scoped `includePaths` that skip discovery and stay in
35589
- * arrival order; only the full-scan branch reads it.
35582
+ * How the full-scan lint pass plans its file batches. `"cost"` (the default)
35583
+ * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
35584
+ * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
35585
+ * an even share of files AND bytes, so no 100-file chunk is a straggler while
35586
+ * the remainder-batch worker idles — and the heavy files are SPREAD across
35587
+ * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
35588
+ * lacked (it packed the heaviest files into one wave-1 straggler batch,
35589
+ * measurably regressing size-skewed repos, which is why it never earned the
35590
+ * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
35591
+ * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
35592
+ * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
35593
+ * never reach this — they pass user-scoped `includePaths` that skip discovery
35594
+ * and stay in arrival order; only the full-scan branch reads it.
35590
35595
  */
35591
35596
  var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35592
35597
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
@@ -35611,6 +35616,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35611
35616
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35612
35617
  return true;
35613
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
+ } }) {};
35614
35638
  const DIAGNOSTIC_SURFACES = [
35615
35639
  "cli",
35616
35640
  "prComment",
@@ -36943,6 +36967,7 @@ function* collectSecurityScanFiles(rootDirectory) {
36943
36967
  isGeneratedBundleByName: classification.isGeneratedBundleByName
36944
36968
  });
36945
36969
  }
36970
+ yield null;
36946
36971
  }
36947
36972
  for (const candidates of [
36948
36973
  priorityCandidates,
@@ -37068,9 +37093,18 @@ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
37068
37093
  const session = createSecurityScanSession(rootDirectory, options);
37069
37094
  if (session === null) return [];
37070
37095
  let sliceStartedAt = performance.now();
37071
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
37072
- await yieldToEventLoop();
37073
- sliceStartedAt = performance.now();
37096
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
37097
+ if (file === null) {
37098
+ if (performance.now() - sliceStartedAt >= 12) {
37099
+ await yieldToEventLoop();
37100
+ sliceStartedAt = performance.now();
37101
+ }
37102
+ continue;
37103
+ }
37104
+ for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
37105
+ await yieldToEventLoop();
37106
+ sliceStartedAt = performance.now();
37107
+ }
37074
37108
  }
37075
37109
  return session.diagnostics;
37076
37110
  };
@@ -37289,6 +37323,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37289
37323
  return [...seen].filter((pattern) => pattern.length > 0);
37290
37324
  };
37291
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
+ };
37292
37400
  const readSystemFacts = () => ({
37293
37401
  availableCores: os.availableParallelism(),
37294
37402
  totalMemoryBytes: os.totalmem(),
@@ -37357,6 +37465,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37357
37465
  releaseSlot();
37358
37466
  }
37359
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
+ };
37360
37483
  /**
37361
37484
  * Resolves a path to its canonical, symlink-free form, falling back to
37362
37485
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37652,13 +37775,28 @@ const checkDeadCode = async (options) => {
37652
37775
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37653
37776
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37654
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
+ }
37655
37793
  const spawnAndRun = () => {
37656
37794
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37657
37795
  rootDirectory,
37658
37796
  entryPatterns,
37659
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37797
+ tsConfigPath,
37660
37798
  ignorePatterns,
37661
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37799
+ deslopJsModuleSpecifier,
37662
37800
  parseConcurrency: options.parseConcurrency
37663
37801
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37664
37802
  };
@@ -37719,6 +37857,7 @@ const checkDeadCode = async (options) => {
37719
37857
  category: DEAD_CODE_CATEGORY
37720
37858
  });
37721
37859
  }
37860
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37722
37861
  return diagnostics;
37723
37862
  };
37724
37863
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37821,12 +37960,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
37821
37960
  */
37822
37961
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37823
37962
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37963
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37824
37964
  return yield* tryPromise({
37825
37965
  try: (signal) => checkDeadCode({
37826
37966
  rootDirectory: input.rootDirectory,
37827
37967
  parseConcurrency: input.parseConcurrency,
37828
37968
  workerTimeoutMs: input.workerTimeoutMs,
37829
- abortSignal: signal
37969
+ abortSignal: signal,
37970
+ cacheEnabled,
37971
+ onCacheOutcome: input.onCacheOutcome
37830
37972
  }),
37831
37973
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37832
37974
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38752,23 +38894,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38752
38894
  }
38753
38895
  };
38754
38896
  };
38755
- const atomicWriteJson = (filePath, value) => {
38756
- try {
38757
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38758
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38759
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38760
- NFS.renameSync(temporaryPath, filePath);
38761
- } catch {
38762
- return;
38763
- }
38764
- };
38765
- const failOpenReadJson = (filePath, fallback) => {
38766
- try {
38767
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38768
- } catch {
38769
- return fallback;
38770
- }
38771
- };
38772
38897
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38773
38898
  const decodeFileDiagnostics = (raw) => {
38774
38899
  if (!Array.isArray(raw)) return null;
@@ -39832,22 +39957,70 @@ const hashFileContents = (filePath) => {
39832
39957
  return null;
39833
39958
  }
39834
39959
  };
39835
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39836
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39837
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39838
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39839
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39840
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39841
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39842
- };
39843
- const resolveUserCacheScope = () => {
39844
- try {
39845
- return String(process.getuid?.() ?? os.userInfo().username);
39846
- } catch {
39847
- return "shared";
39960
+ /**
39961
+ * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39962
+ * `batchIncludePaths` greedily fills 100-file batches and leaves a small
39963
+ * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
39964
+ * is set by whichever 100-file chunk happened to collect the most work while
39965
+ * the remainder's worker idles), this planner keeps the SAME mandatory batch
39966
+ * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
39967
+ * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
39968
+ * each to the least-loaded batch by cumulative byte size. Every batch ends up
39969
+ * with an even share of files AND bytes, so no batch is a straggler; heavy
39970
+ * files are SPREAD across batches (the documented precondition for `cost`
39971
+ * ordering to beat `arrival`) instead of concentrated in one.
39972
+ *
39973
+ * Deliberately NOT worker-count-aware: splitting into extra batches to feed
39974
+ * idle workers was measured (619-file corpus, 10 cores) to REGRESS the lint
39975
+ * wall — each additional concurrent subprocess pays a contended cold start
39976
+ * (node boot + plugin import + native dlopen, ~180 ms uncontended and 2-3×
39977
+ * that in a full wave) that outweighs the smaller per-batch file share.
39978
+ *
39979
+ * Every batch also respects the spawn-args char budget; a file that fits no
39980
+ * existing batch opens a new one, mirroring `batchIncludePaths`. Within a
39981
+ * batch, files return in arrival order, and the assignment is fully
39982
+ * deterministic (ties broken by lowest batch index), so a plan is
39983
+ * reproducible for a fixed (files, sizes) input. Batch composition cannot
39984
+ * change diagnostics — rules that look beyond one file (`CROSS_FILE_RULE_IDS`)
39985
+ * read the OTHER files via the filesystem, never via batch-mates — so the
39986
+ * plan only moves wall-clock time.
39987
+ *
39988
+ * O(files × batches); the linear least-loaded scan is fine at real-world
39989
+ * scale (a 3,500-file monorepo is 36 batches ≈ 126k comparisons).
39990
+ */
39991
+ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39992
+ if (files.length === 0) return [];
39993
+ const batchCount = Math.ceil(files.length / 100);
39994
+ const maxFilesPerBatch = Math.ceil(files.length / batchCount);
39995
+ const baseArgsLengthChars = estimateArgsLength(baseArgs);
39996
+ const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
39997
+ const batches = Array.from({ length: batchCount }, () => ({
39998
+ fileIndexes: [],
39999
+ totalSizeBytes: 0,
40000
+ argsLengthChars: baseArgsLengthChars
40001
+ }));
40002
+ for (const fileIndex of descendingSizeIndexes) {
40003
+ const entryLengthChars = files[fileIndex].length + 1;
40004
+ let targetBatch = null;
40005
+ for (const batch of batches) {
40006
+ if (batch.fileIndexes.length >= maxFilesPerBatch) continue;
40007
+ if (batch.fileIndexes.length > 0 && batch.argsLengthChars + entryLengthChars > 24e3) continue;
40008
+ if (targetBatch === null || batch.totalSizeBytes < targetBatch.totalSizeBytes || batch.totalSizeBytes === targetBatch.totalSizeBytes && batch.fileIndexes.length < targetBatch.fileIndexes.length) targetBatch = batch;
40009
+ }
40010
+ if (targetBatch === null) {
40011
+ targetBatch = {
40012
+ fileIndexes: [],
40013
+ totalSizeBytes: 0,
40014
+ argsLengthChars: baseArgsLengthChars
40015
+ };
40016
+ batches.push(targetBatch);
40017
+ }
40018
+ targetBatch.fileIndexes.push(fileIndex);
40019
+ targetBatch.totalSizeBytes += sizeByFile.get(files[fileIndex]) ?? 0;
40020
+ targetBatch.argsLengthChars += entryLengthChars;
39848
40021
  }
40022
+ return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39849
40023
  };
39850
- const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39851
40024
  /**
39852
40025
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39853
40026
  * the runner: once for the primary scan, once for the
@@ -39906,7 +40079,7 @@ const reactHooksJsPluginDropNote = (error) => {
39906
40079
  * 6. always restore disable directives + clean up the temp dir
39907
40080
  */
39908
40081
  const runOxlint = async (options) => {
39909
- 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;
40082
+ 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;
39910
40083
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39911
40084
  const severityControls = buildRuleSeverityControls(userConfig);
39912
40085
  validateRuleRegistration();
@@ -39958,8 +40131,15 @@ const runOxlint = async (options) => {
39958
40131
  "json",
39959
40132
  ...sharedArgs
39960
40133
  ];
39961
- const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39962
- const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
40134
+ await yieldToEventLoop();
40135
+ const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
40136
+ const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
40137
+ const sizeByFile = sizedScanFiles !== null && lintBatchOrdering === "cost" ? new Map(sizedScanFiles.map((entry) => [entry.path, entry.sizeBytes])) : null;
40138
+ const buildFileBatches = (passBaseArgs, passFiles) => sizeByFile !== null ? planLintBatches({
40139
+ baseArgs: passBaseArgs,
40140
+ files: passFiles,
40141
+ sizeByFile
40142
+ }) : batchIncludePaths(passBaseArgs, passFiles);
39963
40143
  const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39964
40144
  if (files.length === 0) return {
39965
40145
  diagnostics: [],
@@ -39973,7 +40153,7 @@ const runOxlint = async (options) => {
39973
40153
  };
39974
40154
  const passConfigPath = Path.join(configDirectory, configFileName);
39975
40155
  const passBaseArgs = makeBaseArgs(passConfigPath);
39976
- const passFileBatches = batchIncludePaths(passBaseArgs, files);
40156
+ const passFileBatches = buildFileBatches(passBaseArgs, files);
39977
40157
  const spawnPass = () => spawnLintBatches({
39978
40158
  baseArgs: passBaseArgs,
39979
40159
  fileBatches: passFileBatches,
@@ -40022,6 +40202,7 @@ const runOxlint = async (options) => {
40022
40202
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40023
40203
  const missFiles = [];
40024
40204
  const replayedDiagnostics = [];
40205
+ let hashSliceStartedAt = performance.now();
40025
40206
  for (const candidateFile of candidateFiles) {
40026
40207
  const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40027
40208
  if (contentHash === null) {
@@ -40033,6 +40214,10 @@ const runOxlint = async (options) => {
40033
40214
  const cachedDiagnostics = cache.lookup(cacheKey);
40034
40215
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
40035
40216
  else replayedDiagnostics.push(...cachedDiagnostics);
40217
+ if (performance.now() - hashSliceStartedAt >= 12) {
40218
+ await yieldToEventLoop();
40219
+ hashSliceStartedAt = performance.now();
40220
+ }
40036
40221
  }
40037
40222
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40038
40223
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
@@ -40075,7 +40260,7 @@ const runOxlint = async (options) => {
40075
40260
  ]);
40076
40261
  }
40077
40262
  const baseArgs = makeBaseArgs(configPath);
40078
- const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
40263
+ const fileBatches = buildFileBatches(baseArgs, candidateFiles);
40079
40264
  const runBatches = () => spawnLintBatches({
40080
40265
  baseArgs,
40081
40266
  fileBatches,
@@ -40962,7 +41147,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40962
41147
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40963
41148
  rootDirectory: scanDirectory,
40964
41149
  parseConcurrency: deadCodeParseConcurrency,
40965
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41150
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41151
+ onCacheOutcome: (didHitCache) => {
41152
+ deadCodeCacheHit = didHitCache;
41153
+ }
40966
41154
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40967
41155
  yield* set(deadCodeFailure, {
40968
41156
  didFail: true,
@@ -40990,6 +41178,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40990
41178
  let lastReportedTotalFileCount = 0;
40991
41179
  let lintCacheHitFileCount = null;
40992
41180
  let lintCacheTotalFileCount = null;
41181
+ let deadCodeCacheHit = null;
40993
41182
  const baseLintStream = linterService.run({
40994
41183
  rootDirectory: scanDirectory,
40995
41184
  project,
@@ -41119,6 +41308,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41119
41308
  securityScanFailed,
41120
41309
  lintCacheHitFileCount,
41121
41310
  lintCacheTotalFileCount,
41311
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41122
41312
  suppressedRuleCounts: transform.summarizeSuppressions()
41123
41313
  };
41124
41314
  }).pipe(withSpan("runInspect", { attributes: {
@@ -41759,4 +41949,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41759
41949
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41760
41950
 
41761
41951
  //# sourceMappingURL=index.js.map
41762
- //# debugId=be2212ec-649e-55f5-a360-fc2eea17659d
41952
+ //# debugId=e96374bd-b4c9-5452-8570-b81c11a2959a